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