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  protected boolean supportsDebugLogging()
381  {
382    return true;
383  }
384
385
386
387  /**
388   * {@inheritDoc}
389   */
390  @Override()
391  @Nullable()
392  protected String getToolCompletionMessage()
393  {
394    return completionMessage.get();
395  }
396
397
398
399  /**
400   * {@inheritDoc}
401   */
402  @Override()
403  public void addToolArguments(@NotNull final ArgumentParser parser)
404         throws ArgumentException
405  {
406    this.parser = parser;
407
408
409    ldifFile = new FileArgument('l', "ldifFile", true, 0, null,
410         INFO_LDIFSEARCH_ARG_DESC_LDIF_FILE.get(), true, true, true, false);
411    ldifFile.addLongIdentifier("ldif-file", true);
412    ldifFile.addLongIdentifier("inputFile", true);
413    ldifFile.addLongIdentifier("input-file", true);
414    ldifFile.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_INPUT.get());
415    parser.addArgument(ldifFile);
416
417
418    final String ldifPWDesc;
419    if (PING_SERVER_AVAILABLE)
420    {
421      ldifPWDesc = INFO_LDIFSEARCH_ARG_DESC_LDIF_PW_FILE_PING_SERVER.get();
422    }
423    else
424    {
425      ldifPWDesc = INFO_LDIFSEARCH_ARG_DESC_LDIF_PW_FILE_STANDALONE.get();
426    }
427    ldifEncryptionPassphraseFile = new FileArgument(null,
428         "ldifEncryptionPassphraseFile", false, 1, null, ldifPWDesc, true,
429         true, true, false);
430    ldifEncryptionPassphraseFile.addLongIdentifier(
431         "ldif-encryption-passphrase-file", true);
432    ldifEncryptionPassphraseFile.addLongIdentifier("ldifPassphraseFile", true);
433    ldifEncryptionPassphraseFile.addLongIdentifier("ldif-passphrase-file",
434         true);
435    ldifEncryptionPassphraseFile.addLongIdentifier("ldifEncryptionPasswordFile",
436         true);
437    ldifEncryptionPassphraseFile.addLongIdentifier(
438         "ldif-encryption-password-file", true);
439    ldifEncryptionPassphraseFile.addLongIdentifier("ldifPasswordFile", true);
440    ldifEncryptionPassphraseFile.addLongIdentifier("ldif-password-file", true);
441    ldifEncryptionPassphraseFile.addLongIdentifier(
442         "inputEncryptionPassphraseFile", true);
443    ldifEncryptionPassphraseFile.addLongIdentifier(
444         "input-encryption-passphrase-file", true);
445    ldifEncryptionPassphraseFile.addLongIdentifier("inputPassphraseFile", true);
446    ldifEncryptionPassphraseFile.addLongIdentifier("input-passphrase-file",
447         true);
448    ldifEncryptionPassphraseFile.addLongIdentifier(
449         "inputEncryptionPasswordFile", true);
450    ldifEncryptionPassphraseFile.addLongIdentifier(
451         "input-encryption-password-file", true);
452    ldifEncryptionPassphraseFile.addLongIdentifier("inputPasswordFile", true);
453    ldifEncryptionPassphraseFile.addLongIdentifier("input-password-file", true);
454    ldifEncryptionPassphraseFile.setArgumentGroupName(
455         INFO_LDIFSEARCH_ARG_GROUP_INPUT.get());
456    parser.addArgument(ldifEncryptionPassphraseFile);
457
458
459    stripTrailingSpaces = new BooleanArgument(null, "stripTrailingSpaces", 1,
460         INFO_LDIFSEARCH_ARG_DESC_STRIP_TRAILING_SPACES.get());
461    stripTrailingSpaces.addLongIdentifier("strip-trailing-spaces", true);
462    stripTrailingSpaces.addLongIdentifier("ignoreTrailingSpaces", true);
463    stripTrailingSpaces.addLongIdentifier("ignore-trailing-spaces", true);
464    stripTrailingSpaces.setArgumentGroupName(
465         INFO_LDIFSEARCH_ARG_GROUP_INPUT.get());
466    parser.addArgument(stripTrailingSpaces);
467
468
469    final String schemaPathDesc;
470    if (PING_SERVER_AVAILABLE)
471    {
472      schemaPathDesc = INFO_LDIFSEARCH_ARG_DESC_SCHEMA_PATH_PING_SERVER.get();
473    }
474    else
475    {
476      schemaPathDesc = INFO_LDIFSEARCH_ARG_DESC_SCHEMA_PATH_STANDALONE.get();
477    }
478    schemaPath = new FileArgument(null, "schemaPath", false, 0, null,
479         schemaPathDesc, true, true, false, false);
480    schemaPath.addLongIdentifier("schema-path", true);
481    schemaPath.addLongIdentifier("schemaFile", true);
482    schemaPath.addLongIdentifier("schema-file", true);
483    schemaPath.addLongIdentifier("schemaDirectory", true);
484    schemaPath.addLongIdentifier("schema-directory", true);
485    schemaPath.addLongIdentifier("schema", true);
486    schemaPath.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_INPUT.get());
487    parser.addArgument(schemaPath);
488
489
490    checkSchema = new BooleanArgument(null, "checkSchema", 1,
491         INFO_LDIFSEARCH_ARG_DESC_CHECK_SCHEMA.get());
492    checkSchema.addLongIdentifier("check-schema", true);
493    checkSchema.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_INPUT.get());
494    parser.addArgument(checkSchema);
495
496
497    isCompressed = new BooleanArgument(null, "isCompressed", 1,
498         INFO_LDIFSEARCH_ARG_DESC_IS_COMPRESSED.get());
499    isCompressed.addLongIdentifier("is-compressed", true);
500    isCompressed.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_INPUT.get());
501    isCompressed.setHidden(true);
502    parser.addArgument(isCompressed);
503
504
505    outputFile = new FileArgument('o', "outputFile", false, 1, null,
506         INFO_LDIFSEARCH_ARG_DESC_OUTPUT_FILE.get(), false, true, true, false);
507    outputFile.addLongIdentifier("output-file", true);
508    outputFile.addLongIdentifier("outputLDIF", true);
509    outputFile.addLongIdentifier("output-ldif", true);
510    outputFile.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get());
511    parser.addArgument(outputFile);
512
513
514    separateOutputFilePerSearch = new BooleanArgument(null,
515         "separateOutputFilePerSearch", 1,
516         INFO_LDIFSEARCH_ARG_DESC_SEPARATE_OUTPUT_FILES.get());
517    separateOutputFilePerSearch.addLongIdentifier(
518         "separate-output-file-per-search", true);
519    separateOutputFilePerSearch.addLongIdentifier("separateOutputFiles", true);
520    separateOutputFilePerSearch.addLongIdentifier("separate-output-files",
521         true);
522    separateOutputFilePerSearch.setArgumentGroupName(
523         INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get());
524    parser.addArgument(separateOutputFilePerSearch);
525
526
527    compressOutput = new BooleanArgument(null, "compressOutput", 1,
528         INFO_LDIFSEARCH_ARG_DESC_COMPRESS_OUTPUT.get());
529    compressOutput.addLongIdentifier("compress-output", true);
530    compressOutput.addLongIdentifier("compressLDIF", true);
531    compressOutput.addLongIdentifier("compress-ldif", true);
532    compressOutput.addLongIdentifier("compress", true);
533    compressOutput.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get());
534    parser.addArgument(compressOutput);
535
536
537    encryptOutput = new BooleanArgument(null, "encryptOutput", 1,
538         INFO_LDIFSEARCH_ARG_DESC_ENCRYPT_OUTPUT.get());
539    encryptOutput.addLongIdentifier("encrypt-output", true);
540    encryptOutput.addLongIdentifier("encryptLDIF", true);
541    encryptOutput.addLongIdentifier("encrypt-ldif", true);
542    encryptOutput.addLongIdentifier("encrypt", true);
543    encryptOutput.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get());
544    parser.addArgument(encryptOutput);
545
546
547    outputEncryptionPassphraseFile = new FileArgument(null,
548         "outputEncryptionPassphraseFile", false, 1, null,
549         INFO_LDIFSEARCH_ARG_DESC_OUTPUT_PW_FILE.get(), true, true, true,
550         false);
551    outputEncryptionPassphraseFile.addLongIdentifier(
552         "output-encryption-passphrase-file", true);
553    outputEncryptionPassphraseFile.addLongIdentifier("outputPassphraseFile",
554         true);
555    outputEncryptionPassphraseFile.addLongIdentifier("output-passphrase-file",
556         true);
557    outputEncryptionPassphraseFile.addLongIdentifier(
558         "outputEncryptionPasswordFile", true);
559    outputEncryptionPassphraseFile.addLongIdentifier(
560         "output-encryption-password-file", true);
561    outputEncryptionPassphraseFile.addLongIdentifier("outputPasswordFile",
562         true);
563    outputEncryptionPassphraseFile.addLongIdentifier("output-password-file",
564         true);
565    outputEncryptionPassphraseFile.addLongIdentifier(
566         "outputEncryptionPasswordFile", true);
567    outputEncryptionPassphraseFile.addLongIdentifier(
568         "output-encryption-password-file", true);
569    outputEncryptionPassphraseFile.addLongIdentifier("outputPasswordFile",
570         true);
571    outputEncryptionPassphraseFile.addLongIdentifier("output-password-file",
572         true);
573    outputEncryptionPassphraseFile.setArgumentGroupName(
574         INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get());
575    parser.addArgument(outputEncryptionPassphraseFile);
576
577
578    overwriteExistingOutputFile = new BooleanArgument('O',
579         "overwriteExistingOutputFile", 1,
580         INFO_LDIFSEARCH_ARG_DESC_OVERWRITE_EXISTING.get());
581    overwriteExistingOutputFile.addLongIdentifier(
582         "overwrite-existing-output-file", true);
583    overwriteExistingOutputFile.addLongIdentifier(
584         "overwriteExistingOutputFiles", true);
585    overwriteExistingOutputFile.addLongIdentifier(
586         "overwrite-existing-output-files", true);
587    overwriteExistingOutputFile.addLongIdentifier("overwriteExistingOutput",
588         true);
589    overwriteExistingOutputFile.addLongIdentifier("overwrite-existing-output",
590         true);
591    overwriteExistingOutputFile.addLongIdentifier("overwriteExisting", true);
592    overwriteExistingOutputFile.addLongIdentifier("overwrite-existing", true);
593    overwriteExistingOutputFile.addLongIdentifier("overwrite", true);
594    overwriteExistingOutputFile.setArgumentGroupName(
595         INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get());
596    parser.addArgument(overwriteExistingOutputFile);
597
598
599    final Set<String> outputFormatAllowedValues = StaticUtils.setOf("ldif",
600         "json", "csv", "multi-valued-csv", "tab-delimited",
601         "multi-valued-tab-delimited", "dns-only", "values-only");
602    outputFormat = new StringArgument(null, "outputFormat", false, 1,
603         "{ldif|json|csv|multi-valued-csv|tab-delimited|" +
604              "multi-valued-tab-delimited|dns-only|values-only}",
605         INFO_LDIFSEARCH_ARG_DESC_OUTPUT_FORMAT.get(),
606         outputFormatAllowedValues, "ldif");
607    outputFormat.addLongIdentifier("output-format", true);
608    outputFormat.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get());
609    parser.addArgument(outputFormat);
610
611
612    wrapColumn = new IntegerArgument(null, "wrapColumn", false, 1, null,
613         INFO_LDIFSEARCH_ARG_DESC_WRAP_COLUMN.get(), 5, Integer.MAX_VALUE);
614    wrapColumn.addLongIdentifier("wrap-column", true);
615    wrapColumn.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get());
616    parser.addArgument(wrapColumn);
617
618
619    doNotWrap = new BooleanArgument('T', "doNotWrap", 1,
620         INFO_LDIFSEARCH_ARG_DESC_DO_NOT_WRAP.get());
621    doNotWrap.addLongIdentifier("do-not-wrap", true);
622    doNotWrap.addLongIdentifier("dontWrap", true);
623    doNotWrap.addLongIdentifier("dont-wrap", true);
624    doNotWrap.addLongIdentifier("noWrap", true);
625    doNotWrap.addLongIdentifier("no-wrap", true);
626    doNotWrap.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_OUTPUT.get());
627    parser.addArgument(doNotWrap);
628
629
630    baseDN = new DNArgument('b', "baseDN", false, 1, null,
631         INFO_LDIFSEARCH_ARG_DESC_BASE_DN.get());
632    baseDN.addLongIdentifier("base-dn", true);
633    baseDN.addLongIdentifier("searchBaseDN", true);
634    baseDN.addLongIdentifier("search-base-dn", true);
635    baseDN.addLongIdentifier("searchBase", true);
636    baseDN.addLongIdentifier("search-base", true);
637    baseDN.addLongIdentifier("base", true);
638    baseDN.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get());
639    parser.addArgument(baseDN);
640
641
642    scope = new ScopeArgument('s', "scope", false, null,
643         INFO_LDIFSEARCH_ARG_DESC_SCOPE.get());
644    scope.addLongIdentifier("searchScope", true);
645    scope.addLongIdentifier("search-scope", true);
646    scope.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get());
647    parser.addArgument(scope);
648
649
650    filterFile = new FileArgument('f', "filterFile", false, 0, null,
651         INFO_LDIFSEARCH_ARG_DESC_FILTER_FILE.get(), true, true, true, false);
652    filterFile.addLongIdentifier("filter-file", true);
653    filterFile.addLongIdentifier("filtersFile", true);
654    filterFile.addLongIdentifier("filters-file", true);
655    filterFile.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get());
656    parser.addArgument(filterFile);
657
658
659    ldapURLFile = new FileArgument(null, "ldapURLFile", false, 0, null,
660         INFO_LDIFSEARCH_ARG_DESC_LDAP_URL_FILE.get(), true, true, true, false);
661    ldapURLFile.addLongIdentifier("ldap-url-file", true);
662    ldapURLFile.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get());
663    parser.addArgument(ldapURLFile);
664
665
666    sizeLimit = new IntegerArgument('z', "sizeLimit", false, 1, null,
667         INFO_LDIFSEARCH_ARG_DESC_SIZE_LIMIT.get(), 0, Integer.MAX_VALUE, 0);
668    sizeLimit.addLongIdentifier("size-limit", true);
669    sizeLimit.addLongIdentifier("searchSizeLimit", true);
670    sizeLimit.addLongIdentifier("search-size-limit", true);
671    sizeLimit.setArgumentGroupName(INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get());
672    sizeLimit.setHidden(true);
673    parser.addArgument(sizeLimit);
674
675
676    timeLimitSeconds = new IntegerArgument('t', "timeLimitSeconds", false, 1,
677         null, INFO_LDIFSEARCH_ARG_DESC_TIME_LIMIT_SECONDS.get(), 0,
678         Integer.MAX_VALUE, 0);
679    timeLimitSeconds.addLongIdentifier("time-limit-seconds", true);
680    timeLimitSeconds.addLongIdentifier("timeLimit", true);
681    timeLimitSeconds.setArgumentGroupName(
682         INFO_LDIFSEARCH_ARG_GROUP_CRITERIA.get());
683    timeLimitSeconds.setHidden(true);
684    parser.addArgument(timeLimitSeconds);
685
686
687    parser.addDependentArgumentSet(separateOutputFilePerSearch, outputFile);
688    parser.addDependentArgumentSet(compressOutput, outputFile);
689    parser.addDependentArgumentSet(encryptOutput, outputFile);
690    parser.addDependentArgumentSet(overwriteExistingOutputFile, outputFile);
691    parser.addDependentArgumentSet(outputEncryptionPassphraseFile,
692         encryptOutput);
693
694    parser.addExclusiveArgumentSet(wrapColumn, doNotWrap);
695    parser.addExclusiveArgumentSet(baseDN, ldapURLFile);
696    parser.addExclusiveArgumentSet(scope, ldapURLFile);
697    parser.addExclusiveArgumentSet(filterFile, ldapURLFile);
698    parser.addExclusiveArgumentSet(outputFormat, separateOutputFilePerSearch);
699  }
700
701
702
703  /**
704   * {@inheritDoc}
705   */
706  @Override()
707  public void doExtendedArgumentValidation()
708         throws ArgumentException
709  {
710    // If the output file exists and either compressOutput or encryptOutput is
711    // present, then the overwrite argument must also be present.
712    final File outFile = outputFile.getValue();
713    if ((outFile != null) && outFile.exists() &&
714         (compressOutput.isPresent() || encryptOutput.isPresent()) &&
715         (! overwriteExistingOutputFile.isPresent()))
716    {
717      throw new ArgumentException(
718           ERR_LDIFSEARCH_APPEND_WITH_COMPRESSION_OR_ENCRYPTION.get(
719                compressOutput.getIdentifierString(),
720                encryptOutput.getIdentifierString(),
721                overwriteExistingOutputFile.getIdentifierString()));
722    }
723
724
725    // Create the set of LDAP URLs to use when issuing the searches.
726    final List<String> trailingArgs = parser.getTrailingArguments();
727    final List<String> requestedAttributes = new ArrayList<>();
728    if (filterFile.isPresent())
729    {
730      // If there are trailing arguments, then make sure the first one is not a
731      // valid filter.
732      if (! trailingArgs.isEmpty())
733      {
734        try
735        {
736          Filter.create(trailingArgs.get(0));
737          throw new ArgumentException(
738               ERR_LDIFSEARCH_FILTER_FILE_WITH_TRAILING_FILTER.get());
739        }
740        catch (final LDAPException e)
741        {
742          // This was expected.
743        }
744      }
745
746      requestedAttributes.addAll(trailingArgs);
747      readFilterFile();
748    }
749    else if (ldapURLFile.isPresent())
750    {
751      // Make sure there aren't any trailing arguments.
752      if (! trailingArgs.isEmpty())
753      {
754        throw new ArgumentException(
755             ERR_LDIFSEARCH_LDAP_URL_FILE_WITH_TRAILING_ARGS.get());
756      }
757
758      readLDAPURLFile();
759
760
761      // If there are multiple LDAP URLs, and if they should not be sent to
762      // separate output files, then they must all have the same set of
763      // requested attributes.
764      if ((searchURLs.size() > 1) &&
765           (! separateOutputFilePerSearch.isPresent()))
766      {
767        final Iterator<LDAPURL> iterator = searchURLs.iterator();
768        final Set<String> requestedAttrs =
769             new HashSet<>(Arrays.asList(iterator.next().getAttributes()));
770        while (iterator.hasNext())
771        {
772          final Set<String> attrSet = new HashSet<>(Arrays.asList(
773               iterator.next().getAttributes()));
774          if (! requestedAttrs.equals(attrSet))
775          {
776            throw new ArgumentException(
777                 ERR_LDIFSEARCH_DIFFERENT_URL_ATTRS_IN_SAME_FILE.get(
778                      ldapURLFile.getIdentifierString(),
779                      separateOutputFilePerSearch.getIdentifierString()));
780          }
781        }
782      }
783    }
784    else
785    {
786      // Make sure there is at least one trailing argument, and that it's a
787      // valid filter.  If there are any others, then they must be the
788      // requested arguments.
789      if (trailingArgs.isEmpty())
790      {
791        throw new ArgumentException(ERR_LDIFSEARCH_NO_FILTER.get());
792      }
793
794
795      final Filter filter;
796      try
797      {
798        final List<String> trailingArgList = new ArrayList<>(trailingArgs);
799        final Iterator<String> trailingArgIterator = trailingArgList.iterator();
800        filter = Filter.create(trailingArgIterator.next());
801
802        while (trailingArgIterator.hasNext())
803        {
804          requestedAttributes.add(trailingArgIterator.next());
805        }
806      }
807      catch (final LDAPException e)
808      {
809        Debug.debugException(e);
810        throw new ArgumentException(
811             ERR_LDIFSEARCH_FIRST_TRAILING_ARG_NOT_FILTER.get(
812                  trailingArgs.get(0)),
813             e);
814      }
815
816
817      DN dn = baseDN.getValue();
818      if (dn == null)
819      {
820        dn = DN.NULL_DN;
821      }
822
823      SearchScope searchScope = scope.getValue();
824      if (searchScope == null)
825      {
826        searchScope = SearchScope.SUB;
827      }
828
829      try
830      {
831        searchURLs.add(new LDAPURL("ldap", null, null, dn,
832             requestedAttributes.toArray(StaticUtils.NO_STRINGS),
833             searchScope, filter));
834      }
835      catch (final LDAPException e)
836      {
837        Debug.debugException(e);
838        // This should never happen.
839        throw new ArgumentException(StaticUtils.getExceptionMessage(e), e);
840      }
841    }
842
843
844    // Create the result writer.
845    final String outputFormatStr =
846         StaticUtils.toLowerCase(outputFormat.getValue());
847    if (outputFormatStr.equals("json"))
848    {
849      resultWriter = new JSONLDAPResultWriter(getOut());
850    }
851    else if (outputFormatStr.equals("csv") ||
852             outputFormatStr.equals("multi-valued-csv") ||
853             outputFormatStr.equals("tab-delimited") ||
854             outputFormatStr.equals("multi-valued-tab-delimited"))
855    {
856      // These output formats cannot be used with the --ldapURLFile argument.
857      if (ldapURLFile.isPresent())
858      {
859        throw new ArgumentException(
860             ERR_LDIFSEARCH_OUTPUT_FORMAT_NOT_SUPPORTED_WITH_URLS.get(
861                  outputFormat.getValue(), ldapURLFile.getIdentifierString()));
862      }
863
864
865      // These output formats require a set of requested attributes.
866      if (requestedAttributes.isEmpty())
867      {
868        throw new ArgumentException(
869             ERR_LDIFSEARCH_OUTPUT_FORMAT_REQUIRES_REQUESTED_ATTRS.get(
870                  outputFormat.getValue()));
871      }
872
873      final OutputFormat format;
874      final boolean includeAllValues;
875      switch (outputFormatStr)
876      {
877        case "multi-valued-csv":
878          format = OutputFormat.CSV;
879          includeAllValues = true;
880          break;
881        case "tab-delimited":
882          format = OutputFormat.TAB_DELIMITED_TEXT;
883          includeAllValues = false;
884          break;
885        case "multi-valued-tab-delimited":
886          format = OutputFormat.TAB_DELIMITED_TEXT;
887          includeAllValues = true;
888          break;
889        case "csv":
890        default:
891          format = OutputFormat.CSV;
892          includeAllValues = false;
893          break;
894      }
895
896
897      resultWriter = new ColumnBasedLDAPResultWriter(getOut(),
898           format, requestedAttributes, WRAP_COLUMN, includeAllValues);
899    }
900    else if (outputFormatStr.equals("dns-only"))
901    {
902      resultWriter = new DNsOnlyLDAPResultWriter(getOut());
903    }
904    else if (outputFormatStr.equals("values-only"))
905    {
906      resultWriter = new ValuesOnlyLDAPResultWriter(getOut());
907    }
908    else
909    {
910      final int wc;
911      if (doNotWrap.isPresent())
912      {
913        wc = Integer.MAX_VALUE;
914      }
915      else if (wrapColumn.isPresent())
916      {
917        wc = wrapColumn.getValue();
918      }
919      else
920      {
921        wc = WRAP_COLUMN;
922      }
923
924      resultWriter = new LDIFLDAPResultWriter(getOut(), wc);
925    }
926  }
927
928
929
930  /**
931   * Uses the contents of any specified filter files, along with the configured
932   * base DN, scope, and requested attributes, to populate the set of search
933   * URLs.
934   *
935   * @throws  ArgumentException  If a problem is encountered while constructing
936   *                             the search URLs.
937   */
938  private void readFilterFile()
939          throws ArgumentException
940  {
941    DN dn = baseDN.getValue();
942    if (dn == null)
943    {
944      dn = DN.NULL_DN;
945    }
946
947    SearchScope searchScope = scope.getValue();
948    if (searchScope == null)
949    {
950      searchScope = SearchScope.SUB;
951    }
952
953    final String[] requestedAttributes =
954         parser.getTrailingArguments().toArray(StaticUtils.NO_STRINGS);
955
956    for (final File f : filterFile.getValues())
957    {
958      final InputStream inputStream;
959      try
960      {
961        inputStream = openInputStream(f);
962      }
963      catch (final LDAPException e)
964      {
965        Debug.debugException(e);
966        throw new ArgumentException(e.getMessage(), e);
967      }
968
969      try (BufferedReader reader =
970                new BufferedReader(new InputStreamReader(inputStream)))
971      {
972        while (true)
973        {
974          final String line = reader.readLine();
975          if (line == null)
976          {
977            break;
978          }
979
980          if (line.isEmpty() || line.startsWith("#"))
981          {
982            continue;
983          }
984
985          try
986          {
987            final Filter filter = Filter.create(line.trim());
988            searchURLs.add(new LDAPURL("ldap", null, null, dn,
989                 requestedAttributes, searchScope, filter));
990          }
991          catch (final LDAPException e)
992          {
993            Debug.debugException(e);
994            throw new ArgumentException(
995                 ERR_LDIFSEARCH_FILTER_FILE_INVALID_FILTER.get(line,
996                      f.getAbsolutePath(), e.getMessage()),
997                 e);
998          }
999        }
1000      }
1001      catch (final IOException e)
1002      {
1003        Debug.debugException(e);
1004        throw new ArgumentException(
1005             ERR_LDIFSEARCH_ERROR_READING_FILTER_FILE.get(f.getAbsolutePath(),
1006                  StaticUtils.getExceptionMessage(e)),
1007             e);
1008      }
1009      finally
1010      {
1011        try
1012        {
1013          inputStream.close();
1014        }
1015        catch (final Exception e)
1016        {
1017          Debug.debugException(e);
1018        }
1019      }
1020
1021    }
1022
1023    if (searchURLs.isEmpty())
1024    {
1025      throw new ArgumentException(ERR_LDIFSEARCH_NO_FILTERS_FROM_FILE.get(
1026           filterFile.getValues().get(0).getAbsolutePath()));
1027    }
1028  }
1029
1030
1031
1032  /**
1033   * Uses the contents of any specified LDAP URL files to populate the set of
1034   * search URLs.
1035   *
1036   * @throws  ArgumentException  If a problem is encountered while constructing
1037   *                             the search URLs.
1038   */
1039  private void readLDAPURLFile()
1040          throws ArgumentException
1041  {
1042    for (final File f : ldapURLFile.getValues())
1043    {
1044      final InputStream inputStream;
1045      try
1046      {
1047        inputStream = openInputStream(f);
1048      }
1049      catch (final LDAPException e)
1050      {
1051        Debug.debugException(e);
1052        throw new ArgumentException(e.getMessage(), e);
1053      }
1054
1055      try (BufferedReader reader =
1056                new BufferedReader(new InputStreamReader(inputStream)))
1057      {
1058        while (true)
1059        {
1060          final String line = reader.readLine();
1061          if (line == null)
1062          {
1063            break;
1064          }
1065
1066          if (line.isEmpty() || line.startsWith("#"))
1067          {
1068            continue;
1069          }
1070
1071          try
1072          {
1073            searchURLs.add(new LDAPURL(line.trim()));
1074          }
1075          catch (final LDAPException e)
1076          {
1077            Debug.debugException(e);
1078            throw new ArgumentException(
1079                 ERR_LDIFSEARCH_LDAP_URL_FILE_INVALID_URL.get(line,
1080                      f.getAbsolutePath(), e.getMessage()),
1081                 e);
1082          }
1083        }
1084      }
1085      catch (final IOException e)
1086      {
1087        Debug.debugException(e);
1088        throw new ArgumentException(
1089             ERR_LDIFSEARCH_ERROR_READING_LDAP_URL_FILE.get(f.getAbsolutePath(),
1090                  StaticUtils.getExceptionMessage(e)),
1091             e);
1092      }
1093      finally
1094      {
1095        try
1096        {
1097          inputStream.close();
1098        }
1099        catch (final Exception e)
1100        {
1101          Debug.debugException(e);
1102        }
1103      }
1104    }
1105
1106    if (searchURLs.isEmpty())
1107    {
1108      throw new ArgumentException(ERR_LDIFSEARCH_NO_URLS_FROM_FILE.get(
1109           ldapURLFile.getValues().get(0).getAbsolutePath()));
1110    }
1111  }
1112
1113
1114
1115  /**
1116   * {@inheritDoc}
1117   */
1118  @Override()
1119  @NotNull()
1120  public ResultCode doToolProcessing()
1121  {
1122    // Get the schema to use when performing LDIF processing.
1123    final Schema schema;
1124    try
1125    {
1126      if (schemaPath.isPresent())
1127      {
1128        schema = getSchema(schemaPath.getValues());
1129      }
1130      else if (PING_SERVER_AVAILABLE)
1131      {
1132        schema = getSchema(Collections.singletonList(StaticUtils.constructPath(
1133             PING_SERVER_ROOT, "config", "schema")));
1134      }
1135      else
1136      {
1137        schema = Schema.getDefaultStandardSchema();
1138      }
1139    }
1140    catch (final Exception e)
1141    {
1142      Debug.debugException(e);
1143      logCompletionMessage(true,
1144           ERR_LDIFSEARCH_CANNOT_GET_SCHEMA.get(
1145                StaticUtils.getExceptionMessage(e)));
1146      return ResultCode.LOCAL_ERROR;
1147    }
1148
1149
1150    // Create search entry parers for all of the search URLs.
1151    final Map<LDAPURL,SearchEntryParer> urlMap = new LinkedHashMap<>();
1152    for (final LDAPURL url : searchURLs)
1153    {
1154      final SearchEntryParer parer = new SearchEntryParer(
1155           Arrays.asList(url.getAttributes()), schema);
1156      urlMap.put(url, parer);
1157    }
1158
1159
1160    // If we should check schema, then create the entry validator.
1161    final EntryValidator entryValidator;
1162    if (checkSchema.isPresent())
1163    {
1164      entryValidator = new EntryValidator(schema);
1165    }
1166    else
1167    {
1168      entryValidator = null;
1169    }
1170
1171
1172    // Create the output files, if appropriate.
1173    OutputStream outputStream = null;
1174    SearchEntryParer singleParer = null;
1175    final Map<LDAPURL,LDIFSearchSeparateSearchDetails> separateWriters =
1176         new LinkedHashMap<>();
1177    try
1178    {
1179      if (outputFile.isPresent())
1180      {
1181        final int numURLs = searchURLs.size();
1182        if (separateOutputFilePerSearch.isPresent() && (numURLs > 1))
1183        {
1184          int i=1;
1185          for (final LDAPURL url : searchURLs)
1186          {
1187            final File f = new
1188                 File(outputFile.getValue().getAbsolutePath() + '.' + i);
1189            final LDIFSearchSeparateSearchDetails details =
1190                 new LDIFSearchSeparateSearchDetails(url, f,
1191                      createLDIFWriter(f, url), schema);
1192            separateWriters.put(url, details);
1193            i++;
1194          }
1195        }
1196        else
1197        {
1198          try
1199          {
1200            outputStream = createOutputStream(outputFile.getValue());
1201            resultWriter.updateOutputStream(outputStream);
1202          }
1203          catch (final Exception e)
1204          {
1205            Debug.debugException(e);
1206            throw new LDAPException(ResultCode.LOCAL_ERROR,
1207                 ERR_LDIFSEARCH_CANNOT_WRITE_TO_FILE.get(
1208                      outputFile.getValue().getAbsolutePath(),
1209                      StaticUtils.getExceptionMessage(e)),
1210                 e);
1211          }
1212        }
1213      }
1214
1215
1216      // If we're not using separate writers, then write any appropriate header
1217      // to the top of the output.
1218      if (separateWriters.isEmpty())
1219      {
1220        resultWriter.writeHeader();
1221      }
1222
1223
1224      // Iterate through the LDIF files and process the entries they contain.
1225      boolean errorEncountered = false;
1226      final List<LDAPURL> matchingURLs = new ArrayList<>();
1227      final List<String> entryInvalidReasons = new ArrayList<>();
1228      for (final File f : ldifFile.getValues())
1229      {
1230        final LDIFReader ldifReader;
1231        try
1232        {
1233          ldifReader = new LDIFReader(openInputStream(f));
1234
1235          if (stripTrailingSpaces.isPresent())
1236          {
1237            ldifReader.setTrailingSpaceBehavior(TrailingSpaceBehavior.STRIP);
1238          }
1239          else
1240          {
1241            ldifReader.setTrailingSpaceBehavior(TrailingSpaceBehavior.REJECT);
1242          }
1243        }
1244        catch (final Exception e)
1245        {
1246          Debug.debugException(e);
1247          logCompletionMessage(true,
1248               ERR_LDIFSEARCH_CANNOT_OPEN_LDIF_FILE.get(f.getName(),
1249                    StaticUtils.getExceptionMessage(e)));
1250          return ResultCode.LOCAL_ERROR;
1251        }
1252
1253        try
1254        {
1255          while (true)
1256          {
1257            final Entry entry;
1258            try
1259            {
1260              entry = ldifReader.readEntry();
1261            }
1262            catch (final LDIFException e)
1263            {
1264              Debug.debugException(e);
1265              if (e.mayContinueReading())
1266              {
1267                commentToErr(ERR_LDIFSEARCH_RECOVERABLE_READ_ERROR.get(
1268                     f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)));
1269                errorEncountered = true;
1270                continue;
1271              }
1272              else
1273              {
1274                logCompletionMessage(true,
1275                     ERR_LDIFSEARCH_UNRECOVERABLE_READ_ERROR.get(
1276                          f.getAbsolutePath(),
1277                          StaticUtils.getExceptionMessage(e)));
1278                return ResultCode.LOCAL_ERROR;
1279              }
1280            }
1281            catch (final Exception e)
1282            {
1283              logCompletionMessage(true,
1284                   ERR_LDIFSEARCH_UNRECOVERABLE_READ_ERROR.get(
1285                        f.getAbsolutePath(),
1286                        StaticUtils.getExceptionMessage(e)));
1287              return ResultCode.LOCAL_ERROR;
1288            }
1289
1290            if (entry == null)
1291            {
1292              break;
1293            }
1294
1295            if (entryValidator != null)
1296            {
1297              entryInvalidReasons.clear();
1298              if (! entryValidator.entryIsValid(entry, entryInvalidReasons))
1299              {
1300                commentToErr(ERR_LDIFSEARCH_ENTRY_VIOLATES_SCHEMA.get(
1301                     entry.getDN()));
1302                for (final String invalidReason : entryInvalidReasons)
1303                {
1304                  commentToErr("- " + invalidReason);
1305                }
1306
1307                err();
1308                errorEncountered = true;
1309                continue;
1310              }
1311            }
1312
1313            if (separateWriters.isEmpty())
1314            {
1315              matchingURLs.clear();
1316              for (final LDAPURL url : searchURLs)
1317              {
1318                if (urlMatchesEntry(url, entry))
1319                {
1320                  matchingURLs.add(url);
1321                }
1322              }
1323
1324              if (matchingURLs.isEmpty())
1325              {
1326                continue;
1327              }
1328
1329              try
1330              {
1331                if (searchURLs.size() > 1)
1332                {
1333                  resultWriter.writeComment(
1334                       INFO_LDIFSEARCH_ENTRY_MATCHES_URLS.get(entry.getDN()));
1335                  for (final LDAPURL url : matchingURLs)
1336                  {
1337                    resultWriter.writeComment(url.toString());
1338                  }
1339                }
1340
1341                if (singleParer == null)
1342                {
1343                  singleParer = new SearchEntryParer(
1344                       Arrays.asList(searchURLs.get(0).getAttributes()),
1345                       schema);
1346                }
1347
1348                resultWriter.writeSearchResultEntry(
1349                     new SearchResultEntry(singleParer.pareEntry(entry)));
1350
1351                if (! outputFile.isPresent())
1352                {
1353                  resultWriter.flush();
1354                }
1355              }
1356              catch (final Exception e)
1357              {
1358                Debug.debugException(e);
1359                if (outputFile.isPresent())
1360                {
1361                  logCompletionMessage(true,
1362                       ERR_LDIFSEARCH_WRITE_ERROR_WITH_FILE.get(entry.getDN(),
1363                            outputFile.getValue().getAbsolutePath(),
1364                            StaticUtils.getExceptionMessage(e)));
1365                }
1366                else
1367                {
1368                  logCompletionMessage(true,
1369                       ERR_LDIFSEARCH_WRITE_ERROR_NO_FILE.get(entry.getDN(),
1370                            StaticUtils.getExceptionMessage(e)));
1371                }
1372                return ResultCode.LOCAL_ERROR;
1373              }
1374            }
1375            else
1376            {
1377              for (final LDIFSearchSeparateSearchDetails details :
1378                   separateWriters.values())
1379              {
1380                final LDAPURL url = details.getLDAPURL();
1381                if (urlMatchesEntry(url, entry))
1382                {
1383                  try
1384                  {
1385                    final Entry paredEntry =
1386                         details.getSearchEntryParer().pareEntry(entry);
1387                    details.getLDIFWriter().writeEntry(paredEntry);
1388                  }
1389                  catch (final Exception ex)
1390                  {
1391                    Debug.debugException(ex);
1392                    logCompletionMessage(true,
1393                         ERR_LDIFSEARCH_WRITE_ERROR_WITH_FILE.get(entry.getDN(),
1394                              details.getOutputFile().getAbsolutePath(),
1395                              StaticUtils.getExceptionMessage(ex)));
1396                    return ResultCode.LOCAL_ERROR;
1397                  }
1398                }
1399              }
1400            }
1401          }
1402        }
1403        finally
1404        {
1405          try
1406          {
1407            ldifReader.close();
1408          }
1409          catch (final Exception e)
1410          {
1411            Debug.debugException(e);
1412          }
1413        }
1414      }
1415
1416      if (errorEncountered)
1417      {
1418        logCompletionMessage(true,
1419             WARN_LDIFSEARCH_COMPLETED_WITH_ERRORS.get());
1420        return ResultCode.PARAM_ERROR;
1421      }
1422      else
1423      {
1424        logCompletionMessage(false,
1425             INFO_LDIFSEARCH_COMPLETED_SUCCESSFULLY.get());
1426        return ResultCode.SUCCESS;
1427      }
1428    }
1429    catch (final LDAPException e)
1430    {
1431      Debug.debugException(e);
1432      logCompletionMessage(true, e.getMessage());
1433      return e.getResultCode();
1434    }
1435    finally
1436    {
1437      try
1438      {
1439        resultWriter.flush();
1440        if (outputStream != null)
1441        {
1442          outputStream.close();
1443        }
1444      }
1445      catch (final Exception e)
1446      {
1447        Debug.debugException(e);
1448      }
1449
1450      for (final LDIFSearchSeparateSearchDetails details :
1451           separateWriters.values())
1452      {
1453        try
1454        {
1455          details.getLDIFWriter().close();
1456        }
1457        catch (final Exception e)
1458        {
1459          Debug.debugException(e);
1460        }
1461      }
1462    }
1463  }
1464
1465
1466
1467  /**
1468   * Retrieves the schema contained in the specified paths.
1469   *
1470   * @param  paths  The paths to use to access the schema.
1471   *
1472   * @return  The schema read from the specified files.
1473   *
1474   * @throws  Exception  If a problem is encountered while loading the schema.
1475   */
1476  @NotNull()
1477  private static Schema getSchema(@NotNull final List<File> paths)
1478          throws Exception
1479  {
1480    final Set<File> schemaFiles = new LinkedHashSet<>();
1481    for (final File f : paths)
1482    {
1483      if (f.exists())
1484      {
1485        if (f.isFile())
1486        {
1487          schemaFiles.add(f);
1488        }
1489        else if (f.isDirectory())
1490        {
1491          final TreeMap<String,File> sortedFiles = new TreeMap<>();
1492          for (final File fileInDir : f.listFiles())
1493          {
1494            if (fileInDir.isFile())
1495            {
1496              sortedFiles.put(fileInDir.getName(), fileInDir);
1497            }
1498          }
1499
1500          schemaFiles.addAll(sortedFiles.values());
1501        }
1502      }
1503    }
1504
1505    return Schema.getSchema(new ArrayList<>(schemaFiles));
1506  }
1507
1508
1509
1510  /**
1511   * Opens the input stream to use to read from the specified file.
1512   *
1513   * @param  f  The file for which to open the input stream.  It may optionally
1514   *            be compressed and/or encrypted.
1515   *
1516   * @return  The input stream that was created.
1517   *
1518   * @throws  LDAPException  If a problem is encountered while opening the file.
1519   */
1520  @NotNull()
1521  private InputStream openInputStream(@NotNull final File f)
1522          throws LDAPException
1523  {
1524    if (ldifEncryptionPassphraseFile.isPresent() &&
1525       (! ldifEncryptionPassphraseFileRead))
1526    {
1527      readPassphraseFile(ldifEncryptionPassphraseFile.getValue());
1528      ldifEncryptionPassphraseFileRead = true;
1529    }
1530
1531
1532    boolean closeStream = true;
1533    InputStream inputStream = null;
1534    try
1535    {
1536      inputStream = new FileInputStream(f);
1537
1538      final ObjectPair<InputStream,char[]> p =
1539           ToolUtils.getPossiblyPassphraseEncryptedInputStream(
1540                inputStream, inputEncryptionPassphrases,
1541                (! ldifEncryptionPassphraseFile.isPresent()),
1542                INFO_LDIFSEARCH_ENTER_ENCRYPTION_PW.get(f.getName()),
1543                ERR_LDIFSEARCH_WRONG_ENCRYPTION_PW.get(), getOut(), getErr());
1544      inputStream = p.getFirst();
1545      addPassphrase(p.getSecond());
1546
1547      inputStream = ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream);
1548      closeStream = false;
1549      return inputStream;
1550    }
1551    catch (final Exception e)
1552    {
1553      Debug.debugException(e);
1554      throw new LDAPException(ResultCode.LOCAL_ERROR,
1555           ERR_LDIFSEARCH_ERROR_OPENING_INPUT_FILE.get(f.getAbsolutePath(),
1556                StaticUtils.getExceptionMessage(e)),
1557           e);
1558    }
1559    finally
1560    {
1561      if ((inputStream != null) && closeStream)
1562      {
1563        try
1564        {
1565          inputStream.close();
1566        }
1567        catch (final Exception e)
1568        {
1569          Debug.debugException(e);
1570        }
1571      }
1572    }
1573  }
1574
1575
1576
1577  /**
1578   * Reads the contents of the specified passphrase file and adds it to the list
1579   * of passphrases.
1580   *
1581   * @param  f  The passphrase file to read.
1582   *
1583   * @throws  LDAPException  If a problem is encountered while trying to read
1584   *                         the passphrase from the provided file.
1585   */
1586  private void readPassphraseFile(@NotNull final File f)
1587          throws LDAPException
1588  {
1589    try
1590    {
1591      addPassphrase(getPasswordFileReader().readPassword(f));
1592    }
1593    catch (final Exception e)
1594    {
1595      Debug.debugException(e);
1596      throw new LDAPException(ResultCode.LOCAL_ERROR,
1597           ERR_LDIFSEARCH_CANNOT_READ_PW_FILE.get(f.getAbsolutePath(),
1598                StaticUtils.getExceptionMessage(e)),
1599           e);
1600    }
1601  }
1602
1603
1604
1605  /**
1606   * Updates the list of encryption passphrases with the provided passphrase, if
1607   * it is not already present.
1608   *
1609   * @param  passphrase  The passphrase to be added.  It may optionally be
1610   *                     {@code null} (in which case no action will be taken).
1611   */
1612  private void addPassphrase(@Nullable final char[] passphrase)
1613  {
1614    if (passphrase == null)
1615    {
1616      return;
1617    }
1618
1619    for (final char[] existingPassphrase : inputEncryptionPassphrases)
1620    {
1621      if (Arrays.equals(existingPassphrase, passphrase))
1622      {
1623        return;
1624      }
1625    }
1626
1627    inputEncryptionPassphrases.add(passphrase);
1628  }
1629
1630
1631
1632  /**
1633   * Creates an output stream that may be used to write to the specified file.
1634   *
1635   * @param  f  The file to be written.
1636   *
1637   * @return  The output stream that was created.
1638   *
1639   * @throws  LDAPException  If a problem occurs while creating the output
1640   *                         stream.
1641   */
1642  @NotNull()
1643  private OutputStream createOutputStream(@NotNull final File f)
1644          throws LDAPException
1645  {
1646    OutputStream outputStream = null;
1647    boolean closeOutputStream = true;
1648    try
1649    {
1650      try
1651      {
1652
1653        outputStream = new FileOutputStream(f,
1654             (! overwriteExistingOutputFile.isPresent()));
1655      }
1656      catch (final Exception e)
1657      {
1658        Debug.debugException(e);
1659        throw new LDAPException(ResultCode.LOCAL_ERROR,
1660             ERR_LDIFSEARCH_CANNOT_OPEN_OUTPUT_FILE.get(f.getAbsolutePath(),
1661                  StaticUtils.getExceptionMessage(e)),
1662             e);
1663      }
1664
1665      if (encryptOutput.isPresent())
1666      {
1667        try
1668        {
1669          final char[] passphrase;
1670          if (outputEncryptionPassphraseFile.isPresent())
1671          {
1672            passphrase = getPasswordFileReader().readPassword(
1673                 outputEncryptionPassphraseFile.getValue());
1674          }
1675          else
1676          {
1677            passphrase = ToolUtils.promptForEncryptionPassphrase(false, true,
1678                 INFO_LDIFSEARCH_PROMPT_OUTPUT_FILE_ENC_PW.get(),
1679                 INFO_LDIFSEARCH_CONFIRM_OUTPUT_FILE_ENC_PW.get(), getOut(),
1680                 getErr()).toCharArray();
1681          }
1682
1683          outputStream = new PassphraseEncryptedOutputStream(passphrase,
1684               outputStream, null, true, true);
1685        }
1686        catch (final Exception e)
1687        {
1688          Debug.debugException(e);
1689          throw new LDAPException(ResultCode.LOCAL_ERROR,
1690               ERR_LDIFSEARCH_CANNOT_ENCRYPT_OUTPUT_FILE.get(
1691                    StaticUtils.getExceptionMessage(e)),
1692               e);
1693        }
1694      }
1695
1696      if (compressOutput.isPresent())
1697      {
1698        try
1699        {
1700          outputStream = new GZIPOutputStream(outputStream);
1701        }
1702        catch (final Exception e)
1703        {
1704          Debug.debugException(e);
1705          throw new LDAPException(ResultCode.LOCAL_ERROR,
1706               ERR_LDIFSEARCH_CANNOT_COMPRESS_OUTPUT_FILE.get(
1707                    f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)),
1708               e);
1709        }
1710      }
1711
1712      closeOutputStream = false;
1713      return outputStream;
1714    }
1715    finally
1716    {
1717      if (closeOutputStream && (outputStream != null))
1718      {
1719        try
1720        {
1721          outputStream.close();
1722        }
1723        catch (final Exception e)
1724        {
1725          Debug.debugException(e);
1726        }
1727      }
1728    }
1729  }
1730
1731
1732
1733  /**
1734   * Creates an LDIF writer to write to the specified file.
1735   *
1736   * @param  f        The file to be written.
1737   * @param  ldapURL  The LDAP URL with which the file will be associated.  It
1738   *                  may be {@code null} if the file is shared across multiple
1739   *                  URLs.
1740   *
1741   * @return  The LDIF writer that was created.
1742   *
1743   * @throws  LDAPException  If a problem occurs while creating the LDIF writer.
1744   */
1745  @NotNull()
1746  private LDIFWriter createLDIFWriter(@NotNull final File f,
1747                                      @Nullable final LDAPURL ldapURL)
1748          throws LDAPException
1749  {
1750    boolean closeOutputStream = true;
1751    final OutputStream outputStream = createOutputStream(f);
1752    try
1753    {
1754      final LDIFWriter ldifWriter = new LDIFWriter(outputStream);
1755      if (doNotWrap.isPresent())
1756      {
1757        ldifWriter.setWrapColumn(0);
1758      }
1759      else if (wrapColumn.isPresent())
1760      {
1761        ldifWriter.setWrapColumn(wrapColumn.getValue());
1762      }
1763      else
1764      {
1765        ldifWriter.setWrapColumn(WRAP_COLUMN);
1766      }
1767
1768      if (ldapURL != null)
1769      {
1770        try
1771        {
1772          ldifWriter.writeComment(
1773               INFO_LDIFSEARCH_ENTRIES_MATCHING_URL.get(ldapURL.toString()),
1774               false, true);
1775        }
1776        catch (final Exception e)
1777        {
1778          Debug.debugException(e);
1779        }
1780      }
1781
1782      closeOutputStream = false;
1783      return ldifWriter;
1784    }
1785    finally
1786    {
1787      if (closeOutputStream)
1788      {
1789        try
1790        {
1791          outputStream.close();
1792        }
1793        catch (final Exception e)
1794        {
1795          Debug.debugException(e);
1796        }
1797      }
1798    }
1799  }
1800
1801
1802
1803  /**
1804   * Indicates whether the given entry matches the criteria in the provided LDAP
1805   * URL.
1806   *
1807   * @param  url    The URL for which to make the determination.
1808   * @param  entry  The entry for which to make the determination.
1809   *
1810   * @return  {@code true} if the entry matches the criteria in the LDAP URL, or
1811   *          {@code false} if not.
1812   */
1813  private boolean urlMatchesEntry(@NotNull final LDAPURL url,
1814                                  @NotNull final Entry entry)
1815  {
1816    try
1817    {
1818      return (entry.matchesBaseAndScope(url.getBaseDN(), url.getScope()) &&
1819           url.getFilter().matchesEntry(entry));
1820    }
1821    catch (final Exception e)
1822    {
1823      Debug.debugException(e);
1824      return false;
1825    }
1826  }
1827
1828
1829
1830  /**
1831   * Writes a line-wrapped, commented version of the provided message to
1832   * standard output.
1833   *
1834   * @param  message  The message to be written.
1835   */
1836  private void commentToOut(@NotNull final String message)
1837  {
1838    getOut().flush();
1839    for (final String line : StaticUtils.wrapLine(message, (WRAP_COLUMN - 2)))
1840    {
1841      out("# " + line);
1842    }
1843    getOut().flush();
1844  }
1845
1846
1847
1848  /**
1849   * Writes a line-wrapped, commented version of the provided message to
1850   * standard error.
1851   *
1852   * @param  message  The message to be written.
1853   */
1854  private void commentToErr(@NotNull final String message)
1855  {
1856    getErr().flush();
1857    for (final String line : StaticUtils.wrapLine(message, (WRAP_COLUMN - 2)))
1858    {
1859      err("# " + line);
1860    }
1861    getErr().flush();
1862  }
1863
1864
1865
1866  /**
1867   * Writes the provided message and sets it as the completion message.
1868   *
1869   * @param  isError  Indicates whether the message should be written to
1870   *                  standard error rather than standard output.
1871   * @param  message  The message to be written.
1872   */
1873  private void logCompletionMessage(final boolean isError,
1874                                    @NotNull final String message)
1875  {
1876    completionMessage.compareAndSet(null, message);
1877
1878    if (! outputFile.isPresent())
1879    {
1880      resultWriter.writeComment(message);
1881    }
1882  }
1883
1884
1885
1886  /**
1887   * {@inheritDoc}
1888   */
1889  @Override()
1890  @NotNull()
1891  public LinkedHashMap<String[],String> getExampleUsages()
1892  {
1893    final LinkedHashMap<String[],String> examples = new LinkedHashMap<>();
1894
1895    examples.put(
1896         new String[]
1897         {
1898           "--ldifFile", "data.ldif",
1899           "(uid=jdoe)"
1900         },
1901         INFO_LDIFSEARCH_EXAMPLE_1.get());
1902
1903    examples.put(
1904         new String[]
1905         {
1906           "--ldifFile", "data.ldif",
1907           "--outputFile", "people.ldif",
1908           "--baseDN", "dc=example,dc=com",
1909           "--scope", "sub",
1910           "(objectClass=person)",
1911           "givenName",
1912           "sn",
1913           "cn",
1914         },
1915         INFO_LDIFSEARCH_EXAMPLE_2.get());
1916
1917    return examples;
1918  }
1919}