001/*
002 * Copyright 2008-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2008-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) 2008-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.ldap.sdk.examples;
037
038
039
040import java.io.File;
041import java.io.FileInputStream;
042import java.io.InputStream;
043import java.io.IOException;
044import java.io.OutputStream;
045import java.util.ArrayList;
046import java.util.Iterator;
047import java.util.TreeMap;
048import java.util.LinkedHashMap;
049import java.util.List;
050import java.util.concurrent.atomic.AtomicLong;
051import java.util.zip.GZIPInputStream;
052
053import com.unboundid.ldap.sdk.Entry;
054import com.unboundid.ldap.sdk.LDAPConnection;
055import com.unboundid.ldap.sdk.LDAPException;
056import com.unboundid.ldap.sdk.ResultCode;
057import com.unboundid.ldap.sdk.Version;
058import com.unboundid.ldap.sdk.schema.Schema;
059import com.unboundid.ldap.sdk.schema.EntryValidator;
060import com.unboundid.ldap.sdk.unboundidds.tools.ToolUtils;
061import com.unboundid.ldif.DuplicateValueBehavior;
062import com.unboundid.ldif.LDIFException;
063import com.unboundid.ldif.LDIFReader;
064import com.unboundid.ldif.LDIFReaderEntryTranslator;
065import com.unboundid.ldif.LDIFWriter;
066import com.unboundid.util.Debug;
067import com.unboundid.util.LDAPCommandLineTool;
068import com.unboundid.util.NotNull;
069import com.unboundid.util.Nullable;
070import com.unboundid.util.StaticUtils;
071import com.unboundid.util.ThreadSafety;
072import com.unboundid.util.ThreadSafetyLevel;
073import com.unboundid.util.args.ArgumentException;
074import com.unboundid.util.args.ArgumentParser;
075import com.unboundid.util.args.BooleanArgument;
076import com.unboundid.util.args.FileArgument;
077import com.unboundid.util.args.IntegerArgument;
078import com.unboundid.util.args.StringArgument;
079
080
081
082/**
083 * This class provides a simple tool that can be used to validate that the
084 * contents of an LDIF file are valid.  This includes ensuring that the contents
085 * can be parsed as valid LDIF, and it can also ensure that the LDIF content
086 * conforms to the server schema.  It will obtain the schema by connecting to
087 * the server and retrieving the default schema (i.e., the schema which governs
088 * the root DSE).  By default, a thorough set of validation will be performed,
089 * but it is possible to disable certain types of validation.
090 * <BR><BR>
091 * Some of the APIs demonstrated by this example include:
092 * <UL>
093 *   <LI>Argument Parsing (from the {@code com.unboundid.util.args}
094 *       package)</LI>
095 *   <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util}
096 *       package)</LI>
097 *   <LI>LDIF Processing (from the {@code com.unboundid.ldif} package)</LI>
098 *   <LI>Schema Parsing (from the {@code com.unboundid.ldap.sdk.schema}
099 *       package)</LI>
100 * </UL>
101 * <BR><BR>
102 * Supported arguments include those allowed by the {@link LDAPCommandLineTool}
103 * class (to obtain the information to use to connect to the server to read the
104 * schema), as well as the following additional arguments:
105 * <UL>
106 *   <LI>"--schemaDirectory {path}" -- specifies the path to a directory
107 *       containing files with schema definitions.  If this argument is
108 *       provided, then no attempt will be made to communicate with a directory
109 *       server.</LI>
110 *   <LI>"-f {path}" or "--ldifFile {path}" -- specifies the path to the LDIF
111 *       file to be validated.</LI>
112 *   <LI>"-c" or "--isCompressed" -- indicates that the LDIF file is
113 *       compressed.</LI>
114 *   <LI>"-R {path}" or "--rejectFile {path}" -- specifies the path to the file
115 *       to be written with information about all entries that failed
116 *       validation.</LI>
117 *   <LI>"-t {num}" or "--numThreads {num}" -- specifies the number of
118 *       concurrent threads to use when processing the LDIF.  If this is not
119 *       provided, then a default of one thread will be used.</LI>
120 *   <LI>"--ignoreUndefinedObjectClasses" -- indicates that the validation
121 *       process should ignore validation failures due to entries that contain
122 *       object classes not defined in the server schema.</LI>
123 *   <LI>"--ignoreUndefinedAttributes" -- indicates that the validation process
124 *       should ignore validation failures due to entries that contain
125 *       attributes not defined in the server schema.</LI>
126 *   <LI>"--ignoreMalformedDNs" -- indicates that the validation process should
127 *       ignore validation failures due to entries with malformed DNs.</LI>
128 *   <LI>"--ignoreMissingRDNValues" -- indicates that the validation process
129 *       should ignore validation failures due to entries that contain an RDN
130 *       attribute value that is not present in the set of entry
131 *       attributes.</LI>
132 *   <LI>"--ignoreStructuralObjectClasses" -- indicates that the validation
133 *       process should ignore validation failures due to entries that either do
134 *       not have a structural object class or that have multiple structural
135 *       object classes.</LI>
136 *   <LI>"--ignoreProhibitedObjectClasses" -- indicates that the validation
137 *       process should ignore validation failures due to entries containing
138 *       auxiliary classes that are not allowed by a DIT content rule, or
139 *       abstract classes that are not subclassed by an auxiliary or structural
140 *       class contained in the entry.</LI>
141 *   <LI>"--ignoreProhibitedAttributes" -- indicates that the validation process
142 *       should ignore validation failures due to entries including attributes
143 *       that are not allowed or are explicitly prohibited by a DIT content
144 *       rule.</LI>
145 *   <LI>"--ignoreMissingAttributes" -- indicates that the validation process
146 *       should ignore validation failures due to entries missing required
147 *       attributes.</LI>
148 *   <LI>"--ignoreSingleValuedAttributes" -- indicates that the validation
149 *       process should ignore validation failures due to single-valued
150 *       attributes containing multiple values.</LI>
151 *   <LI>"--ignoreAttributeSyntax" -- indicates that the validation process
152 *       should ignore validation failures due to attribute values which violate
153 *       the associated attribute syntax.</LI>
154 *   <LI>"--ignoreSyntaxViolationsForAttribute" -- indicates that the validation
155 *       process should ignore validation failures due to attribute values which
156 *       violate the associated attribute syntax, but only for the specified
157 *       attribute types.</LI>
158 *   <LI>"--ignoreNameForms" -- indicates that the validation process should
159 *       ignore validation failures due to name form violations (in which the
160 *       entry's RDN does not comply with the associated name form).</LI>
161 * </UL>
162 */
163@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
164public final class ValidateLDIF
165       extends LDAPCommandLineTool
166       implements LDIFReaderEntryTranslator
167{
168  /**
169   * The end-of-line character for this platform.
170   */
171  @NotNull private static final String EOL =
172       StaticUtils.getSystemProperty("line.separator", "\n");
173
174
175
176  // The arguments used by this program.
177  @Nullable private BooleanArgument ignoreDuplicateValues;
178  @Nullable private BooleanArgument ignoreUndefinedObjectClasses;
179  @Nullable private BooleanArgument ignoreUndefinedAttributes;
180  @Nullable private BooleanArgument ignoreMalformedDNs;
181  @Nullable private BooleanArgument ignoreMissingRDNValues;
182  @Nullable private BooleanArgument ignoreMissingSuperiorObjectClasses;
183  @Nullable private BooleanArgument ignoreStructuralObjectClasses;
184  @Nullable private BooleanArgument ignoreProhibitedObjectClasses;
185  @Nullable private BooleanArgument ignoreProhibitedAttributes;
186  @Nullable private BooleanArgument ignoreMissingAttributes;
187  @Nullable private BooleanArgument ignoreSingleValuedAttributes;
188  @Nullable private BooleanArgument ignoreAttributeSyntax;
189  @Nullable private BooleanArgument ignoreNameForms;
190  @Nullable private BooleanArgument isCompressed;
191  @Nullable private FileArgument    schemaDirectory;
192  @Nullable private FileArgument    ldifFile;
193  @Nullable private FileArgument    rejectFile;
194  @Nullable private FileArgument    encryptionPassphraseFile;
195  @Nullable private IntegerArgument numThreads;
196  @Nullable private StringArgument  ignoreSyntaxViolationsForAttribute;
197
198  // The counter used to keep track of the number of entries processed.
199  @NotNull private final AtomicLong entriesProcessed = new AtomicLong(0L);
200
201  // The counter used to keep track of the number of entries that could not be
202  // parsed as valid entries.
203  @NotNull private final AtomicLong malformedEntries = new AtomicLong(0L);
204
205  // The entry validator that will be used to validate the entries.
206  @Nullable private EntryValidator entryValidator;
207
208  // The LDIF writer that will be used to write rejected entries.
209  @Nullable private LDIFWriter rejectWriter;
210
211
212
213  /**
214   * Parse the provided command line arguments and make the appropriate set of
215   * changes.
216   *
217   * @param  args  The command line arguments provided to this program.
218   */
219  public static void main(@NotNull final String[] args)
220  {
221    final ResultCode resultCode = main(args, System.out, System.err);
222    if (resultCode != ResultCode.SUCCESS)
223    {
224      System.exit(resultCode.intValue());
225    }
226  }
227
228
229
230  /**
231   * Parse the provided command line arguments and make the appropriate set of
232   * changes.
233   *
234   * @param  args       The command line arguments provided to this program.
235   * @param  outStream  The output stream to which standard out should be
236   *                    written.  It may be {@code null} if output should be
237   *                    suppressed.
238   * @param  errStream  The output stream to which standard error should be
239   *                    written.  It may be {@code null} if error messages
240   *                    should be suppressed.
241   *
242   * @return  A result code indicating whether the processing was successful.
243   */
244  @NotNull()
245  public static ResultCode main(@NotNull final String[] args,
246                                @Nullable final OutputStream outStream,
247                                @Nullable final OutputStream errStream)
248  {
249    final ValidateLDIF validateLDIF = new ValidateLDIF(outStream, errStream);
250    return validateLDIF.runTool(args);
251  }
252
253
254
255  /**
256   * Creates a new instance of this tool.
257   *
258   * @param  outStream  The output stream to which standard out should be
259   *                    written.  It may be {@code null} if output should be
260   *                    suppressed.
261   * @param  errStream  The output stream to which standard error should be
262   *                    written.  It may be {@code null} if error messages
263   *                    should be suppressed.
264   */
265  public ValidateLDIF(@Nullable final OutputStream outStream,
266                      @Nullable final OutputStream errStream)
267  {
268    super(outStream, errStream);
269  }
270
271
272
273  /**
274   * Retrieves the name for this tool.
275   *
276   * @return  The name for this tool.
277   */
278  @Override()
279  @NotNull()
280  public String getToolName()
281  {
282    return "validate-ldif";
283  }
284
285
286
287  /**
288   * Retrieves the description for this tool.
289   *
290   * @return  The description for this tool.
291   */
292  @Override()
293  @NotNull()
294  public String getToolDescription()
295  {
296    return "Validate the contents of an LDIF file " +
297           "against the server schema.";
298  }
299
300
301
302  /**
303   * Retrieves the version string for this tool.
304   *
305   * @return  The version string for this tool.
306   */
307  @Override()
308  @NotNull()
309  public String getToolVersion()
310  {
311    return Version.NUMERIC_VERSION_STRING;
312  }
313
314
315
316  /**
317   * Indicates whether this tool should provide support for an interactive mode,
318   * in which the tool offers a mode in which the arguments can be provided in
319   * a text-driven menu rather than requiring them to be given on the command
320   * line.  If interactive mode is supported, it may be invoked using the
321   * "--interactive" argument.  Alternately, if interactive mode is supported
322   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
323   * interactive mode may be invoked by simply launching the tool without any
324   * arguments.
325   *
326   * @return  {@code true} if this tool supports interactive mode, or
327   *          {@code false} if not.
328   */
329  @Override()
330  public boolean supportsInteractiveMode()
331  {
332    return true;
333  }
334
335
336
337  /**
338   * Indicates whether this tool defaults to launching in interactive mode if
339   * the tool is invoked without any command-line arguments.  This will only be
340   * used if {@link #supportsInteractiveMode()} returns {@code true}.
341   *
342   * @return  {@code true} if this tool defaults to using interactive mode if
343   *          launched without any command-line arguments, or {@code false} if
344   *          not.
345   */
346  @Override()
347  public boolean defaultsToInteractiveMode()
348  {
349    return true;
350  }
351
352
353
354  /**
355   * Indicates whether this tool should provide arguments for redirecting output
356   * to a file.  If this method returns {@code true}, then the tool will offer
357   * an "--outputFile" argument that will specify the path to a file to which
358   * all standard output and standard error content will be written, and it will
359   * also offer a "--teeToStandardOut" argument that can only be used if the
360   * "--outputFile" argument is present and will cause all output to be written
361   * to both the specified output file and to standard output.
362   *
363   * @return  {@code true} if this tool should provide arguments for redirecting
364   *          output to a file, or {@code false} if not.
365   */
366  @Override()
367  protected boolean supportsOutputFile()
368  {
369    return true;
370  }
371
372
373
374  /**
375   * Indicates whether this tool should default to interactively prompting for
376   * the bind password if a password is required but no argument was provided
377   * to indicate how to get the password.
378   *
379   * @return  {@code true} if this tool should default to interactively
380   *          prompting for the bind password, or {@code false} if not.
381   */
382  @Override()
383  protected boolean defaultToPromptForBindPassword()
384  {
385    return true;
386  }
387
388
389
390  /**
391   * Indicates whether this tool supports the use of a properties file for
392   * specifying default values for arguments that aren't specified on the
393   * command line.
394   *
395   * @return  {@code true} if this tool supports the use of a properties file
396   *          for specifying default values for arguments that aren't specified
397   *          on the command line, or {@code false} if not.
398   */
399  @Override()
400  public boolean supportsPropertiesFile()
401  {
402    return true;
403  }
404
405
406
407  /**
408   * Indicates whether this tool supports the ability to generate a debug log
409   * file.  If this method returns {@code true}, then the tool will expose
410   * additional arguments that can control debug logging.
411   *
412   * @return  {@code true} if this tool supports the ability to generate a debug
413   *          log file, or {@code false} if not.
414   */
415  @Override()
416  protected boolean supportsDebugLogging()
417  {
418    return true;
419  }
420
421
422
423  /**
424   * Indicates whether the LDAP-specific arguments should include alternate
425   * versions of all long identifiers that consist of multiple words so that
426   * they are available in both camelCase and dash-separated versions.
427   *
428   * @return  {@code true} if this tool should provide multiple versions of
429   *          long identifiers for LDAP-specific arguments, or {@code false} if
430   *          not.
431   */
432  @Override()
433  protected boolean includeAlternateLongIdentifiers()
434  {
435    return true;
436  }
437
438
439
440  /**
441   * Indicates whether this tool should provide a command-line argument that
442   * allows for low-level SSL debugging.  If this returns {@code true}, then an
443   * "--enableSSLDebugging}" argument will be added that sets the
444   * "javax.net.debug" system property to "all" before attempting any
445   * communication.
446   *
447   * @return  {@code true} if this tool should offer an "--enableSSLDebugging"
448   *          argument, or {@code false} if not.
449   */
450  @Override()
451  protected boolean supportsSSLDebugging()
452  {
453    return true;
454  }
455
456
457
458  /**
459   * Adds the arguments used by this program that aren't already provided by the
460   * generic {@code LDAPCommandLineTool} framework.
461   *
462   * @param  parser  The argument parser to which the arguments should be added.
463   *
464   * @throws  ArgumentException  If a problem occurs while adding the arguments.
465   */
466  @Override()
467  public void addNonLDAPArguments(@NotNull final ArgumentParser parser)
468         throws ArgumentException
469  {
470    String description = "The path to the LDIF file to process.  The tool " +
471         "will automatically attempt to detect whether the file is " +
472         "encrypted or compressed.";
473    ldifFile = new FileArgument('f', "ldifFile", true, 1, "{path}", description,
474                                true, true, true, false);
475    ldifFile.addLongIdentifier("ldif-file", true);
476    parser.addArgument(ldifFile);
477
478
479    // Add an argument that makes it possible to read a compressed LDIF file.
480    // Note that this argument is no longer needed for dealing with compressed
481    // files, since the tool will automatically detect whether a file is
482    // compressed.  However, the argument is still provided for the purpose of
483    // backward compatibility.
484    description = "Indicates that the specified LDIF file is compressed " +
485                  "using gzip compression.";
486    isCompressed = new BooleanArgument('c', "isCompressed", description);
487    isCompressed.addLongIdentifier("is-compressed", true);
488    isCompressed.setHidden(true);
489    parser.addArgument(isCompressed);
490
491
492    // Add an argument that indicates that the tool should read the encryption
493    // passphrase from a file.
494    description = "Indicates that the specified LDIF file is encrypted and " +
495         "that the encryption passphrase is contained in the specified " +
496         "file.  If the LDIF data is encrypted and this argument is not " +
497         "provided, then the tool will interactively prompt for the " +
498         "encryption passphrase.";
499    encryptionPassphraseFile = new FileArgument(null,
500         "encryptionPassphraseFile", false, 1, null, description, true, true,
501         true, false);
502    encryptionPassphraseFile.addLongIdentifier("encryption-passphrase-file",
503         true);
504    encryptionPassphraseFile.addLongIdentifier("encryptionPasswordFile", true);
505    encryptionPassphraseFile.addLongIdentifier("encryption-password-file",
506         true);
507    parser.addArgument(encryptionPassphraseFile);
508
509
510    description = "The path to the file to which rejected entries should be " +
511                  "written.";
512    rejectFile = new FileArgument('R', "rejectFile", false, 1, "{path}",
513                                  description, false, true, true, false);
514    rejectFile.addLongIdentifier("reject-file", true);
515    parser.addArgument(rejectFile);
516
517    description = "The path to a directory containing one or more LDIF files " +
518                  "with the schema information to use.  If this is provided, " +
519                  "then no LDAP communication will be performed.";
520    schemaDirectory = new FileArgument(null, "schemaDirectory", false, 1,
521         "{path}", description, true, true, false, true);
522    schemaDirectory.addLongIdentifier("schema-directory", true);
523    parser.addArgument(schemaDirectory);
524
525    description = "The number of threads to use when processing the LDIF file.";
526    numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}",
527         description, 1, Integer.MAX_VALUE, 1);
528    numThreads.addLongIdentifier("num-threads", true);
529    parser.addArgument(numThreads);
530
531    description = "Ignore validation failures due to entries containing " +
532                  "duplicate values for the same attribute.";
533    ignoreDuplicateValues =
534         new BooleanArgument(null, "ignoreDuplicateValues", description);
535    ignoreDuplicateValues.setArgumentGroupName(
536         "Validation Strictness Arguments");
537    ignoreDuplicateValues.addLongIdentifier("ignore-duplicate-values", true);
538    parser.addArgument(ignoreDuplicateValues);
539
540    description = "Ignore validation failures due to object classes not " +
541                  "defined in the schema.";
542    ignoreUndefinedObjectClasses =
543         new BooleanArgument(null, "ignoreUndefinedObjectClasses", description);
544    ignoreUndefinedObjectClasses.setArgumentGroupName(
545         "Validation Strictness Arguments");
546    ignoreUndefinedObjectClasses.addLongIdentifier(
547         "ignore-undefined-object-classes", true);
548    parser.addArgument(ignoreUndefinedObjectClasses);
549
550    description = "Ignore validation failures due to attributes not defined " +
551                  "in the schema.";
552    ignoreUndefinedAttributes =
553         new BooleanArgument(null, "ignoreUndefinedAttributes", description);
554    ignoreUndefinedAttributes.setArgumentGroupName(
555         "Validation Strictness Arguments");
556    ignoreUndefinedAttributes.addLongIdentifier("ignore-undefined-attributes",
557         true);
558    parser.addArgument(ignoreUndefinedAttributes);
559
560    description = "Ignore validation failures due to entries with malformed " +
561                  "DNs.";
562    ignoreMalformedDNs =
563         new BooleanArgument(null, "ignoreMalformedDNs", description);
564    ignoreMalformedDNs.setArgumentGroupName("Validation Strictness Arguments");
565    ignoreMalformedDNs.addLongIdentifier("ignore-malformed-dns", true);
566    parser.addArgument(ignoreMalformedDNs);
567
568    description = "Ignore validation failures due to entries with RDN " +
569                  "attribute values that are missing from the set of entry " +
570                  "attributes.";
571    ignoreMissingRDNValues =
572         new BooleanArgument(null, "ignoreMissingRDNValues", description);
573    ignoreMissingRDNValues.setArgumentGroupName(
574         "Validation Strictness Arguments");
575    ignoreMissingRDNValues.addLongIdentifier("ignore-missing-rdn-values", true);
576    parser.addArgument(ignoreMissingRDNValues);
577
578    description = "Ignore validation failures due to entries without exactly " +
579                  "structural object class.";
580    ignoreStructuralObjectClasses =
581         new BooleanArgument(null, "ignoreStructuralObjectClasses",
582                             description);
583    ignoreStructuralObjectClasses.setArgumentGroupName(
584         "Validation Strictness Arguments");
585    ignoreStructuralObjectClasses.addLongIdentifier(
586         "ignore-structural-object-classes", true);
587    parser.addArgument(ignoreStructuralObjectClasses);
588
589    description = "Ignore validation failures due to entries with object " +
590                  "classes that are not allowed.";
591    ignoreProhibitedObjectClasses =
592         new BooleanArgument(null, "ignoreProhibitedObjectClasses",
593                             description);
594    ignoreProhibitedObjectClasses.setArgumentGroupName(
595         "Validation Strictness Arguments");
596    ignoreProhibitedObjectClasses.addLongIdentifier(
597         "ignore-prohibited-object-classes", true);
598    parser.addArgument(ignoreProhibitedObjectClasses);
599
600    description = "Ignore validation failures due to entries that are " +
601                  "one or more superior object classes.";
602    ignoreMissingSuperiorObjectClasses =
603         new BooleanArgument(null, "ignoreMissingSuperiorObjectClasses",
604              description);
605    ignoreMissingSuperiorObjectClasses.setArgumentGroupName(
606         "Validation Strictness Arguments");
607    ignoreMissingSuperiorObjectClasses.addLongIdentifier(
608         "ignore-missing-superior-object-classes", true);
609    parser.addArgument(ignoreMissingSuperiorObjectClasses);
610
611    description = "Ignore validation failures due to entries with attributes " +
612                  "that are not allowed.";
613    ignoreProhibitedAttributes =
614         new BooleanArgument(null, "ignoreProhibitedAttributes", description);
615    ignoreProhibitedAttributes.setArgumentGroupName(
616         "Validation Strictness Arguments");
617    ignoreProhibitedAttributes.addLongIdentifier(
618         "ignore-prohibited-attributes", true);
619    parser.addArgument(ignoreProhibitedAttributes);
620
621    description = "Ignore validation failures due to entries missing " +
622                  "required attributes.";
623    ignoreMissingAttributes =
624         new BooleanArgument(null, "ignoreMissingAttributes", description);
625    ignoreMissingAttributes.setArgumentGroupName(
626         "Validation Strictness Arguments");
627    ignoreMissingAttributes.addLongIdentifier("ignore-missing-attributes",
628         true);
629    parser.addArgument(ignoreMissingAttributes);
630
631    description = "Ignore validation failures due to entries with multiple " +
632                  "values for single-valued attributes.";
633    ignoreSingleValuedAttributes =
634         new BooleanArgument(null, "ignoreSingleValuedAttributes", description);
635    ignoreSingleValuedAttributes.setArgumentGroupName(
636         "Validation Strictness Arguments");
637    ignoreSingleValuedAttributes.addLongIdentifier(
638         "ignore-single-valued-attributes", true);
639    parser.addArgument(ignoreSingleValuedAttributes);
640
641    description = "Ignore validation failures due to entries with attribute " +
642                  "values that violate their associated syntax.  If this is " +
643                  "provided, then no attribute syntax violations will be " +
644                  "flagged.  If this is not provided, then all attribute " +
645                  "syntax violations will be flagged except for violations " +
646                  "in those attributes excluded by the " +
647                  "--ignoreSyntaxViolationsForAttribute argument.";
648    ignoreAttributeSyntax =
649         new BooleanArgument(null, "ignoreAttributeSyntax", description);
650    ignoreAttributeSyntax.setArgumentGroupName(
651         "Validation Strictness Arguments");
652    ignoreAttributeSyntax.addLongIdentifier("ignore-attribute-syntax", true);
653    parser.addArgument(ignoreAttributeSyntax);
654
655    description = "The name or OID of an attribute for which to ignore " +
656                  "validation failures due to violations of the associated " +
657                  "attribute syntax.  This argument can only be used if the " +
658                  "--ignoreAttributeSyntax argument is not provided.";
659    ignoreSyntaxViolationsForAttribute = new StringArgument(null,
660         "ignoreSyntaxViolationsForAttribute", false, 0, "{attr}", description);
661    ignoreSyntaxViolationsForAttribute.setArgumentGroupName(
662         "Validation Strictness Arguments");
663    ignoreSyntaxViolationsForAttribute.addLongIdentifier(
664         "ignore-syntax-violations-for-attribute", true);
665    parser.addArgument(ignoreSyntaxViolationsForAttribute);
666
667    description = "Ignore validation failures due to entries with RDNs " +
668                  "that violate the associated name form definition.";
669    ignoreNameForms = new BooleanArgument(null, "ignoreNameForms", description);
670    ignoreNameForms.setArgumentGroupName("Validation Strictness Arguments");
671    ignoreNameForms.addLongIdentifier("ignore-name-forms", true);
672    parser.addArgument(ignoreNameForms);
673
674
675    // The ignoreAttributeSyntax and ignoreAttributeSyntaxForAttribute arguments
676    // cannot be used together.
677    parser.addExclusiveArgumentSet(ignoreAttributeSyntax,
678         ignoreSyntaxViolationsForAttribute);
679  }
680
681
682
683  /**
684   * Performs the actual processing for this tool.  In this case, it gets a
685   * connection to the directory server and uses it to retrieve the server
686   * schema.  It then reads the LDIF file and validates each entry accordingly.
687   *
688   * @return  The result code for the processing that was performed.
689   */
690  @Override()
691  @NotNull()
692  public ResultCode doToolProcessing()
693  {
694    // Get the connection to the directory server and use it to read the schema.
695    final Schema schema;
696    if (schemaDirectory.isPresent())
697    {
698      final File schemaDir = schemaDirectory.getValue();
699
700      try
701      {
702        final TreeMap<String,File> fileMap = new TreeMap<>();
703        for (final File f : schemaDir.listFiles())
704        {
705          final String name = f.getName();
706          if (f.isFile() && name.endsWith(".ldif"))
707          {
708            fileMap.put(name, f);
709          }
710        }
711
712        if (fileMap.isEmpty())
713        {
714          err("No LDIF files found in directory " +
715              schemaDir.getAbsolutePath());
716          return ResultCode.PARAM_ERROR;
717        }
718
719        final ArrayList<File> fileList = new ArrayList<>(fileMap.values());
720        schema = Schema.getSchema(fileList);
721      }
722      catch (final Exception e)
723      {
724        Debug.debugException(e);
725        err("Unable to read schema from files in directory " +
726            schemaDir.getAbsolutePath() + ":  " +
727             StaticUtils.getExceptionMessage(e));
728        return ResultCode.LOCAL_ERROR;
729      }
730    }
731    else
732    {
733      try
734      {
735        final LDAPConnection connection = getConnection();
736        schema = connection.getSchema();
737        connection.close();
738      }
739      catch (final LDAPException le)
740      {
741        Debug.debugException(le);
742        err("Unable to connect to the directory server and read the schema:  ",
743            le.getMessage());
744        return le.getResultCode();
745      }
746    }
747
748
749    // Get the encryption passphrase, if it was provided.
750    String encryptionPassphrase = null;
751    if (encryptionPassphraseFile.isPresent())
752    {
753      try
754      {
755        encryptionPassphrase = ToolUtils.readEncryptionPassphraseFromFile(
756             encryptionPassphraseFile.getValue());
757      }
758      catch (final LDAPException e)
759      {
760        Debug.debugException(e);
761        err(e.getMessage());
762        return e.getResultCode();
763      }
764    }
765
766
767    // Create the entry validator and initialize its configuration.
768    entryValidator = new EntryValidator(schema);
769    entryValidator.setCheckAttributeSyntax(!ignoreAttributeSyntax.isPresent());
770    entryValidator.setCheckMalformedDNs(!ignoreMalformedDNs.isPresent());
771    entryValidator.setCheckEntryMissingRDNValues(
772         !ignoreMissingRDNValues.isPresent());
773    entryValidator.setCheckMissingAttributes(
774         !ignoreMissingAttributes.isPresent());
775    entryValidator.setCheckNameForms(!ignoreNameForms.isPresent());
776    entryValidator.setCheckProhibitedAttributes(
777         !ignoreProhibitedAttributes.isPresent());
778    entryValidator.setCheckProhibitedObjectClasses(
779         !ignoreProhibitedObjectClasses.isPresent());
780    entryValidator.setCheckMissingSuperiorObjectClasses(
781         !ignoreMissingSuperiorObjectClasses.isPresent());
782    entryValidator.setCheckSingleValuedAttributes(
783         !ignoreSingleValuedAttributes.isPresent());
784    entryValidator.setCheckStructuralObjectClasses(
785         !ignoreStructuralObjectClasses.isPresent());
786    entryValidator.setCheckUndefinedAttributes(
787         !ignoreUndefinedAttributes.isPresent());
788    entryValidator.setCheckUndefinedObjectClasses(
789         !ignoreUndefinedObjectClasses.isPresent());
790
791    if (ignoreSyntaxViolationsForAttribute.isPresent())
792    {
793      entryValidator.setIgnoreSyntaxViolationAttributeTypes(
794           ignoreSyntaxViolationsForAttribute.getValues());
795    }
796
797
798    // Create an LDIF reader that can be used to read through the LDIF file.
799    final LDIFReader ldifReader;
800    rejectWriter = null;
801    try
802    {
803      InputStream inputStream = new FileInputStream(ldifFile.getValue());
804
805      inputStream = ToolUtils.getPossiblyPassphraseEncryptedInputStream(
806           inputStream, encryptionPassphrase, false,
807           "LDIF file '" + ldifFile.getValue().getPath() +
808                "' is encrypted.  Please enter the encryption passphrase:",
809             "ERROR:  The provided passphrase was incorrect.",
810             getOut(), getErr()).getFirst();
811
812      if (isCompressed.isPresent())
813      {
814        inputStream = new GZIPInputStream(inputStream);
815      }
816      else
817      {
818        inputStream =
819             ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream);
820      }
821
822      ldifReader = new LDIFReader(inputStream, numThreads.getValue(), this);
823    }
824    catch (final Exception e)
825    {
826      Debug.debugException(e);
827      err("Unable to open the LDIF reader:  ",
828           StaticUtils.getExceptionMessage(e));
829      return ResultCode.LOCAL_ERROR;
830    }
831
832    ldifReader.setSchema(schema);
833    if (ignoreDuplicateValues.isPresent())
834    {
835      ldifReader.setDuplicateValueBehavior(DuplicateValueBehavior.STRIP);
836    }
837    else
838    {
839      ldifReader.setDuplicateValueBehavior(DuplicateValueBehavior.REJECT);
840    }
841
842    try
843    {
844      // Create an LDIF writer that can be used to write information about
845      // rejected entries.
846      try
847      {
848        if (rejectFile.isPresent())
849        {
850          rejectWriter = new LDIFWriter(rejectFile.getValue());
851        }
852      }
853      catch (final Exception e)
854      {
855        Debug.debugException(e);
856        err("Unable to create the reject writer:  ",
857             StaticUtils.getExceptionMessage(e));
858        return ResultCode.LOCAL_ERROR;
859      }
860
861      ResultCode resultCode = ResultCode.SUCCESS;
862      while (true)
863      {
864        try
865        {
866          final Entry e = ldifReader.readEntry();
867          if (e == null)
868          {
869            // Because we're performing parallel processing and returning null
870            // from the translate method, LDIFReader.readEntry() should never
871            // return a non-null value.  However, it can throw an LDIFException
872            // if it encounters an invalid entry, or an IOException if there's
873            // a problem reading from the file, so we should still iterate
874            // through all of the entries to catch and report on those problems.
875            break;
876          }
877        }
878        catch (final LDIFException le)
879        {
880          Debug.debugException(le);
881          malformedEntries.incrementAndGet();
882
883          if (resultCode == ResultCode.SUCCESS)
884          {
885            resultCode = ResultCode.DECODING_ERROR;
886          }
887
888          if (rejectWriter != null)
889          {
890            try
891            {
892              rejectWriter.writeComment(
893                   "Unable to parse an entry read from LDIF:", false, false);
894              if (le.mayContinueReading())
895              {
896                rejectWriter.writeComment(
897                     StaticUtils.getExceptionMessage(le), false, true);
898              }
899              else
900              {
901                rejectWriter.writeComment(
902                     StaticUtils.getExceptionMessage(le), false,
903                     false);
904                rejectWriter.writeComment("Unable to continue LDIF processing.",
905                     false, true);
906                err("Aborting LDIF processing:  ",
907                     StaticUtils.getExceptionMessage(le));
908                return ResultCode.LOCAL_ERROR;
909              }
910            }
911            catch (final IOException ioe)
912            {
913              Debug.debugException(ioe);
914              err("Unable to write to the reject file:",
915                  StaticUtils.getExceptionMessage(ioe));
916              err("LDIF parse failure that triggered the rejection:  ",
917                  StaticUtils.getExceptionMessage(le));
918              return ResultCode.LOCAL_ERROR;
919            }
920          }
921        }
922        catch (final IOException ioe)
923        {
924          Debug.debugException(ioe);
925
926          if (rejectWriter != null)
927          {
928            try
929            {
930              rejectWriter.writeComment("I/O error reading from LDIF:", false,
931                   false);
932              rejectWriter.writeComment(StaticUtils.getExceptionMessage(ioe),
933                   false, true);
934              return ResultCode.LOCAL_ERROR;
935            }
936            catch (final Exception ex)
937            {
938              Debug.debugException(ex);
939              err("I/O error reading from LDIF:",
940                   StaticUtils.getExceptionMessage(ioe));
941              return ResultCode.LOCAL_ERROR;
942            }
943          }
944        }
945      }
946
947      if (malformedEntries.get() > 0)
948      {
949        out(malformedEntries.get() + " entries were malformed and could not " +
950            "be read from the LDIF file.");
951      }
952
953      if (entryValidator.getInvalidEntries() > 0)
954      {
955        if (resultCode == ResultCode.SUCCESS)
956        {
957          resultCode = ResultCode.OBJECT_CLASS_VIOLATION;
958        }
959
960        for (final String s : entryValidator.getInvalidEntrySummary(true))
961        {
962          out(s);
963        }
964      }
965      else
966      {
967        if (malformedEntries.get() == 0)
968        {
969          out("No errors were encountered.");
970        }
971      }
972
973      return resultCode;
974    }
975    finally
976    {
977      try
978      {
979        ldifReader.close();
980      }
981      catch (final Exception e)
982      {
983        Debug.debugException(e);
984      }
985
986      try
987      {
988        if (rejectWriter != null)
989        {
990          rejectWriter.close();
991        }
992      }
993      catch (final Exception e)
994      {
995        Debug.debugException(e);
996      }
997    }
998  }
999
1000
1001
1002  /**
1003   * Examines the provided entry to determine whether it conforms to the
1004   * server schema.
1005   *
1006   * @param  entry           The entry to be examined.
1007   * @param  firstLineNumber The line number of the LDIF source on which the
1008   *                         provided entry begins.
1009   *
1010   * @return  The updated entry.  This method will always return {@code null}
1011   *          because all of the real processing needed for the entry is
1012   *          performed in this method and the entry isn't needed any more
1013   *          after this method is done.
1014   */
1015  @Override()
1016  @Nullable()
1017  public Entry translate(@NotNull final Entry entry, final long firstLineNumber)
1018  {
1019    final ArrayList<String> invalidReasons = new ArrayList<>(5);
1020    if (! entryValidator.entryIsValid(entry, invalidReasons))
1021    {
1022      if (rejectWriter != null)
1023      {
1024        synchronized (this)
1025        {
1026          try
1027          {
1028            rejectWriter.writeEntry(entry, listToString(invalidReasons));
1029          }
1030          catch (final IOException ioe)
1031          {
1032            Debug.debugException(ioe);
1033          }
1034        }
1035      }
1036    }
1037
1038    final long numEntries = entriesProcessed.incrementAndGet();
1039    if ((numEntries % 1000L) == 0L)
1040    {
1041      out("Processed ", numEntries, " entries.");
1042    }
1043
1044    return null;
1045  }
1046
1047
1048
1049  /**
1050   * Converts the provided list of strings into a single string.  It will
1051   * contain line breaks after all but the last element.
1052   *
1053   * @param  l  The list of strings to convert to a single string.
1054   *
1055   * @return  The string from the provided list, or {@code null} if the provided
1056   *          list is empty or {@code null}.
1057   */
1058  @Nullable()
1059  private static String listToString(@Nullable final List<String> l)
1060  {
1061    if ((l == null) || (l.isEmpty()))
1062    {
1063      return null;
1064    }
1065
1066    final StringBuilder buffer = new StringBuilder();
1067    final Iterator<String> iterator = l.iterator();
1068    while (iterator.hasNext())
1069    {
1070      buffer.append(iterator.next());
1071      if (iterator.hasNext())
1072      {
1073        buffer.append(EOL);
1074      }
1075    }
1076
1077    return buffer.toString();
1078  }
1079
1080
1081
1082  /**
1083   * {@inheritDoc}
1084   */
1085  @Override()
1086  @NotNull()
1087  public LinkedHashMap<String[],String> getExampleUsages()
1088  {
1089    final LinkedHashMap<String[],String> examples =
1090         new LinkedHashMap<>(StaticUtils.computeMapCapacity(2));
1091
1092    String[] args =
1093    {
1094      "--hostname", "server.example.com",
1095      "--port", "389",
1096      "--ldifFile", "data.ldif",
1097      "--rejectFile", "rejects.ldif",
1098      "--numThreads", "4"
1099    };
1100    String description =
1101         "Validate the contents of the 'data.ldif' file using the schema " +
1102         "defined in the specified directory server using four concurrent " +
1103         "threads.  All types of validation will be performed, and " +
1104         "information about any errors will be written to the 'rejects.ldif' " +
1105         "file.";
1106    examples.put(args, description);
1107
1108
1109    args = new String[]
1110    {
1111      "--schemaDirectory", "/ds/config/schema",
1112      "--ldifFile", "data.ldif",
1113      "--rejectFile", "rejects.ldif",
1114      "--ignoreStructuralObjectClasses",
1115      "--ignoreAttributeSyntax"
1116    };
1117    description =
1118         "Validate the contents of the 'data.ldif' file using the schema " +
1119         "defined in LDIF files contained in the /ds/config/schema directory " +
1120         "using a single thread.  Any errors resulting from entries that do " +
1121         "not have exactly one structural object class or from values which " +
1122         "violate the syntax for their associated attribute types will be " +
1123         "ignored.  Information about any other failures will be written to " +
1124         "the 'rejects.ldif' file.";
1125    examples.put(args, description);
1126
1127    return examples;
1128  }
1129
1130
1131
1132  /**
1133   * @return EntryValidator
1134   *
1135   * Returns the EntryValidator
1136   */
1137  @Nullable()
1138  public EntryValidator getEntryValidator()
1139  {
1140    return entryValidator;
1141  }
1142}