/* * 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.makeldif; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashMap; import java.util.Random; import java.util.StringTokenizer; import com.unboundid.util.Base64; import com.slamd.common.Constants; /** * This program allows for the creation of LDIF files in an automated but * customizable manner. You can use template files to specify the kinds of * entries that will be created in the final LDIF, and a few kinds of operations * can be done to help generate more realistic data. * * * @author Neil A. Wilson */ public class MakeLDIF { /** * The version number assigned to the current MakeLDIF source base. */ public static final String VERSION_STRING = "1.3.2"; /** * The end of line character that should be used on the current platform. */ public static String EOL = System.getProperty("line.separator"); /** * The RDN attributes that will be used by default if none are specified. */ public static final String[] DEFAULT_RDN_ATTRS = { "cn" }; /** * The set of characters that should be included in numeric values. */ public static final char[] NUMERIC_CHARS = "0123456789".toCharArray(); /** * The set of characters that should be included in alphabetic values. */ public static final char[] ALPHA_CHARS = "abcdefghijklmnopqrstuvwxyz".toCharArray(); /** * The set of characters that should be included in alphanumeric values. */ public static final char[] ALPHANUMERIC_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789".toCharArray(); /** * The set of characters that should be included in hexadecimal values. */ public static final char[] HEX_CHARS = "0123456789abcdef".toCharArray(); /** * The set of characters that should be included in base64 values. */ public static final char[] BASE64_CHARS = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789+/").toCharArray(); /** * The set of months that will be used if the name of a month is required. */ public static final String[] MONTH_NAMES = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; // A list of the branches defined in the template file. These need to be // processed in order. ArrayList<Branch> branches; // Indicates whether the end of the CSV file has been reached. boolean csvEndReached = false; // Indicates whether MakeLDIF is running in debug mode. boolean debugMode = false; // Indicates whether filter lists are to be generated. boolean generateFilterList = false; // Indicates whether to ignore the first line of the CSV file. boolean ignoreCSVHeaderLine = false; // Indicates whether all the filter information should be stored in a single // filter file or separated into files based on the filter types. boolean separateFilterFiles = false; // Indicates whether branch entries should not be written to the resulting // LDIF file. boolean skipBranchEntries = false; // Indicates whether long lines in the LDIF output are to be wrapped. By // default they are not, but can be if you include the "-w" parameter. boolean wrapLongLines = false; // The reader used to read information from the CSV file. BufferedReader csvReader; // The writer used to send the DNs of the entries created into a file BufferedWriter dnWriter; // The writer used to send bind information into a file. BufferedWriter bindInfoWriter; // The writer used to send information into the LDIF file. BufferedWriter ldifWriter; // The writer used to send information to the login information file. BufferedWriter loginWriter; // A character array that contains 5000 characters. This will be re-used // multiple times for improved performance rather than repeatedly // re-allocating memory each time an array is needed. char[] chars5000 = new char[5000]; // A map containing lists of values read from file. HashMap<String,ValueList> fileLists; // A map containing the filter lists. HashMap<String,UniqueSortedList> filterListHash; // A map containing the templates defined in the template file. HashMap<String,Template> templateHash; // A map containing value lists HashMap<String,ValueList> valueLists; // The total number of entries written to the LDIF file so far int entriesWritten; // The counter used to keep track of the LDIF filename counter if a limited // number of entries should be written to a single LDIF file. int fileNameCounter = 1; // The value that indicates the current position in the list of first names int firstNameIndex; // The value that indicates the current position in the list of last names int lastNameIndex; // The maximum number of entries that will be allowed to match a search filter // in order for it to be included in the filter file. int maxFilterMatches = -1; // The maximum number of entries that should be written to a single LDIF file. int maxPerFile = -1; // The maximum number of entries that should be generated for each template // under each branch. int maxPerTemplate = -1; // The minimum number of entries that will be needed to match a search filter // in order for it to be included in the filter file. int minFilterMatches = 1; // The number of times that the larger of the first/last name lists has been // completed int nameLoopCounter; // The counter that is added to the last name after all unique combinations of // first and last names have been exhausted so that we can re-use all of those // values and still maintain uniqueness. int nameUniquenessCounter; // The number of elements in the first name list. int numFirstNames; // The number of elements in the last name list. int numLastNames; // The number of characters to include in substring filters. int numSubstringChars = 3; // The seed that is to be used to initialize the random number generator. long randomSeed = -1; // The random number generator being used. Random random; // The name of the file to use to write the DN and password for generated // entries (provided that they have a password). Specified with the "-b" // parameter. String bindInfoFile = null; // The delimiter to use for the CSV data. String csvDelimiter = null; // The path to the CSV file containing data to use when generating the // entries. String csvFile = null; // The name of the file to use to write the DNs of the entries that have been // created. String dnFile = null; // The base that we will use for GUIDs instead of the MAC address of the // network interface (since that may not exist, and Java can't get to it // without native calls anyway). String guidBase; // The name of the file to which search filter information will be written. // Specified with the "-F" parameter. String filterFile = null; // The name of the file containing the list of first names to use. Specified // with the "-f" parameter. String firstNameFile = "first.names"; // The name of the file containing the list of last names to use. Specified // with the "-l" parameter. String lastNameFile = "last.names"; // The path and name of the LDIF file to create. Specified with the "-o" // parameter. String ldifFile = null; // The path and name of the file to create with login ID/password information. // Specified with the "-L" parameter. String loginFile = null; // The name of the attribute that will be used as the login ID for the user. // Specified with the "-i" parameter. String loginIDAttr = "uid"; // The path to the directory containing the MakeLDIF resource files. // Specified with the "-r" parameter. String resourceDir = null; // The path and name of the template file to use. Specified with the "-t" // parameter. String templateFile = null; // The list of first names to use when generating the LDIF String[] firstNames; // The list of last names to use when generating the LDIF String[] lastNames; /** * Creates a new instance of the LDIF generator and passes the command-line * arguments along to it. * * @param args The command-line arguments provided to the program. */ public static void main(String[] args) { new MakeLDIF(args); } /** * Creates a new instance of the LDIF generator that parses the command-line * parameters and coordinates the LDIF creation. * * @param args The command-line arguments provided to the program. */ public MakeLDIF(String[] args) { // Temporary storage for the filter types information. ArrayList<String[]> filterTypesList = new ArrayList<String[]>(); // Initialize the filter list hash. filterListHash = new HashMap<String,UniqueSortedList>(); // Iterate through the command-line parameters and set the values of the // corresponding instance variables for (int i=0; i < args.length; i++) { if (args[i].equals("-r")) { resourceDir = args[++i]; } else if (args[i].equals("-f")) { firstNameFile = args[++i]; } else if (args[i].equals("-l")) { lastNameFile = args[++i]; } else if (args[i].equals("-o")) { ldifFile = args[++i]; } else if (args[i].equals("-t")) { templateFile = args[++i]; } else if (args[i].equals("-c")) { csvFile = args[++i]; } else if (args[i].equals("-C")) { csvDelimiter = args[++i]; int tabPos = csvDelimiter.indexOf("\\t"); while (tabPos >= 0) { csvDelimiter = csvDelimiter.substring(0, tabPos) + '\t' + csvDelimiter.substring(tabPos+2); tabPos = csvDelimiter.indexOf("\\t"); } } else if (args[i].equals("-I")) { ignoreCSVHeaderLine = true; } else if (args[i].equals("-d")) { dnFile = args[++i]; } else if (args[i].equals("-b")) { bindInfoFile = args[++i]; } else if (args[i].equals("-i")) { loginIDAttr = args[++i].toLowerCase(); } else if (args[i].equals("-L")) { loginFile = args[++i]; } else if (args[i].equals("-F")) { filterFile = args[++i]; } else if (args[i].equals("-M")) { separateFilterFiles = true; } else if (args[i].equals("-T")) { String typesStr = args[++i]; int colonPos = typesStr.indexOf(':'); if ((colonPos <= 0) || (colonPos == (typesStr.length() - 1))) { System.err.println("ERROR: filter types must be in the format " + "attr:type[,type2]"); displayUsage(); System.exit(1); } String attrName = typesStr.substring(0, colonPos).toLowerCase(); ArrayList<String> typesList = new ArrayList<String>(); StringTokenizer st = new StringTokenizer(typesStr.substring(colonPos+1), ","); boolean maintainEq = false; boolean maintainSubstring = false; boolean maintainSubInitial = false; boolean maintainSubAny = false; boolean maintainSubFinal = false; while (st.hasMoreTokens()) { String type = st.nextToken().toLowerCase(); if (type.equals("eq")) { maintainEq = true; } else if (type.equals("sub")) { maintainSubstring = true; maintainSubInitial = true; maintainSubAny = true; maintainSubFinal = true; } else if (type.equals("subinitial")) { maintainSubInitial = true; } else if (type.equals("subany")) { maintainSubAny = true; } else if (type.equals("subfinal")) { maintainSubFinal = true; } else { System.err.println("ERROR: " + type + " is not a valid filter " + "type. Allowed types are eq, sub, subInitial," + "subAny, and subFinal"); displayUsage(); System.exit(1); } } filterListHash.put(attrName, new UniqueSortedList(maintainEq, maintainSubstring, maintainSubInitial, maintainSubAny, maintainSubFinal)); String[] attrFilterElements = new String[typesList.size()+1]; attrFilterElements[0] = attrName; for (int j=0; j < typesList.size(); j++) { attrFilterElements[j+1] = typesList.get(j); } filterTypesList.add(attrFilterElements); } else if (args[i].equals("-s")) { randomSeed = Long.parseLong(args[++i]); } else if (args[i].equals("-m")) { maxPerFile = Integer.parseInt(args[++i]); } else if (args[i].equals("-x")) { maxPerTemplate = Integer.parseInt(args[++i]); } else if (args[i].equals("-n")) { numSubstringChars = Integer.parseInt(args[++i]); } else if (args[i].equals("-N")) { minFilterMatches = Integer.parseInt(args[++i]); } else if (args[i].equals("-X")) { maxFilterMatches = Integer.parseInt(args[++i]); } else if (args[i].equals("-w")) { wrapLongLines = true; } else if (args[i].equals("-S")) { skipBranchEntries = true; } else if (args[i].equals("-U")) { EOL = "\n"; } else if (args[i].equals("-D")) { debugMode = true; System.err.println("Debug mode enabled."); } else if (args[i].equals("-V")) { displayVersion(); System.exit(0); } else if (args[i].equals("-H")) { displayUsage(); System.exit(0); } else { System.err.println("Unknown argument: " + args[i]); displayUsage(); System.exit(1); } } // Make sure that values were specified for the required attributes if (ldifFile == null) { System.err.println("Error: No output file specified (use -o)"); displayUsage(); System.exit(1); } if (templateFile == null) { System.err.println("Error: No template file specified (use -t)"); displayUsage(); System.exit(1); } // If there were any filter types specified, then make sure there was a // filter file specified. If so, then finalize the filter type information. if (! filterListHash.isEmpty()) { generateFilterList = true; if (filterFile == null) { System.err.println("Error: A filter file must be specified when " + "a list of filter types is provided."); displayUsage(); System.exit(1); } else { for (UniqueSortedList usl : filterListHash.values()) { usl.setMatchCriteria(numSubstringChars, minFilterMatches, maxFilterMatches); } } } else if (filterFile != null) { System.err.println("A filter file was specified, but no filter types " + "were provided. A filter file will not be created."); } // Initialize the random number generator. if (randomSeed >= 0) { random = new Random(randomSeed); } else { random = new Random(); } // Set the values of the remaining instance variables fileLists = new HashMap<String,ValueList>(); templateHash = new HashMap<String,Template>(); valueLists = new HashMap<String,ValueList>(); branches = new ArrayList<Branch>(); firstNames = new String[0]; lastNames = new String[0]; firstNameIndex = 0; lastNameIndex = 0; nameUniquenessCounter = 1; nameLoopCounter = 0; StringBuilder buffer = new StringBuilder(12); generateRandomValue(HEX_CHARS, 12, buffer); guidBase = buffer.toString(); // Load the first and last name lists into memory loadNames(); // Load the template file information into memory loadTemplate(); // Now do the actual work generateLDIF(); } /** * Reads the first and last name files and stores the values read into the * appropriate lists. The memory requirements for this should be minimal, * because we are storing the names separately and then using an algorithm to * extract every possible unique combination. */ public void loadNames() { BufferedReader reader = null; // Open the file containing the first names File f = getFileForName(firstNameFile); try { reader = new BufferedReader(new FileReader(f)); } catch (IOException ioe) { if (debugMode) { ioe.printStackTrace(); } System.err.println("Could not open first name file " + f.getAbsolutePath() + " -- " + ioe); System.exit(1); } // Read the first names into the first name list ArrayList<String> firstNameList = new ArrayList<String>(); try { while (reader.ready()) { String line = reader.readLine(); if (line.length() > 0) { firstNameList.add(line); } } reader.close(); } catch (IOException ioe) { if (debugMode) { ioe.printStackTrace(); } System.err.println("Error reading first name file -- " + ioe); System.exit(1); } firstNames = new String[firstNameList.size()]; firstNameList.toArray(firstNames); numFirstNames = firstNames.length; // Open the file containing the last names f = getFileForName(lastNameFile); try { reader = new BufferedReader(new FileReader(f)); } catch (IOException ioe) { if (debugMode) { ioe.printStackTrace(); } System.err.println("Could not open last name file " + f.getAbsolutePath() + " -- " + ioe); System.exit(1); } // Read the last names into the last name list ArrayList<String> lastNameList = new ArrayList<String>(); try { while (reader.ready()) { String line = reader.readLine(); if (line.length() > 0) { lastNameList.add(line); } } reader.close(); } catch (IOException ioe) { if (debugMode) { ioe.printStackTrace(); } System.err.println("Error reading last name file -- " + ioe); System.exit(1); } lastNames = new String[lastNameList.size()]; lastNameList.toArray(lastNames); numLastNames = lastNames.length; } /** * Reads the template file and extracts the branch and template definitions * from it. */ public void loadTemplate() { // The set of variables that have been defined in the template file. HashMap<String,String> variableHash = new HashMap<String,String>(); // The lines contained in the current definition ArrayList<String> currentDefinition = new ArrayList<String>(); // The type of definition we are working on (0=unknown, 1=branch, // 2=template) int definitionType = 0; // The name of the branch or template we are working on String name = null; // Open the template file File f = getFileForName(templateFile); BufferedReader reader = null; try { reader = new BufferedReader(new FileReader(f)); } catch (IOException ioe) { if (debugMode) { ioe.printStackTrace(); } System.err.println("Could not open template file " + f.getAbsolutePath() + " -- " + ioe); System.exit(1); } // Read through the template file an entry at a time (an entry is a set of // consecutive non-blank lines) and process it. This code is pretty // inefficient, but it works and is only called at the very beginning of the // program and not when actually creating the LDIF. boolean lastLine = false; try { while (! lastLine) { String line = ""; if (reader.ready()) { line = reader.readLine(); } else { lastLine = true; } // If the line starts with "define", then it is a variable definition. // Parse it and store it in the variable hash. if (line.toLowerCase().startsWith("define ")) { int equalPos = line.indexOf('='); if (equalPos < 0) { System.err.println("ERROR: Variable definitions must be in the " + "form:"); System.err.println(" define name=value"); System.err.println("Line '" + line + "' ignored."); } String variableName = line.substring(7, equalPos).trim(); String variableValue = line.substring(equalPos+1).trim(); variableHash.put(variableName, variableValue); continue; } // If the line contains a value between opening and closing brackets // ("[" and "]"), then it will be considered a variable definition and // its value will be replaced with the value of the specified variable // as we are reading in the template. This allows variable definitions // to be used anywhere in branch or template entries, even in the name // of the branch or template. boolean needReprocess = true; int openPos = 0; int startPos = 0; while (needReprocess && ((openPos = line.indexOf('[', startPos)) >= 0)) { if ((openPos > 0) && (line.charAt(openPos-1) == '\\')) { needReprocess = true; line = line.substring(0, openPos-1) + '[' + line.substring(openPos+1); startPos = openPos; continue; } needReprocess = false; int closePos = line.indexOf(']'); if (closePos > 0) { String variableName = line.substring(openPos+1, closePos); String variableValue = variableHash.get(variableName); line = line.substring(0, openPos) + variableValue + line.substring(closePos+1); needReprocess = true; } } // If we have a non-blank line, then just store it in the current // definition list. Also, try to determine whether it is a branch // or a template definition to ease processing later. if (line.length() > 0) { currentDefinition.add(line); if (line.toLowerCase().startsWith("branch: ")) { definitionType = 1; name = line.substring(8); } else if (line.toLowerCase().startsWith("template: ")) { definitionType = 2; name = line.substring(10); } } else { // Only try to process it if there is something to process if (! currentDefinition.isEmpty()) { if (definitionType == 1) { // It's a branch, so try to get the relevant information from it. Branch branch = new Branch(name); int numSubordinates = 0; String templateName = null; for (int i=0; i < currentDefinition.size(); i++) { line = currentDefinition.get(i); if (line.toLowerCase().startsWith("branch: ")) { // Ignore this } else if (line.toLowerCase().startsWith("subordinatetemplate: ")) { String templateLine = line.substring(21); int colonPos = templateLine.indexOf(':'); if (colonPos > 0) { templateName = templateLine.substring(0, colonPos); numSubordinates = Integer.parseInt( templateLine.substring(colonPos+1)); branch.addTemplate(templateName, numSubordinates); } } else { branch.addExtraLine(line); } } branches.add(branch); } else if (definitionType == 2) { // It's a template, so determine which RDN attribute to use and // parse out the template and subtemplate names. Everything else // will go into the attribute list. Template template = new Template(name, DEFAULT_RDN_ATTRS); for (int i=0; i < currentDefinition.size(); i++) { line = currentDefinition.get(i); if (line.toLowerCase().startsWith("template: ")) { // ignore this because we already have the template name and // don't want to make it an attribute } else if (line.toLowerCase().startsWith( "subordinatetemplate: ")) { // It's a subtemplate definition so get the name and count // from it String stLine = line.substring(21); int colonPos = stLine.indexOf(':'); if (colonPos > 0) { String stName = stLine.substring(0, colonPos); int stCnt = Integer.parseInt(stLine.substring(colonPos+1)); template.addSubtemplate(stName, stCnt); } else { template.addSubtemplate(stLine, 1); } } else if (line.toLowerCase().startsWith("rdnattr: ")) { String rdnAttrStr = line.substring(9); if (rdnAttrStr.indexOf('+') < 0) { template.setRDNAttributes(new String[] { rdnAttrStr }); } else { ArrayList<String> rdnAttrs = new ArrayList<String>(); StringTokenizer st = new StringTokenizer(rdnAttrStr, "+"); while (st.hasMoreTokens()) { rdnAttrs.add(st.nextToken()); } String[] rdnAttrArray = new String[rdnAttrs.size()]; rdnAttrs.toArray(rdnAttrArray); template.setRDNAttributes(rdnAttrArray); } } else if (line.toLowerCase().startsWith("extends: ")) { template.parentTemplateName = line.substring(9); } else if (line.indexOf(": ") > 0) { if ((line.indexOf(":: ") > 0) && (line.indexOf(":: ") < line.indexOf(": "))) { String attrName = line.substring(0, line.indexOf(":: ")); String value = line.substring(line.indexOf(":: ") + 3); template.addAttribute(attrName, ":: ", value); } else { String attrName = line.substring(0, line.indexOf(": ")); String value = line.substring(line.indexOf(": ") + 2); template.addAttribute(attrName, ": ", value); } } } // Finalize this template to configure it for more efficient // processing later. template.completeInitialization(); templateHash.put(name.toLowerCase(), template); } currentDefinition.clear(); definitionType = 0; name = null; } } } reader.close(); } catch (IOException ioe) { if (debugMode) { ioe.printStackTrace(); } System.err.println("Error reading template file " + templateFile); ioe.printStackTrace(); System.exit(1); } } /** * Actually generates the LDIF file by creating the specified branch entries, * then the subordinate entries, and writing them to disk. */ public void generateLDIF() { try { // Open the CSV file for reading. if (csvFile != null) { File f = getFileForName(csvFile); csvReader = new BufferedReader(new FileReader(f)); if (ignoreCSVHeaderLine) { csvReader.readLine(); } } // Open the output file for writing if (dnFile != null) { dnWriter = new BufferedWriter(new FileWriter(dnFile)); } if (bindInfoFile != null) { bindInfoWriter = new BufferedWriter(new FileWriter(bindInfoFile)); } if (loginFile != null) { loginWriter = new BufferedWriter(new FileWriter(loginFile)); } ldifWriter = new BufferedWriter(new FileWriter(ldifFile)); // Iterate through all the branches and process them in the order they // were defined in the template file for (int i=0; i < branches.size(); i++) { // Create the branch entry itself. This will just be a standard entry // with no real processing. Branch branch = branches.get(i); if (! skipBranchEntries) { createBranchEntry(branch); } // Create the appropriate number of subordinate entries for that branch if (branch.hasSubordinates()) { String[] templates = branch.getSubordinateTemplates(); for (int j=0; j < templates.length; j++) { Template template = getTemplate(templates[j]); if (template == null) { System.err.println("Unable to find template " + templates[j] + " for branch " + branch.getDN() + " -- aborting"); ldifWriter.flush(); ldifWriter.close(); if (dnFile != null) { dnWriter.flush(); dnWriter.close(); } if (bindInfoFile != null) { bindInfoWriter.flush(); bindInfoWriter.close(); } System.exit(1); } String parentDN = branch.getDN(); int numSubordinates = branch.numEntriesForTemplate(templates[j]); if (maxPerTemplate >= 0) { numSubordinates = Math.min(numSubordinates, maxPerTemplate); } template.resetCounters(); template.reinitializeCustomTags(); for (int k=0; ((! csvEndReached) && (k < numSubordinates)); k++) { try { createTemplateEntry(parentDN, null, template); } catch (Exception e) { if (debugMode) { e.printStackTrace(); } System.err.println("ERROR creating entry below parent " + parentDN + ": " + e); System.err.println("The entry was not written to the LDIF " + "file."); } } } } } ldifWriter.flush(); ldifWriter.close(); if (dnFile != null) { dnWriter.flush(); dnWriter.close(); } if (bindInfoFile != null) { bindInfoWriter.flush(); bindInfoWriter.close(); } if (loginFile != null) { loginWriter.flush(); loginWriter.close(); } if (csvReader != null) { csvReader.close(); } System.out.println("Processing complete."); System.out.println(entriesWritten + " total entries written."); if (! filterListHash.isEmpty()) { if (! separateFilterFiles) { System.out.println("Writing filters to " + filterFile); BufferedWriter filterWriter = new BufferedWriter(new FileWriter(filterFile)); for (String attr : filterListHash.keySet()) { UniqueSortedList usl = filterListHash.get(attr); if (usl.maintainEqualityList()) { String[] values = usl.getEqualityValues(); for (int i=0; i < values.length; i++) { filterWriter.write('(' + attr + '=' + values[i] + ')' + EOL); } System.out.println("Wrote " + values.length + " equality filters for " + attr); } if (usl.maintainSubInitialList()) { String[] values = usl.getSubInitialValues(); for (int i=0; i < values.length; i++) { filterWriter.write('(' + attr + '=' + values[i] + "*)" + EOL); } System.out.println("Wrote " + values.length + " subInitial filters for " + attr); } if (usl.maintainSubAnyList()) { String[] values = usl.getSubAnyValues(); for (int i=0; i < values.length; i++) { filterWriter.write('(' + attr + "=*" + values[i] + "*)" + EOL); } System.out.println("Wrote " + values.length + " subAny filters for " + attr); } if (usl.maintainSubFinalList()) { String[] values = usl.getSubFinalValues(); for (int i=0; i < values.length; i++) { filterWriter.write('(' + attr + "=*" + values[i] + ')' + EOL); } System.out.println("Wrote " + values.length + " subFinal filters for " + attr); } } filterWriter.flush(); filterWriter.close(); } else { for (String attr : filterListHash.keySet()) { UniqueSortedList usl = filterListHash.get(attr); if (usl.maintainEqualityList()) { String filename = filterFile + '.' + attr + ".eq"; BufferedWriter fileWriter = new BufferedWriter(new FileWriter(filename)); String[] values = usl.getEqualityValues(); for (int i=0; i < values.length; i++) { fileWriter.write('(' + attr + '=' + values[i] + ')' + EOL); } System.out.println("Wrote " + values.length + " equality filters for " + attr + " into " + filename); fileWriter.flush(); fileWriter.close(); } if (usl.maintainSubstringList()) { String filename = filterFile + '.' + attr + ".sub"; BufferedWriter fileWriter = new BufferedWriter(new FileWriter(filename)); int totalValues = 0; String[] values = usl.getSubInitialValues(); for (int i=0; i < values.length; i++) { fileWriter.write('(' + attr + '=' + values[i] + "*)" + EOL); } totalValues += values.length; values = usl.getSubAnyValues(); for (int i=0; i < values.length; i++) { fileWriter.write('(' + attr + "=*" + values[i] + "*)" + EOL); } totalValues += values.length; values = usl.getSubFinalValues(); for (int i=0; i < values.length; i++) { fileWriter.write('(' + attr + "=*" + values[i] + ')' + EOL); } totalValues += values.length; System.out.println("Wrote " + totalValues + " substring filters for " + attr + " into " + filename); fileWriter.flush(); fileWriter.close(); } else { if (usl.maintainSubInitialList()) { String filename = filterFile + '.' + attr + ".subInitial"; BufferedWriter fileWriter = new BufferedWriter(new FileWriter(filename)); String[] values = usl.getSubInitialValues(); for (int i=0; i < values.length; i++) { fileWriter.write('(' + attr + '=' + values[i] + "*)" + EOL); } System.out.println("Wrote " + values.length + " subInitial filters for " + attr + " into " + filename); fileWriter.flush(); fileWriter.close(); } if (usl.maintainSubAnyList()) { String filename = filterFile + '.' + attr + ".subAny"; BufferedWriter fileWriter = new BufferedWriter(new FileWriter(filename)); String[] values = usl.getSubAnyValues(); for (int i=0; i < values.length; i++) { fileWriter.write('(' + attr + "=*" + values[i] + "*)" + EOL); } System.out.println("Wrote " + values.length + " subAny filters for " + attr + " into " + filename); fileWriter.flush(); fileWriter.close(); } if (usl.maintainSubFinalList()) { String filename = filterFile + '.' + attr + ".subFinal"; BufferedWriter fileWriter = new BufferedWriter(new FileWriter(filename)); String[] values = usl.getSubFinalValues(); for (int i=0; i < values.length; i++) { fileWriter.write('(' + attr + "=*" + values[i] + ')' + EOL); } System.out.println("Wrote " + values.length + " subFinal filters for " + attr + " into " + filename); fileWriter.flush(); fileWriter.close(); } } } } } } catch (IOException ioe) { if (debugMode) { ioe.printStackTrace(); } System.err.println("Error writing to LDIF file " + templateFile); ioe.printStackTrace(); System.exit(1); } } /** * Creates an entry in LDIF form based on the provided DN. It attempts to * determine the type of entry to create based on the RDN attribute. * Supported RDN attributes are dc, o, ou, c, and l. If any other RDN * attributes are specified, then the entry will be created using the * extensibleObject objectclass. Multivalued RDNs are not supported, and * neither are escaped commas in the RDN. * * @param branch The branch entry to create. */ public void createBranchEntry(Branch branch) { // If the entry DN is null or empty, then abort. If the entry DN doesn't // have an equal sign, then abort. String entryDN = branch.getDN(); if ((entryDN == null) || (entryDN.length() == 0) || (entryDN.indexOf('=') <= 0)) { return; } // Try to find the RDN attribute and value. String rdnAttr = entryDN.substring(0, entryDN.indexOf('=')); String lowerRDNAttr = rdnAttr.toLowerCase(); String rdnValue = ""; if (entryDN.indexOf(',', entryDN.indexOf('=')) > 0) { rdnValue = entryDN.substring(entryDN.indexOf('=') + 1, entryDN.indexOf(',')); } else { rdnValue = entryDN.substring(entryDN.indexOf('=') + 1); } // Create the string to be returned. StringBuilders are faster than string // concatenation, but it makes this code much harder to read, and because // branch entries are the vast minority in LDIF files, I prefer greatly // improved readability to a very minor performance boost. String entry = null; // Create the appropriate entry type based on the RDN attribute if (lowerRDNAttr.equals("dc")) { entry = "dn: " + entryDN + EOL + "objectclass: top" + EOL + "objectclass: domain" + EOL + "dc: " + rdnValue + EOL; } else if (lowerRDNAttr.equals("o")) { entry = "dn: " + entryDN + EOL + "objectclass: top" + EOL + "objectclass: organization" + EOL + "o: " + rdnValue + EOL; } else if (lowerRDNAttr.equals("ou")) { entry = "dn: " + entryDN + EOL + "objectclass: top" + EOL + "objectclass: organizationalUnit" + EOL + "ou: " + rdnValue + EOL; } else if (lowerRDNAttr.equals("c")) { entry = "dn: " + entryDN + EOL + "objectclass: top" + EOL + "objectclass: country" + EOL + "c: " + rdnValue + EOL; } else if (lowerRDNAttr.equals("l")) { entry = "dn: " + entryDN + EOL + "objectclass: top" + EOL + "objectclass: locality" + EOL + "l: " + rdnValue + EOL; } else { entry = "dn: " + entryDN + EOL + "objectclass: top" + EOL + "objectclass: extensibleObject" + EOL + rdnAttr + ": " + rdnValue + EOL; } // Add the extra line information. Extra lines will be parsed for // variable replacement, but no other tokens will be recognized. String[] extraLines = branch.getExtraLines(); for (int i=0; i < extraLines.length; i++) { entry += extraLines[i] + EOL; } entry += EOL; writeEntry(entryDN, entry, null, null); } /** * Creates a subordinate entry for the specified branch. Any translation that * needs to be done on the attribute values will be taken care of here. * * @param parentDN The DN under which to create the entries. * @param parentAttributes The set of attributes that comprise the parent * entry. * @param template The template to use for the entry. */ public void createTemplateEntry(String parentDN, AttributeList parentAttributes, Template template) { int[] subtemplateCounts = template.getSubtemplateCounts(); String[] rdnAttrs = template.getRDNAttributes(); String[] lowerRDNAttrs = template.getLowerRDNAttributes(); String[] subtemplateNames = template.getSubtemplateNames(); String[][] attrComponents = template.getAttributeComponents(); AttributeList attrList = new AttributeList(); // Get placeholders for the first and last names to use. They won't // actually be retrieved unless the "<first>" or "<last>" tags are // encountered. String firstName = null; String lastName = null; // Set a flag that indicates whether this value needs to be reprocessed or // not. If any changes were made to a value in one iteration, then it // should be reprocessed so that additional processing can be performed if // necessary. Another flag should be used to determine if we're in a // reprocess in order to know boolean needReprocess = false; String value = null; String loginIDValue = null; String passwordValue = null; String[] rdnValues = new String[rdnAttrs.length]; // The name of the current attribute we are working on in lowercase. String lowerAttrName; // Get the next CSV line data if appropriate. String[] csvData = null; if (csvFile != null) { csvData = nextCSVLine(); if (csvData == null) { if (debugMode) { System.err.println("Reached the end of the CSV file"); } csvEndReached = true; return; } } // Iterate through all the attributes and do any processing that needs to be // done. for (int i=0; i < attrComponents.length; i++) { lowerAttrName = attrComponents[i][3]; value = attrComponents[i][2]; needReprocess = true; int pos; // If the value contains "<presence:", then determine if it should // actually be included in this entry. If not, then just go to the next // attribute if ((pos = value.indexOf("<presence:")) >= 0) { int closePos = value.indexOf('>', pos); if (closePos > pos) { String numStr = value.substring(pos+10, closePos); try { int percentage = Integer.parseInt(numStr); int randomValue = ((random.nextInt() & 0x7FFFFFFF) % 100) + 1; if (randomValue <= percentage) { // We have determined that this value should be included in the // LDIF output, so remove the "<presence:x>" tag and let it go on // to do the rest of the processing on this entry value = value.substring(0, pos) + value.substring(closePos+1); } else { // We have determined that this value should not be included in // the LDIF output, so just go on to the next one. continue; } } catch (NumberFormatException nfe) { if (debugMode) { nfe.printStackTrace(); } } } } // If the value contains "<ifpresent:{attrname}>", then determine if it // should actually be included in this entry. If not, then just go to the // next attribute. if ((pos = value.indexOf("<ifpresent:")) >= 0) { int closePos = value.indexOf('>', pos); if (closePos > pos) { int colonPos = value.indexOf(':', pos+11); if ((colonPos > 0) && (colonPos < closePos)) { // Look for a specific value to be present. boolean matchFound = false; String attrName = value.substring(pos+11, colonPos); String matchValue = value.substring(colonPos+1, closePos); String[] values = attrList.getValues(attrName); for (int j=0; j < values.length; j++) { if (matchValue.equalsIgnoreCase(values[j])) { value = value.substring(0, pos) + value.substring(closePos+1); matchFound = true; break; } } if (! matchFound) { continue; } } else { // Just look for the attribute to be present. String attrName = value.substring(pos+11, closePos); String attrValue = attrList.getValue(attrName); if ((attrValue == null) || (attrValue.length() == 0)) { // The requested attribute is not present, so skip this line. continue; } else { value = value.substring(0, pos) + value.substring(closePos+1); } } } } // If the value contains "<ifabsent:{attrname}>", then determine if it // should actually be included in this entry. If not, then just go to the // next attribute. if ((pos = value.indexOf("<ifabsent:")) >= 0) { int closePos = value.indexOf('>', pos); if (closePos > pos) { int colonPos = value.indexOf(':', pos+10); if ((colonPos > 0) && (colonPos < closePos)) { // Look for a specific value to be present. boolean matchFound = false; String attrName = value.substring(pos+10, colonPos); String matchValue = value.substring(colonPos+1, closePos); String[] values = attrList.getValues(attrName); for (int j=0; j < values.length; j++) { if (matchValue.equalsIgnoreCase(values[j])) { matchFound = true; break; } } if (matchFound) { continue; } else { value = value.substring(0, pos) + value.substring(closePos+1); } } else { // Just look for the attribute to be present. String attrName = value.substring(pos+10, closePos); String attrValue = attrList.getValue(attrName); if ((attrValue != null) && (attrValue.length() > 0)) { // The requested attribute is present, so skip this line. continue; } else { value = value.substring(0, pos) + value.substring(closePos+1); } } } } while (needReprocess && (value.indexOf('<') >= 0)) { needReprocess = false; // If the value contains "<first>" then replace that with the first name if ((pos = value.indexOf("<first>")) >= 0) { if (firstName == null) { String[] names = nextFirstAndLastNames(); firstName = names[0]; lastName = names[1]; } value = value.substring(0, pos) + firstName + value.substring(pos + 7); needReprocess = true; } // If the value contains "<last>" then replace that with the last name if ((pos = value.indexOf("<last>")) >= 0) { if (firstName == null) { String[] names = nextFirstAndLastNames(); firstName = names[0]; lastName = names[1]; } value = value.substring(0, pos) + lastName + value.substring(pos + 6); needReprocess = true; } // If the value contains "<dn>" then replace that with the DN of the // current entry. Note for this to be valid, the RDN attribute must // already have a value. if ((pos = value.indexOf("<dn>")) >= 0) { StringBuilder buffer = new StringBuilder(75); buffer.append(value.substring(0, pos)); String separator = ""; for (int j=0; j < rdnAttrs.length; j++) { buffer.append(separator); buffer.append(rdnAttrs[j]); buffer.append('='); buffer.append(rdnValues[j]); separator = "+"; } buffer.append(','); buffer.append(parentDN); buffer.append(value.substring(pos+4)); value = buffer.toString(); needReprocess = true; } // If the value contains "<_dn>", then replace that with the DN of the // current entry, but with underscores instead of commas. This is an // undocumented feature for use with identity server. Note for this to // be valid, the RDN attribute must already have a value. if ((pos = value.indexOf("<_dn>")) >= 0) { StringBuilder buffer = new StringBuilder(75); String separator = ""; for (int j=0; j < rdnAttrs.length; j++) { buffer.append(separator); buffer.append(rdnAttrs[j]); buffer.append('='); buffer.append(rdnValues[j]); separator = "+"; } buffer.append('_'); buffer.append(parentDN); value = value.substring(0, pos) + buffer.toString().replace(',', '_') + value.substring(pos + 5); needReprocess = true; } // If the value contains "<parentdn>" then replace that with the DN of // the parent entry if ((pos = value.indexOf("<parentdn>")) >= 0) { value = value.substring(0, pos) + parentDN + value.substring(pos + 10); needReprocess = true; } // If the value contains "<_parentdn>", then replace that with the DN of // the parent entry, but with underscores instead of commas. This is an // undocumented feature for use with identity server. if ((pos = value.indexOf("<_parentdn>")) >= 0) { value = value.substring(0, pos) + parentDN.replace(',', '_') + value.substring(pos + 11); needReprocess = true; } // If the value contains "<parent:attr>" then replace that with the // value of the specified attribute from the parent entry if that is // available or an empty string if not. if ((pos = value.indexOf("<parent:")) >= 0) { int closePos = value.indexOf('>', pos); String attrName = value.substring(pos+8, closePos); String attrValue; if (parentAttributes == null) { attrValue = ""; } else { attrValue = parentAttributes.getValue(attrName); } value = value.substring(0, pos) + attrValue + value.substring(closePos+1); needReprocess = true; } // If the value contains "<ancestordn:depth>", then replace that with // the DN of the specified ancestor entry (e.g., if "depth" is 1, then // the result will be the entry's parent, 2 will be the grandparent, // etc.). Note that if the entry does not have an ancestor with the // specified depth, then this token will be replaced with an empty // string. if ((pos = value.indexOf("<ancestordn:")) >= 0) { int closePos = value.indexOf('>', pos); int depth = Integer.parseInt(value.substring(pos+12, closePos)); String ancestordn = ""; if (depth == 1) { ancestordn = parentDN; } else { int startPos = 0; for (int j=1; ((startPos >= 0) && (j < depth)); j++) { startPos = parentDN.indexOf(',', startPos+1); } if (startPos >= 0) { ancestordn = parentDN.substring(startPos+1); } } value = value.substring(0, pos) + ancestordn + value.substring(closePos+1); needReprocess = true; } // If the value contains "<_ancestordn:depth>", then replace that with // the DN of the specified ancestor entry, but with underscores instead // of commas in the ancestor entry. if ((pos = value.indexOf("<_ancestordn:")) >= 0) { int closePos = value.indexOf('>', pos); int depth = Integer.parseInt(value.substring(pos+13, closePos)); String ancestordn = ""; int startPos = 0; for (int j=1; ((startPos >= 0) && (j < depth)); j++) { startPos = parentDN.indexOf(',', startPos); } if (startPos > 0) { ancestordn = parentDN.substring(startPos+1).replace(',', '_'); } value = value.substring(0, pos) + ancestordn + value.substring(closePos+1); needReprocess = true; } // If the value contains "<csvfield:number>", then replace that with the // corresponding field from the CSV data. if ((pos = value.indexOf("<csvfield:")) >= 0) { if (csvFile == null) { System.err.println("ERROR: CSV field referenced but no CSV file " + "provided (use -c)"); csvEndReached = true; return; } int closePos = value.indexOf('>', pos); int fieldNum = Integer.parseInt(value.substring(pos+10, closePos)); String fieldData; if (fieldNum >= csvData.length) { if (debugMode) { System.err.println("Invalid field number " + fieldNum + " -- only " + csvData.length + " fields on CSV data line"); } fieldData = ""; } else { fieldData = csvData[fieldNum]; } value = value.substring(0, pos) + fieldData + value.substring(closePos+1); } // If the value contains "<exec:cmd,arg1,arg2,...,argn>" then replace // that with the output of that command. Note that this is a real // performance killer, so it should be used only if necessary. Also, // any end of line characters included in the output will be stripped // so if there are multiple lines of output, then they will be run // together. if ((pos = value.indexOf("<exec:")) >= 0) { int closePos = value.indexOf('>', pos); String cmdStr = value.substring(pos+6, closePos); StringTokenizer tokenizer = new StringTokenizer(cmdStr, ","); ArrayList<String> cmdList = new ArrayList<String>(); while (tokenizer.hasMoreTokens()) { cmdList.add(tokenizer.nextToken()); } String[] cmdArray = new String[cmdList.size()]; for (int j=0; j < cmdArray.length; j++) { cmdArray[j] = cmdList.get(j); } try { Process p = Runtime.getRuntime().exec(cmdArray); BufferedInputStream bis = new BufferedInputStream(p.getInputStream()); StringBuilder buffer = new StringBuilder(80); byte[] buf = new byte[1024]; int readLength = 0; while ((readLength = bis.read(buf)) > 0) { for (int j=0; j < readLength; j++) { if ((buf[j] != '\r') && (buf[j] != '\n')) { buffer.append((char) buf[j]); } } } bis.close(); value = value.substring(0, pos) + buffer.toString() + value.substring(closePos+1); needReprocess = true; } catch (IOException ioe) { if (debugMode) { ioe.printStackTrace(); } System.err.println("Error executing command " + cmdArray[0] + ": " + ioe); } } // If the value contains "<random:chars:characters:length>" then // generate a random string of length characters from the provided // character set. if ((pos = value.indexOf("<random:chars:")) >= 0) { // Get the set of characters to use in the resulting value. int colonPos = value.indexOf(':', pos+14); int closePos = value.indexOf('>', colonPos+1); String charSet = value.substring(pos+14, colonPos); // See if there is an additional colon followed by a number. If so, // then the length will be a random number between the two. int count; int colonPos2 = value.indexOf(':', colonPos+1); if ((colonPos2 > 0) && (colonPos2 < closePos)) { int minValue = Integer.parseInt(value.substring(colonPos+1, colonPos2)); int maxValue = Integer.parseInt(value.substring(colonPos2+1, closePos)); int span = maxValue - minValue + 1; count = (random.nextInt() & 0x7FFFFFFF) % span + minValue; } else { count = Integer.parseInt(value.substring(colonPos+1, closePos)); } StringBuilder buffer = new StringBuilder(value.length() + count); buffer.append(value.substring(0, pos)); generateRandomValue(charSet.toCharArray(), count, buffer); buffer.append(value.substring(closePos+1)); value = buffer.toString(); needReprocess = true; } // If the value contains "<random:alpha:num>" then generate a random // alphabetic value and use it. if ((pos = value.indexOf("<random:alpha:")) >= 0) { // See if there is an additional colon followed by a number. If so, // then the length will be a random number between the two. int count; int closePos = value.indexOf('>', pos+14); int colonPos = value.indexOf(':', pos+14); if ((colonPos > 0) && (colonPos < closePos)) { int minValue = Integer.parseInt(value.substring(pos+14, colonPos)); int maxValue = Integer.parseInt(value.substring(colonPos+1, closePos)); int span = maxValue - minValue + 1; count = (random.nextInt() & 0x7FFFFFFF) % span + minValue; } else { count = Integer.parseInt(value.substring(pos+14, closePos)); } // Generate the new value. StringBuilder buffer = new StringBuilder(value.length() + count); buffer.append(value.substring(0, pos)); generateRandomValue(ALPHA_CHARS, count, buffer); buffer.append(value.substring(closePos+1)); value = buffer.toString(); needReprocess = true; } // If the value contains "<random:numeric:num>" then generate a random // numeric value and use it. This can also take the form // "<random:numeric:min:max>" or "<random:numeric:min:max:length>". if ((pos = value.indexOf("<random:numeric:")) >= 0) { int closePos = value.indexOf('>', pos); // See if there is an extra colon. If so, then generate a random // number between x and y. Otherwise, generate a random number with // the specified number of digits. int extraColonPos = value.indexOf(':', pos+16); if ((extraColonPos > 0) && (extraColonPos < closePos)) { // See if there is one more colon separating the max from the // length. If so, then get it and create a padded value of at least // length digits. If not, then just generate the random value. int extraColonPos2 = value.indexOf(':', extraColonPos+1); if ((extraColonPos2 > 0) && (extraColonPos2 < closePos)) { String lowerBoundStr = value.substring(pos+16, extraColonPos); String upperBoundStr = value.substring(extraColonPos+1, extraColonPos2); String lengthStr = value.substring(extraColonPos2+1, closePos); int lowerBound = Integer.parseInt(lowerBoundStr); int upperBound = Integer.parseInt(upperBoundStr); int length = Integer.parseInt(lengthStr); int span = (upperBound - lowerBound + 1); int randomValue = (random.nextInt() & 0x7FFFFFFF) % span + lowerBound; String valueStr = String.valueOf(randomValue); while (valueStr.length() < length) { valueStr = '0' + valueStr; } value = value.substring(0, pos) + valueStr + value.substring(closePos+1); } else { String lowerBoundStr = value.substring(pos+16, extraColonPos); String upperBoundStr = value.substring(extraColonPos+1, closePos); int lowerBound = Integer.parseInt(lowerBoundStr); int upperBound = Integer.parseInt(upperBoundStr); int span = (upperBound - lowerBound + 1); int randomValue = (random.nextInt() & 0x7FFFFFFF) % span + lowerBound; value = value.substring(0, pos) + randomValue + value.substring(closePos+1); } } else { // Get the number of characters to include in the value int numPos = pos + 16; int count = Integer.parseInt(value.substring(numPos, closePos)); StringBuilder buffer = new StringBuilder(value.length() + count); buffer.append(value.substring(0, pos)); generateRandomValue(NUMERIC_CHARS, count, buffer); buffer.append(value.substring(closePos+1)); value = buffer.toString(); } needReprocess = true; } // If the value contains "<random:alphanumeric:num>" then generate a // random alphanumeric value and use it if ((pos = value.indexOf("<random:alphanumeric:")) >= 0) { // See if there is an additional colon followed by a number. If so, // then the length will be a random number between the two. int count; int closePos = value.indexOf('>', pos+21); int colonPos = value.indexOf(':', pos+21); if ((colonPos > 0) && (colonPos < closePos)) { int minValue = Integer.parseInt(value.substring(pos+21, colonPos)); int maxValue = Integer.parseInt(value.substring(colonPos+1, closePos)); int span = maxValue - minValue + 1; count = (random.nextInt() & 0x7FFFFFFF) % span + minValue; } else { count = Integer.parseInt(value.substring(pos+21, closePos)); } // Generate the new value. StringBuilder buffer = new StringBuilder(value.length()+count); buffer.append(value.substring(0, pos)); generateRandomValue(ALPHANUMERIC_CHARS, count, buffer); buffer.append(value.substring(closePos+1)); value = buffer.toString(); needReprocess = true; } // If the value contains "<random:hex:num>" then generate a random // hexadecimal value and use it if ((pos = value.indexOf("<random:hex:")) >= 0) { // See if there is an additional colon followed by a number. If so, // then the length will be a random number between the two. int count; int closePos = value.indexOf('>', pos+12); int colonPos = value.indexOf(':', pos+12); if ((colonPos > 0) && (colonPos < closePos)) { int minValue = Integer.parseInt(value.substring(pos+12, colonPos)); int maxValue = Integer.parseInt(value.substring(colonPos+1, closePos)); int span = maxValue - minValue + 1; count = (random.nextInt() & 0x7FFFFFFF) % span + minValue; } else { count = Integer.parseInt(value.substring(pos+12, closePos)); } // Generate the new value. StringBuilder buffer = new StringBuilder(value.length()+count); buffer.append(value.substring(0, pos)); generateRandomValue(HEX_CHARS, count, buffer); buffer.append(value.substring(closePos+1)); value = buffer.toString(); needReprocess = true; } // If the value contains "<random:base64:num>" then generate a random // base64 value and use it if ((pos = value.indexOf("<random:base64:")) >= 0) { // See if there is an additional colon followed by a number. If so, // then the length will be a random number between the two. int count; int closePos = value.indexOf('>', pos+15); int colonPos = value.indexOf(':', pos+15); if ((colonPos > 0) && (colonPos < closePos)) { int minValue = Integer.parseInt(value.substring(pos+15, colonPos)); int maxValue = Integer.parseInt(value.substring(colonPos+1, closePos)); int span = maxValue - minValue + 1; count = (random.nextInt() & 0x7FFFFFFF) % span + minValue; } else { count = Integer.parseInt(value.substring(pos+15, closePos)); } // Generate the new value. StringBuilder buffer = new StringBuilder(value.length()+count); buffer.append(value.substring(0, pos)); generateRandomValue(BASE64_CHARS, count, buffer); switch (count % 4) { case 1: buffer.append("==="); break; case 2: buffer.append("=="); break; case 3: buffer.append('='); break; } buffer.append(value.substring(closePos+1)); value = buffer.toString(); needReprocess = true; } // If the value contains "<random:telephone>" then generate a random // telephone number and use it if ((pos = value.indexOf("<random:telephone>")) >= 0) { // Get the number of characters to include in the value StringBuilder buffer = new StringBuilder(value.length()+10); buffer.append(value.substring(0, pos)); generateRandomValue(NUMERIC_CHARS, 3, buffer); buffer.append('-'); generateRandomValue(NUMERIC_CHARS, 3, buffer); buffer.append('-'); generateRandomValue(NUMERIC_CHARS, 4, buffer); buffer.append(value.substring(pos+18)); value = buffer.toString(); needReprocess = true; } // If the value contains "<random:month>" then choose a random month // name. Optionally, look for "<random:month:length>" and use at most // length characters of the month name. if ((pos = value.indexOf("<random:month")) >= 0) { int closePos = value.indexOf('>', pos+13); String monthStr = MONTH_NAMES[(random.nextInt() & 0x7FFFFFFF) % 12]; // See if there is another colon that specifies the length. int colonPos = value.indexOf(':', pos+13); if ((colonPos > 0) && (colonPos < closePos)) { String lengthStr = value.substring(colonPos+1, closePos); int length = Integer.parseInt(lengthStr); if (monthStr.length() > length) { monthStr = monthStr.substring(0, length); } } value = value.substring(0, pos) + monthStr + value.substring(closePos+1); needReprocess = true; } // If the value contains "<guid>" then generate a GUID and use it if ((pos = value.indexOf("<guid>")) >= 0) { // Get the number of characters to include in the value StringBuilder buffer = new StringBuilder(value.length()+32); buffer.append(value.substring(0, pos)); generateGUID(buffer); buffer.append(value.substring(pos+6)); value = buffer.toString(); needReprocess = true; } // If the value contains "<sequential>" then use the next sequential // value for that attribute if ((pos = value.indexOf("<sequential")) >= 0) { int closePos = value.indexOf('>', pos); // Get the sequential counter for that attribute Counter c = template.getCounter(attrComponents[i][3]); if (c == null) { // If a starting point was specified, then use it. If not, then // use 0. int colonPos = value.indexOf(':', pos); int startingValue = 0; if ((colonPos > pos) && (colonPos < closePos)) { startingValue = Integer.parseInt(value.substring(colonPos+1, closePos)); } c = new Counter(startingValue); template.addCounter(attrComponents[i][3], c); } value = value.substring(0, pos) + c.getNext() + value.substring(closePos+1); needReprocess = true; } // If the value contains "<list:" then treat it as a value list and get // the value from that if ((pos = value.indexOf("<list:")) >= 0) { int closePos = value.indexOf('>', pos); // See if a value list already exists for the specified attribute. If // not, then create one ValueList vl = valueLists.get(attrComponents[i][3]); if (vl == null) { vl = new ValueList(randomSeed); String listVals = value.substring(pos+6, closePos); StringTokenizer tokenizer = new StringTokenizer(listVals, ","); while (tokenizer.hasMoreElements()) { String listValue = tokenizer.nextToken(); int colonPos = listValue.indexOf(':'); if (colonPos > 0) { String val = listValue.substring(0, colonPos); int weight = Integer.parseInt(listValue.substring(colonPos+1)); vl.addValue(val, weight); } else { vl.addValue(listValue); } } vl.completeInitialization(); valueLists.put(attrComponents[i][3], vl); } // Get the list value and use it value = value.substring(0, pos) + vl.nextValue() + value.substring(closePos+1); needReprocess = true; } // If the value contains "<file:" then treat it as a file containing // potential values. if ((pos = value.indexOf("<file:")) >= 0) { int closePos = value.indexOf('>', pos); String filename = value.substring(pos+6, closePos); // See if a value list already exists for the specified attribute. If // not, then create one ValueList vl = fileLists.get(filename); if (vl == null) { vl = new ValueList(randomSeed); BufferedReader fileReader = null; File f = getFileForName(filename); try { fileReader = new BufferedReader(new FileReader(f)); } catch (IOException ioe) { if (debugMode) { ioe.printStackTrace(); } System.err.println("Error: Unable to open file" + f.getAbsolutePath() + " to get value list -- aborting"); try { ldifWriter.flush(); ldifWriter.close(); } catch (IOException ioe2) {} System.exit(1); } try { while (fileReader.ready()) { vl.addValue(fileReader.readLine()); } } catch (IOException ioe) { if (debugMode) { ioe.printStackTrace(); } System.err.println("Error reading file" + f.getAbsolutePath() + " to get value list -- " + ioe + "-- aborting"); try { ldifWriter.flush(); ldifWriter.close(); } catch (IOException ioe2) {} System.exit(1); } try { fileReader.close(); } catch (IOException ioe) {} vl.completeInitialization(); fileLists.put(filename, vl); } value = value.substring(0, pos) + vl.nextValue() + value.substring(closePos+1); needReprocess = true; } } needReprocess = true; while (needReprocess && ((pos = value.indexOf('{')) >= 0)) { // If there is a backslash in front of the curly brace, then we don't // want to consider it an attribute name. if ((pos > 0) && (value.charAt(pos-1) == '\\')) { boolean keepGoing = true; boolean nonEscaped = false; while (keepGoing) { value = value.substring(0, pos-1) + value.substring(pos); pos = value.indexOf('{', pos); if (pos < 0) { keepGoing = false; } else if (value.charAt(pos-1) != '\\') { nonEscaped = true; } } if (! nonEscaped) { break; } } // If the value has "{attr}", then try to replace it with the value of // that attribute. Note that attribute replacement will only work // properly for attributes that are defined in the template before the // attribute that attempts to use its value. If the specified attribute // has more than one value, then the first value found will be used. int closePos = value.indexOf('}', pos); if (closePos > 0) { int colonPos = value.indexOf(':', pos); int substringChars = -1; String attrName = null; if ((colonPos > 0) && (colonPos < closePos)) { attrName = value.substring(pos+1, colonPos).toLowerCase(); String numStr = value.substring(colonPos+1, closePos); try { substringChars = Integer.parseInt(numStr); } catch (NumberFormatException nfe) { if (debugMode) { nfe.printStackTrace(); } } } else { attrName = value.substring(pos+1, closePos).toLowerCase(); } String attrValue = attrList.getValueForLowerName(attrName); if ((colonPos > 0) && (colonPos < closePos) && (substringChars > 0) && (attrValue.length() > substringChars)) { attrValue = attrValue.substring(0, substringChars); } StringBuilder buffer = new StringBuilder(75); buffer.append(value.substring(0, pos)).append(attrValue). append(value.substring(closePos+1)); value = buffer.toString(); needReprocess = true; } } if ((pos = value.indexOf("<base64:")) >= 0) { String charset; String valueToEncode; int closePos = value.indexOf('>', pos+8); int colonPos = value.indexOf(':', pos+8); if ((closePos > 0) && (colonPos > 0) && (colonPos < closePos)) { charset = value.substring(pos+8, colonPos); valueToEncode = value.substring(colonPos+1, closePos); } else { charset = "UTF-8"; valueToEncode = value.substring(pos+8, closePos); } try { String encodedStr = Base64.encode(valueToEncode.getBytes(charset)); value = value.substring(0, pos) + encodedStr + value.substring(closePos+1); } catch (UnsupportedEncodingException uee) { if (debugMode) { uee.printStackTrace(); } System.out.println("Could not base64-encode the value \"" + valueToEncode + "\" -- unsupported encoding " + "type " + charset); } } if ((pos = value.indexOf("<custom:")) >= 0) { int closePos = value.indexOf('>', pos+8); String className; String[] tagArgs; int colonPos = value.indexOf(':', pos+8); if (colonPos > 0) { className = value.substring(pos+8, colonPos); String argString = value.substring(colonPos+1, closePos); ArrayList<String> argList = new ArrayList<String>(); StringTokenizer tokenizer = new StringTokenizer(argString, ","); while (tokenizer.hasMoreTokens()) { argList.add(tokenizer.nextToken()); } tagArgs = new String[argList.size()]; argList.toArray(tagArgs); } else { className = value.substring(pos+8, closePos); tagArgs = new String[0]; } String tagName = attrComponents[i][3] + ' ' + className; CustomTag customTag = template.getCustomTag(tagName); if (customTag == null) { try { Class tagClass = Constants.classForName(className); customTag = (CustomTag) tagClass.newInstance(); customTag.initialize(); template.addCustomTag(tagName, customTag); } catch (Exception e) { if (debugMode) { e.printStackTrace(); } System.out.println("Could not create custom tag " + className + ": " + e); } } value = value.substring(0, pos) + customTag.generateOutput(tagArgs) + value.substring(closePos+1); } if ((pos = value.indexOf("<loop:")) >= 0) { int closePos = value.indexOf('>', pos+6); int colonPos = value.indexOf(':', pos+6); if ((closePos > 0) && (colonPos > 0) && (colonPos < closePos)) { int lowerBound = Integer.parseInt(value.substring(pos+6, colonPos)); int upperBound = Integer.parseInt(value.substring(colonPos+1, closePos)); int span = (upperBound - lowerBound + 1); for (int j=0; j < span; j++) { String copy = value; while ((pos = copy.indexOf("<loop:")) >= 0) { closePos = copy.indexOf('>', pos+6); colonPos = copy.indexOf(':', pos+6); lowerBound = Integer.parseInt(copy.substring(pos+6, colonPos)); copy = copy.substring(0, pos) + (lowerBound + j) + copy.substring(closePos+1); } attrList.addAttribute(attrComponents[i][0], attrComponents[i][3], attrComponents[i][1], copy); } } } else { attrList.addAttribute(attrComponents[i][0], attrComponents[i][3], attrComponents[i][1], value); } for (int j=0; j < rdnAttrs.length; j++) { if ((rdnValues[j] == null) && (lowerAttrName.equals(lowerRDNAttrs[j]))) { rdnValues[j] = value; } } if ((loginIDValue == null) && (lowerAttrName.equals(loginIDAttr))) { loginIDValue = value; } if ((passwordValue == null) && (lowerAttrName.equals("userpassword"))) { passwordValue = value; } if (generateFilterList) { UniqueSortedList usl = filterListHash.get(lowerAttrName); if (usl != null) { usl.addString(value.toLowerCase()); } } } // All of the values have been processed. Now try to calculate the DN for // the entry. for (int i=0; i < rdnValues.length; i++) { if (rdnValues[i] == null) { System.out.println("Could not calculate the DN for the entry (no " + "value for RDN attribute " + rdnAttrs[i] + " found)"); System.out.println(attrList.toString()); return; } } // Create the DN for the entry StringBuilder dnBuffer = new StringBuilder(75); String separator = ""; for (int i=0; i < rdnAttrs.length; i++) { dnBuffer.append(separator); dnBuffer.append(rdnAttrs[i]); dnBuffer.append('='); dnBuffer.append(rdnValues[i]); separator = "+"; } dnBuffer.append(','); dnBuffer.append(parentDN); String entryDN = dnBuffer.toString(); // Write the completed entry to the LDIF file. writeEntry(entryDN, "dn: " + entryDN + EOL + attrList.toString(), passwordValue, loginIDValue); // If this entry is to have subordinate entries, then create them for (int i=0; i < subtemplateNames.length; i++) { Template subtemplate = templateHash.get(subtemplateNames[i].toLowerCase()); if (subtemplate != null) { subtemplate.resetCounters(); subtemplate.reinitializeCustomTags(); int numSubEntries = subtemplateCounts[i]; if (maxPerTemplate >= 0) { numSubEntries = Math.min(maxPerTemplate, numSubEntries); } for (int j=0; ((! csvEndReached) && (j < numSubEntries)); j++) { try { createTemplateEntry(entryDN, attrList, subtemplate); } catch (Exception e) { if (debugMode) { e.printStackTrace(); } System.err.println("ERROR creating entry below parent " + entryDN + ": " + e); System.err.println("The entry was not written to the LDIF file."); } } } else { System.out.println("ERROR: Subordinate template " + subtemplateNames[i] + " requested by template " + template.getName() + " is not defined. Skipping " + subtemplateCounts[i] + " subordinate entries."); } } } /** * Generates a random value of the indicated length from the specified * character set. * * @param charSet The character set from which the random characters are to * be taken. * @param length The length of the random value to generate. * @param buffer The string buffer into which the random characters are to * be written. */ public void generateRandomValue(char[] charSet, int length, StringBuilder buffer) { // A performance optimization in an attempt to avoid continually // re-allocating character arrays. If the requested array is less than // 5000 characters long, then use the space we've already allocated for this // purpose. If it is longer than 5000 characters, then we'll take the // hit of re-allocating the memory. if (length <= 5000) { for (int i=0; i < length; i++) { chars5000[i] = charSet[(random.nextInt() & 0x7FFFFFFF) % charSet.length]; } buffer.append(chars5000, 0, length); } else { char[] retArray = new char[length]; for (int i=0; i < length; i++) { retArray[i] = charSet[(random.nextInt() & 0x7FFFFFFF) % charSet.length]; } buffer.append(retArray); } } /** * Generates a globally-unique identifier. Technically speaking, it's not * guaranteed to be globally unique, but this should be good enough for most * purposes. * * @param buffer The string buffer into which the GUID should be written. */ public void generateGUID(StringBuilder buffer) { StringBuilder buf2 = new StringBuilder(20); if (randomSeed < 0) { buf2.append(Long.toHexString(System.currentTimeMillis())); } else { buf2.append(Long.toHexString(random.nextLong())); } generateRandomValue(HEX_CHARS, 20-buf2.length(), buf2); buffer.append(buf2.substring(0, 8)); buffer.append('-'); buffer.append(buf2.substring(8, 12)); buffer.append('-'); buffer.append(buf2.substring(12, 16)); buffer.append('-'); buffer.append(buf2.substring(16)); buffer.append('-'); buffer.append(guidBase); } /** * Creates a "wrapped" version of the entry, where long lines are wrapped at * 75 columns. * * @param entry The entry to be wrapped. * * @return The wrapped entry. */ public String wrapEntry(String entry) { // Using a string buffer here instead of string concatenation gives a huge // performance boost. Even things like " " + line are slower than // returnStr.append(" "); returnStr.append(line); StringBuilder buffer = new StringBuilder(1000); StringTokenizer tokenizer = new StringTokenizer(entry, EOL); while (tokenizer.hasMoreTokens()) { String line = tokenizer.nextToken(); if (line.length() > 75) { buffer.append(line.substring(0, 76)); buffer.append(EOL); line = line.substring(76); while (line.length() > 74) { buffer.append(' '); buffer.append(line.substring(0, 75)); buffer.append(EOL); line = line.substring(75); } if (line.length() > 0) { buffer.append(' '); buffer.append(line); buffer.append(EOL); } } else { buffer.append(line); buffer.append(EOL); } } return buffer.toString() + EOL; } /** * Retrieves the next values that should be used for the first and last names * (the first element will be the first name, the second element will be the * last name). The combination of the first and last name is guaranteed to be * unique for this LDIF file and therefore could be used for the uid. * * @return The next values that should be used for the first and last names. */ public String[] nextFirstAndLastNames() { // Start with the plain first and last names at the appropriate positions // in the name lists String first = firstNames[firstNameIndex]; String last = lastNames[lastNameIndex]; // If we have exhausted all possible unique combinations of first and last // names, then we need to add a counter to the end of the last name to // ensure that we can still have unique combinations if (nameUniquenessCounter > 1) { last += nameUniquenessCounter; } // Create the array to return String[] names = new String[] { first, last }; // Now find the indexes of the next values to retrieve. Most of the time // (if neither index is at the end of the list), they will both just be // incremented by one. Check for that condition first. if ((firstNameIndex+1 < numFirstNames) && (lastNameIndex+1 < numLastNames)) { firstNameIndex++; lastNameIndex++; } else { if ((firstNameIndex+1 >= numFirstNames) && (numFirstNames >= numLastNames)) { // We're at the end of the first names list and the first names list is // larger than the last names list. Set the first name list counter to // zero, increment the name loop counter, and set the last name index // value to that. If the resulting last name index is greater than or // equal to the length, then we've been through all possible unique // combinations at least once so increment the name uniqueness counter. firstNameIndex = 0; nameLoopCounter++; lastNameIndex = nameLoopCounter; if (lastNameIndex >= numLastNames) { lastNameIndex = 0; nameUniquenessCounter++; } } else if ((lastNameIndex+1 >= numLastNames) && (numLastNames > numFirstNames)) { // This is the same condition as above, except we are at the end of the // last name list and that list is bigger. So do exactly the same thing // but interchange the first and last name references lastNameIndex = 0; nameLoopCounter++; firstNameIndex = nameLoopCounter; if (firstNameIndex >= numFirstNames) { firstNameIndex = 0; nameUniquenessCounter++; } } else if (firstNameIndex+1 >= numFirstNames) { // We are at the end of the first name list and the first name list is // smaller than the last name list. Just start it over at zero and // increment the last name index. firstNameIndex = 0; lastNameIndex++; } else { // We are at the end of the last name list and the last name list is // smaller than the first name list. Just start it over at zero and // increment the first name index lastNameIndex = 0; firstNameIndex++; } } return names; } /** * Reads the next line from the CSV file and splits it into separate fields. * * @return An array containing the fields read from the next line of the CSV * file, or <CODE>null</CODE> if there is no more data in the file. */ public String[] nextCSVLine() { try { String line = csvReader.readLine(); if (line == null) { return null; } ArrayList<String> elementList = new ArrayList<String>(); if (csvDelimiter == null) { // This is actually a CSV file, so parse it accordingly. boolean keepReading = true; int startPos = 0; int searchStartPos = 0; while (keepReading) { int commaPos = line.indexOf(',', searchStartPos); if (commaPos < 0) { // No more delimiters on the line, so the remainder is the last // element. String substring = line.substring(startPos).trim(); int lastPos = substring.length() - 1; if ((substring.charAt(0) == '"') && (substring.charAt(lastPos) == '"')) { substring = substring.substring(1, lastPos); } elementList.add(substring); keepReading = false; } else { // We have found a comma. See if it is one that should be // considered a delimiter. if ((commaPos > 0) && (line.charAt(commaPos-1) == '\\')) { // This is an escaped comma, so it shouldn't be a delimiter. line = line.substring(0, commaPos-1) + line.substring(commaPos); searchStartPos = commaPos; continue; } String substring = line.substring(startPos, commaPos); if (substring.indexOf('"') >= 0) { // There is at least one quotation mark on the line. Determine // whether the comma we found was inside quoted section. int numQuotes = 0; for (int i=0; i < substring.length(); i++) { if (substring.charAt(i) == '"') { numQuotes++; } if ((numQuotes % 2) == 0) { // There are an even number of quotation marks, so we can // assume we're not in the middle of a quoted section. substring = substring.trim(); int lastPos = substring.length() - 1; // If the substring is surrounded by quotes, then strip them // off. if ((substring.charAt(0) == '"') && (substring.charAt(lastPos) == '"')) { substring = substring.substring(1, lastPos); } elementList.add(substring); startPos = commaPos+1; searchStartPos = startPos; } else { // There are an odd number of quotes, so we can assume we are // in a quoted section. searchStartPos = commaPos+1; continue; } } } else { // No quotation marks in the substring, so we have an element. elementList.add(line.substring(startPos, commaPos).trim()); startPos = commaPos + 1; searchStartPos = startPos; } } } } else { // This is a delimited text file, so parse it accordingly. boolean keepReading = true; int startPos = 0; while (keepReading) { int delimiterPos = line.indexOf(csvDelimiter, startPos); if (delimiterPos < 0) { // No more delimiters on the line, so the remainder is the last // element. elementList.add(line.substring(startPos)); keepReading = false; } else { // There is a delimiter, so grab the element and move forward in the // string. elementList.add(line.substring(startPos, delimiterPos)); startPos = delimiterPos + csvDelimiter.length(); } } } String[] elements = new String[elementList.size()]; elementList.toArray(elements); return elements; } catch (IOException ioe) { if (debugMode) { ioe.printStackTrace(); } System.err.println("Error reading CSV file \"" + csvFile + "\" -- " + ioe); return null; } } /** * Retrieves the template definition with the specified name. If the * requested template could not be found, <CODE>null</CODE> is returned. If * the requested template is a subordinate of some other template, then the * returned template will have the complete set of attributes from any parent * templates that it might have. * * @param templateName The name of the template to retrieve. * * @return The requested template, or <CODE>null</CODE> if the template could * not be found. */ public Template getTemplate(String templateName) { Template t = templateHash.get(templateName.toLowerCase()); if (t == null) { return null; } if ((t.getParentTemplateName() == null) || (t.getParentTemplateName().length() == 0)) { return t; } else { Template pt = getTemplate(t.getParentTemplateName()); if (pt == null) { return t; } else { String[][] parentComponents = pt.getAttributeComponents(); String[][] childComponents = t.getAttributeComponents(); String[][] newComponents = new String[parentComponents.length + childComponents.length][]; Template nt = (Template) t.clone(); nt.parentTemplateName = null; for (int i=0; i < parentComponents.length; i++) { newComponents[i] = parentComponents[i]; } for (int i=0; i < childComponents.length; i++) { newComponents[i+parentComponents.length] = childComponents[i]; } nt.attrComponents = newComponents; return nt; } } } /** * Writes the specified entry into the LDIF file. If the entry needs to be * wrapped, then that will be done. This will also keep track of the total * number of entries written so that progress details will be printed as the * entries are written. * * @param entryDN The DN of the entry to be written. * @param entry The entry to be written into the LDIF file. * @param passwordValue The password from the entry, if present. * @param loginIDValue The login ID from the entry, if present. */ public void writeEntry(String entryDN, String entry, String passwordValue, String loginIDValue) { if (dnFile != null) { try { dnWriter.write(entryDN + EOL); } catch (IOException ioe) { if (debugMode) { ioe.printStackTrace(); } System.err.println("Error writing to DN file: " + ioe); } } if ((loginIDValue != null) && (passwordValue != null) && (loginFile != null)) { try { loginWriter.write(loginIDValue + '\t' + passwordValue + EOL); } catch (IOException ioe) { if (debugMode) { ioe.printStackTrace(); } System.err.println("Error writing to login info file: " + ioe); } } if ((passwordValue != null) && (bindInfoFile != null)) { try { bindInfoWriter.write(entryDN + '\t' + passwordValue + EOL); } catch (IOException ioe) { if (debugMode) { ioe.printStackTrace(); } System.err.println("Error writing to bind info file: " + ioe); } } if (wrapLongLines) { entry = wrapEntry(entry); } try { ldifWriter.write(entry); } catch (IOException ioe) { if (debugMode) { ioe.printStackTrace(); } System.err.println("Error writing entry to LDIF file: " + ioe); } entriesWritten++; if ((entriesWritten % 1000) == 0) { System.out.println("Processed " + entriesWritten + " entries"); } if ((maxPerFile > 0) && ((entriesWritten % maxPerFile) == 0)) { try { ldifWriter.flush(); ldifWriter.close(); fileNameCounter++; ldifWriter = new BufferedWriter(new FileWriter(ldifFile + '.' + fileNameCounter)); } catch (IOException ioe) { if (debugMode) { ioe.printStackTrace(); } System.err.println("Error creating a new LDIF file: " + ioe); } } } /** * Retrieves the path to the specified file. * * @param name The name of the file to retrieve. * * @return The appropriate path to use for the file. */ private File getFileForName(String name) { File f = new File(name); if (f.exists() || (resourceDir == null)) { return f; } return new File(resourceDir, name); } /** * Displays version information for the MakeLDIF program. */ public void displayVersion() { System.err.println("MakeLDIF version " + VERSION_STRING); } /** * Displays information about how to use this program. */ public void displayUsage() { System.err.println( "Usage: java MakeLDIF {options}" + EOL + " valid {options} include:" + EOL + "-r {path} -- The path to the directory containing MakeLDIF " + EOL + " resource files" + EOL + "-f {filename} -- The name of the file containing first names" + EOL + "-l {filename} -- The name of the file containing last names" + EOL + "-t {filename} -- The LDIF template file" + EOL + "-o {filename} -- The output file to create" + EOL + "-c {filename} -- A CSV file containing data to use in generating" + EOL + " LDIF data" + EOL + "-C {delimiter} -- The delimiter to use for the fields in the CSV " + EOL + " file instead of commas" + EOL + "-I -- Indicates that the first line of the CSV file" + EOL + " should be ignored as a header line" + EOL + "-d {filename} -- Write DNs of created entries to this file" + EOL + "-b {filename} -- Write bind information to this file" + EOL + " (format: dn{tab}password)" + EOL + "-i {attr} -- Specifies the login ID attribute (default is uid)" + EOL + "-L {filename} -- Write login information to this file" + EOL + " (format: loginID{tab}password)" + EOL + "-F {filename} -- Write generated filters to this file" + EOL + "-M -- Indicates that a separate filter file should be" + EOL + " used per index type." + EOL + "-T {attr:types} -- Specifies that the specified filter types are to" + EOL + " be generated for the given attribute. Filter" + EOL + " types may be eq, sub, subInitial, subAny, or " + EOL + " subFinal (ex: \"cn:eq,sub\")." + EOL + "-n {value} -- Specifies the number of characters to include in " + EOL + " substring filters (default is 3)." + EOL + "-N {value} -- Specifies the minimum number of entries that " + EOL + " should match a filter before it will be written" + EOL + " to the filter file (default is 1)." + EOL + "-X {value} -- Specifies the maximum number of entries that " + EOL + " should match a filter for it to be written to the" + EOL + " filter file (default is unlimited)." + EOL + "-s {value} -- Specifies a seed to use for the random number" + EOL + " generator (default is time-based)." + EOL + "-m {value} -- Specifies the maximum number of entries that" + EOL + " should be written to a single file (default is" + EOL + " unlimited)." + EOL + "-x {value} -- Specifies the maximum number of entries for each" + EOL + " template that should be created under each branch" + EOL + " (can be used to validate the configuration)." + EOL + "-w -- Wrap long lines" + EOL + "-S -- Skip branch entries" + EOL + "-U -- Always use UNIX line separators (\\n)" + EOL + "-D -- Operate in debug mode (print some additional " + EOL + " information about failures)." + EOL + "-H -- Display usage information" + EOL + "-V -- Display version information" + EOL ); } }