/* * Sun Public License * * The contents of this file are subject to the Sun Public License Version * 1.0 (the "License"). You may not use this file except in compliance with * the License. A copy of the License is available at http://www.sun.com/ * * The Original Code is the SLAMD Distributed Load Generation Engine. * The Initial Developer of the Original Code is Neil A. Wilson. * Portions created by Neil A. Wilson are Copyright (C) 2004-2010. * Some preexisting portions Copyright (C) 2002-2006 Sun Microsystems, Inc. * All Rights Reserved. * * Contributor(s): Neil A. Wilson */ package com.slamd.tools.ldifstructure; import java.awt.BorderLayout; import java.awt.Font; import java.awt.GridLayout; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.text.DecimalFormat; import java.text.ParseException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.TreeMap; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTextArea; import javax.swing.JTree; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreePath; /** * This program defines a utility that can examine an LDIF file and gather * summary information about the structure of the associated directory. * * * @author Neil A. Wilson */ public class LDIFStructure implements TreeSelectionListener, ListSelectionListener { /** * The end-of-line character for this operating system. */ public static final String EOL = System.getProperty("line.separator"); // A list of the root nodes read from the LDIF. private ArrayList<LDIFNode> rootNodes; // Indicates whether we should ignore hierarchy when examining the data. private boolean ignoreHierarchy; // The decimal format used to format numeric values. private DecimalFormat decimalFormat; // The mapping to hold all nodes read from the LDIF. private HashMap<String,LDIFNode> nodeMap; // The maximum number of entries to process. private int maxEntries; // The main window used for the application. private JFrame appWindow; // The label used to display the total number of descendants. private JLabel descendantCountLabel; // The label used to display the number of direct descendants. private JLabel childCountLabel; // The label used to display the DN of the current entry. private JLabel dnLabel; // The list box used to display the entry types for the children immediately // below a given entry. private JList childTypeList; // The text area used to display information about a particular entry type. private JTextArea entryTypeArea; // The tree used to graphically represent the hierarchy of the LDIF. private JTree ditTree; // The currently selected LDIF node. private LDIFNode selectedNode; // The LDIF file from which the information has been read. private String ldifFile; /** * Parses the command-line arguments and takes the appropriate action. * * @param args The command-line arguments provided to this program. * * @throws Exception If a problem occurs while examining the data. */ public static void main(String[] args) throws Exception { boolean aggregateOnly = false; boolean ignoreHierarchy = false; int maxEntries = 0; String ldifFile = null; String outputFile = null; for (int i=0; i < args.length; i++) { if (args[i].equals("-l")) { ldifFile = args[++i]; } else if (args[i].equals("-o")) { outputFile = args[++i]; } else if (args[i].equals("-x")) { maxEntries = Integer.parseInt(args[++i]); } else if (args[i].equals("-i")) { ignoreHierarchy = true; } else if (args[i].equals("-a")) { aggregateOnly = true; } else if (args[i].equals("-H")) { displayUsage(); System.exit(0); } else { System.err.println("ERROR: Unrecognized argument \"" + args[i] + '"'); displayUsage(); System.exit(1); } } if (ldifFile == null) { System.err.println("ERROR: No LDIF file specified (use -l)"); displayUsage(); System.exit(1); } LDIFStructure ldifStructure = new LDIFStructure(ldifFile, ignoreHierarchy, maxEntries); if (outputFile == null) { ldifStructure.displayGUI(); } else { ldifStructure.writeOutputFile(outputFile, aggregateOnly); System.out.println("Wrote LDIF structure information to " + outputFile); } } /** * Processes the provided LDIF file and populates the appropriate internal * structures based on its contents. * * @param ldifFile The path to the LDIF file to process. * @param ignoreHierarchy Indicates whether to ignore hierarchy and just * look at unique objectclass combinations. * @param maxEntries The maximum number of entries to process, or -1 if * there should be no maximum. * * @throws IOException If a problem occurs while reading from the LDIF. * * @throws ParseException If a problem occurs while parsing an entry from * the LDIF. */ public LDIFStructure(String ldifFile, boolean ignoreHierarchy, int maxEntries) throws IOException, ParseException { // Initialize the appropriate instance variables. this.ldifFile = ldifFile; this.ignoreHierarchy = ignoreHierarchy; decimalFormat = new DecimalFormat("0.00"); rootNodes = new ArrayList<LDIFNode>(); nodeMap = new HashMap<String,LDIFNode>(); String baseDN = null; // Create the LDIF reader and use it to start reading entries. LDIFReader reader = new LDIFReader(ldifFile); LDIFEntry entry = reader.nextEntry(); int entriesRead = 0; while (entry != null) { if ((maxEntries > 0) && (entriesRead >= maxEntries)) { break; } else { entriesRead++; } // If we are to ignore hierarchy, then we need to set the DN to a // consistent value so that all entries will be treated the same. The // base DN will be the DN of the first entry we read from the file, and // then we'll prepend the entry's actual RDN to it. if (ignoreHierarchy) { if (baseDN == null) { baseDN = entry.getNormalizedDN(); LDIFNode node = new LDIFNode(baseDN, null); rootNodes.add(node); nodeMap.put(baseDN, node); entry = reader.nextEntry(); continue; } entry.setDN("rdn=x," + baseDN); } // Get the parent DN for the entry. If it doesn't have a parent, then it // must be a root node. String parentDN = entry.getParentDN(); if (parentDN == null) { String normalizedDN = entry.getNormalizedDN(); LDIFNode node = new LDIFNode(normalizedDN, null); rootNodes.add(node); nodeMap.put(normalizedDN, node); } else { // See if the parent node is already present. If so, then just // increment its child count. Otherwise, create a new node for it. LDIFNode parentNode = nodeMap.get(parentDN); if (parentNode == null) { String grandparentDN = entry.getGrandparentDN(); if (grandparentDN == null) { // This could be a multi-level suffix, so make it a root node. String normalizedDN = entry.getNormalizedDN(); LDIFNode node = new LDIFNode(normalizedDN, null); rootNodes.add(node); nodeMap.put(normalizedDN, node); } else { // See if we have a node for the grandparent DN. If we do, then // Add a new node for the parent DN and make it a child of the // grandparent. If not, then it must be an out-of-order LDIF. LDIFNode grandparentNode = nodeMap.get(grandparentDN); if (grandparentNode == null) { // This could be a multi-level suffix, so make it a root node. String normalizedDN = entry.getNormalizedDN(); LDIFNode node = new LDIFNode(normalizedDN, null); rootNodes.add(node); nodeMap.put(normalizedDN, node); } else { parentNode = new LDIFNode(parentDN, grandparentNode); parentNode.addChild(entry); grandparentNode.addChildNode(parentNode); nodeMap.put(parentDN, parentNode); } } } else { parentNode.addChild(entry); } } // Print out a status message if appropriate and then read the next entry. if ((entriesRead % 1000) == 0) { System.out.println("Processed " + entriesRead + " entries."); } entry = reader.nextEntry(); } // We should be done processing the LDIF. Close the file and print out the // resulting tree. reader.close(); System.out.println("End of LDIF reached. Processed " + entriesRead + " entries"); } /** * Writes an output file with summary information collected from the LDIF. * * @param outputFile The path to the output file to write. * @param aggregateOnly Indicates whether to only write aggregate * information for each node rather than separate * output for each unique objectclass combination. * * @throws IOException If a problem occurs while trying to write the output * file. */ public void writeOutputFile(String outputFile, boolean aggregateOnly) throws IOException { // Open the output file for writing. PrintWriter writer = new PrintWriter(new FileWriter(outputFile)); // Iterate through the root nodes and write information about them and their // subordinates to the the output file. for (int i=0; i < rootNodes.size(); i++) { LDIFNode n = rootNodes.get(i); writeNode(n, writer, aggregateOnly); } // Close the output file. writer.flush(); writer.close(); } /** * Writes information about the provided node to the given writer. * * @param node The LDIF node to be written. * @param writer The writer to which the information should be * written. * @param aggregateOnly Indicates whether to only write aggregate * information for each node rather than separate * output for each unique objectclass combination. */ private void writeNode(LDIFNode node, PrintWriter writer, boolean aggregateOnly) { writer.println("Entry DN: " + node.getNormalizedDN()); writer.println("Immediate Children: " + node.getNumChildren()); writer.println("Total Descendants: " + node.getNumDescendants()); LDIFEntryType[] entryTypes = node.getSortedChildTypes(); if (entryTypes.length > 1) { LDIFEntryType t = node.getAggregateChildEntryType(); writer.println(" Entry Type: Aggregate"); LinkedHashMap<String,Integer> objectClassCounts = t.getAggregateObjectClassCounts(); Iterator iterator = objectClassCounts.keySet().iterator(); while (iterator.hasNext()) { String s = (String) iterator.next(); int count = objectClassCounts.get(s); double percent = 100.0 * count / node.getNumChildren(); writer.println(" objectClass: " + s + " (" + decimalFormat.format(percent) + "% of matching entries)"); } iterator = t.getAttributes().keySet().iterator(); while (iterator.hasNext()) { String s = (String) iterator.next(); LDIFAttributeInfo i = t.getAttributes().get(s); double percentOfEntries = 100.0 * i.getNumEntries() / t.getNumEntries(); double valuesPerEntry = i.getAverageValuesPerEntry(); double charsPerValue = i.getAverageCharactersPerValue(); writer.println(" " + s + ": " + decimalFormat.format(percentOfEntries) + "% of entries, " + decimalFormat.format(valuesPerEntry) + " values per entry, " + decimalFormat.format(charsPerValue) + " characters per value"); if (i.getNumUniqueValues() > 0) { TreeMap valueCounts = i.getUniqueValues(); String separator=""; writer.print(" <"); Iterator iterator2 = valueCounts.keySet().iterator(); while (iterator2.hasNext()) { String value = (String) iterator2.next(); writer.print(separator + value + ':' + valueCounts.get(value)); separator = ","; } writer.println(">"); } } writer.println(); } if ((! aggregateOnly) || (entryTypes.length == 1)) { for (int i=0; i < entryTypes.length; i++) { String[] objectClasses = entryTypes[i].getObjectClasses(); String label = objectClasses[0]; for (int j=1; j < objectClasses.length; j++) { label = label + ' ' + objectClasses[j]; } double pct = 100.0 * entryTypes[i].getNumEntries() / node.getNumChildren(); writer.println(" Entry Type: " + label); writer.println(" Matching Entries: " + entryTypes[i].getNumEntries() + '(' + decimalFormat.format(pct) + " percent of entries " + "immediately below " + node.getNormalizedDN() + ')'); for (int j=0; j < objectClasses.length; j++) { writer.println(" objectClass: " + objectClasses[j]); } Iterator iterator = entryTypes[i].getAttributes().keySet().iterator(); while (iterator.hasNext()) { String s = (String) iterator.next(); LDIFAttributeInfo ai = entryTypes[i].getAttributes().get(s); double percentOfEntries = 100.0 * ai.getNumEntries() / entryTypes[i].getNumEntries(); double valuesPerEntry = ai.getAverageValuesPerEntry(); double charsPerValue = ai.getAverageCharactersPerValue(); writer.println(" " + s + ": " + decimalFormat.format(percentOfEntries) + "% of entries, " + decimalFormat.format(valuesPerEntry) + " values per entry, " + decimalFormat.format(charsPerValue) + " characters per value"); if (ai.getNumUniqueValues() > 0) { TreeMap valueCounts = ai.getUniqueValues(); String separator=""; writer.print(" <"); Iterator iterator2 = valueCounts.keySet().iterator(); while (iterator2.hasNext()) { String value = (String) iterator2.next(); writer.print(separator + value + ':' + valueCounts.get(value)); separator = ","; } writer.println(">"); } } writer.println(); } } writer.println(); writer.println("--------------------------------------------------"); writer.println(); for (int i=0; i < node.getChildNodes().size(); i++) { LDIFNode n = (LDIFNode) node.getChildNodes().get(i); writeNode(n, writer, aggregateOnly); } } /** * Generates and displays the GUI that may be used to browse the contents of * the LDIF. */ public void displayGUI() { // Now create the GUI to display to the end user. Start with the main // window. appWindow = new JFrame("LDIF Browser: " + ldifFile); appWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); appWindow.getContentPane().setLayout(new BorderLayout()); // Create a split pane that will be used to split the DIT tree from // the rest of the data. JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); // Create the tree and put it on the left side of the window. DefaultMutableTreeNode topNode = new DefaultMutableTreeNode("Root Nodes"); for (int i=0; i < rootNodes.size(); i++) { addChildNode(topNode, rootNodes.get(i)); } ditTree = new JTree(topNode); ditTree.setRootVisible(false); ditTree.setShowsRootHandles(true); ditTree.addTreeSelectionListener(this); JScrollPane scrollPane = new JScrollPane(); scrollPane.getViewport().setView(ditTree); splitPane.add(scrollPane, JSplitPane.LEFT); // Create the DN and count labels and add them to a panel using a grid // layout. dnLabel = new JLabel("Entry DN: {none selected}"); childCountLabel = new JLabel("Immediate Children: N/A"); descendantCountLabel = new JLabel("Total Descendants: N/A"); JPanel labelPanel = new JPanel(new GridLayout(3, 1)); labelPanel.add(dnLabel, 0); labelPanel.add(childCountLabel, 1); labelPanel.add(descendantCountLabel, 2); // Create the child node list and put it and the label panel on another // panel with a border layout. Then add that panel to the main window. childTypeList = new JList(); childTypeList.setVisibleRowCount(10); childTypeList.addListSelectionListener(this); scrollPane = new JScrollPane(); scrollPane.getViewport().setView(childTypeList); JPanel listAndLabelPanel = new JPanel(new BorderLayout()); listAndLabelPanel.add(labelPanel, BorderLayout.NORTH); listAndLabelPanel.add(scrollPane, BorderLayout.CENTER); // Create a new panel to hold the label, list, and entry type information // and then add those elements. JPanel rightPanel = new JPanel(new BorderLayout()); entryTypeArea = new JTextArea("No entry has been selected. Please " + "choose a node from the tree to the left", 30, 80); entryTypeArea.setEditable(false); entryTypeArea.setFont(new Font("Monospaced", Font.PLAIN, 12)); scrollPane = new JScrollPane(); scrollPane.getViewport().setView(entryTypeArea); rightPanel.add(listAndLabelPanel, BorderLayout.NORTH); rightPanel.add(scrollPane, BorderLayout.CENTER); splitPane.add(rightPanel, JSplitPane.RIGHT); appWindow.getContentPane().add(splitPane, BorderLayout.CENTER); // Size the window properly and make it visible. appWindow.pack(); appWindow.setVisible(true); } /** * Adds the provided LDIF node into the DIT tree as a child of the provided * parent node. This will also recursively add all descendants of the * provided child node. * * @param parentTreeNode The parent node below which the child should be * added. * @param childLDIFNode The child LDIF node to add below the parent. */ public void addChildNode(DefaultMutableTreeNode parentTreeNode, LDIFNode childLDIFNode) { DefaultMutableTreeNode childTreeNode = new DefaultMutableTreeNode(childLDIFNode.getNormalizedDN()); for (int i=0; i < childLDIFNode.getChildNodes().size(); i++) { LDIFNode n = (LDIFNode) childLDIFNode.getChildNodes().get(i); addChildNode(childTreeNode, n); } parentTreeNode.add(childTreeNode); } /** * Indicates that the selected node of the tree has changed and that the * other components should be updated to reflect that change. * * @param selectionEvent The selection even with information on the change. */ public void valueChanged(TreeSelectionEvent selectionEvent) { // Get the currently selected node from the DIT tree. Then convert it to // an LDIF node. TreePath selectionPath = ditTree.getSelectionPath(); if (selectionPath == null) { return; } DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) selectionPath.getLastPathComponent(); String entryDN = (String) treeNode.getUserObject(); if (entryDN == null) { return; } selectedNode = nodeMap.get(entryDN); if (selectedNode == null) { return; } // At this point, we have the node, so use it to update the display. dnLabel.setText("Entry DN: " + entryDN); childCountLabel.setText("Immediate Children: " + selectedNode.getNumChildren()); descendantCountLabel.setText("Total Descendants: " + selectedNode.getNumDescendants()); LDIFEntryType[] childTypes = selectedNode.getSortedChildTypes(); if (childTypes.length > 1) { String[] typeStrings = new String[childTypes.length+1]; typeStrings[0] = selectedNode.getNumChildren() + ": Aggregate"; for (int i=0; i < childTypes.length; i++) { StringBuilder displayStr = new StringBuilder(); displayStr.append(childTypes[i].getNumEntries()); displayStr.append(": "); for (int j=0; j < childTypes[i].getObjectClasses().length; j++) { displayStr.append(' '); displayStr.append(childTypes[i].getObjectClasses()[j]); } typeStrings[i+1] = displayStr.toString(); } childTypeList.setListData(typeStrings); } else { String[] typeStrings = new String[childTypes.length]; for (int i=0; i < childTypes.length; i++) { StringBuilder displayStr = new StringBuilder(); displayStr.append(childTypes[i].getNumEntries()); displayStr.append(": "); for (int j=0; j < childTypes[i].getObjectClasses().length; j++) { displayStr.append(' '); displayStr.append(childTypes[i].getObjectClasses()[j]); } typeStrings[i] = displayStr.toString(); } childTypeList.setListData(typeStrings); } appWindow.repaint(); } /** * Indicates that the selected item in the entry type list has changed and * that the other components should be updated to reflect that change. * * @param selectionEvent The selection even with information on the change. */ public void valueChanged(ListSelectionEvent selectionEvent) { entryTypeArea.setText(""); if (selectedNode == null) { return; } String valueStr = (String) childTypeList.getSelectedValue(); if (valueStr == null) { return; } int pos = valueStr.indexOf(": "); if (pos < 0) { return; } String key = valueStr.substring(pos+3); boolean isAggregate; LDIFEntryType entryType; if (key.equals("Aggregate")) { isAggregate = true; entryType = selectedNode.getAggregateChildEntryType(); } else { isAggregate = false; entryType = (LDIFEntryType) selectedNode.getChildEntryTypes().get(key); } if (entryType == null) { return; } StringBuilder b = new StringBuilder(); int matchingEntries = entryType.getNumEntries(); double percentOfEntries = 100.0 * matchingEntries / selectedNode.getNumChildren(); b.append("Matching Entries: "); b.append(matchingEntries); b.append(" ("); b.append(decimalFormat.format(percentOfEntries)); b.append("% of child entries below "); b.append(selectedNode.getNormalizedDN()); b.append(')'); b.append(EOL); if (isAggregate) { LinkedHashMap<String,Integer> objectClassCounts = entryType.getAggregateObjectClassCounts(); Iterator iterator = objectClassCounts.keySet().iterator(); while (iterator.hasNext()) { String s = (String) iterator.next(); int count = objectClassCounts.get(s); percentOfEntries = 100.0 * count / matchingEntries; b.append("objectClass: "); b.append(s); b.append(" ("); b.append(decimalFormat.format(percentOfEntries)); b.append("% of matching entries)"); b.append(EOL); } } else { for (int i=0; i < entryType.getObjectClasses().length; i++) { b.append("objectClass: "); b.append(entryType.getObjectClasses()[i]); b.append(EOL); } } Iterator iterator = entryType.getAttributes().values().iterator(); while (iterator.hasNext()) { LDIFAttributeInfo ai = (LDIFAttributeInfo) iterator.next(); percentOfEntries = 100.0 * ai.getNumEntries() / matchingEntries; b.append(ai.getAttributeName()); b.append(": "); b.append(decimalFormat.format(percentOfEntries)); b.append("% of entries, "); b.append(decimalFormat.format(ai.getAverageValuesPerEntry())); b.append(" values per entry, "); b.append(decimalFormat.format(ai.getAverageCharactersPerValue())); b.append(" characters per value"); b.append(EOL); if (ai.getNumUniqueValues() > 0) { String separator = ""; b.append(" <"); Iterator iterator2 = ai.getUniqueValues().keySet().iterator(); while (iterator2.hasNext()) { String s = (String) iterator2.next(); b.append(separator); b.append(s); b.append(':'); b.append(ai.getUniqueValues().get(s)); separator = ","; } b.append('>'); b.append(EOL); } } entryTypeArea.setText(b.toString()); entryTypeArea.setSelectionStart(0); entryTypeArea.setSelectionEnd(0); appWindow.repaint(); } /** * Writes usage information for this program to standard error. */ public static void displayUsage() { System.out.println( "USAGE: java LDIFStructure {options}" + EOL + " where {options} include:" + EOL + "-l {ldifFile} -- The LDIF file to process" + EOL + "-o {outFile} -- The output file to create (instead of showing a GUI)" + EOL + "-x {maxCount} -- Process at most this number of entries" + EOL + "-i -- Ignore hierarchy and only focus on objectclass sets" + EOL + "-a -- Only show aggregate data (ingored in GUI mode)" + EOL + "-H -- Display this usage information" ); } }