001/*
002 * Copyright 2010-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2010-2016 UnboundID Corp.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.ldap.sdk.examples;
022
023
024
025import java.io.IOException;
026import java.io.OutputStream;
027import java.io.Serializable;
028import java.text.ParseException;
029import java.util.LinkedHashMap;
030import java.util.LinkedHashSet;
031import java.util.List;
032import java.util.Random;
033import java.util.concurrent.CyclicBarrier;
034import java.util.concurrent.atomic.AtomicBoolean;
035import java.util.concurrent.atomic.AtomicLong;
036
037import com.unboundid.ldap.sdk.LDAPConnection;
038import com.unboundid.ldap.sdk.LDAPConnectionOptions;
039import com.unboundid.ldap.sdk.LDAPException;
040import com.unboundid.ldap.sdk.ResultCode;
041import com.unboundid.ldap.sdk.SearchScope;
042import com.unboundid.ldap.sdk.Version;
043import com.unboundid.util.ColumnFormatter;
044import com.unboundid.util.FixedRateBarrier;
045import com.unboundid.util.FormattableColumn;
046import com.unboundid.util.HorizontalAlignment;
047import com.unboundid.util.LDAPCommandLineTool;
048import com.unboundid.util.ObjectPair;
049import com.unboundid.util.OutputFormat;
050import com.unboundid.util.RateAdjustor;
051import com.unboundid.util.ResultCodeCounter;
052import com.unboundid.util.ThreadSafety;
053import com.unboundid.util.ThreadSafetyLevel;
054import com.unboundid.util.ValuePattern;
055import com.unboundid.util.WakeableSleeper;
056import com.unboundid.util.args.ArgumentException;
057import com.unboundid.util.args.ArgumentParser;
058import com.unboundid.util.args.BooleanArgument;
059import com.unboundid.util.args.FileArgument;
060import com.unboundid.util.args.IntegerArgument;
061import com.unboundid.util.args.ScopeArgument;
062import com.unboundid.util.args.StringArgument;
063
064import static com.unboundid.util.Debug.*;
065import static com.unboundid.util.StaticUtils.*;
066
067
068
069/**
070 * This class provides a tool that can be used to search an LDAP directory
071 * server repeatedly using multiple threads, and then modify each entry
072 * returned by that server.  It can help provide an estimate of the combined
073 * search and modify performance that a directory server is able to achieve.
074 * Either or both of the base DN and the search filter may be a value pattern as
075 * described in the {@link ValuePattern} class.  This makes it possible to
076 * search over a range of entries rather than repeatedly performing searches
077 * with the same base DN and filter.
078 * <BR><BR>
079 * Some of the APIs demonstrated by this example include:
080 * <UL>
081 *   <LI>Argument Parsing (from the {@code com.unboundid.util.args}
082 *       package)</LI>
083 *   <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util}
084 *       package)</LI>
085 *   <LI>LDAP Communication (from the {@code com.unboundid.ldap.sdk}
086 *       package)</LI>
087 *   <LI>Value Patterns (from the {@code com.unboundid.util} package)</LI>
088 * </UL>
089 * <BR><BR>
090 * All of the necessary information is provided using command line arguments.
091 * Supported arguments include those allowed by the {@link LDAPCommandLineTool}
092 * class, as well as the following additional arguments:
093 * <UL>
094 *   <LI>"-b {baseDN}" or "--baseDN {baseDN}" -- specifies the base DN to use
095 *       for the searches.  This must be provided.  It may be a simple DN, or it
096 *       may be a value pattern to express a range of base DNs.</LI>
097 *   <LI>"-s {scope}" or "--scope {scope}" -- specifies the scope to use for the
098 *       search.  The scope value should be one of "base", "one", "sub", or
099 *       "subord".  If this isn't specified, then a scope of "sub" will be
100 *       used.</LI>
101 *   <LI>"-f {filter}" or "--filter {filter}" -- specifies the filter to use for
102 *       the searches.  This must be provided.  It may be a simple filter, or it
103 *       may be a value pattern to express a range of filters.</LI>
104 *   <LI>"-A {name}" or "--attribute {name}" -- specifies the name of an
105 *       attribute that should be included in entries returned from the server.
106 *       If this is not provided, then all user attributes will be requested.
107 *       This may include special tokens that the server may interpret, like
108 *       "1.1" to indicate that no attributes should be returned, "*", for all
109 *       user attributes, or "+" for all operational attributes.  Multiple
110 *       attributes may be requested with multiple instances of this
111 *       argument.</LI>
112 *   <LI>"-m {name}" or "--modifyAttribute {name}" -- specifies the name of the
113 *       attribute to modify.  Multiple attributes may be modified by providing
114 *       multiple instances of this argument.  At least one attribute must be
115 *       provided.</LI>
116 *   <LI>"-l {num}" or "--valueLength {num}" -- specifies the length in bytes to
117 *       use for the values of the target attributes to modify.  If this is not
118 *       provided, then a default length of 10 bytes will be used.</LI>
119 *   <LI>"-C {chars}" or "--characterSet {chars}" -- specifies the set of
120 *       characters that will be used to generate the values to use for the
121 *       target attributes to modify.  It should only include ASCII characters.
122 *       Values will be generated from randomly-selected characters from this
123 *       set.  If this is not provided, then a default set of lowercase
124 *       alphabetic characters will be used.</LI>
125 *   <LI>"-t {num}" or "--numThreads {num}" -- specifies the number of
126 *       concurrent threads to use when performing the searches.  If this is not
127 *       provided, then a default of one thread will be used.</LI>
128 *   <LI>"-i {sec}" or "--intervalDuration {sec}" -- specifies the length of
129 *       time in seconds between lines out output.  If this is not provided,
130 *       then a default interval duration of five seconds will be used.</LI>
131 *   <LI>"-I {num}" or "--numIntervals {num}" -- specifies the maximum number of
132 *       intervals for which to run.  If this is not provided, then it will
133 *       run forever.</LI>
134 *   <LI>"--iterationsBeforeReconnect {num}" -- specifies the number of search
135 *       iterations that should be performed on a connection before that
136 *       connection is closed and replaced with a newly-established (and
137 *       authenticated, if appropriate) connection.</LI>
138 *   <LI>"-r {ops-per-second}" or "--ratePerSecond {ops-per-second}" --
139 *       specifies the target number of operations to perform per second.  Each
140 *       search and modify operation will be counted separately for this
141 *       purpose, so if a value of 1 is specified and a search returns two
142 *       entries, then a total of three seconds will be required (one for the
143 *       search and one for the modify for each entry).  It is still necessary
144 *       to specify a sufficient number of threads for achieving this rate.  If
145 *       this option is not provided, then the tool will run at the maximum rate
146 *       for the specified number of threads.</LI>
147 *   <LI>"--variableRateData {path}" -- specifies the path to a file containing
148 *       information needed to allow the tool to vary the target rate over time.
149 *       If this option is not provided, then the tool will either use a fixed
150 *       target rate as specified by the "--ratePerSecond" argument, or it will
151 *       run at the maximum rate.</LI>
152 *   <LI>"--generateSampleRateFile {path}" -- specifies the path to a file to
153 *       which sample data will be written illustrating and describing the
154 *       format of the file expected to be used in conjunction with the
155 *       "--variableRateData" argument.</LI>
156 *   <LI>"--warmUpIntervals {num}" -- specifies the number of intervals to
157 *       complete before beginning overall statistics collection.</LI>
158 *   <LI>"--timestampFormat {format}" -- specifies the format to use for
159 *       timestamps included before each output line.  The format may be one of
160 *       "none" (for no timestamps), "with-date" (to include both the date and
161 *       the time), or "without-date" (to include only time time).</LI>
162 *   <LI>"-Y {authzID}" or "--proxyAs {authzID}" -- Use the proxied
163 *       authorization v2 control to request that the operations be processed
164 *       using an alternate authorization identity.  In this case, the bind DN
165 *       should be that of a user that has permission to use this control.  The
166 *       authorization identity may be a value pattern.</LI>
167 *   <LI>"--suppressErrorResultCodes" -- Indicates that information about the
168 *       result codes for failed operations should not be displayed.</LI>
169 *   <LI>"-c" or "--csv" -- Generate output in CSV format rather than a
170 *       display-friendly format.</LI>
171 * </UL>
172 */
173@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
174public final class SearchAndModRate
175       extends LDAPCommandLineTool
176       implements Serializable
177{
178  /**
179   * The serial version UID for this serializable class.
180   */
181  private static final long serialVersionUID = 3242469381380526294L;
182
183
184
185  // Indicates whether a request has been made to stop running.
186  private final AtomicBoolean stopRequested;
187
188  // The argument used to indicate whether to generate output in CSV format.
189  private BooleanArgument csvFormat;
190
191  // The argument used to indicate whether to suppress information about error
192  // result codes.
193  private BooleanArgument suppressErrors;
194
195  // The argument used to specify the collection interval.
196  private IntegerArgument collectionInterval;
197
198  // The argument used to specify the number of search and modify iterations on
199  // a connection before it is closed and re-established.
200  private IntegerArgument iterationsBeforeReconnect;
201
202  // The argument used to specify the number of intervals.
203  private IntegerArgument numIntervals;
204
205  // The argument used to specify the number of threads.
206  private IntegerArgument numThreads;
207
208  // The argument used to specify the seed to use for the random number
209  // generator.
210  private IntegerArgument randomSeed;
211
212  // The target rate of operations per second.
213  private IntegerArgument ratePerSecond;
214
215  // The argument used to specify a variable rate file.
216  private FileArgument sampleRateFile;
217
218  // The argument used to specify a variable rate file.
219  private FileArgument variableRateData;
220
221  // The argument used to specify the length of the values to generate.
222  private IntegerArgument valueLength;
223
224  // The number of warm-up intervals to perform.
225  private IntegerArgument warmUpIntervals;
226
227  // The argument used to specify the scope for the searches.
228  private ScopeArgument scopeArg;
229
230  // The argument used to specify the base DNs for the searches.
231  private StringArgument baseDN;
232
233  // The argument used to specify the set of characters to use when generating
234  // values.
235  private StringArgument characterSet;
236
237  // The argument used to specify the filters for the searches.
238  private StringArgument filter;
239
240  // The argument used to specify the attributes to modify.
241  private StringArgument modifyAttributes;
242
243  // The argument used to specify the proxied authorization identity.
244  private StringArgument proxyAs;
245
246  // The argument used to specify the attributes to return.
247  private StringArgument returnAttributes;
248
249  // The argument used to specify the timestamp format.
250  private StringArgument timestampFormat;
251
252  // The thread currently being used to run the searchrate tool.
253  private volatile Thread runningThread;
254
255  // A wakeable sleeper that will be used to sleep between reporting intervals.
256  private final WakeableSleeper sleeper;
257
258
259
260  /**
261   * Parse the provided command line arguments and make the appropriate set of
262   * changes.
263   *
264   * @param  args  The command line arguments provided to this program.
265   */
266  public static void main(final String[] args)
267  {
268    final ResultCode resultCode = main(args, System.out, System.err);
269    if (resultCode != ResultCode.SUCCESS)
270    {
271      System.exit(resultCode.intValue());
272    }
273  }
274
275
276
277  /**
278   * Parse the provided command line arguments and make the appropriate set of
279   * changes.
280   *
281   * @param  args       The command line arguments provided to this program.
282   * @param  outStream  The output stream to which standard out should be
283   *                    written.  It may be {@code null} if output should be
284   *                    suppressed.
285   * @param  errStream  The output stream to which standard error should be
286   *                    written.  It may be {@code null} if error messages
287   *                    should be suppressed.
288   *
289   * @return  A result code indicating whether the processing was successful.
290   */
291  public static ResultCode main(final String[] args,
292                                final OutputStream outStream,
293                                final OutputStream errStream)
294  {
295    final SearchAndModRate searchAndModRate =
296         new SearchAndModRate(outStream, errStream);
297    return searchAndModRate.runTool(args);
298  }
299
300
301
302  /**
303   * Creates a new instance of this tool.
304   *
305   * @param  outStream  The output stream to which standard out should be
306   *                    written.  It may be {@code null} if output should be
307   *                    suppressed.
308   * @param  errStream  The output stream to which standard error should be
309   *                    written.  It may be {@code null} if error messages
310   *                    should be suppressed.
311   */
312  public SearchAndModRate(final OutputStream outStream,
313                          final OutputStream errStream)
314  {
315    super(outStream, errStream);
316
317    stopRequested = new AtomicBoolean(false);
318    sleeper = new WakeableSleeper();
319  }
320
321
322
323  /**
324   * Retrieves the name for this tool.
325   *
326   * @return  The name for this tool.
327   */
328  @Override()
329  public String getToolName()
330  {
331    return "search-and-mod-rate";
332  }
333
334
335
336  /**
337   * Retrieves the description for this tool.
338   *
339   * @return  The description for this tool.
340   */
341  @Override()
342  public String getToolDescription()
343  {
344    return "Perform repeated searches against an " +
345           "LDAP directory server and modify each entry returned.";
346  }
347
348
349
350  /**
351   * Retrieves the version string for this tool.
352   *
353   * @return  The version string for this tool.
354   */
355  @Override()
356  public String getToolVersion()
357  {
358    return Version.NUMERIC_VERSION_STRING;
359  }
360
361
362
363  /**
364   * Indicates whether this tool should provide support for an interactive mode,
365   * in which the tool offers a mode in which the arguments can be provided in
366   * a text-driven menu rather than requiring them to be given on the command
367   * line.  If interactive mode is supported, it may be invoked using the
368   * "--interactive" argument.  Alternately, if interactive mode is supported
369   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
370   * interactive mode may be invoked by simply launching the tool without any
371   * arguments.
372   *
373   * @return  {@code true} if this tool supports interactive mode, or
374   *          {@code false} if not.
375   */
376  @Override()
377  public boolean supportsInteractiveMode()
378  {
379    return true;
380  }
381
382
383
384  /**
385   * Indicates whether this tool defaults to launching in interactive mode if
386   * the tool is invoked without any command-line arguments.  This will only be
387   * used if {@link #supportsInteractiveMode()} returns {@code true}.
388   *
389   * @return  {@code true} if this tool defaults to using interactive mode if
390   *          launched without any command-line arguments, or {@code false} if
391   *          not.
392   */
393  @Override()
394  public boolean defaultsToInteractiveMode()
395  {
396    return true;
397  }
398
399
400
401  /**
402   * Indicates whether this tool supports the use of a properties file for
403   * specifying default values for arguments that aren't specified on the
404   * command line.
405   *
406   * @return  {@code true} if this tool supports the use of a properties file
407   *          for specifying default values for arguments that aren't specified
408   *          on the command line, or {@code false} if not.
409   */
410  @Override()
411  public boolean supportsPropertiesFile()
412  {
413    return true;
414  }
415
416
417
418  /**
419   * Indicates whether the LDAP-specific arguments should include alternate
420   * versions of all long identifiers that consist of multiple words so that
421   * they are available in both camelCase and dash-separated versions.
422   *
423   * @return  {@code true} if this tool should provide multiple versions of
424   *          long identifiers for LDAP-specific arguments, or {@code false} if
425   *          not.
426   */
427  @Override()
428  protected boolean includeAlternateLongIdentifiers()
429  {
430    return true;
431  }
432
433
434
435  /**
436   * Adds the arguments used by this program that aren't already provided by the
437   * generic {@code LDAPCommandLineTool} framework.
438   *
439   * @param  parser  The argument parser to which the arguments should be added.
440   *
441   * @throws  ArgumentException  If a problem occurs while adding the arguments.
442   */
443  @Override()
444  public void addNonLDAPArguments(final ArgumentParser parser)
445         throws ArgumentException
446  {
447    String description = "The base DN to use for the searches.  It may be a " +
448         "simple DN or a value pattern to specify a range of DNs (e.g., " +
449         "\"uid=user.[1-1000],ou=People,dc=example,dc=com\").  See " +
450         ValuePattern.PUBLIC_JAVADOC_URL + " for complete details about the " +
451         "value pattern syntax.  This must be provided.";
452    baseDN = new StringArgument('b', "baseDN", true, 1, "{dn}", description);
453    baseDN.setArgumentGroupName("Search And Modification Arguments");
454    baseDN.addLongIdentifier("base-dn");
455    parser.addArgument(baseDN);
456
457
458    description = "The scope to use for the searches.  It should be 'base', " +
459                  "'one', 'sub', or 'subord'.  If this is not provided, then " +
460                  "a default scope of 'sub' will be used.";
461    scopeArg = new ScopeArgument('s', "scope", false, "{scope}", description,
462                                 SearchScope.SUB);
463    scopeArg.setArgumentGroupName("Search And Modification Arguments");
464    parser.addArgument(scopeArg);
465
466
467    description = "The filter to use for the searches.  It may be a simple " +
468                  "filter or a value pattern to specify a range of filters " +
469                  "(e.g., \"(uid=user.[1-1000])\").  See " +
470                  ValuePattern.PUBLIC_JAVADOC_URL + " for complete details " +
471                  "about the value pattern syntax.  This must be provided.";
472    filter = new StringArgument('f', "filter", true, 1, "{filter}",
473                                description);
474    filter.setArgumentGroupName("Search And Modification Arguments");
475    parser.addArgument(filter);
476
477
478    description = "The name of an attribute to include in entries returned " +
479                  "from the searches.  Multiple attributes may be requested " +
480                  "by providing this argument multiple times.  If no request " +
481                  "attributes are provided, then the entries returned will " +
482                  "include all user attributes.";
483    returnAttributes = new StringArgument('A', "attribute", false, 0, "{name}",
484                                          description);
485    returnAttributes.setArgumentGroupName("Search And Modification Arguments");
486    parser.addArgument(returnAttributes);
487
488
489    description = "The name of the attribute to modify.  Multiple attributes " +
490                  "may be specified by providing this argument multiple " +
491                  "times.  At least one attribute must be specified.";
492    modifyAttributes = new StringArgument('m', "modifyAttribute", true, 0,
493                                          "{name}", description);
494    modifyAttributes.setArgumentGroupName("Search And Modification Arguments");
495    modifyAttributes.addLongIdentifier("modify-attribute");
496    parser.addArgument(modifyAttributes);
497
498
499    description = "The length in bytes to use when generating values for the " +
500                  "modifications.  If this is not provided, then a default " +
501                  "length of ten bytes will be used.";
502    valueLength = new IntegerArgument('l', "valueLength", true, 1, "{num}",
503                                      description, 1, Integer.MAX_VALUE, 10);
504    valueLength.setArgumentGroupName("Search And Modification Arguments");
505    valueLength.addLongIdentifier("value-length");
506    parser.addArgument(valueLength);
507
508
509    description = "The set of characters to use to generate the values for " +
510                  "the modifications.  It should only include ASCII " +
511                  "characters.  If this is not provided, then a default set " +
512                  "of lowercase alphabetic characters will be used.";
513    characterSet = new StringArgument('C', "characterSet", true, 1, "{chars}",
514                                      description,
515                                      "abcdefghijklmnopqrstuvwxyz");
516    characterSet.setArgumentGroupName("Search And Modification Arguments");
517    characterSet.addLongIdentifier("character-set");
518    parser.addArgument(characterSet);
519
520
521    description = "The number of threads to use to perform the searches.  If " +
522                  "this is not provided, then a default of one thread will " +
523                  "be used.";
524    numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}",
525                                     description, 1, Integer.MAX_VALUE, 1);
526    numThreads.setArgumentGroupName("Rate Management Arguments");
527    numThreads.addLongIdentifier("num-threads");
528    parser.addArgument(numThreads);
529
530
531    description = "The length of time in seconds between output lines.  If " +
532                  "this is not provided, then a default interval of five " +
533                  "seconds will be used.";
534    collectionInterval = new IntegerArgument('i', "intervalDuration", true, 1,
535                                             "{num}", description, 1,
536                                             Integer.MAX_VALUE, 5);
537    collectionInterval.setArgumentGroupName("Rate Management Arguments");
538    collectionInterval.addLongIdentifier("interval-duration");
539    parser.addArgument(collectionInterval);
540
541
542    description = "The maximum number of intervals for which to run.  If " +
543                  "this is not provided, then the tool will run until it is " +
544                  "interrupted.";
545    numIntervals = new IntegerArgument('I', "numIntervals", true, 1, "{num}",
546                                       description, 1, Integer.MAX_VALUE,
547                                       Integer.MAX_VALUE);
548    numIntervals.setArgumentGroupName("Rate Management Arguments");
549    numIntervals.addLongIdentifier("num-intervals");
550    parser.addArgument(numIntervals);
551
552    description = "The number of search and modify iterations that should be " +
553                  "processed on a connection before that connection is " +
554                  "closed and replaced with a newly-established (and " +
555                  "authenticated, if appropriate) connection.  If this is " +
556                  "not provided, then connections will not be periodically " +
557                  "closed and re-established.";
558    iterationsBeforeReconnect = new IntegerArgument(null,
559         "iterationsBeforeReconnect", false, 1, "{num}", description, 0);
560    iterationsBeforeReconnect.setArgumentGroupName("Rate Management Arguments");
561    iterationsBeforeReconnect.addLongIdentifier("iterations-before-reconnect");
562    parser.addArgument(iterationsBeforeReconnect);
563
564    description = "The target number of searches to perform per second.  It " +
565                  "is still necessary to specify a sufficient number of " +
566                  "threads for achieving this rate.  If neither this option " +
567                  "nor --variableRateData is provided, then the tool will " +
568                  "run at the maximum rate for the specified number of " +
569                  "threads.";
570    ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1,
571                                        "{searches-per-second}", description,
572                                        1, Integer.MAX_VALUE);
573    ratePerSecond.setArgumentGroupName("Rate Management Arguments");
574    ratePerSecond.addLongIdentifier("rate-per-second");
575    parser.addArgument(ratePerSecond);
576
577    final String variableRateDataArgName = "variableRateData";
578    final String generateSampleRateFileArgName = "generateSampleRateFile";
579    description = RateAdjustor.getVariableRateDataArgumentDescription(
580         generateSampleRateFileArgName);
581    variableRateData = new FileArgument(null, variableRateDataArgName, false, 1,
582                                        "{path}", description, true, true, true,
583                                        false);
584    variableRateData.setArgumentGroupName("Rate Management Arguments");
585    variableRateData.addLongIdentifier("variable-rate-data");
586    parser.addArgument(variableRateData);
587
588    description = RateAdjustor.getGenerateSampleVariableRateFileDescription(
589         variableRateDataArgName);
590    sampleRateFile = new FileArgument(null, generateSampleRateFileArgName,
591                                      false, 1, "{path}", description, false,
592                                      true, true, false);
593    sampleRateFile.setArgumentGroupName("Rate Management Arguments");
594    sampleRateFile.addLongIdentifier("generate-sample-rate-file");
595    sampleRateFile.setUsageArgument(true);
596    parser.addArgument(sampleRateFile);
597    parser.addExclusiveArgumentSet(variableRateData, sampleRateFile);
598
599    description = "The number of intervals to complete before beginning " +
600                  "overall statistics collection.  Specifying a nonzero " +
601                  "number of warm-up intervals gives the client and server " +
602                  "a chance to warm up without skewing performance results.";
603    warmUpIntervals = new IntegerArgument(null, "warmUpIntervals", true, 1,
604         "{num}", description, 0, Integer.MAX_VALUE, 0);
605    warmUpIntervals.setArgumentGroupName("Rate Management Arguments");
606    warmUpIntervals.addLongIdentifier("warm-up-intervals");
607    parser.addArgument(warmUpIntervals);
608
609    description = "Indicates the format to use for timestamps included in " +
610                  "the output.  A value of 'none' indicates that no " +
611                  "timestamps should be included.  A value of 'with-date' " +
612                  "indicates that both the date and the time should be " +
613                  "included.  A value of 'without-date' indicates that only " +
614                  "the time should be included.";
615    final LinkedHashSet<String> allowedFormats = new LinkedHashSet<String>(3);
616    allowedFormats.add("none");
617    allowedFormats.add("with-date");
618    allowedFormats.add("without-date");
619    timestampFormat = new StringArgument(null, "timestampFormat", true, 1,
620         "{format}", description, allowedFormats, "none");
621    timestampFormat.addLongIdentifier("timestamp-format");
622    parser.addArgument(timestampFormat);
623
624    description = "Indicates that the proxied authorization control (as " +
625                  "defined in RFC 4370) should be used to request that " +
626                  "operations be processed using an alternate authorization " +
627                  "identity.  This may be a simple authorization ID or it " +
628                  "may be a value pattern to specify a range of " +
629                  "identities.  See " + ValuePattern.PUBLIC_JAVADOC_URL +
630                  " for complete details about the value pattern syntax.";
631    proxyAs = new StringArgument('Y', "proxyAs", false, 1, "{authzID}",
632                                 description);
633    proxyAs.addLongIdentifier("proxy-as");
634    parser.addArgument(proxyAs);
635
636    description = "Indicates that information about the result codes for " +
637                  "failed operations should not be displayed.";
638    suppressErrors = new BooleanArgument(null,
639         "suppressErrorResultCodes", 1, description);
640    suppressErrors.addLongIdentifier("suppress-error-result-codes");
641    parser.addArgument(suppressErrors);
642
643    description = "Generate output in CSV format rather than a " +
644                  "display-friendly format";
645    csvFormat = new BooleanArgument('c', "csv", 1, description);
646    parser.addArgument(csvFormat);
647
648    description = "Specifies the seed to use for the random number generator.";
649    randomSeed = new IntegerArgument('R', "randomSeed", false, 1, "{value}",
650         description);
651    randomSeed.addLongIdentifier("random-seed");
652    parser.addArgument(randomSeed);
653  }
654
655
656
657  /**
658   * Indicates whether this tool supports creating connections to multiple
659   * servers.  If it is to support multiple servers, then the "--hostname" and
660   * "--port" arguments will be allowed to be provided multiple times, and
661   * will be required to be provided the same number of times.  The same type of
662   * communication security and bind credentials will be used for all servers.
663   *
664   * @return  {@code true} if this tool supports creating connections to
665   *          multiple servers, or {@code false} if not.
666   */
667  @Override()
668  protected boolean supportsMultipleServers()
669  {
670    return true;
671  }
672
673
674
675  /**
676   * Retrieves the connection options that should be used for connections
677   * created for use with this tool.
678   *
679   * @return  The connection options that should be used for connections created
680   *          for use with this tool.
681   */
682  @Override()
683  public LDAPConnectionOptions getConnectionOptions()
684  {
685    final LDAPConnectionOptions options = new LDAPConnectionOptions();
686    options.setUseSynchronousMode(true);
687    return options;
688  }
689
690
691
692  /**
693   * Performs the actual processing for this tool.  In this case, it gets a
694   * connection to the directory server and uses it to perform the requested
695   * searches.
696   *
697   * @return  The result code for the processing that was performed.
698   */
699  @Override()
700  public ResultCode doToolProcessing()
701  {
702    runningThread = Thread.currentThread();
703
704    try
705    {
706      return doToolProcessingInternal();
707    }
708    finally
709    {
710      runningThread = null;
711    }
712  }
713
714
715
716  /**
717   * Performs the actual processing for this tool.  In this case, it gets a
718   * connection to the directory server and uses it to perform the requested
719   * searches.
720   *
721   * @return  The result code for the processing that was performed.
722   */
723  private ResultCode doToolProcessingInternal()
724  {
725    // If the sample rate file argument was specified, then generate the sample
726    // variable rate data file and return.
727    if (sampleRateFile.isPresent())
728    {
729      try
730      {
731        RateAdjustor.writeSampleVariableRateFile(sampleRateFile.getValue());
732        return ResultCode.SUCCESS;
733      }
734      catch (final Exception e)
735      {
736        debugException(e);
737        err("An error occurred while trying to write sample variable data " +
738             "rate file '", sampleRateFile.getValue().getAbsolutePath(),
739             "':  ", getExceptionMessage(e));
740        return ResultCode.LOCAL_ERROR;
741      }
742    }
743
744
745    // Determine the random seed to use.
746    final Long seed;
747    if (randomSeed.isPresent())
748    {
749      seed = Long.valueOf(randomSeed.getValue());
750    }
751    else
752    {
753      seed = null;
754    }
755
756    // Create value patterns for the base DN, filter, and proxied authorization
757    // DN.
758    final ValuePattern dnPattern;
759    try
760    {
761      dnPattern = new ValuePattern(baseDN.getValue(), seed);
762    }
763    catch (final ParseException pe)
764    {
765      debugException(pe);
766      err("Unable to parse the base DN value pattern:  ", pe.getMessage());
767      return ResultCode.PARAM_ERROR;
768    }
769
770    final ValuePattern filterPattern;
771    try
772    {
773      filterPattern = new ValuePattern(filter.getValue(), seed);
774    }
775    catch (final ParseException pe)
776    {
777      debugException(pe);
778      err("Unable to parse the filter pattern:  ", pe.getMessage());
779      return ResultCode.PARAM_ERROR;
780    }
781
782    final ValuePattern authzIDPattern;
783    if (proxyAs.isPresent())
784    {
785      try
786      {
787        authzIDPattern = new ValuePattern(proxyAs.getValue(), seed);
788      }
789      catch (final ParseException pe)
790      {
791        debugException(pe);
792        err("Unable to parse the proxied authorization pattern:  ",
793            pe.getMessage());
794        return ResultCode.PARAM_ERROR;
795      }
796    }
797    else
798    {
799      authzIDPattern = null;
800    }
801
802
803    // Get the attributes to return.
804    final String[] returnAttrs;
805    if (returnAttributes.isPresent())
806    {
807      final List<String> attrList = returnAttributes.getValues();
808      returnAttrs = new String[attrList.size()];
809      attrList.toArray(returnAttrs);
810    }
811    else
812    {
813      returnAttrs = NO_STRINGS;
814    }
815
816
817    // Get the names of the attributes to modify.
818    final String[] modAttrs = new String[modifyAttributes.getValues().size()];
819    modifyAttributes.getValues().toArray(modAttrs);
820
821
822    // Get the character set as a byte array.
823    final byte[] charSet = getBytes(characterSet.getValue());
824
825
826    // If the --ratePerSecond option was specified, then limit the rate
827    // accordingly.
828    FixedRateBarrier fixedRateBarrier = null;
829    if (ratePerSecond.isPresent() || variableRateData.isPresent())
830    {
831      // We might not have a rate per second if --variableRateData is specified.
832      // The rate typically doesn't matter except when we have warm-up
833      // intervals.  In this case, we'll run at the max rate.
834      final int intervalSeconds = collectionInterval.getValue();
835      final int ratePerInterval =
836           (ratePerSecond.getValue() == null)
837           ? Integer.MAX_VALUE
838           : ratePerSecond.getValue() * intervalSeconds;
839      fixedRateBarrier =
840           new FixedRateBarrier(1000L * intervalSeconds, ratePerInterval);
841    }
842
843
844    // If --variableRateData was specified, then initialize a RateAdjustor.
845    RateAdjustor rateAdjustor = null;
846    if (variableRateData.isPresent())
847    {
848      try
849      {
850        rateAdjustor = RateAdjustor.newInstance(fixedRateBarrier,
851             ratePerSecond.getValue(), variableRateData.getValue());
852      }
853      catch (final IOException e)
854      {
855        debugException(e);
856        err("Initializing the variable rates failed: " + e.getMessage());
857        return ResultCode.PARAM_ERROR;
858      }
859      catch (final IllegalArgumentException e)
860      {
861        debugException(e);
862        err("Initializing the variable rates failed: " + e.getMessage());
863        return ResultCode.PARAM_ERROR;
864      }
865    }
866
867
868    // Determine whether to include timestamps in the output and if so what
869    // format should be used for them.
870    final boolean includeTimestamp;
871    final String timeFormat;
872    if (timestampFormat.getValue().equalsIgnoreCase("with-date"))
873    {
874      includeTimestamp = true;
875      timeFormat       = "dd/MM/yyyy HH:mm:ss";
876    }
877    else if (timestampFormat.getValue().equalsIgnoreCase("without-date"))
878    {
879      includeTimestamp = true;
880      timeFormat       = "HH:mm:ss";
881    }
882    else
883    {
884      includeTimestamp = false;
885      timeFormat       = null;
886    }
887
888
889    // Determine whether any warm-up intervals should be run.
890    final long totalIntervals;
891    final boolean warmUp;
892    int remainingWarmUpIntervals = warmUpIntervals.getValue();
893    if (remainingWarmUpIntervals > 0)
894    {
895      warmUp = true;
896      totalIntervals = 0L + numIntervals.getValue() + remainingWarmUpIntervals;
897    }
898    else
899    {
900      warmUp = true;
901      totalIntervals = 0L + numIntervals.getValue();
902    }
903
904
905    // Create the table that will be used to format the output.
906    final OutputFormat outputFormat;
907    if (csvFormat.isPresent())
908    {
909      outputFormat = OutputFormat.CSV;
910    }
911    else
912    {
913      outputFormat = OutputFormat.COLUMNS;
914    }
915
916    final ColumnFormatter formatter = new ColumnFormatter(includeTimestamp,
917         timeFormat, outputFormat, " ",
918         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
919                  "Searches/Sec"),
920         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
921                  "Srch Dur ms"),
922         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
923                  "Mods/Sec"),
924         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
925                  "Mod Dur ms"),
926         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
927                  "Errors/Sec"),
928         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
929                  "Searches/Sec"),
930         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
931                  "Srch Dur ms"),
932         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
933                  "Mods/Sec"),
934         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
935                  "Mod Dur ms"));
936
937
938    // Create values to use for statistics collection.
939    final AtomicLong        searchCounter   = new AtomicLong(0L);
940    final AtomicLong        errorCounter    = new AtomicLong(0L);
941    final AtomicLong        modCounter      = new AtomicLong(0L);
942    final AtomicLong        modDurations    = new AtomicLong(0L);
943    final AtomicLong        searchDurations = new AtomicLong(0L);
944    final ResultCodeCounter rcCounter       = new ResultCodeCounter();
945
946
947    // Determine the length of each interval in milliseconds.
948    final long intervalMillis = 1000L * collectionInterval.getValue();
949
950
951    // Create the threads to use for the searches.
952    final Random random = new Random();
953    final CyclicBarrier barrier = new CyclicBarrier(numThreads.getValue() + 1);
954    final SearchAndModRateThread[] threads =
955         new SearchAndModRateThread[numThreads.getValue()];
956    for (int i=0; i < threads.length; i++)
957    {
958      final LDAPConnection connection;
959      try
960      {
961        connection = getConnection();
962      }
963      catch (final LDAPException le)
964      {
965        debugException(le);
966        err("Unable to connect to the directory server:  ",
967            getExceptionMessage(le));
968        return le.getResultCode();
969      }
970
971      threads[i] = new SearchAndModRateThread(this, i, connection, dnPattern,
972           scopeArg.getValue(), filterPattern, returnAttrs, modAttrs,
973           valueLength.getValue(), charSet, authzIDPattern,
974           iterationsBeforeReconnect.getValue(), random.nextLong(), barrier,
975           searchCounter, modCounter, searchDurations, modDurations,
976           errorCounter, rcCounter, fixedRateBarrier);
977      threads[i].start();
978    }
979
980
981    // Display the table header.
982    for (final String headerLine : formatter.getHeaderLines(true))
983    {
984      out(headerLine);
985    }
986
987
988    // Start the RateAdjustor before the threads so that the initial value is
989    // in place before any load is generated unless we're doing a warm-up in
990    // which case, we'll start it after the warm-up is complete.
991    if ((rateAdjustor != null) && (remainingWarmUpIntervals <= 0))
992    {
993      rateAdjustor.start();
994    }
995
996
997    // Indicate that the threads can start running.
998    try
999    {
1000      barrier.await();
1001    }
1002    catch (final Exception e)
1003    {
1004      debugException(e);
1005    }
1006
1007    long overallStartTime = System.nanoTime();
1008    long nextIntervalStartTime = System.currentTimeMillis() + intervalMillis;
1009
1010
1011    boolean setOverallStartTime = false;
1012    long    lastSearchDuration  = 0L;
1013    long    lastModDuration     = 0L;
1014    long    lastNumErrors       = 0L;
1015    long    lastNumSearches     = 0L;
1016    long    lastNumMods          = 0L;
1017    long    lastEndTime         = System.nanoTime();
1018    for (long i=0; i < totalIntervals; i++)
1019    {
1020      if (rateAdjustor != null)
1021      {
1022        if (! rateAdjustor.isAlive())
1023        {
1024          out("All of the rates in " + variableRateData.getValue().getName() +
1025              " have been completed.");
1026          break;
1027        }
1028      }
1029
1030      final long startTimeMillis = System.currentTimeMillis();
1031      final long sleepTimeMillis = nextIntervalStartTime - startTimeMillis;
1032      nextIntervalStartTime += intervalMillis;
1033      if (sleepTimeMillis > 0)
1034      {
1035        sleeper.sleep(sleepTimeMillis);
1036      }
1037
1038      if (stopRequested.get())
1039      {
1040        break;
1041      }
1042
1043      final long endTime          = System.nanoTime();
1044      final long intervalDuration = endTime - lastEndTime;
1045
1046      final long numSearches;
1047      final long numMods;
1048      final long numErrors;
1049      final long totalSearchDuration;
1050      final long totalModDuration;
1051      if (warmUp && (remainingWarmUpIntervals > 0))
1052      {
1053        numSearches         = searchCounter.getAndSet(0L);
1054        numMods             = modCounter.getAndSet(0L);
1055        numErrors           = errorCounter.getAndSet(0L);
1056        totalSearchDuration = searchDurations.getAndSet(0L);
1057        totalModDuration    = modDurations.getAndSet(0L);
1058      }
1059      else
1060      {
1061        numSearches         = searchCounter.get();
1062        numMods             = modCounter.get();
1063        numErrors           = errorCounter.get();
1064        totalSearchDuration = searchDurations.get();
1065        totalModDuration    = modDurations.get();
1066      }
1067
1068      final long recentNumSearches = numSearches - lastNumSearches;
1069      final long recentNumMods = numMods - lastNumMods;
1070      final long recentNumErrors = numErrors - lastNumErrors;
1071      final long recentSearchDuration =
1072           totalSearchDuration - lastSearchDuration;
1073      final long recentModDuration = totalModDuration - lastModDuration;
1074
1075      final double numSeconds = intervalDuration / 1000000000.0d;
1076      final double recentSearchRate = recentNumSearches / numSeconds;
1077      final double recentModRate = recentNumMods / numSeconds;
1078      final double recentErrorRate  = recentNumErrors / numSeconds;
1079
1080      final double recentAvgSearchDuration;
1081      if (recentNumSearches > 0L)
1082      {
1083        recentAvgSearchDuration =
1084             1.0d * recentSearchDuration / recentNumSearches / 1000000;
1085      }
1086      else
1087      {
1088        recentAvgSearchDuration = 0.0d;
1089      }
1090
1091      final double recentAvgModDuration;
1092      if (recentNumMods > 0L)
1093      {
1094        recentAvgModDuration =
1095             1.0d * recentModDuration / recentNumMods / 1000000;
1096      }
1097      else
1098      {
1099        recentAvgModDuration = 0.0d;
1100      }
1101
1102      if (warmUp && (remainingWarmUpIntervals > 0))
1103      {
1104        out(formatter.formatRow(recentSearchRate, recentAvgSearchDuration,
1105             recentModRate, recentAvgModDuration, recentErrorRate, "warming up",
1106             "warming up", "warming up", "warming up"));
1107
1108        remainingWarmUpIntervals--;
1109        if (remainingWarmUpIntervals == 0)
1110        {
1111          out("Warm-up completed.  Beginning overall statistics collection.");
1112          setOverallStartTime = true;
1113          if (rateAdjustor != null)
1114          {
1115            rateAdjustor.start();
1116          }
1117        }
1118      }
1119      else
1120      {
1121        if (setOverallStartTime)
1122        {
1123          overallStartTime    = lastEndTime;
1124          setOverallStartTime = false;
1125        }
1126
1127        final double numOverallSeconds =
1128             (endTime - overallStartTime) / 1000000000.0d;
1129        final double overallSearchRate = numSearches / numOverallSeconds;
1130        final double overallModRate = numMods / numOverallSeconds;
1131
1132        final double overallAvgSearchDuration;
1133        if (numSearches > 0L)
1134        {
1135          overallAvgSearchDuration =
1136               1.0d * totalSearchDuration / numSearches / 1000000;
1137        }
1138        else
1139        {
1140          overallAvgSearchDuration = 0.0d;
1141        }
1142
1143        final double overallAvgModDuration;
1144        if (numMods > 0L)
1145        {
1146          overallAvgModDuration =
1147               1.0d * totalModDuration / numMods / 1000000;
1148        }
1149        else
1150        {
1151          overallAvgModDuration = 0.0d;
1152        }
1153
1154        out(formatter.formatRow(recentSearchRate, recentAvgSearchDuration,
1155             recentModRate, recentAvgModDuration, recentErrorRate,
1156             overallSearchRate, overallAvgSearchDuration, overallModRate,
1157             overallAvgModDuration));
1158
1159        lastNumSearches    = numSearches;
1160        lastNumMods        = numMods;
1161        lastNumErrors      = numErrors;
1162        lastSearchDuration = totalSearchDuration;
1163        lastModDuration    = totalModDuration;
1164      }
1165
1166      final List<ObjectPair<ResultCode,Long>> rcCounts =
1167           rcCounter.getCounts(true);
1168      if ((! suppressErrors.isPresent()) && (! rcCounts.isEmpty()))
1169      {
1170        err("\tError Results:");
1171        for (final ObjectPair<ResultCode,Long> p : rcCounts)
1172        {
1173          err("\t", p.getFirst().getName(), ":  ", p.getSecond());
1174        }
1175      }
1176
1177      lastEndTime = endTime;
1178    }
1179
1180
1181    // Shut down the RateAdjustor if we have one.
1182    if (rateAdjustor != null)
1183    {
1184      rateAdjustor.shutDown();
1185    }
1186
1187    // Stop all of the threads.
1188    ResultCode resultCode = ResultCode.SUCCESS;
1189    for (final SearchAndModRateThread t : threads)
1190    {
1191      final ResultCode r = t.stopRunning();
1192      if (resultCode == ResultCode.SUCCESS)
1193      {
1194        resultCode = r;
1195      }
1196    }
1197
1198    return resultCode;
1199  }
1200
1201
1202
1203  /**
1204   * Requests that this tool stop running.  This method will attempt to wait
1205   * for all threads to complete before returning control to the caller.
1206   */
1207  public void stopRunning()
1208  {
1209    stopRequested.set(true);
1210    sleeper.wakeup();
1211
1212    final Thread t = runningThread;
1213    if (t != null)
1214    {
1215      try
1216      {
1217        t.join();
1218      }
1219      catch (final Exception e)
1220      {
1221        debugException(e);
1222      }
1223    }
1224  }
1225
1226
1227
1228  /**
1229   * {@inheritDoc}
1230   */
1231  @Override()
1232  public LinkedHashMap<String[],String> getExampleUsages()
1233  {
1234    final LinkedHashMap<String[],String> examples =
1235         new LinkedHashMap<String[],String>(2);
1236
1237    String[] args =
1238    {
1239      "--hostname", "server.example.com",
1240      "--port", "389",
1241      "--bindDN", "uid=admin,dc=example,dc=com",
1242      "--bindPassword", "password",
1243      "--baseDN", "dc=example,dc=com",
1244      "--scope", "sub",
1245      "--filter", "(uid=user.[1-1000000])",
1246      "--attribute", "givenName",
1247      "--attribute", "sn",
1248      "--attribute", "mail",
1249      "--modifyAttribute", "description",
1250      "--valueLength", "10",
1251      "--characterSet", "abcdefghijklmnopqrstuvwxyz0123456789",
1252      "--numThreads", "10"
1253    };
1254    String description =
1255         "Test search and modify performance by searching randomly across a " +
1256         "set of one million users located below 'dc=example,dc=com' with " +
1257         "ten concurrent threads.  The entries returned to the client will " +
1258         "include the givenName, sn, and mail attributes, and the " +
1259         "description attribute of each entry returned will be replaced " +
1260         "with a string of ten randomly-selected alphanumeric characters.";
1261    examples.put(args, description);
1262
1263    args = new String[]
1264    {
1265      "--generateSampleRateFile", "variable-rate-data.txt"
1266    };
1267    description =
1268         "Generate a sample variable rate definition file that may be used " +
1269         "in conjunction with the --variableRateData argument.  The sample " +
1270         "file will include comments that describe the format for data to be " +
1271         "included in this file.";
1272    examples.put(args, description);
1273
1274    return examples;
1275  }
1276}