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