package com.hwlcn.ldap.util; import com.hwlcn.core.annotation.NotMutable; import com.hwlcn.core.annotation.ThreadSafety; import java.io.IOException; import java.io.Serializable; import java.text.ParseException; import java.util.ArrayList; import java.util.Random; import java.util.concurrent.atomic.AtomicBoolean; import static com.hwlcn.ldap.util.Debug.*; import static com.hwlcn.ldap.util.StaticUtils.*; import static com.hwlcn.ldap.util.UtilityMessages.*; /** * This class provides a method for generating a string value comprised of zero * or more components. The components may be any combination of zero or more * strings, sequential numeric ranges, and random numeric ranges. These * components should be formatted as follows: * <UL> * <LI>Strings are simply any kind of static text that will be used as-is * without any modification, except that double opening or closing square * brackets (i.e., "<CODE>[[</CODE>" or "<CODE>]]</CODE>") will be * replaced with single opening or closing square brackets to distinguish * them from the square brackets used in numeric ranges or URL * references.</LI> * <LI>Sequential numeric ranges consist of an opening square bracket, a * numeric value to be used as the lower bound for the range, a colon, a * second numeric value to be used as the upper bound for the range, an * optional '<CODE>x</CODE>' character followed by a numeric value to be * used as the increment, an optional '<CODE>%</CODE>' character followed * by a format string as allowed by the {@link java.text.DecimalFormat} * class to define how the resulting value should be formatted, and a * closing square bracket to indicate the end of the range.</LI> * <LI>Random numeric ranges consist of an opening square bracket, a * numeric value to be used as the lower bound for the range, a dash, a * second numeric value to be used as the upper bound for the range, an * optional '<CODE>%</CODE>' character followed by a format string as * allowed by the {@link java.text.DecimalFormat} class to define how the * resulting value should be formatted, and a closing square bracket to * indicate the end of the range.</LI> * <LI>Strings read from a file specified by a given URL. That file may be * contained on the local filesystem (using a URL like * "file:///tmp/mydata.txt") or read from a remote server via HTTP (using * a URL like "http://server.example.com/mydata.txt"). In either case, * the provided URL must not contain a closing square bracket character. * If this option is used, then that file must contain one value per line, * and its contents will be read into memory and values from the file will * be selected in a random order and used in place of the bracketed * URL.</LI> * <LI>Back-references that will be replaced with the same value as the * bracketed token in the specified position in the string. For example, * a component of "[ref:1]" will be replaced with the same value as used * in the first bracketed component of the value pattern. Back-references * must only reference components that have been previously defined in the * value pattern, and not those which appear after the reference.</LI> * </UL> * <BR> * It must be possible to represent all of the numeric values used in sequential * or random numeric ranges as {@code long} values. In a sequential numeric * range, if the first value is larger than the second value, then values will * be chosen in descending rather than ascending order (and if an increment is * given, then it should be positive). In addition, once the end of a * sequential range has been reached, then the value will wrap around to the * beginning of that range. * <BR> * Examples of value pattern components include: * <UL> * <LI><CODE>Hello</CODE> -- The static text "<CODE>Hello</CODE>".</LI> * <LI><CODE>[[Hello]]</CODE> -- The static text "<CODE>[Hello]</CODE>" (note * that the double square brackets were replaced with single square * brackets).</LI> * <LI><CODE>[0:1000]</CODE> -- A sequential numeric range that will iterate * in ascending sequential order from 0 to 1000. The 1002nd value that is * requested will cause the value to be wrapped around to 0 again.</LI> * <LI><CODE>[1000:0]</CODE> -- A sequential numeric range that will iterate * in descending sequential order from 1000 to 0. The 1002nd value that is * requested will cause the value to be wrapped around to 1000 again.</LI> * <LI><CODE>[0:1000x5%0000]</CODE> -- A sequential numeric range that will * iterate in ascending sequential order from 0 to 1000 in increments of * five with all values represented as four-digit numbers padded with * leading zeroes. For example, the first four values generated by this * component will be "0000", "0005", "0010", and "0015".</LI> * <LI><CODE>[0-1000]</CODE> -- A random numeric range that will choose values * at random between 0 and 1000, inclusive.</LI> * <LI><CODE>[0-1000%0000]</CODE> -- A random numeric range that will choose * values at random between 0 and 1000, inclusive, and values will be * padded with leading zeroes as necessary so that they are represented * using four digits.</LI> * <LI><CODE>[file:///tmp/mydata.txt]</CODE> -- A URL reference that will * cause randomly-selected lines from the specified local file to be used * in place of the bracketed range.</LI> * <LI><CODE>[http://server.example.com/tmp/mydata.txt]</CODE> -- A URL * reference that will cause randomly-selected lines from the specified * remote HTTP-accessible file to be used in place of the bracketed * range.</LI> * </UL> * <BR> * Examples of full value pattern strings include: * <UL> * <LI><CODE>dc=example,dc=com</CODE> -- A value pattern containing only * static text and no numeric components.</LI> * <LI><CODE>[1000:9999]</CODE> -- A value pattern containing only a numeric * component that will choose numbers in sequential order from 1000 to * 9999.</LI> * <LI><CODE>(uid=user.[1-1000000])</CODE> -- A value pattern that combines * the static text "<CODE>(uid=user.</CODE>" with a value chosen randomly * between one and one million, and another static text string of * "<CODE>)</CODE>".</LI> * <LI><CODE>uid=user.[1-1000000],ou=org[1-10],dc=example,dc=com</CODE> -- A * value pattern containing two numeric components interspersed between * three static text components.</LI> * <LI><CODE>uid=user.[1-1000000],ou=org[ref:1],dc=example,dc=com</CODE> -- A * value pattern in which the organization number will be the same as the * randomly-selected user number.</LI> * </UL> */ @NotMutable() @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) public final class ValuePattern implements Serializable { private static final long serialVersionUID = 4502778464751705304L; private final boolean hasBackReference; private final String pattern; private final ThreadLocal<ArrayList<String>> refLists; private final ThreadLocal<StringBuilder> buffers; private final ValuePatternComponent[] components; public ValuePattern(final String s) throws ParseException { this(s, null); } public ValuePattern(final String s, final Long r) throws ParseException { Validator.ensureNotNull(s); pattern = s; refLists = new ThreadLocal<ArrayList<String>>(); buffers = new ThreadLocal<StringBuilder>(); final AtomicBoolean hasRef = new AtomicBoolean(false); final Random random; if (r == null) { random = new Random(); } else { random = new Random(r); } final ArrayList<ValuePatternComponent> l = new ArrayList<ValuePatternComponent>(3); parse(s, 0, l, random, hasRef); hasBackReference = hasRef.get(); if (hasBackReference) { int availableReferences = 0; for (final ValuePatternComponent c : l) { if (c instanceof BackReferenceValuePatternComponent) { final BackReferenceValuePatternComponent brvpc = (BackReferenceValuePatternComponent) c; if (brvpc.getIndex() > availableReferences) { throw new ParseException( ERR_REF_VALUE_PATTERN_INVALID_INDEX.get(brvpc.getIndex()), 0); } } if (c.supportsBackReference()) { availableReferences++; } } } components = new ValuePatternComponent[l.size()]; l.toArray(components); } private static void parse(final String s, final int o, final ArrayList<ValuePatternComponent> l, final Random r, final AtomicBoolean ref) throws ParseException { int pos = s.indexOf("[["); if (pos >= 0) { if (pos > 0) { parse(s.substring(0, pos), o, l, r, ref); } l.add(new StringValuePatternComponent("[")); if (pos < (s.length() - 2)) { parse(s.substring(pos+2), (o+pos+2), l, r, ref); } return; } pos = s.indexOf("]]"); if (pos >= 0) { if (pos > 0) { parse(s.substring(0, pos), o, l, r, ref); } l.add(new StringValuePatternComponent("]")); if (pos < (s.length() - 2)) { parse(s.substring(pos+2), (o+pos+2), l, r, ref); } return; } pos = s.indexOf('['); if (pos >= 0) { final int closePos = s.indexOf(']'); if (closePos < 0) { throw new ParseException( ERR_VALUE_PATTERN_UNMATCHED_OPEN.get(o+pos), (o+pos)); } else if (closePos < pos) { throw new ParseException( ERR_VALUE_PATTERN_UNMATCHED_CLOSE.get(o+closePos), (o+closePos)); } if (pos > 0) { l.add(new StringValuePatternComponent(s.substring(0, pos))); } final String bracketedToken = s.substring(pos+1, closePos); if (bracketedToken.startsWith("file:")) { final String path = bracketedToken.substring(5); try { l.add(new FileValuePatternComponent(path, r.nextLong())); } catch (IOException ioe) { debugException(ioe); throw new ParseException(ERR_FILE_VALUE_PATTERN_NOT_USABLE.get( path, getExceptionMessage(ioe)), o+pos); } } else if (bracketedToken.startsWith("http://")) { try { l.add(new HTTPValuePatternComponent(bracketedToken, r.nextLong())); } catch (IOException ioe) { debugException(ioe); throw new ParseException(ERR_HTTP_VALUE_PATTERN_NOT_USABLE.get( bracketedToken, getExceptionMessage(ioe)), o+pos); } } else if (bracketedToken.startsWith("ref:")) { ref.set(true); final String valueStr = bracketedToken.substring(4); try { final int index = Integer.parseInt(valueStr); if (index == 0) { throw new ParseException(ERR_REF_VALUE_PATTERN_ZERO_INDEX.get(), (o+pos+4)); } else if (index < 0) { throw new ParseException( ERR_REF_VALUE_PATTERN_NOT_VALID.get(valueStr), (o+pos+4)); } else { l.add(new BackReferenceValuePatternComponent(index)); } } catch (final NumberFormatException nfe) { debugException(nfe); throw new ParseException( ERR_REF_VALUE_PATTERN_NOT_VALID.get(valueStr), (o+pos+4)); } } else { l.add(parseNumericComponent(s.substring(pos+1, closePos), (o+pos+1), r)); } if (closePos < (s.length() - 1)) { parse(s.substring(closePos+1), (o+closePos+1), l, r, ref); } return; } pos = s.indexOf(']'); if (pos >= 0) { throw new ParseException( ERR_VALUE_PATTERN_UNMATCHED_CLOSE.get(o+pos), (o+pos)); } l.add(new StringValuePatternComponent(s)); } private static ValuePatternComponent parseNumericComponent(final String s, final int o, final Random r) throws ParseException { boolean delimiterFound = false; boolean sequential = false; int pos = 0; long lowerBound = 0L; lowerBoundLoop: for ( ; pos < s.length(); pos++) { switch (s.charAt(pos)) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': break; case '-': if (pos == 0) { break; } else { delimiterFound = true; sequential = false; try { lowerBound = Long.parseLong(s.substring(0, pos)); } catch (NumberFormatException nfe) { Debug.debugException(nfe); throw new ParseException( ERR_VALUE_PATTERN_VALUE_NOT_LONG.get((o-1), Long.MIN_VALUE, Long.MAX_VALUE), (o-1)); } pos++; break lowerBoundLoop; } case ':': delimiterFound = true; sequential = true; if (pos == 0) { throw new ParseException( ERR_VALUE_PATTERN_EMPTY_LOWER_BOUND.get(o-1), (o-1)); } else { try { lowerBound = Long.parseLong(s.substring(0, pos)); } catch (NumberFormatException nfe) { Debug.debugException(nfe); throw new ParseException( ERR_VALUE_PATTERN_VALUE_NOT_LONG.get((o-1), Long.MIN_VALUE, Long.MAX_VALUE), (o-1)); } } pos++; break lowerBoundLoop; default: throw new ParseException( ERR_VALUE_PATTERN_INVALID_CHARACTER.get(s.charAt(pos), (o+pos)), (o+pos)); } } if (! delimiterFound) { throw new ParseException(ERR_VALUE_PATTERN_NO_DELIMITER.get(o-1), (o-1)); } boolean hasIncrement = false; int startPos = pos; long upperBound = lowerBound; long increment = 1L; String formatString = null; delimiterFound = false; upperBoundLoop: for ( ; pos < s.length(); pos++) { switch (s.charAt(pos)) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': break; case '-': if (pos == startPos) { break; } else { throw new ParseException( ERR_VALUE_PATTERN_INVALID_CHARACTER.get('-', (o+pos)), (o+pos)); } case 'x': delimiterFound = true; hasIncrement = true; if (pos == startPos) { throw new ParseException( ERR_VALUE_PATTERN_EMPTY_UPPER_BOUND.get(o-1), (o-1)); } else { try { upperBound = Long.parseLong(s.substring(startPos, pos)); } catch (NumberFormatException nfe) { Debug.debugException(nfe); throw new ParseException( ERR_VALUE_PATTERN_VALUE_NOT_LONG.get((o-1), Long.MIN_VALUE, Long.MAX_VALUE), (o-1)); } } pos++; break upperBoundLoop; case '%': delimiterFound = true; hasIncrement = false; if (pos == startPos) { throw new ParseException( ERR_VALUE_PATTERN_EMPTY_UPPER_BOUND.get(o-1), (o-1)); } else { try { upperBound = Long.parseLong(s.substring(startPos, pos)); } catch (NumberFormatException nfe) { Debug.debugException(nfe); throw new ParseException( ERR_VALUE_PATTERN_VALUE_NOT_LONG.get((o-1), Long.MIN_VALUE, Long.MAX_VALUE), (o-1)); } } pos++; break upperBoundLoop; default: throw new ParseException( ERR_VALUE_PATTERN_INVALID_CHARACTER.get(s.charAt(pos), (o+pos)), (o+pos)); } } if (! delimiterFound) { if (pos == startPos) { throw new ParseException( ERR_VALUE_PATTERN_EMPTY_UPPER_BOUND.get(o-1), (o-1)); } try { upperBound = Long.parseLong(s.substring(startPos, pos)); } catch (NumberFormatException nfe) { Debug.debugException(nfe); throw new ParseException( ERR_VALUE_PATTERN_VALUE_NOT_LONG.get((o-1), Long.MIN_VALUE, Long.MAX_VALUE), (o-1)); } if (sequential) { return new SequentialValuePatternComponent(lowerBound, upperBound, 1, null); } else { return new RandomValuePatternComponent(lowerBound, upperBound, r.nextLong(), null); } } if (hasIncrement) { delimiterFound = false; startPos = pos; incrementLoop: for ( ; pos < s.length(); pos++) { switch (s.charAt(pos)) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': break; case '-': if (pos == startPos) { break; } else { throw new ParseException( ERR_VALUE_PATTERN_INVALID_CHARACTER.get('-', (o+pos)), (o+pos)); } case '%': delimiterFound = true; if (pos == startPos) { throw new ParseException( ERR_VALUE_PATTERN_EMPTY_INCREMENT.get(o-1), (o-1)); } else if (pos == (s.length() - 1)) { throw new ParseException( ERR_VALUE_PATTERN_EMPTY_FORMAT.get(o-1), (o-1)); } else { try { increment = Long.parseLong(s.substring(startPos, pos)); } catch (NumberFormatException nfe) { Debug.debugException(nfe); throw new ParseException( ERR_VALUE_PATTERN_VALUE_NOT_LONG.get((o-1), Long.MIN_VALUE, Long.MAX_VALUE), (o-1)); } formatString = s.substring(pos+1); } break incrementLoop; default: throw new ParseException( ERR_VALUE_PATTERN_INVALID_CHARACTER.get(s.charAt(pos), (o+pos)), (o+pos)); } } if (! delimiterFound) { if (pos == startPos) { throw new ParseException( ERR_VALUE_PATTERN_EMPTY_INCREMENT.get(o-1), (o-1)); } try { increment = Long.parseLong(s.substring(startPos, pos)); } catch (NumberFormatException nfe) { Debug.debugException(nfe); throw new ParseException( ERR_VALUE_PATTERN_VALUE_NOT_LONG.get((o-1), Long.MIN_VALUE, Long.MAX_VALUE), (o-1)); } } } else { formatString = s.substring(pos); if (formatString.length() == 0) { throw new ParseException( ERR_VALUE_PATTERN_EMPTY_FORMAT.get(o-1), (o-1)); } } if (sequential) { return new SequentialValuePatternComponent(lowerBound, upperBound, increment, formatString); } else { return new RandomValuePatternComponent(lowerBound, upperBound, r.nextLong(), formatString); } } public String nextValue() { StringBuilder buffer = buffers.get(); if (buffer == null) { buffer = new StringBuilder(); buffers.set(buffer); } else { buffer.setLength(0); } ArrayList<String> refList = refLists.get(); if (hasBackReference) { if (refList == null) { refList = new ArrayList<String>(10); refLists.set(refList); } else { refList.clear(); } } for (final ValuePatternComponent c : components) { if (hasBackReference) { if (c instanceof BackReferenceValuePatternComponent) { final BackReferenceValuePatternComponent brvpc = (BackReferenceValuePatternComponent) c; final String value = refList.get(brvpc.getIndex() - 1); buffer.append(value); refList.add(value); } else if (c.supportsBackReference()) { final int startPos = buffer.length(); c.append(buffer); refList.add(buffer.substring(startPos)); } else { c.append(buffer); } } else { c.append(buffer); } } return buffer.toString(); } @Override() public String toString() { return pattern; } }