001    /*
002     * Copyright 2009-2013 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2009-2013 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.OutputStream;
026    import java.io.Serializable;
027    import java.text.ParseException;
028    import java.util.LinkedHashMap;
029    import java.util.LinkedHashSet;
030    import java.util.List;
031    import java.util.concurrent.CyclicBarrier;
032    import java.util.concurrent.atomic.AtomicLong;
033    
034    import com.unboundid.ldap.sdk.LDAPConnection;
035    import com.unboundid.ldap.sdk.LDAPConnectionOptions;
036    import com.unboundid.ldap.sdk.LDAPException;
037    import com.unboundid.ldap.sdk.ResultCode;
038    import com.unboundid.ldap.sdk.SearchScope;
039    import com.unboundid.ldap.sdk.Version;
040    import com.unboundid.util.ColumnFormatter;
041    import com.unboundid.util.FixedRateBarrier;
042    import com.unboundid.util.FormattableColumn;
043    import com.unboundid.util.HorizontalAlignment;
044    import com.unboundid.util.LDAPCommandLineTool;
045    import com.unboundid.util.ObjectPair;
046    import com.unboundid.util.OutputFormat;
047    import com.unboundid.util.ResultCodeCounter;
048    import com.unboundid.util.ThreadSafety;
049    import com.unboundid.util.ThreadSafetyLevel;
050    import com.unboundid.util.ValuePattern;
051    import com.unboundid.util.args.ArgumentException;
052    import com.unboundid.util.args.ArgumentParser;
053    import com.unboundid.util.args.BooleanArgument;
054    import com.unboundid.util.args.IntegerArgument;
055    import com.unboundid.util.args.ScopeArgument;
056    import com.unboundid.util.args.StringArgument;
057    
058    import static com.unboundid.util.StaticUtils.*;
059    
060    
061    
062    /**
063     * This class provides a tool that can be used to test authentication processing
064     * in an LDAP directory server using multiple threads.  Each authentication will
065     * consist of two operations:  a search to find the target entry followed by a
066     * bind to verify the credentials for that user.  The search will use the given
067     * base DN and filter, either or both of which may be a value pattern as
068     * described in the {@link ValuePattern} class.  This makes it possible to
069     * search over a range of entries rather than repeatedly performing searches
070     * with the same base DN and filter.
071     * <BR><BR>
072     * Some of the APIs demonstrated by this example include:
073     * <UL>
074     *   <LI>Argument Parsing (from the {@code com.unboundid.util.args}
075     *       package)</LI>
076     *   <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util}
077     *       package)</LI>
078     *   <LI>LDAP Communication (from the {@code com.unboundid.ldap.sdk}
079     *       package)</LI>
080     *   <LI>Value Patterns (from the {@code com.unboundid.util} package)</LI>
081     * </UL>
082     * Each search must match exactly one entry, and this tool will then attempt to
083     * authenticate as the user associated with that entry.  It supports simple
084     * authentication, as well as the CRAM-MD5, DIGEST-MD5, and PLAIN SASL
085     * mechanisms.
086     * <BR><BR>
087     * All of the necessary information is provided using command line arguments.
088     * Supported arguments include those allowed by the {@link LDAPCommandLineTool}
089     * class, as well as the following additional arguments:
090     * <UL>
091     *   <LI>"-b {baseDN}" or "--baseDN {baseDN}" -- specifies the base DN to use
092     *       for the searches.  This must be provided.  It may be a simple DN, or it
093     *       may be a value pattern to express a range of base DNs.</LI>
094     *   <LI>"-s {scope}" or "--scope {scope}" -- specifies the scope to use for the
095     *       search.  The scope value should be one of "base", "one", "sub", or
096     *       "subord".  If this isn't specified, then a scope of "sub" will be
097     *       used.</LI>
098     *   <LI>"-f {filter}" or "--filter {filter}" -- specifies the filter to use for
099     *       the searches.  This must be provided.  It may be a simple filter, or it
100     *       may be a value pattern to express a range of filters.</LI>
101     *   <LI>"-A {name}" or "--attribute {name}" -- specifies the name of an
102     *       attribute that should be included in entries returned from the server.
103     *       If this is not provided, then all user attributes will be requested.
104     *       This may include special tokens that the server may interpret, like
105     *       "1.1" to indicate that no attributes should be returned, "*", for all
106     *       user attributes, or "+" for all operational attributes.  Multiple
107     *       attributes may be requested with multiple instances of this
108     *       argument.</LI>
109     *   <LI>"-C {password}" or "--credentials {password}" -- specifies the password
110     *       to use when authenticating users identified by the searches.</LI>
111     *   <LI>"-a {authType}" or "--authType {authType}" -- specifies the type of
112     *       authentication to attempt.  Supported values include "SIMPLE",
113     *       "CRAM-MD5", "DIGEST-MD5", and "PLAIN".
114     *   <LI>"-t {num}" or "--numThreads {num}" -- specifies the number of
115     *       concurrent threads to use when performing the authentication
116     *       processing.  If this is not provided, then a default of one thread will
117     *       be used.</LI>
118     *   <LI>"-i {sec}" or "--intervalDuration {sec}" -- specifies the length of
119     *       time in seconds between lines out output.  If this is not provided,
120     *       then a default interval duration of five seconds will be used.</LI>
121     *   <LI>"-I {num}" or "--numIntervals {num}" -- specifies the maximum number of
122     *       intervals for which to run.  If this is not provided, then it will
123     *       run forever.</LI>
124     *   <LI>"-r {auths-per-second}" or "--ratePerSecond {auths-per-second}" --
125     *       specifies the target number of authorizations to perform per second.
126     *       It is still necessary to specify a sufficient number of threads for
127     *       achieving this rate.  If this option is not provided, then the tool
128     *       will run at the maximum rate for the specified number of threads.</LI>
129     *   <LI>"--warmUpIntervals {num}" -- specifies the number of intervals to
130     *       complete before beginning overall statistics collection.</LI>
131     *   <LI>"--timestampFormat {format}" -- specifies the format to use for
132     *       timestamps included before each output line.  The format may be one of
133     *       "none" (for no timestamps), "with-date" (to include both the date and
134     *       the time), or "without-date" (to include only time time).</LI>
135     *   <LI>"--suppressErrorResultCodes" -- Indicates that information about the
136     *       result codes for failed operations should not be displayed.</LI>
137     *   <LI>"-c" or "--csv" -- Generate output in CSV format rather than a
138     *       display-friendly format.</LI>
139     * </UL>
140     */
141    @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
142    public final class AuthRate
143           extends LDAPCommandLineTool
144           implements Serializable
145    {
146      /**
147       * The serial version UID for this serializable class.
148       */
149      private static final long serialVersionUID = 6918029871717330547L;
150    
151    
152    
153      // The argument used to indicate whether to generate output in CSV format.
154      private BooleanArgument csvFormat;
155    
156      // The argument used to indicate whether to suppress information about error
157      // result codes.
158      private BooleanArgument suppressErrorsArgument;
159    
160      // The argument used to specify the collection interval.
161      private IntegerArgument collectionInterval;
162    
163      // The argument used to specify the number of intervals.
164      private IntegerArgument numIntervals;
165    
166      // The argument used to specify the number of threads.
167      private IntegerArgument numThreads;
168    
169      // The argument used to specify the seed to use for the random number
170      // generator.
171      private IntegerArgument randomSeed;
172    
173      // The target rate of auths per second.
174      private IntegerArgument ratePerSecond;
175    
176      // The number of warm-up intervals to perform.
177      private IntegerArgument warmUpIntervals;
178    
179      // The argument used to specify the attributes to return.
180      private StringArgument attributes;
181    
182      // The argument used to specify the type of authentication to perform.
183      private StringArgument authType;
184    
185      // The argument used to specify the base DNs for the searches.
186      private StringArgument baseDN;
187    
188      // The argument used to specify the filters for the searches.
189      private StringArgument filter;
190    
191      // The argument used to specify the scope for the searches.
192      private ScopeArgument scopeArg;
193    
194      // The argument used to specify the timestamp format.
195      private StringArgument timestampFormat;
196    
197      // The argument used to specify the password to use to authenticate.
198      private StringArgument userPassword;
199    
200    
201    
202      /**
203       * Parse the provided command line arguments and make the appropriate set of
204       * changes.
205       *
206       * @param  args  The command line arguments provided to this program.
207       */
208      public static void main(final String[] args)
209      {
210        final ResultCode resultCode = main(args, System.out, System.err);
211        if (resultCode != ResultCode.SUCCESS)
212        {
213          System.exit(resultCode.intValue());
214        }
215      }
216    
217    
218    
219      /**
220       * Parse the provided command line arguments and make the appropriate set of
221       * changes.
222       *
223       * @param  args       The command line arguments provided to this program.
224       * @param  outStream  The output stream to which standard out should be
225       *                    written.  It may be {@code null} if output should be
226       *                    suppressed.
227       * @param  errStream  The output stream to which standard error should be
228       *                    written.  It may be {@code null} if error messages
229       *                    should be suppressed.
230       *
231       * @return  A result code indicating whether the processing was successful.
232       */
233      public static ResultCode main(final String[] args,
234                                    final OutputStream outStream,
235                                    final OutputStream errStream)
236      {
237        final AuthRate authRate = new AuthRate(outStream, errStream);
238        return authRate.runTool(args);
239      }
240    
241    
242    
243      /**
244       * Creates a new instance of this tool.
245       *
246       * @param  outStream  The output stream to which standard out should be
247       *                    written.  It may be {@code null} if output should be
248       *                    suppressed.
249       * @param  errStream  The output stream to which standard error should be
250       *                    written.  It may be {@code null} if error messages
251       *                    should be suppressed.
252       */
253      public AuthRate(final OutputStream outStream, final OutputStream errStream)
254      {
255        super(outStream, errStream);
256      }
257    
258    
259    
260      /**
261       * Retrieves the name for this tool.
262       *
263       * @return  The name for this tool.
264       */
265      @Override()
266      public String getToolName()
267      {
268        return "authrate";
269      }
270    
271    
272    
273      /**
274       * Retrieves the description for this tool.
275       *
276       * @return  The description for this tool.
277       */
278      @Override()
279      public String getToolDescription()
280      {
281        return "Perform repeated authentications against an LDAP directory " +
282               "server, where each authentication consists of a search to " +
283               "find a user followed by a bind to verify the credentials " +
284               "for that user.";
285      }
286    
287    
288    
289      /**
290       * Retrieves the version string for this tool.
291       *
292       * @return  The version string for this tool.
293       */
294      @Override()
295      public String getToolVersion()
296      {
297        return Version.NUMERIC_VERSION_STRING;
298      }
299    
300    
301    
302      /**
303       * Adds the arguments used by this program that aren't already provided by the
304       * generic {@code LDAPCommandLineTool} framework.
305       *
306       * @param  parser  The argument parser to which the arguments should be added.
307       *
308       * @throws  ArgumentException  If a problem occurs while adding the arguments.
309       */
310      @Override()
311      public void addNonLDAPArguments(final ArgumentParser parser)
312             throws ArgumentException
313      {
314        String description = "The base DN to use for the searches.  It may be a " +
315             "simple DN or a value pattern to specify a range of DNs (e.g., " +
316             "\"uid=user.[1-1000],ou=People,dc=example,dc=com\").  This must be " +
317             "provided.";
318        baseDN = new StringArgument('b', "baseDN", true, 1, "{dn}", description);
319        parser.addArgument(baseDN);
320    
321    
322        description = "The scope to use for the searches.  It should be 'base', " +
323                      "'one', 'sub', or 'subord'.  If this is not provided, a " +
324                      "default scope of 'sub' will be used.";
325        scopeArg = new ScopeArgument('s', "scope", false, "{scope}", description,
326                                     SearchScope.SUB);
327        parser.addArgument(scopeArg);
328    
329    
330        description = "The filter to use for the searches.  It may be a simple " +
331                      "filter or a value pattern to specify a range of filters " +
332                      "(e.g., \"(uid=user.[1-1000])\").  This must be provided.";
333        filter = new StringArgument('f', "filter", true, 1, "{filter}",
334                                    description);
335        parser.addArgument(filter);
336    
337    
338        description = "The name of an attribute to include in entries returned " +
339                      "from the searches.  Multiple attributes may be requested " +
340                      "by providing this argument multiple times.  If no return " +
341                      "attributes are specified, then entries will be returned " +
342                      "with all user attributes.";
343        attributes = new StringArgument('A', "attribute", false, 0, "{name}",
344                                        description);
345        parser.addArgument(attributes);
346    
347    
348        description = "The password to use when binding as the users returned " +
349                      "from the searches.  This must be provided.";
350        userPassword = new StringArgument('C', "credentials", true, 1, "{password}",
351                                          description);
352        parser.addArgument(userPassword);
353    
354    
355        description = "The type of authentication to perform.  Allowed values " +
356                      "are:  SIMPLE, CRAM-MD5, DIGEST-MD5, and PLAIN.  If no "+
357                      "value is provided, then SIMPLE authentication will be " +
358                      "performed.";
359        final LinkedHashSet<String> allowedAuthTypes = new LinkedHashSet<String>(4);
360        allowedAuthTypes.add("simple");
361        allowedAuthTypes.add("cram-md5");
362        allowedAuthTypes.add("digest-md5");
363        allowedAuthTypes.add("plain");
364        authType = new StringArgument('a', "authType", true, 1, "{authType}",
365                                      description, allowedAuthTypes, "simple");
366        parser.addArgument(authType);
367    
368    
369        description = "The number of threads to use to perform the " +
370                      "authentication processing.  If this is not provided, then " +
371                      "a default of one thread will be used.";
372        numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}",
373                                         description, 1, Integer.MAX_VALUE, 1);
374        parser.addArgument(numThreads);
375    
376    
377        description = "The length of time in seconds between output lines.  If " +
378                      "this is not provided, then a default interval of five " +
379                      "seconds will be used.";
380        collectionInterval = new IntegerArgument('i', "intervalDuration", true, 1,
381                                                 "{num}", description, 1,
382                                                 Integer.MAX_VALUE, 5);
383        parser.addArgument(collectionInterval);
384    
385    
386        description = "The maximum number of intervals for which to run.  If " +
387                      "this is not provided, then the tool will run until it is " +
388                      "interrupted.";
389        numIntervals = new IntegerArgument('I', "numIntervals", true, 1, "{num}",
390                                           description, 1, Integer.MAX_VALUE,
391                                           Integer.MAX_VALUE);
392        parser.addArgument(numIntervals);
393    
394        description = "The target number of authorizations to perform per " +
395                      "second.  It is still necessary to specify a sufficient " +
396                      "number of threads for achieving this rate.  If this " +
397                      "option is not provided, then the tool will run at the " +
398                      "maximum rate for the specified number of threads.";
399        ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1,
400                                            "{auths-per-second}", description,
401                                            1, Integer.MAX_VALUE);
402        parser.addArgument(ratePerSecond);
403    
404        description = "The number of intervals to complete before beginning " +
405                      "overall statistics collection.  Specifying a nonzero " +
406                      "number of warm-up intervals gives the client and server " +
407                      "a chance to warm up without skewing performance results.";
408        warmUpIntervals = new IntegerArgument(null, "warmUpIntervals", true, 1,
409             "{num}", description, 0, Integer.MAX_VALUE, 0);
410        parser.addArgument(warmUpIntervals);
411    
412        description = "Indicates the format to use for timestamps included in " +
413                      "the output.  A value of 'none' indicates that no " +
414                      "timestamps should be included.  A value of 'with-date' " +
415                      "indicates that both the date and the time should be " +
416                      "included.  A value of 'without-date' indicates that only " +
417                      "the time should be included.";
418        final LinkedHashSet<String> allowedFormats = new LinkedHashSet<String>(3);
419        allowedFormats.add("none");
420        allowedFormats.add("with-date");
421        allowedFormats.add("without-date");
422        timestampFormat = new StringArgument(null, "timestampFormat", true, 1,
423             "{format}", description, allowedFormats, "none");
424        parser.addArgument(timestampFormat);
425    
426        description = "Indicates that information about the result codes for " +
427                      "failed operations should not be displayed.";
428        suppressErrorsArgument = new BooleanArgument(null,
429             "suppressErrorResultCodes", 1, description);
430        parser.addArgument(suppressErrorsArgument);
431    
432        description = "Generate output in CSV format rather than a " +
433                      "display-friendly format";
434        csvFormat = new BooleanArgument('c', "csv", 1, description);
435        parser.addArgument(csvFormat);
436    
437        description = "Specifies the seed to use for the random number generator.";
438        randomSeed = new IntegerArgument('R', "randomSeed", false, 1, "{value}",
439             description);
440        parser.addArgument(randomSeed);
441      }
442    
443    
444    
445      /**
446       * Indicates whether this tool supports creating connections to multiple
447       * servers.  If it is to support multiple servers, then the "--hostname" and
448       * "--port" arguments will be allowed to be provided multiple times, and
449       * will be required to be provided the same number of times.  The same type of
450       * communication security and bind credentials will be used for all servers.
451       *
452       * @return  {@code true} if this tool supports creating connections to
453       *          multiple servers, or {@code false} if not.
454       */
455      @Override()
456      protected boolean supportsMultipleServers()
457      {
458        return true;
459      }
460    
461    
462    
463      /**
464       * Retrieves the connection options that should be used for connections
465       * created for use with this tool.
466       *
467       * @return  The connection options that should be used for connections created
468       *          for use with this tool.
469       */
470      @Override()
471      public LDAPConnectionOptions getConnectionOptions()
472      {
473        final LDAPConnectionOptions options = new LDAPConnectionOptions();
474        options.setAutoReconnect(true);
475        options.setUseSynchronousMode(true);
476        return options;
477      }
478    
479    
480    
481      /**
482       * Performs the actual processing for this tool.  In this case, it gets a
483       * connection to the directory server and uses it to perform the requested
484       * searches.
485       *
486       * @return  The result code for the processing that was performed.
487       */
488      @Override()
489      public ResultCode doToolProcessing()
490      {
491        // Determine the random seed to use.
492        final Long seed;
493        if (randomSeed.isPresent())
494        {
495          seed = Long.valueOf(randomSeed.getValue());
496        }
497        else
498        {
499          seed = null;
500        }
501    
502        // Create value patterns for the base DN and filter.
503        final ValuePattern dnPattern;
504        try
505        {
506          dnPattern = new ValuePattern(baseDN.getValue(), seed);
507        }
508        catch (ParseException pe)
509        {
510          err("Unable to parse the base DN value pattern:  ", pe.getMessage());
511          return ResultCode.PARAM_ERROR;
512        }
513    
514        final ValuePattern filterPattern;
515        try
516        {
517          filterPattern = new ValuePattern(filter.getValue(), seed);
518        }
519        catch (ParseException pe)
520        {
521          err("Unable to parse the filter pattern:  ", pe.getMessage());
522          return ResultCode.PARAM_ERROR;
523        }
524    
525    
526        // Get the attributes to return.
527        final String[] attrs;
528        if (attributes.isPresent())
529        {
530          final List<String> attrList = attributes.getValues();
531          attrs = new String[attrList.size()];
532          attrList.toArray(attrs);
533        }
534        else
535        {
536          attrs = NO_STRINGS;
537        }
538    
539    
540        // If the --ratePerSecond option was specified, then limit the rate
541        // accordingly.
542        FixedRateBarrier fixedRateBarrier = null;
543        if (ratePerSecond.isPresent())
544        {
545          final int intervalSeconds = collectionInterval.getValue();
546          final int ratePerInterval = ratePerSecond.getValue() * intervalSeconds;
547    
548          fixedRateBarrier =
549               new FixedRateBarrier(1000L * intervalSeconds, ratePerInterval);
550        }
551    
552    
553        // Determine whether to include timestamps in the output and if so what
554        // format should be used for them.
555        final boolean includeTimestamp;
556        final String timeFormat;
557        if (timestampFormat.getValue().equalsIgnoreCase("with-date"))
558        {
559          includeTimestamp = true;
560          timeFormat       = "dd/MM/yyyy HH:mm:ss";
561        }
562        else if (timestampFormat.getValue().equalsIgnoreCase("without-date"))
563        {
564          includeTimestamp = true;
565          timeFormat       = "HH:mm:ss";
566        }
567        else
568        {
569          includeTimestamp = false;
570          timeFormat       = null;
571        }
572    
573    
574        // Determine whether any warm-up intervals should be run.
575        final long totalIntervals;
576        final boolean warmUp;
577        int remainingWarmUpIntervals = warmUpIntervals.getValue();
578        if (remainingWarmUpIntervals > 0)
579        {
580          warmUp = true;
581          totalIntervals = 0L + numIntervals.getValue() + remainingWarmUpIntervals;
582        }
583        else
584        {
585          warmUp = true;
586          totalIntervals = 0L + numIntervals.getValue();
587        }
588    
589    
590        // Create the table that will be used to format the output.
591        final OutputFormat outputFormat;
592        if (csvFormat.isPresent())
593        {
594          outputFormat = OutputFormat.CSV;
595        }
596        else
597        {
598          outputFormat = OutputFormat.COLUMNS;
599        }
600    
601        final ColumnFormatter formatter = new ColumnFormatter(includeTimestamp,
602             timeFormat, outputFormat, " ",
603             new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
604                      "Auths/Sec"),
605             new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
606                      "Avg Dur ms"),
607             new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
608                      "Errors/Sec"),
609             new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
610                      "Auths/Sec"),
611             new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
612                      "Avg Dur ms"));
613    
614    
615        // Create values to use for statistics collection.
616        final AtomicLong        authCounter   = new AtomicLong(0L);
617        final AtomicLong        errorCounter  = new AtomicLong(0L);
618        final AtomicLong        authDurations = new AtomicLong(0L);
619        final ResultCodeCounter rcCounter     = new ResultCodeCounter();
620    
621    
622        // Determine the length of each interval in milliseconds.
623        final long intervalMillis = 1000L * collectionInterval.getValue();
624    
625    
626        // Create the threads to use for the searches.
627        final CyclicBarrier barrier = new CyclicBarrier(numThreads.getValue() + 1);
628        final AuthRateThread[] threads = new AuthRateThread[numThreads.getValue()];
629        for (int i=0; i < threads.length; i++)
630        {
631          final LDAPConnection searchConnection;
632          final LDAPConnection bindConnection;
633          try
634          {
635            searchConnection = getConnection();
636            bindConnection   = getConnection();
637          }
638          catch (LDAPException le)
639          {
640            err("Unable to connect to the directory server:  ",
641                getExceptionMessage(le));
642            return le.getResultCode();
643          }
644    
645          threads[i] = new AuthRateThread(this, i, searchConnection, bindConnection,
646               dnPattern, scopeArg.getValue(), filterPattern, attrs,
647               userPassword.getValue(), authType.getValue(), barrier, authCounter,
648               authDurations, errorCounter, rcCounter, fixedRateBarrier);
649          threads[i].start();
650        }
651    
652    
653        // Display the table header.
654        for (final String headerLine : formatter.getHeaderLines(true))
655        {
656          out(headerLine);
657        }
658    
659    
660        // Indicate that the threads can start running.
661        try
662        {
663          barrier.await();
664        } catch (Exception e) {}
665        long overallStartTime = System.nanoTime();
666        long nextIntervalStartTime = System.currentTimeMillis() + intervalMillis;
667    
668    
669        boolean setOverallStartTime = false;
670        long    lastDuration        = 0L;
671        long    lastNumErrors       = 0L;
672        long    lastNumAuths        = 0L;
673        long    lastEndTime         = System.nanoTime();
674        for (long i=0; i < totalIntervals; i++)
675        {
676          final long startTimeMillis = System.currentTimeMillis();
677          final long sleepTimeMillis = nextIntervalStartTime - startTimeMillis;
678          nextIntervalStartTime += intervalMillis;
679          try
680          {
681            if (sleepTimeMillis > 0)
682            {
683              Thread.sleep(sleepTimeMillis);
684            }
685          } catch (Exception e) {}
686    
687          final long endTime          = System.nanoTime();
688          final long intervalDuration = endTime - lastEndTime;
689    
690          final long numAuths;
691          final long numErrors;
692          final long totalDuration;
693          if (warmUp && (remainingWarmUpIntervals > 0))
694          {
695            numAuths      = authCounter.getAndSet(0L);
696            numErrors     = errorCounter.getAndSet(0L);
697            totalDuration = authDurations.getAndSet(0L);
698          }
699          else
700          {
701            numAuths      = authCounter.get();
702            numErrors     = errorCounter.get();
703            totalDuration = authDurations.get();
704          }
705    
706          final long recentNumAuths  = numAuths - lastNumAuths;
707          final long recentNumErrors = numErrors - lastNumErrors;
708          final long recentDuration = totalDuration - lastDuration;
709    
710          final double numSeconds = intervalDuration / 1000000000.0d;
711          final double recentAuthRate = recentNumAuths / numSeconds;
712          final double recentErrorRate  = recentNumErrors / numSeconds;
713    
714          final double recentAvgDuration;
715          if (recentNumAuths > 0L)
716          {
717            recentAvgDuration = 1.0d * recentDuration / recentNumAuths / 1000000;
718          }
719          else
720          {
721            recentAvgDuration = 0.0d;
722          }
723    
724          if (warmUp && (remainingWarmUpIntervals > 0))
725          {
726            out(formatter.formatRow(recentAuthRate, recentAvgDuration,
727                 recentErrorRate, "warming up", "warming up"));
728    
729            remainingWarmUpIntervals--;
730            if (remainingWarmUpIntervals == 0)
731            {
732              out("Warm-up completed.  Beginning overall statistics collection.");
733              setOverallStartTime = true;
734            }
735          }
736          else
737          {
738            if (setOverallStartTime)
739            {
740              overallStartTime    = lastEndTime;
741              setOverallStartTime = false;
742            }
743    
744            final double numOverallSeconds =
745                 (endTime - overallStartTime) / 1000000000.0d;
746            final double overallAuthRate = numAuths / numOverallSeconds;
747    
748            final double overallAvgDuration;
749            if (numAuths > 0L)
750            {
751              overallAvgDuration = 1.0d * totalDuration / numAuths / 1000000;
752            }
753            else
754            {
755              overallAvgDuration = 0.0d;
756            }
757    
758            out(formatter.formatRow(recentAuthRate, recentAvgDuration,
759                 recentErrorRate, overallAuthRate, overallAvgDuration));
760    
761            lastNumAuths    = numAuths;
762            lastNumErrors   = numErrors;
763            lastDuration    = totalDuration;
764          }
765    
766          final List<ObjectPair<ResultCode,Long>> rcCounts =
767               rcCounter.getCounts(true);
768          if ((! suppressErrorsArgument.isPresent()) && (! rcCounts.isEmpty()))
769          {
770            err("\tError Results:");
771            for (final ObjectPair<ResultCode,Long> p : rcCounts)
772            {
773              err("\t", p.getFirst().getName(), ":  ", p.getSecond());
774            }
775          }
776    
777          lastEndTime = endTime;
778        }
779    
780    
781        // Stop all of the threads.
782        ResultCode resultCode = ResultCode.SUCCESS;
783        for (final AuthRateThread t : threads)
784        {
785          final ResultCode r = t.stopRunning();
786          if (resultCode == ResultCode.SUCCESS)
787          {
788            resultCode = r;
789          }
790        }
791    
792        return resultCode;
793      }
794    
795    
796    
797      /**
798       * {@inheritDoc}
799       */
800      @Override()
801      public LinkedHashMap<String[],String> getExampleUsages()
802      {
803        final LinkedHashMap<String[],String> examples =
804             new LinkedHashMap<String[],String>(1);
805    
806        final String[] args =
807        {
808          "--hostname", "server.example.com",
809          "--port", "389",
810          "--bindDN", "uid=admin,dc=example,dc=com",
811          "--bindPassword", "password",
812          "--baseDN", "dc=example,dc=com",
813          "--scope", "sub",
814          "--filter", "(uid=user.[1-1000000])",
815          "--credentials", "password",
816          "--numThreads", "10"
817        };
818        final String description =
819             "Test authentication performance by searching randomly across a set " +
820             "of one million users located below 'dc=example,dc=com' with ten " +
821             "concurrent threads and performing simple binds with a password of " +
822             "'password'.  The searches will be performed anonymously.";
823        examples.put(args, description);
824    
825        return examples;
826      }
827    }