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