001/*
002 * Copyright 2020-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2020-2024 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-2024 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   * Indicates whether this tool supports the ability to generate a debug log
394   * file.  If this method returns {@code true}, then the tool will expose
395   * additional arguments that can control debug logging.
396   *
397   * @return  {@code true} if this tool supports the ability to generate a debug
398   *          log file, or {@code false} if not.
399   */
400  @Override()
401  protected boolean supportsDebugLogging()
402  {
403    return true;
404  }
405
406
407
408  /**
409   * Retrieves an optional message that may provide additional information about
410   * the way that the tool completed its processing.  For example if the tool
411   * exited with an error message, it may be useful for this method to return
412   * that error message.
413   * <BR><BR>
414   * The message returned by this method is intended for purposes and is not
415   * meant to be parsed or programmatically interpreted.
416   *
417   * @return  An optional message that may provide additional information about
418   *          the completion state for this tool, or {@code null} if no
419   *          completion message is available.
420   */
421  @Override()
422  @Nullable()
423  protected String getToolCompletionMessage()
424  {
425    return completionMessage.get();
426  }
427
428
429
430  /**
431   * Adds the command-line arguments supported for use with this tool to the
432   * provided argument parser.  The tool may need to retain references to the
433   * arguments (and/or the argument parser, if trailing arguments are allowed)
434   * to it in order to obtain their values for use in later processing.
435   *
436   * @param  parser  The argument parser to which the arguments are to be added.
437   *
438   * @throws  ArgumentException  If a problem occurs while adding any of the
439   *                             tool-specific arguments to the provided
440   *                             argument parser.
441   */
442  @Override()
443  public void addToolArguments(@NotNull final ArgumentParser parser)
444         throws ArgumentException
445  {
446    toolArg = new StringArgument(null, "tool", true, 1,
447         "{searchrate|modrate|authrate|search-and-mod-rate}",
448         "The tool to invoke against the LDAP listener.  It may be one of " +
449              "searchrate, modrate, authrate, or search-and-mod-rate.  If " +
450              "this is not provided, then the searchrate tool will be invoked.",
451         StaticUtils.setOf(TOOL_NAME_SEARCHRATE,
452              TOOL_NAME_MODRATE,
453              TOOL_NAME_AUTHRATE,
454              TOOL_NAME_SEARCH_AND_MOD_RATE),
455         TOOL_NAME_SEARCHRATE);
456    toolArg.addLongIdentifier("toolName", true);
457    toolArg.addLongIdentifier("tool-name", true);
458    parser.addArgument(toolArg);
459
460
461    numThreadsArg = new IntegerArgument('t', "numThreads", true, 1, "{num}",
462         "The number of concurrent threads (each using its own connection) " +
463              "to use to process requests.  If this is not provided, then a " +
464              "single thread will be used.",
465         1, Integer.MAX_VALUE, 1);
466    numThreadsArg.addLongIdentifier("num-threads", true);
467    numThreadsArg.addLongIdentifier("threads", true);
468    parser.addArgument(numThreadsArg);
469
470
471    entriesPerSearchArg = new IntegerArgument(null, "entriesPerSearch", true,
472         1, "{num}",
473         "The number of entries to return in response to each search " +
474              "request.  If this is provided, the value must be between 0 " +
475              "and 100.  If it is not provided, then a single entry will be " +
476              "returned.",
477         0, 100, 1);
478    entriesPerSearchArg.addLongIdentifier("entries-per-search", true);
479    entriesPerSearchArg.addLongIdentifier("numEntries", true);
480    entriesPerSearchArg.addLongIdentifier("num-entries", true);
481    entriesPerSearchArg.addLongIdentifier("entries", true);
482    parser.addArgument(entriesPerSearchArg);
483
484
485    bindOnlyArg = new BooleanArgument(null, "bindOnly", 1,
486         "Indicates that the authrate tool should only issue bind requests.  " +
487              "If this is not provided, the authrate tool will perform both " +
488              "search and bind operations.  This argument will only be used " +
489              "in conjunction with the authrate tool.");
490    bindOnlyArg.addLongIdentifier("bind-only", true);
491    parser.addArgument(bindOnlyArg);
492
493
494    resultCodeArg = new IntegerArgument(null, "resultCode", true, 1,
495         "{intValue}",
496         "The integer value for the result code to return in response to " +
497              "each request.  If this is not provided, then a result code of " +
498              "0 (success) will be returned.",
499         0, Integer.MAX_VALUE, ResultCode.SUCCESS_INT_VALUE);
500    resultCodeArg.addLongIdentifier("result-code", true);
501    parser.addArgument(resultCodeArg);
502
503
504    diagnosticMessageArg = new StringArgument(null, "diagnosticMessage", false,
505         1, "{message}",
506         "The diagnostic message to return in response to each request.  If " +
507              "this is not provided, then no diagnostic message will be " +
508              "returned.");
509    diagnosticMessageArg.addLongIdentifier("diagnostic-message", true);
510    diagnosticMessageArg.addLongIdentifier("errorMessage", true);
511    diagnosticMessageArg.addLongIdentifier("error-message", true);
512    diagnosticMessageArg.addLongIdentifier("message", true);
513    parser.addArgument(diagnosticMessageArg);
514
515
516    useSSLArg = new BooleanArgument('Z', "useSSL", 1,
517         "Encrypt communication with SSL.  If this argument is not provided, " +
518              "then the communication will not be encrypted.");
519    useSSLArg.addLongIdentifier("use-ssl", true);
520    useSSLArg.addLongIdentifier("ssl", true);
521    useSSLArg.addLongIdentifier("useTLS", true);
522    useSSLArg.addLongIdentifier("use-tls", true);
523    useSSLArg.addLongIdentifier("tls", true);
524    parser.addArgument(useSSLArg);
525
526
527    numIntervalsArg = new IntegerArgument('I', "numIntervals", false, 1,
528         "{num}",
529         "The number of intervals to use when running the performance " +
530              "measurement tool.  If this argument is provided in " +
531              "conjunction with the --warmUpIntervals argument, then the " +
532              "warm-up intervals will not be included in this count, and the " +
533              "total number of intervals run will be the sum of the two " +
534              "values.  If this argument is not provided, then the tool will " +
535              "run until it is interrupted (e.g., by pressing Ctrl+C or by " +
536              "killing the underlying Java process).",
537         0, Integer.MAX_VALUE);
538    numIntervalsArg.addLongIdentifier("num-intervals", true);
539    numIntervalsArg.addLongIdentifier("intervals", true);
540    parser.addArgument(numIntervalsArg);
541
542
543    intervalDurationSecondsArg = new IntegerArgument('i',
544         "intervalDurationSeconds", true, 1, "{num}",
545         "The length of time in seconds to use for each tool interval (that " +
546              "is, the length of time between each line of output giving " +
547              "statistical information for operations processed in that " +
548              "interval).  If this is not provided, then a default interval " +
549              "duration of five seconds will be used.",
550         1, Integer.MAX_VALUE, 5);
551    intervalDurationSecondsArg.addLongIdentifier("interval-duration-seconds",
552         true);
553    intervalDurationSecondsArg.addLongIdentifier("intervalDuration", true);
554    intervalDurationSecondsArg.addLongIdentifier("interval-duration", true);
555    parser.addArgument(intervalDurationSecondsArg);
556
557
558    warmUpIntervalsArg = new IntegerArgument(null, "warmUpIntervals", true, 1,
559         "{num}",
560         "The number of intervals to run before starting to actually " +
561              "collect statistics to include in the final result.  This can " +
562              "give the JVM and JIT a chance to identify and optimize " +
563              "hotspots in the code for the best and most stable " +
564              "performance.  If this is not provided, then no warm-up " +
565              "intervals will be used and the tool will start collecting " +
566              "statistics right away.",
567         0, Integer.MAX_VALUE, 0);
568    warmUpIntervalsArg.addLongIdentifier("warm-up-intervals", true);
569    warmUpIntervalsArg.addLongIdentifier("warmup-intervals", true);
570    warmUpIntervalsArg.addLongIdentifier("warmUp", true);
571    warmUpIntervalsArg.addLongIdentifier("warm-up", true);
572    parser.addArgument(warmUpIntervalsArg);
573  }
574
575
576
577  /**
578   * Performs the core set of processing for this tool.
579   *
580   * @return  A result code that indicates whether the processing completed
581   *          successfully.
582   */
583  @Override()
584  @NotNull()
585  public ResultCode doToolProcessing()
586  {
587    // Create the socket factory to use for accepting connections.  If the
588    // --useSSL argument was provided, then create a temporary keystore and
589    // generate a certificate in it.
590    final ServerSocketFactory serverSocketFactory;
591    if (useSSLArg.isPresent())
592    {
593      try
594      {
595        final File keyStoreFile = File.createTempFile(
596             "test-ldap-sdk-performance-keystore-", ".jks");
597        keyStoreFile.deleteOnExit();
598        keyStoreFile.delete();
599
600        final ByteArrayOutputStream out = new ByteArrayOutputStream();
601        final ResultCode manageCertificatesResultCode =
602             ManageCertificates.main(null, out, out,
603                  "generate-self-signed-certificate",
604                  "--keystore", keyStoreFile.getAbsolutePath(),
605                  "--keystore-password", keyStoreFile.getAbsolutePath(),
606                  "--keystore-type", CryptoHelper.KEY_STORE_TYPE_JKS,
607                  "--alias", "server-cert",
608                  "--subject-dn", "CN=Test LDAP SDK Performance");
609        if (manageCertificatesResultCode != ResultCode.SUCCESS)
610        {
611          final String message = "ERROR:  Unable to use the " +
612               "manage-certificates tool to generate a self-signed server " +
613               "certificate to use for SSL communication.";
614          completionMessage.compareAndSet(null, message);
615          wrapErr(0, WRAP_COLUMN, message);
616          err();
617          wrapErr(0, WRAP_COLUMN, "The manage-certificates output was:");
618          err();
619          err(StaticUtils.toUTF8String(out.toByteArray()));
620          return manageCertificatesResultCode;
621        }
622
623        final SSLUtil sslUtil = new SSLUtil(
624             new KeyStoreKeyManager(keyStoreFile,
625                  keyStoreFile.getAbsolutePath().toCharArray(),
626                  CryptoHelper.KEY_STORE_TYPE_JKS, "server-cert"),
627             new TrustAllTrustManager());
628        serverSocketFactory = sslUtil.createSSLServerSocketFactory();
629      }
630      catch (final Exception e)
631      {
632        Debug.debugException(e);
633
634        final String message = "ERROR:  Unable to initialize support for SSL " +
635             "communication:  " + StaticUtils.getExceptionMessage(e);
636        completionMessage.compareAndSet(null, message);
637        wrapErr(0, WRAP_COLUMN, message);
638        return ResultCode.LOCAL_ERROR;
639      }
640    }
641    else
642    {
643      serverSocketFactory = ServerSocketFactory.getDefault();
644    }
645
646
647    // Create the search result entries to return in response to each search.
648    final int numEntries = entriesPerSearchArg.getValue();
649    final List<Entry> entries = new ArrayList<>(numEntries);
650    for (int i=1; i <= numEntries; i++)
651    {
652      entries.add(new Entry(
653           "uid=user." + i + ",ou=People,dc=example,dc=com",
654           new Attribute("objectClass", "top", "person", "organizationalPerson",
655                "inetOrgPerson"),
656           new Attribute("uid", "user." + i),
657           new Attribute("givenName", "User"),
658           new Attribute("sn", String.valueOf(i)),
659           new Attribute("cn", "User " + i),
660           new Attribute("mail", "user." + i + "@example.com"),
661           new Attribute("userPassword", "password")));
662    }
663
664
665    // Create a canned response request handler to use to return the responses.
666    final CannedResponseRequestHandler cannedResponseRequestHandler =
667         new CannedResponseRequestHandler(
668              ResultCode.valueOf(resultCodeArg.getValue()),
669              null, // Matched DN
670              diagnosticMessageArg.getValue(),
671              Collections.<String>emptyList(), // Referral URLs
672              entries,
673              Collections.<SearchResultReference>emptyList());
674
675
676    // Create the LDAP listener to handle the requests.
677    final LDAPListenerConfig listenerConfig =
678         new LDAPListenerConfig(0, cannedResponseRequestHandler);
679    listenerConfig.setServerSocketFactory(serverSocketFactory);
680
681    final LDAPListener ldapListener = new LDAPListener(listenerConfig);
682    try
683    {
684      ldapListener.startListening();
685    }
686    catch (final Exception e)
687    {
688      Debug.debugException(e);
689
690      final String message = "ERROR:  Unable to start listening for client " +
691           "connections:  " + StaticUtils.getExceptionMessage(e);
692      completionMessage.compareAndSet(null, message);
693      wrapErr(0, WRAP_COLUMN, message);
694      return ResultCode.LOCAL_ERROR;
695    }
696
697    try
698    {
699      final int listenPort = ldapListener.getListenPort();
700      final String toolName = StaticUtils.toLowerCase(toolArg.getValue());
701      switch (toolName)
702      {
703        case TOOL_NAME_SEARCHRATE:
704          return invokeSearchRate(listenPort);
705        case TOOL_NAME_MODRATE:
706          return invokeModRate(listenPort);
707        case TOOL_NAME_AUTHRATE:
708          return invokeAuthRate(listenPort);
709        case TOOL_NAME_SEARCH_AND_MOD_RATE:
710          return invokeSearchAndModRate(listenPort);
711        default:
712          // This should never happen.
713          final String message = "ERROR:  Unrecognized tool name:  " + toolName;
714          completionMessage.compareAndSet(null, message);
715          wrapErr(0, WRAP_COLUMN, message);
716          return ResultCode.PARAM_ERROR;
717      }
718    }
719    finally
720    {
721      ldapListener.shutDown(true);
722    }
723  }
724
725
726
727  /**
728   * Invokes the {@link SearchRate} tool with an appropriate set of arguments.
729   *
730   * @param  listenPort  The port on which the LDAP listener is listening.
731   *
732   * @return  The result code obtained from the {@code SearchRate} tool.
733   */
734  @NotNull()
735  private ResultCode invokeSearchRate(final int listenPort)
736  {
737    final List<String> searchRateArgs = new ArrayList<>();
738
739    searchRateArgs.add("--hostname");
740    searchRateArgs.add("localhost");
741
742    searchRateArgs.add("--port");
743    searchRateArgs.add(String.valueOf(listenPort));
744
745    if (useSSLArg.isPresent())
746    {
747      searchRateArgs.add("--useSSL");
748      searchRateArgs.add("--trustAll");
749    }
750
751    searchRateArgs.add("--baseDN");
752    searchRateArgs.add("dc=example,dc=com");
753
754    searchRateArgs.add("--scope");
755    searchRateArgs.add("sub");
756
757    searchRateArgs.add("--filter");
758    searchRateArgs.add("(objectClass=*)");
759
760    searchRateArgs.add("--numThreads");
761    searchRateArgs.add(String.valueOf(numThreadsArg.getValue()));
762
763    if (numIntervalsArg.isPresent())
764    {
765      searchRateArgs.add("--numIntervals");
766      searchRateArgs.add(String.valueOf(numIntervalsArg.getValue()));
767    }
768
769    if (intervalDurationSecondsArg.isPresent())
770    {
771      searchRateArgs.add("--intervalDuration");
772      searchRateArgs.add(String.valueOf(
773           intervalDurationSecondsArg.getValue()));
774    }
775
776    if (warmUpIntervalsArg.isPresent())
777    {
778      searchRateArgs.add("--warmUpIntervals");
779      searchRateArgs.add(String.valueOf(warmUpIntervalsArg.getValue()));
780    }
781
782    final String[] searchRateArgsArray =
783         searchRateArgs.toArray(StaticUtils.NO_STRINGS);
784
785    final SearchRate searchRate = new SearchRate(getOut(), getErr());
786
787    final ResultCode searchRateResultCode =
788         searchRate.runTool(searchRateArgsArray);
789    if (searchRateResultCode == ResultCode.SUCCESS)
790    {
791      final String message = "The searchrate tool completed successfully.";
792      completionMessage.compareAndSet(null, message);
793      wrapOut(0, WRAP_COLUMN, message);
794    }
795    else
796    {
797      final String message =
798           "ERROR:  The searchrate tool exited with error result code " +
799                searchRateResultCode + '.';
800      completionMessage.compareAndSet(null, message);
801      wrapErr(0, WRAP_COLUMN, message);
802    }
803
804    return searchRateResultCode;
805  }
806
807
808
809  /**
810   * Invokes the {@link ModRate} tool with an appropriate set of arguments.
811   *
812   * @param  listenPort  The port on which the LDAP listener is listening.
813   *
814   * @return  The result code obtained from the {@code ModRate} tool.
815   */
816  @NotNull()
817  private ResultCode invokeModRate(final int listenPort)
818  {
819    final List<String> modRateArgs = new ArrayList<>();
820
821    modRateArgs.add("--hostname");
822    modRateArgs.add("localhost");
823
824    modRateArgs.add("--port");
825    modRateArgs.add(String.valueOf(listenPort));
826
827    if (useSSLArg.isPresent())
828    {
829      modRateArgs.add("--useSSL");
830      modRateArgs.add("--trustAll");
831    }
832
833    modRateArgs.add("--entryDN");
834    modRateArgs.add("dc=example,dc=com");
835
836    modRateArgs.add("--attribute");
837    modRateArgs.add("description");
838
839    modRateArgs.add("--valuePattern");
840    modRateArgs.add("value");
841
842    modRateArgs.add("--numThreads");
843    modRateArgs.add(String.valueOf(numThreadsArg.getValue()));
844
845    if (numIntervalsArg.isPresent())
846    {
847      modRateArgs.add("--numIntervals");
848      modRateArgs.add(String.valueOf(numIntervalsArg.getValue()));
849    }
850
851    if (intervalDurationSecondsArg.isPresent())
852    {
853      modRateArgs.add("--intervalDuration");
854      modRateArgs.add(String.valueOf(
855           intervalDurationSecondsArg.getValue()));
856    }
857
858    if (warmUpIntervalsArg.isPresent())
859    {
860      modRateArgs.add("--warmUpIntervals");
861      modRateArgs.add(String.valueOf(warmUpIntervalsArg.getValue()));
862    }
863
864    final String[] modRateArgsArray =
865         modRateArgs.toArray(StaticUtils.NO_STRINGS);
866
867    final ModRate modRate = new ModRate(getOut(), getErr());
868
869    final ResultCode modRateResultCode =
870         modRate.runTool(modRateArgsArray);
871    if (modRateResultCode == ResultCode.SUCCESS)
872    {
873      final String message = "The modrate tool completed successfully.";
874      completionMessage.compareAndSet(null, message);
875      wrapOut(0, WRAP_COLUMN, message);
876    }
877    else
878    {
879      final String message =
880           "ERROR:  The modrate tool exited with error result code " +
881                modRateResultCode + '.';
882      completionMessage.compareAndSet(null, message);
883      wrapErr(0, WRAP_COLUMN, message);
884    }
885
886    return modRateResultCode;
887  }
888
889
890
891  /**
892   * Invokes the {@link AuthRate} tool with an appropriate set of arguments.
893   *
894   * @param  listenPort  The port on which the LDAP listener is listening.
895   *
896   * @return  The result code obtained from the {@code AuthRate} tool.
897   */
898  @NotNull()
899  private ResultCode invokeAuthRate(final int listenPort)
900  {
901    final List<String> authRateArgs = new ArrayList<>();
902
903    authRateArgs.add("--hostname");
904    authRateArgs.add("localhost");
905
906    authRateArgs.add("--port");
907    authRateArgs.add(String.valueOf(listenPort));
908
909    if (useSSLArg.isPresent())
910    {
911      authRateArgs.add("--useSSL");
912      authRateArgs.add("--trustAll");
913    }
914
915    if (bindOnlyArg.isPresent())
916    {
917      authRateArgs.add("--bindOnly");
918
919      authRateArgs.add("--baseDN");
920      authRateArgs.add("uid=user.1,ou=People,dc=example,dc=com");
921    }
922    else
923    {
924      authRateArgs.add("--baseDN");
925      authRateArgs.add("dc=example,dc=com");
926
927      authRateArgs.add("--scope");
928      authRateArgs.add("sub");
929
930      authRateArgs.add("--filter");
931      authRateArgs.add("(uid=user.1)");
932    }
933
934    authRateArgs.add("--credentials");
935    authRateArgs.add("password");
936
937    authRateArgs.add("--numThreads");
938    authRateArgs.add(String.valueOf(numThreadsArg.getValue()));
939
940    if (numIntervalsArg.isPresent())
941    {
942      authRateArgs.add("--numIntervals");
943      authRateArgs.add(String.valueOf(numIntervalsArg.getValue()));
944    }
945
946    if (intervalDurationSecondsArg.isPresent())
947    {
948      authRateArgs.add("--intervalDuration");
949      authRateArgs.add(String.valueOf(
950           intervalDurationSecondsArg.getValue()));
951    }
952
953    if (warmUpIntervalsArg.isPresent())
954    {
955      authRateArgs.add("--warmUpIntervals");
956      authRateArgs.add(String.valueOf(warmUpIntervalsArg.getValue()));
957    }
958
959    final String[] authRateArgsArray =
960         authRateArgs.toArray(StaticUtils.NO_STRINGS);
961
962    final AuthRate authRate = new AuthRate(getOut(), getErr());
963
964    final ResultCode authRateResultCode =
965         authRate.runTool(authRateArgsArray);
966    if (authRateResultCode == ResultCode.SUCCESS)
967    {
968      final String message = "The authrate tool completed successfully.";
969      completionMessage.compareAndSet(null, message);
970      wrapOut(0, WRAP_COLUMN, message);
971    }
972    else
973    {
974      final String message =
975           "ERROR:  The authrate tool exited with error result code " +
976                authRateResultCode + '.';
977      completionMessage.compareAndSet(null, message);
978      wrapErr(0, WRAP_COLUMN, message);
979    }
980
981    return authRateResultCode;
982  }
983
984
985
986  /**
987   * Invokes the {@link SearchAndModRate} tool with an appropriate set of
988   * arguments.
989   *
990   * @param  listenPort  The port on which the LDAP listener is listening.
991   *
992   * @return  The result code obtained from the {@code SearchAndModRate} tool.
993   */
994  @NotNull()
995  private ResultCode invokeSearchAndModRate(final int listenPort)
996  {
997    final List<String> searchAndModRateArgs = new ArrayList<>();
998
999    searchAndModRateArgs.add("--hostname");
1000    searchAndModRateArgs.add("localhost");
1001
1002    searchAndModRateArgs.add("--port");
1003    searchAndModRateArgs.add(String.valueOf(listenPort));
1004
1005    if (useSSLArg.isPresent())
1006    {
1007      searchAndModRateArgs.add("--useSSL");
1008      searchAndModRateArgs.add("--trustAll");
1009    }
1010
1011    searchAndModRateArgs.add("--baseDN");
1012    searchAndModRateArgs.add("dc=example,dc=com");
1013
1014    searchAndModRateArgs.add("--scope");
1015    searchAndModRateArgs.add("sub");
1016
1017    searchAndModRateArgs.add("--filter");
1018    searchAndModRateArgs.add("(objectClass=*)");
1019
1020    searchAndModRateArgs.add("--modifyAttribute");
1021    searchAndModRateArgs.add("description");
1022
1023    searchAndModRateArgs.add("--valueLength");
1024    searchAndModRateArgs.add("10");
1025
1026    searchAndModRateArgs.add("--numThreads");
1027    searchAndModRateArgs.add(String.valueOf(numThreadsArg.getValue()));
1028
1029    if (numIntervalsArg.isPresent())
1030    {
1031      searchAndModRateArgs.add("--numIntervals");
1032      searchAndModRateArgs.add(String.valueOf(numIntervalsArg.getValue()));
1033    }
1034
1035    if (intervalDurationSecondsArg.isPresent())
1036    {
1037      searchAndModRateArgs.add("--intervalDuration");
1038      searchAndModRateArgs.add(String.valueOf(
1039           intervalDurationSecondsArg.getValue()));
1040    }
1041
1042    if (warmUpIntervalsArg.isPresent())
1043    {
1044      searchAndModRateArgs.add("--warmUpIntervals");
1045      searchAndModRateArgs.add(String.valueOf(warmUpIntervalsArg.getValue()));
1046    }
1047
1048    final String[] searchAndModRateArgsArray =
1049         searchAndModRateArgs.toArray(StaticUtils.NO_STRINGS);
1050
1051    final SearchAndModRate searchAndModRate =
1052         new SearchAndModRate(getOut(), getErr());
1053
1054    final ResultCode searchAndModRateResultCode =
1055         searchAndModRate.runTool(searchAndModRateArgsArray);
1056    if (searchAndModRateResultCode == ResultCode.SUCCESS)
1057    {
1058      final String message =
1059           "The search-and-mod-rate tool completed successfully.";
1060      completionMessage.compareAndSet(null, message);
1061      wrapOut(0, WRAP_COLUMN, message);
1062    }
1063    else
1064    {
1065      final String message =
1066           "ERROR:  The search-and-mod-rate tool exited with error result " +
1067                "code " + searchAndModRateResultCode + '.';
1068      completionMessage.compareAndSet(null, message);
1069      wrapErr(0, WRAP_COLUMN, message);
1070    }
1071
1072    return searchAndModRateResultCode;
1073  }
1074
1075
1076
1077  /**
1078   * Retrieves a set of information that may be used to generate example usage
1079   * information.  Each element in the returned map should consist of a map
1080   * between an example set of arguments and a string that describes the
1081   * behavior of the tool when invoked with that set of arguments.
1082   *
1083   * @return  A set of information that may be used to generate example usage
1084   *          information.  It may be {@code null} or empty if no example usage
1085   *          information is available.
1086   */
1087  @Override()
1088  @NotNull()
1089  public LinkedHashMap<String[],String> getExampleUsages()
1090  {
1091    final LinkedHashMap<String[],String> examples = new LinkedHashMap<>();
1092
1093    examples.put(
1094         new String[]
1095         {
1096           "--numThreads", "10"
1097         },
1098         "Test LDAP SDK performance with the searchrate tool using ten " +
1099              "concurrent threads.  Communication will use an insecure " +
1100              "connection, and each search will return a success result with " +
1101              "a single matching entry.  The tool will continue to run until " +
1102              "it is interrupted.");
1103
1104    examples.put(
1105         new String[]
1106         {
1107           "--tool", "modrate",
1108           "--numThreads", "10",
1109           "--useSSL",
1110           "--resultCode", "32",
1111           "--diagnosticMessage", "The base entry does not exist",
1112           "--warmUpIntervals", "5",
1113           "--numIntervals", "10",
1114           "--intervalDurationSeconds", "5"
1115         },
1116         "Test LDAP SDK performance with the modrate tool using ten " +
1117              "concurrent threads over SSL-encrypted connections.  Each " +
1118              "modify will return an error result with a result code of 32 " +
1119              "(noSuchObject) and a diagnostic message of 'The target entry " +
1120              "does not exist'.  The tool will run five warm-up intervals " +
1121              "of five seconds each, and then ten 5-second intervals in " +
1122              "which it will capture statistics.  The tool will exit after " +
1123              "those last ten intervals have completed.");
1124
1125    return examples;
1126  }
1127}