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