001/*
002 * Copyright 2017-2019 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2017-2019 Ping Identity Corporation
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.ldap.sdk.unboundidds.tools;
022
023
024
025import java.io.BufferedReader;
026import java.io.File;
027import java.io.FileOutputStream;
028import java.io.FileReader;
029import java.io.IOException;
030import java.io.OutputStream;
031import java.io.PrintStream;
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.Collections;
035import java.util.EnumSet;
036import java.util.Iterator;
037import java.util.LinkedHashMap;
038import java.util.List;
039import java.util.Map;
040import java.util.Set;
041import java.util.StringTokenizer;
042import java.util.concurrent.atomic.AtomicLong;
043import java.util.zip.GZIPOutputStream;
044
045import com.unboundid.asn1.ASN1OctetString;
046import com.unboundid.ldap.sdk.Control;
047import com.unboundid.ldap.sdk.DN;
048import com.unboundid.ldap.sdk.DereferencePolicy;
049import com.unboundid.ldap.sdk.ExtendedResult;
050import com.unboundid.ldap.sdk.Filter;
051import com.unboundid.ldap.sdk.LDAPConnectionOptions;
052import com.unboundid.ldap.sdk.LDAPConnection;
053import com.unboundid.ldap.sdk.LDAPConnectionPool;
054import com.unboundid.ldap.sdk.LDAPException;
055import com.unboundid.ldap.sdk.LDAPResult;
056import com.unboundid.ldap.sdk.LDAPSearchException;
057import com.unboundid.ldap.sdk.LDAPURL;
058import com.unboundid.ldap.sdk.ResultCode;
059import com.unboundid.ldap.sdk.SearchRequest;
060import com.unboundid.ldap.sdk.SearchResult;
061import com.unboundid.ldap.sdk.SearchScope;
062import com.unboundid.ldap.sdk.UnsolicitedNotificationHandler;
063import com.unboundid.ldap.sdk.Version;
064import com.unboundid.ldap.sdk.controls.AssertionRequestControl;
065import com.unboundid.ldap.sdk.controls.AuthorizationIdentityRequestControl;
066import com.unboundid.ldap.sdk.controls.ManageDsaITRequestControl;
067import com.unboundid.ldap.sdk.controls.MatchedValuesFilter;
068import com.unboundid.ldap.sdk.controls.MatchedValuesRequestControl;
069import com.unboundid.ldap.sdk.controls.PersistentSearchChangeType;
070import com.unboundid.ldap.sdk.controls.PersistentSearchRequestControl;
071import com.unboundid.ldap.sdk.controls.ProxiedAuthorizationV1RequestControl;
072import com.unboundid.ldap.sdk.controls.ProxiedAuthorizationV2RequestControl;
073import com.unboundid.ldap.sdk.controls.ServerSideSortRequestControl;
074import com.unboundid.ldap.sdk.controls.SimplePagedResultsControl;
075import com.unboundid.ldap.sdk.controls.SortKey;
076import com.unboundid.ldap.sdk.controls.SubentriesRequestControl;
077import com.unboundid.ldap.sdk.controls.VirtualListViewRequestControl;
078import com.unboundid.ldap.sdk.persist.PersistUtils;
079import com.unboundid.ldap.sdk.transformations.EntryTransformation;
080import com.unboundid.ldap.sdk.transformations.ExcludeAttributeTransformation;
081import com.unboundid.ldap.sdk.transformations.MoveSubtreeTransformation;
082import com.unboundid.ldap.sdk.transformations.RedactAttributeTransformation;
083import com.unboundid.ldap.sdk.transformations.RenameAttributeTransformation;
084import com.unboundid.ldap.sdk.transformations.ScrambleAttributeTransformation;
085import com.unboundid.ldap.sdk.unboundidds.controls.AccountUsableRequestControl;
086import com.unboundid.ldap.sdk.unboundidds.controls.ExcludeBranchRequestControl;
087import com.unboundid.ldap.sdk.unboundidds.controls.
088            GetAuthorizationEntryRequestControl;
089import com.unboundid.ldap.sdk.unboundidds.controls.
090            GetBackendSetIDRequestControl;
091import com.unboundid.ldap.sdk.unboundidds.controls.
092            GetEffectiveRightsRequestControl;
093import com.unboundid.ldap.sdk.unboundidds.controls.GetServerIDRequestControl;
094import com.unboundid.ldap.sdk.unboundidds.controls.
095            GetUserResourceLimitsRequestControl;
096import com.unboundid.ldap.sdk.unboundidds.controls.JoinBaseDN;
097import com.unboundid.ldap.sdk.unboundidds.controls.JoinRequestControl;
098import com.unboundid.ldap.sdk.unboundidds.controls.JoinRequestValue;
099import com.unboundid.ldap.sdk.unboundidds.controls.JoinRule;
100import com.unboundid.ldap.sdk.unboundidds.controls.
101            MatchingEntryCountRequestControl;
102import com.unboundid.ldap.sdk.unboundidds.controls.
103            OperationPurposeRequestControl;
104import com.unboundid.ldap.sdk.unboundidds.controls.
105            OverrideSearchLimitsRequestControl;
106import com.unboundid.ldap.sdk.unboundidds.controls.PasswordPolicyRequestControl;
107import com.unboundid.ldap.sdk.unboundidds.controls.
108            PermitUnindexedSearchRequestControl;
109import com.unboundid.ldap.sdk.unboundidds.controls.
110            RealAttributesOnlyRequestControl;
111import com.unboundid.ldap.sdk.unboundidds.controls.
112            RejectUnindexedSearchRequestControl;
113import com.unboundid.ldap.sdk.unboundidds.controls.
114            ReturnConflictEntriesRequestControl;
115import com.unboundid.ldap.sdk.unboundidds.controls.
116            RouteToBackendSetRequestControl;
117import com.unboundid.ldap.sdk.unboundidds.controls.RouteToServerRequestControl;
118import com.unboundid.ldap.sdk.unboundidds.controls.
119            SoftDeletedEntryAccessRequestControl;
120import com.unboundid.ldap.sdk.unboundidds.controls.
121            SuppressOperationalAttributeUpdateRequestControl;
122import com.unboundid.ldap.sdk.unboundidds.controls.SuppressType;
123import com.unboundid.ldap.sdk.unboundidds.controls.
124            VirtualAttributesOnlyRequestControl;
125import com.unboundid.ldap.sdk.unboundidds.extensions.
126            StartAdministrativeSessionExtendedRequest;
127import com.unboundid.ldap.sdk.unboundidds.extensions.
128            StartAdministrativeSessionPostConnectProcessor;
129import com.unboundid.ldif.LDIFWriter;
130import com.unboundid.util.Debug;
131import com.unboundid.util.FilterFileReader;
132import com.unboundid.util.FixedRateBarrier;
133import com.unboundid.util.LDAPCommandLineTool;
134import com.unboundid.util.OutputFormat;
135import com.unboundid.util.PassphraseEncryptedOutputStream;
136import com.unboundid.util.StaticUtils;
137import com.unboundid.util.TeeOutputStream;
138import com.unboundid.util.ThreadSafety;
139import com.unboundid.util.ThreadSafetyLevel;
140import com.unboundid.util.args.ArgumentException;
141import com.unboundid.util.args.ArgumentParser;
142import com.unboundid.util.args.BooleanArgument;
143import com.unboundid.util.args.ControlArgument;
144import com.unboundid.util.args.DNArgument;
145import com.unboundid.util.args.FileArgument;
146import com.unboundid.util.args.FilterArgument;
147import com.unboundid.util.args.IntegerArgument;
148import com.unboundid.util.args.ScopeArgument;
149import com.unboundid.util.args.StringArgument;
150
151import static com.unboundid.ldap.sdk.unboundidds.tools.ToolMessages.*;
152
153
154
155/**
156 * This class provides an implementation of an LDAP command-line tool that may
157 * be used to issue searches to a directory server.  Matching entries will be
158 * output in the LDAP data interchange format (LDIF), to standard output and/or
159 * to a specified file.  This is a much more full-featured tool than the
160 * {@link com.unboundid.ldap.sdk.examples.LDAPSearch} tool, and includes a
161 * number of features only intended for use with Ping Identity, UnboundID, and
162 * Nokia/Alcatel-Lucent 8661 server products.
163 * <BR>
164 * <BLOCKQUOTE>
165 *   <B>NOTE:</B>  This class, and other classes within the
166 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
167 *   supported for use against Ping Identity, UnboundID, and
168 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
169 *   for proprietary functionality or for external specifications that are not
170 *   considered stable or mature enough to be guaranteed to work in an
171 *   interoperable way with other types of LDAP servers.
172 * </BLOCKQUOTE>
173 */
174@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
175public final class LDAPSearch
176       extends LDAPCommandLineTool
177       implements UnsolicitedNotificationHandler
178{
179  /**
180   * The column at which to wrap long lines.
181   */
182  private static int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
183
184
185
186  // The set of arguments supported by this program.
187  private BooleanArgument accountUsable = null;
188  private BooleanArgument authorizationIdentity = null;
189  private BooleanArgument compressOutput = null;
190  private BooleanArgument continueOnError = null;
191  private BooleanArgument countEntries = null;
192  private BooleanArgument dontWrap = null;
193  private BooleanArgument dryRun = null;
194  private BooleanArgument encryptOutput = null;
195  private BooleanArgument followReferrals = null;
196  private BooleanArgument hideRedactedValueCount = null;
197  private BooleanArgument getBackendSetID = null;
198  private BooleanArgument getServerID = null;
199  private BooleanArgument getUserResourceLimits = null;
200  private BooleanArgument includeReplicationConflictEntries = null;
201  private BooleanArgument includeSubentries = null;
202  private BooleanArgument joinRequireMatch = null;
203  private BooleanArgument manageDsaIT = null;
204  private BooleanArgument permitUnindexedSearch = null;
205  private BooleanArgument realAttributesOnly = null;
206  private BooleanArgument rejectUnindexedSearch = null;
207  private BooleanArgument retryFailedOperations = null;
208  private BooleanArgument separateOutputFilePerSearch = null;
209  private BooleanArgument suppressBase64EncodedValueComments = null;
210  private BooleanArgument teeResultsToStandardOut = null;
211  private BooleanArgument useAdministrativeSession = null;
212  private BooleanArgument usePasswordPolicyControl = null;
213  private BooleanArgument terse = null;
214  private BooleanArgument typesOnly = null;
215  private BooleanArgument verbose = null;
216  private BooleanArgument virtualAttributesOnly = null;
217  private ControlArgument bindControl = null;
218  private ControlArgument searchControl = null;
219  private DNArgument baseDN = null;
220  private DNArgument excludeBranch = null;
221  private DNArgument moveSubtreeFrom = null;
222  private DNArgument moveSubtreeTo = null;
223  private DNArgument proxyV1As = null;
224  private FileArgument encryptionPassphraseFile = null;
225  private FileArgument filterFile = null;
226  private FileArgument ldapURLFile = null;
227  private FileArgument outputFile = null;
228  private FilterArgument assertionFilter = null;
229  private FilterArgument filter = null;
230  private FilterArgument joinFilter = null;
231  private FilterArgument matchedValuesFilter = null;
232  private IntegerArgument joinSizeLimit = null;
233  private IntegerArgument ratePerSecond = null;
234  private IntegerArgument scrambleRandomSeed = null;
235  private IntegerArgument simplePageSize = null;
236  private IntegerArgument sizeLimit = null;
237  private IntegerArgument timeLimitSeconds = null;
238  private IntegerArgument wrapColumn = null;
239  private ScopeArgument joinScope = null;
240  private ScopeArgument scope = null;
241  private StringArgument dereferencePolicy = null;
242  private StringArgument excludeAttribute = null;
243  private StringArgument getAuthorizationEntryAttribute = null;
244  private StringArgument getEffectiveRightsAttribute = null;
245  private StringArgument getEffectiveRightsAuthzID = null;
246  private StringArgument includeSoftDeletedEntries = null;
247  private StringArgument joinBaseDN = null;
248  private StringArgument joinRequestedAttribute = null;
249  private StringArgument joinRule = null;
250  private StringArgument matchingEntryCountControl = null;
251  private StringArgument operationPurpose = null;
252  private StringArgument outputFormat = null;
253  private StringArgument overrideSearchLimit = null;
254  private StringArgument persistentSearch = null;
255  private StringArgument proxyAs = null;
256  private StringArgument redactAttribute = null;
257  private StringArgument renameAttributeFrom = null;
258  private StringArgument renameAttributeTo = null;
259  private StringArgument requestedAttribute = null;
260  private StringArgument routeToBackendSet = null;
261  private StringArgument routeToServer = null;
262  private StringArgument scrambleAttribute = null;
263  private StringArgument scrambleJSONField = null;
264  private StringArgument sortOrder = null;
265  private StringArgument suppressOperationalAttributeUpdates = null;
266  private StringArgument virtualListView = null;
267
268  // The argument parser used by this tool.
269  private volatile ArgumentParser parser = null;
270
271  // Controls that should be sent to the server but need special validation.
272  private volatile JoinRequestControl joinRequestControl = null;
273  private final List<RouteToBackendSetRequestControl>
274       routeToBackendSetRequestControls = new ArrayList<>(10);
275  private volatile MatchedValuesRequestControl
276       matchedValuesRequestControl = null;
277  private volatile MatchingEntryCountRequestControl
278       matchingEntryCountRequestControl = null;
279  private volatile OverrideSearchLimitsRequestControl
280       overrideSearchLimitsRequestControl = null;
281  private volatile PersistentSearchRequestControl
282       persistentSearchRequestControl = null;
283  private volatile ServerSideSortRequestControl sortRequestControl = null;
284  private volatile VirtualListViewRequestControl vlvRequestControl = null;
285
286  // Other values decoded from arguments.
287  private volatile DereferencePolicy derefPolicy = null;
288
289  // The print streams used for standard output and error.
290  private final AtomicLong outputFileCounter = new AtomicLong(1);
291  private volatile PrintStream errStream = null;
292  private volatile PrintStream outStream = null;
293
294  // The output handler for this tool.
295  private volatile LDAPSearchOutputHandler outputHandler =
296       new LDIFLDAPSearchOutputHandler(this, WRAP_COLUMN);
297
298  // The list of entry transformations to apply.
299  private volatile List<EntryTransformation> entryTransformations = null;
300
301  // The encryption passphrase to use if the output is to be encrypted.
302  private String encryptionPassphrase = null;
303
304
305
306  /**
307   * Runs this tool with the provided command-line arguments.  It will use the
308   * JVM-default streams for standard input, output, and error.
309   *
310   * @param  args  The command-line arguments to provide to this program.
311   */
312  public static void main(final String... args)
313  {
314    final ResultCode resultCode = main(System.out, System.err, args);
315    if (resultCode != ResultCode.SUCCESS)
316    {
317      System.exit(Math.min(resultCode.intValue(), 255));
318    }
319  }
320
321
322
323  /**
324   * Runs this tool with the provided streams and command-line arguments.
325   *
326   * @param  out   The output stream to use for standard output.  If this is
327   *               {@code null}, then standard output will be suppressed.
328   * @param  err   The output stream to use for standard error.  If this is
329   *               {@code null}, then standard error will be suppressed.
330   * @param  args  The command-line arguments provided to this program.
331   *
332   * @return  The result code obtained when running the tool.  Any result code
333   *          other than {@link ResultCode#SUCCESS} indicates an error.
334   */
335  public static ResultCode main(final OutputStream out, final OutputStream err,
336                                final String... args)
337  {
338    final LDAPSearch tool = new LDAPSearch(out, err);
339    return tool.runTool(args);
340  }
341
342
343
344  /**
345   * Creates a new instance of this tool with the provided streams.
346   *
347   * @param  out  The output stream to use for standard output.  If this is
348   *              {@code null}, then standard output will be suppressed.
349   * @param  err  The output stream to use for standard error.  If this is
350   *              {@code null}, then standard error will be suppressed.
351   */
352  public LDAPSearch(final OutputStream out, final OutputStream err)
353  {
354    super(out, err);
355  }
356
357
358
359  /**
360   * {@inheritDoc}
361   */
362  @Override()
363  public String getToolName()
364  {
365    return "ldapsearch";
366  }
367
368
369
370  /**
371   * {@inheritDoc}
372   */
373  @Override()
374  public String getToolDescription()
375  {
376    return INFO_LDAPSEARCH_TOOL_DESCRIPTION.get();
377  }
378
379
380
381  /**
382   * {@inheritDoc}
383   */
384  @Override()
385  public List<String> getAdditionalDescriptionParagraphs()
386  {
387    return Arrays.asList(
388         INFO_LDAPSEARCH_ADDITIONAL_DESCRIPTION_PARAGRAPH_1.get(),
389         INFO_LDAPSEARCH_ADDITIONAL_DESCRIPTION_PARAGRAPH_2.get());
390  }
391
392
393
394  /**
395   * {@inheritDoc}
396   */
397  @Override()
398  public String getToolVersion()
399  {
400    return Version.NUMERIC_VERSION_STRING;
401  }
402
403
404
405  /**
406   * {@inheritDoc}
407   */
408  @Override()
409  public int getMinTrailingArguments()
410  {
411    return 0;
412  }
413
414
415
416  /**
417   * {@inheritDoc}
418   */
419  @Override()
420  public int getMaxTrailingArguments()
421  {
422    return -1;
423  }
424
425
426
427  /**
428   * {@inheritDoc}
429   */
430  @Override()
431  public String getTrailingArgumentsPlaceholder()
432  {
433    return INFO_LDAPSEARCH_TRAILING_ARGS_PLACEHOLDER.get();
434  }
435
436
437
438  /**
439   * {@inheritDoc}
440   */
441  @Override()
442  public boolean supportsInteractiveMode()
443  {
444    return true;
445  }
446
447
448
449  /**
450   * {@inheritDoc}
451   */
452  @Override()
453  public boolean defaultsToInteractiveMode()
454  {
455    return true;
456  }
457
458
459
460  /**
461   * {@inheritDoc}
462   */
463  @Override()
464  public boolean supportsPropertiesFile()
465  {
466    return true;
467  }
468
469
470
471  /**
472   * {@inheritDoc}
473   */
474  @Override()
475  protected boolean defaultToPromptForBindPassword()
476  {
477    return true;
478  }
479
480
481
482  /**
483   * {@inheritDoc}
484   */
485  @Override()
486  protected boolean includeAlternateLongIdentifiers()
487  {
488    return true;
489  }
490
491
492
493  /**
494   * {@inheritDoc}
495   */
496  @Override()
497  protected Set<Character> getSuppressedShortIdentifiers()
498  {
499    return Collections.singleton('T');
500  }
501
502
503
504  /**
505   * {@inheritDoc}
506   */
507  @Override()
508  public void addNonLDAPArguments(final ArgumentParser parser)
509         throws ArgumentException
510  {
511    this.parser = parser;
512
513    baseDN = new DNArgument('b', "baseDN", false, 1, null,
514         INFO_LDAPSEARCH_ARG_DESCRIPTION_BASE_DN.get());
515    baseDN.addLongIdentifier("base-dn", true);
516    baseDN.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_OPS.get());
517    parser.addArgument(baseDN);
518
519    scope = new ScopeArgument('s', "scope", false, null,
520         INFO_LDAPSEARCH_ARG_DESCRIPTION_SCOPE.get(), SearchScope.SUB);
521    scope.addLongIdentifier("searchScope", true);
522    scope.addLongIdentifier("search-scope", true);
523    scope.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_OPS.get());
524    parser.addArgument(scope);
525
526    sizeLimit = new IntegerArgument('z', "sizeLimit", false, 1, null,
527         INFO_LDAPSEARCH_ARG_DESCRIPTION_SIZE_LIMIT.get(), 0,
528         Integer.MAX_VALUE, 0);
529    sizeLimit.addLongIdentifier("size-limit", true);
530    sizeLimit.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_OPS.get());
531    parser.addArgument(sizeLimit);
532
533    timeLimitSeconds = new IntegerArgument('l', "timeLimitSeconds", false, 1,
534         null, INFO_LDAPSEARCH_ARG_DESCRIPTION_TIME_LIMIT.get(), 0,
535         Integer.MAX_VALUE, 0);
536    timeLimitSeconds.addLongIdentifier("timeLimit", true);
537    timeLimitSeconds.addLongIdentifier("time-limit-seconds", true);
538    timeLimitSeconds.addLongIdentifier("time-limit", true);
539    timeLimitSeconds.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_OPS.get());
540    parser.addArgument(timeLimitSeconds);
541
542    final Set<String> derefAllowedValues =
543         StaticUtils.setOf("never", "always", "search", "find");
544    dereferencePolicy = new StringArgument('a', "dereferencePolicy", false, 1,
545         "{never|always|search|find}",
546         INFO_LDAPSEARCH_ARG_DESCRIPTION_DEREFERENCE_POLICY.get(),
547         derefAllowedValues, "never");
548    dereferencePolicy.addLongIdentifier("dereference-policy", true);
549    dereferencePolicy.setArgumentGroupName(
550         INFO_LDAPSEARCH_ARG_GROUP_OPS.get());
551    parser.addArgument(dereferencePolicy);
552
553    typesOnly = new BooleanArgument('A', "typesOnly", 1,
554         INFO_LDAPSEARCH_ARG_DESCRIPTION_TYPES_ONLY.get());
555    typesOnly.addLongIdentifier("types-only", true);
556    typesOnly.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_OPS.get());
557    parser.addArgument(typesOnly);
558
559    requestedAttribute = new StringArgument(null, "requestedAttribute", false,
560         0, INFO_PLACEHOLDER_ATTR.get(),
561         INFO_LDAPSEARCH_ARG_DESCRIPTION_REQUESTED_ATTR.get());
562    requestedAttribute.addLongIdentifier("requested-attribute", true);
563    requestedAttribute.setArgumentGroupName(
564         INFO_LDAPSEARCH_ARG_GROUP_OPS.get());
565    parser.addArgument(requestedAttribute);
566
567    filter = new FilterArgument(null, "filter", false, 0,
568         INFO_PLACEHOLDER_FILTER.get(),
569         INFO_LDAPSEARCH_ARG_DESCRIPTION_FILTER.get());
570    filter.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_OPS.get());
571    parser.addArgument(filter);
572
573    filterFile = new FileArgument('f', "filterFile", false, 0, null,
574         INFO_LDAPSEARCH_ARG_DESCRIPTION_FILTER_FILE.get(), true, true,
575         true, false);
576    filterFile.addLongIdentifier("filename", true);
577    filterFile.addLongIdentifier("filter-file", true);
578    filterFile.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_OPS.get());
579    parser.addArgument(filterFile);
580
581    ldapURLFile = new FileArgument(null, "ldapURLFile", false, 0, null,
582         INFO_LDAPSEARCH_ARG_DESCRIPTION_LDAP_URL_FILE.get(), true, true,
583         true, false);
584    ldapURLFile.addLongIdentifier("ldap-url-file", true);
585    ldapURLFile.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_OPS.get());
586    parser.addArgument(ldapURLFile);
587
588    followReferrals = new BooleanArgument(null, "followReferrals", 1,
589         INFO_LDAPSEARCH_ARG_DESCRIPTION_FOLLOW_REFERRALS.get());
590    followReferrals.addLongIdentifier("follow-referrals", true);
591    followReferrals.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_OPS.get());
592    parser.addArgument(followReferrals);
593
594    retryFailedOperations = new BooleanArgument(null, "retryFailedOperations",
595         1, INFO_LDAPSEARCH_ARG_DESCRIPTION_RETRY_FAILED_OPERATIONS.get());
596    retryFailedOperations.addLongIdentifier("retry-failed-operations", true);
597    retryFailedOperations.setArgumentGroupName(
598         INFO_LDAPSEARCH_ARG_GROUP_OPS.get());
599    parser.addArgument(retryFailedOperations);
600
601    continueOnError = new BooleanArgument('c', "continueOnError", 1,
602         INFO_LDAPSEARCH_ARG_DESCRIPTION_CONTINUE_ON_ERROR.get());
603    continueOnError.addLongIdentifier("continue-on-error", true);
604    continueOnError.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_OPS.get());
605    parser.addArgument(continueOnError);
606
607    ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1,
608         INFO_PLACEHOLDER_NUM.get(),
609         INFO_LDAPSEARCH_ARG_DESCRIPTION_RATE_PER_SECOND.get(), 1,
610         Integer.MAX_VALUE);
611    ratePerSecond.addLongIdentifier("rate-per-second", true);
612    ratePerSecond.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_OPS.get());
613    parser.addArgument(ratePerSecond);
614
615    useAdministrativeSession = new BooleanArgument(null,
616         "useAdministrativeSession", 1,
617         INFO_LDAPSEARCH_ARG_DESCRIPTION_USE_ADMIN_SESSION.get());
618    useAdministrativeSession.addLongIdentifier("use-administrative-session",
619         true);
620    useAdministrativeSession.setArgumentGroupName(
621         INFO_LDAPSEARCH_ARG_GROUP_OPS.get());
622    parser.addArgument(useAdministrativeSession);
623
624    dryRun = new BooleanArgument('n', "dryRun", 1,
625         INFO_LDAPSEARCH_ARG_DESCRIPTION_DRY_RUN.get());
626    dryRun.addLongIdentifier("dry-run", true);
627    dryRun.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_OPS.get());
628    parser.addArgument(dryRun);
629
630    wrapColumn = new IntegerArgument(null, "wrapColumn", false, 1, null,
631         INFO_LDAPSEARCH_ARG_DESCRIPTION_WRAP_COLUMN.get(), 0,
632         Integer.MAX_VALUE);
633    wrapColumn.addLongIdentifier("wrap-column", true);
634    wrapColumn.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_DATA.get());
635    parser.addArgument(wrapColumn);
636
637    dontWrap = new BooleanArgument('T', "dontWrap", 1,
638         INFO_LDAPSEARCH_ARG_DESCRIPTION_DONT_WRAP.get());
639    dontWrap.addLongIdentifier("doNotWrap", true);
640    dontWrap.addLongIdentifier("dont-wrap", true);
641    dontWrap.addLongIdentifier("do-not-wrap", true);
642    dontWrap.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_DATA.get());
643    parser.addArgument(dontWrap);
644
645    suppressBase64EncodedValueComments = new BooleanArgument(null,
646         "suppressBase64EncodedValueComments", 1,
647         INFO_LDAPSEARCH_ARG_DESCRIPTION_SUPPRESS_BASE64_COMMENTS.get());
648    suppressBase64EncodedValueComments.addLongIdentifier(
649         "suppress-base64-encoded-value-comments", true);
650    suppressBase64EncodedValueComments.setArgumentGroupName(
651         INFO_LDAPSEARCH_ARG_GROUP_DATA.get());
652    parser.addArgument(suppressBase64EncodedValueComments);
653
654    countEntries = new BooleanArgument(null, "countEntries", 1,
655         INFO_LDAPSEARCH_ARG_DESCRIPTION_COUNT_ENTRIES.get());
656    countEntries.addLongIdentifier("count-entries", true);
657    countEntries.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_OPS.get());
658    countEntries.setHidden(true);
659    parser.addArgument(countEntries);
660
661    outputFile = new FileArgument(null, "outputFile", false, 1, null,
662         INFO_LDAPSEARCH_ARG_DESCRIPTION_OUTPUT_FILE.get(), false, true, true,
663         false);
664    outputFile.addLongIdentifier("output-file", true);
665    outputFile.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_DATA.get());
666    parser.addArgument(outputFile);
667
668    compressOutput = new BooleanArgument(null, "compressOutput", 1,
669         INFO_LDAPSEARCH_ARG_DESCRIPTION_COMPRESS_OUTPUT.get());
670    compressOutput.addLongIdentifier("compress-output", true);
671    compressOutput.addLongIdentifier("compress", true);
672    compressOutput.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_DATA.get());
673    parser.addArgument(compressOutput);
674
675    encryptOutput = new BooleanArgument(null, "encryptOutput", 1,
676         INFO_LDAPSEARCH_ARG_DESCRIPTION_ENCRYPT_OUTPUT.get());
677    encryptOutput.addLongIdentifier("encrypt-output", true);
678    encryptOutput.addLongIdentifier("encrypt", true);
679    encryptOutput.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_DATA.get());
680    parser.addArgument(encryptOutput);
681
682    encryptionPassphraseFile = new FileArgument(null,
683         "encryptionPassphraseFile", false, 1, null,
684         INFO_LDAPSEARCH_ARG_DESCRIPTION_ENCRYPTION_PW_FILE.get(), true, true,
685         true, false);
686    encryptionPassphraseFile.addLongIdentifier("encryption-passphrase-file",
687         true);
688    encryptionPassphraseFile.addLongIdentifier("encryptionPasswordFile", true);
689    encryptionPassphraseFile.addLongIdentifier("encryption-password-file",
690         true);
691    encryptionPassphraseFile.setArgumentGroupName(
692         INFO_LDAPSEARCH_ARG_GROUP_DATA.get());
693    parser.addArgument(encryptionPassphraseFile);
694
695    separateOutputFilePerSearch = new BooleanArgument(null,
696         "separateOutputFilePerSearch", 1,
697         INFO_LDAPSEARCH_ARG_DESCRIPTION_SEPARATE_OUTPUT_FILES.get());
698    separateOutputFilePerSearch.addLongIdentifier(
699         "separate-output-file-per-search", true);
700    separateOutputFilePerSearch.setArgumentGroupName(
701         INFO_LDAPSEARCH_ARG_GROUP_DATA.get());
702    parser.addArgument(separateOutputFilePerSearch);
703
704    teeResultsToStandardOut = new BooleanArgument(null,
705         "teeResultsToStandardOut", 1,
706         INFO_LDAPSEARCH_ARG_DESCRIPTION_TEE.get("outputFile"));
707    teeResultsToStandardOut.addLongIdentifier(
708         "tee-results-to-standard-out", true);
709    teeResultsToStandardOut.setArgumentGroupName(
710         INFO_LDAPSEARCH_ARG_GROUP_DATA.get());
711    parser.addArgument(teeResultsToStandardOut);
712
713    final Set<String> outputFormatAllowedValues =
714         StaticUtils.setOf("ldif", "json", "csv", "tab-delimited");
715    outputFormat = new StringArgument(null, "outputFormat", false, 1,
716         "{ldif|json|csv|tab-delimited}",
717         INFO_LDAPSEARCH_ARG_DESCRIPTION_OUTPUT_FORMAT.get(
718              requestedAttribute.getIdentifierString(),
719              ldapURLFile.getIdentifierString()),
720         outputFormatAllowedValues, "ldif");
721    outputFormat.addLongIdentifier("output-format", true);
722    outputFormat.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_DATA.get());
723    parser.addArgument(outputFormat);
724
725    terse = new BooleanArgument(null, "terse", 1,
726         INFO_LDAPSEARCH_ARG_DESCRIPTION_TERSE.get());
727    terse.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_DATA.get());
728    parser.addArgument(terse);
729
730    verbose = new BooleanArgument('v', "verbose", 1,
731         INFO_LDAPSEARCH_ARG_DESCRIPTION_VERBOSE.get());
732    verbose.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_DATA.get());
733    parser.addArgument(verbose);
734
735    bindControl = new ControlArgument(null, "bindControl", false, 0, null,
736         INFO_LDAPSEARCH_ARG_DESCRIPTION_BIND_CONTROL.get());
737    bindControl.addLongIdentifier("bind-control", true);
738    bindControl.setArgumentGroupName(
739         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
740    parser.addArgument(bindControl);
741
742    searchControl = new ControlArgument('J', "control", false, 0, null,
743         INFO_LDAPSEARCH_ARG_DESCRIPTION_SEARCH_CONTROL.get());
744    searchControl.addLongIdentifier("searchControl", true);
745    searchControl.addLongIdentifier("search-control", true);
746    searchControl.setArgumentGroupName(
747         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
748    parser.addArgument(searchControl);
749
750    authorizationIdentity = new BooleanArgument('E', "authorizationIdentity",
751         1, INFO_LDAPSEARCH_ARG_DESCRIPTION_AUTHZ_IDENTITY.get());
752    authorizationIdentity.addLongIdentifier("reportAuthzID", true);
753    authorizationIdentity.addLongIdentifier("authorization-identity", true);
754    authorizationIdentity.addLongIdentifier("report-authzid", true);
755    authorizationIdentity.setArgumentGroupName(
756         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
757    parser.addArgument(authorizationIdentity);
758
759    assertionFilter = new FilterArgument(null, "assertionFilter", false, 1,
760         INFO_PLACEHOLDER_FILTER.get(),
761         INFO_LDAPSEARCH_ARG_DESCRIPTION_ASSERTION_FILTER.get());
762    assertionFilter.addLongIdentifier("assertion-filter", true);
763    assertionFilter.setArgumentGroupName(
764         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
765    parser.addArgument(assertionFilter);
766
767    getAuthorizationEntryAttribute = new StringArgument(null,
768         "getAuthorizationEntryAttribute", false, 0,
769         INFO_PLACEHOLDER_ATTR.get(),
770         INFO_LDAPSEARCH_ARG_DESCRIPTION_GET_AUTHZ_ENTRY_ATTR.get());
771    getAuthorizationEntryAttribute.addLongIdentifier(
772         "get-authorization-entry-attribute", true);
773    getAuthorizationEntryAttribute.setArgumentGroupName(
774         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
775    parser.addArgument(getAuthorizationEntryAttribute);
776
777    getBackendSetID = new BooleanArgument(null, "getBackendSetID",
778         1, INFO_LDAPSEARCH_ARG_DESCRIPTION_GET_BACKEND_SET_ID.get());
779    getBackendSetID.addLongIdentifier("get-backend-set-id", true);
780    getBackendSetID.setArgumentGroupName(
781         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
782    parser.addArgument(getBackendSetID);
783
784    getServerID = new BooleanArgument(null, "getServerID",
785         1, INFO_LDAPSEARCH_ARG_DESCRIPTION_GET_SERVER_ID.get());
786    getServerID.addLongIdentifier("get-server-id", true);
787    getServerID.setArgumentGroupName(
788         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
789    parser.addArgument(getServerID);
790
791    getUserResourceLimits = new BooleanArgument(null, "getUserResourceLimits",
792         1, INFO_LDAPSEARCH_ARG_DESCRIPTION_GET_USER_RESOURCE_LIMITS.get());
793    getUserResourceLimits.addLongIdentifier("get-user-resource-limits", true);
794    getUserResourceLimits.setArgumentGroupName(
795         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
796    parser.addArgument(getUserResourceLimits);
797
798    accountUsable = new BooleanArgument(null, "accountUsable", 1,
799         INFO_LDAPSEARCH_ARG_DESCRIPTION_ACCOUNT_USABLE.get());
800    accountUsable.addLongIdentifier("account-usable", true);
801    accountUsable.setArgumentGroupName(
802         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
803    parser.addArgument(accountUsable);
804
805    excludeBranch = new DNArgument(null, "excludeBranch", false, 0, null,
806         INFO_LDAPSEARCH_ARG_DESCRIPTION_EXCLUDE_BRANCH.get());
807    excludeBranch.addLongIdentifier("exclude-branch", true);
808    excludeBranch.setArgumentGroupName(
809         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
810    parser.addArgument(excludeBranch);
811
812    getEffectiveRightsAuthzID = new StringArgument('g',
813         "getEffectiveRightsAuthzID", false, 1,
814         INFO_PLACEHOLDER_AUTHZID.get(),
815         INFO_LDAPSEARCH_ARG_DESCRIPTION_GET_EFFECTIVE_RIGHTS_AUTHZID.get(
816              "getEffectiveRightsAttribute"));
817    getEffectiveRightsAuthzID.addLongIdentifier(
818         "get-effective-rights-authzid", true);
819    getEffectiveRightsAuthzID.setArgumentGroupName(
820         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
821    parser.addArgument(getEffectiveRightsAuthzID);
822
823    getEffectiveRightsAttribute = new StringArgument('e',
824         "getEffectiveRightsAttribute", false, 0,
825         INFO_PLACEHOLDER_ATTR.get(),
826         INFO_LDAPSEARCH_ARG_DESCRIPTION_GET_EFFECTIVE_RIGHTS_ATTR.get());
827    getEffectiveRightsAttribute.addLongIdentifier(
828         "get-effective-rights-attribute", true);
829    getEffectiveRightsAttribute.setArgumentGroupName(
830         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
831    parser.addArgument(getEffectiveRightsAttribute);
832
833    includeReplicationConflictEntries = new BooleanArgument(null,
834         "includeReplicationConflictEntries", 1,
835         INFO_LDAPSEARCH_ARG_DESCRIPTION_INCLUDE_REPL_CONFLICTS.get());
836    includeReplicationConflictEntries.addLongIdentifier(
837         "include-replication-conflict-entries", true);
838    includeReplicationConflictEntries.setArgumentGroupName(
839         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
840    parser.addArgument(includeReplicationConflictEntries);
841
842    final Set<String> softDeleteAllowedValues = StaticUtils.setOf(
843         "with-non-deleted-entries", "without-non-deleted-entries",
844         "deleted-entries-in-undeleted-form");
845    includeSoftDeletedEntries = new StringArgument(null,
846         "includeSoftDeletedEntries", false, 1,
847         "{with-non-deleted-entries|without-non-deleted-entries|" +
848              "deleted-entries-in-undeleted-form}",
849         INFO_LDAPSEARCH_ARG_DESCRIPTION_INCLUDE_SOFT_DELETED.get(),
850         softDeleteAllowedValues);
851    includeSoftDeletedEntries.addLongIdentifier(
852         "include-soft-deleted-entries", true);
853    includeSoftDeletedEntries.setArgumentGroupName(
854         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
855    parser.addArgument(includeSoftDeletedEntries);
856
857    includeSubentries = new BooleanArgument(null, "includeSubentries", 1,
858         INFO_LDAPSEARCH_ARG_DESCRIPTION_INCLUDE_SUBENTRIES.get());
859    includeSubentries.addLongIdentifier("includeLDAPSubentries", true);
860    includeSubentries.addLongIdentifier("include-subentries", true);
861    includeSubentries.addLongIdentifier("include-ldap-subentries", true);
862    includeSubentries.setArgumentGroupName(
863         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
864    parser.addArgument(includeSubentries);
865
866    joinRule = new StringArgument(null, "joinRule", false, 1,
867         "{dn:sourceAttr|reverse-dn:targetAttr|equals:sourceAttr:targetAttr|" +
868              "contains:sourceAttr:targetAttr }",
869         INFO_LDAPSEARCH_ARG_DESCRIPTION_JOIN_RULE.get());
870    joinRule.addLongIdentifier("join-rule", true);
871    joinRule.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
872    parser.addArgument(joinRule);
873
874    joinBaseDN = new StringArgument(null, "joinBaseDN", false, 1,
875         "{search-base|source-entry-dn|{dn}}",
876         INFO_LDAPSEARCH_ARG_DESCRIPTION_JOIN_BASE_DN.get());
877    joinBaseDN.addLongIdentifier("join-base-dn", true);
878    joinBaseDN.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
879    parser.addArgument(joinBaseDN);
880
881    joinScope = new ScopeArgument(null, "joinScope", false, null,
882         INFO_LDAPSEARCH_ARG_DESCRIPTION_JOIN_SCOPE.get());
883    joinScope.addLongIdentifier("join-scope", true);
884    joinScope.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
885    parser.addArgument(joinScope);
886
887    joinSizeLimit = new IntegerArgument(null, "joinSizeLimit", false, 1,
888         INFO_PLACEHOLDER_NUM.get(),
889         INFO_LDAPSEARCH_ARG_DESCRIPTION_JOIN_SIZE_LIMIT.get(), 0,
890         Integer.MAX_VALUE);
891    joinSizeLimit.addLongIdentifier("join-size-limit", true);
892    joinSizeLimit.setArgumentGroupName(
893         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
894    parser.addArgument(joinSizeLimit);
895
896    joinFilter = new FilterArgument(null, "joinFilter", false, 1, null,
897         INFO_LDAPSEARCH_ARG_DESCRIPTION_JOIN_FILTER.get());
898    joinFilter.addLongIdentifier("join-filter", true);
899    joinFilter.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
900    parser.addArgument(joinFilter);
901
902    joinRequestedAttribute = new StringArgument(null, "joinRequestedAttribute",
903         false, 0, INFO_PLACEHOLDER_ATTR.get(),
904         INFO_LDAPSEARCH_ARG_DESCRIPTION_JOIN_ATTR.get());
905    joinRequestedAttribute.addLongIdentifier("join-requested-attribute", true);
906    joinRequestedAttribute.setArgumentGroupName(
907         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
908    parser.addArgument(joinRequestedAttribute);
909
910    joinRequireMatch = new BooleanArgument(null, "joinRequireMatch", 1,
911         INFO_LDAPSEARCH_ARG_DESCRIPTION_JOIN_REQUIRE_MATCH.get());
912    joinRequireMatch.addLongIdentifier("join-require-match", true);
913    joinRequireMatch.setArgumentGroupName(
914         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
915    parser.addArgument(joinRequireMatch);
916
917    manageDsaIT = new BooleanArgument(null, "manageDsaIT", 1,
918         INFO_LDAPSEARCH_ARG_DESCRIPTION_MANAGE_DSA_IT.get());
919    manageDsaIT.addLongIdentifier("manage-dsa-it", true);
920    manageDsaIT.setArgumentGroupName(
921         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
922    parser.addArgument(manageDsaIT);
923
924    matchedValuesFilter = new FilterArgument(null, "matchedValuesFilter",
925         false, 0, INFO_PLACEHOLDER_FILTER.get(),
926         INFO_LDAPSEARCH_ARG_DESCRIPTION_MATCHED_VALUES_FILTER.get());
927    matchedValuesFilter.addLongIdentifier("matched-values-filter", true);
928    matchedValuesFilter.setArgumentGroupName(
929         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
930    parser.addArgument(matchedValuesFilter);
931
932    matchingEntryCountControl = new StringArgument(null,
933         "matchingEntryCountControl", false, 1,
934         "{examineCount=NNN[:alwaysExamine][:allowUnindexed]" +
935              "[:skipResolvingExplodedIndexes]" +
936              "[:fastShortCircuitThreshold=NNN]" +
937              "[:slowShortCircuitThreshold=NNN][:debug]}",
938         INFO_LDAPSEARCH_ARG_DESCRIPTION_MATCHING_ENTRY_COUNT_CONTROL.get());
939    matchingEntryCountControl.addLongIdentifier("matchingEntryCount", true);
940    matchingEntryCountControl.addLongIdentifier(
941         "matching-entry-count-control", true);
942    matchingEntryCountControl.addLongIdentifier("matching-entry-count", true);
943    matchingEntryCountControl.setArgumentGroupName(
944         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
945    parser.addArgument(matchingEntryCountControl);
946
947    operationPurpose = new StringArgument(null, "operationPurpose", false, 1,
948         INFO_PLACEHOLDER_PURPOSE.get(),
949         INFO_LDAPSEARCH_ARG_DESCRIPTION_OPERATION_PURPOSE.get());
950    operationPurpose.addLongIdentifier("operation-purpose", true);
951    operationPurpose.setArgumentGroupName(
952         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
953    parser.addArgument(operationPurpose);
954
955    overrideSearchLimit = new StringArgument(null, "overrideSearchLimit",
956         false, 0, INFO_LDAPSEARCH_NAME_VALUE_PLACEHOLDER.get(),
957         INFO_LDAPSEARCH_ARG_DESCRIPTION_OVERRIDE_SEARCH_LIMIT.get());
958    overrideSearchLimit.addLongIdentifier("overrideSearchLimits", true);
959    overrideSearchLimit.addLongIdentifier("override-search-limit", true);
960    overrideSearchLimit.addLongIdentifier("override-search-limits", true);
961    overrideSearchLimit.setArgumentGroupName(
962         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
963    parser.addArgument(overrideSearchLimit);
964
965    persistentSearch = new StringArgument('C', "persistentSearch", false, 1,
966         "ps[:changetype[:changesonly[:entrychgcontrols]]]",
967         INFO_LDAPSEARCH_ARG_DESCRIPTION_PERSISTENT_SEARCH.get());
968    persistentSearch.addLongIdentifier("persistent-search", true);
969    persistentSearch.setArgumentGroupName(
970         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
971    parser.addArgument(persistentSearch);
972
973    proxyAs = new StringArgument('Y', "proxyAs", false, 1,
974         INFO_PLACEHOLDER_AUTHZID.get(),
975         INFO_LDAPSEARCH_ARG_DESCRIPTION_PROXY_AS.get());
976    proxyAs.addLongIdentifier("proxy-as", true);
977    proxyAs.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
978    parser.addArgument(proxyAs);
979
980    proxyV1As = new DNArgument(null, "proxyV1As", false, 1, null,
981         INFO_LDAPSEARCH_ARG_DESCRIPTION_PROXY_V1_AS.get());
982    proxyV1As.addLongIdentifier("proxy-v1-as", true);
983    proxyV1As.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
984    parser.addArgument(proxyV1As);
985
986    routeToBackendSet = new StringArgument(null, "routeToBackendSet",
987         false, 0,
988         INFO_LDAPSEARCH_ARG_PLACEHOLDER_ROUTE_TO_BACKEND_SET.get(),
989         INFO_LDAPSEARCH_ARG_DESCRIPTION_ROUTE_TO_BACKEND_SET.get());
990    routeToBackendSet.addLongIdentifier("route-to-backend-set", true);
991    routeToBackendSet.setArgumentGroupName(
992         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
993    parser.addArgument(routeToBackendSet);
994
995    routeToServer = new StringArgument(null, "routeToServer", false, 1,
996         INFO_LDAPSEARCH_ARG_PLACEHOLDER_ROUTE_TO_SERVER.get(),
997         INFO_LDAPSEARCH_ARG_DESCRIPTION_ROUTE_TO_SERVER.get());
998    routeToServer.addLongIdentifier("route-to-server", true);
999    routeToServer.setArgumentGroupName(
1000         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
1001    parser.addArgument(routeToServer);
1002
1003    final Set<String> suppressOperationalAttributeUpdatesAllowedValues =
1004         StaticUtils.setOf("last-access-time", "last-login-time",
1005              "last-login-ip", "lastmod");
1006    suppressOperationalAttributeUpdates = new StringArgument(null,
1007         "suppressOperationalAttributeUpdates", false, -1,
1008         INFO_PLACEHOLDER_ATTR.get(),
1009         INFO_LDAPSEARCH_ARG_DESCRIPTION_SUPPRESS_OP_ATTR_UPDATES.get(),
1010         suppressOperationalAttributeUpdatesAllowedValues);
1011    suppressOperationalAttributeUpdates.addLongIdentifier(
1012         "suppress-operational-attribute-updates", true);
1013    suppressOperationalAttributeUpdates.setArgumentGroupName(
1014         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
1015    parser.addArgument(suppressOperationalAttributeUpdates);
1016
1017    usePasswordPolicyControl = new BooleanArgument(null,
1018         "usePasswordPolicyControl", 1,
1019         INFO_LDAPSEARCH_ARG_DESCRIPTION_PASSWORD_POLICY.get());
1020    usePasswordPolicyControl.addLongIdentifier("use-password-policy-control",
1021         true);
1022    usePasswordPolicyControl.setArgumentGroupName(
1023         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
1024    parser.addArgument(usePasswordPolicyControl);
1025
1026    realAttributesOnly = new BooleanArgument(null, "realAttributesOnly", 1,
1027         INFO_LDAPSEARCH_ARG_DESCRIPTION_REAL_ATTRS_ONLY.get());
1028    realAttributesOnly.addLongIdentifier("real-attributes-only", true);
1029    realAttributesOnly.setArgumentGroupName(
1030         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
1031    parser.addArgument(realAttributesOnly);
1032
1033    sortOrder = new StringArgument('S', "sortOrder", false, 1, null,
1034         INFO_LDAPSEARCH_ARG_DESCRIPTION_SORT_ORDER.get());
1035    sortOrder.addLongIdentifier("sort-order", true);
1036    sortOrder.setArgumentGroupName(INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
1037    parser.addArgument(sortOrder);
1038
1039    simplePageSize = new IntegerArgument(null, "simplePageSize", false, 1,
1040         null, INFO_LDAPSEARCH_ARG_DESCRIPTION_PAGE_SIZE.get(), 1,
1041         Integer.MAX_VALUE);
1042    simplePageSize.addLongIdentifier("simple-page-size", true);
1043    simplePageSize.setArgumentGroupName(
1044         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
1045    parser.addArgument(simplePageSize);
1046
1047    virtualAttributesOnly = new BooleanArgument(null,
1048         "virtualAttributesOnly", 1,
1049         INFO_LDAPSEARCH_ARG_DESCRIPTION_VIRTUAL_ATTRS_ONLY.get());
1050    virtualAttributesOnly.addLongIdentifier("virtual-attributes-only", true);
1051    virtualAttributesOnly.setArgumentGroupName(
1052         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
1053    parser.addArgument(virtualAttributesOnly);
1054
1055    virtualListView = new StringArgument('G', "virtualListView", false, 1,
1056         "{before:after:index:count | before:after:value}",
1057         INFO_LDAPSEARCH_ARG_DESCRIPTION_VLV.get("sortOrder"));
1058    virtualListView.addLongIdentifier("vlv", true);
1059    virtualListView.addLongIdentifier("virtual-list-view", true);
1060    virtualListView.setArgumentGroupName(
1061         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
1062    parser.addArgument(virtualListView);
1063
1064    rejectUnindexedSearch = new BooleanArgument(null, "rejectUnindexedSearch",
1065         1, INFO_LDAPSEARCH_ARG_DESCRIPTION_REJECT_UNINDEXED_SEARCH.get());
1066    rejectUnindexedSearch.addLongIdentifier("rejectUnindexedSearches", true);
1067    rejectUnindexedSearch.addLongIdentifier("rejectUnindexed", true);
1068    rejectUnindexedSearch.addLongIdentifier("rejectIfUnindexed", true);
1069    rejectUnindexedSearch.addLongIdentifier("reject-unindexed-search", true);
1070    rejectUnindexedSearch.addLongIdentifier("reject-unindexed-searches", true);
1071    rejectUnindexedSearch.addLongIdentifier("reject-unindexed", true);
1072    rejectUnindexedSearch.addLongIdentifier("reject-if-unindexed", true);
1073    rejectUnindexedSearch.setArgumentGroupName(
1074         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
1075    parser.addArgument(rejectUnindexedSearch);
1076
1077    permitUnindexedSearch = new BooleanArgument(null, "permitUnindexedSearch",
1078         1, INFO_LDAPSEARCH_ARG_DESCRIPTION_PERMIT_UNINDEXED_SEARCH.get());
1079    permitUnindexedSearch.addLongIdentifier("permitUnindexedSearches", true);
1080    permitUnindexedSearch.addLongIdentifier("permitUnindexed", true);
1081    permitUnindexedSearch.addLongIdentifier("permitIfUnindexed", true);
1082    permitUnindexedSearch.addLongIdentifier("permit-unindexed-search", true);
1083    permitUnindexedSearch.addLongIdentifier("permit-unindexed-searches", true);
1084    permitUnindexedSearch.addLongIdentifier("permit-unindexed", true);
1085    permitUnindexedSearch.addLongIdentifier("permit-if-unindexed", true);
1086    permitUnindexedSearch.setArgumentGroupName(
1087         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
1088    parser.addArgument(permitUnindexedSearch);
1089
1090    excludeAttribute = new StringArgument(null, "excludeAttribute", false, 0,
1091         INFO_PLACEHOLDER_ATTR.get(),
1092         INFO_LDAPSEARCH_ARG_DESCRIPTION_EXCLUDE_ATTRIBUTE.get());
1093    excludeAttribute.addLongIdentifier("exclude-attribute", true);
1094    excludeAttribute.setArgumentGroupName(
1095         INFO_LDAPSEARCH_ARG_GROUP_TRANSFORMATIONS.get());
1096    parser.addArgument(excludeAttribute);
1097
1098    redactAttribute = new StringArgument(null, "redactAttribute", false, 0,
1099         INFO_PLACEHOLDER_ATTR.get(),
1100         INFO_LDAPSEARCH_ARG_DESCRIPTION_REDACT_ATTRIBUTE.get());
1101    redactAttribute.addLongIdentifier("redact-attribute", true);
1102    redactAttribute.setArgumentGroupName(
1103         INFO_LDAPSEARCH_ARG_GROUP_TRANSFORMATIONS.get());
1104    parser.addArgument(redactAttribute);
1105
1106    hideRedactedValueCount = new BooleanArgument(null, "hideRedactedValueCount",
1107         1, INFO_LDAPSEARCH_ARG_DESCRIPTION_HIDE_REDACTED_VALUE_COUNT.get());
1108    hideRedactedValueCount.addLongIdentifier("hide-redacted-value-count", true);
1109    hideRedactedValueCount.setArgumentGroupName(
1110         INFO_LDAPSEARCH_ARG_GROUP_TRANSFORMATIONS.get());
1111    parser.addArgument(hideRedactedValueCount);
1112
1113    scrambleAttribute = new StringArgument(null, "scrambleAttribute", false, 0,
1114         INFO_PLACEHOLDER_ATTR.get(),
1115         INFO_LDAPSEARCH_ARG_DESCRIPTION_SCRAMBLE_ATTRIBUTE.get());
1116    scrambleAttribute.addLongIdentifier("scramble-attribute", true);
1117    scrambleAttribute.setArgumentGroupName(
1118         INFO_LDAPSEARCH_ARG_GROUP_TRANSFORMATIONS.get());
1119    parser.addArgument(scrambleAttribute);
1120
1121    scrambleJSONField = new StringArgument(null, "scrambleJSONField", false, 0,
1122         INFO_PLACEHOLDER_FIELD_NAME.get(),
1123         INFO_LDAPSEARCH_ARG_DESCRIPTION_SCRAMBLE_JSON_FIELD.get());
1124    scrambleJSONField.addLongIdentifier("scramble-json-field", true);
1125    scrambleJSONField.setArgumentGroupName(
1126         INFO_LDAPSEARCH_ARG_GROUP_TRANSFORMATIONS.get());
1127    parser.addArgument(scrambleJSONField);
1128
1129    scrambleRandomSeed = new IntegerArgument(null, "scrambleRandomSeed", false,
1130         1, null, INFO_LDAPSEARCH_ARG_DESCRIPTION_SCRAMBLE_RANDOM_SEED.get());
1131    scrambleRandomSeed.addLongIdentifier("scramble-random-seed", true);
1132    scrambleRandomSeed.setArgumentGroupName(
1133         INFO_LDAPSEARCH_ARG_GROUP_TRANSFORMATIONS.get());
1134    parser.addArgument(scrambleRandomSeed);
1135
1136    renameAttributeFrom = new StringArgument(null, "renameAttributeFrom", false,
1137         0, INFO_PLACEHOLDER_ATTR.get(),
1138         INFO_LDAPSEARCH_ARG_DESCRIPTION_RENAME_ATTRIBUTE_FROM.get());
1139    renameAttributeFrom.addLongIdentifier("rename-attribute-from", true);
1140    renameAttributeFrom.setArgumentGroupName(
1141         INFO_LDAPSEARCH_ARG_GROUP_TRANSFORMATIONS.get());
1142    parser.addArgument(renameAttributeFrom);
1143
1144    renameAttributeTo = new StringArgument(null, "renameAttributeTo", false,
1145         0, INFO_PLACEHOLDER_ATTR.get(),
1146         INFO_LDAPSEARCH_ARG_DESCRIPTION_RENAME_ATTRIBUTE_TO.get());
1147    renameAttributeTo.addLongIdentifier("rename-attribute-to", true);
1148    renameAttributeTo.setArgumentGroupName(
1149         INFO_LDAPSEARCH_ARG_GROUP_TRANSFORMATIONS.get());
1150    parser.addArgument(renameAttributeTo);
1151
1152    moveSubtreeFrom = new DNArgument(null, "moveSubtreeFrom", false, 0,
1153         INFO_PLACEHOLDER_ATTR.get(),
1154         INFO_LDAPSEARCH_ARG_DESCRIPTION_MOVE_SUBTREE_FROM.get());
1155    moveSubtreeFrom.addLongIdentifier("move-subtree-from", true);
1156    moveSubtreeFrom.setArgumentGroupName(
1157         INFO_LDAPSEARCH_ARG_GROUP_TRANSFORMATIONS.get());
1158    parser.addArgument(moveSubtreeFrom);
1159
1160    moveSubtreeTo = new DNArgument(null, "moveSubtreeTo", false, 0,
1161         INFO_PLACEHOLDER_ATTR.get(),
1162         INFO_LDAPSEARCH_ARG_DESCRIPTION_MOVE_SUBTREE_TO.get());
1163    moveSubtreeTo.addLongIdentifier("move-subtree-to", true);
1164    moveSubtreeTo.setArgumentGroupName(
1165         INFO_LDAPSEARCH_ARG_GROUP_TRANSFORMATIONS.get());
1166    parser.addArgument(moveSubtreeTo);
1167
1168
1169    // The "--scriptFriendly" argument is provided for compatibility with legacy
1170    // ldapsearch tools, but is not actually used by this tool.
1171    final BooleanArgument scriptFriendly = new BooleanArgument(null,
1172         "scriptFriendly", 1,
1173         INFO_LDAPSEARCH_ARG_DESCRIPTION_SCRIPT_FRIENDLY.get());
1174    scriptFriendly.addLongIdentifier("script-friendly", true);
1175    scriptFriendly.setHidden(true);
1176    parser.addArgument(scriptFriendly);
1177
1178
1179    // The "-V" / "--ldapVersion" argument is provided for compatibility with
1180    // legacy ldapsearch tools, but is not actually used by this tool.
1181    final IntegerArgument ldapVersion = new IntegerArgument('V', "ldapVersion",
1182         false, 1, null, INFO_LDAPSEARCH_ARG_DESCRIPTION_LDAP_VERSION.get());
1183    ldapVersion.addLongIdentifier("ldap-version", true);
1184    ldapVersion.setHidden(true);
1185    parser.addArgument(ldapVersion);
1186
1187
1188    // The baseDN and ldapURLFile arguments can't be used together.
1189    parser.addExclusiveArgumentSet(baseDN, ldapURLFile);
1190
1191    // The scope and ldapURLFile arguments can't be used together.
1192    parser.addExclusiveArgumentSet(scope, ldapURLFile);
1193
1194    // The requestedAttribute and ldapURLFile arguments can't be used together.
1195    parser.addExclusiveArgumentSet(requestedAttribute, ldapURLFile);
1196
1197    // The filter and ldapURLFile arguments can't be used together.
1198    parser.addExclusiveArgumentSet(filter, ldapURLFile);
1199
1200    // The filterFile and ldapURLFile arguments can't be used together.
1201    parser.addExclusiveArgumentSet(filterFile, ldapURLFile);
1202
1203    // The followReferrals and manageDsaIT arguments can't be used together.
1204    parser.addExclusiveArgumentSet(followReferrals, manageDsaIT);
1205
1206    // The persistent search argument can't be used with either the filterFile
1207    // or ldapURLFile arguments.
1208    parser.addExclusiveArgumentSet(persistentSearch, filterFile);
1209    parser.addExclusiveArgumentSet(persistentSearch, ldapURLFile);
1210
1211    // The realAttributesOnly and virtualAttributesOnly arguments can't be used
1212    // together.
1213    parser.addExclusiveArgumentSet(realAttributesOnly, virtualAttributesOnly);
1214
1215    // The simplePageSize and virtualListView arguments can't be used together.
1216    parser.addExclusiveArgumentSet(simplePageSize, virtualListView);
1217
1218    // The terse and verbose arguments can't be used together.
1219    parser.addExclusiveArgumentSet(terse, verbose);
1220
1221    // The getEffectiveRightsAttribute argument requires the
1222    // getEffectiveRightsAuthzID argument.
1223    parser.addDependentArgumentSet(getEffectiveRightsAttribute,
1224         getEffectiveRightsAuthzID);
1225
1226    // The virtualListView argument requires the sortOrder argument.
1227    parser.addDependentArgumentSet(virtualListView, sortOrder);
1228
1229    // The rejectUnindexedSearch and permitUnindexedSearch arguments can't be
1230    // used together.
1231    parser.addExclusiveArgumentSet(rejectUnindexedSearch,
1232         permitUnindexedSearch);
1233
1234    // The separateOutputFilePerSearch argument requires the outputFile
1235    // argument.  It also requires either the filter, filterFile or ldapURLFile
1236    // argument.
1237    parser.addDependentArgumentSet(separateOutputFilePerSearch, outputFile);
1238    parser.addDependentArgumentSet(separateOutputFilePerSearch, filter,
1239         filterFile, ldapURLFile);
1240
1241    // The teeResultsToStandardOut argument requires the outputFile argument.
1242    parser.addDependentArgumentSet(teeResultsToStandardOut, outputFile);
1243
1244    // The wrapColumn and dontWrap arguments must not be used together.
1245    parser.addExclusiveArgumentSet(wrapColumn, dontWrap);
1246
1247    // All arguments that specifically pertain to join processing can only be
1248    // used if the joinRule argument is provided.
1249    parser.addDependentArgumentSet(joinBaseDN, joinRule);
1250    parser.addDependentArgumentSet(joinScope, joinRule);
1251    parser.addDependentArgumentSet(joinSizeLimit, joinRule);
1252    parser.addDependentArgumentSet(joinFilter, joinRule);
1253    parser.addDependentArgumentSet(joinRequestedAttribute, joinRule);
1254    parser.addDependentArgumentSet(joinRequireMatch, joinRule);
1255
1256    // The countEntries argument must not be used in conjunction with the
1257    // filter, filterFile, LDAPURLFile, or persistentSearch arguments.
1258    parser.addExclusiveArgumentSet(countEntries, filter);
1259    parser.addExclusiveArgumentSet(countEntries, filterFile);
1260    parser.addExclusiveArgumentSet(countEntries, ldapURLFile);
1261    parser.addExclusiveArgumentSet(countEntries, persistentSearch);
1262
1263
1264    // The hideRedactedValueCount argument requires the redactAttribute
1265    // argument.
1266    parser.addDependentArgumentSet(hideRedactedValueCount, redactAttribute);
1267
1268    // The scrambleJSONField and scrambleRandomSeed arguments require the
1269    // scrambleAttribute argument.
1270    parser.addDependentArgumentSet(scrambleJSONField, scrambleAttribute);
1271    parser.addDependentArgumentSet(scrambleRandomSeed, scrambleAttribute);
1272
1273    // The renameAttributeFrom and renameAttributeTo arguments must be provided
1274    // together.
1275    parser.addDependentArgumentSet(renameAttributeFrom, renameAttributeTo);
1276    parser.addDependentArgumentSet(renameAttributeTo, renameAttributeFrom);
1277
1278    // The moveSubtreeFrom and moveSubtreeTo arguments must be provided
1279    // together.
1280    parser.addDependentArgumentSet(moveSubtreeFrom, moveSubtreeTo);
1281    parser.addDependentArgumentSet(moveSubtreeTo, moveSubtreeFrom);
1282
1283
1284    // The compressOutput argument can only be used if an output file is
1285    // specified and results aren't going to be teed.
1286    parser.addDependentArgumentSet(compressOutput, outputFile);
1287    parser.addExclusiveArgumentSet(compressOutput, teeResultsToStandardOut);
1288
1289
1290    // The encryptOutput argument can only be used if an output file is
1291    // specified and results aren't going to be teed.
1292    parser.addDependentArgumentSet(encryptOutput, outputFile);
1293    parser.addExclusiveArgumentSet(encryptOutput, teeResultsToStandardOut);
1294
1295
1296    // The encryptionPassphraseFile argument can only be used if the
1297    // encryptOutput argument is also provided.
1298    parser.addDependentArgumentSet(encryptionPassphraseFile, encryptOutput);
1299  }
1300
1301
1302
1303  /**
1304   * {@inheritDoc}
1305   */
1306  @Override()
1307  protected List<Control> getBindControls()
1308  {
1309    final ArrayList<Control> bindControls = new ArrayList<>(10);
1310
1311    if (bindControl.isPresent())
1312    {
1313      bindControls.addAll(bindControl.getValues());
1314    }
1315
1316    if (authorizationIdentity.isPresent())
1317    {
1318      bindControls.add(new AuthorizationIdentityRequestControl(false));
1319    }
1320
1321    if (getAuthorizationEntryAttribute.isPresent())
1322    {
1323      bindControls.add(new GetAuthorizationEntryRequestControl(true, true,
1324           getAuthorizationEntryAttribute.getValues()));
1325    }
1326
1327    if (getUserResourceLimits.isPresent())
1328    {
1329      bindControls.add(new GetUserResourceLimitsRequestControl());
1330    }
1331
1332    if (usePasswordPolicyControl.isPresent())
1333    {
1334      bindControls.add(new PasswordPolicyRequestControl());
1335    }
1336
1337    if (suppressOperationalAttributeUpdates.isPresent())
1338    {
1339      final EnumSet<SuppressType> suppressTypes =
1340           EnumSet.noneOf(SuppressType.class);
1341      for (final String s : suppressOperationalAttributeUpdates.getValues())
1342      {
1343        if (s.equalsIgnoreCase("last-access-time"))
1344        {
1345          suppressTypes.add(SuppressType.LAST_ACCESS_TIME);
1346        }
1347        else if (s.equalsIgnoreCase("last-login-time"))
1348        {
1349          suppressTypes.add(SuppressType.LAST_LOGIN_TIME);
1350        }
1351        else if (s.equalsIgnoreCase("last-login-ip"))
1352        {
1353          suppressTypes.add(SuppressType.LAST_LOGIN_IP);
1354        }
1355      }
1356
1357      bindControls.add(new SuppressOperationalAttributeUpdateRequestControl(
1358           suppressTypes));
1359    }
1360
1361    return bindControls;
1362  }
1363
1364
1365
1366  /**
1367   * {@inheritDoc}
1368   */
1369  @Override()
1370  protected boolean supportsMultipleServers()
1371  {
1372    // We will support providing information about multiple servers.  This tool
1373    // will not communicate with multiple servers concurrently, but it can
1374    // accept information about multiple servers in the event that multiple
1375    // searches are to be performed and a server goes down in the middle of
1376    // those searches.  In this case, we can resume processing on a
1377    // newly-created connection, possibly to a different server.
1378    return true;
1379  }
1380
1381
1382
1383  /**
1384   * {@inheritDoc}
1385   */
1386  @Override()
1387  public void doExtendedNonLDAPArgumentValidation()
1388         throws ArgumentException
1389  {
1390    // If wrapColumn was provided, then use its value.  Otherwise, if dontWrap
1391    // was provided, then use that.
1392    if (wrapColumn.isPresent())
1393    {
1394      final int wc = wrapColumn.getValue();
1395      if (wc <= 0)
1396      {
1397        WRAP_COLUMN = Integer.MAX_VALUE;
1398      }
1399      else
1400      {
1401        WRAP_COLUMN = wc;
1402      }
1403    }
1404    else if (dontWrap.isPresent())
1405    {
1406      WRAP_COLUMN = Integer.MAX_VALUE;
1407    }
1408
1409
1410    // If the ldapURLFile argument was provided, then there must not be any
1411    // trailing arguments.
1412    final List<String> trailingArgs = parser.getTrailingArguments();
1413    if (ldapURLFile.isPresent())
1414    {
1415      if (! trailingArgs.isEmpty())
1416      {
1417        throw new ArgumentException(
1418             ERR_LDAPSEARCH_TRAILING_ARGS_WITH_URL_FILE.get(
1419                  ldapURLFile.getIdentifierString()));
1420      }
1421    }
1422
1423
1424    // If the filter or filterFile argument was provided, then there may
1425    // optionally be trailing arguments, but the first trailing argument must
1426    // not be a filter.
1427    if (filter.isPresent() || filterFile.isPresent())
1428    {
1429      if (! trailingArgs.isEmpty())
1430      {
1431        try
1432        {
1433          Filter.create(trailingArgs.get(0));
1434          throw new ArgumentException(
1435               ERR_LDAPSEARCH_TRAILING_FILTER_WITH_FILTER_FILE.get(
1436                    filterFile.getIdentifierString()));
1437        }
1438        catch (final LDAPException le)
1439        {
1440          // This is the normal condition.  Not even worth debugging the
1441          // exception.
1442        }
1443      }
1444    }
1445
1446
1447    // If none of the ldapURLFile, filter, or filterFile arguments was provided,
1448    // then there must be at least one trailing argument, and the first trailing
1449    // argument must be a valid search filter.
1450    if (! (ldapURLFile.isPresent() || filter.isPresent() ||
1451           filterFile.isPresent()))
1452    {
1453      if (trailingArgs.isEmpty())
1454      {
1455        throw new ArgumentException(ERR_LDAPSEARCH_NO_TRAILING_ARGS.get(
1456             filterFile.getIdentifierString(),
1457             ldapURLFile.getIdentifierString()));
1458      }
1459
1460      try
1461      {
1462        Filter.create(trailingArgs.get(0));
1463      }
1464      catch (final Exception e)
1465      {
1466        Debug.debugException(e);
1467        throw new ArgumentException(
1468             ERR_LDAPSEARCH_FIRST_TRAILING_ARG_NOT_FILTER.get(
1469                  trailingArgs.get(0)),
1470             e);
1471      }
1472    }
1473
1474
1475    // There should never be a case in which a trailing argument starts with a
1476    // dash, and it's probably an attempt to use a named argument but that was
1477    // inadvertently put after the filter.  Warn about the problem, but don't
1478    // fail.
1479    for (final String s : trailingArgs)
1480    {
1481      if (s.startsWith("-"))
1482      {
1483        commentToErr(WARN_LDAPSEARCH_TRAILING_ARG_STARTS_WITH_DASH.get(s));
1484        break;
1485      }
1486    }
1487
1488
1489    // If any matched values filters are specified, then validate them and
1490    // pre-create the matched values request control.
1491    if (matchedValuesFilter.isPresent())
1492    {
1493      final List<Filter> filterList = matchedValuesFilter.getValues();
1494      final MatchedValuesFilter[] matchedValuesFilters =
1495           new MatchedValuesFilter[filterList.size()];
1496      for (int i=0; i < matchedValuesFilters.length; i++)
1497      {
1498        try
1499        {
1500          matchedValuesFilters[i] =
1501               MatchedValuesFilter.create(filterList.get(i));
1502        }
1503        catch (final Exception e)
1504        {
1505          Debug.debugException(e);
1506          throw new ArgumentException(
1507               ERR_LDAPSEARCH_INVALID_MATCHED_VALUES_FILTER.get(
1508                    filterList.get(i).toString()),
1509               e);
1510        }
1511      }
1512
1513      matchedValuesRequestControl =
1514           new MatchedValuesRequestControl(true, matchedValuesFilters);
1515    }
1516
1517
1518    // If we should use the matching entry count request control, then validate
1519    // the argument value and pre-create the control.
1520    if (matchingEntryCountControl.isPresent())
1521    {
1522      boolean allowUnindexed               = false;
1523      boolean alwaysExamine                = false;
1524      boolean debug                        = false;
1525      boolean skipResolvingExplodedIndexes = false;
1526      Integer examineCount                 = null;
1527      Long    fastShortCircuitThreshold    = null;
1528      Long    slowShortCircuitThreshold    = null;
1529
1530      try
1531      {
1532        for (final String element :
1533             matchingEntryCountControl.getValue().toLowerCase().split(":"))
1534        {
1535          if (element.startsWith("examinecount="))
1536          {
1537            examineCount = Integer.parseInt(element.substring(13));
1538          }
1539          else if (element.equals("allowunindexed"))
1540          {
1541            allowUnindexed = true;
1542          }
1543          else if (element.equals("alwaysexamine"))
1544          {
1545            alwaysExamine = true;
1546          }
1547          else if (element.equals("skipresolvingexplodedindexes"))
1548          {
1549            skipResolvingExplodedIndexes = true;
1550          }
1551          else if (element.startsWith("fastshortcircuitthreshold="))
1552          {
1553            fastShortCircuitThreshold = Long.parseLong(element.substring(26));
1554          }
1555          else if (element.startsWith("slowshortcircuitthreshold="))
1556          {
1557            slowShortCircuitThreshold = Long.parseLong(element.substring(26));
1558          }
1559          else if (element.equals("debug"))
1560          {
1561            debug = true;
1562          }
1563          else
1564          {
1565            throw new ArgumentException(
1566                 ERR_LDAPSEARCH_MATCHING_ENTRY_COUNT_INVALID_VALUE.get(
1567                      matchingEntryCountControl.getIdentifierString()));
1568          }
1569        }
1570      }
1571      catch (final ArgumentException ae)
1572      {
1573        Debug.debugException(ae);
1574        throw ae;
1575      }
1576      catch (final Exception e)
1577      {
1578        Debug.debugException(e);
1579        throw new ArgumentException(
1580             ERR_LDAPSEARCH_MATCHING_ENTRY_COUNT_INVALID_VALUE.get(
1581                  matchingEntryCountControl.getIdentifierString()),
1582             e);
1583      }
1584
1585      if (examineCount == null)
1586      {
1587        throw new ArgumentException(
1588             ERR_LDAPSEARCH_MATCHING_ENTRY_COUNT_INVALID_VALUE.get(
1589                  matchingEntryCountControl.getIdentifierString()));
1590      }
1591
1592      matchingEntryCountRequestControl = new MatchingEntryCountRequestControl(
1593           true, examineCount, alwaysExamine, allowUnindexed,
1594           skipResolvingExplodedIndexes, fastShortCircuitThreshold,
1595           slowShortCircuitThreshold, debug);
1596    }
1597
1598
1599    // If we should include the override search limits request control, then
1600    // validate the provided values.
1601    if (overrideSearchLimit.isPresent())
1602    {
1603      final LinkedHashMap<String,String> properties =
1604           new LinkedHashMap<>(StaticUtils.computeMapCapacity(10));
1605      for (final String value : overrideSearchLimit.getValues())
1606      {
1607        final int equalPos = value.indexOf('=');
1608        if (equalPos < 0)
1609        {
1610          throw new ArgumentException(
1611               ERR_LDAPSEARCH_OVERRIDE_LIMIT_NO_EQUAL.get(
1612                    overrideSearchLimit.getIdentifierString()));
1613        }
1614        else if (equalPos == 0)
1615        {
1616          throw new ArgumentException(
1617               ERR_LDAPSEARCH_OVERRIDE_LIMIT_EMPTY_PROPERTY_NAME.get(
1618                    overrideSearchLimit.getIdentifierString()));
1619        }
1620
1621        final String propertyName = value.substring(0, equalPos);
1622        if (properties.containsKey(propertyName))
1623        {
1624          throw new ArgumentException(
1625               ERR_LDAPSEARCH_OVERRIDE_LIMIT_DUPLICATE_PROPERTY_NAME.get(
1626                    overrideSearchLimit.getIdentifierString(), propertyName));
1627        }
1628
1629        if (equalPos == (value.length() - 1))
1630        {
1631          throw new ArgumentException(
1632               ERR_LDAPSEARCH_OVERRIDE_LIMIT_EMPTY_PROPERTY_VALUE.get(
1633                    overrideSearchLimit.getIdentifierString(), propertyName));
1634        }
1635
1636        properties.put(propertyName, value.substring(equalPos+1));
1637      }
1638
1639      overrideSearchLimitsRequestControl =
1640           new OverrideSearchLimitsRequestControl(properties, false);
1641    }
1642
1643
1644    // If we should use the persistent search request control, then validate
1645    // the argument value and pre-create the control.
1646    if (persistentSearch.isPresent())
1647    {
1648      boolean changesOnly = true;
1649      boolean returnECs   = true;
1650      EnumSet<PersistentSearchChangeType> changeTypes =
1651           EnumSet.allOf(PersistentSearchChangeType.class);
1652      try
1653      {
1654        final String[] elements =
1655             persistentSearch.getValue().toLowerCase().split(":");
1656        if (elements.length == 0)
1657        {
1658          throw new ArgumentException(
1659               ERR_LDAPSEARCH_PERSISTENT_SEARCH_INVALID_VALUE.get(
1660                    persistentSearch.getIdentifierString()));
1661        }
1662
1663        final String header = StaticUtils.toLowerCase(elements[0]);
1664        if (! (header.equals("ps") || header.equals("persist") ||
1665             header.equals("persistent") || header.equals("psearch") ||
1666             header.equals("persistentsearch")))
1667        {
1668          throw new ArgumentException(
1669               ERR_LDAPSEARCH_PERSISTENT_SEARCH_INVALID_VALUE.get(
1670                    persistentSearch.getIdentifierString()));
1671        }
1672
1673        if (elements.length > 1)
1674        {
1675          final String ctString = StaticUtils.toLowerCase(elements[1]);
1676          if (ctString.equals("any"))
1677          {
1678            changeTypes = EnumSet.allOf(PersistentSearchChangeType.class);
1679          }
1680          else
1681          {
1682            changeTypes.clear();
1683            for (final String t : ctString.split(","))
1684            {
1685              if (t.equals("add"))
1686              {
1687                changeTypes.add(PersistentSearchChangeType.ADD);
1688              }
1689              else if (t.equals("del") || t.equals("delete"))
1690              {
1691                changeTypes.add(PersistentSearchChangeType.DELETE);
1692              }
1693              else if (t.equals("mod") || t.equals("modify"))
1694              {
1695                changeTypes.add(PersistentSearchChangeType.MODIFY);
1696              }
1697              else if (t.equals("moddn") || t.equals("modrdn") ||
1698                   t.equals("modifydn") || t.equals("modifyrdn"))
1699              {
1700                changeTypes.add(PersistentSearchChangeType.MODIFY_DN);
1701              }
1702              else
1703              {
1704                throw new ArgumentException(
1705                     ERR_LDAPSEARCH_PERSISTENT_SEARCH_INVALID_VALUE.get(
1706                          persistentSearch.getIdentifierString()));
1707              }
1708            }
1709          }
1710        }
1711
1712        if (elements.length > 2)
1713        {
1714          if (elements[2].equalsIgnoreCase("true") || elements[2].equals("1"))
1715          {
1716            changesOnly = true;
1717          }
1718          else if (elements[2].equalsIgnoreCase("false") ||
1719               elements[2].equals("0"))
1720          {
1721            changesOnly = false;
1722          }
1723          else
1724          {
1725            throw new ArgumentException(
1726                 ERR_LDAPSEARCH_PERSISTENT_SEARCH_INVALID_VALUE.get(
1727                      persistentSearch.getIdentifierString()));
1728          }
1729        }
1730
1731        if (elements.length > 3)
1732        {
1733          if (elements[3].equalsIgnoreCase("true") || elements[3].equals("1"))
1734          {
1735            returnECs = true;
1736          }
1737          else if (elements[3].equalsIgnoreCase("false") ||
1738               elements[3].equals("0"))
1739          {
1740            returnECs = false;
1741          }
1742          else
1743          {
1744            throw new ArgumentException(
1745                 ERR_LDAPSEARCH_PERSISTENT_SEARCH_INVALID_VALUE.get(
1746                      persistentSearch.getIdentifierString()));
1747          }
1748        }
1749      }
1750      catch (final ArgumentException ae)
1751      {
1752        Debug.debugException(ae);
1753        throw ae;
1754      }
1755      catch (final Exception e)
1756      {
1757        Debug.debugException(e);
1758        throw new ArgumentException(
1759             ERR_LDAPSEARCH_PERSISTENT_SEARCH_INVALID_VALUE.get(
1760                  persistentSearch.getIdentifierString()),
1761             e);
1762      }
1763
1764      persistentSearchRequestControl = new PersistentSearchRequestControl(
1765           changeTypes, changesOnly, returnECs, true);
1766    }
1767
1768
1769    // If we should use the server-side sort request control, then validate the
1770    // sort order and pre-create the control.
1771    if (sortOrder.isPresent())
1772    {
1773      final ArrayList<SortKey> sortKeyList = new ArrayList<>(5);
1774      final StringTokenizer tokenizer =
1775           new StringTokenizer(sortOrder.getValue(), ", ");
1776      while (tokenizer.hasMoreTokens())
1777      {
1778        final String token = tokenizer.nextToken();
1779
1780        final boolean ascending;
1781        String attributeName;
1782        if (token.startsWith("-"))
1783        {
1784          ascending = false;
1785          attributeName = token.substring(1);
1786        }
1787        else if (token.startsWith("+"))
1788        {
1789          ascending = true;
1790          attributeName = token.substring(1);
1791        }
1792        else
1793        {
1794          ascending = true;
1795          attributeName = token;
1796        }
1797
1798        final String matchingRuleID;
1799        final int colonPos = attributeName.indexOf(':');
1800        if (colonPos >= 0)
1801        {
1802          matchingRuleID = attributeName.substring(colonPos+1);
1803          attributeName = attributeName.substring(0, colonPos);
1804        }
1805        else
1806        {
1807          matchingRuleID = null;
1808        }
1809
1810        final StringBuilder invalidReason = new StringBuilder();
1811        if (! PersistUtils.isValidLDAPName(attributeName, false, invalidReason))
1812        {
1813          throw new ArgumentException(
1814               ERR_LDAPSEARCH_SORT_ORDER_INVALID_VALUE.get(
1815                    sortOrder.getIdentifierString()));
1816        }
1817
1818        sortKeyList.add(
1819             new SortKey(attributeName, matchingRuleID, (! ascending)));
1820      }
1821
1822      if (sortKeyList.isEmpty())
1823      {
1824        throw new ArgumentException(
1825             ERR_LDAPSEARCH_SORT_ORDER_INVALID_VALUE.get(
1826                  sortOrder.getIdentifierString()));
1827      }
1828
1829      final SortKey[] sortKeyArray = new SortKey[sortKeyList.size()];
1830      sortKeyList.toArray(sortKeyArray);
1831
1832      sortRequestControl = new ServerSideSortRequestControl(sortKeyArray);
1833    }
1834
1835
1836    // If we should use the virtual list view request control, then validate the
1837    // argument value and pre-create the control.
1838    if (virtualListView.isPresent())
1839    {
1840      try
1841      {
1842        final String[] elements = virtualListView.getValue().split(":");
1843        if (elements.length == 4)
1844        {
1845          vlvRequestControl = new VirtualListViewRequestControl(
1846               Integer.parseInt(elements[2]), Integer.parseInt(elements[0]),
1847               Integer.parseInt(elements[1]), Integer.parseInt(elements[3]),
1848               null);
1849        }
1850        else if (elements.length == 3)
1851        {
1852          vlvRequestControl = new VirtualListViewRequestControl(elements[2],
1853               Integer.parseInt(elements[0]), Integer.parseInt(elements[1]),
1854               null);
1855        }
1856        else
1857        {
1858          throw new ArgumentException(
1859               ERR_LDAPSEARCH_VLV_INVALID_VALUE.get(
1860                    virtualListView.getIdentifierString()));
1861        }
1862      }
1863      catch (final ArgumentException ae)
1864      {
1865        Debug.debugException(ae);
1866        throw ae;
1867      }
1868      catch (final Exception e)
1869      {
1870        Debug.debugException(e);
1871        throw new ArgumentException(
1872             ERR_LDAPSEARCH_VLV_INVALID_VALUE.get(
1873                  virtualListView.getIdentifierString()),
1874             e);
1875      }
1876    }
1877
1878
1879    // If we should use the LDAP join request control, then validate and
1880    // pre-create that control.
1881    if (joinRule.isPresent())
1882    {
1883      final JoinRule rule;
1884      try
1885      {
1886        final String[] elements = joinRule.getValue().toLowerCase().split(":");
1887        final String ruleName = StaticUtils.toLowerCase(elements[0]);
1888        if (ruleName.equals("dn"))
1889        {
1890          rule = JoinRule.createDNJoin(elements[1]);
1891        }
1892        else if (ruleName.equals("reverse-dn") || ruleName.equals("reversedn"))
1893        {
1894          rule = JoinRule.createReverseDNJoin(elements[1]);
1895        }
1896        else if (ruleName.equals("equals") || ruleName.equals("equality"))
1897        {
1898          rule = JoinRule.createEqualityJoin(elements[1], elements[2], false);
1899        }
1900        else if (ruleName.equals("contains") || ruleName.equals("substring"))
1901        {
1902          rule = JoinRule.createContainsJoin(elements[1], elements[2], false);
1903        }
1904        else
1905        {
1906          throw new ArgumentException(
1907               ERR_LDAPSEARCH_JOIN_RULE_INVALID_VALUE.get(
1908                    joinRule.getIdentifierString()));
1909        }
1910      }
1911      catch (final ArgumentException ae)
1912      {
1913        Debug.debugException(ae);
1914        throw ae;
1915      }
1916      catch (final Exception e)
1917      {
1918        Debug.debugException(e);
1919        throw new ArgumentException(
1920             ERR_LDAPSEARCH_JOIN_RULE_INVALID_VALUE.get(
1921                  joinRule.getIdentifierString()),
1922             e);
1923      }
1924
1925      final JoinBaseDN joinBase;
1926      if (joinBaseDN.isPresent())
1927      {
1928        final String s = StaticUtils.toLowerCase(joinBaseDN.getValue());
1929        if (s.equals("search-base") || s.equals("search-base-dn"))
1930        {
1931          joinBase = JoinBaseDN.createUseSearchBaseDN();
1932        }
1933        else if (s.equals("source-entry-dn") || s.equals("source-dn"))
1934        {
1935          joinBase = JoinBaseDN.createUseSourceEntryDN();
1936        }
1937        else
1938        {
1939          try
1940          {
1941            final DN dn = new DN(joinBaseDN.getValue());
1942            joinBase = JoinBaseDN.createUseCustomBaseDN(joinBaseDN.getValue());
1943          }
1944          catch (final Exception e)
1945          {
1946            Debug.debugException(e);
1947            throw new ArgumentException(
1948                 ERR_LDAPSEARCH_JOIN_BASE_DN_INVALID_VALUE.get(
1949                      joinBaseDN.getIdentifierString()),
1950                 e);
1951          }
1952        }
1953      }
1954      else
1955      {
1956        joinBase = JoinBaseDN.createUseSearchBaseDN();
1957      }
1958
1959      final String[] joinAttrs;
1960      if (joinRequestedAttribute.isPresent())
1961      {
1962        final List<String> valueList = joinRequestedAttribute.getValues();
1963        joinAttrs = new String[valueList.size()];
1964        valueList.toArray(joinAttrs);
1965      }
1966      else
1967      {
1968        joinAttrs = null;
1969      }
1970
1971      joinRequestControl = new JoinRequestControl(new JoinRequestValue(rule,
1972           joinBase, joinScope.getValue(), DereferencePolicy.NEVER,
1973           joinSizeLimit.getValue(), joinFilter.getValue(), joinAttrs,
1974           joinRequireMatch.isPresent(), null));
1975    }
1976
1977
1978    // If we should use the route to backend set request control, then validate
1979    // and pre-create those controls.
1980    if (routeToBackendSet.isPresent())
1981    {
1982      final List<String> values = routeToBackendSet.getValues();
1983      final Map<String,List<String>> idsByRP = new LinkedHashMap<>(
1984           StaticUtils.computeMapCapacity(values.size()));
1985      for (final String value : values)
1986      {
1987        final int colonPos = value.indexOf(':');
1988        if (colonPos <= 0)
1989        {
1990          throw new ArgumentException(
1991               ERR_LDAPSEARCH_ROUTE_TO_BACKEND_SET_INVALID_FORMAT.get(value,
1992                    routeToBackendSet.getIdentifierString()));
1993        }
1994
1995        final String rpID = value.substring(0, colonPos);
1996        final String bsID = value.substring(colonPos+1);
1997
1998        List<String> idsForRP = idsByRP.get(rpID);
1999        if (idsForRP == null)
2000        {
2001          idsForRP = new ArrayList<>(values.size());
2002          idsByRP.put(rpID, idsForRP);
2003        }
2004        idsForRP.add(bsID);
2005      }
2006
2007      for (final Map.Entry<String,List<String>> e : idsByRP.entrySet())
2008      {
2009        final String rpID = e.getKey();
2010        final List<String> bsIDs = e.getValue();
2011        routeToBackendSetRequestControls.add(
2012             RouteToBackendSetRequestControl.createAbsoluteRoutingRequest(true,
2013                  rpID, bsIDs));
2014      }
2015    }
2016
2017
2018    // Parse the dereference policy.
2019    final String derefStr =
2020         StaticUtils.toLowerCase(dereferencePolicy.getValue());
2021    if (derefStr.equals("always"))
2022    {
2023      derefPolicy = DereferencePolicy.ALWAYS;
2024    }
2025    else if (derefStr.equals("search"))
2026    {
2027      derefPolicy = DereferencePolicy.SEARCHING;
2028    }
2029    else if (derefStr.equals("find"))
2030    {
2031      derefPolicy = DereferencePolicy.FINDING;
2032    }
2033    else
2034    {
2035      derefPolicy = DereferencePolicy.NEVER;
2036    }
2037
2038
2039    // See if any entry transformations need to be applied.
2040    final ArrayList<EntryTransformation> transformations = new ArrayList<>(5);
2041    if (excludeAttribute.isPresent())
2042    {
2043      transformations.add(new ExcludeAttributeTransformation(null,
2044           excludeAttribute.getValues()));
2045    }
2046
2047    if (redactAttribute.isPresent())
2048    {
2049      transformations.add(new RedactAttributeTransformation(null, true,
2050           (! hideRedactedValueCount.isPresent()),
2051           redactAttribute.getValues()));
2052    }
2053
2054    if (scrambleAttribute.isPresent())
2055    {
2056      final Long randomSeed;
2057      if (scrambleRandomSeed.isPresent())
2058      {
2059        randomSeed = scrambleRandomSeed.getValue().longValue();
2060      }
2061      else
2062      {
2063        randomSeed = null;
2064      }
2065
2066      transformations.add(new ScrambleAttributeTransformation(null, randomSeed,
2067           true, scrambleAttribute.getValues(), scrambleJSONField.getValues()));
2068    }
2069
2070    if (renameAttributeFrom.isPresent())
2071    {
2072      if (renameAttributeFrom.getNumOccurrences() !=
2073          renameAttributeTo.getNumOccurrences())
2074      {
2075        throw new ArgumentException(
2076             ERR_LDAPSEARCH_RENAME_ATTRIBUTE_MISMATCH.get());
2077      }
2078
2079      final Iterator<String> sourceIterator =
2080           renameAttributeFrom.getValues().iterator();
2081      final Iterator<String> targetIterator =
2082           renameAttributeTo.getValues().iterator();
2083      while (sourceIterator.hasNext())
2084      {
2085        transformations.add(new RenameAttributeTransformation(null,
2086             sourceIterator.next(), targetIterator.next(), true));
2087      }
2088    }
2089
2090    if (moveSubtreeFrom.isPresent())
2091    {
2092      if (moveSubtreeFrom.getNumOccurrences() !=
2093          moveSubtreeTo.getNumOccurrences())
2094      {
2095        throw new ArgumentException(ERR_LDAPSEARCH_MOVE_SUBTREE_MISMATCH.get());
2096      }
2097
2098      final Iterator<DN> sourceIterator =
2099           moveSubtreeFrom.getValues().iterator();
2100      final Iterator<DN> targetIterator = moveSubtreeTo.getValues().iterator();
2101      while (sourceIterator.hasNext())
2102      {
2103        transformations.add(new MoveSubtreeTransformation(sourceIterator.next(),
2104             targetIterator.next()));
2105      }
2106    }
2107
2108    if (! transformations.isEmpty())
2109    {
2110      entryTransformations = transformations;
2111    }
2112
2113
2114    // Create the output handler.
2115    final String outputFormatStr =
2116         StaticUtils.toLowerCase(outputFormat.getValue());
2117    if (outputFormatStr.equals("json"))
2118    {
2119      outputHandler = new JSONLDAPSearchOutputHandler(this);
2120    }
2121    else if (outputFormatStr.equals("csv") ||
2122             outputFormatStr.equals("tab-delimited"))
2123    {
2124      // These output formats cannot be used with the --ldapURLFile argument.
2125      if (ldapURLFile.isPresent())
2126      {
2127        throw new ArgumentException(
2128             ERR_LDAPSEARCH_OUTPUT_FORMAT_NOT_SUPPORTED_WITH_URLS.get(
2129                  outputFormat.getValue(), ldapURLFile.getIdentifierString()));
2130      }
2131
2132      // These output formats require the requested attributes to be specified
2133      // via the --requestedAttribute argument rather than as unnamed trailing
2134      // arguments.
2135      final List<String> requestedAttributes = requestedAttribute.getValues();
2136      if ((requestedAttributes == null) || requestedAttributes.isEmpty())
2137      {
2138        throw new ArgumentException(
2139             ERR_LDAPSEARCH_OUTPUT_FORMAT_REQUIRES_REQUESTED_ATTR_ARG.get(
2140                  outputFormat.getValue(),
2141                  requestedAttribute.getIdentifierString()));
2142      }
2143
2144      switch (trailingArgs.size())
2145      {
2146        case 0:
2147          // This is fine.
2148          break;
2149
2150        case 1:
2151          // Make sure that the trailing argument is a filter rather than a
2152          // requested attribute.  It's sufficient to ensure that neither the
2153          // filter nor filterFile argument was provided.
2154          if (filter.isPresent() || filterFile.isPresent())
2155          {
2156            throw new ArgumentException(
2157                 ERR_LDAPSEARCH_OUTPUT_FORMAT_REQUIRES_REQUESTED_ATTR_ARG.get(
2158                      outputFormat.getValue(),
2159                      requestedAttribute.getIdentifierString()));
2160          }
2161          break;
2162
2163        default:
2164          throw new ArgumentException(
2165               ERR_LDAPSEARCH_OUTPUT_FORMAT_REQUIRES_REQUESTED_ATTR_ARG.get(
2166                    outputFormat.getValue(),
2167                    requestedAttribute.getIdentifierString()));
2168      }
2169
2170      outputHandler = new ColumnFormatterLDAPSearchOutputHandler(this,
2171           (outputFormatStr.equals("csv")
2172                ? OutputFormat.CSV
2173                : OutputFormat.TAB_DELIMITED_TEXT),
2174           requestedAttributes, WRAP_COLUMN);
2175    }
2176    else
2177    {
2178      outputHandler = new LDIFLDAPSearchOutputHandler(this, WRAP_COLUMN);
2179    }
2180  }
2181
2182
2183
2184  /**
2185   * {@inheritDoc}
2186   */
2187  @Override()
2188  public LDAPConnectionOptions getConnectionOptions()
2189  {
2190    final LDAPConnectionOptions options = new LDAPConnectionOptions();
2191
2192    options.setUseSynchronousMode(true);
2193    options.setFollowReferrals(followReferrals.isPresent());
2194    options.setUnsolicitedNotificationHandler(this);
2195
2196    return options;
2197  }
2198
2199
2200
2201  /**
2202   * {@inheritDoc}
2203   */
2204  @Override()
2205  public ResultCode doToolProcessing()
2206  {
2207    // If we should encrypt the output, then get the encryption passphrase.
2208    if (encryptOutput.isPresent())
2209    {
2210      if (encryptionPassphraseFile.isPresent())
2211      {
2212        try
2213        {
2214          encryptionPassphrase = ToolUtils.readEncryptionPassphraseFromFile(
2215               encryptionPassphraseFile.getValue());
2216        }
2217        catch (final LDAPException e)
2218        {
2219          Debug.debugException(e);
2220          wrapErr(0, WRAP_COLUMN, e.getMessage());
2221          return e.getResultCode();
2222        }
2223      }
2224      else
2225      {
2226        try
2227        {
2228          encryptionPassphrase = ToolUtils.promptForEncryptionPassphrase(false,
2229               true, getOut(), getErr());
2230        }
2231        catch (final LDAPException e)
2232        {
2233          Debug.debugException(e);
2234          wrapErr(0, WRAP_COLUMN, e.getMessage());
2235          return e.getResultCode();
2236        }
2237      }
2238    }
2239
2240
2241    // If we should use an output file, then set that up now.  Otherwise, write
2242    // the header to standard output.
2243    if (outputFile.isPresent())
2244    {
2245      if (! separateOutputFilePerSearch.isPresent())
2246      {
2247        try
2248        {
2249          OutputStream s = new FileOutputStream(outputFile.getValue());
2250
2251          if (encryptOutput.isPresent())
2252          {
2253            s = new PassphraseEncryptedOutputStream(encryptionPassphrase, s);
2254          }
2255
2256          if (compressOutput.isPresent())
2257          {
2258            s = new GZIPOutputStream(s);
2259          }
2260
2261          if (teeResultsToStandardOut.isPresent())
2262          {
2263            outStream = new PrintStream(new TeeOutputStream(s, getOut()));
2264          }
2265          else
2266          {
2267            outStream = new PrintStream(s);
2268          }
2269          errStream = outStream;
2270        }
2271        catch (final Exception e)
2272        {
2273          Debug.debugException(e);
2274          wrapErr(0, WRAP_COLUMN, ERR_LDAPSEARCH_CANNOT_OPEN_OUTPUT_FILE.get(
2275               outputFile.getValue().getAbsolutePath(),
2276               StaticUtils.getExceptionMessage(e)));
2277          return ResultCode.LOCAL_ERROR;
2278        }
2279
2280        outputHandler.formatHeader();
2281      }
2282    }
2283    else
2284    {
2285      outputHandler.formatHeader();
2286    }
2287
2288
2289    // Examine the arguments to determine the sets of controls to use for each
2290    // type of request.
2291    final List<Control> searchControls = getSearchControls();
2292
2293
2294    // If appropriate, ensure that any search result entries that include
2295    // base64-encoded attribute values will also include comments that attempt
2296    // to provide a human-readable representation of that value.
2297    final boolean originalCommentAboutBase64EncodedValues =
2298         LDIFWriter.commentAboutBase64EncodedValues();
2299    LDIFWriter.setCommentAboutBase64EncodedValues(
2300         ! suppressBase64EncodedValueComments.isPresent());
2301
2302
2303    LDAPConnectionPool pool = null;
2304    try
2305    {
2306      // Create a connection pool that will be used to communicate with the
2307      // directory server.
2308      if (! dryRun.isPresent())
2309      {
2310        try
2311        {
2312          final StartAdministrativeSessionPostConnectProcessor p;
2313          if (useAdministrativeSession.isPresent())
2314          {
2315            p = new StartAdministrativeSessionPostConnectProcessor(
2316                 new StartAdministrativeSessionExtendedRequest(getToolName(),
2317                      true));
2318          }
2319          else
2320          {
2321            p = null;
2322          }
2323
2324          pool = getConnectionPool(1, 1, 0, p, null, true,
2325               new ReportBindResultLDAPConnectionPoolHealthCheck(this, true,
2326                    false));
2327        }
2328        catch (final LDAPException le)
2329        {
2330          // This shouldn't happen since the pool won't throw an exception if an
2331          // attempt to create an initial connection fails.
2332          Debug.debugException(le);
2333          commentToErr(ERR_LDAPSEARCH_CANNOT_CREATE_CONNECTION_POOL.get(
2334               StaticUtils.getExceptionMessage(le)));
2335          return le.getResultCode();
2336        }
2337
2338        if (retryFailedOperations.isPresent())
2339        {
2340          pool.setRetryFailedOperationsDueToInvalidConnections(true);
2341        }
2342      }
2343
2344
2345      // If appropriate, create a rate limiter.
2346      final FixedRateBarrier rateLimiter;
2347      if (ratePerSecond.isPresent())
2348      {
2349        rateLimiter = new FixedRateBarrier(1000L, ratePerSecond.getValue());
2350      }
2351      else
2352      {
2353        rateLimiter = null;
2354      }
2355
2356
2357      // If one or more LDAP URL files are provided, then construct search
2358      // requests from those URLs.
2359      if (ldapURLFile.isPresent())
2360      {
2361        return searchWithLDAPURLs(pool, rateLimiter, searchControls);
2362      }
2363
2364
2365      // Get the set of requested attributes, as a combination of the
2366      // requestedAttribute argument values and any trailing arguments.
2367      final ArrayList<String> attrList = new ArrayList<>(10);
2368      if (requestedAttribute.isPresent())
2369      {
2370        attrList.addAll(requestedAttribute.getValues());
2371      }
2372
2373      final List<String> trailingArgs = parser.getTrailingArguments();
2374      if (! trailingArgs.isEmpty())
2375      {
2376        final Iterator<String> trailingArgIterator = trailingArgs.iterator();
2377        if (! (filter.isPresent() || filterFile.isPresent()))
2378        {
2379          trailingArgIterator.next();
2380        }
2381
2382        while (trailingArgIterator.hasNext())
2383        {
2384          attrList.add(trailingArgIterator.next());
2385        }
2386      }
2387
2388      final String[] attributes = new String[attrList.size()];
2389      attrList.toArray(attributes);
2390
2391
2392      // If either or both the filter or filterFile arguments are provided, then
2393      // use them to get the filters to process.  Otherwise, the first trailing
2394      // argument should be a filter.
2395      ResultCode resultCode = ResultCode.SUCCESS;
2396      if (filter.isPresent() || filterFile.isPresent())
2397      {
2398        if (filter.isPresent())
2399        {
2400          for (final Filter f : filter.getValues())
2401          {
2402            final ResultCode rc = searchWithFilter(pool, f, attributes,
2403                 rateLimiter, searchControls);
2404            if (rc != ResultCode.SUCCESS)
2405            {
2406              if (resultCode == ResultCode.SUCCESS)
2407              {
2408                resultCode = rc;
2409              }
2410
2411              if (! continueOnError.isPresent())
2412              {
2413                return resultCode;
2414              }
2415            }
2416          }
2417        }
2418
2419        if (filterFile.isPresent())
2420        {
2421          final ResultCode rc = searchWithFilterFile(pool, attributes,
2422               rateLimiter, searchControls);
2423          if (rc != ResultCode.SUCCESS)
2424          {
2425            if (resultCode == ResultCode.SUCCESS)
2426            {
2427              resultCode = rc;
2428            }
2429
2430            if (! continueOnError.isPresent())
2431            {
2432              return resultCode;
2433            }
2434          }
2435        }
2436      }
2437      else
2438      {
2439        final Filter f;
2440        try
2441        {
2442          final String filterStr =
2443               parser.getTrailingArguments().iterator().next();
2444          f = Filter.create(filterStr);
2445        }
2446        catch (final LDAPException le)
2447        {
2448          // This should never happen.
2449          Debug.debugException(le);
2450          displayResult(le.toLDAPResult());
2451          return le.getResultCode();
2452        }
2453
2454        resultCode =
2455             searchWithFilter(pool, f, attributes, rateLimiter, searchControls);
2456      }
2457
2458      return resultCode;
2459    }
2460    finally
2461    {
2462      if (pool != null)
2463      {
2464        try
2465        {
2466          pool.close();
2467        }
2468        catch (final Exception e)
2469        {
2470          Debug.debugException(e);
2471        }
2472      }
2473
2474      if (outStream != null)
2475      {
2476        try
2477        {
2478          outStream.close();
2479          outStream = null;
2480        }
2481        catch (final Exception e)
2482        {
2483          Debug.debugException(e);
2484        }
2485      }
2486
2487      if (errStream != null)
2488      {
2489        try
2490        {
2491          errStream.close();
2492          errStream = null;
2493        }
2494        catch (final Exception e)
2495        {
2496          Debug.debugException(e);
2497        }
2498      }
2499
2500      LDIFWriter.setCommentAboutBase64EncodedValues(
2501           originalCommentAboutBase64EncodedValues);
2502    }
2503  }
2504
2505
2506
2507  /**
2508   * Processes a set of searches using LDAP URLs read from one or more files.
2509   *
2510   * @param  pool            The connection pool to use to communicate with the
2511   *                         directory server.
2512   * @param  rateLimiter     An optional fixed-rate barrier that can be used for
2513   *                         request rate limiting.
2514   * @param  searchControls  The set of controls to include in search requests.
2515   *
2516   * @return  A result code indicating the result of the processing.
2517   */
2518  private ResultCode searchWithLDAPURLs(final LDAPConnectionPool pool,
2519                                        final FixedRateBarrier rateLimiter,
2520                                        final List<Control> searchControls)
2521  {
2522    ResultCode resultCode = ResultCode.SUCCESS;
2523    for (final File f : ldapURLFile.getValues())
2524    {
2525      BufferedReader reader = null;
2526
2527      try
2528      {
2529        reader = new BufferedReader(new FileReader(f));
2530        while (true)
2531        {
2532          final String line = reader.readLine();
2533          if (line == null)
2534          {
2535            break;
2536          }
2537
2538          if ((line.length() == 0) || line.startsWith("#"))
2539          {
2540            continue;
2541          }
2542
2543          final LDAPURL url;
2544          try
2545          {
2546            url = new LDAPURL(line);
2547          }
2548          catch (final LDAPException le)
2549          {
2550            Debug.debugException(le);
2551
2552            commentToErr(ERR_LDAPSEARCH_MALFORMED_LDAP_URL.get(
2553                 f.getAbsolutePath(), line));
2554            if (resultCode == ResultCode.SUCCESS)
2555            {
2556              resultCode = le.getResultCode();
2557            }
2558
2559            if (continueOnError.isPresent())
2560            {
2561              continue;
2562            }
2563            else
2564            {
2565              return resultCode;
2566            }
2567          }
2568
2569          final SearchRequest searchRequest = new SearchRequest(
2570               new LDAPSearchListener(outputHandler, entryTransformations),
2571               url.getBaseDN().toString(), url.getScope(), derefPolicy,
2572               sizeLimit.getValue(), timeLimitSeconds.getValue(),
2573               typesOnly.isPresent(), url.getFilter(), url.getAttributes());
2574          final ResultCode rc =
2575               doSearch(pool, searchRequest, rateLimiter, searchControls);
2576          if (rc != ResultCode.SUCCESS)
2577          {
2578            if (resultCode == ResultCode.SUCCESS)
2579            {
2580              resultCode = rc;
2581            }
2582
2583            if (! continueOnError.isPresent())
2584            {
2585              return resultCode;
2586            }
2587          }
2588        }
2589      }
2590      catch (final IOException ioe)
2591      {
2592        commentToErr(ERR_LDAPSEARCH_CANNOT_READ_LDAP_URL_FILE.get(
2593             f.getAbsolutePath(), StaticUtils.getExceptionMessage(ioe)));
2594        return ResultCode.LOCAL_ERROR;
2595      }
2596      finally
2597      {
2598        if (reader != null)
2599        {
2600          try
2601          {
2602            reader.close();
2603          }
2604          catch (final Exception e)
2605          {
2606            Debug.debugException(e);
2607          }
2608        }
2609      }
2610    }
2611
2612    return resultCode;
2613  }
2614
2615
2616
2617  /**
2618   * Processes a set of searches using filters read from one or more files.
2619   *
2620   * @param  pool            The connection pool to use to communicate with the
2621   *                         directory server.
2622   * @param  attributes      The set of attributes to request that the server
2623   *                         include in matching entries.
2624   * @param  rateLimiter     An optional fixed-rate barrier that can be used for
2625   *                         request rate limiting.
2626   * @param  searchControls  The set of controls to include in search requests.
2627   *
2628   * @return  A result code indicating the result of the processing.
2629   */
2630  private ResultCode searchWithFilterFile(final LDAPConnectionPool pool,
2631                                          final String[] attributes,
2632                                          final FixedRateBarrier rateLimiter,
2633                                          final List<Control> searchControls)
2634  {
2635    ResultCode resultCode = ResultCode.SUCCESS;
2636    for (final File f : filterFile.getValues())
2637    {
2638      FilterFileReader reader = null;
2639
2640      try
2641      {
2642        reader = new FilterFileReader(f);
2643        while (true)
2644        {
2645          final Filter searchFilter;
2646          try
2647          {
2648            searchFilter = reader.readFilter();
2649          }
2650          catch (final LDAPException le)
2651          {
2652            Debug.debugException(le);
2653            commentToErr(ERR_LDAPSEARCH_MALFORMED_FILTER.get(
2654                 f.getAbsolutePath(), le.getMessage()));
2655            if (resultCode == ResultCode.SUCCESS)
2656            {
2657              resultCode = le.getResultCode();
2658            }
2659
2660            if (continueOnError.isPresent())
2661            {
2662              continue;
2663            }
2664            else
2665            {
2666              return resultCode;
2667            }
2668          }
2669
2670          if (searchFilter == null)
2671          {
2672            break;
2673          }
2674
2675          final ResultCode rc = searchWithFilter(pool, searchFilter, attributes,
2676               rateLimiter, searchControls);
2677          if (rc != ResultCode.SUCCESS)
2678          {
2679            if (resultCode == ResultCode.SUCCESS)
2680            {
2681              resultCode = rc;
2682            }
2683
2684            if (! continueOnError.isPresent())
2685            {
2686              return resultCode;
2687            }
2688          }
2689        }
2690      }
2691      catch (final IOException ioe)
2692      {
2693        Debug.debugException(ioe);
2694        commentToErr(ERR_LDAPSEARCH_CANNOT_READ_FILTER_FILE.get(
2695             f.getAbsolutePath(), StaticUtils.getExceptionMessage(ioe)));
2696        return ResultCode.LOCAL_ERROR;
2697      }
2698      finally
2699      {
2700        if (reader != null)
2701        {
2702          try
2703          {
2704            reader.close();
2705          }
2706          catch (final Exception e)
2707          {
2708            Debug.debugException(e);
2709          }
2710        }
2711      }
2712    }
2713
2714    return resultCode;
2715  }
2716
2717
2718
2719  /**
2720   * Processes a search using the provided filter.
2721   *
2722   * @param  pool            The connection pool to use to communicate with the
2723   *                         directory server.
2724   * @param  filter          The filter to use for the search.
2725   * @param  attributes      The set of attributes to request that the server
2726   *                         include in matching entries.
2727   * @param  rateLimiter     An optional fixed-rate barrier that can be used for
2728   *                         request rate limiting.
2729   * @param  searchControls  The set of controls to include in search requests.
2730   *
2731   * @return  A result code indicating the result of the processing.
2732   */
2733  private ResultCode searchWithFilter(final LDAPConnectionPool pool,
2734                                      final Filter filter,
2735                                      final String[] attributes,
2736                                      final FixedRateBarrier rateLimiter,
2737                                      final List<Control> searchControls)
2738  {
2739    final String baseDNString;
2740    if (baseDN.isPresent())
2741    {
2742      baseDNString = baseDN.getStringValue();
2743    }
2744    else
2745    {
2746      baseDNString = "";
2747    }
2748
2749    final SearchRequest searchRequest = new SearchRequest(
2750         new LDAPSearchListener(outputHandler, entryTransformations),
2751         baseDNString, scope.getValue(), derefPolicy, sizeLimit.getValue(),
2752         timeLimitSeconds.getValue(), typesOnly.isPresent(), filter,
2753         attributes);
2754    return doSearch(pool, searchRequest, rateLimiter, searchControls);
2755  }
2756
2757
2758
2759  /**
2760   * Processes a search with the provided information.
2761   *
2762   * @param  pool            The connection pool to use to communicate with the
2763   *                         directory server.
2764   * @param  searchRequest   The search request to process.
2765   * @param  rateLimiter     An optional fixed-rate barrier that can be used for
2766   *                         request rate limiting.
2767   * @param  searchControls  The set of controls to include in search requests.
2768   *
2769   * @return  A result code indicating the result of the processing.
2770   */
2771  private ResultCode doSearch(final LDAPConnectionPool pool,
2772                              final SearchRequest searchRequest,
2773                              final FixedRateBarrier rateLimiter,
2774                              final List<Control> searchControls)
2775  {
2776    if (separateOutputFilePerSearch.isPresent())
2777    {
2778      try
2779      {
2780        final String path = outputFile.getValue().getAbsolutePath() + '.' +
2781             outputFileCounter.getAndIncrement();
2782
2783        OutputStream s = new FileOutputStream(path);
2784
2785        if (encryptOutput.isPresent())
2786        {
2787          s = new PassphraseEncryptedOutputStream(encryptionPassphrase, s);
2788        }
2789
2790        if (compressOutput.isPresent())
2791        {
2792          s = new GZIPOutputStream(s);
2793        }
2794
2795        if (teeResultsToStandardOut.isPresent())
2796        {
2797          outStream = new PrintStream(new TeeOutputStream(s, getOut()));
2798        }
2799        else
2800        {
2801          outStream = new PrintStream(s);
2802        }
2803        errStream = outStream;
2804      }
2805      catch (final Exception e)
2806      {
2807        Debug.debugException(e);
2808        wrapErr(0, WRAP_COLUMN, ERR_LDAPSEARCH_CANNOT_OPEN_OUTPUT_FILE.get(
2809             outputFile.getValue().getAbsolutePath(),
2810             StaticUtils.getExceptionMessage(e)));
2811        return ResultCode.LOCAL_ERROR;
2812      }
2813
2814      outputHandler.formatHeader();
2815    }
2816
2817    try
2818    {
2819      if (rateLimiter != null)
2820      {
2821        rateLimiter.await();
2822      }
2823
2824
2825      ASN1OctetString pagedResultsCookie = null;
2826      boolean multiplePages = false;
2827      long totalEntries = 0;
2828      long totalReferences = 0;
2829
2830      SearchResult searchResult;
2831      try
2832      {
2833        while (true)
2834        {
2835          searchRequest.setControls(searchControls);
2836          if (simplePageSize.isPresent())
2837          {
2838            searchRequest.addControl(new SimplePagedResultsControl(
2839                 simplePageSize.getValue(), pagedResultsCookie));
2840          }
2841
2842          if (dryRun.isPresent())
2843          {
2844            searchResult = new SearchResult(-1, ResultCode.SUCCESS,
2845                 INFO_LDAPSEARCH_DRY_RUN_REQUEST_NOT_SENT.get(
2846                      dryRun.getIdentifierString(),
2847                      String.valueOf(searchRequest)),
2848                 null, null, 0, 0, null);
2849            break;
2850          }
2851          else
2852          {
2853            if (! terse.isPresent())
2854            {
2855              if (verbose.isPresent() || persistentSearch.isPresent() ||
2856                  filterFile.isPresent() || ldapURLFile.isPresent() ||
2857                  (filter.isPresent() && (filter.getNumOccurrences() > 1)))
2858              {
2859                commentToOut(INFO_LDAPSEARCH_SENDING_SEARCH_REQUEST.get(
2860                     String.valueOf(searchRequest)));
2861              }
2862            }
2863            searchResult = pool.search(searchRequest);
2864          }
2865
2866          if (searchResult.getEntryCount() > 0)
2867          {
2868            totalEntries += searchResult.getEntryCount();
2869          }
2870
2871          if (searchResult.getReferenceCount() > 0)
2872          {
2873            totalReferences += searchResult.getReferenceCount();
2874          }
2875
2876          if (simplePageSize.isPresent())
2877          {
2878            final SimplePagedResultsControl pagedResultsControl;
2879            try
2880            {
2881              pagedResultsControl = SimplePagedResultsControl.get(searchResult);
2882              if (pagedResultsControl == null)
2883              {
2884                throw new LDAPSearchException(new SearchResult(
2885                     searchResult.getMessageID(), ResultCode.CONTROL_NOT_FOUND,
2886                     ERR_LDAPSEARCH_MISSING_PAGED_RESULTS_RESPONSE_CONTROL.
2887                          get(),
2888                     searchResult.getMatchedDN(),
2889                     searchResult.getReferralURLs(),
2890                     searchResult.getSearchEntries(),
2891                     searchResult.getSearchReferences(),
2892                     searchResult.getEntryCount(),
2893                     searchResult.getReferenceCount(),
2894                     searchResult.getResponseControls()));
2895              }
2896
2897              if (pagedResultsControl.moreResultsToReturn())
2898              {
2899                if (verbose.isPresent())
2900                {
2901                  commentToOut(
2902                       INFO_LDAPSEARCH_INTERMEDIATE_PAGED_SEARCH_RESULT.get());
2903                  displayResult(searchResult);
2904                }
2905
2906                multiplePages = true;
2907                pagedResultsCookie = pagedResultsControl.getCookie();
2908              }
2909              else
2910              {
2911                break;
2912              }
2913            }
2914            catch (final LDAPException le)
2915            {
2916              Debug.debugException(le);
2917              throw new LDAPSearchException(new SearchResult(
2918                   searchResult.getMessageID(), ResultCode.CONTROL_NOT_FOUND,
2919                   ERR_LDAPSEARCH_CANNOT_DECODE_PAGED_RESULTS_RESPONSE_CONTROL.
2920                        get(StaticUtils.getExceptionMessage(le)),
2921                   searchResult.getMatchedDN(), searchResult.getReferralURLs(),
2922                   searchResult.getSearchEntries(),
2923                   searchResult.getSearchReferences(),
2924                   searchResult.getEntryCount(),
2925                   searchResult.getReferenceCount(),
2926                   searchResult.getResponseControls()));
2927            }
2928          }
2929          else
2930          {
2931            break;
2932          }
2933        }
2934      }
2935      catch (final LDAPSearchException lse)
2936      {
2937        Debug.debugException(lse);
2938        searchResult = lse.toLDAPResult();
2939
2940        if (searchResult.getEntryCount() > 0)
2941        {
2942          totalEntries += searchResult.getEntryCount();
2943        }
2944
2945        if (searchResult.getReferenceCount() > 0)
2946        {
2947          totalReferences += searchResult.getReferenceCount();
2948        }
2949      }
2950
2951      if ((searchResult.getResultCode() != ResultCode.SUCCESS) ||
2952          (searchResult.getDiagnosticMessage() != null) ||
2953          (! terse.isPresent()))
2954      {
2955        displayResult(searchResult);
2956      }
2957
2958      if (multiplePages && (! terse.isPresent()))
2959      {
2960        commentToOut(INFO_LDAPSEARCH_TOTAL_SEARCH_ENTRIES.get(totalEntries));
2961
2962        if (totalReferences > 0)
2963        {
2964          commentToOut(INFO_LDAPSEARCH_TOTAL_SEARCH_REFERENCES.get(
2965               totalReferences));
2966        }
2967      }
2968
2969      if (countEntries.isPresent())
2970      {
2971        return ResultCode.valueOf((int) Math.min(totalEntries, 255));
2972      }
2973      else
2974      {
2975        return searchResult.getResultCode();
2976      }
2977    }
2978    finally
2979    {
2980      if (separateOutputFilePerSearch.isPresent())
2981      {
2982        try
2983        {
2984          outStream.close();
2985        }
2986        catch (final Exception e)
2987        {
2988          Debug.debugException(e);
2989        }
2990
2991        outStream = null;
2992        errStream = null;
2993      }
2994    }
2995  }
2996
2997
2998
2999  /**
3000   * Retrieves a list of the controls that should be used when processing search
3001   * operations.
3002   *
3003   * @return  A list of the controls that should be used when processing search
3004   *          operations.
3005   *
3006   * @throws  LDAPException  If a problem is encountered while generating the
3007   *                         controls for a search request.
3008   */
3009  private List<Control> getSearchControls()
3010  {
3011    final ArrayList<Control> controls = new ArrayList<>(10);
3012
3013    if (searchControl.isPresent())
3014    {
3015      controls.addAll(searchControl.getValues());
3016    }
3017
3018    if (joinRequestControl != null)
3019    {
3020      controls.add(joinRequestControl);
3021    }
3022
3023    if (matchedValuesRequestControl != null)
3024    {
3025      controls.add(matchedValuesRequestControl);
3026    }
3027
3028    if (matchingEntryCountRequestControl != null)
3029    {
3030      controls.add(matchingEntryCountRequestControl);
3031    }
3032
3033    if (overrideSearchLimitsRequestControl != null)
3034    {
3035      controls.add(overrideSearchLimitsRequestControl);
3036    }
3037
3038    if (persistentSearchRequestControl != null)
3039    {
3040      controls.add(persistentSearchRequestControl);
3041    }
3042
3043    if (sortRequestControl != null)
3044    {
3045      controls.add(sortRequestControl);
3046    }
3047
3048    if (vlvRequestControl != null)
3049    {
3050      controls.add(vlvRequestControl);
3051    }
3052
3053    controls.addAll(routeToBackendSetRequestControls);
3054
3055    if (accountUsable.isPresent())
3056    {
3057      controls.add(new AccountUsableRequestControl(true));
3058    }
3059
3060    if (getBackendSetID.isPresent())
3061    {
3062      controls.add(new GetBackendSetIDRequestControl(false));
3063    }
3064
3065    if (getServerID.isPresent())
3066    {
3067      controls.add(new GetServerIDRequestControl(false));
3068    }
3069
3070    if (includeReplicationConflictEntries.isPresent())
3071    {
3072      controls.add(new ReturnConflictEntriesRequestControl(true));
3073    }
3074
3075    if (includeSoftDeletedEntries.isPresent())
3076    {
3077      final String valueStr =
3078           StaticUtils.toLowerCase(includeSoftDeletedEntries.getValue());
3079      if (valueStr.equals("with-non-deleted-entries"))
3080      {
3081        controls.add(new SoftDeletedEntryAccessRequestControl(true, true,
3082             false));
3083      }
3084      else if (valueStr.equals("without-non-deleted-entries"))
3085      {
3086        controls.add(new SoftDeletedEntryAccessRequestControl(true, false,
3087             false));
3088      }
3089      else
3090      {
3091        controls.add(new SoftDeletedEntryAccessRequestControl(true, false,
3092             true));
3093      }
3094    }
3095
3096    if (includeSubentries.isPresent())
3097    {
3098      controls.add(new SubentriesRequestControl(true));
3099    }
3100
3101    if (manageDsaIT.isPresent())
3102    {
3103      controls.add(new ManageDsaITRequestControl(true));
3104    }
3105
3106    if (realAttributesOnly.isPresent())
3107    {
3108      controls.add(new RealAttributesOnlyRequestControl(true));
3109    }
3110
3111    if (routeToServer.isPresent())
3112    {
3113      controls.add(new RouteToServerRequestControl(false,
3114           routeToServer.getValue(), false, false, false));
3115    }
3116
3117    if (virtualAttributesOnly.isPresent())
3118    {
3119      controls.add(new VirtualAttributesOnlyRequestControl(true));
3120    }
3121
3122    if (excludeBranch.isPresent())
3123    {
3124      final ArrayList<String> dns =
3125           new ArrayList<>(excludeBranch.getValues().size());
3126      for (final DN dn : excludeBranch.getValues())
3127      {
3128        dns.add(dn.toString());
3129      }
3130      controls.add(new ExcludeBranchRequestControl(true, dns));
3131    }
3132
3133    if (assertionFilter.isPresent())
3134    {
3135      controls.add(new AssertionRequestControl(
3136           assertionFilter.getValue(), true));
3137    }
3138
3139    if (getEffectiveRightsAuthzID.isPresent())
3140    {
3141      final String[] attributes;
3142      if (getEffectiveRightsAttribute.isPresent())
3143      {
3144        attributes = new String[getEffectiveRightsAttribute.getValues().size()];
3145        for (int i=0; i < attributes.length; i++)
3146        {
3147          attributes[i] = getEffectiveRightsAttribute.getValues().get(i);
3148        }
3149      }
3150      else
3151      {
3152        attributes = StaticUtils.NO_STRINGS;
3153      }
3154
3155      controls.add(new GetEffectiveRightsRequestControl(true,
3156           getEffectiveRightsAuthzID.getValue(), attributes));
3157    }
3158
3159    if (operationPurpose.isPresent())
3160    {
3161      controls.add(new OperationPurposeRequestControl(true, "ldapsearch",
3162           Version.NUMERIC_VERSION_STRING, "LDAPSearch.getSearchControls",
3163           operationPurpose.getValue()));
3164    }
3165
3166    if (proxyAs.isPresent())
3167    {
3168      controls.add(new ProxiedAuthorizationV2RequestControl(
3169           proxyAs.getValue()));
3170    }
3171
3172    if (proxyV1As.isPresent())
3173    {
3174      controls.add(new ProxiedAuthorizationV1RequestControl(
3175           proxyV1As.getValue()));
3176    }
3177
3178    if (suppressOperationalAttributeUpdates.isPresent())
3179    {
3180      final EnumSet<SuppressType> suppressTypes =
3181           EnumSet.noneOf(SuppressType.class);
3182      for (final String s : suppressOperationalAttributeUpdates.getValues())
3183      {
3184        if (s.equalsIgnoreCase("last-access-time"))
3185        {
3186          suppressTypes.add(SuppressType.LAST_ACCESS_TIME);
3187        }
3188        else if (s.equalsIgnoreCase("last-login-time"))
3189        {
3190          suppressTypes.add(SuppressType.LAST_LOGIN_TIME);
3191        }
3192        else if (s.equalsIgnoreCase("last-login-ip"))
3193        {
3194          suppressTypes.add(SuppressType.LAST_LOGIN_IP);
3195        }
3196      }
3197
3198      controls.add(new SuppressOperationalAttributeUpdateRequestControl(
3199           suppressTypes));
3200    }
3201
3202    if (rejectUnindexedSearch.isPresent())
3203    {
3204      controls.add(new RejectUnindexedSearchRequestControl());
3205    }
3206
3207    if (permitUnindexedSearch.isPresent())
3208    {
3209      controls.add(new PermitUnindexedSearchRequestControl());
3210    }
3211
3212    return controls;
3213  }
3214
3215
3216
3217  /**
3218   * Displays information about the provided result, including special
3219   * processing for a number of supported response controls.
3220   *
3221   * @param  result  The result to examine.
3222   */
3223  private void displayResult(final LDAPResult result)
3224  {
3225    outputHandler.formatResult(result);
3226  }
3227
3228
3229
3230  /**
3231   * Writes the provided message to the output stream.
3232   *
3233   * @param  message  The message to be written.
3234   */
3235  void writeOut(final String message)
3236  {
3237    if (outStream == null)
3238    {
3239      out(message);
3240    }
3241    else
3242    {
3243      outStream.println(message);
3244    }
3245  }
3246
3247
3248
3249  /**
3250   * Writes the provided message to the error stream.
3251   *
3252   * @param  message  The message to be written.
3253   */
3254  private void writeErr(final String message)
3255  {
3256    if (errStream == null)
3257    {
3258      err(message);
3259    }
3260    else
3261    {
3262      errStream.println(message);
3263    }
3264  }
3265
3266
3267
3268  /**
3269   * Writes a line-wrapped, commented version of the provided message to
3270   * standard output.
3271   *
3272   * @param  message  The message to be written.
3273   */
3274  private void commentToOut(final String message)
3275  {
3276    if (terse.isPresent())
3277    {
3278      return;
3279    }
3280
3281    for (final String line : StaticUtils.wrapLine(message, (WRAP_COLUMN - 2)))
3282    {
3283      writeOut("# " + line);
3284    }
3285  }
3286
3287
3288
3289  /**
3290   * Writes a line-wrapped, commented version of the provided message to
3291   * standard error.
3292   *
3293   * @param  message  The message to be written.
3294   */
3295  private void commentToErr(final String message)
3296  {
3297    for (final String line : StaticUtils.wrapLine(message, (WRAP_COLUMN - 2)))
3298    {
3299      writeErr("# " + line);
3300    }
3301  }
3302
3303
3304
3305  /**
3306   * Sets the output handler that should be used by this tool  This is primarily
3307   * intended for testing purposes.
3308   *
3309   * @param  outputHandler  The output handler that should be used by this tool.
3310   */
3311  void setOutputHandler(final LDAPSearchOutputHandler outputHandler)
3312  {
3313    this.outputHandler = outputHandler;
3314  }
3315
3316
3317
3318  /**
3319   * {@inheritDoc}
3320   */
3321  @Override()
3322  public void handleUnsolicitedNotification(final LDAPConnection connection,
3323                                            final ExtendedResult notification)
3324  {
3325    outputHandler.formatUnsolicitedNotification(connection, notification);
3326  }
3327
3328
3329
3330  /**
3331   * {@inheritDoc}
3332   */
3333  @Override()
3334  public LinkedHashMap<String[],String> getExampleUsages()
3335  {
3336    final LinkedHashMap<String[],String> examples =
3337         new LinkedHashMap<>(StaticUtils.computeMapCapacity(5));
3338
3339    String[] args =
3340    {
3341      "--hostname", "directory.example.com",
3342      "--port", "389",
3343      "--bindDN", "uid=jdoe,ou=People,dc=example,dc=com",
3344      "--bindPassword", "password",
3345      "--baseDN", "ou=People,dc=example,dc=com",
3346      "--searchScope", "sub",
3347      "(uid=jqpublic)",
3348      "givenName",
3349      "sn",
3350      "mail"
3351    };
3352    examples.put(args, INFO_LDAPSEARCH_EXAMPLE_1.get());
3353
3354
3355    args = new String[]
3356    {
3357      "--hostname", "directory.example.com",
3358      "--port", "636",
3359      "--useSSL",
3360      "--saslOption", "mech=PLAIN",
3361      "--saslOption", "authID=u:jdoe",
3362      "--bindPasswordFile", "/path/to/password/file",
3363      "--baseDN", "ou=People,dc=example,dc=com",
3364      "--searchScope", "sub",
3365      "--filterFile", "/path/to/filter/file",
3366      "--outputFile", "/path/to/base/output/file",
3367      "--separateOutputFilePerSearch",
3368      "--requestedAttribute", "*",
3369      "--requestedAttribute", "+"
3370    };
3371    examples.put(args, INFO_LDAPSEARCH_EXAMPLE_2.get());
3372
3373
3374    args = new String[]
3375    {
3376      "--hostname", "directory.example.com",
3377      "--port", "389",
3378      "--useStartTLS",
3379      "--trustStorePath", "/path/to/truststore/file",
3380      "--baseDN", "",
3381      "--searchScope", "base",
3382      "--outputFile", "/path/to/output/file",
3383      "--teeResultsToStandardOut",
3384      "(objectClass=*)",
3385      "*",
3386      "+"
3387    };
3388    examples.put(args, INFO_LDAPSEARCH_EXAMPLE_3.get());
3389
3390
3391    args = new String[]
3392    {
3393      "--hostname", "directory.example.com",
3394      "--port", "389",
3395      "--bindDN", "uid=admin,dc=example,dc=com",
3396      "--baseDN", "dc=example,dc=com",
3397      "--searchScope", "sub",
3398      "--outputFile", "/path/to/output/file",
3399      "--simplePageSize", "100",
3400      "(objectClass=*)",
3401      "*",
3402      "+"
3403    };
3404    examples.put(args, INFO_LDAPSEARCH_EXAMPLE_4.get());
3405
3406
3407    args = new String[]
3408    {
3409      "--hostname", "directory.example.com",
3410      "--port", "389",
3411      "--bindDN", "uid=admin,dc=example,dc=com",
3412      "--baseDN", "dc=example,dc=com",
3413      "--searchScope", "sub",
3414      "(&(givenName=John)(sn=Doe))",
3415      "debugsearchindex"
3416    };
3417    examples.put(args, INFO_LDAPSEARCH_EXAMPLE_5.get());
3418
3419    return examples;
3420  }
3421}