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