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