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     */
021    package com.unboundid.ldap.sdk.examples;
022    
023    
024    
025    import java.io.File;
026    import java.io.FileInputStream;
027    import java.io.InputStream;
028    import java.io.IOException;
029    import java.io.OutputStream;
030    import java.util.ArrayList;
031    import java.util.Iterator;
032    import java.util.TreeMap;
033    import java.util.LinkedHashMap;
034    import java.util.List;
035    import java.util.concurrent.atomic.AtomicLong;
036    import java.util.zip.GZIPInputStream;
037    
038    import com.unboundid.ldap.sdk.Entry;
039    import com.unboundid.ldap.sdk.LDAPConnection;
040    import com.unboundid.ldap.sdk.LDAPException;
041    import com.unboundid.ldap.sdk.ResultCode;
042    import com.unboundid.ldap.sdk.Version;
043    import com.unboundid.ldap.sdk.schema.Schema;
044    import com.unboundid.ldap.sdk.schema.EntryValidator;
045    import com.unboundid.ldif.DuplicateValueBehavior;
046    import com.unboundid.ldif.LDIFException;
047    import com.unboundid.ldif.LDIFReader;
048    import com.unboundid.ldif.LDIFReaderEntryTranslator;
049    import com.unboundid.ldif.LDIFWriter;
050    import com.unboundid.util.LDAPCommandLineTool;
051    import com.unboundid.util.ThreadSafety;
052    import com.unboundid.util.ThreadSafetyLevel;
053    import com.unboundid.util.args.ArgumentException;
054    import com.unboundid.util.args.ArgumentParser;
055    import com.unboundid.util.args.BooleanArgument;
056    import com.unboundid.util.args.FileArgument;
057    import com.unboundid.util.args.IntegerArgument;
058    
059    import 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)
137    public 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    }