/* * 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.jobs; import java.util.Random; import java.util.UUID; import com.slamd.common.SLAMDException; import com.slamd.parameter.InvalidValueException; import com.unboundid.ldap.sdk.Entry; import com.unboundid.util.Base64; /** * This class provides a utility that can generate entries from a template. * * * @author Neil A. Wilson */ public class TemplateBasedEntryGenerator { /** * 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" }; // The entry number for the first entry to be generated. private final int firstEntryNumber; // The names of the attributes used in the template. private final String[] attributeNames; // The values of the attributes used in the template. private final String[] attributeValues; /** * Creates a new template-based entry generator from the provided template * lines. * * @param templateLines The lines that comprise the template to use to * generate the entries. * @param firstEntryNumber The entry number for the first entry to be * generated. * * @throws InvalidValueException If the provided template cannot be parsed * or there is a problem with a parameter. */ public TemplateBasedEntryGenerator(final String[] templateLines, final int firstEntryNumber) throws InvalidValueException { this.firstEntryNumber = firstEntryNumber; attributeNames = new String[templateLines.length]; attributeValues = new String[templateLines.length]; // Parse the template and set up for generating the entries. for (int i=0; i < templateLines.length; i++) { int colonPos = templateLines[i].indexOf(':'); if (colonPos < 0) { throw new InvalidValueException("No colon found in template line \"" + templateLines[i] + "\" to separate the attribute name from the " + "value."); } else if (colonPos == 0) { throw new InvalidValueException("No attribute name found in template " + "line \"" + templateLines[i] + "\"."); } else if (colonPos == (templateLines[i].length() - 1)) { throw new InvalidValueException("No attribute value found in " + "template line \"" + templateLines[i] + "\"."); } attributeNames[i] = templateLines[i].substring(0, colonPos); char nextChar = templateLines[i].charAt(colonPos+1); if (nextChar == ' ') { attributeValues[i] = templateLines[i].substring(colonPos+2).trim(); } else if (nextChar == ':') { throw new InvalidValueException("Attribute " + attributeNames[i] + " uses base64-encoding which is not supported."); } else { throw new InvalidValueException("Invalid character sequence found " + "in template line \"" + templateLines[i] + "\" -- illegal " + "character '" + templateLines[i].charAt(colonPos+1) + "' in column " + (colonPos+1)); } } } /** * Creates a randomly-generated LDAP entry to be added to the directory. * * @param random The random number generator to use for the entry. * @param entryNumber The unique entry number for the entry. * @param dn The DN to use for the entry. * * @return The randomly-generated entry, or {@code null} if all entries have * been created. * * @throws SLAMDException If a problem is encountered while trying to * generate the entry. */ public Entry createEntry(final Random random, final int entryNumber, final String dn) throws SLAMDException { Entry entry = new Entry(dn); for (int i=0; i < attributeNames.length; i++) { String value = processValue(random, attributeValues[i], entry, entryNumber, (entryNumber - firstEntryNumber)); if (value != null) { entry.addAttribute(attributeNames[i], value); } } return entry; } /** * Generates the appropriate value from the given line in the template. * * @param random The random number generator to use. * @param value The value to be processed. * @param entry The entry being generated. * @param entryNumber The unique number assigned to the entry being * created. * @param entryInSequence A counter used to determine how many entries have * been created so far, not including the current * entry. * * @return The generated value, or {@code null} if there should not be a * value for the attribute. * * @throws SLAMDException If a problem is encountered while generating the * value. */ private static String processValue(final Random random, final String value, final Entry entry, final int entryNumber, final int entryInSequence) throws SLAMDException { String v = value; boolean 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 = v.indexOf("<presence:")) >= 0) { int closePos = v.indexOf('>', pos); if (closePos > pos) { String numStr = v.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 // entry, so remove the "<presence:x>" tag and let it go on to do // the rest of the processing on this entry v = v.substring(0, pos) + v.substring(closePos+1); } else { // We have determined that this value should not be included in // the entry, so return null. return null; } } catch (NumberFormatException nfe) { return null; } } } // 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 = v.indexOf("<ifpresent:")) >= 0) { int closePos = v.indexOf('>', pos); if (closePos > pos) { int colonPos = v.indexOf(':', pos+11); if ((colonPos > 0) && (colonPos < closePos)) { // Look for a specific value to be present String attrName = v.substring(pos+11, colonPos); String matchValue = v.substring(colonPos+1, closePos); if (! entry.hasAttributeValue(attrName, matchValue)) { return null; } else { v = v.substring(0, pos) + v.substring(closePos+1); } } else { // Just look for the attribute to be present. if (! entry.hasAttribute(v.substring(pos+11, closePos))) { return null; } else { v = v.substring(0, pos) + v.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 = v.indexOf("<ifabsent:")) >= 0) { int closePos = v.indexOf('>', pos); if (closePos > pos) { int colonPos = v.indexOf(':', pos+10); if ((colonPos > 0) && (colonPos < closePos)) { // Look for a specific value to be present. String attrName = v.substring(pos+11, colonPos); String matchValue = v.substring(colonPos+1, closePos); if (entry.hasAttributeValue(attrName, matchValue)) { return null; } else { v = v.substring(0, pos) + v.substring(closePos+1); } } else { // Just look for the attribute to be present. if (entry.hasAttribute(v.substring(pos+11, closePos))) { return null; } else { v = v.substring(0, pos) + v.substring(closePos+1); } } } } while (needReprocess && v.contains("<")) { needReprocess = false; // If the value contains "<entryNumber>" then replace that with the first // name if ((pos = v.indexOf("<entrynumber>")) >= 0) { v = v.substring(0, pos) + entryNumber + v.substring(pos + 13); needReprocess = true; } if ((pos = v.indexOf("<entryNumber>")) >= 0) { v = v.substring(0, pos) + entryNumber + v.substring(pos + 13); needReprocess = true; } // If the value contains "<random:chars:characters:length>" then // generate a random string of length characters from the provided // character set. if ((pos = v.indexOf("<random:chars:")) >= 0) { // Get the set of characters to use in the resulting value. int colonPos = v.indexOf(':', pos+14); int closePos = v.indexOf('>', colonPos+1); String charSet = v.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 = v.indexOf(':', colonPos+1); if ((colonPos2 > 0) && (colonPos2 < closePos)) { int minValue = Integer.parseInt(v.substring(colonPos+1, colonPos2)); int maxValue = Integer.parseInt(v.substring(colonPos2+1, closePos)); int span = maxValue - minValue + 1; count = (random.nextInt() & 0x7FFFFFFF) % span + minValue; } else { count = Integer.parseInt(v.substring(colonPos+1, closePos)); } String randVal = generateRandomValue(random, charSet.toCharArray(), count); v = v.substring(0, pos) + randVal + v.substring(closePos+1); needReprocess = true; } // If the value contains "<random:alpha:num>" then generate a random // alphabetic value and use it. if ((pos = v.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 = v.indexOf('>', pos+14); int colonPos = v.indexOf(':', pos+14); if ((colonPos > 0) && (colonPos < closePos)) { int minValue = Integer.parseInt(v.substring(pos+14, colonPos)); int maxValue = Integer.parseInt(v.substring(colonPos+1, closePos)); int span = maxValue - minValue + 1; count = (random.nextInt() & 0x7FFFFFFF) % span + minValue; } else { count = Integer.parseInt(v.substring(pos+14, closePos)); } // Generate the new value. String randVal = generateRandomValue(random, ALPHA_CHARS, count); v = v.substring(0, pos) + randVal + v.substring(closePos + 1); 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 = v.indexOf("<random:numeric:")) >= 0) { int closePos = v.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 = v.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 = v.indexOf(':', extraColonPos+1); if ((extraColonPos2 > 0) && (extraColonPos2 < closePos)) { String lowerBoundStr = v.substring(pos+16, extraColonPos); String upperBoundStr = v.substring(extraColonPos+1, extraColonPos2); String lengthStr = v.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; } v = v.substring(0, pos) + valueStr + v.substring(closePos+1); } else { String lowerBoundStr = v.substring(pos+16, extraColonPos); String upperBoundStr = v.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; v = v.substring(0, pos) + randomValue + v.substring(closePos+1); } } else { // Get the number of characters to include in the value int numPos = pos + 16; int count = Integer.parseInt(v.substring(numPos, closePos)); String randVal = generateRandomValue(random, NUMERIC_CHARS, count); v = v.substring(0, pos) + randVal + v.substring(closePos+1); } needReprocess = true; } // If the value contains "<random:alphanumeric:num>" then generate a // random alphanumeric value and use it if ((pos = v.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 = v.indexOf('>', pos+21); int colonPos = v.indexOf(':', pos+21); if ((colonPos > 0) && (colonPos < closePos)) { int minValue = Integer.parseInt(v.substring(pos+21, colonPos)); int maxValue = Integer.parseInt(v.substring(colonPos+1, closePos)); int span = maxValue - minValue + 1; count = (random.nextInt() & 0x7FFFFFFF) % span + minValue; } else { count = Integer.parseInt(v.substring(pos+21, closePos)); } // Generate the new value. String randVal = generateRandomValue(random, ALPHANUMERIC_CHARS, count); v = v.substring(0, pos) + randVal + v.substring(closePos + 1); needReprocess = true; } // If the value contains "<random:hex:num>" then generate a random // hexadecimal value and use it if ((pos = v.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 = v.indexOf('>', pos+12); int colonPos = v.indexOf(':', pos+12); if ((colonPos > 0) && (colonPos < closePos)) { int minValue = Integer.parseInt(v.substring(pos+12, colonPos)); int maxValue = Integer.parseInt(v.substring(colonPos+1, closePos)); int span = maxValue - minValue + 1; count = (random.nextInt() & 0x7FFFFFFF) % span + minValue; } else { count = Integer.parseInt(v.substring(pos+12, closePos)); } // Generate the new value. String randVal = generateRandomValue(random, HEX_CHARS, count); v = v.substring(0, pos) + randVal + v.substring(closePos + 1); needReprocess = true; } // If the value contains "<random:base64:num>" then generate a random // base64 value and use it if ((pos = v.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 = v.indexOf('>', pos+15); int colonPos = v.indexOf(':', pos+15); if ((colonPos > 0) && (colonPos < closePos)) { int minValue = Integer.parseInt(v.substring(pos+15, colonPos)); int maxValue = Integer.parseInt(v.substring(colonPos+1, closePos)); int span = maxValue - minValue + 1; count = (random.nextInt() & 0x7FFFFFFF) % span + minValue; } else { count = Integer.parseInt(v.substring(pos+15, closePos)); } // Generate the new value. String randVal = generateRandomValue(random, BASE64_CHARS, count); switch (count % 4) { case 1: randVal += "==="; break; case 2: randVal += "=="; break; case 3: randVal += "="; break; } v = v.substring(0, pos) + randVal + v .substring(closePos + 1); needReprocess = true; } // If the value contains "<random:telephone>" then generate a random // telephone number and use it if ((pos = v.indexOf("<random:telephone>")) >= 0) { // Get the number of characters to include in the value String randVal = generateRandomValue(random, NUMERIC_CHARS, 10); v = v.substring(0, pos) + randVal.substring(0, 3) + '-' + randVal.substring(3, 6) + '-' + randVal.substring(6) + v.substring(pos + 18); 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 = v.indexOf("<random:month")) >= 0) { int closePos = v.indexOf('>', pos+13); String monthStr = MONTH_NAMES[(random.nextInt() & 0x7FFFFFFF) % 12]; // See if there is another colon that specifies the length. int colonPos = v.indexOf(':', pos+13); if ((colonPos > 0) && (colonPos < closePos)) { String lengthStr = v.substring(colonPos+1, closePos); int length = Integer.parseInt(lengthStr); if (monthStr.length() > length) { monthStr = monthStr.substring(0, length); } } v = v.substring(0, pos) + monthStr + v.substring(closePos+1); needReprocess = true; } // If the value contains "<guid>" then generate a GUID and use it if ((pos = v.indexOf("<guid>")) >= 0) { // Get the number of characters to include in the value v = v.substring(0, pos) + UUID.randomUUID().toString() + v.substring(pos + 6); needReprocess = true; } // If the value contains "<sequential>" then use the next sequential // value for that attribute if ((pos = v.indexOf("<sequential")) >= 0) { int closePos = v.indexOf('>', pos); // If a starting point was specified, then use it. If not, then use 0. int colonPos = v.indexOf(':', pos); int startingValue = 0; if ((colonPos > pos) && (colonPos < closePos)) { startingValue = Integer.parseInt(v.substring(colonPos+1, closePos)); } v = v.substring(0, pos) + (startingValue + entryInSequence) + v.substring(closePos+1); needReprocess = true; } } needReprocess = true; while (needReprocess && ((pos = v.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) && (v.charAt(pos-1) == '\\')) { boolean keepGoing = true; boolean nonEscaped = false; while (keepGoing) { v = v.substring(0, pos-1) + v.substring(pos); pos = v.indexOf('{', pos); if (pos < 0) { keepGoing = false; } else if (v.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 = v.indexOf('}', pos); if (closePos > 0) { int colonPos = v.indexOf(':', pos); int substringChars = -1; String attrName; if ((colonPos > 0) && (colonPos < closePos)) { attrName = v.substring(pos+1, colonPos); String numStr = v.substring(colonPos+1, closePos); try { substringChars = Integer.parseInt(numStr); } catch (NumberFormatException nfe) { throw new SLAMDException("Could not parse an attribute value " + "range as an integer: " + nfe, nfe); } } else { attrName = v.substring(pos+1, closePos); } String attrValue = entry.getAttributeValue(attrName); if (attrValue == null) { attrValue = ""; } if ((colonPos > 0) && (colonPos < closePos) && (substringChars > 0) && (attrValue.length() > substringChars)) { attrValue = attrValue.substring(0, substringChars); } v = v.substring(0, pos) + attrValue + v.substring(closePos+1); needReprocess = true; } } if ((pos = v.indexOf("<base64:")) >= 0) { int closePos = v.indexOf('>', pos+8); String valueToEncode = v.substring(pos+8, closePos); v = v.substring(0, pos) + Base64.encode(valueToEncode) + v.substring(closePos+1); } return v; } /** * Retrieves a string containing the specified number of randomly-chosen * characters. * * @param random The random-number generator to use. * @param charSet The character set from which to take the characters to use * in the generated value. * @param length The number of characters to include in the string. * * @return A string containing the specified number of randomly-chosen * characters. */ private static String generateRandomValue(final Random random, final char[] charSet, final int length) { StringBuilder buffer = new StringBuilder(length); for (int i=0; i < length; i++) { buffer.append(charSet[random.nextInt(charSet.length)]); } return buffer.toString(); } }