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