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