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