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