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