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 */
021package com.unboundid.ldap.sdk.examples;
022
023
024
025import java.io.IOException;
026import java.io.OutputStream;
027import java.io.Serializable;
028import java.text.ParseException;
029import java.util.LinkedHashMap;
030import java.util.LinkedHashSet;
031import java.util.List;
032import java.util.concurrent.CyclicBarrier;
033import java.util.concurrent.atomic.AtomicBoolean;
034import java.util.concurrent.atomic.AtomicLong;
035
036import com.unboundid.ldap.sdk.LDAPConnection;
037import com.unboundid.ldap.sdk.LDAPConnectionOptions;
038import com.unboundid.ldap.sdk.LDAPException;
039import com.unboundid.ldap.sdk.ResultCode;
040import com.unboundid.ldap.sdk.SearchScope;
041import com.unboundid.ldap.sdk.Version;
042import com.unboundid.util.ColumnFormatter;
043import com.unboundid.util.FixedRateBarrier;
044import com.unboundid.util.FormattableColumn;
045import com.unboundid.util.HorizontalAlignment;
046import com.unboundid.util.LDAPCommandLineTool;
047import com.unboundid.util.ObjectPair;
048import com.unboundid.util.OutputFormat;
049import com.unboundid.util.RateAdjustor;
050import com.unboundid.util.ResultCodeCounter;
051import com.unboundid.util.ThreadSafety;
052import com.unboundid.util.ThreadSafetyLevel;
053import com.unboundid.util.ValuePattern;
054import com.unboundid.util.WakeableSleeper;
055import com.unboundid.util.args.ArgumentException;
056import com.unboundid.util.args.ArgumentParser;
057import com.unboundid.util.args.BooleanArgument;
058import com.unboundid.util.args.FileArgument;
059import com.unboundid.util.args.IntegerArgument;
060import com.unboundid.util.args.ScopeArgument;
061import com.unboundid.util.args.StringArgument;
062
063import static com.unboundid.util.Debug.*;
064import 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)
157public 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 supports the use of a properties file for
375   * specifying default values for arguments that aren't specified on the
376   * command line.
377   *
378   * @return  {@code true} if this tool supports the use of a properties file
379   *          for specifying default values for arguments that aren't specified
380   *          on the command line, or {@code false} if not.
381   */
382  @Override()
383  public boolean supportsPropertiesFile()
384  {
385    return true;
386  }
387
388
389
390  /**
391   * Indicates whether the LDAP-specific arguments should include alternate
392   * versions of all long identifiers that consist of multiple words so that
393   * they are available in both camelCase and dash-separated versions.
394   *
395   * @return  {@code true} if this tool should provide multiple versions of
396   *          long identifiers for LDAP-specific arguments, or {@code false} if
397   *          not.
398   */
399  @Override()
400  protected boolean includeAlternateLongIdentifiers()
401  {
402    return true;
403  }
404
405
406
407  /**
408   * Adds the arguments used by this program that aren't already provided by the
409   * generic {@code LDAPCommandLineTool} framework.
410   *
411   * @param  parser  The argument parser to which the arguments should be added.
412   *
413   * @throws  ArgumentException  If a problem occurs while adding the arguments.
414   */
415  @Override()
416  public void addNonLDAPArguments(final ArgumentParser parser)
417         throws ArgumentException
418  {
419    String description = "The base DN to use for the searches.  It may be a " +
420         "simple DN or a value pattern to specify a range of DNs (e.g., " +
421         "\"uid=user.[1-1000],ou=People,dc=example,dc=com\").  See " +
422         ValuePattern.PUBLIC_JAVADOC_URL + " for complete details about the " +
423         "value pattern syntax.  This must be provided.";
424    baseDN = new StringArgument('b', "baseDN", true, 1, "{dn}", description);
425    baseDN.setArgumentGroupName("Search and Authentication Arguments");
426    baseDN.addLongIdentifier("base-dn");
427    parser.addArgument(baseDN);
428
429
430    description = "The scope to use for the searches.  It should be 'base', " +
431                  "'one', 'sub', or 'subord'.  If this is not provided, a " +
432                  "default scope of 'sub' will be used.";
433    scopeArg = new ScopeArgument('s', "scope", false, "{scope}", description,
434                                 SearchScope.SUB);
435    scopeArg.setArgumentGroupName("Search and Authentication Arguments");
436    parser.addArgument(scopeArg);
437
438
439    description = "The filter to use for the searches.  It may be a simple " +
440                  "filter or a value pattern to specify a range of filters " +
441                  "(e.g., \"(uid=user.[1-1000])\").  See " +
442                  ValuePattern.PUBLIC_JAVADOC_URL + " for complete details " +
443                  "about the value pattern syntax.  This must be provided.";
444    filter = new StringArgument('f', "filter", true, 1, "{filter}",
445                                description);
446    filter.setArgumentGroupName("Search and Authentication Arguments");
447    parser.addArgument(filter);
448
449
450    description = "The name of an attribute to include in entries returned " +
451                  "from the searches.  Multiple attributes may be requested " +
452                  "by providing this argument multiple times.  If no return " +
453                  "attributes are specified, then entries will be returned " +
454                  "with all user attributes.";
455    attributes = new StringArgument('A', "attribute", false, 0, "{name}",
456                                    description);
457    attributes.setArgumentGroupName("Search and Authentication Arguments");
458    parser.addArgument(attributes);
459
460
461    description = "The password to use when binding as the users returned " +
462                  "from the searches.  This must be provided.";
463    userPassword = new StringArgument('C', "credentials", true, 1, "{password}",
464                                      description);
465    userPassword.setArgumentGroupName("Search and Authentication Arguments");
466    parser.addArgument(userPassword);
467
468
469    description = "The type of authentication to perform.  Allowed values " +
470                  "are:  SIMPLE, CRAM-MD5, DIGEST-MD5, and PLAIN.  If no "+
471                  "value is provided, then SIMPLE authentication will be " +
472                  "performed.";
473    final LinkedHashSet<String> allowedAuthTypes = new LinkedHashSet<String>(4);
474    allowedAuthTypes.add("simple");
475    allowedAuthTypes.add("cram-md5");
476    allowedAuthTypes.add("digest-md5");
477    allowedAuthTypes.add("plain");
478    authType = new StringArgument('a', "authType", true, 1, "{authType}",
479                                  description, allowedAuthTypes, "simple");
480    authType.setArgumentGroupName("Search and Authentication Arguments");
481    authType.addLongIdentifier("auth-type");
482    parser.addArgument(authType);
483
484
485    description = "The number of threads to use to perform the " +
486                  "authentication processing.  If this is not provided, then " +
487                  "a default of one thread will be used.";
488    numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}",
489                                     description, 1, Integer.MAX_VALUE, 1);
490    numThreads.setArgumentGroupName("Rate Management Arguments");
491    numThreads.addLongIdentifier("num-threads");
492    parser.addArgument(numThreads);
493
494
495    description = "The length of time in seconds between output lines.  If " +
496                  "this is not provided, then a default interval of five " +
497                  "seconds will be used.";
498    collectionInterval = new IntegerArgument('i', "intervalDuration", true, 1,
499                                             "{num}", description, 1,
500                                             Integer.MAX_VALUE, 5);
501    collectionInterval.setArgumentGroupName("Rate Management Arguments");
502    collectionInterval.addLongIdentifier("interval-duration");
503    parser.addArgument(collectionInterval);
504
505
506    description = "The maximum number of intervals for which to run.  If " +
507                  "this is not provided, then the tool will run until it is " +
508                  "interrupted.";
509    numIntervals = new IntegerArgument('I', "numIntervals", true, 1, "{num}",
510                                       description, 1, Integer.MAX_VALUE,
511                                       Integer.MAX_VALUE);
512    numIntervals.setArgumentGroupName("Rate Management Arguments");
513    numIntervals.addLongIdentifier("num-intervals");
514    parser.addArgument(numIntervals);
515
516    description = "The target number of authorizations to perform per " +
517                  "second.  It is still necessary to specify a sufficient " +
518                  "number of threads for achieving this rate.  If neither " +
519                  "this option nor --variableRateData is provided, then the " +
520                  "tool will run at the maximum rate for the specified " +
521                  "number of threads.";
522    ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1,
523                                        "{auths-per-second}", description,
524                                        1, Integer.MAX_VALUE);
525    ratePerSecond.setArgumentGroupName("Rate Management Arguments");
526    ratePerSecond.addLongIdentifier("rate-per-second");
527    parser.addArgument(ratePerSecond);
528
529    final String variableRateDataArgName = "variableRateData";
530    final String generateSampleRateFileArgName = "generateSampleRateFile";
531    description = RateAdjustor.getVariableRateDataArgumentDescription(
532         generateSampleRateFileArgName);
533    variableRateData = new FileArgument(null, variableRateDataArgName, false, 1,
534                                        "{path}", description, true, true, true,
535                                        false);
536    variableRateData.setArgumentGroupName("Rate Management Arguments");
537    variableRateData.addLongIdentifier("variable-rate-data");
538    parser.addArgument(variableRateData);
539
540    description = RateAdjustor.getGenerateSampleVariableRateFileDescription(
541         variableRateDataArgName);
542    sampleRateFile = new FileArgument(null, generateSampleRateFileArgName,
543                                      false, 1, "{path}", description, false,
544                                      true, true, false);
545    sampleRateFile.setArgumentGroupName("Rate Management Arguments");
546    sampleRateFile.addLongIdentifier("generate-sample-rate-file");
547    sampleRateFile.setUsageArgument(true);
548    parser.addArgument(sampleRateFile);
549    parser.addExclusiveArgumentSet(variableRateData, sampleRateFile);
550
551    description = "The number of intervals to complete before beginning " +
552                  "overall statistics collection.  Specifying a nonzero " +
553                  "number of warm-up intervals gives the client and server " +
554                  "a chance to warm up without skewing performance results.";
555    warmUpIntervals = new IntegerArgument(null, "warmUpIntervals", true, 1,
556         "{num}", description, 0, Integer.MAX_VALUE, 0);
557    warmUpIntervals.setArgumentGroupName("Rate Management Arguments");
558    warmUpIntervals.addLongIdentifier("warm-up-intervals");
559    parser.addArgument(warmUpIntervals);
560
561    description = "Indicates the format to use for timestamps included in " +
562                  "the output.  A value of 'none' indicates that no " +
563                  "timestamps should be included.  A value of 'with-date' " +
564                  "indicates that both the date and the time should be " +
565                  "included.  A value of 'without-date' indicates that only " +
566                  "the time should be included.";
567    final LinkedHashSet<String> allowedFormats = new LinkedHashSet<String>(3);
568    allowedFormats.add("none");
569    allowedFormats.add("with-date");
570    allowedFormats.add("without-date");
571    timestampFormat = new StringArgument(null, "timestampFormat", true, 1,
572         "{format}", description, allowedFormats, "none");
573    timestampFormat.addLongIdentifier("timestamp-format");
574    parser.addArgument(timestampFormat);
575
576    description = "Indicates that information about the result codes for " +
577                  "failed operations should not be displayed.";
578    suppressErrorsArgument = new BooleanArgument(null,
579         "suppressErrorResultCodes", 1, description);
580    suppressErrorsArgument.addLongIdentifier("suppress-error-result-codes");
581    parser.addArgument(suppressErrorsArgument);
582
583    description = "Generate output in CSV format rather than a " +
584                  "display-friendly format";
585    csvFormat = new BooleanArgument('c', "csv", 1, description);
586    parser.addArgument(csvFormat);
587
588    description = "Specifies the seed to use for the random number generator.";
589    randomSeed = new IntegerArgument('R', "randomSeed", false, 1, "{value}",
590         description);
591    randomSeed.addLongIdentifier("random-seed");
592    parser.addArgument(randomSeed);
593  }
594
595
596
597  /**
598   * Indicates whether this tool supports creating connections to multiple
599   * servers.  If it is to support multiple servers, then the "--hostname" and
600   * "--port" arguments will be allowed to be provided multiple times, and
601   * will be required to be provided the same number of times.  The same type of
602   * communication security and bind credentials will be used for all servers.
603   *
604   * @return  {@code true} if this tool supports creating connections to
605   *          multiple servers, or {@code false} if not.
606   */
607  @Override()
608  protected boolean supportsMultipleServers()
609  {
610    return true;
611  }
612
613
614
615  /**
616   * Retrieves the connection options that should be used for connections
617   * created for use with this tool.
618   *
619   * @return  The connection options that should be used for connections created
620   *          for use with this tool.
621   */
622  @Override()
623  public LDAPConnectionOptions getConnectionOptions()
624  {
625    final LDAPConnectionOptions options = new LDAPConnectionOptions();
626    options.setUseSynchronousMode(true);
627    return options;
628  }
629
630
631
632  /**
633   * Performs the actual processing for this tool.  In this case, it gets a
634   * connection to the directory server and uses it to perform the requested
635   * searches.
636   *
637   * @return  The result code for the processing that was performed.
638   */
639  @Override()
640  public ResultCode doToolProcessing()
641  {
642    runningThread = Thread.currentThread();
643
644    try
645    {
646      return doToolProcessingInternal();
647    }
648    finally
649    {
650      runningThread = null;
651    }
652  }
653
654
655
656  /**
657   * Performs the actual processing for this tool.  In this case, it gets a
658   * connection to the directory server and uses it to perform the requested
659   * searches.
660   *
661   * @return  The result code for the processing that was performed.
662   */
663  private ResultCode doToolProcessingInternal()
664  {
665    // If the sample rate file argument was specified, then generate the sample
666    // variable rate data file and return.
667    if (sampleRateFile.isPresent())
668    {
669      try
670      {
671        RateAdjustor.writeSampleVariableRateFile(sampleRateFile.getValue());
672        return ResultCode.SUCCESS;
673      }
674      catch (final Exception e)
675      {
676        debugException(e);
677        err("An error occurred while trying to write sample variable data " +
678             "rate file '", sampleRateFile.getValue().getAbsolutePath(),
679             "':  ", getExceptionMessage(e));
680        return ResultCode.LOCAL_ERROR;
681      }
682    }
683
684
685    // Determine the random seed to use.
686    final Long seed;
687    if (randomSeed.isPresent())
688    {
689      seed = Long.valueOf(randomSeed.getValue());
690    }
691    else
692    {
693      seed = null;
694    }
695
696    // Create value patterns for the base DN and filter.
697    final ValuePattern dnPattern;
698    try
699    {
700      dnPattern = new ValuePattern(baseDN.getValue(), seed);
701    }
702    catch (ParseException pe)
703    {
704      debugException(pe);
705      err("Unable to parse the base DN value pattern:  ", pe.getMessage());
706      return ResultCode.PARAM_ERROR;
707    }
708
709    final ValuePattern filterPattern;
710    try
711    {
712      filterPattern = new ValuePattern(filter.getValue(), seed);
713    }
714    catch (ParseException pe)
715    {
716      debugException(pe);
717      err("Unable to parse the filter pattern:  ", pe.getMessage());
718      return ResultCode.PARAM_ERROR;
719    }
720
721
722    // Get the attributes to return.
723    final String[] attrs;
724    if (attributes.isPresent())
725    {
726      final List<String> attrList = attributes.getValues();
727      attrs = new String[attrList.size()];
728      attrList.toArray(attrs);
729    }
730    else
731    {
732      attrs = NO_STRINGS;
733    }
734
735
736    // If the --ratePerSecond option was specified, then limit the rate
737    // accordingly.
738    FixedRateBarrier fixedRateBarrier = null;
739    if (ratePerSecond.isPresent() || variableRateData.isPresent())
740    {
741      // We might not have a rate per second if --variableRateData is specified.
742      // The rate typically doesn't matter except when we have warm-up
743      // intervals.  In this case, we'll run at the max rate.
744      final int intervalSeconds = collectionInterval.getValue();
745      final int ratePerInterval =
746           (ratePerSecond.getValue() == null)
747           ? Integer.MAX_VALUE
748           : ratePerSecond.getValue() * intervalSeconds;
749      fixedRateBarrier =
750           new FixedRateBarrier(1000L * intervalSeconds, ratePerInterval);
751    }
752
753
754    // If --variableRateData was specified, then initialize a RateAdjustor.
755    RateAdjustor rateAdjustor = null;
756    if (variableRateData.isPresent())
757    {
758      try
759      {
760        rateAdjustor = RateAdjustor.newInstance(fixedRateBarrier,
761             ratePerSecond.getValue(), variableRateData.getValue());
762      }
763      catch (IOException e)
764      {
765        debugException(e);
766        err("Initializing the variable rates failed: " + e.getMessage());
767        return ResultCode.PARAM_ERROR;
768      }
769      catch (IllegalArgumentException e)
770      {
771        debugException(e);
772        err("Initializing the variable rates failed: " + e.getMessage());
773        return ResultCode.PARAM_ERROR;
774      }
775    }
776
777
778    // Determine whether to include timestamps in the output and if so what
779    // format should be used for them.
780    final boolean includeTimestamp;
781    final String timeFormat;
782    if (timestampFormat.getValue().equalsIgnoreCase("with-date"))
783    {
784      includeTimestamp = true;
785      timeFormat       = "dd/MM/yyyy HH:mm:ss";
786    }
787    else if (timestampFormat.getValue().equalsIgnoreCase("without-date"))
788    {
789      includeTimestamp = true;
790      timeFormat       = "HH:mm:ss";
791    }
792    else
793    {
794      includeTimestamp = false;
795      timeFormat       = null;
796    }
797
798
799    // Determine whether any warm-up intervals should be run.
800    final long totalIntervals;
801    final boolean warmUp;
802    int remainingWarmUpIntervals = warmUpIntervals.getValue();
803    if (remainingWarmUpIntervals > 0)
804    {
805      warmUp = true;
806      totalIntervals = 0L + numIntervals.getValue() + remainingWarmUpIntervals;
807    }
808    else
809    {
810      warmUp = true;
811      totalIntervals = 0L + numIntervals.getValue();
812    }
813
814
815    // Create the table that will be used to format the output.
816    final OutputFormat outputFormat;
817    if (csvFormat.isPresent())
818    {
819      outputFormat = OutputFormat.CSV;
820    }
821    else
822    {
823      outputFormat = OutputFormat.COLUMNS;
824    }
825
826    final ColumnFormatter formatter = new ColumnFormatter(includeTimestamp,
827         timeFormat, outputFormat, " ",
828         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
829                  "Auths/Sec"),
830         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
831                  "Avg Dur ms"),
832         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
833                  "Errors/Sec"),
834         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
835                  "Auths/Sec"),
836         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
837                  "Avg Dur ms"));
838
839
840    // Create values to use for statistics collection.
841    final AtomicLong        authCounter   = new AtomicLong(0L);
842    final AtomicLong        errorCounter  = new AtomicLong(0L);
843    final AtomicLong        authDurations = new AtomicLong(0L);
844    final ResultCodeCounter rcCounter     = new ResultCodeCounter();
845
846
847    // Determine the length of each interval in milliseconds.
848    final long intervalMillis = 1000L * collectionInterval.getValue();
849
850
851    // Create the threads to use for the searches.
852    final CyclicBarrier barrier = new CyclicBarrier(numThreads.getValue() + 1);
853    final AuthRateThread[] threads = new AuthRateThread[numThreads.getValue()];
854    for (int i=0; i < threads.length; i++)
855    {
856      final LDAPConnection searchConnection;
857      final LDAPConnection bindConnection;
858      try
859      {
860        searchConnection = getConnection();
861        bindConnection   = getConnection();
862      }
863      catch (LDAPException le)
864      {
865        debugException(le);
866        err("Unable to connect to the directory server:  ",
867            getExceptionMessage(le));
868        return le.getResultCode();
869      }
870
871      threads[i] = new AuthRateThread(this, i, searchConnection, bindConnection,
872           dnPattern, scopeArg.getValue(), filterPattern, attrs,
873           userPassword.getValue(), authType.getValue(), barrier, authCounter,
874           authDurations, errorCounter, rcCounter, fixedRateBarrier);
875      threads[i].start();
876    }
877
878
879    // Display the table header.
880    for (final String headerLine : formatter.getHeaderLines(true))
881    {
882      out(headerLine);
883    }
884
885
886    // Start the RateAdjustor before the threads so that the initial value is
887    // in place before any load is generated unless we're doing a warm-up in
888    // which case, we'll start it after the warm-up is complete.
889    if ((rateAdjustor != null) && (remainingWarmUpIntervals <= 0))
890    {
891      rateAdjustor.start();
892    }
893
894
895    // Indicate that the threads can start running.
896    try
897    {
898      barrier.await();
899    }
900    catch (final Exception e)
901    {
902      debugException(e);
903    }
904
905    long overallStartTime = System.nanoTime();
906    long nextIntervalStartTime = System.currentTimeMillis() + intervalMillis;
907
908
909    boolean setOverallStartTime = false;
910    long    lastDuration        = 0L;
911    long    lastNumErrors       = 0L;
912    long    lastNumAuths        = 0L;
913    long    lastEndTime         = System.nanoTime();
914    for (long i=0; i < totalIntervals; i++)
915    {
916      if (rateAdjustor != null)
917      {
918        if (! rateAdjustor.isAlive())
919        {
920          out("All of the rates in " + variableRateData.getValue().getName() +
921              " have been completed.");
922          break;
923        }
924      }
925
926      final long startTimeMillis = System.currentTimeMillis();
927      final long sleepTimeMillis = nextIntervalStartTime - startTimeMillis;
928      nextIntervalStartTime += intervalMillis;
929      if (sleepTimeMillis > 0)
930      {
931        sleeper.sleep(sleepTimeMillis);
932      }
933
934      if (stopRequested.get())
935      {
936        break;
937      }
938
939      final long endTime          = System.nanoTime();
940      final long intervalDuration = endTime - lastEndTime;
941
942      final long numAuths;
943      final long numErrors;
944      final long totalDuration;
945      if (warmUp && (remainingWarmUpIntervals > 0))
946      {
947        numAuths      = authCounter.getAndSet(0L);
948        numErrors     = errorCounter.getAndSet(0L);
949        totalDuration = authDurations.getAndSet(0L);
950      }
951      else
952      {
953        numAuths      = authCounter.get();
954        numErrors     = errorCounter.get();
955        totalDuration = authDurations.get();
956      }
957
958      final long recentNumAuths  = numAuths - lastNumAuths;
959      final long recentNumErrors = numErrors - lastNumErrors;
960      final long recentDuration = totalDuration - lastDuration;
961
962      final double numSeconds = intervalDuration / 1000000000.0d;
963      final double recentAuthRate = recentNumAuths / numSeconds;
964      final double recentErrorRate  = recentNumErrors / numSeconds;
965
966      final double recentAvgDuration;
967      if (recentNumAuths > 0L)
968      {
969        recentAvgDuration = 1.0d * recentDuration / recentNumAuths / 1000000;
970      }
971      else
972      {
973        recentAvgDuration = 0.0d;
974      }
975
976      if (warmUp && (remainingWarmUpIntervals > 0))
977      {
978        out(formatter.formatRow(recentAuthRate, recentAvgDuration,
979             recentErrorRate, "warming up", "warming up"));
980
981        remainingWarmUpIntervals--;
982        if (remainingWarmUpIntervals == 0)
983        {
984          out("Warm-up completed.  Beginning overall statistics collection.");
985          setOverallStartTime = true;
986          if (rateAdjustor != null)
987          {
988            rateAdjustor.start();
989          }
990        }
991      }
992      else
993      {
994        if (setOverallStartTime)
995        {
996          overallStartTime    = lastEndTime;
997          setOverallStartTime = false;
998        }
999
1000        final double numOverallSeconds =
1001             (endTime - overallStartTime) / 1000000000.0d;
1002        final double overallAuthRate = numAuths / numOverallSeconds;
1003
1004        final double overallAvgDuration;
1005        if (numAuths > 0L)
1006        {
1007          overallAvgDuration = 1.0d * totalDuration / numAuths / 1000000;
1008        }
1009        else
1010        {
1011          overallAvgDuration = 0.0d;
1012        }
1013
1014        out(formatter.formatRow(recentAuthRate, recentAvgDuration,
1015             recentErrorRate, overallAuthRate, overallAvgDuration));
1016
1017        lastNumAuths    = numAuths;
1018        lastNumErrors   = numErrors;
1019        lastDuration    = totalDuration;
1020      }
1021
1022      final List<ObjectPair<ResultCode,Long>> rcCounts =
1023           rcCounter.getCounts(true);
1024      if ((! suppressErrorsArgument.isPresent()) && (! rcCounts.isEmpty()))
1025      {
1026        err("\tError Results:");
1027        for (final ObjectPair<ResultCode,Long> p : rcCounts)
1028        {
1029          err("\t", p.getFirst().getName(), ":  ", p.getSecond());
1030        }
1031      }
1032
1033      lastEndTime = endTime;
1034    }
1035
1036
1037    // Shut down the RateAdjustor if we have one.
1038    if (rateAdjustor != null)
1039    {
1040      rateAdjustor.shutDown();
1041    }
1042
1043
1044    // Stop all of the threads.
1045    ResultCode resultCode = ResultCode.SUCCESS;
1046    for (final AuthRateThread t : threads)
1047    {
1048      final ResultCode r = t.stopRunning();
1049      if (resultCode == ResultCode.SUCCESS)
1050      {
1051        resultCode = r;
1052      }
1053    }
1054
1055    return resultCode;
1056  }
1057
1058
1059
1060  /**
1061   * Requests that this tool stop running.  This method will attempt to wait
1062   * for all threads to complete before returning control to the caller.
1063   */
1064  public void stopRunning()
1065  {
1066    stopRequested.set(true);
1067    sleeper.wakeup();
1068
1069    final Thread t = runningThread;
1070    if (t != null)
1071    {
1072      try
1073      {
1074        t.join();
1075      }
1076      catch (final Exception e)
1077      {
1078        debugException(e);
1079      }
1080    }
1081  }
1082
1083
1084
1085  /**
1086   * {@inheritDoc}
1087   */
1088  @Override()
1089  public LinkedHashMap<String[],String> getExampleUsages()
1090  {
1091    final LinkedHashMap<String[],String> examples =
1092         new LinkedHashMap<String[],String>(2);
1093
1094    String[] args =
1095    {
1096      "--hostname", "server.example.com",
1097      "--port", "389",
1098      "--bindDN", "uid=admin,dc=example,dc=com",
1099      "--bindPassword", "password",
1100      "--baseDN", "dc=example,dc=com",
1101      "--scope", "sub",
1102      "--filter", "(uid=user.[1-1000000])",
1103      "--credentials", "password",
1104      "--numThreads", "10"
1105    };
1106    String description =
1107         "Test authentication performance by searching randomly across a set " +
1108         "of one million users located below 'dc=example,dc=com' with ten " +
1109         "concurrent threads and performing simple binds with a password of " +
1110         "'password'.  The searches will be performed anonymously.";
1111    examples.put(args, description);
1112
1113    args = new String[]
1114    {
1115      "--generateSampleRateFile", "variable-rate-data.txt"
1116    };
1117    description =
1118         "Generate a sample variable rate definition file that may be used " +
1119         "in conjunction with the --variableRateData argument.  The sample " +
1120         "file will include comments that describe the format for data to be " +
1121         "included in this file.";
1122    examples.put(args, description);
1123
1124    return examples;
1125  }
1126}