001/*
002 * Copyright 2011-2016 UnboundID Corp.
003 *
004 * This program is free software; you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License (GPLv2 only)
006 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
007 * as published by the Free Software Foundation.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program; if not, see <http://www.gnu.org/licenses>.
016 */
017
018package com.unboundid.scim.tools;
019
020import com.unboundid.ldap.sdk.LDAPException;
021import com.unboundid.ldap.sdk.ResultCode;
022import com.unboundid.scim.data.BaseResource;
023import com.unboundid.scim.schema.ResourceDescriptor;
024import com.unboundid.scim.sdk.Debug;
025import com.unboundid.scim.sdk.ResourceNotFoundException;
026import com.unboundid.scim.sdk.SCIMEndpoint;
027import com.unboundid.scim.sdk.SCIMException;
028import com.unboundid.scim.sdk.SCIMService;
029import com.unboundid.util.ColumnFormatter;
030import com.unboundid.util.CommandLineTool;
031import com.unboundid.util.FixedRateBarrier;
032import com.unboundid.util.FormattableColumn;
033import com.unboundid.util.HorizontalAlignment;
034import com.unboundid.util.OutputFormat;
035import com.unboundid.util.ValuePattern;
036import com.unboundid.util.WakeableSleeper;
037import com.unboundid.util.args.ArgumentException;
038import com.unboundid.util.args.ArgumentParser;
039import com.unboundid.util.args.BooleanArgument;
040import com.unboundid.util.args.FileArgument;
041import com.unboundid.util.args.IntegerArgument;
042import com.unboundid.util.args.StringArgument;
043import com.unboundid.util.ssl.KeyStoreKeyManager;
044import com.unboundid.util.ssl.PromptTrustManager;
045import com.unboundid.util.ssl.SSLUtil;
046import com.unboundid.util.ssl.TrustAllTrustManager;
047import com.unboundid.util.ssl.TrustStoreTrustManager;
048
049import org.apache.http.auth.AuthScope;
050import org.apache.http.auth.UsernamePasswordCredentials;
051import org.apache.http.client.config.RequestConfig;
052import org.apache.http.config.Registry;
053import org.apache.http.config.RegistryBuilder;
054import org.apache.http.config.SocketConfig;
055import org.apache.http.conn.socket.ConnectionSocketFactory;
056import org.apache.http.conn.socket.PlainConnectionSocketFactory;
057import org.apache.http.conn.ssl.NoopHostnameVerifier;
058import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
059import org.apache.http.impl.client.BasicCredentialsProvider;
060import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
061import org.glassfish.jersey.apache.connector.ApacheClientProperties;
062import org.glassfish.jersey.apache.connector.ApacheConnectorProvider;
063import org.glassfish.jersey.client.ClientConfig;
064
065import javax.net.ssl.KeyManager;
066import javax.net.ssl.TrustManager;
067import javax.ws.rs.client.ClientRequestContext;
068import javax.ws.rs.client.ClientRequestFilter;
069import javax.ws.rs.core.MediaType;
070import java.io.IOException;
071import java.io.OutputStream;
072import java.net.URI;
073import java.net.URISyntaxException;
074import java.security.GeneralSecurityException;
075import java.text.ParseException;
076import java.util.Arrays;
077import java.util.LinkedHashMap;
078import java.util.LinkedHashSet;
079import java.util.List;
080import java.util.concurrent.CyclicBarrier;
081import java.util.concurrent.atomic.AtomicLong;
082import java.util.concurrent.atomic.AtomicReference;
083import java.util.logging.ConsoleHandler;
084
085import static com.unboundid.scim.sdk.Debug.debugException;
086import static com.unboundid.util.StaticUtils.NO_STRINGS;
087import static com.unboundid.scim.tools.ToolMessages.*;
088import static com.unboundid.util.StaticUtils.getExceptionMessage;
089
090/**
091 * This class provides a tool that can be used to query a SCIM server repeatedly
092 * using multiple threads.  It can help provide an estimate of the query
093 * performance that a SCIM server is able to achieve.  The query filter may be
094 * a value pattern as described in the {@link com.unboundid.util.ValuePattern}
095 * class.  This makes it possible to query over a range of resources rather
096 * than repeatedly performing queries with the same filter.
097 * <BR><BR>
098 * All of the necessary information is provided using command line arguments.
099 * Supported arguments are as follows:
100 * <UL>
101 *   <LI>"-h {address}" or "--hostname {address}" -- Specifies the address of
102 *       the SCIM server.  If this isn't specified, then a default of
103 *       "localhost" will be used.</LI>
104 *   <LI>"-p {port}" or "--port {port}" -- Specifies the port number of the
105 *       SCIM server.  If this isn't specified, then a default port of 80
106 *       will be used.</LI>
107 *   <LI>"--contextPath {path}" -- specifies the context path of
108 *       the SCIM server.  If no context path is specified, then the default
109 *       value '/' is used.</LI>
110 *   <LI>"--authID {userName}" -- Specifies the authentication ID to use when
111 *       authenticating using basic auth.</LI>
112 *   <LI>"-w {password}" or "--authPassword {password}" -- Specifies the
113 *       password to use when authenticating using basic auth or a
114 *       password-based SASL mechanism.</LI>
115 *   <LI>"--bearerToken {b64token}" -- Specifies the OAuth2 bearer
116 *       token to use when authenticating using OAuth</LI>
117 *   <LI>"--resourceName {resource-name}" -- specifies the name of resources to
118 *       be queried.  If this isn't specified, then a default of "User" will
119 *       be used.</LI>
120 *   <LI>"-x" or "--xml" -- Specifies XML format in requests rather than
121 *       JSON format.</LI>
122 *   <LI>"-f {filter}" or "--filter {filter}" -- specifies the filter to use for
123 *       the queries.  It may be a simple filter, or it may be a value pattern
124 *       to express a range of filters. If this isn't specified, then no
125 *       filtering is requested.</LI>
126 *   <LI>"-A {name}" or "--attribute {name}" -- specifies the name of an
127 *       attribute that should be included in resources returned from the
128 *       server. If this isn't specified, then all resource attributes will be
129 *       requested. Multiple attributes may be requested with multiple instances
130 *       of this argument.</LI>
131 *   <LI>"-t {num}" or "--numThreads {num}" -- specifies the number of
132 *       concurrent threads to use when performing the queries.  If this is not
133 *       provided, then a default of one thread will be used.</LI>
134 *   <LI>"-i {sec}" or "--intervalDuration {sec}" -- specifies the length of
135 *       time in seconds between lines out output.  If this is not provided,
136 *       then a default interval duration of five seconds will be used.</LI>
137 *   <LI>"-I {num}" or "--numIntervals {num}" -- specifies the maximum number of
138 *       intervals for which to run.  If this is not provided, then it will
139 *       run forever.</LI>
140 *   <LI>"-r {queries-per-second}" or "--ratePerSecond {queries-per-second}"
141 *       -- specifies the target number of queries to perform per second.  It
142 *       is still necessary to specify a sufficient number of threads for
143 *       achieving this rate.  If this option is not provided, then the tool
144 *       will run at the maximum rate for the specified number of threads.</LI>
145 *   <LI>"--warmUpIntervals {num}" -- specifies the number of intervals to
146 *       complete before beginning overall statistics collection.</LI>
147 *   <LI>"--timestampFormat {format}" -- specifies the format to use for
148 *       timestamps included before each output line.  The format may be one of
149 *       "none" (for no timestamps), "with-date" (to include both the date and
150 *       the time), or "without-date" (to include only time time).</LI>
151 *   <LI>"-c" or "--csv" -- Generate output in CSV format rather than a
152 *       display-friendly format.</LI>
153 * </UL>
154 */
155public class SCIMQueryRate
156    extends CommandLineTool
157{
158  // Arguments used to communicate with a SCIM server.
159  private FileArgument    authPasswordFile;
160  private IntegerArgument port;
161  private StringArgument  authID;
162  private StringArgument  authPassword;
163  private StringArgument  bearerToken;
164  private StringArgument  contextPath;
165  private StringArgument  host;
166  private BooleanArgument trustAll;
167  private BooleanArgument useSSL;
168  private FileArgument    keyStorePasswordFile;
169  private FileArgument    trustStorePasswordFile;
170  private StringArgument  certificateNickname;
171  private StringArgument  keyStoreFormat;
172  private StringArgument  keyStorePath;
173  private StringArgument  keyStorePassword;
174  private StringArgument  trustStoreFormat;
175  private StringArgument  trustStorePath;
176  private StringArgument  trustStorePassword;
177
178  // The argument used to indicate whether to generate output in CSV format.
179  private BooleanArgument csvFormat;
180
181  // The argument used to indicate whether to use XML format in requests rather
182  // than JSON format.
183  private BooleanArgument xmlFormat;
184
185  // The argument used to specify the collection interval.
186  private IntegerArgument collectionInterval;
187
188  // The argument used to specify the number of intervals.
189  private IntegerArgument numIntervals;
190
191  // The argument used to specify the number of threads.
192  private IntegerArgument numThreads;
193
194  // The argument used to specify the seed to use for the random number
195  // generator.
196  private IntegerArgument randomSeed;
197
198  // The target rate of searches per second.
199  private IntegerArgument ratePerSecond;
200
201  // The number of warm-up intervals to perform.
202  private IntegerArgument warmUpIntervals;
203
204  // The argument used to specify the attributes to return.
205  private StringArgument attributes;
206
207  // The argument used to specify the filters for the queries.
208  private StringArgument filter;
209
210  // The argument used to specify a resource ID (or pattern).
211  private StringArgument resourceId;
212
213  // The argument used to specify the name of resources to be queried.
214  private StringArgument resourceName;
215
216  // The argument used to specify the timestamp format.
217  private StringArgument timestampFormat;
218
219  // The prompt trust manager that will be shared by all connections created
220  // for which it is appropriate.  This will allow them to benefit from the
221  // common cache.
222  private final AtomicReference<PromptTrustManager> promptTrustManager =
223      new AtomicReference<PromptTrustManager>();
224
225
226  /**
227   * Parse the provided command line arguments and make the appropriate set of
228   * changes.
229   *
230   * @param  args  The command line arguments provided to this program.
231   */
232  public static void main(final String[] args)
233  {
234    final ResultCode resultCode = main(args, System.out, System.err);
235    if (resultCode != ResultCode.SUCCESS)
236    {
237      System.exit(resultCode.intValue());
238    }
239  }
240
241
242
243  /**
244   * Parse the provided command line arguments and make the appropriate set of
245   * changes.
246   *
247   * @param  args       The command line arguments provided to this program.
248   * @param  outStream  The output stream to which standard out should be
249   *                    written.  It may be {@code null} if output should be
250   *                    suppressed.
251   * @param  errStream  The output stream to which standard error should be
252   *                    written.  It may be {@code null} if error messages
253   *                    should be suppressed.
254   *
255   * @return  A result code indicating whether the processing was successful.
256   */
257  public static ResultCode main(final String[] args,
258                                final OutputStream outStream,
259                                final OutputStream errStream)
260  {
261    final SCIMQueryRate queryRate = new SCIMQueryRate(outStream, errStream);
262    return queryRate.runTool(args);
263  }
264
265
266
267  /**
268   * Creates a new instance of this tool.
269   *
270   * @param  outStream  The output stream to which standard out should be
271   *                    written.  It may be {@code null} if output should be
272   *                    suppressed.
273   * @param  errStream  The output stream to which standard error should be
274   *                    written.  It may be {@code null} if error messages
275   *                    should be suppressed.
276   */
277  public SCIMQueryRate(final OutputStream outStream,
278                       final OutputStream errStream)
279  {
280    super(outStream, errStream);
281  }
282
283
284
285  /**
286   * Retrieves the name for this tool.
287   *
288   * @return  The name for this tool.
289   */
290  @Override()
291  public String getToolName()
292  {
293    return "scim-query-rate";
294  }
295
296
297
298  /**
299   * Retrieves the description for this tool.
300   *
301   * @return  The description for this tool.
302   */
303  @Override()
304  public String getToolDescription()
305  {
306    return INFO_QUERY_TOOL_DESC.get();
307  }
308
309
310
311  /**
312   * {@inheritDoc}
313   */
314  @Override()
315  public void addToolArguments(final ArgumentParser parser)
316         throws ArgumentException
317  {
318    host = new StringArgument(
319        'h', "hostname", true, 1,
320        INFO_QUERY_TOOL_ARG_PLACEHOLDER_HOSTNAME.get(),
321        INFO_QUERY_TOOL_ARG_DESC_HOSTNAME.get(),
322        "localhost");
323    parser.addArgument(host);
324
325
326    port = new IntegerArgument(
327        'p', "port", true, 1,
328        INFO_QUERY_TOOL_ARG_PLACEHOLDER_PORT.get(),
329        INFO_QUERY_TOOL_ARG_DESC_PORT.get(),
330        1, 65535, 80);
331    parser.addArgument(port);
332
333
334    contextPath = new StringArgument(null, "contextPath", false, 1,
335         INFO_QUERY_TOOL_ARG_PLACEHOLDER_CONTEXT_PATH.get(),
336         INFO_QUERY_TOOL_ARG_DESC_CONTEXT_PATH.get(),
337         Arrays.asList("/"));
338    parser.addArgument(contextPath);
339
340
341    authID = new StringArgument(
342        null, "authID", false, 1,
343        INFO_QUERY_TOOL_ARG_PLACEHOLDER_AUTHID.get(),
344        INFO_QUERY_TOOL_ARG_DESC_AUTHID.get());
345    parser.addArgument(authID);
346
347
348    authPassword = new StringArgument(
349        'w', "authPassword", false, 1,
350        INFO_QUERY_TOOL_ARG_PLACEHOLDER_AUTH_PASSWORD.get(),
351        INFO_QUERY_TOOL_ARG_DESC_AUTH_PASSWORD.get());
352    parser.addArgument(authPassword);
353
354
355    bearerToken = new StringArgument(
356            null, "bearerToken", false, 1,
357            INFO_QUERY_TOOL_ARG_PLACEHOLDER_BEARER_TOKEN.get(),
358            INFO_QUERY_TOOL_ARG_DESC_BEARER_TOKEN.get());
359    parser.addArgument(bearerToken);
360
361
362    authPasswordFile = new FileArgument(
363        'j', "authPasswordFile", false, 1,
364        INFO_QUERY_TOOL_ARG_PLACEHOLDER_AUTH_PASSWORD_FILE.get(),
365        INFO_QUERY_TOOL_ARG_DESC_AUTH_PASSWORD_FILE.get(),
366        true, true, true, false);
367    parser.addArgument(authPasswordFile);
368
369
370    resourceName = new StringArgument(
371        null, "resourceName", false, 1,
372        INFO_QUERY_TOOL_ARG_PLACEHOLDER_RESOURCE_NAME.get(),
373        INFO_QUERY_TOOL_ARG_DESC_RESOURCE_NAME.get(),
374        null, Arrays.asList("User"));
375    parser.addArgument(resourceName);
376
377
378    xmlFormat = new BooleanArgument(
379        'x', "xml", 1,
380        INFO_QUERY_TOOL_ARG_DESC_XML_FORMAT.get());
381    parser.addArgument(xmlFormat);
382
383    filter = new StringArgument(
384        'f', "filter", false, 1,
385        INFO_QUERY_TOOL_ARG_PLACEHOLDER_FILTER.get(),
386        INFO_QUERY_TOOL_ARG_DESC_FILTER.get());
387    parser.addArgument(filter);
388
389
390    resourceId = new StringArgument(
391        'd', "resourceID", false, 1,
392        INFO_QUERY_TOOL_ARG_PLACEHOLDER_RESOURCE_ID.get(),
393        INFO_QUERY_TOOL_ARG_DESC_RESOURCE_ID.get());
394    parser.addArgument(resourceId);
395
396
397    attributes = new StringArgument(
398        'A', "attribute", false, 0,
399        INFO_QUERY_TOOL_ARG_PLACEHOLDER_ATTRIBUTE.get(),
400        INFO_QUERY_TOOL_ARG_DESC_ATTRIBUTE.get());
401    parser.addArgument(attributes);
402
403
404    numThreads = new IntegerArgument(
405        't', "numThreads", true, 1,
406        INFO_QUERY_TOOL_ARG_PLACEHOLDER_NUM_THREADS.get(),
407        INFO_QUERY_TOOL_ARG_DESC_NUM_THREADS.get(),
408        1, Integer.MAX_VALUE, 1);
409    parser.addArgument(numThreads);
410
411
412    collectionInterval = new IntegerArgument(
413        'i', "intervalDuration", true, 1,
414        INFO_QUERY_TOOL_ARG_PLACEHOLDER_INTERVAL_DURATION.get(),
415        INFO_QUERY_TOOL_ARG_DESC_INTERVAL_DURATION.get(), 1,
416        Integer.MAX_VALUE, 5);
417    parser.addArgument(collectionInterval);
418
419
420    numIntervals = new IntegerArgument(
421        'I', "numIntervals", true, 1,
422        INFO_QUERY_TOOL_ARG_PLACEHOLDER_NUM_INTERVALS.get(),
423        INFO_QUERY_TOOL_ARG_DESC_NUM_INTERVALS.get(),
424        1, Integer.MAX_VALUE,
425        Integer.MAX_VALUE);
426    parser.addArgument(numIntervals);
427
428    ratePerSecond = new IntegerArgument(
429        'r', "ratePerSecond", false, 1,
430        INFO_QUERY_TOOL_ARG_PLACEHOLDER_RATE_PER_SECOND.get(),
431        INFO_QUERY_TOOL_ARG_DESC_RATE_PER_SECOND.get(),
432        1, Integer.MAX_VALUE);
433    parser.addArgument(ratePerSecond);
434
435    warmUpIntervals = new IntegerArgument(
436        null, "warmUpIntervals", true, 1,
437        INFO_QUERY_TOOL_ARG_PLACEHOLDER_WARM_UP_INTERVALS.get(),
438        INFO_QUERY_TOOL_ARG_DESC_WARM_UP_INTERVALS.get(),
439        0, Integer.MAX_VALUE, 0);
440    parser.addArgument(warmUpIntervals);
441
442    final LinkedHashSet<String> allowedFormats = new LinkedHashSet<String>(3);
443    allowedFormats.add("none");
444    allowedFormats.add("with-date");
445    allowedFormats.add("without-date");
446    timestampFormat = new StringArgument(
447        null, "timestampFormat", true, 1,
448        INFO_QUERY_TOOL_ARG_PLACEHOLDER_TIMESTAMP_FORMAT.get(),
449        INFO_QUERY_TOOL_ARG_DESC_TIMESTAMP_FORMAT.get(),
450        allowedFormats, "none");
451    parser.addArgument(timestampFormat);
452
453    csvFormat = new BooleanArgument(
454        'c', "csv", 1,
455        INFO_QUERY_TOOL_ARG_DESC_CSV_FORMAT.get());
456    parser.addArgument(csvFormat);
457
458    randomSeed = new IntegerArgument(
459        'R', "randomSeed", false, 1,
460        INFO_QUERY_TOOL_ARG_PLACEHOLDER_RANDOM_SEED.get(),
461        INFO_QUERY_TOOL_ARG_DESC_RANDOM_SEED.get());
462    parser.addArgument(randomSeed);
463
464    useSSL = new BooleanArgument('Z', "useSSL", 1,
465         INFO_SCIM_TOOL_DESCRIPTION_USE_SSL.get());
466    parser.addArgument(useSSL);
467
468    trustAll = new BooleanArgument('X', "trustAll", 1,
469         INFO_SCIM_TOOL_DESCRIPTION_TRUST_ALL.get());
470    parser.addArgument(trustAll);
471
472    keyStorePath = new StringArgument('K', "keyStorePath", false, 1,
473         INFO_SCIM_TOOL_PLACEHOLDER_PATH.get(),
474         INFO_SCIM_TOOL_DESCRIPTION_KEY_STORE_PATH.get());
475    parser.addArgument(keyStorePath);
476
477    keyStorePassword = new StringArgument('W', "keyStorePassword", false, 1,
478         INFO_SCIM_TOOL_PLACEHOLDER_PASSWORD.get(),
479         INFO_SCIM_TOOL_DESCRIPTION_KEY_STORE_PASSWORD.get());
480    parser.addArgument(keyStorePassword);
481
482    keyStorePasswordFile = new FileArgument('u', "keyStorePasswordFile", false,
483         1, INFO_SCIM_TOOL_PLACEHOLDER_PATH.get(),
484         INFO_SCIM_TOOL_DESCRIPTION_KEY_STORE_PASSWORD_FILE.get());
485    parser.addArgument(keyStorePasswordFile);
486
487    keyStoreFormat = new StringArgument(null, "keyStoreFormat", false, 1,
488         INFO_SCIM_TOOL_PLACEHOLDER_FORMAT.get(),
489         INFO_SCIM_TOOL_DESCRIPTION_KEY_STORE_FORMAT.get());
490    parser.addArgument(keyStoreFormat);
491
492    trustStorePath = new StringArgument('P', "trustStorePath", false, 1,
493         INFO_SCIM_TOOL_PLACEHOLDER_PATH.get(),
494         INFO_SCIM_TOOL_DESCRIPTION_TRUST_STORE_PATH.get());
495    parser.addArgument(trustStorePath);
496
497    trustStorePassword = new StringArgument('T', "trustStorePassword", false, 1,
498         INFO_SCIM_TOOL_PLACEHOLDER_PASSWORD.get(),
499         INFO_SCIM_TOOL_DESCRIPTION_TRUST_STORE_PASSWORD.get());
500    parser.addArgument(trustStorePassword);
501
502    trustStorePasswordFile = new FileArgument('U', "trustStorePasswordFile",
503         false, 1, INFO_SCIM_TOOL_PLACEHOLDER_PATH.get(),
504         INFO_SCIM_TOOL_DESCRIPTION_TRUST_STORE_PASSWORD_FILE.get());
505    parser.addArgument(trustStorePasswordFile);
506
507    trustStoreFormat = new StringArgument(null, "trustStoreFormat", false, 1,
508         INFO_SCIM_TOOL_PLACEHOLDER_FORMAT.get(),
509         INFO_SCIM_TOOL_DESCRIPTION_TRUST_STORE_FORMAT.get());
510    parser.addArgument(trustStoreFormat);
511
512    certificateNickname = new StringArgument('N', "certNickname", false, 1,
513         INFO_SCIM_TOOL_PLACEHOLDER_CERT_NICKNAME.get(),
514         INFO_SCIM_TOOL_DESCRIPTION_CERT_NICKNAME.get());
515    parser.addArgument(certificateNickname);
516
517    parser.addDependentArgumentSet(authID, authPassword, authPasswordFile);
518    parser.addExclusiveArgumentSet(authPassword, authPasswordFile, bearerToken);
519    parser.addExclusiveArgumentSet(authID, bearerToken);
520    parser.addExclusiveArgumentSet(keyStorePassword, keyStorePasswordFile);
521    parser.addExclusiveArgumentSet(trustStorePassword, trustStorePasswordFile);
522    parser.addExclusiveArgumentSet(trustAll, trustStorePath);
523    parser.addExclusiveArgumentSet(filter, resourceId);
524  }
525
526
527
528  /**
529   * {@inheritDoc}
530   */
531  @Override()
532  public LinkedHashMap<String[],String> getExampleUsages()
533  {
534    final LinkedHashMap<String[],String> examples =
535         new LinkedHashMap<String[],String>();
536
537    final String[] args1 =
538    {
539      "--hostname", "server.example.com",
540      "--port", "80",
541      "--authID", "admin",
542      "--authPassword", "password",
543      "--xml",
544      "--filter", "userName eq \"user.[1-1000000]\"",
545      "--attribute", "userName",
546      "--attribute", "name",
547      "--numThreads", "8"
548    };
549    examples.put(args1, INFO_QUERY_TOOL_EXAMPLE_1.get());
550
551    final String[] args2 =
552    {
553      "--hostname", "server.example.com",
554      "--port", "80",
555      "--authID", "admin",
556      "--authPassword", "password",
557      "--resourceID", "uid=user.[1-1000000],ou=people,dc=example,dc=com",
558      "--attribute", "userName",
559      "--attribute", "name",
560      "--numThreads", "8"
561    };
562    examples.put(args2, INFO_QUERY_TOOL_EXAMPLE_2.get());
563
564    return examples;
565  }
566
567
568
569  /**
570   * Performs the actual processing for this tool.  In this case, it gets a
571   * connection to the directory server and uses it to perform the requested
572   * searches.
573   *
574   * @return  The result code for the processing that was performed.
575   */
576  @Override()
577  public ResultCode doToolProcessing()
578  {
579    //Initalize the Debugger
580    Debug.setEnabled(true);
581    Debug.getLogger().addHandler(new ConsoleHandler());
582    Debug.getLogger().setUseParentHandlers(false);
583
584    // Determine the random seed to use.
585    final Long seed;
586    if (randomSeed.isPresent())
587    {
588      seed = Long.valueOf(randomSeed.getValue());
589    }
590    else
591    {
592      seed = null;
593    }
594
595    // Create a value pattern for the filter.
596    final ValuePattern filterPattern;
597    boolean isQuery = true;
598    if (filter.isPresent())
599    {
600      try
601      {
602        filterPattern = new ValuePattern(filter.getValue(), seed);
603      }
604      catch (ParseException pe)
605      {
606        Debug.debugException(pe);
607        err(ERR_QUERY_TOOL_BAD_FILTER_PATTERN.get(pe.getMessage()));
608        return ResultCode.PARAM_ERROR;
609      }
610    }
611    else if (resourceId.isPresent())
612    {
613      isQuery = false;
614      try
615      {
616        filterPattern = new ValuePattern(resourceId.getValue());
617      }
618      catch (ParseException pe)
619      {
620        Debug.debugException(pe);
621        err(ERR_QUERY_TOOL_BAD_RESOURCE_ID_PATTERN.get(pe.getMessage()));
622        return ResultCode.PARAM_ERROR;
623      }
624    }
625    else
626    {
627      filterPattern = null;
628    }
629
630    // Get the attributes to return.
631    final String[] attrs;
632    if (attributes.isPresent())
633    {
634      final List<String> attrList = attributes.getValues();
635      attrs = new String[attrList.size()];
636      attrList.toArray(attrs);
637    }
638    else
639    {
640      attrs = NO_STRINGS;
641    }
642
643
644    // If the --ratePerSecond option was specified, then limit the rate
645    // accordingly.
646    FixedRateBarrier fixedRateBarrier = null;
647    if (ratePerSecond.isPresent())
648    {
649      final int intervalSeconds = collectionInterval.getValue();
650      final int ratePerInterval = ratePerSecond.getValue() * intervalSeconds;
651
652      fixedRateBarrier =
653           new FixedRateBarrier(1000L * intervalSeconds, ratePerInterval);
654    }
655
656
657    // Determine whether to include timestamps in the output and if so what
658    // format should be used for them.
659    final boolean includeTimestamp;
660    final String timeFormat;
661    if (timestampFormat.getValue().equalsIgnoreCase("with-date"))
662    {
663      includeTimestamp = true;
664      timeFormat       = "dd/MM/yyyy HH:mm:ss";
665    }
666    else if (timestampFormat.getValue().equalsIgnoreCase("without-date"))
667    {
668      includeTimestamp = true;
669      timeFormat       = "HH:mm:ss";
670    }
671    else
672    {
673      includeTimestamp = false;
674      timeFormat       = null;
675    }
676
677
678    // Determine whether any warm-up intervals should be run.
679    final long totalIntervals;
680    final boolean warmUp;
681    int remainingWarmUpIntervals = warmUpIntervals.getValue();
682    if (remainingWarmUpIntervals > 0)
683    {
684      warmUp = true;
685      totalIntervals = 0L + numIntervals.getValue() + remainingWarmUpIntervals;
686    }
687    else
688    {
689      warmUp = true;
690      totalIntervals = 0L + numIntervals.getValue();
691    }
692
693
694    // Create the table that will be used to format the output.
695    final OutputFormat outputFormat;
696    if (csvFormat.isPresent())
697    {
698      outputFormat = OutputFormat.CSV;
699    }
700    else
701    {
702      outputFormat = OutputFormat.COLUMNS;
703    }
704
705    final ColumnFormatter formatter = new ColumnFormatter(includeTimestamp,
706         timeFormat, outputFormat, " ",
707         new FormattableColumn(15, HorizontalAlignment.RIGHT, "Recent",
708                  "Queries/Sec"),
709         new FormattableColumn(15, HorizontalAlignment.RIGHT, "Recent",
710                  "Avg Dur ms"),
711         new FormattableColumn(15, HorizontalAlignment.RIGHT, "Recent",
712                  "Resources/Query"),
713         new FormattableColumn(15, HorizontalAlignment.RIGHT, "Recent",
714                  "Errors/Sec"),
715         new FormattableColumn(15, HorizontalAlignment.RIGHT, "Overall",
716                  "Queries/Sec"),
717         new FormattableColumn(15, HorizontalAlignment.RIGHT, "Overall",
718                  "Avg Dur ms"));
719
720
721    // Create values to use for statistics collection.
722    final AtomicLong        queryCounter    = new AtomicLong(0L);
723    final AtomicLong        resourceCounter = new AtomicLong(0L);
724    final AtomicLong        errorCounter    = new AtomicLong(0L);
725    final AtomicLong        queryDurations  = new AtomicLong(0L);
726
727
728    // Determine the length of each interval in milliseconds.
729    final long intervalMillis = 1000L * collectionInterval.getValue();
730
731
732    // We will use Apache's HttpClient library for this tool.
733    SSLUtil sslUtil;
734    try
735    {
736      sslUtil = createSSLUtil();
737    }
738    catch (LDAPException e)
739    {
740      debugException(e);
741      err(e.getMessage());
742      return e.getResultCode();
743    }
744
745    RegistryBuilder<ConnectionSocketFactory> registryBuilder =
746        RegistryBuilder.create();
747    final String schemeName;
748    if (sslUtil != null)
749    {
750      try
751      {
752        SSLConnectionSocketFactory sslConnectionSocketFactory =
753            new SSLConnectionSocketFactory(sslUtil.createSSLContext("TLS"),
754                new NoopHostnameVerifier());
755        schemeName = "https";
756        registryBuilder.register(schemeName, sslConnectionSocketFactory);
757      }
758      catch (GeneralSecurityException e)
759      {
760        debugException(e);
761        err(ERR_SCIM_TOOL_CANNOT_CREATE_SSL_CONTEXT.get(
762            getExceptionMessage(e)));
763        return ResultCode.LOCAL_ERROR;
764      }
765    }
766    else
767    {
768      schemeName = "http";
769      registryBuilder.register(schemeName, new PlainConnectionSocketFactory());
770    }
771    final Registry<ConnectionSocketFactory> socketFactoryRegistry =
772        registryBuilder.build();
773
774    RequestConfig requestConfig = RequestConfig.custom()
775        .setConnectionRequestTimeout(30000)
776        .setExpectContinueEnabled(true).build();
777
778    SocketConfig socketConfig = SocketConfig.custom()
779        .setSoTimeout(30000)
780        .setSoReuseAddress(true)
781        .build();
782
783    final PoolingHttpClientConnectionManager mgr =
784        new PoolingHttpClientConnectionManager(socketFactoryRegistry);
785    mgr.setMaxTotal(numThreads.getValue());
786    mgr.setDefaultMaxPerRoute(numThreads.getValue());
787    mgr.setDefaultSocketConfig(socketConfig);
788    mgr.setValidateAfterInactivity(-1);
789
790    ClientConfig jerseyConfig = new ClientConfig();
791
792    jerseyConfig.property(ApacheClientProperties.CONNECTION_MANAGER, mgr);
793    jerseyConfig.property(ApacheClientProperties.REQUEST_CONFIG, requestConfig);
794    ApacheConnectorProvider connectorProvider = new ApacheConnectorProvider();
795    jerseyConfig.connectorProvider(connectorProvider);
796
797    if (authID.isPresent())
798    {
799      try
800      {
801        final String password;
802        if (authPassword.isPresent())
803        {
804          password = authPassword.getValue();
805        }
806        else if (authPasswordFile.isPresent())
807        {
808          password = authPasswordFile.getNonBlankFileLines().get(0);
809        }
810        else
811        {
812          password = null;
813        }
814
815        BasicCredentialsProvider provider = new BasicCredentialsProvider();
816        provider.setCredentials(
817            new AuthScope(host.getValue(), port.getValue()),
818            new UsernamePasswordCredentials(authID.getValue(), password)
819        );
820
821        jerseyConfig.property(
822            ApacheClientProperties.CREDENTIALS_PROVIDER, provider);
823        jerseyConfig.property(
824            ApacheClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, true);
825      }
826      catch (IOException e)
827      {
828        Debug.debugException(e);
829        err(ERR_QUERY_TOOL_SET_BASIC_AUTH.get(e.getMessage()));
830        return ResultCode.LOCAL_ERROR;
831      }
832    }
833    else if (bearerToken.isPresent())
834    {
835      jerseyConfig.register(
836          new ClientRequestFilter()
837          {
838            public void filter(final ClientRequestContext clientRequestContext)
839                throws IOException
840            {
841              try
842              {
843                clientRequestContext.getHeaders().add(
844                    "Authorization", "Bearer " + bearerToken.getValue());
845              }
846              catch (Exception ex)
847              {
848                throw new RuntimeException(
849                    "Unable to add authorization handler", ex);
850              }
851            }
852          }
853      );
854    }
855
856    // Create the SCIM client to use for the queries.
857    final URI uri;
858    try
859    {
860      final String path;
861      if (contextPath.getValue().startsWith("/"))
862      {
863        path = contextPath.getValue();
864      }
865      else
866      {
867        path = "/" + contextPath.getValue();
868      }
869      uri = new URI(schemeName, null, host.getValue(), port.getValue(),
870                    path, null, null);
871    }
872    catch (URISyntaxException e)
873    {
874      Debug.debugException(e);
875      err(ERR_QUERY_TOOL_CANNOT_CREATE_URL.get(e.getMessage()));
876      return ResultCode.OTHER;
877    }
878    final SCIMService service = new SCIMService(uri, jerseyConfig);
879
880    if (xmlFormat.isPresent())
881    {
882      service.setContentType(MediaType.APPLICATION_XML_TYPE);
883      service.setAcceptType(MediaType.APPLICATION_XML_TYPE);
884    }
885
886    // Retrieve the resource schema.
887    final ResourceDescriptor resourceDescriptor;
888    try
889    {
890      resourceDescriptor =
891        service.getResourceDescriptor(resourceName.getValue(), null);
892      if(resourceDescriptor == null)
893      {
894        throw new ResourceNotFoundException("Resource " +
895            resourceName.getValue() +
896            " is not defined by the service provider");
897      }
898    }
899    catch (SCIMException e)
900    {
901      Debug.debugException(e);
902      err(ERR_QUERY_TOOL_RETRIEVE_RESOURCE_SCHEMA.get(e.getMessage()));
903      return ResultCode.OTHER;
904    }
905
906    final SCIMEndpoint<? extends BaseResource> endpoint =
907        service.getEndpoint(resourceDescriptor,
908            BaseResource.BASE_RESOURCE_FACTORY);
909
910    // Create the threads to use for the searches.
911    final CyclicBarrier barrier = new CyclicBarrier(numThreads.getValue() + 1);
912    final QueryRateThread[] threads =
913         new QueryRateThread[numThreads.getValue()];
914    for (int i=0; i < threads.length; i++)
915    {
916      threads[i] =
917          new QueryRateThread(i, isQuery, endpoint, filterPattern, attrs,
918              barrier, queryCounter, resourceCounter, queryDurations,
919              errorCounter, fixedRateBarrier);
920      threads[i].start();
921    }
922
923
924    // Display the table header.
925    for (final String headerLine : formatter.getHeaderLines(true))
926    {
927      out(headerLine);
928    }
929
930
931    // Indicate that the threads can start running.
932    try
933    {
934      barrier.await();
935    }
936    catch (Exception e)
937    {
938      Debug.debugException(e);
939    }
940    long overallStartTime = System.nanoTime();
941    long nextIntervalStartTime = System.currentTimeMillis() + intervalMillis;
942
943
944    boolean setOverallStartTime = false;
945    long    lastDuration        = 0L;
946    long    lastNumEntries      = 0L;
947    long    lastNumErrors       = 0L;
948    long    lastNumSearches     = 0L;
949    long    lastEndTime         = System.nanoTime();
950    for (long i=0; i < totalIntervals; i++)
951    {
952      final long startTimeMillis = System.currentTimeMillis();
953      final long sleepTimeMillis = nextIntervalStartTime - startTimeMillis;
954      nextIntervalStartTime += intervalMillis;
955      try
956      {
957        if (sleepTimeMillis > 0)
958        {
959          Thread.sleep(sleepTimeMillis);
960        }
961      }
962      catch (Exception e)
963      {
964        Debug.debugException(e);
965      }
966
967      final long endTime          = System.nanoTime();
968      final long intervalDuration = endTime - lastEndTime;
969
970      final long numSearches;
971      final long numEntries;
972      final long numErrors;
973      final long totalDuration;
974      if (warmUp && (remainingWarmUpIntervals > 0))
975      {
976        numSearches   = queryCounter.getAndSet(0L);
977        numEntries    = resourceCounter.getAndSet(0L);
978        numErrors     = errorCounter.getAndSet(0L);
979        totalDuration = queryDurations.getAndSet(0L);
980      }
981      else
982      {
983        numSearches   = queryCounter.get();
984        numEntries    = resourceCounter.get();
985        numErrors     = errorCounter.get();
986        totalDuration = queryDurations.get();
987      }
988
989      final long recentNumSearches = numSearches - lastNumSearches;
990      final long recentNumEntries = numEntries - lastNumEntries;
991      final long recentNumErrors = numErrors - lastNumErrors;
992      final long recentDuration = totalDuration - lastDuration;
993
994      final double numSeconds = intervalDuration / 1000000000.0d;
995      final double recentSearchRate = recentNumSearches / numSeconds;
996      final double recentErrorRate  = recentNumErrors / numSeconds;
997
998      final double recentAvgDuration;
999      final double recentEntriesPerSearch;
1000      if (recentNumSearches > 0L)
1001      {
1002        recentEntriesPerSearch = 1.0d * recentNumEntries / recentNumSearches;
1003        recentAvgDuration = 1.0d * recentDuration / recentNumSearches / 1000000;
1004      }
1005      else
1006      {
1007        recentEntriesPerSearch = 0.0d;
1008        recentAvgDuration = 0.0d;
1009      }
1010
1011
1012      if (warmUp && (remainingWarmUpIntervals > 0))
1013      {
1014        out(formatter.formatRow(recentSearchRate, recentAvgDuration,
1015             recentEntriesPerSearch, recentErrorRate, "warming up",
1016             "warming up"));
1017
1018        remainingWarmUpIntervals--;
1019        if (remainingWarmUpIntervals == 0)
1020        {
1021          out(INFO_QUERY_TOOL_WARM_UP_COMPLETED.get());
1022          setOverallStartTime = true;
1023        }
1024      }
1025      else
1026      {
1027        if (setOverallStartTime)
1028        {
1029          overallStartTime    = lastEndTime;
1030          setOverallStartTime = false;
1031        }
1032
1033        final double numOverallSeconds =
1034             (endTime - overallStartTime) / 1000000000.0d;
1035        final double overallSearchRate = numSearches / numOverallSeconds;
1036
1037        final double overallAvgDuration;
1038        if (numSearches > 0L)
1039        {
1040          overallAvgDuration = 1.0d * totalDuration / numSearches / 1000000;
1041        }
1042        else
1043        {
1044          overallAvgDuration = 0.0d;
1045        }
1046
1047        out(formatter.formatRow(recentSearchRate, recentAvgDuration,
1048             recentEntriesPerSearch, recentErrorRate, overallSearchRate,
1049             overallAvgDuration));
1050
1051        lastNumSearches = numSearches;
1052        lastNumEntries  = numEntries;
1053        lastNumErrors   = numErrors;
1054        lastDuration    = totalDuration;
1055      }
1056
1057      lastEndTime = endTime;
1058    }
1059
1060
1061    // Stop all of the threads.
1062    ResultCode resultCode = ResultCode.SUCCESS;
1063    for (final QueryRateThread t : threads)
1064    {
1065      t.signalShutdown();
1066    }
1067
1068    // Interrupt any blocked threads after a grace period.
1069    final WakeableSleeper sleeper = new WakeableSleeper();
1070    sleeper.sleep(1000);
1071    mgr.shutdown();
1072
1073    for (final QueryRateThread t : threads)
1074    {
1075      final ResultCode r = t.waitForShutdown();
1076      if (resultCode == ResultCode.SUCCESS)
1077      {
1078        resultCode = r;
1079      }
1080    }
1081
1082    return resultCode;
1083  }
1084
1085
1086
1087  /**
1088   * Creates the SSLUtil instance to use for secure communication.
1089   *
1090   * @return  The SSLUtil instance to use for secure communication, or
1091   *          {@code null} if secure communication is not needed.
1092   *
1093   * @throws LDAPException   If a problem occurs while creating the SSLUtil
1094   *                         instance.
1095   */
1096  private SSLUtil createSSLUtil()
1097          throws LDAPException
1098  {
1099    if (useSSL.isPresent())
1100    {
1101      KeyManager keyManager = null;
1102      if (keyStorePath.isPresent())
1103      {
1104        char[] pw = null;
1105        if (keyStorePassword.isPresent())
1106        {
1107          pw = keyStorePassword.getValue().toCharArray();
1108        }
1109        else if (keyStorePasswordFile.isPresent())
1110        {
1111          try
1112          {
1113            pw = keyStorePasswordFile.getNonBlankFileLines().get(0).
1114                      toCharArray();
1115          }
1116          catch (Exception e)
1117          {
1118            Debug.debugException(e);
1119            throw new LDAPException(ResultCode.LOCAL_ERROR,
1120                 ERR_SCIM_TOOL_CANNOT_READ_KEY_STORE_PASSWORD.get(
1121                      getExceptionMessage(e)), e);
1122          }
1123        }
1124
1125        try
1126        {
1127          keyManager = new KeyStoreKeyManager(keyStorePath.getValue(), pw,
1128               keyStoreFormat.getValue(), certificateNickname.getValue());
1129        }
1130        catch (Exception e)
1131        {
1132          Debug.debugException(e);
1133          throw new LDAPException(ResultCode.LOCAL_ERROR,
1134               ERR_SCIM_TOOL_CANNOT_CREATE_KEY_MANAGER.get(
1135                    getExceptionMessage(e)), e);
1136        }
1137      }
1138
1139      TrustManager trustManager;
1140      if (trustAll.isPresent())
1141      {
1142        trustManager = new TrustAllTrustManager(false);
1143      }
1144      else if (trustStorePath.isPresent())
1145      {
1146        char[] pw = null;
1147        if (trustStorePassword.isPresent())
1148        {
1149          pw = trustStorePassword.getValue().toCharArray();
1150        }
1151        else if (trustStorePasswordFile.isPresent())
1152        {
1153          try
1154          {
1155            pw = trustStorePasswordFile.getNonBlankFileLines().get(0).
1156                      toCharArray();
1157          }
1158          catch (Exception e)
1159          {
1160            Debug.debugException(e);
1161            throw new LDAPException(ResultCode.LOCAL_ERROR,
1162                 ERR_SCIM_TOOL_CANNOT_READ_TRUST_STORE_PASSWORD.get(
1163                      getExceptionMessage(e)), e);
1164          }
1165        }
1166
1167        trustManager = new TrustStoreTrustManager(trustStorePath.getValue(), pw,
1168             trustStoreFormat.getValue(), true);
1169      }
1170      else
1171      {
1172        trustManager = promptTrustManager.get();
1173        if (trustManager == null)
1174        {
1175          final PromptTrustManager m = new PromptTrustManager();
1176          promptTrustManager.compareAndSet(null, m);
1177          trustManager = promptTrustManager.get();
1178        }
1179      }
1180
1181      return new SSLUtil(keyManager, trustManager);
1182    }
1183    else
1184    {
1185      return null;
1186    }
1187  }
1188}