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