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