/*
* 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.scripting.ldap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;
import java.util.StringTokenizer;
import com.unboundid.util.Base64;
/**
* This class handles the work of generating LDAP entries based on information
* in a template file. The template files may contain various kinds of tokens
* that allow the entries generated to be highly customizable. The tokens that
* are supported are:
*
* <UL>
* <LI><presence:<I>percent</I>> -- Indicates that the associated
* attribute/value should be present in approximately <I>percent</I>
* percent of the entries generated.</LI>
* <LI><parentdn> -- Will be replaced by the DN of the parent
* entry.</LI>
* <LI><random:alpha:<I>length</I>> -- Will be replaced by a random
* string of <I>length</I> alphabetic characters.</LI>
* <LI><random:numeric:<I>length</I>> -- Will be replaced by a random
* string of <I>length</I> numeric digits.</LI>
* <LI><random:alphanumeric:<I>length</I>> -- Will be replaced by a
* random string of <I>length</I> alphanumeric characters.</LI>
* <LI><random:hex:<I>length</I>> -- Will be replaced by a random
* string of <I>length</I> hexadecimal digits.</LI>
* <LI><random:base64:<I>length</I>> -- Will be replaced by a random
* string of <I>length</I> base64-encoded characters.</LI>
* <LI><random:telephone> -- Will be replaced with a randomly-generated
* telephone number in the format 123-456-7890.</LI>
* <LI><sequential> -- Will be replaced by a sequentially-increasing
* numeric value with an initial value of zero.</LI>
* <LI><sequential:<I>first-value</I>> -- Will be replaced by a
* sequentially-increasing numeric value with an initial value of
* <I>first-value</I>.</LI>
* <LI><guid> -- Will be replaced by a number in GUID (globally-unique
* identifier) format. The value generated is not guaranteed to be
* globally unique, but will be unique among any entries created by this
* generator.</LI>
* <LI><list:<I>value1</I>,<I>value2</I>,...,<I>valueN</I>> --
* Will be replaced by a value chosen at random from the list of provided
* values. Each value will have an equal chance of being chosen.</LI>
* <LI><list:<I>value1</I>:<I>weight1</I>,<I>value2</I>:<I>weight2</I>,...,
* <I>valueN</I>:<I>weightN</I>> -- Will be replaced by a value
* chosen at random from the list of provided values. The weight
* associated with each value determines the likelihood of that value
* being chosen.</LI>
* <LI>{<I>attr</I>} -- Will be replaced with the value of the attribute
* <I>attr</I>. Note that the specified attribute must be assigned a
* value in the template file before any line that references its value in
* this manner.</LI>
* <LI>{<I>attr</I>:<I>length</I>} -- Will be replaced with the first
* <I>length</I> characters from the value of the attribute <I>attr</I>.
* Note that the specified attribute must be assigned a value in the
* template file before any line that references its value in this
* manner.</LI>
* </UL>
*
*
* @author Neil A. Wilson
*/
public class LDAPEntryGenerator
{
/**
* The end of line character that should be used on the current platform.
*/
public static final String EOL = System.getProperty("line.separator");
/**
* 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();
// A map containing the templates defined in the template file.
private HashMap<String,LDAPEntryTemplate> templateHash;
// A map containing value lists
private HashMap<String,AttributeValueList> valueLists;
// The random number generator being used.
private Random random;
// 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).
private String guidBase;
/**
* Creates a new instance of the LDIF generator that parses the command-line
* parameters and coordinates the LDIF creation.
*/
public LDAPEntryGenerator()
{
// Set the values of the instance variables
random = new Random();
templateHash = new HashMap<String,LDAPEntryTemplate>();
valueLists = new HashMap<String,AttributeValueList>();
guidBase = generateRandomValue(HEX_CHARS, 12);
}
/**
* Adds the specified entry template for use with this entry generator.
*
* @param template The template to use for this entry generator.
*/
public void addTemplate(LDAPEntryTemplate template)
{
templateHash.put(template.getName(), template);
}
/**
* Reads the template file and extracts the branch and template definitions
* from it.
*
* @param templateFileLines The lines from the template file that will be
* used for the template generation.
*/
public void parseTemplateFile(String[] templateFileLines)
{
// The lines contained in the current definition
ArrayList<String> currentDefinition = new ArrayList<String>();
// The name of the branch or template we are working on
String name = null;
// 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 when reading the template
// information and not when actually creating the entries.
int lineNumber = 0;
while (lineNumber < templateFileLines.length)
{
String line = templateFileLines[lineNumber];
if ((line.length() > 0) && (lineNumber < (templateFileLines.length-1)))
{
currentDefinition.add(line);
if (line.toLowerCase().startsWith("template: "))
{
name = line.substring(10);
}
}
else
{
if ((line.length() > 0) && (lineNumber >= (templateFileLines.length-1)))
{
currentDefinition.add(line);
}
// Only try to process it if there is something to process
if (! currentDefinition.isEmpty())
{
// 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.
LDAPEntryTemplate template = new LDAPEntryTemplate(name, "cn");
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("rdnattr: "))
{
template.rdnAttribute = line.substring(9);
}
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();
name = null;
}
}
lineNumber++;
}
}
/**
* 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 entry.
* @param templateName The name of the template to use to generate the
* entry.
*
* @return The LDAP entry that was created, or a null entry if a problem
* occurred.
*/
public LDAPEntryVariable createTemplateEntry(String parentDN,
String templateName)
{
LDAPEntryTemplate template = templateHash.get(templateName.toLowerCase());
if (template == null)
{
return new LDAPEntryVariable();
}
String rdnAttr = template.getRDNAttribute();
String[][] attrComponents = template.getAttributeComponents();
LDAPEntryVariable entry = new LDAPEntryVariable();
// 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 rdnValue = null;
// Iterate through all the attributes and do any processing that needs to be
// done.
for (int i=0; i < attrComponents.length; i++)
{
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 = (Math.abs(random.nextInt()) % 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) {}
}
}
while (needReprocess && (value.indexOf('<') >= 0))
{
needReprocess = false;
// 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);
}
// If the value contains "<random:alpha:num>" then generate a random
// alphabetic value and use it.
if ((pos = value.indexOf("<random:alpha:")) >= 0)
{
// Get the number of characters to include in the value
int numPos = pos + 14;
int count = Integer.parseInt(
value.substring(numPos, value.indexOf('>', numPos)));
String randVal = generateRandomValue(ALPHA_CHARS, count);
value = value.substring(0, pos) + randVal +
value.substring(value.indexOf('>', numPos) + 1);
needReprocess = true;
}
// If the value contains "<random:numeric:num>" then generate a random
// numeric value and use it
if ((pos = value.indexOf("<random:numeric:")) >= 0)
{
// Get the number of characters to include in the value
int numPos = pos + 16;
int count = Integer.parseInt(
value.substring(numPos, value.indexOf('>', numPos)));
String randVal = generateRandomValue(NUMERIC_CHARS, count);
value = value.substring(0, pos) + randVal +
value.substring(value.indexOf('>', numPos) + 1);
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)
{
// Get the number of characters to include in the value
int numPos = pos + 21;
int count = Integer.parseInt(
value.substring(numPos,value.indexOf('>', numPos)));
String randVal = generateRandomValue(ALPHANUMERIC_CHARS, count);
value = value.substring(0, pos) + randVal +
value.substring(value.indexOf('>', numPos) + 1);
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)
{
// Get the number of characters to include in the value
int numPos = pos + 12;
int count = Integer.parseInt(
value.substring(numPos, value.indexOf('>', numPos)));
String randVal = generateRandomValue(HEX_CHARS, count);
value = value.substring(0, pos) + randVal +
value.substring(value.indexOf('>', numPos) + 1);
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)
{
// Get the number of characters to include in the value
int numPos = pos + 15;
int count = Integer.parseInt(
value.substring(numPos, value.indexOf('>', numPos)));
String randVal = generateRandomValue(BASE64_CHARS, count);
switch (count % 4)
{
case 1: randVal += "===";
break;
case 2: randVal += "==";
break;
case 3: randVal += "=";
break;
}
// Convert the base64-encoded data to a byte array and then convert
// the binary value back to a string. It may not be readable, but it
// should be enough to get the binary data in the entry.
try
{
byte[] binaryValue = Base64.decode(randVal);
randVal = new String(binaryValue);
} catch (Exception e) {}
value = value.substring(0, pos) + randVal +
value.substring(value.indexOf('>', numPos) + 1);
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
String randVal = generateRandomValue(NUMERIC_CHARS, 10);
value = value.substring(0, pos) + randVal.substring(0, 3) + '-' +
randVal.substring(3, 6) + '-' + randVal.substring(6) +
value.substring(pos + 18);
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
value = value.substring(0, pos) + generateGUID() +
value.substring(pos + 6);
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
int counterValue = template.getCounterValue(attrComponents[i][3]);
if (counterValue == Integer.MIN_VALUE)
{
int colonPos = value.indexOf(':', pos);
int firstValue = 0;
if ((colonPos > pos) && (colonPos < closePos))
{
firstValue = Integer.parseInt(value.substring(colonPos+1,
closePos));
}
SequentialValueCounter c = new SequentialValueCounter(firstValue);
template.addCounter(attrComponents[i][3], c);
counterValue = c.getNext();
}
value = value.substring(0, pos) + counterValue +
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
AttributeValueList avl = valueLists.get(attrComponents[i][3]);
if (avl == null)
{
avl = new AttributeValueList();
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));
avl.addValue(val, weight);
}
else
{
avl.addValue(listValue);
}
}
avl.completeInitialization();
valueLists.put(attrComponents[i][3], avl);
}
// Get the list value and use it
value = value.substring(0, pos) + avl.nextValue() +
value.substring(closePos+1);
needReprocess = true;
}
}
needReprocess = true;
while (needReprocess && ((pos = value.indexOf('{')) >= 0))
{
needReprocess = false;
// 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) {}
}
else
{
attrName = value.substring(pos+1, closePos).toLowerCase();
}
String attrValue = entry.getAttributeValue(attrName);
if (attrValue == null)
{
attrValue = "";
}
if ((colonPos > 0) && (colonPos < closePos) && (substringChars > 0) &&
(attrValue.length() > substringChars))
{
attrValue = attrValue.substring(0, substringChars);
}
value = value.substring(0, pos) + attrValue +
value.substring(closePos+1);
needReprocess = true;
}
}
entry.addAttribute(new LDAPAttributeVariable(attrComponents[i][0],
value));
if ((rdnValue == null) && (attrComponents[i][3].equals(rdnAttr)))
{
rdnValue = value;
}
}
// All of the values have been processed. Now try to calculate the DN for
// the entry.
if (rdnValue == null)
{
System.err.println("Returning NULL entry -- no RDN value");
return null;
}
// Create the DN for the entry and return it.
String entryDN = rdnAttr + '=' + rdnValue + ',' + parentDN;
entry.setDN(entryDN);
return entry;
}
/**
* 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.
*
* @return The random value that was generated.
*/
public String generateRandomValue(char[] charSet, int length)
{
char[] retArray = new char[length];
for (int i=0; i < retArray.length; i++)
{
retArray[i] = charSet[Math.abs(random.nextInt()) % charSet.length];
}
return new String(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.
*
* @return The GUID that was generated.
*/
public String generateGUID()
{
String timeStr = Long.toHexString(System.currentTimeMillis());
String tmpStr = timeStr +
generateRandomValue(HEX_CHARS, 20-timeStr.length());
return tmpStr.substring(0, 8) + '-' + tmpStr.substring(8, 12) + '-' +
tmpStr.substring(12,16) + '-' + tmpStr.substring(16) + '-' +
guidBase;
}
}