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