001/*
002 * Copyright 2020-2023 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2020-2023 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2020-2023 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.examples;
037
038
039
040import java.io.ByteArrayOutputStream;
041import java.io.File;
042import java.io.OutputStream;
043import java.util.ArrayList;
044import java.util.Collections;
045import java.util.LinkedHashMap;
046import java.util.List;
047import java.util.concurrent.atomic.AtomicReference;
048import javax.net.ServerSocketFactory;
049
050import com.unboundid.ldap.listener.CannedResponseRequestHandler;
051import com.unboundid.ldap.listener.LDAPListener;
052import com.unboundid.ldap.listener.LDAPListenerConfig;
053import com.unboundid.ldap.sdk.Attribute;
054import com.unboundid.ldap.sdk.Entry;
055import com.unboundid.ldap.sdk.ResultCode;
056import com.unboundid.ldap.sdk.SearchResultReference;
057import com.unboundid.ldap.sdk.Version;
058import com.unboundid.util.CommandLineTool;
059import com.unboundid.util.CryptoHelper;
060import com.unboundid.util.Debug;
061import com.unboundid.util.NotNull;
062import com.unboundid.util.Nullable;
063import com.unboundid.util.StaticUtils;
064import com.unboundid.util.ThreadSafety;
065import com.unboundid.util.ThreadSafetyLevel;
066import com.unboundid.util.args.ArgumentException;
067import com.unboundid.util.args.ArgumentParser;
068import com.unboundid.util.args.BooleanArgument;
069import com.unboundid.util.args.IntegerArgument;
070import com.unboundid.util.args.StringArgument;
071import com.unboundid.util.ssl.KeyStoreKeyManager;
072import com.unboundid.util.ssl.SSLUtil;
073import com.unboundid.util.ssl.TrustAllTrustManager;
074import com.unboundid.util.ssl.cert.ManageCertificates;
075
076
077
078/**
079 * This class implements a command-line tool that can be helpful in measuring
080 * the performance of the LDAP SDK itself.  It creates an {@link LDAPListener}
081 * that uses a {@link CannedResponseRequestHandler} to return a predefined
082 * response to any request that it receives.  It will then use one of the
083 * {@link SearchRate}, {@link ModRate}, {@link AuthRate}, or
084 * {@link SearchAndModRate} tools to issue concurrent operations against that
085 * listener instance as quickly as possible.
086 */
087@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
088public final class TestLDAPSDKPerformance
089       extends CommandLineTool
090{
091  /**
092   * The column at which to wrap long lines.
093   */
094  private static final int WRAP_COLUMN =
095       StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
096
097
098
099  /**
100   * The name for the authrate tool.
101   */
102  @NotNull private static final String TOOL_NAME_AUTHRATE = "authrate";
103
104
105
106  /**
107   * The name for the modrate tool.
108   */
109  @NotNull private static final String TOOL_NAME_MODRATE = "modrate";
110
111
112
113  /**
114   * The name for the search-and-mod-rate tool.
115   */
116  @NotNull private static final String TOOL_NAME_SEARCH_AND_MOD_RATE =
117       "search-and-mod-rate";
118
119
120
121  /**
122   * The name for the searchrate tool.
123   */
124  @NotNull private static final String TOOL_NAME_SEARCHRATE = "searchrate";
125
126
127
128  // A reference to the completion message for the tool.
129  @NotNull private final AtomicReference<String> completionMessage;
130
131  // The argument used to indicate that the authrate tool should only perform
132  // binds rather than both binds and searches.
133  @Nullable private BooleanArgument bindOnlyArg;
134
135  // The argument used to indicate whether to communicate with the listener
136  // over an SSL-encrypted connection.
137  @Nullable private BooleanArgument useSSLArg;
138
139  // The argument used to specify the number of entries to return in response to
140  // each search.
141  @Nullable private IntegerArgument entriesPerSearchArg;
142
143  // The argument used to specify the duration (in seconds) to use for each
144  // interval.
145  @Nullable private IntegerArgument intervalDurationSecondsArg;
146
147  // The argument used to specify the number of intervals to complete.
148  @Nullable private IntegerArgument numIntervalsArg;
149
150  // The argument used to specify the number of concurrent threads to use when
151  // searching.
152  @Nullable private IntegerArgument numThreadsArg;
153
154  // The argument used to specify the result code to return in response to each
155  // operation.
156  @Nullable private IntegerArgument resultCodeArg;
157
158  // The argument used to specify the number of warm-up intervals to use whose
159  // performance will be ignored in the final results.
160  @Nullable private IntegerArgument warmUpIntervalsArg;
161
162  // The argument used to specify the diagnostic message to include in each
163  // search result done message.
164  @Nullable private StringArgument diagnosticMessageArg;
165
166  // The argument used to specify the name of the tool to invoke for the
167  // performance testing.
168  @Nullable private StringArgument toolArg;
169
170
171
172  /**
173   * Runs this tool with the provided set of command-line arguments.
174   *
175   * @param  args  The command-line arguments provided to this program.
176   */
177  public static void main(@NotNull final String... args)
178  {
179    final ResultCode resultCode = main(System.out, System.err, args);
180    if (resultCode != ResultCode.SUCCESS)
181    {
182      System.exit(resultCode.intValue());
183    }
184  }
185
186
187
188  /**
189   * Runs this tool with the provided set of command-line arguments.
190   *
191   * @param  out   The output stream to use for standard output.  It may be
192   *               {@code null} if standard output should be suppressed.
193   * @param  err   The output stream to use for standard error.  It may be
194   *               {@code null} if standard error should be suppressed.
195   * @param  args  The command-line arguments provided to this program.
196   *
197   * @return  A result code indicating the result of tool processing.  Any
198   *          result code other than {@link ResultCode#SUCCESS} should be
199   *          considered an error.
200   */
201  @NotNull()
202  public static ResultCode main(@Nullable final OutputStream out,
203                                @Nullable final OutputStream err,
204                                @NotNull final String... args)
205  {
206    final TestLDAPSDKPerformance tool = new TestLDAPSDKPerformance(out, err);
207    return tool.runTool(args);
208  }
209
210
211
212  /**
213   * Creates a new instance of this command-line tool.
214   *
215   * @param  out  The output stream to use for standard output.  It may be
216   *              {@code null} if standard output should be suppressed.
217   * @param  err  The output stream to use for standard error.  It may be
218   *              {@code null} if standard error should be suppressed.
219   */
220  public TestLDAPSDKPerformance(@Nullable final OutputStream out,
221                                @Nullable final OutputStream err)
222  {
223    super(out, err);
224
225    completionMessage = new AtomicReference<>();
226
227    bindOnlyArg = null;
228    useSSLArg = null;
229    entriesPerSearchArg = null;
230    intervalDurationSecondsArg = null;
231    numIntervalsArg = null;
232    numThreadsArg = null;
233    resultCodeArg = null;
234    warmUpIntervalsArg = null;
235    diagnosticMessageArg = null;
236    toolArg = null;
237  }
238
239
240
241  /**
242   * Retrieves the name of this tool.  It should be the name of the command used
243   * to invoke this tool.
244   *
245   * @return  The name for this tool.
246   */
247  @Override()
248  @NotNull()
249  public String getToolName()
250  {
251    return "test-ldap-sdk-performance";
252  }
253
254
255
256  /**
257   * Retrieves a human-readable description for this tool.  If the description
258   * should include multiple paragraphs, then this method should return the text
259   * for the first paragraph, and the
260   * {@link #getAdditionalDescriptionParagraphs()} method should be used to
261   * return the text for the subsequent paragraphs.
262   *
263   * @return  A human-readable description for this tool.
264   */
265  @Override()
266  @NotNull()
267  public String getToolDescription()
268  {
269    return "Provides a mechanism to help test the performance of the LDAP SDK.";
270  }
271
272
273
274  /**
275   * Retrieves additional paragraphs that should be included in the description
276   * for this tool.  If the tool description should include multiple paragraphs,
277   * then the {@link #getToolDescription()} method should return the text of the
278   * first paragraph, and each item in the list returned by this method should
279   * be the text for each subsequent paragraph.  If the tool description should
280   * only have a single paragraph, then this method may return {@code null} or
281   * an empty list.
282   *
283   * @return  Additional paragraphs that should be included in the description
284   *          for this tool, or {@code null} or an empty list if only a single
285   *          description paragraph (whose text is returned by the
286   *          {@code getToolDescription} method) is needed.
287   */
288  @Override()
289  @NotNull()
290  public List<String> getAdditionalDescriptionParagraphs()
291  {
292    return Collections.singletonList(
293         "It creates an LDAP listener that uses a canned-response request " +
294              "handler to return a predefined response to all requests.  It" +
295              "then invokes another tool (either searchrate, modrate, " +
296              "authrate, or search-and-mod-rate) to issue concurrent " +
297              "requests against that listener as quickly as possible.");
298  }
299
300
301
302  /**
303   * Retrieves a version string for this tool, if available.
304   *
305   * @return  A version string for this tool, or {@code null} if none is
306   *          available.
307   */
308  @Override()
309  @NotNull()
310  public String getToolVersion()
311  {
312    return Version.NUMERIC_VERSION_STRING;
313  }
314
315
316
317  /**
318   * Indicates whether this tool should provide support for an interactive mode,
319   * in which the tool offers a mode in which the arguments can be provided in
320   * a text-driven menu rather than requiring them to be given on the command
321   * line.  If interactive mode is supported, it may be invoked using the
322   * "--interactive" argument.  Alternately, if interactive mode is supported
323   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
324   * interactive mode may be invoked by simply launching the tool without any
325   * arguments.
326   *
327   * @return  {@code true} if this tool supports interactive mode, or
328   *          {@code false} if not.
329   */
330  @Override()
331  public boolean supportsInteractiveMode()
332  {
333    return true;
334  }
335
336
337
338  /**
339   * Indicates whether this tool defaults to launching in interactive mode if
340   * the tool is invoked without any command-line arguments.  This will only be
341   * used if {@link #supportsInteractiveMode()} returns {@code true}.
342   *
343   * @return  {@code true} if this tool defaults to using interactive mode if
344   *          launched without any command-line arguments, or {@code false} if
345   *          not.
346   */
347  @Override()
348  public boolean defaultsToInteractiveMode()
349  {
350    return true;
351  }
352
353
354
355  /**
356   * Indicates whether this tool supports the use of a properties file for
357   * specifying default values for arguments that aren't specified on the
358   * command line.
359   *
360   * @return  {@code true} if this tool supports the use of a properties file
361   *          for specifying default values for arguments that aren't specified
362   *          on the command line, or {@code false} if not.
363   */
364  @Override()
365  public boolean supportsPropertiesFile()
366  {
367    return true;
368  }
369
370
371
372  /**
373   * Indicates whether this tool should provide arguments for redirecting output
374   * to a file.  If this method returns {@code true}, then the tool will offer
375   * an "--outputFile" argument that will specify the path to a file to which
376   * all standard output and standard error content will be written, and it will
377   * also offer a "--teeToStandardOut" argument that can only be used if the
378   * "--outputFile" argument is present and will cause all output to be written
379   * to both the specified output file and to standard output.
380   *
381   * @return  {@code true} if this tool should provide arguments for redirecting
382   *          output to a file, or {@code false} if not.
383   */
384  @Override()
385  protected boolean supportsOutputFile()
386  {
387    return true;
388  }
389
390
391
392  /**
393   * Retrieves an optional message that may provide additional information about
394   * the way that the tool completed its processing.  For example if the tool
395   * exited with an error message, it may be useful for this method to return
396   * that error message.
397   * <BR><BR>
398   * The message returned by this method is intended for purposes and is not
399   * meant to be parsed or programmatically interpreted.
400   *
401   * @return  An optional message that may provide additional information about
402   *          the completion state for this tool, or {@code null} if no
403   *          completion message is available.
404   */
405  @Override()
406  @Nullable()
407  protected String getToolCompletionMessage()
408  {
409    return completionMessage.get();
410  }
411
412
413
414  /**
415   * Adds the command-line arguments supported for use with this tool to the
416   * provided argument parser.  The tool may need to retain references to the
417   * arguments (and/or the argument parser, if trailing arguments are allowed)
418   * to it in order to obtain their values for use in later processing.
419   *
420   * @param  parser  The argument parser to which the arguments are to be added.
421   *
422   * @throws  ArgumentException  If a problem occurs while adding any of the
423   *                             tool-specific arguments to the provided
424   *                             argument parser.
425   */
426  @Override()
427  public void addToolArguments(@NotNull final ArgumentParser parser)
428         throws ArgumentException
429  {
430    toolArg = new StringArgument(null, "tool", true, 1,
431         "{searchrate|modrate|authrate|search-and-mod-rate}",
432         "The tool to invoke against the LDAP listener.  It may be one of " +
433              "searchrate, modrate, authrate, or search-and-mod-rate.  If " +
434              "this is not provided, then the searchrate tool will be invoked.",
435         StaticUtils.setOf(TOOL_NAME_SEARCHRATE,
436              TOOL_NAME_MODRATE,
437              TOOL_NAME_AUTHRATE,
438              TOOL_NAME_SEARCH_AND_MOD_RATE),
439         TOOL_NAME_SEARCHRATE);
440    toolArg.addLongIdentifier("toolName", true);
441    toolArg.addLongIdentifier("tool-name", true);
442    parser.addArgument(toolArg);
443
444
445    numThreadsArg = new IntegerArgument('t', "numThreads", true, 1, "{num}",
446         "The number of concurrent threads (each using its own connection) " +
447              "to use to process requests.  If this is not provided, then a " +
448              "single thread will be used.",
449         1, Integer.MAX_VALUE, 1);
450    numThreadsArg.addLongIdentifier("num-threads", true);
451    numThreadsArg.addLongIdentifier("threads", true);
452    parser.addArgument(numThreadsArg);
453
454
455    entriesPerSearchArg = new IntegerArgument(null, "entriesPerSearch", true,
456         1, "{num}",
457         "The number of entries to return in response to each search " +
458              "request.  If this is provided, the value must be between 0 " +
459              "and 100.  If it is not provided, then a single entry will be " +
460              "returned.",
461         0, 100, 1);
462    entriesPerSearchArg.addLongIdentifier("entries-per-search", true);
463    entriesPerSearchArg.addLongIdentifier("numEntries", true);
464    entriesPerSearchArg.addLongIdentifier("num-entries", true);
465    entriesPerSearchArg.addLongIdentifier("entries", true);
466    parser.addArgument(entriesPerSearchArg);
467
468
469    bindOnlyArg = new BooleanArgument(null, "bindOnly", 1,
470         "Indicates that the authrate tool should only issue bind requests.  " +
471              "If this is not provided, the authrate tool will perform both " +
472              "search and bind operations.  This argument will only be used " +
473              "in conjunction with the authrate tool.");
474    bindOnlyArg.addLongIdentifier("bind-only", true);
475    parser.addArgument(bindOnlyArg);
476
477
478    resultCodeArg = new IntegerArgument(null, "resultCode", true, 1,
479         "{intValue}",
480         "The integer value for the result code to return in response to " +
481              "each request.  If this is not provided, then a result code of " +
482              "0 (success) will be returned.",
483         0, Integer.MAX_VALUE, ResultCode.SUCCESS_INT_VALUE);
484    resultCodeArg.addLongIdentifier("result-code", true);
485    parser.addArgument(resultCodeArg);
486
487
488    diagnosticMessageArg = new StringArgument(null, "diagnosticMessage", false,
489         1, "{message}",
490         "The diagnostic message to return in response to each request.  If " +
491              "this is not provided, then no diagnostic message will be " +
492              "returned.");
493    diagnosticMessageArg.addLongIdentifier("diagnostic-message", true);
494    diagnosticMessageArg.addLongIdentifier("errorMessage", true);
495    diagnosticMessageArg.addLongIdentifier("error-message", true);
496    diagnosticMessageArg.addLongIdentifier("message", true);
497    parser.addArgument(diagnosticMessageArg);
498
499
500    useSSLArg = new BooleanArgument('Z', "useSSL", 1,
501         "Encrypt communication with SSL.  If this argument is not provided, " +
502              "then the communication will not be encrypted.");
503    useSSLArg.addLongIdentifier("use-ssl", true);
504    useSSLArg.addLongIdentifier("ssl", true);
505    useSSLArg.addLongIdentifier("useTLS", true);
506    useSSLArg.addLongIdentifier("use-tls", true);
507    useSSLArg.addLongIdentifier("tls", true);
508    parser.addArgument(useSSLArg);
509
510
511    numIntervalsArg = new IntegerArgument('I', "numIntervals", false, 1,
512         "{num}",
513         "The number of intervals to use when running the performance " +
514              "measurement tool.  If this argument is provided in " +
515              "conjunction with the --warmUpIntervals argument, then the " +
516              "warm-up intervals will not be included in this count, and the " +
517              "total number of intervals run will be the sum of the two " +
518              "values.  If this argument is not provided, then the tool will " +
519              "run until it is interrupted (e.g., by pressing Ctrl+C or by " +
520              "killing the underlying Java process).",
521         0, Integer.MAX_VALUE);
522    numIntervalsArg.addLongIdentifier("num-intervals", true);
523    numIntervalsArg.addLongIdentifier("intervals", true);
524    parser.addArgument(numIntervalsArg);
525
526
527    intervalDurationSecondsArg = new IntegerArgument('i',
528         "intervalDurationSeconds", true, 1, "{num}",
529         "The length of time in seconds to use for each tool interval (that " +
530              "is, the length of time between each line of output giving " +
531              "statistical information for operations processed in that " +
532              "interval).  If this is not provided, then a default interval " +
533              "duration of five seconds will be used.",
534         1, Integer.MAX_VALUE, 5);
535    intervalDurationSecondsArg.addLongIdentifier("interval-duration-seconds",
536         true);
537    intervalDurationSecondsArg.addLongIdentifier("intervalDuration", true);
538    intervalDurationSecondsArg.addLongIdentifier("interval-duration", true);
539    parser.addArgument(intervalDurationSecondsArg);
540
541
542    warmUpIntervalsArg = new IntegerArgument(null, "warmUpIntervals", true, 1,
543         "{num}",
544         "The number of intervals to run before starting to actually " +
545              "collect statistics to include in the final result.  This can " +
546              "give the JVM and JIT a chance to identify and optimize " +
547              "hotspots in the code for the best and most stable " +
548              "performance.  If this is not provided, then no warm-up " +
549              "intervals will be used and the tool will start collecting " +
550              "statistics right away.",
551         0, Integer.MAX_VALUE, 0);
552    warmUpIntervalsArg.addLongIdentifier("warm-up-intervals", true);
553    warmUpIntervalsArg.addLongIdentifier("warmup-intervals", true);
554    warmUpIntervalsArg.addLongIdentifier("warmUp", true);
555    warmUpIntervalsArg.addLongIdentifier("warm-up", true);
556    parser.addArgument(warmUpIntervalsArg);
557  }
558
559
560
561  /**
562   * Performs the core set of processing for this tool.
563   *
564   * @return  A result code that indicates whether the processing completed
565   *          successfully.
566   */
567  @Override()
568  @NotNull()
569  public ResultCode doToolProcessing()
570  {
571    // Create the socket factory to use for accepting connections.  If the
572    // --useSSL argument was provided, then create a temporary keystore and
573    // generate a certificate in it.
574    final ServerSocketFactory serverSocketFactory;
575    if (useSSLArg.isPresent())
576    {
577      try
578      {
579        final File keyStoreFile = File.createTempFile(
580             "test-ldap-sdk-performance-keystore-", ".jks");
581        keyStoreFile.deleteOnExit();
582        keyStoreFile.delete();
583
584        final ByteArrayOutputStream out = new ByteArrayOutputStream();
585        final ResultCode manageCertificatesResultCode =
586             ManageCertificates.main(null, out, out,
587                  "generate-self-signed-certificate",
588                  "--keystore", keyStoreFile.getAbsolutePath(),
589                  "--keystore-password", keyStoreFile.getAbsolutePath(),
590                  "--keystore-type", CryptoHelper.KEY_STORE_TYPE_JKS,
591                  "--alias", "server-cert",
592                  "--subject-dn", "CN=Test LDAP SDK Performance");
593        if (manageCertificatesResultCode != ResultCode.SUCCESS)
594        {
595          final String message = "ERROR:  Unable to use the " +
596               "manage-certificates tool to generate a self-signed server " +
597               "certificate to use for SSL communication.";
598          completionMessage.compareAndSet(null, message);
599          wrapErr(0, WRAP_COLUMN, message);
600          err();
601          wrapErr(0, WRAP_COLUMN, "The manage-certificates output was:");
602          err();
603          err(StaticUtils.toUTF8String(out.toByteArray()));
604          return manageCertificatesResultCode;
605        }
606
607        final SSLUtil sslUtil = new SSLUtil(
608             new KeyStoreKeyManager(keyStoreFile,
609                  keyStoreFile.getAbsolutePath().toCharArray(),
610                  CryptoHelper.KEY_STORE_TYPE_JKS, "server-cert"),
611             new TrustAllTrustManager());
612        serverSocketFactory = sslUtil.createSSLServerSocketFactory();
613      }
614      catch (final Exception e)
615      {
616        Debug.debugException(e);
617
618        final String message = "ERROR:  Unable to initialize support for SSL " +
619             "communication:  " + StaticUtils.getExceptionMessage(e);
620        completionMessage.compareAndSet(null, message);
621        wrapErr(0, WRAP_COLUMN, message);
622        return ResultCode.LOCAL_ERROR;
623      }
624    }
625    else
626    {
627      serverSocketFactory = ServerSocketFactory.getDefault();
628    }
629
630
631    // Create the search result entries to return in response to each search.
632    final int numEntries = entriesPerSearchArg.getValue();
633    final List<Entry> entries = new ArrayList<>(numEntries);
634    for (int i=1; i <= numEntries; i++)
635    {
636      entries.add(new Entry(
637           "uid=user." + i + ",ou=People,dc=example,dc=com",
638           new Attribute("objectClass", "top", "person", "organizationalPerson",
639                "inetOrgPerson"),
640           new Attribute("uid", "user." + i),
641           new Attribute("givenName", "User"),
642           new Attribute("sn", String.valueOf(i)),
643           new Attribute("cn", "User " + i),
644           new Attribute("mail", "user." + i + "@example.com"),
645           new Attribute("userPassword", "password")));
646    }
647
648
649    // Create a canned response request handler to use to return the responses.
650    final CannedResponseRequestHandler cannedResponseRequestHandler =
651         new CannedResponseRequestHandler(
652              ResultCode.valueOf(resultCodeArg.getValue()),
653              null, // Matched DN
654              diagnosticMessageArg.getValue(),
655              Collections.<String>emptyList(), // Referral URLs
656              entries,
657              Collections.<SearchResultReference>emptyList());
658
659
660    // Create the LDAP listener to handle the requests.
661    final LDAPListenerConfig listenerConfig =
662         new LDAPListenerConfig(0, cannedResponseRequestHandler);
663    listenerConfig.setServerSocketFactory(serverSocketFactory);
664
665    final LDAPListener ldapListener = new LDAPListener(listenerConfig);
666    try
667    {
668      ldapListener.startListening();
669    }
670    catch (final Exception e)
671    {
672      Debug.debugException(e);
673
674      final String message = "ERROR:  Unable to start listening for client " +
675           "connections:  " + StaticUtils.getExceptionMessage(e);
676      completionMessage.compareAndSet(null, message);
677      wrapErr(0, WRAP_COLUMN, message);
678      return ResultCode.LOCAL_ERROR;
679    }
680
681    try
682    {
683      final int listenPort = ldapListener.getListenPort();
684      final String toolName = StaticUtils.toLowerCase(toolArg.getValue());
685      switch (toolName)
686      {
687        case TOOL_NAME_SEARCHRATE:
688          return invokeSearchRate(listenPort);
689        case TOOL_NAME_MODRATE:
690          return invokeModRate(listenPort);
691        case TOOL_NAME_AUTHRATE:
692          return invokeAuthRate(listenPort);
693        case TOOL_NAME_SEARCH_AND_MOD_RATE:
694          return invokeSearchAndModRate(listenPort);
695        default:
696          // This should never happen.
697          final String message = "ERROR:  Unrecognized tool name:  " + toolName;
698          completionMessage.compareAndSet(null, message);
699          wrapErr(0, WRAP_COLUMN, message);
700          return ResultCode.PARAM_ERROR;
701      }
702    }
703    finally
704    {
705      ldapListener.shutDown(true);
706    }
707  }
708
709
710
711  /**
712   * Invokes the {@link SearchRate} tool with an appropriate set of arguments.
713   *
714   * @param  listenPort  The port on which the LDAP listener is listening.
715   *
716   * @return  The result code obtained from the {@code SearchRate} tool.
717   */
718  @NotNull()
719  private ResultCode invokeSearchRate(final int listenPort)
720  {
721    final List<String> searchRateArgs = new ArrayList<>();
722
723    searchRateArgs.add("--hostname");
724    searchRateArgs.add("localhost");
725
726    searchRateArgs.add("--port");
727    searchRateArgs.add(String.valueOf(listenPort));
728
729    if (useSSLArg.isPresent())
730    {
731      searchRateArgs.add("--useSSL");
732      searchRateArgs.add("--trustAll");
733    }
734
735    searchRateArgs.add("--baseDN");
736    searchRateArgs.add("dc=example,dc=com");
737
738    searchRateArgs.add("--scope");
739    searchRateArgs.add("sub");
740
741    searchRateArgs.add("--filter");
742    searchRateArgs.add("(objectClass=*)");
743
744    searchRateArgs.add("--numThreads");
745    searchRateArgs.add(String.valueOf(numThreadsArg.getValue()));
746
747    if (numIntervalsArg.isPresent())
748    {
749      searchRateArgs.add("--numIntervals");
750      searchRateArgs.add(String.valueOf(numIntervalsArg.getValue()));
751    }
752
753    if (intervalDurationSecondsArg.isPresent())
754    {
755      searchRateArgs.add("--intervalDuration");
756      searchRateArgs.add(String.valueOf(
757           intervalDurationSecondsArg.getValue()));
758    }
759
760    if (warmUpIntervalsArg.isPresent())
761    {
762      searchRateArgs.add("--warmUpIntervals");
763      searchRateArgs.add(String.valueOf(warmUpIntervalsArg.getValue()));
764    }
765
766    final String[] searchRateArgsArray =
767         searchRateArgs.toArray(StaticUtils.NO_STRINGS);
768
769    final SearchRate searchRate = new SearchRate(getOut(), getErr());
770
771    final ResultCode searchRateResultCode =
772         searchRate.runTool(searchRateArgsArray);
773    if (searchRateResultCode == ResultCode.SUCCESS)
774    {
775      final String message = "The searchrate tool completed successfully.";
776      completionMessage.compareAndSet(null, message);
777      wrapOut(0, WRAP_COLUMN, message);
778    }
779    else
780    {
781      final String message =
782           "ERROR:  The searchrate tool exited with error result code " +
783                searchRateResultCode + '.';
784      completionMessage.compareAndSet(null, message);
785      wrapErr(0, WRAP_COLUMN, message);
786    }
787
788    return searchRateResultCode;
789  }
790
791
792
793  /**
794   * Invokes the {@link ModRate} tool with an appropriate set of arguments.
795   *
796   * @param  listenPort  The port on which the LDAP listener is listening.
797   *
798   * @return  The result code obtained from the {@code ModRate} tool.
799   */
800  @NotNull()
801  private ResultCode invokeModRate(final int listenPort)
802  {
803    final List<String> modRateArgs = new ArrayList<>();
804
805    modRateArgs.add("--hostname");
806    modRateArgs.add("localhost");
807
808    modRateArgs.add("--port");
809    modRateArgs.add(String.valueOf(listenPort));
810
811    if (useSSLArg.isPresent())
812    {
813      modRateArgs.add("--useSSL");
814      modRateArgs.add("--trustAll");
815    }
816
817    modRateArgs.add("--entryDN");
818    modRateArgs.add("dc=example,dc=com");
819
820    modRateArgs.add("--attribute");
821    modRateArgs.add("description");
822
823    modRateArgs.add("--valuePattern");
824    modRateArgs.add("value");
825
826    modRateArgs.add("--numThreads");
827    modRateArgs.add(String.valueOf(numThreadsArg.getValue()));
828
829    if (numIntervalsArg.isPresent())
830    {
831      modRateArgs.add("--numIntervals");
832      modRateArgs.add(String.valueOf(numIntervalsArg.getValue()));
833    }
834
835    if (intervalDurationSecondsArg.isPresent())
836    {
837      modRateArgs.add("--intervalDuration");
838      modRateArgs.add(String.valueOf(
839           intervalDurationSecondsArg.getValue()));
840    }
841
842    if (warmUpIntervalsArg.isPresent())
843    {
844      modRateArgs.add("--warmUpIntervals");
845      modRateArgs.add(String.valueOf(warmUpIntervalsArg.getValue()));
846    }
847
848    final String[] modRateArgsArray =
849         modRateArgs.toArray(StaticUtils.NO_STRINGS);
850
851    final ModRate modRate = new ModRate(getOut(), getErr());
852
853    final ResultCode modRateResultCode =
854         modRate.runTool(modRateArgsArray);
855    if (modRateResultCode == ResultCode.SUCCESS)
856    {
857      final String message = "The modrate tool completed successfully.";
858      completionMessage.compareAndSet(null, message);
859      wrapOut(0, WRAP_COLUMN, message);
860    }
861    else
862    {
863      final String message =
864           "ERROR:  The modrate tool exited with error result code " +
865                modRateResultCode + '.';
866      completionMessage.compareAndSet(null, message);
867      wrapErr(0, WRAP_COLUMN, message);
868    }
869
870    return modRateResultCode;
871  }
872
873
874
875  /**
876   * Invokes the {@link AuthRate} tool with an appropriate set of arguments.
877   *
878   * @param  listenPort  The port on which the LDAP listener is listening.
879   *
880   * @return  The result code obtained from the {@code AuthRate} tool.
881   */
882  @NotNull()
883  private ResultCode invokeAuthRate(final int listenPort)
884  {
885    final List<String> authRateArgs = new ArrayList<>();
886
887    authRateArgs.add("--hostname");
888    authRateArgs.add("localhost");
889
890    authRateArgs.add("--port");
891    authRateArgs.add(String.valueOf(listenPort));
892
893    if (useSSLArg.isPresent())
894    {
895      authRateArgs.add("--useSSL");
896      authRateArgs.add("--trustAll");
897    }
898
899    if (bindOnlyArg.isPresent())
900    {
901      authRateArgs.add("--bindOnly");
902
903      authRateArgs.add("--baseDN");
904      authRateArgs.add("uid=user.1,ou=People,dc=example,dc=com");
905    }
906    else
907    {
908      authRateArgs.add("--baseDN");
909      authRateArgs.add("dc=example,dc=com");
910
911      authRateArgs.add("--scope");
912      authRateArgs.add("sub");
913
914      authRateArgs.add("--filter");
915      authRateArgs.add("(uid=user.1)");
916    }
917
918    authRateArgs.add("--credentials");
919    authRateArgs.add("password");
920
921    authRateArgs.add("--numThreads");
922    authRateArgs.add(String.valueOf(numThreadsArg.getValue()));
923
924    if (numIntervalsArg.isPresent())
925    {
926      authRateArgs.add("--numIntervals");
927      authRateArgs.add(String.valueOf(numIntervalsArg.getValue()));
928    }
929
930    if (intervalDurationSecondsArg.isPresent())
931    {
932      authRateArgs.add("--intervalDuration");
933      authRateArgs.add(String.valueOf(
934           intervalDurationSecondsArg.getValue()));
935    }
936
937    if (warmUpIntervalsArg.isPresent())
938    {
939      authRateArgs.add("--warmUpIntervals");
940      authRateArgs.add(String.valueOf(warmUpIntervalsArg.getValue()));
941    }
942
943    final String[] authRateArgsArray =
944         authRateArgs.toArray(StaticUtils.NO_STRINGS);
945
946    final AuthRate authRate = new AuthRate(getOut(), getErr());
947
948    final ResultCode authRateResultCode =
949         authRate.runTool(authRateArgsArray);
950    if (authRateResultCode == ResultCode.SUCCESS)
951    {
952      final String message = "The authrate tool completed successfully.";
953      completionMessage.compareAndSet(null, message);
954      wrapOut(0, WRAP_COLUMN, message);
955    }
956    else
957    {
958      final String message =
959           "ERROR:  The authrate tool exited with error result code " +
960                authRateResultCode + '.';
961      completionMessage.compareAndSet(null, message);
962      wrapErr(0, WRAP_COLUMN, message);
963    }
964
965    return authRateResultCode;
966  }
967
968
969
970  /**
971   * Invokes the {@link SearchAndModRate} tool with an appropriate set of
972   * arguments.
973   *
974   * @param  listenPort  The port on which the LDAP listener is listening.
975   *
976   * @return  The result code obtained from the {@code SearchAndModRate} tool.
977   */
978  @NotNull()
979  private ResultCode invokeSearchAndModRate(final int listenPort)
980  {
981    final List<String> searchAndModRateArgs = new ArrayList<>();
982
983    searchAndModRateArgs.add("--hostname");
984    searchAndModRateArgs.add("localhost");
985
986    searchAndModRateArgs.add("--port");
987    searchAndModRateArgs.add(String.valueOf(listenPort));
988
989    if (useSSLArg.isPresent())
990    {
991      searchAndModRateArgs.add("--useSSL");
992      searchAndModRateArgs.add("--trustAll");
993    }
994
995    searchAndModRateArgs.add("--baseDN");
996    searchAndModRateArgs.add("dc=example,dc=com");
997
998    searchAndModRateArgs.add("--scope");
999    searchAndModRateArgs.add("sub");
1000
1001    searchAndModRateArgs.add("--filter");
1002    searchAndModRateArgs.add("(objectClass=*)");
1003
1004    searchAndModRateArgs.add("--modifyAttribute");
1005    searchAndModRateArgs.add("description");
1006
1007    searchAndModRateArgs.add("--valueLength");
1008    searchAndModRateArgs.add("10");
1009
1010    searchAndModRateArgs.add("--numThreads");
1011    searchAndModRateArgs.add(String.valueOf(numThreadsArg.getValue()));
1012
1013    if (numIntervalsArg.isPresent())
1014    {
1015      searchAndModRateArgs.add("--numIntervals");
1016      searchAndModRateArgs.add(String.valueOf(numIntervalsArg.getValue()));
1017    }
1018
1019    if (intervalDurationSecondsArg.isPresent())
1020    {
1021      searchAndModRateArgs.add("--intervalDuration");
1022      searchAndModRateArgs.add(String.valueOf(
1023           intervalDurationSecondsArg.getValue()));
1024    }
1025
1026    if (warmUpIntervalsArg.isPresent())
1027    {
1028      searchAndModRateArgs.add("--warmUpIntervals");
1029      searchAndModRateArgs.add(String.valueOf(warmUpIntervalsArg.getValue()));
1030    }
1031
1032    final String[] searchAndModRateArgsArray =
1033         searchAndModRateArgs.toArray(StaticUtils.NO_STRINGS);
1034
1035    final SearchAndModRate searchAndModRate =
1036         new SearchAndModRate(getOut(), getErr());
1037
1038    final ResultCode searchAndModRateResultCode =
1039         searchAndModRate.runTool(searchAndModRateArgsArray);
1040    if (searchAndModRateResultCode == ResultCode.SUCCESS)
1041    {
1042      final String message =
1043           "The search-and-mod-rate tool completed successfully.";
1044      completionMessage.compareAndSet(null, message);
1045      wrapOut(0, WRAP_COLUMN, message);
1046    }
1047    else
1048    {
1049      final String message =
1050           "ERROR:  The search-and-mod-rate tool exited with error result " +
1051                "code " + searchAndModRateResultCode + '.';
1052      completionMessage.compareAndSet(null, message);
1053      wrapErr(0, WRAP_COLUMN, message);
1054    }
1055
1056    return searchAndModRateResultCode;
1057  }
1058
1059
1060
1061  /**
1062   * Retrieves a set of information that may be used to generate example usage
1063   * information.  Each element in the returned map should consist of a map
1064   * between an example set of arguments and a string that describes the
1065   * behavior of the tool when invoked with that set of arguments.
1066   *
1067   * @return  A set of information that may be used to generate example usage
1068   *          information.  It may be {@code null} or empty if no example usage
1069   *          information is available.
1070   */
1071  @Override()
1072  @NotNull()
1073  public LinkedHashMap<String[],String> getExampleUsages()
1074  {
1075    final LinkedHashMap<String[],String> examples = new LinkedHashMap<>();
1076
1077    examples.put(
1078         new String[]
1079         {
1080           "--numThreads", "10"
1081         },
1082         "Test LDAP SDK performance with the searchrate tool using ten " +
1083              "concurrent threads.  Communication will use an insecure " +
1084              "connection, and each search will return a success result with " +
1085              "a single matching entry.  The tool will continue to run until " +
1086              "it is interrupted.");
1087
1088    examples.put(
1089         new String[]
1090         {
1091           "--tool", "modrate",
1092           "--numThreads", "10",
1093           "--useSSL",
1094           "--resultCode", "32",
1095           "--diagnosticMessage", "The base entry does not exist",
1096           "--warmUpIntervals", "5",
1097           "--numIntervals", "10",
1098           "--intervalDurationSeconds", "5"
1099         },
1100         "Test LDAP SDK performance with the modrate tool using ten " +
1101              "concurrent threads over SSL-encrypted connections.  Each " +
1102              "modify will return an error result with a result code of 32 " +
1103              "(noSuchObject) and a diagnostic message of 'The target entry " +
1104              "does not exist'.  The tool will run five warm-up intervals " +
1105              "of five seconds each, and then ten 5-second intervals in " +
1106              "which it will capture statistics.  The tool will exit after " +
1107              "those last ten intervals have completed.");
1108
1109    return examples;
1110  }
1111}