001/*
002 * Copyright 2008-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2016 UnboundID Corp.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.ldap.sdk.examples;
022
023
024
025import java.io.File;
026import java.io.FileInputStream;
027import java.io.InputStream;
028import java.io.IOException;
029import java.io.OutputStream;
030import java.util.ArrayList;
031import java.util.Iterator;
032import java.util.TreeMap;
033import java.util.LinkedHashMap;
034import java.util.List;
035import java.util.concurrent.atomic.AtomicLong;
036import java.util.zip.GZIPInputStream;
037
038import com.unboundid.ldap.sdk.Entry;
039import com.unboundid.ldap.sdk.LDAPConnection;
040import com.unboundid.ldap.sdk.LDAPException;
041import com.unboundid.ldap.sdk.ResultCode;
042import com.unboundid.ldap.sdk.Version;
043import com.unboundid.ldap.sdk.schema.Schema;
044import com.unboundid.ldap.sdk.schema.EntryValidator;
045import com.unboundid.ldif.DuplicateValueBehavior;
046import com.unboundid.ldif.LDIFException;
047import com.unboundid.ldif.LDIFReader;
048import com.unboundid.ldif.LDIFReaderEntryTranslator;
049import com.unboundid.ldif.LDIFWriter;
050import com.unboundid.util.LDAPCommandLineTool;
051import com.unboundid.util.ThreadSafety;
052import com.unboundid.util.ThreadSafetyLevel;
053import com.unboundid.util.args.ArgumentException;
054import com.unboundid.util.args.ArgumentParser;
055import com.unboundid.util.args.BooleanArgument;
056import com.unboundid.util.args.FileArgument;
057import com.unboundid.util.args.IntegerArgument;
058
059import static com.unboundid.util.StaticUtils.*;
060
061
062
063/**
064 * This class provides a simple tool that can be used to validate that the
065 * contents of an LDIF file are valid.  This includes ensuring that the contents
066 * can be parsed as valid LDIF, and it can also ensure that the LDIF content
067 * conforms to the server schema.  It will obtain the schema by connecting to
068 * the server and retrieving the default schema (i.e., the schema which governs
069 * the root DSE).  By default, a thorough set of validation will be performed,
070 * but it is possible to disable certain types of validation.
071 * <BR><BR>
072 * Some of the APIs demonstrated by this example include:
073 * <UL>
074 *   <LI>Argument Parsing (from the {@code com.unboundid.util.args}
075 *       package)</LI>
076 *   <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util}
077 *       package)</LI>
078 *   <LI>LDIF Processing (from the {@code com.unboundid.ldif} package)</LI>
079 *   <LI>Schema Parsing (from the {@code com.unboundid.ldap.sdk.schema}
080 *       package)</LI>
081 * </UL>
082 * <BR><BR>
083 * Supported arguments include those allowed by the {@link LDAPCommandLineTool}
084 * class (to obtain the information to use to connect to the server to read the
085 * schema), as well as the following additional arguments:
086 * <UL>
087 *   <LI>"--schemaDirectory {path}" -- specifies the path to a directory
088 *       containing files with schema definitions.  If this argument is
089 *       provided, then no attempt will be made to communicate with a directory
090 *       server.</LI>
091 *   <LI>"-f {path}" or "--ldifFile {path}" -- specifies the path to the LDIF
092 *       file to be validated.</LI>
093 *   <LI>"-c" or "--isCompressed" -- indicates that the LDIF file is
094 *       compressed.</LI>
095 *   <LI>"-R {path}" or "--rejectFile {path}" -- specifies the path to the file
096 *       to be written with information about all entries that failed
097 *       validation.</LI>
098 *   <LI>"-t {num}" or "--numThreads {num}" -- specifies the number of
099 *       concurrent threads to use when processing the LDIF.  If this is not
100 *       provided, then a default of one thread will be used.</LI>
101 *   <LI>"--ignoreUndefinedObjectClasses" -- indicates that the validation
102 *       process should ignore validation failures due to entries that contain
103 *       object classes not defined in the server schema.</LI>
104 *   <LI>"--ignoreUndefinedAttributes" -- indicates that the validation process
105 *       should ignore validation failures due to entries that contain
106 *       attributes not defined in the server schema.</LI>
107 *   <LI>"--ignoreMalformedDNs" -- indicates that the validation process should
108 *       ignore validation failures due to entries with malformed DNs.</LI>
109 *   <LI>"--ignoreStructuralObjectClasses" -- indicates that the validation
110 *       process should ignore validation failures due to entries that either do
111 *       not have a structural object class or that have multiple structural
112 *       object classes.</LI>
113 *   <LI>"--ignoreProhibitedObjectClasses" -- indicates that the validation
114 *       process should ignore validation failures due to entries containing
115 *       auxiliary classes that are not allowed by a DIT content rule, or
116 *       abstract classes that are not subclassed by an auxiliary or structural
117 *       class contained in the entry.</LI>
118 *   <LI>"--ignoreProhibitedAttributes" -- indicates that the validation process
119 *       should ignore validation failures due to entries including attributes
120 *       that are not allowed or are explicitly prohibited by a DIT content
121 *       rule.</LI>
122 *   <LI>"--ignoreMissingAttributes" -- indicates that the validation process
123 *       should ignore validation failures due to entries missing required
124 *       attributes.</LI>
125 *   <LI>"--ignoreSingleValuedAttributes" -- indicates that the validation
126 *       process should ignore validation failures due to single-valued
127 *       attributes containing multiple values.</LI>
128 *   <LI>"--ignoreAttributeSyntax" -- indicates that the validation process
129 *       should ignore validation failures due to attribute values which violate
130 *       the associated attribute syntax.</LI>
131 *   <LI>"--ignoreNameForms" -- indicates that the validation process should
132 *       ignore validation failures due to name form violations (in which the
133 *       entry's RDN does not comply with the associated name form).</LI>
134 * </UL>
135 */
136@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
137public final class ValidateLDIF
138       extends LDAPCommandLineTool
139       implements LDIFReaderEntryTranslator
140{
141  /**
142   * The end-of-line character for this platform.
143   */
144  private static final String EOL = System.getProperty("line.separator", "\n");
145
146
147
148  // The arguments used by this program.
149  private BooleanArgument ignoreDuplicateValues;
150  private BooleanArgument ignoreUndefinedObjectClasses;
151  private BooleanArgument ignoreUndefinedAttributes;
152  private BooleanArgument ignoreMalformedDNs;
153  private BooleanArgument ignoreMissingSuperiorObjectClasses;
154  private BooleanArgument ignoreStructuralObjectClasses;
155  private BooleanArgument ignoreProhibitedObjectClasses;
156  private BooleanArgument ignoreProhibitedAttributes;
157  private BooleanArgument ignoreMissingAttributes;
158  private BooleanArgument ignoreSingleValuedAttributes;
159  private BooleanArgument ignoreAttributeSyntax;
160  private BooleanArgument ignoreNameForms;
161  private BooleanArgument isCompressed;
162  private FileArgument    schemaDirectory;
163  private FileArgument    ldifFile;
164  private FileArgument    rejectFile;
165  private IntegerArgument numThreads;
166
167  // The counter used to keep track of the number of entries processed.
168  private final AtomicLong entriesProcessed = new AtomicLong(0L);
169
170  // The counter used to keep track of the number of entries that could not be
171  // parsed as valid entries.
172  private final AtomicLong malformedEntries = new AtomicLong(0L);
173
174  // The entry validator that will be used to validate the entries.
175  private EntryValidator entryValidator;
176
177  // The LDIF writer that will be used to write rejected entries.
178  private LDIFWriter rejectWriter;
179
180
181
182  /**
183   * Parse the provided command line arguments and make the appropriate set of
184   * changes.
185   *
186   * @param  args  The command line arguments provided to this program.
187   */
188  public static void main(final String[] args)
189  {
190    final ResultCode resultCode = main(args, System.out, System.err);
191    if (resultCode != ResultCode.SUCCESS)
192    {
193      System.exit(resultCode.intValue());
194    }
195  }
196
197
198
199  /**
200   * Parse the provided command line arguments and make the appropriate set of
201   * changes.
202   *
203   * @param  args       The command line arguments provided to this program.
204   * @param  outStream  The output stream to which standard out should be
205   *                    written.  It may be {@code null} if output should be
206   *                    suppressed.
207   * @param  errStream  The output stream to which standard error should be
208   *                    written.  It may be {@code null} if error messages
209   *                    should be suppressed.
210   *
211   * @return  A result code indicating whether the processing was successful.
212   */
213  public static ResultCode main(final String[] args,
214                                final OutputStream outStream,
215                                final OutputStream errStream)
216  {
217    final ValidateLDIF validateLDIF = new ValidateLDIF(outStream, errStream);
218    return validateLDIF.runTool(args);
219  }
220
221
222
223  /**
224   * Creates a new instance of this tool.
225   *
226   * @param  outStream  The output stream to which standard out should be
227   *                    written.  It may be {@code null} if output should be
228   *                    suppressed.
229   * @param  errStream  The output stream to which standard error should be
230   *                    written.  It may be {@code null} if error messages
231   *                    should be suppressed.
232   */
233  public ValidateLDIF(final OutputStream outStream,
234                      final OutputStream errStream)
235  {
236    super(outStream, errStream);
237  }
238
239
240
241  /**
242   * Retrieves the name for this tool.
243   *
244   * @return  The name for this tool.
245   */
246  @Override()
247  public String getToolName()
248  {
249    return "validate-ldif";
250  }
251
252
253
254  /**
255   * Retrieves the description for this tool.
256   *
257   * @return  The description for this tool.
258   */
259  @Override()
260  public String getToolDescription()
261  {
262    return "Validate the contents of an LDIF file " +
263           "against the server schema.";
264  }
265
266
267
268  /**
269   * Retrieves the version string for this tool.
270   *
271   * @return  The version string for this tool.
272   */
273  @Override()
274  public String getToolVersion()
275  {
276    return Version.NUMERIC_VERSION_STRING;
277  }
278
279
280
281  /**
282   * Indicates whether this tool should provide support for an interactive mode,
283   * in which the tool offers a mode in which the arguments can be provided in
284   * a text-driven menu rather than requiring them to be given on the command
285   * line.  If interactive mode is supported, it may be invoked using the
286   * "--interactive" argument.  Alternately, if interactive mode is supported
287   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
288   * interactive mode may be invoked by simply launching the tool without any
289   * arguments.
290   *
291   * @return  {@code true} if this tool supports interactive mode, or
292   *          {@code false} if not.
293   */
294  @Override()
295  public boolean supportsInteractiveMode()
296  {
297    return true;
298  }
299
300
301
302  /**
303   * Indicates whether this tool defaults to launching in interactive mode if
304   * the tool is invoked without any command-line arguments.  This will only be
305   * used if {@link #supportsInteractiveMode()} returns {@code true}.
306   *
307   * @return  {@code true} if this tool defaults to using interactive mode if
308   *          launched without any command-line arguments, or {@code false} if
309   *          not.
310   */
311  @Override()
312  public boolean defaultsToInteractiveMode()
313  {
314    return true;
315  }
316
317
318
319  /**
320   * Indicates whether this tool supports the use of a properties file for
321   * specifying default values for arguments that aren't specified on the
322   * command line.
323   *
324   * @return  {@code true} if this tool supports the use of a properties file
325   *          for specifying default values for arguments that aren't specified
326   *          on the command line, or {@code false} if not.
327   */
328  @Override()
329  public boolean supportsPropertiesFile()
330  {
331    return true;
332  }
333
334
335
336  /**
337   * Indicates whether the LDAP-specific arguments should include alternate
338   * versions of all long identifiers that consist of multiple words so that
339   * they are available in both camelCase and dash-separated versions.
340   *
341   * @return  {@code true} if this tool should provide multiple versions of
342   *          long identifiers for LDAP-specific arguments, or {@code false} if
343   *          not.
344   */
345  @Override()
346  protected boolean includeAlternateLongIdentifiers()
347  {
348    return true;
349  }
350
351
352
353  /**
354   * Adds the arguments used by this program that aren't already provided by the
355   * generic {@code LDAPCommandLineTool} framework.
356   *
357   * @param  parser  The argument parser to which the arguments should be added.
358   *
359   * @throws  ArgumentException  If a problem occurs while adding the arguments.
360   */
361  @Override()
362  public void addNonLDAPArguments(final ArgumentParser parser)
363         throws ArgumentException
364  {
365    String description = "The path to the LDIF file to process.";
366    ldifFile = new FileArgument('f', "ldifFile", true, 1, "{path}", description,
367                                true, true, true, false);
368    ldifFile.addLongIdentifier("ldif-file");
369    parser.addArgument(ldifFile);
370
371    description = "Indicates that the specified LDIF file is compressed " +
372                  "using gzip compression.";
373    isCompressed = new BooleanArgument('c', "isCompressed", description);
374    isCompressed.addLongIdentifier("is-compressed");
375    parser.addArgument(isCompressed);
376
377    description = "The path to the file to which rejected entries should be " +
378                  "written.";
379    rejectFile = new FileArgument('R', "rejectFile", false, 1, "{path}",
380                                  description, false, true, true, false);
381    rejectFile.addLongIdentifier("reject-file");
382    parser.addArgument(rejectFile);
383
384    description = "The path to a directory containing one or more LDIF files " +
385                  "with the schema information to use.  If this is provided, " +
386                  "then no LDAP communication will be performed.";
387    schemaDirectory = new FileArgument(null, "schemaDirectory", false, 1,
388         "{path}", description, true, true, false, true);
389    schemaDirectory.addLongIdentifier("schema-directory");
390    parser.addArgument(schemaDirectory);
391
392    description = "The number of threads to use when processing the LDIF file.";
393    numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}",
394         description, 1, Integer.MAX_VALUE, 1);
395    numThreads.addLongIdentifier("num-threads");
396    parser.addArgument(numThreads);
397
398    description = "Ignore validation failures due to entries containing " +
399                  "duplicate values for the same attribute.";
400    ignoreDuplicateValues =
401         new BooleanArgument(null, "ignoreDuplicateValues", description);
402    ignoreDuplicateValues.setArgumentGroupName(
403         "Validation Strictness Arguments");
404    ignoreDuplicateValues.addLongIdentifier("ignore-duplicate-values");
405    parser.addArgument(ignoreDuplicateValues);
406
407    description = "Ignore validation failures due to object classes not " +
408                  "defined in the schema.";
409    ignoreUndefinedObjectClasses =
410         new BooleanArgument(null, "ignoreUndefinedObjectClasses", description);
411    ignoreUndefinedObjectClasses.setArgumentGroupName(
412         "Validation Strictness Arguments");
413    ignoreUndefinedObjectClasses.addLongIdentifier(
414         "ignore-undefined-object-classes");
415    parser.addArgument(ignoreUndefinedObjectClasses);
416
417    description = "Ignore validation failures due to attributes not defined " +
418                  "in the schema.";
419    ignoreUndefinedAttributes =
420         new BooleanArgument(null, "ignoreUndefinedAttributes", description);
421    ignoreUndefinedAttributes.setArgumentGroupName(
422         "Validation Strictness Arguments");
423    ignoreUndefinedAttributes.addLongIdentifier("ignore-undefined-attributes");
424    parser.addArgument(ignoreUndefinedAttributes);
425
426    description = "Ignore validation failures due to entries with malformed " +
427                  "DNs.";
428    ignoreMalformedDNs =
429         new BooleanArgument(null, "ignoreMalformedDNs", description);
430    ignoreMalformedDNs.setArgumentGroupName("Validation Strictness Arguments");
431    ignoreMalformedDNs.addLongIdentifier("ignore-malformed-dns");
432    parser.addArgument(ignoreMalformedDNs);
433
434    description = "Ignore validation failures due to entries without exactly " +
435                  "structural object class.";
436    ignoreStructuralObjectClasses =
437         new BooleanArgument(null, "ignoreStructuralObjectClasses",
438                             description);
439    ignoreStructuralObjectClasses.setArgumentGroupName(
440         "Validation Strictness Arguments");
441    ignoreStructuralObjectClasses.addLongIdentifier(
442         "ignore-structural-object-classes");
443    parser.addArgument(ignoreStructuralObjectClasses);
444
445    description = "Ignore validation failures due to entries with object " +
446                  "classes that are not allowed.";
447    ignoreProhibitedObjectClasses =
448         new BooleanArgument(null, "ignoreProhibitedObjectClasses",
449                             description);
450    ignoreProhibitedObjectClasses.setArgumentGroupName(
451         "Validation Strictness Arguments");
452    ignoreProhibitedObjectClasses.addLongIdentifier(
453         "ignore-prohibited-object-classes");
454    parser.addArgument(ignoreProhibitedObjectClasses);
455
456    description = "Ignore validation failures due to entries that are " +
457                  "one or more superior object classes.";
458    ignoreMissingSuperiorObjectClasses =
459         new BooleanArgument(null, "ignoreMissingSuperiorObjectClasses",
460              description);
461    ignoreMissingSuperiorObjectClasses.setArgumentGroupName(
462         "Validation Strictness Arguments");
463    ignoreMissingSuperiorObjectClasses.addLongIdentifier(
464         "ignore-missing-superior-object-classes");
465    parser.addArgument(ignoreMissingSuperiorObjectClasses);
466
467    description = "Ignore validation failures due to entries with attributes " +
468                  "that are not allowed.";
469    ignoreProhibitedAttributes =
470         new BooleanArgument(null, "ignoreProhibitedAttributes", description);
471    ignoreProhibitedAttributes.setArgumentGroupName(
472         "Validation Strictness Arguments");
473    ignoreProhibitedAttributes.addLongIdentifier(
474         "ignore-prohibited-attributes");
475    parser.addArgument(ignoreProhibitedAttributes);
476
477    description = "Ignore validation failures due to entries missing " +
478                  "required attributes.";
479    ignoreMissingAttributes =
480         new BooleanArgument(null, "ignoreMissingAttributes", description);
481    ignoreMissingAttributes.setArgumentGroupName(
482         "Validation Strictness Arguments");
483    ignoreMissingAttributes.addLongIdentifier("ignore-missing-attributes");
484    parser.addArgument(ignoreMissingAttributes);
485
486    description = "Ignore validation failures due to entries with multiple " +
487                  "values for single-valued attributes.";
488    ignoreSingleValuedAttributes =
489         new BooleanArgument(null, "ignoreSingleValuedAttributes", description);
490    ignoreSingleValuedAttributes.setArgumentGroupName(
491         "Validation Strictness Arguments");
492    ignoreSingleValuedAttributes.addLongIdentifier(
493         "ignore-single-valued-attributes");
494    parser.addArgument(ignoreSingleValuedAttributes);
495
496    description = "Ignore validation failures due to entries with attribute " +
497                  "values that violate their associated syntax.";
498    ignoreAttributeSyntax =
499         new BooleanArgument(null, "ignoreAttributeSyntax", description);
500    ignoreAttributeSyntax.setArgumentGroupName(
501         "Validation Strictness Arguments");
502    ignoreAttributeSyntax.addLongIdentifier("ignore-attribute-syntax");
503    parser.addArgument(ignoreAttributeSyntax);
504
505    description = "Ignore validation failures due to entries with RDNs " +
506                  "that violate the associated name form definition.";
507    ignoreNameForms = new BooleanArgument(null, "ignoreNameForms", description);
508    ignoreNameForms.setArgumentGroupName("Validation Strictness Arguments");
509    ignoreNameForms.addLongIdentifier("ignore-name-forms");
510    parser.addArgument(ignoreNameForms);
511  }
512
513
514
515  /**
516   * Performs the actual processing for this tool.  In this case, it gets a
517   * connection to the directory server and uses it to retrieve the server
518   * schema.  It then reads the LDIF file and validates each entry accordingly.
519   *
520   * @return  The result code for the processing that was performed.
521   */
522  @Override()
523  public ResultCode doToolProcessing()
524  {
525    // Get the connection to the directory server and use it to read the schema.
526    final Schema schema;
527    if (schemaDirectory.isPresent())
528    {
529      final File schemaDir = schemaDirectory.getValue();
530
531      try
532      {
533        final TreeMap<String,File> fileMap = new TreeMap<String,File>();
534        for (final File f : schemaDir.listFiles())
535        {
536          final String name = f.getName();
537          if (f.isFile() && name.endsWith(".ldif"))
538          {
539            fileMap.put(name, f);
540          }
541        }
542
543        if (fileMap.isEmpty())
544        {
545          err("No LDIF files found in directory " +
546              schemaDir.getAbsolutePath());
547          return ResultCode.PARAM_ERROR;
548        }
549
550        final ArrayList<File> fileList = new ArrayList<File>(fileMap.values());
551        schema = Schema.getSchema(fileList);
552      }
553      catch (Exception e)
554      {
555        err("Unable to read schema from files in directory " +
556            schemaDir.getAbsolutePath() + ":  " + getExceptionMessage(e));
557        return ResultCode.LOCAL_ERROR;
558      }
559    }
560    else
561    {
562      try
563      {
564        final LDAPConnection connection = getConnection();
565        schema = connection.getSchema();
566        connection.close();
567      }
568      catch (LDAPException le)
569      {
570        err("Unable to connect to the directory server and read the schema:  ",
571            le.getMessage());
572        return le.getResultCode();
573      }
574    }
575
576
577    // Create the entry validator and initialize its configuration.
578    entryValidator = new EntryValidator(schema);
579    entryValidator.setCheckAttributeSyntax(!ignoreAttributeSyntax.isPresent());
580    entryValidator.setCheckMalformedDNs(!ignoreMalformedDNs.isPresent());
581    entryValidator.setCheckMissingAttributes(
582         !ignoreMissingAttributes.isPresent());
583    entryValidator.setCheckNameForms(!ignoreNameForms.isPresent());
584    entryValidator.setCheckProhibitedAttributes(
585         !ignoreProhibitedAttributes.isPresent());
586    entryValidator.setCheckProhibitedObjectClasses(
587         !ignoreProhibitedObjectClasses.isPresent());
588    entryValidator.setCheckMissingSuperiorObjectClasses(
589         !ignoreMissingSuperiorObjectClasses.isPresent());
590    entryValidator.setCheckSingleValuedAttributes(
591         !ignoreSingleValuedAttributes.isPresent());
592    entryValidator.setCheckStructuralObjectClasses(
593         !ignoreStructuralObjectClasses.isPresent());
594    entryValidator.setCheckUndefinedAttributes(
595         !ignoreUndefinedAttributes.isPresent());
596    entryValidator.setCheckUndefinedObjectClasses(
597         !ignoreUndefinedObjectClasses.isPresent());
598
599
600    // Create an LDIF reader that can be used to read through the LDIF file.
601    final LDIFReader ldifReader;
602    rejectWriter = null;
603    try
604    {
605      InputStream inputStream = new FileInputStream(ldifFile.getValue());
606      if (isCompressed.isPresent())
607      {
608        inputStream = new GZIPInputStream(inputStream);
609      }
610      ldifReader = new LDIFReader(inputStream, numThreads.getValue(), this);
611    }
612    catch (Exception e)
613    {
614      err("Unable to open the LDIF reader:  ", getExceptionMessage(e));
615      return ResultCode.LOCAL_ERROR;
616    }
617
618    ldifReader.setSchema(schema);
619    if (ignoreDuplicateValues.isPresent())
620    {
621      ldifReader.setDuplicateValueBehavior(DuplicateValueBehavior.STRIP);
622    }
623    else
624    {
625      ldifReader.setDuplicateValueBehavior(DuplicateValueBehavior.REJECT);
626    }
627
628    try
629    {
630      // Create an LDIF writer that can be used to write information about
631      // rejected entries.
632      try
633      {
634        if (rejectFile.isPresent())
635        {
636          rejectWriter = new LDIFWriter(rejectFile.getValue());
637        }
638      }
639      catch (Exception e)
640      {
641        err("Unable to create the reject writer:  ", getExceptionMessage(e));
642        return ResultCode.LOCAL_ERROR;
643      }
644
645      ResultCode resultCode = ResultCode.SUCCESS;
646      while (true)
647      {
648        try
649        {
650          final Entry e = ldifReader.readEntry();
651          if (e == null)
652          {
653            // Because we're performing parallel processing and returning null
654            // from the translate method, LDIFReader.readEntry() should never
655            // return a non-null value.  However, it can throw an LDIFException
656            // if it encounters an invalid entry, or an IOException if there's
657            // a problem reading from the file, so we should still iterate
658            // through all of the entries to catch and report on those problems.
659            break;
660          }
661        }
662        catch (LDIFException le)
663        {
664          malformedEntries.incrementAndGet();
665
666          if (resultCode == ResultCode.SUCCESS)
667          {
668            resultCode = ResultCode.DECODING_ERROR;
669          }
670
671          if (rejectWriter != null)
672          {
673            try
674            {
675              rejectWriter.writeComment(
676                   "Unable to parse an entry read from LDIF:", false, false);
677              if (le.mayContinueReading())
678              {
679                rejectWriter.writeComment(getExceptionMessage(le), false, true);
680              }
681              else
682              {
683                rejectWriter.writeComment(getExceptionMessage(le), false,
684                                          false);
685                rejectWriter.writeComment("Unable to continue LDIF processing.",
686                                          false, true);
687                err("Aborting LDIF processing:  ", getExceptionMessage(le));
688                return ResultCode.LOCAL_ERROR;
689              }
690            }
691            catch (IOException ioe)
692            {
693              err("Unable to write to the reject file:",
694                  getExceptionMessage(ioe));
695              err("LDIF parse failure that triggered the rejection:  ",
696                  getExceptionMessage(le));
697              return ResultCode.LOCAL_ERROR;
698            }
699          }
700        }
701        catch (IOException ioe)
702        {
703
704          if (rejectWriter != null)
705          {
706            try
707            {
708              rejectWriter.writeComment("I/O error reading from LDIF:", false,
709                                        false);
710              rejectWriter.writeComment(getExceptionMessage(ioe), false,
711                                        true);
712              return ResultCode.LOCAL_ERROR;
713            }
714            catch (Exception ex)
715            {
716              err("I/O error reading from LDIF:", getExceptionMessage(ioe));
717              return ResultCode.LOCAL_ERROR;
718            }
719          }
720        }
721      }
722
723      if (malformedEntries.get() > 0)
724      {
725        out(malformedEntries.get() + " entries were malformed and could not " +
726            "be read from the LDIF file.");
727      }
728
729      if (entryValidator.getInvalidEntries() > 0)
730      {
731        if (resultCode == ResultCode.SUCCESS)
732        {
733          resultCode = ResultCode.OBJECT_CLASS_VIOLATION;
734        }
735
736        for (final String s : entryValidator.getInvalidEntrySummary(true))
737        {
738          out(s);
739        }
740      }
741      else
742      {
743        if (malformedEntries.get() == 0)
744        {
745          out("No errors were encountered.");
746        }
747      }
748
749      return resultCode;
750    }
751    finally
752    {
753      try
754      {
755        ldifReader.close();
756      }
757      catch (Exception e) {}
758
759      try
760      {
761        if (rejectWriter != null)
762        {
763          rejectWriter.close();
764        }
765      }
766      catch (Exception e) {}
767    }
768  }
769
770
771
772  /**
773   * Examines the provided entry to determine whether it conforms to the
774   * server schema.
775   *
776   * @param  entry           The entry to be examined.
777   * @param  firstLineNumber The line number of the LDIF source on which the
778   *                         provided entry begins.
779   *
780   * @return  The updated entry.  This method will always return {@code null}
781   *          because all of the real processing needed for the entry is
782   *          performed in this method and the entry isn't needed any more
783   *          after this method is done.
784   */
785  public Entry translate(final Entry entry, final long firstLineNumber)
786  {
787    final ArrayList<String> invalidReasons = new ArrayList<String>(5);
788    if (! entryValidator.entryIsValid(entry, invalidReasons))
789    {
790      if (rejectWriter != null)
791      {
792        synchronized (this)
793        {
794          try
795          {
796            rejectWriter.writeEntry(entry, listToString(invalidReasons));
797          }
798          catch (IOException ioe) {}
799        }
800      }
801    }
802
803    final long numEntries = entriesProcessed.incrementAndGet();
804    if ((numEntries % 1000L) == 0L)
805    {
806      out("Processed ", numEntries, " entries.");
807    }
808
809    return null;
810  }
811
812
813
814  /**
815   * Converts the provided list of strings into a single string.  It will
816   * contain line breaks after all but the last element.
817   *
818   * @param  l  The list of strings to convert to a single string.
819   *
820   * @return  The string from the provided list, or {@code null} if the provided
821   *          list is empty or {@code null}.
822   */
823  private static String listToString(final List<String> l)
824  {
825    if ((l == null) || (l.isEmpty()))
826    {
827      return null;
828    }
829
830    final StringBuilder buffer = new StringBuilder();
831    final Iterator<String> iterator = l.iterator();
832    while (iterator.hasNext())
833    {
834      buffer.append(iterator.next());
835      if (iterator.hasNext())
836      {
837        buffer.append(EOL);
838      }
839    }
840
841    return buffer.toString();
842  }
843
844
845
846  /**
847   * {@inheritDoc}
848   */
849  @Override()
850  public LinkedHashMap<String[],String> getExampleUsages()
851  {
852    final LinkedHashMap<String[],String> examples =
853         new LinkedHashMap<String[],String>(2);
854
855    String[] args =
856    {
857      "--hostname", "server.example.com",
858      "--port", "389",
859      "--ldifFile", "data.ldif",
860      "--rejectFile", "rejects.ldif",
861      "--numThreads", "4"
862    };
863    String description =
864         "Validate the contents of the 'data.ldif' file using the schema " +
865         "defined in the specified directory server using four concurrent " +
866         "threads.  All types of validation will be performed, and " +
867         "information about any errors will be written to the 'rejects.ldif' " +
868         "file.";
869    examples.put(args, description);
870
871
872    args = new String[]
873    {
874      "--schemaDirectory", "/ds/config/schema",
875      "--ldifFile", "data.ldif",
876      "--rejectFile", "rejects.ldif",
877      "--ignoreStructuralObjectClasses",
878      "--ignoreAttributeSyntax"
879    };
880    description =
881         "Validate the contents of the 'data.ldif' file using the schema " +
882         "defined in LDIF files contained in the /ds/config/schema directory " +
883         "using a single thread.  Any errors resulting from entries that do " +
884         "not have exactly one structural object class or from values which " +
885         "violate the syntax for their associated attribute types will be " +
886         "ignored.  Information about any other failures will be written to " +
887         "the 'rejects.ldif' file.";
888    examples.put(args, description);
889
890    return examples;
891  }
892
893
894
895  /**
896   * @return EntryValidator
897   *
898   * Returns the EntryValidator
899   */
900  public EntryValidator getEntryValidator()
901  {
902    return entryValidator;
903  }
904}