001/*
002 * Copyright 2020-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2020-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2020-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldif;
037
038
039
040import java.io.BufferedReader;
041import java.io.File;
042import java.io.FileInputStream;
043import java.io.FileOutputStream;
044import java.io.IOException;
045import java.io.InputStream;
046import java.io.InputStreamReader;
047import java.io.OutputStream;
048import java.util.ArrayList;
049import java.util.Arrays;
050import java.util.Collections;
051import java.util.HashSet;
052import java.util.Iterator;
053import java.util.LinkedHashMap;
054import java.util.LinkedHashSet;
055import java.util.List;
056import java.util.Map;
057import java.util.Set;
058import java.util.TreeMap;
059import java.util.concurrent.atomic.AtomicReference;
060import java.util.zip.GZIPOutputStream;
061
062import com.unboundid.ldap.listener.SearchEntryParer;
063import com.unboundid.ldap.sdk.DN;
064import com.unboundid.ldap.sdk.Entry;
065import com.unboundid.ldap.sdk.Filter;
066import com.unboundid.ldap.sdk.InternalSDKHelper;
067import com.unboundid.ldap.sdk.LDAPException;
068import com.unboundid.ldap.sdk.LDAPURL;
069import com.unboundid.ldap.sdk.ResultCode;
070import com.unboundid.ldap.sdk.SearchResultEntry;
071import com.unboundid.ldap.sdk.SearchScope;
072import com.unboundid.ldap.sdk.Version;
073import com.unboundid.ldap.sdk.schema.EntryValidator;
074import com.unboundid.ldap.sdk.schema.Schema;
075import com.unboundid.ldap.sdk.unboundidds.tools.ColumnBasedLDAPResultWriter;
076import com.unboundid.ldap.sdk.unboundidds.tools.DNsOnlyLDAPResultWriter;
077import com.unboundid.ldap.sdk.unboundidds.tools.JSONLDAPResultWriter;
078import com.unboundid.ldap.sdk.unboundidds.tools.LDAPResultWriter;
079import com.unboundid.ldap.sdk.unboundidds.tools.LDIFLDAPResultWriter;
080import com.unboundid.ldap.sdk.unboundidds.tools.ToolUtils;
081import com.unboundid.ldap.sdk.unboundidds.tools.ValuesOnlyLDAPResultWriter;
082import com.unboundid.util.CommandLineTool;
083import com.unboundid.util.Debug;
084import com.unboundid.util.NotNull;
085import com.unboundid.util.Nullable;
086import com.unboundid.util.ObjectPair;
087import com.unboundid.util.OutputFormat;
088import com.unboundid.util.PassphraseEncryptedOutputStream;
089import com.unboundid.util.StaticUtils;
090import com.unboundid.util.ThreadSafety;
091import com.unboundid.util.ThreadSafetyLevel;
092import com.unboundid.util.args.ArgumentException;
093import com.unboundid.util.args.ArgumentParser;
094import com.unboundid.util.args.BooleanArgument;
095import com.unboundid.util.args.DNArgument;
096import com.unboundid.util.args.FileArgument;
097import com.unboundid.util.args.IntegerArgument;
098import com.unboundid.util.args.ScopeArgument;
099import com.unboundid.util.args.StringArgument;
100
101import static com.unboundid.ldif.LDIFMessages.*;
102
103
104
105/**
106 * This class provides a command-line tool that can be used to search for
107 * entries matching a given set of criteria in an LDIF file.
108 */
109@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
110public final class LDIFSearch
111       extends CommandLineTool
112{
113  /**
114   * The server root directory for the Ping Identity Directory Server (or
115   * related Ping Identity server product) that contains this tool, if
116   * applicable.
117   */
118  @Nullable private static final File PING_SERVER_ROOT =
119       InternalSDKHelper.getPingIdentityServerRoot();
120
121
122
123  /**
124   * Indicates whether the tool is running as part of a Ping Identity Directory
125   * Server (or related Ping Identity Server Product) installation.
126   */
127  private static final boolean PING_SERVER_AVAILABLE =
128       (PING_SERVER_ROOT != null);
129
130
131
132  /**
133   * The column at which to wrap long lines.
134   */
135  private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
136
137
138
139  // The argument parser for this tool.
140  @Nullable private volatile ArgumentParser parser;
141
142  // The completion message for this tool.
143  @NotNull private final AtomicReference<String> completionMessage;
144
145  // Indicates whether the LDIF encryption passphrase file has been read.
146  private volatile boolean ldifEncryptionPassphraseFileRead;
147
148  // Encryption passphrases used thus far.
149  @NotNull private final List<char[]> inputEncryptionPassphrases;
150
151  // The list of LDAP URLs to use when processing searches, mapped to the
152  // corresponding search entry parers.
153  @NotNull private final List<LDAPURL> searchURLs;
154
155  // The LDAP result writer for this tool.
156  @NotNull private volatile LDAPResultWriter resultWriter;
157
158  // The command-line arguments supported by this tool.
159  @Nullable private BooleanArgument checkSchema;
160  @Nullable private BooleanArgument compressOutput;
161  @Nullable private BooleanArgument doNotWrap;
162  @Nullable private BooleanArgument encryptOutput;
163  @Nullable private BooleanArgument isCompressed;
164  @Nullable private BooleanArgument overwriteExistingOutputFile;
165  @Nullable private BooleanArgument separateOutputFilePerSearch;
166  @Nullable private BooleanArgument stripTrailingSpaces;
167  @Nullable private DNArgument baseDN;
168  @Nullable private FileArgument filterFile;
169  @Nullable private FileArgument ldapURLFile;
170  @Nullable private FileArgument ldifEncryptionPassphraseFile;
171  @Nullable private FileArgument ldifFile;
172  @Nullable private FileArgument outputFile;
173  @Nullable private FileArgument outputEncryptionPassphraseFile;
174  @Nullable private FileArgument schemaPath;
175  @Nullable private IntegerArgument sizeLimit;
176  @Nullable private IntegerArgument timeLimitSeconds;
177  @Nullable private IntegerArgument wrapColumn;
178  @Nullable private ScopeArgument scope;
179  @Nullable private StringArgument outputFormat = null;
180
181
182
183  /**
184   * Invokes this tool with the provided set of command-line arguments.
185   *
186   * @param  args  The set of arguments provided to this tool.  It may be
187   *               empty but must not be {@code null}.
188   */
189  public static void main(@NotNull final String... args)
190  {
191    final ResultCode resultCode = main(System.out, System.err, args);
192    if (resultCode != ResultCode.SUCCESS)
193    {
194      System.exit(resultCode.intValue());
195    }
196  }
197
198
199
200  /**
201   * Invokes this tool with the provided set of command-line arguments, using
202   * the given output and error streams.
203   *
204   * @param  out   The output stream to use for standard output.  It may be
205   *               {@code null} if standard output should be suppressed.
206   * @param  err   The output stream to use for standard error.  It may be
207   *               {@code null} if standard error should be suppressed.
208   * @param  args  The set of arguments provided to this tool.  It may be
209   *               empty but must not be {@code null}.
210   *
211   * @return  A result code indicating the status of processing.  Any result
212   *          code other than {@link ResultCode#SUCCESS} should be considered
213   *          an error.
214   */
215  @NotNull()
216  public static ResultCode main(@Nullable final OutputStream out,
217                                @Nullable final OutputStream err,
218                                @NotNull final String... args)
219  {
220    final LDIFSearch tool = new LDIFSearch(out, err);
221    return tool.runTool(args);
222  }
223
224
225
226  /**
227   * Creates a new instance of this tool with the provided output and error
228   * streams.
229   *
230   * @param  out  The output stream to use for standard output.  It may be
231   *              {@code null} if standard output should be suppressed.
232   * @param  err  The output stream to use for standard error.  It may be
233   *              {@code null} if standard error should be suppressed.
234   */
235  public LDIFSearch(@Nullable final OutputStream out,
236                    @Nullable final OutputStream err)
237  {
238    super(out, err);
239
240    resultWriter = new LDIFLDAPResultWriter(getOut(), WRAP_COLUMN);
241
242    parser = null;
243    completionMessage = new AtomicReference<>();
244    inputEncryptionPassphrases = new ArrayList<>(5);
245    searchURLs = new ArrayList<>();
246    ldifEncryptionPassphraseFileRead = false;
247
248    checkSchema = null;
249    compressOutput = null;
250    doNotWrap = null;
251    encryptOutput = null;
252    isCompressed = null;
253    overwriteExistingOutputFile = null;
254    separateOutputFilePerSearch = null;
255    stripTrailingSpaces = null;
256    baseDN = null;
257    filterFile = null;
258    ldapURLFile = null;
259    ldifEncryptionPassphraseFile = null;
260    ldifFile = null;
261    outputFile = null;
262    outputFormat = null;
263    outputEncryptionPassphraseFile = null;
264    schemaPath = null;
265    sizeLimit = null;
266    timeLimitSeconds = null;
267    wrapColumn = null;
268    scope = null;
269  }
270
271
272
273  /**
274   * {@inheritDoc}
275   */
276  @Override()
277  @NotNull()
278  public String getToolName()
279  {
280    return "ldifsearch";
281  }
282
283
284
285  /**
286   * {@inheritDoc}
287   */
288  @Override()
289  @NotNull()
290  public String getToolDescription()
291  {
292    return INFO_LDIFSEARCH_TOOL_DESCRIPTION.get();
293  }
294
295
296
297  /**
298   * {@inheritDoc}
299   */
300  @Override()
301  @NotNull()
302  public String getToolVersion()
303  {
304    return Version.NUMERIC_VERSION_STRING;
305  }
306
307
308
309  /**
310   * {@inheritDoc}
311   */
312  @Override()
313  public int getMinTrailingArguments()
314  {
315    return 0;
316  }
317
318
319
320  /**
321   * {@inheritDoc}
322   */
323  @Override()
324  public int getMaxTrailingArguments()
325  {
326    return -1;
327  }
328
329
330
331  /**
332   * {@inheritDoc}
333   */
334  @Override()
335  @NotNull()
336  public String getTrailingArgumentsPlaceholder()
337  {
338    return INFO_LDIFSEARCH_TRAILING_ARGS_PLACEHOLDER.get();
339  }
340
341
342
343  /**
344   * {@inheritDoc}
345   */
346  @Override()
347  public boolean supportsInteractiveMode()
348  {
349    return true;
350  }
351
352
353
354  /**
355   * {@inheritDoc}
356   */
357  @Override()
358  public boolean defaultsToInteractiveMode()
359  {
360    return true;
361  }
362
363
364
365  /**
366   * {@inheritDoc}
367   */
368  @Override()
369  public boolean supportsPropertiesFile()
370  {
371    return true;
372  }
373
374
375
376  /**
377   * {@inheritDoc}
378   */
379  @Override()
380  @Nullable()
381  protected String getToolCompletionMessage()
382  {
383    return completionMessage.get();
384  }
385
386
387
388  /**
389   * {@inheritDoc}
390   */
391  @Override()
392  public void addToolArguments(@NotNull final ArgumentParser parser)
393         throws ArgumentException
394  {
395    this.parser = parser;
396
397
398    ldifFile = new FileArgument('l', "ldifFile", true, 0, null,
399         INFO_LDIFSEARCH_ARG_DESC_LDIF_FILE.get(), true, true, true, false);
400    ldifFile.addLongIdentifier("ldif-file", true);
401    ldifFile.addLongIdentifier("inputFile", true);
402    ldifFile.addLongIdentifier("input-file", true);
403    ldifFile.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_INPUT.get());
404    parser.addArgument(ldifFile);
405
406
407    final String ldifPWDesc;
408    if (PING_SERVER_AVAILABLE)
409    {
410      ldifPWDesc = INFO_LDIFSEARCH_ARG_DESC_LDIF_PW_FILE_PING_SERVER.get();
411    }
412    else
413    {
414      ldifPWDesc = INFO_LDIFSEARCH_ARG_DESC_LDIF_PW_FILE_STANDALONE.get();
415    }
416    ldifEncryptionPassphraseFile = new FileArgument(null,
417         "ldifEncryptionPassphraseFile", false, 1, null, ldifPWDesc, true,
418         true, true, false);
419    ldifEncryptionPassphraseFile.addLongIdentifier(
420         "ldif-encryption-passphrase-file", true);
421    ldifEncryptionPassphraseFile.addLongIdentifier("ldifPassphraseFile", true);
422    ldifEncryptionPassphraseFile.addLongIdentifier("ldif-passphrase-file",
423         true);
424    ldifEncryptionPassphraseFile.addLongIdentifier("ldifEncryptionPasswordFile",
425         true);
426    ldifEncryptionPassphraseFile.addLongIdentifier(
427         "ldif-encryption-password-file", true);
428    ldifEncryptionPassphraseFile.addLongIdentifier("ldifPasswordFile", true);
429    ldifEncryptionPassphraseFile.addLongIdentifier("ldif-password-file", true);
430    ldifEncryptionPassphraseFile.addLongIdentifier(
431         "inputEncryptionPassphraseFile", true);
432    ldifEncryptionPassphraseFile.addLongIdentifier(
433         "input-encryption-passphrase-file", true);
434    ldifEncryptionPassphraseFile.addLongIdentifier("inputPassphraseFile", true);
435    ldifEncryptionPassphraseFile.addLongIdentifier("input-passphrase-file",
436         true);
437    ldifEncryptionPassphraseFile.addLongIdentifier(
438         "inputEncryptionPasswordFile", true);
439    ldifEncryptionPassphraseFile.addLongIdentifier(
440         "input-encryption-password-file", true);
441    ldifEncryptionPassphraseFile.addLongIdentifier("inputPasswordFile", true);
442    ldifEncryptionPassphraseFile.addLongIdentifier("input-password-file", true);
443    ldifEncryptionPassphraseFile.setArgumentGroupName(
444         INFO_LDIFSEARCH_ARG_GROUP_INPUT.get());
445    parser.addArgument(ldifEncryptionPassphraseFile);
446
447
448    stripTrailingSpaces = new BooleanArgument(null, "stripTrailingSpaces", 1,
449         INFO_LDIFSEARCH_ARG_DESC_STRIP_TRAILING_SPACES.get());
450    stripTrailingSpaces.addLongIdentifier("strip-trailing-spaces", true);
451    stripTrailingSpaces.addLongIdentifier("ignoreTrailingSpaces", true);
452    stripTrailingSpaces.addLongIdentifier("ignore-trailing-spaces", true);
453    stripTrailingSpaces.setArgumentGroupName(
454         INFO_LDIFSEARCH_ARG_GROUP_INPUT.get());
455    parser.addArgument(stripTrailingSpaces);
456
457
458    final String schemaPathDesc;
459    if (PING_SERVER_AVAILABLE)
460    {
461      schemaPathDesc = INFO_LDIFSEARCH_ARG_DESC_SCHEMA_PATH_PING_SERVER.get();
462    }
463    else
464    {
465      schemaPathDesc = INFO_LDIFSEARCH_ARG_DESC_SCHEMA_PATH_STANDALONE.get();
466    }
467    schemaPath = new FileArgument(null, "schemaPath", false, 0, null,
468         schemaPathDesc, true, true, false, false);
469    schemaPath.addLongIdentifier("schema-path", true);
470    schemaPath.addLongIdentifier("schemaFile", true);
471    schemaPath.addLongIdentifier("schema-file", true);
472    schemaPath.addLongIdentifier("schemaDirectory", true);
473    schemaPath.addLongIdentifier("schema-directory", true);
474    schemaPath.addLongIdentifier("schema", true);
475    schemaPath.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_INPUT.get());
476    parser.addArgument(schemaPath);
477
478
479    checkSchema = new BooleanArgument(null, "checkSchema", 1,
480         INFO_LDIFSEARCH_ARG_DESC_CHECK_SCHEMA.get());
481    checkSchema.addLongIdentifier("check-schema", true);
482    checkSchema.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_INPUT.get());
483    parser.addArgument(checkSchema);
484
485
486    isCompressed = new BooleanArgument(null, "isCompressed", 1,
487         INFO_LDIFSEARCH_ARG_DESC_IS_COMPRESSED.get());
488    isCompressed.addLongIdentifier("is-compressed", true);
489    isCompressed.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_INPUT.get());
490    isCompressed.setHidden(true);
491    parser.addArgument(isCompressed);
492
493
494    outputFile = new FileArgument('o', "outputFile", false, 1, null,
495         INFO_LDIFSEARCH_ARG_DESC_OUTPUT_FILE.get(), false, true, true, false);
496    outputFile.addLongIdentifier("output-file", true);
497    outputFile.addLongIdentifier("outputLDIF", true);
498    outputFile.addLongIdentifier("output-ldif", true);
499    outputFile.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get());
500    parser.addArgument(outputFile);
501
502
503    separateOutputFilePerSearch = new BooleanArgument(null,
504         "separateOutputFilePerSearch", 1,
505         INFO_LDIFSEARCH_ARG_DESC_SEPARATE_OUTPUT_FILES.get());
506    separateOutputFilePerSearch.addLongIdentifier(
507         "separate-output-file-per-search", true);
508    separateOutputFilePerSearch.addLongIdentifier("separateOutputFiles", true);
509    separateOutputFilePerSearch.addLongIdentifier("separate-output-files",
510         true);
511    separateOutputFilePerSearch.setArgumentGroupName(
512         INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get());
513    parser.addArgument(separateOutputFilePerSearch);
514
515
516    compressOutput = new BooleanArgument(null, "compressOutput", 1,
517         INFO_LDIFSEARCH_ARG_DESC_COMPRESS_OUTPUT.get());
518    compressOutput.addLongIdentifier("compress-output", true);
519    compressOutput.addLongIdentifier("compressLDIF", true);
520    compressOutput.addLongIdentifier("compress-ldif", true);
521    compressOutput.addLongIdentifier("compress", true);
522    compressOutput.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get());
523    parser.addArgument(compressOutput);
524
525
526    encryptOutput = new BooleanArgument(null, "encryptOutput", 1,
527         INFO_LDIFSEARCH_ARG_DESC_ENCRYPT_OUTPUT.get());
528    encryptOutput.addLongIdentifier("encrypt-output", true);
529    encryptOutput.addLongIdentifier("encryptLDIF", true);
530    encryptOutput.addLongIdentifier("encrypt-ldif", true);
531    encryptOutput.addLongIdentifier("encrypt", true);
532    encryptOutput.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get());
533    parser.addArgument(encryptOutput);
534
535
536    outputEncryptionPassphraseFile = new FileArgument(null,
537         "outputEncryptionPassphraseFile", false, 1, null,
538         INFO_LDIFSEARCH_ARG_DESC_OUTPUT_PW_FILE.get(), true, true, true,
539         false);
540    outputEncryptionPassphraseFile.addLongIdentifier(
541         "output-encryption-passphrase-file", true);
542    outputEncryptionPassphraseFile.addLongIdentifier("outputPassphraseFile",
543         true);
544    outputEncryptionPassphraseFile.addLongIdentifier("output-passphrase-file",
545         true);
546    outputEncryptionPassphraseFile.addLongIdentifier(
547         "outputEncryptionPasswordFile", true);
548    outputEncryptionPassphraseFile.addLongIdentifier(
549         "output-encryption-password-file", true);
550    outputEncryptionPassphraseFile.addLongIdentifier("outputPasswordFile",
551         true);
552    outputEncryptionPassphraseFile.addLongIdentifier("output-password-file",
553         true);
554    outputEncryptionPassphraseFile.addLongIdentifier(
555         "outputEncryptionPasswordFile", true);
556    outputEncryptionPassphraseFile.addLongIdentifier(
557         "output-encryption-password-file", true);
558    outputEncryptionPassphraseFile.addLongIdentifier("outputPasswordFile",
559         true);
560    outputEncryptionPassphraseFile.addLongIdentifier("output-password-file",
561         true);
562    outputEncryptionPassphraseFile.setArgumentGroupName(
563         INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get());
564    parser.addArgument(outputEncryptionPassphraseFile);
565
566
567    overwriteExistingOutputFile = new BooleanArgument('O',
568         "overwriteExistingOutputFile", 1,
569         INFO_LDIFSEARCH_ARG_DESC_OVERWRITE_EXISTING.get());
570    overwriteExistingOutputFile.addLongIdentifier(
571         "overwrite-existing-output-file", true);
572    overwriteExistingOutputFile.addLongIdentifier(
573         "overwriteExistingOutputFiles", true);
574    overwriteExistingOutputFile.addLongIdentifier(
575         "overwrite-existing-output-files", true);
576    overwriteExistingOutputFile.addLongIdentifier("overwriteExistingOutput",
577         true);
578    overwriteExistingOutputFile.addLongIdentifier("overwrite-existing-output",
579         true);
580    overwriteExistingOutputFile.addLongIdentifier("overwriteExisting", true);
581    overwriteExistingOutputFile.addLongIdentifier("overwrite-existing", true);
582    overwriteExistingOutputFile.addLongIdentifier("overwrite", true);
583    overwriteExistingOutputFile.setArgumentGroupName(
584         INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get());
585    parser.addArgument(overwriteExistingOutputFile);
586
587
588    final Set<String> outputFormatAllowedValues = StaticUtils.setOf("ldif",
589         "json", "csv", "multi-valued-csv", "tab-delimited",
590         "multi-valued-tab-delimited", "dns-only", "values-only");
591    outputFormat = new StringArgument(null, "outputFormat", false, 1,
592         "{ldif|json|csv|multi-valued-csv|tab-delimited|" +
593              "multi-valued-tab-delimited|dns-only|values-only}",
594         INFO_LDIFSEARCH_ARG_DESC_OUTPUT_FORMAT.get(),
595         outputFormatAllowedValues, "ldif");
596    outputFormat.addLongIdentifier("output-format", true);
597    outputFormat.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get());
598    parser.addArgument(outputFormat);
599
600
601    wrapColumn = new IntegerArgument(null, "wrapColumn", false, 1, null,
602         INFO_LDIFSEARCH_ARG_DESC_WRAP_COLUMN.get(), 5, Integer.MAX_VALUE);
603    wrapColumn.addLongIdentifier("wrap-column", true);
604    wrapColumn.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get());
605    parser.addArgument(wrapColumn);
606
607
608    doNotWrap = new BooleanArgument('T', "doNotWrap", 1,
609         INFO_LDIFSEARCH_ARG_DESC_DO_NOT_WRAP.get());
610    doNotWrap.addLongIdentifier("do-not-wrap", true);
611    doNotWrap.addLongIdentifier("dontWrap", true);
612    doNotWrap.addLongIdentifier("dont-wrap", true);
613    doNotWrap.addLongIdentifier("noWrap", true);
614    doNotWrap.addLongIdentifier("no-wrap", true);
615    doNotWrap.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get());
616    parser.addArgument(doNotWrap);
617
618
619    baseDN = new DNArgument('b', "baseDN", false, 1, null,
620         INFO_LDIFSEARCH_ARG_DESC_BASE_DN.get());
621    baseDN.addLongIdentifier("base-dn", true);
622    baseDN.addLongIdentifier("searchBaseDN", true);
623    baseDN.addLongIdentifier("search-base-dn", true);
624    baseDN.addLongIdentifier("searchBase", true);
625    baseDN.addLongIdentifier("search-base", true);
626    baseDN.addLongIdentifier("base", true);
627    baseDN.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get());
628    parser.addArgument(baseDN);
629
630
631    scope = new ScopeArgument('s', "scope", false, null,
632         INFO_LDIFSEARCH_ARG_DESC_SCOPE.get());
633    scope.addLongIdentifier("searchScope", true);
634    scope.addLongIdentifier("search-scope", true);
635    scope.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get());
636    parser.addArgument(scope);
637
638
639    filterFile = new FileArgument('f', "filterFile", false, 0, null,
640         INFO_LDIFSEARCH_ARG_DESC_FILTER_FILE.get(), true, true, true, false);
641    filterFile.addLongIdentifier("filter-file", true);
642    filterFile.addLongIdentifier("filtersFile", true);
643    filterFile.addLongIdentifier("filters-file", true);
644    filterFile.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get());
645    parser.addArgument(filterFile);
646
647
648    ldapURLFile = new FileArgument(null, "ldapURLFile", false, 0, null,
649         INFO_LDIFSEARCH_ARG_DESC_LDAP_URL_FILE.get(), true, true, true, false);
650    ldapURLFile.addLongIdentifier("ldap-url-file", true);
651    ldapURLFile.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get());
652    parser.addArgument(ldapURLFile);
653
654
655    sizeLimit = new IntegerArgument('z', "sizeLimit", false, 1, null,
656         INFO_LDIFSEARCH_ARG_DESC_SIZE_LIMIT.get(), 0, Integer.MAX_VALUE, 0);
657    sizeLimit.addLongIdentifier("size-limit", true);
658    sizeLimit.addLongIdentifier("searchSizeLimit", true);
659    sizeLimit.addLongIdentifier("search-size-limit", true);
660    sizeLimit.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get());
661    sizeLimit.setHidden(true);
662    parser.addArgument(sizeLimit);
663
664
665    timeLimitSeconds = new IntegerArgument('t', "timeLimitSeconds", false, 1,
666         null, INFO_LDIFSEARCH_ARG_DESC_TIME_LIMIT_SECONDS.get(), 0,
667         Integer.MAX_VALUE, 0);
668    timeLimitSeconds.addLongIdentifier("time-limit-seconds", true);
669    timeLimitSeconds.addLongIdentifier("timeLimit", true);
670    timeLimitSeconds.setArgumentGroupName(
671         INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get());
672    timeLimitSeconds.setHidden(true);
673    parser.addArgument(timeLimitSeconds);
674
675
676    parser.addDependentArgumentSet(separateOutputFilePerSearch, outputFile);
677    parser.addDependentArgumentSet(compressOutput, outputFile);
678    parser.addDependentArgumentSet(encryptOutput, outputFile);
679    parser.addDependentArgumentSet(overwriteExistingOutputFile, outputFile);
680    parser.addDependentArgumentSet(outputEncryptionPassphraseFile,
681         encryptOutput);
682
683    parser.addExclusiveArgumentSet(wrapColumn, doNotWrap);
684    parser.addExclusiveArgumentSet(baseDN, ldapURLFile);
685    parser.addExclusiveArgumentSet(scope, ldapURLFile);
686    parser.addExclusiveArgumentSet(filterFile, ldapURLFile);
687    parser.addExclusiveArgumentSet(outputFormat, separateOutputFilePerSearch);
688  }
689
690
691
692  /**
693   * {@inheritDoc}
694   */
695  @Override()
696  public void doExtendedArgumentValidation()
697         throws ArgumentException
698  {
699    // If the output file exists and either compressOutput or encryptOutput is
700    // present, then the overwrite argument must also be present.
701    final File outFile = outputFile.getValue();
702    if ((outFile != null) && outFile.exists() &&
703         (compressOutput.isPresent() || encryptOutput.isPresent()) &&
704         (! overwriteExistingOutputFile.isPresent()))
705    {
706      throw new ArgumentException(
707           ERR_LDIFSEARCH_APPEND_WITH_COMPRESSION_OR_ENCRYPTION.get(
708                compressOutput.getIdentifierString(),
709                encryptOutput.getIdentifierString(),
710                overwriteExistingOutputFile.getIdentifierString()));
711    }
712
713
714    // Create the set of LDAP URLs to use when issuing the searches.
715    final List<String> trailingArgs = parser.getTrailingArguments();
716    final List<String> requestedAttributes = new ArrayList<>();
717    if (filterFile.isPresent())
718    {
719      // If there are trailing arguments, then make sure the first one is not a
720      // valid filter.
721      if (! trailingArgs.isEmpty())
722      {
723        try
724        {
725          Filter.create(trailingArgs.get(0));
726          throw new ArgumentException(
727               ERR_LDIFSEARCH_FILTER_FILE_WITH_TRAILING_FILTER.get());
728        }
729        catch (final LDAPException e)
730        {
731          // This was expected.
732        }
733      }
734
735      requestedAttributes.addAll(trailingArgs);
736      readFilterFile();
737    }
738    else if (ldapURLFile.isPresent())
739    {
740      // Make sure there aren't any trailing arguments.
741      if (! trailingArgs.isEmpty())
742      {
743        throw new ArgumentException(
744             ERR_LDIFSEARCH_LDAP_URL_FILE_WITH_TRAILING_ARGS.get());
745      }
746
747      readLDAPURLFile();
748
749
750      // If there are multiple LDAP URLs, and if they should not be sent to
751      // separate output files, then they must all have the same set of
752      // requested attributes.
753      if ((searchURLs.size() > 1) &&
754           (! separateOutputFilePerSearch.isPresent()))
755      {
756        final Iterator<LDAPURL> iterator = searchURLs.iterator();
757        final Set<String> requestedAttrs =
758             new HashSet<>(Arrays.asList(iterator.next().getAttributes()));
759        while (iterator.hasNext())
760        {
761          final Set<String> attrSet = new HashSet<>(Arrays.asList(
762               iterator.next().getAttributes()));
763          if (! requestedAttrs.equals(attrSet))
764          {
765            throw new ArgumentException(
766                 ERR_LDIFSEARCH_DIFFERENT_URL_ATTRS_IN_SAME_FILE.get(
767                      ldapURLFile.getIdentifierString(),
768                      separateOutputFilePerSearch.getIdentifierString()));
769          }
770        }
771      }
772    }
773    else
774    {
775      // Make sure there is at least one trailing argument, and that it's a
776      // valid filter.  If there are any others, then they must be the
777      // requested arguments.
778      if (trailingArgs.isEmpty())
779      {
780        throw new ArgumentException(ERR_LDIFSEARCH_NO_FILTER.get());
781      }
782
783
784      final Filter filter;
785      try
786      {
787        final List<String> trailingArgList = new ArrayList<>(trailingArgs);
788        final Iterator<String> trailingArgIterator = trailingArgList.iterator();
789        filter = Filter.create(trailingArgIterator.next());
790
791        while (trailingArgIterator.hasNext())
792        {
793          requestedAttributes.add(trailingArgIterator.next());
794        }
795      }
796      catch (final LDAPException e)
797      {
798        Debug.debugException(e);
799        throw new ArgumentException(
800             ERR_LDIFSEARCH_FIRST_TRAILING_ARG_NOT_FILTER.get(
801                  trailingArgs.get(0)),
802             e);
803      }
804
805
806      DN dn = baseDN.getValue();
807      if (dn == null)
808      {
809        dn = DN.NULL_DN;
810      }
811
812      SearchScope searchScope = scope.getValue();
813      if (searchScope == null)
814      {
815        searchScope = SearchScope.SUB;
816      }
817
818      try
819      {
820        searchURLs.add(new LDAPURL("ldap", null, null, dn,
821             requestedAttributes.toArray(StaticUtils.NO_STRINGS),
822             searchScope, filter));
823      }
824      catch (final LDAPException e)
825      {
826        Debug.debugException(e);
827        // This should never happen.
828        throw new ArgumentException(StaticUtils.getExceptionMessage(e), e);
829      }
830    }
831
832
833    // Create the result writer.
834    final String outputFormatStr =
835         StaticUtils.toLowerCase(outputFormat.getValue());
836    if (outputFormatStr.equals("json"))
837    {
838      resultWriter = new JSONLDAPResultWriter(getOut());
839    }
840    else if (outputFormatStr.equals("csv") ||
841             outputFormatStr.equals("multi-valued-csv") ||
842             outputFormatStr.equals("tab-delimited") ||
843             outputFormatStr.equals("multi-valued-tab-delimited"))
844    {
845      // These output formats cannot be used with the --ldapURLFile argument.
846      if (ldapURLFile.isPresent())
847      {
848        throw new ArgumentException(
849             ERR_LDIFSEARCH_OUTPUT_FORMAT_NOT_SUPPORTED_WITH_URLS.get(
850                  outputFormat.getValue(), ldapURLFile.getIdentifierString()));
851      }
852
853
854      // These output formats require a set of requested attributes.
855      if (requestedAttributes.isEmpty())
856      {
857        throw new ArgumentException(
858             ERR_LDIFSEARCH_OUTPUT_FORMAT_REQUIRES_REQUESTED_ATTRS.get(
859                  outputFormat.getValue()));
860      }
861
862      final OutputFormat format;
863      final boolean includeAllValues;
864      switch (outputFormatStr)
865      {
866        case "multi-valued-csv":
867          format = OutputFormat.CSV;
868          includeAllValues = true;
869          break;
870        case "tab-delimited":
871          format = OutputFormat.TAB_DELIMITED_TEXT;
872          includeAllValues = false;
873          break;
874        case "multi-valued-tab-delimited":
875          format = OutputFormat.TAB_DELIMITED_TEXT;
876          includeAllValues = true;
877          break;
878        case "csv":
879        default:
880          format = OutputFormat.CSV;
881          includeAllValues = false;
882          break;
883      }
884
885
886      resultWriter = new ColumnBasedLDAPResultWriter(getOut(),
887           format, requestedAttributes, WRAP_COLUMN, includeAllValues);
888    }
889    else if (outputFormatStr.equals("dns-only"))
890    {
891      resultWriter = new DNsOnlyLDAPResultWriter(getOut());
892    }
893    else if (outputFormatStr.equals("values-only"))
894    {
895      resultWriter = new ValuesOnlyLDAPResultWriter(getOut());
896    }
897    else
898    {
899      final int wc;
900      if (doNotWrap.isPresent())
901      {
902        wc = Integer.MAX_VALUE;
903      }
904      else if (wrapColumn.isPresent())
905      {
906        wc = wrapColumn.getValue();
907      }
908      else
909      {
910        wc = WRAP_COLUMN;
911      }
912
913      resultWriter = new LDIFLDAPResultWriter(getOut(), wc);
914    }
915  }
916
917
918
919  /**
920   * Uses the contents of any specified filter files, along with the configured
921   * base DN, scope, and requested attributes, to populate the set of search
922   * URLs.
923   *
924   * @throws  ArgumentException  If a problem is encountered while constructing
925   *                             the search URLs.
926   */
927  private void readFilterFile()
928          throws ArgumentException
929  {
930    DN dn = baseDN.getValue();
931    if (dn == null)
932    {
933      dn = DN.NULL_DN;
934    }
935
936    SearchScope searchScope = scope.getValue();
937    if (searchScope == null)
938    {
939      searchScope = SearchScope.SUB;
940    }
941
942    final String[] requestedAttributes =
943         parser.getTrailingArguments().toArray(StaticUtils.NO_STRINGS);
944
945    for (final File f : filterFile.getValues())
946    {
947      final InputStream inputStream;
948      try
949      {
950        inputStream = openInputStream(f);
951      }
952      catch (final LDAPException e)
953      {
954        Debug.debugException(e);
955        throw new ArgumentException(e.getMessage(), e);
956      }
957
958      try (BufferedReader reader =
959                new BufferedReader(new InputStreamReader(inputStream)))
960      {
961        while (true)
962        {
963          final String line = reader.readLine();
964          if (line == null)
965          {
966            break;
967          }
968
969          if (line.isEmpty() || line.startsWith("#"))
970          {
971            continue;
972          }
973
974          try
975          {
976            final Filter filter = Filter.create(line.trim());
977            searchURLs.add(new LDAPURL("ldap", null, null, dn,
978                 requestedAttributes, searchScope, filter));
979          }
980          catch (final LDAPException e)
981          {
982            Debug.debugException(e);
983            throw new ArgumentException(
984                 ERR_LDIFSEARCH_FILTER_FILE_INVALID_FILTER.get(line,
985                      f.getAbsolutePath(), e.getMessage()),
986                 e);
987          }
988        }
989      }
990      catch (final IOException e)
991      {
992        Debug.debugException(e);
993        throw new ArgumentException(
994             ERR_LDIFSEARCH_ERROR_READING_FILTER_FILE.get(f.getAbsolutePath(),
995                  StaticUtils.getExceptionMessage(e)),
996             e);
997      }
998      finally
999      {
1000        try
1001        {
1002          inputStream.close();
1003        }
1004        catch (final Exception e)
1005        {
1006          Debug.debugException(e);
1007        }
1008      }
1009
1010    }
1011
1012    if (searchURLs.isEmpty())
1013    {
1014      throw new ArgumentException(ERR_LDIFSEARCH_NO_FILTERS_FROM_FILE.get(
1015           filterFile.getValues().get(0).getAbsolutePath()));
1016    }
1017  }
1018
1019
1020
1021  /**
1022   * Uses the contents of any specified LDAP URL files to populate the set of
1023   * search URLs.
1024   *
1025   * @throws  ArgumentException  If a problem is encountered while constructing
1026   *                             the search URLs.
1027   */
1028  private void readLDAPURLFile()
1029          throws ArgumentException
1030  {
1031    for (final File f : ldapURLFile.getValues())
1032    {
1033      final InputStream inputStream;
1034      try
1035      {
1036        inputStream = openInputStream(f);
1037      }
1038      catch (final LDAPException e)
1039      {
1040        Debug.debugException(e);
1041        throw new ArgumentException(e.getMessage(), e);
1042      }
1043
1044      try (BufferedReader reader =
1045                new BufferedReader(new InputStreamReader(inputStream)))
1046      {
1047        while (true)
1048        {
1049          final String line = reader.readLine();
1050          if (line == null)
1051          {
1052            break;
1053          }
1054
1055          if (line.isEmpty() || line.startsWith("#"))
1056          {
1057            continue;
1058          }
1059
1060          try
1061          {
1062            searchURLs.add(new LDAPURL(line.trim()));
1063          }
1064          catch (final LDAPException e)
1065          {
1066            Debug.debugException(e);
1067            throw new ArgumentException(
1068                 ERR_LDIFSEARCH_LDAP_URL_FILE_INVALID_URL.get(line,
1069                      f.getAbsolutePath(), e.getMessage()),
1070                 e);
1071          }
1072        }
1073      }
1074      catch (final IOException e)
1075      {
1076        Debug.debugException(e);
1077        throw new ArgumentException(
1078             ERR_LDIFSEARCH_ERROR_READING_LDAP_URL_FILE.get(f.getAbsolutePath(),
1079                  StaticUtils.getExceptionMessage(e)),
1080             e);
1081      }
1082      finally
1083      {
1084        try
1085        {
1086          inputStream.close();
1087        }
1088        catch (final Exception e)
1089        {
1090          Debug.debugException(e);
1091        }
1092      }
1093    }
1094
1095    if (searchURLs.isEmpty())
1096    {
1097      throw new ArgumentException(ERR_LDIFSEARCH_NO_URLS_FROM_FILE.get(
1098           ldapURLFile.getValues().get(0).getAbsolutePath()));
1099    }
1100  }
1101
1102
1103
1104  /**
1105   * {@inheritDoc}
1106   */
1107  @Override()
1108  @NotNull()
1109  public ResultCode doToolProcessing()
1110  {
1111    // Get the schema to use when performing LDIF processing.
1112    final Schema schema;
1113    try
1114    {
1115      if (schemaPath.isPresent())
1116      {
1117        schema = getSchema(schemaPath.getValues());
1118      }
1119      else if (PING_SERVER_AVAILABLE)
1120      {
1121        schema = getSchema(Collections.singletonList(StaticUtils.constructPath(
1122             PING_SERVER_ROOT, "config", "schema")));
1123      }
1124      else
1125      {
1126        schema = Schema.getDefaultStandardSchema();
1127      }
1128    }
1129    catch (final Exception e)
1130    {
1131      Debug.debugException(e);
1132      logCompletionMessage(true,
1133           ERR_LDIFSEARCH_CANNOT_GET_SCHEMA.get(
1134                StaticUtils.getExceptionMessage(e)));
1135      return ResultCode.LOCAL_ERROR;
1136    }
1137
1138
1139    // Create search entry parers for all of the search URLs.
1140    final Map<LDAPURL,SearchEntryParer> urlMap = new LinkedHashMap<>();
1141    for (final LDAPURL url : searchURLs)
1142    {
1143      final SearchEntryParer parer = new SearchEntryParer(
1144           Arrays.asList(url.getAttributes()), schema);
1145      urlMap.put(url, parer);
1146    }
1147
1148
1149    // If we should check schema, then create the entry validator.
1150    final EntryValidator entryValidator;
1151    if (checkSchema.isPresent())
1152    {
1153      entryValidator = new EntryValidator(schema);
1154    }
1155    else
1156    {
1157      entryValidator = null;
1158    }
1159
1160
1161    // Create the output files, if appropriate.
1162    OutputStream outputStream = null;
1163    SearchEntryParer singleParer = null;
1164    final Map<LDAPURL,LDIFSearchSeparateSearchDetails> separateWriters =
1165         new LinkedHashMap<>();
1166    try
1167    {
1168      if (outputFile.isPresent())
1169      {
1170        final int numURLs = searchURLs.size();
1171        if (separateOutputFilePerSearch.isPresent() && (numURLs > 1))
1172        {
1173          int i=1;
1174          for (final LDAPURL url : searchURLs)
1175          {
1176            final File f = new
1177                 File(outputFile.getValue().getAbsolutePath() + '.' + i);
1178            final LDIFSearchSeparateSearchDetails details =
1179                 new LDIFSearchSeparateSearchDetails(url, f,
1180                      createLDIFWriter(f, url), schema);
1181            separateWriters.put(url, details);
1182            i++;
1183          }
1184        }
1185        else
1186        {
1187          try
1188          {
1189            outputStream = createOutputStream(outputFile.getValue());
1190            resultWriter.updateOutputStream(outputStream);
1191          }
1192          catch (final Exception e)
1193          {
1194            Debug.debugException(e);
1195            throw new LDAPException(ResultCode.LOCAL_ERROR,
1196                 ERR_LDIFSEARCH_CANNOT_WRITE_TO_FILE.get(
1197                      outputFile.getValue().getAbsolutePath(),
1198                      StaticUtils.getExceptionMessage(e)),
1199                 e);
1200          }
1201        }
1202      }
1203
1204
1205      // If we're not using separate writers, then write any appropriate header
1206      // to the top of the output.
1207      if (separateWriters.isEmpty())
1208      {
1209        resultWriter.writeHeader();
1210      }
1211
1212
1213      // Iterate through the LDIF files and process the entries they contain.
1214      boolean errorEncountered = false;
1215      final List<LDAPURL> matchingURLs = new ArrayList<>();
1216      final List<String> entryInvalidReasons = new ArrayList<>();
1217      for (final File f : ldifFile.getValues())
1218      {
1219        final LDIFReader ldifReader;
1220        try
1221        {
1222          ldifReader = new LDIFReader(openInputStream(f));
1223
1224          if (stripTrailingSpaces.isPresent())
1225          {
1226            ldifReader.setTrailingSpaceBehavior(TrailingSpaceBehavior.STRIP);
1227          }
1228          else
1229          {
1230            ldifReader.setTrailingSpaceBehavior(TrailingSpaceBehavior.REJECT);
1231          }
1232        }
1233        catch (final Exception e)
1234        {
1235          Debug.debugException(e);
1236          logCompletionMessage(true,
1237               ERR_LDIFSEARCH_CANNOT_OPEN_LDIF_FILE.get(f.getName(),
1238                    StaticUtils.getExceptionMessage(e)));
1239          return ResultCode.LOCAL_ERROR;
1240        }
1241
1242        try
1243        {
1244          while (true)
1245          {
1246            final Entry entry;
1247            try
1248            {
1249              entry = ldifReader.readEntry();
1250            }
1251            catch (final LDIFException e)
1252            {
1253              Debug.debugException(e);
1254              if (e.mayContinueReading())
1255              {
1256                commentToErr(ERR_LDIFSEARCH_RECOVERABLE_READ_ERROR.get(
1257                     f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)));
1258                errorEncountered = true;
1259                continue;
1260              }
1261              else
1262              {
1263                logCompletionMessage(true,
1264                     ERR_LDIFSEARCH_UNRECOVERABLE_READ_ERROR.get(
1265                          f.getAbsolutePath(),
1266                          StaticUtils.getExceptionMessage(e)));
1267                return ResultCode.LOCAL_ERROR;
1268              }
1269            }
1270            catch (final Exception e)
1271            {
1272              logCompletionMessage(true,
1273                   ERR_LDIFSEARCH_UNRECOVERABLE_READ_ERROR.get(
1274                        f.getAbsolutePath(),
1275                        StaticUtils.getExceptionMessage(e)));
1276              return ResultCode.LOCAL_ERROR;
1277            }
1278
1279            if (entry == null)
1280            {
1281              break;
1282            }
1283
1284            if (entryValidator != null)
1285            {
1286              entryInvalidReasons.clear();
1287              if (! entryValidator.entryIsValid(entry, entryInvalidReasons))
1288              {
1289                commentToErr(ERR_LDIFSEARCH_ENTRY_VIOLATES_SCHEMA.get(
1290                     entry.getDN()));
1291                for (final String invalidReason : entryInvalidReasons)
1292                {
1293                  commentToErr("- " + invalidReason);
1294                }
1295
1296                err();
1297                errorEncountered = true;
1298                continue;
1299              }
1300            }
1301
1302            if (separateWriters.isEmpty())
1303            {
1304              matchingURLs.clear();
1305              for (final LDAPURL url : searchURLs)
1306              {
1307                if (urlMatchesEntry(url, entry))
1308                {
1309                  matchingURLs.add(url);
1310                }
1311              }
1312
1313              if (matchingURLs.isEmpty())
1314              {
1315                continue;
1316              }
1317
1318              try
1319              {
1320                if (searchURLs.size() > 1)
1321                {
1322                  resultWriter.writeComment(
1323                       INFO_LDIFSEARCH_ENTRY_MATCHES_URLS.get(entry.getDN()));
1324                  for (final LDAPURL url : matchingURLs)
1325                  {
1326                    resultWriter.writeComment(url.toString());
1327                  }
1328                }
1329
1330                if (singleParer == null)
1331                {
1332                  singleParer = new SearchEntryParer(
1333                       Arrays.asList(searchURLs.get(0).getAttributes()),
1334                       schema);
1335                }
1336
1337                resultWriter.writeSearchResultEntry(
1338                     new SearchResultEntry(singleParer.pareEntry(entry)));
1339
1340                if (! outputFile.isPresent())
1341                {
1342                  resultWriter.flush();
1343                }
1344              }
1345              catch (final Exception e)
1346              {
1347                Debug.debugException(e);
1348                if (outputFile.isPresent())
1349                {
1350                  logCompletionMessage(true,
1351                       ERR_LDIFSEARCH_WRITE_ERROR_WITH_FILE.get(entry.getDN(),
1352                            outputFile.getValue().getAbsolutePath(),
1353                            StaticUtils.getExceptionMessage(e)));
1354                }
1355                else
1356                {
1357                  logCompletionMessage(true,
1358                       ERR_LDIFSEARCH_WRITE_ERROR_NO_FILE.get(entry.getDN(),
1359                            StaticUtils.getExceptionMessage(e)));
1360                }
1361                return ResultCode.LOCAL_ERROR;
1362              }
1363            }
1364            else
1365            {
1366              for (final LDIFSearchSeparateSearchDetails details :
1367                   separateWriters.values())
1368              {
1369                final LDAPURL url = details.getLDAPURL();
1370                if (urlMatchesEntry(url, entry))
1371                {
1372                  try
1373                  {
1374                    final Entry paredEntry =
1375                         details.getSearchEntryParer().pareEntry(entry);
1376                    details.getLDIFWriter().writeEntry(paredEntry);
1377                  }
1378                  catch (final Exception ex)
1379                  {
1380                    Debug.debugException(ex);
1381                    logCompletionMessage(true,
1382                         ERR_LDIFSEARCH_WRITE_ERROR_WITH_FILE.get(entry.getDN(),
1383                              details.getOutputFile().getAbsolutePath(),
1384                              StaticUtils.getExceptionMessage(ex)));
1385                    return ResultCode.LOCAL_ERROR;
1386                  }
1387                }
1388              }
1389            }
1390          }
1391        }
1392        finally
1393        {
1394          try
1395          {
1396            ldifReader.close();
1397          }
1398          catch (final Exception e)
1399          {
1400            Debug.debugException(e);
1401          }
1402        }
1403      }
1404
1405      if (errorEncountered)
1406      {
1407        logCompletionMessage(true,
1408             WARN_LDIFSEARCH_COMPLETED_WITH_ERRORS.get());
1409        return ResultCode.PARAM_ERROR;
1410      }
1411      else
1412      {
1413        logCompletionMessage(false,
1414             INFO_LDIFSEARCH_COMPLETED_SUCCESSFULLY.get());
1415        return ResultCode.SUCCESS;
1416      }
1417    }
1418    catch (final LDAPException e)
1419    {
1420      Debug.debugException(e);
1421      logCompletionMessage(true, e.getMessage());
1422      return e.getResultCode();
1423    }
1424    finally
1425    {
1426      try
1427      {
1428        resultWriter.flush();
1429        if (outputStream != null)
1430        {
1431          outputStream.close();
1432        }
1433      }
1434      catch (final Exception e)
1435      {
1436        Debug.debugException(e);
1437      }
1438
1439      for (final LDIFSearchSeparateSearchDetails details :
1440           separateWriters.values())
1441      {
1442        try
1443        {
1444          details.getLDIFWriter().close();
1445        }
1446        catch (final Exception e)
1447        {
1448          Debug.debugException(e);
1449        }
1450      }
1451    }
1452  }
1453
1454
1455
1456  /**
1457   * Retrieves the schema contained in the specified paths.
1458   *
1459   * @param  paths  The paths to use to access the schema.
1460   *
1461   * @return  The schema read from the specified files.
1462   *
1463   * @throws  Exception  If a problem is encountered while loading the schema.
1464   */
1465  @NotNull()
1466  private static Schema getSchema(@NotNull final List<File> paths)
1467          throws Exception
1468  {
1469    final Set<File> schemaFiles = new LinkedHashSet<>();
1470    for (final File f : paths)
1471    {
1472      if (f.exists())
1473      {
1474        if (f.isFile())
1475        {
1476          schemaFiles.add(f);
1477        }
1478        else if (f.isDirectory())
1479        {
1480          final TreeMap<String,File> sortedFiles = new TreeMap<>();
1481          for (final File fileInDir : f.listFiles())
1482          {
1483            if (fileInDir.isFile())
1484            {
1485              sortedFiles.put(fileInDir.getName(), fileInDir);
1486            }
1487          }
1488
1489          schemaFiles.addAll(sortedFiles.values());
1490        }
1491      }
1492    }
1493
1494    return Schema.getSchema(new ArrayList<>(schemaFiles));
1495  }
1496
1497
1498
1499  /**
1500   * Opens the input stream to use to read from the specified file.
1501   *
1502   * @param  f  The file for which to open the input stream.  It may optionally
1503   *            be compressed and/or encrypted.
1504   *
1505   * @return  The input stream that was created.
1506   *
1507   * @throws  LDAPException  If a problem is encountered while opening the file.
1508   */
1509  @NotNull()
1510  private InputStream openInputStream(@NotNull final File f)
1511          throws LDAPException
1512  {
1513    if (ldifEncryptionPassphraseFile.isPresent() &&
1514       (! ldifEncryptionPassphraseFileRead))
1515    {
1516      readPassphraseFile(ldifEncryptionPassphraseFile.getValue());
1517      ldifEncryptionPassphraseFileRead = true;
1518    }
1519
1520
1521    boolean closeStream = true;
1522    InputStream inputStream = null;
1523    try
1524    {
1525      inputStream = new FileInputStream(f);
1526
1527      final ObjectPair<InputStream,char[]> p =
1528           ToolUtils.getPossiblyPassphraseEncryptedInputStream(
1529                inputStream, inputEncryptionPassphrases,
1530                (! ldifEncryptionPassphraseFile.isPresent()),
1531                INFO_LDIFSEARCH_ENTER_ENCRYPTION_PW.get(f.getName()),
1532                ERR_LDIFSEARCH_WRONG_ENCRYPTION_PW.get(), getOut(), getErr());
1533      inputStream = p.getFirst();
1534      addPassphrase(p.getSecond());
1535
1536      inputStream = ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream);
1537      closeStream = false;
1538      return inputStream;
1539    }
1540    catch (final Exception e)
1541    {
1542      Debug.debugException(e);
1543      throw new LDAPException(ResultCode.LOCAL_ERROR,
1544           ERR_LDIFSEARCH_ERROR_OPENING_INPUT_FILE.get(f.getAbsolutePath(),
1545                StaticUtils.getExceptionMessage(e)),
1546           e);
1547    }
1548    finally
1549    {
1550      if ((inputStream != null) && closeStream)
1551      {
1552        try
1553        {
1554          inputStream.close();
1555        }
1556        catch (final Exception e)
1557        {
1558          Debug.debugException(e);
1559        }
1560      }
1561    }
1562  }
1563
1564
1565
1566  /**
1567   * Reads the contents of the specified passphrase file and adds it to the list
1568   * of passphrases.
1569   *
1570   * @param  f  The passphrase file to read.
1571   *
1572   * @throws  LDAPException  If a problem is encountered while trying to read
1573   *                         the passphrase from the provided file.
1574   */
1575  private void readPassphraseFile(@NotNull final File f)
1576          throws LDAPException
1577  {
1578    try
1579    {
1580      addPassphrase(getPasswordFileReader().readPassword(f));
1581    }
1582    catch (final Exception e)
1583    {
1584      Debug.debugException(e);
1585      throw new LDAPException(ResultCode.LOCAL_ERROR,
1586           ERR_LDIFSEARCH_CANNOT_READ_PW_FILE.get(f.getAbsolutePath(),
1587                StaticUtils.getExceptionMessage(e)),
1588           e);
1589    }
1590  }
1591
1592
1593
1594  /**
1595   * Updates the list of encryption passphrases with the provided passphrase, if
1596   * it is not already present.
1597   *
1598   * @param  passphrase  The passphrase to be added.  It may optionally be
1599   *                     {@code null} (in which case no action will be taken).
1600   */
1601  private void addPassphrase(@Nullable final char[] passphrase)
1602  {
1603    if (passphrase == null)
1604    {
1605      return;
1606    }
1607
1608    for (final char[] existingPassphrase : inputEncryptionPassphrases)
1609    {
1610      if (Arrays.equals(existingPassphrase, passphrase))
1611      {
1612        return;
1613      }
1614    }
1615
1616    inputEncryptionPassphrases.add(passphrase);
1617  }
1618
1619
1620
1621  /**
1622   * Creates an output stream that may be used to write to the specified file.
1623   *
1624   * @param  f  The file to be written.
1625   *
1626   * @return  The output stream that was created.
1627   *
1628   * @throws  LDAPException  If a problem occurs while creating the output
1629   *                         stream.
1630   */
1631  @NotNull()
1632  private OutputStream createOutputStream(@NotNull final File f)
1633          throws LDAPException
1634  {
1635    OutputStream outputStream = null;
1636    boolean closeOutputStream = true;
1637    try
1638    {
1639      try
1640      {
1641
1642        outputStream = new FileOutputStream(f,
1643             (! overwriteExistingOutputFile.isPresent()));
1644      }
1645      catch (final Exception e)
1646      {
1647        Debug.debugException(e);
1648        throw new LDAPException(ResultCode.LOCAL_ERROR,
1649             ERR_LDIFSEARCH_CANNOT_OPEN_OUTPUT_FILE.get(f.getAbsolutePath(),
1650                  StaticUtils.getExceptionMessage(e)),
1651             e);
1652      }
1653
1654      if (encryptOutput.isPresent())
1655      {
1656        try
1657        {
1658          final char[] passphrase;
1659          if (outputEncryptionPassphraseFile.isPresent())
1660          {
1661            passphrase = getPasswordFileReader().readPassword(
1662                 outputEncryptionPassphraseFile.getValue());
1663          }
1664          else
1665          {
1666            passphrase = ToolUtils.promptForEncryptionPassphrase(false, true,
1667                 INFO_LDIFSEARCH_PROMPT_OUTPUT_FILE_ENC_PW.get(),
1668                 INFO_LDIFSEARCH_CONFIRM_OUTPUT_FILE_ENC_PW.get(), getOut(),
1669                 getErr()).toCharArray();
1670          }
1671
1672          outputStream = new PassphraseEncryptedOutputStream(passphrase,
1673               outputStream, null, true, true);
1674        }
1675        catch (final Exception e)
1676        {
1677          Debug.debugException(e);
1678          throw new LDAPException(ResultCode.LOCAL_ERROR,
1679               ERR_LDIFSEARCH_CANNOT_ENCRYPT_OUTPUT_FILE.get(
1680                    StaticUtils.getExceptionMessage(e)),
1681               e);
1682        }
1683      }
1684
1685      if (compressOutput.isPresent())
1686      {
1687        try
1688        {
1689          outputStream = new GZIPOutputStream(outputStream);
1690        }
1691        catch (final Exception e)
1692        {
1693          Debug.debugException(e);
1694          throw new LDAPException(ResultCode.LOCAL_ERROR,
1695               ERR_LDIFSEARCH_CANNOT_COMPRESS_OUTPUT_FILE.get(
1696                    f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)),
1697               e);
1698        }
1699      }
1700
1701      closeOutputStream = false;
1702      return outputStream;
1703    }
1704    finally
1705    {
1706      if (closeOutputStream && (outputStream != null))
1707      {
1708        try
1709        {
1710          outputStream.close();
1711        }
1712        catch (final Exception e)
1713        {
1714          Debug.debugException(e);
1715        }
1716      }
1717    }
1718  }
1719
1720
1721
1722  /**
1723   * Creates an LDIF writer to write to the specified file.
1724   *
1725   * @param  f        The file to be written.
1726   * @param  ldapURL  The LDAP URL with which the file will be associated.  It
1727   *                  may be {@code null} if the file is shared across multiple
1728   *                  URLs.
1729   *
1730   * @return  The LDIF writer that was created.
1731   *
1732   * @throws  LDAPException  If a problem occurs while creating the LDIF writer.
1733   */
1734  @NotNull()
1735  private LDIFWriter createLDIFWriter(@NotNull final File f,
1736                                      @Nullable final LDAPURL ldapURL)
1737          throws LDAPException
1738  {
1739    boolean closeOutputStream = true;
1740    final OutputStream outputStream = createOutputStream(f);
1741    try
1742    {
1743      final LDIFWriter ldifWriter = new LDIFWriter(outputStream);
1744      if (doNotWrap.isPresent())
1745      {
1746        ldifWriter.setWrapColumn(0);
1747      }
1748      else if (wrapColumn.isPresent())
1749      {
1750        ldifWriter.setWrapColumn(wrapColumn.getValue());
1751      }
1752      else
1753      {
1754        ldifWriter.setWrapColumn(WRAP_COLUMN);
1755      }
1756
1757      if (ldapURL != null)
1758      {
1759        try
1760        {
1761          ldifWriter.writeComment(
1762               INFO_LDIFSEARCH_ENTRIES_MATCHING_URL.get(ldapURL.toString()),
1763               false, true);
1764        }
1765        catch (final Exception e)
1766        {
1767          Debug.debugException(e);
1768        }
1769      }
1770
1771      closeOutputStream = false;
1772      return ldifWriter;
1773    }
1774    finally
1775    {
1776      if (closeOutputStream)
1777      {
1778        try
1779        {
1780          outputStream.close();
1781        }
1782        catch (final Exception e)
1783        {
1784          Debug.debugException(e);
1785        }
1786      }
1787    }
1788  }
1789
1790
1791
1792  /**
1793   * Indicates whether the given entry matches the criteria in the provided LDAP
1794   * URL.
1795   *
1796   * @param  url    The URL for which to make the determination.
1797   * @param  entry  The entry for which to make the determination.
1798   *
1799   * @return  {@code true} if the entry matches the criteria in the LDAP URL, or
1800   *          {@code false} if not.
1801   */
1802  private boolean urlMatchesEntry(@NotNull final LDAPURL url,
1803                                  @NotNull final Entry entry)
1804  {
1805    try
1806    {
1807      return (entry.matchesBaseAndScope(url.getBaseDN(), url.getScope()) &&
1808           url.getFilter().matchesEntry(entry));
1809    }
1810    catch (final Exception e)
1811    {
1812      Debug.debugException(e);
1813      return false;
1814    }
1815  }
1816
1817
1818
1819  /**
1820   * Writes a line-wrapped, commented version of the provided message to
1821   * standard output.
1822   *
1823   * @param  message  The message to be written.
1824   */
1825  private void commentToOut(@NotNull final String message)
1826  {
1827    getOut().flush();
1828    for (final String line : StaticUtils.wrapLine(message, (WRAP_COLUMN - 2)))
1829    {
1830      out("# " + line);
1831    }
1832    getOut().flush();
1833  }
1834
1835
1836
1837  /**
1838   * Writes a line-wrapped, commented version of the provided message to
1839   * standard error.
1840   *
1841   * @param  message  The message to be written.
1842   */
1843  private void commentToErr(@NotNull final String message)
1844  {
1845    getErr().flush();
1846    for (final String line : StaticUtils.wrapLine(message, (WRAP_COLUMN - 2)))
1847    {
1848      err("# " + line);
1849    }
1850    getErr().flush();
1851  }
1852
1853
1854
1855  /**
1856   * Writes the provided message and sets it as the completion message.
1857   *
1858   * @param  isError  Indicates whether the message should be written to
1859   *                  standard error rather than standard output.
1860   * @param  message  The message to be written.
1861   */
1862  private void logCompletionMessage(final boolean isError,
1863                                    @NotNull final String message)
1864  {
1865    completionMessage.compareAndSet(null, message);
1866
1867    if (! outputFile.isPresent())
1868    {
1869      resultWriter.writeComment(message);
1870    }
1871  }
1872
1873
1874
1875  /**
1876   * {@inheritDoc}
1877   */
1878  @Override()
1879  @NotNull()
1880  public LinkedHashMap<String[],String> getExampleUsages()
1881  {
1882    final LinkedHashMap<String[],String> examples = new LinkedHashMap<>();
1883
1884    examples.put(
1885         new String[]
1886         {
1887           "--ldifFile", "data.ldif",
1888           "(uid=jdoe)"
1889         },
1890         INFO_LDIFSEARCH_EXAMPLE_1.get());
1891
1892    examples.put(
1893         new String[]
1894         {
1895           "--ldifFile", "data.ldif",
1896           "--outputFile", "people.ldif",
1897           "--baseDN", "dc=example,dc=com",
1898           "--scope", "sub",
1899           "(objectClass=person)",
1900           "givenName",
1901           "sn",
1902           "cn",
1903         },
1904         INFO_LDIFSEARCH_EXAMPLE_2.get());
1905
1906    return examples;
1907  }
1908}