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