001/*
002 * Copyright 2007-2020 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2007-2020 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2007-2020 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldif;
037
038
039
040import java.io.BufferedReader;
041import java.io.Closeable;
042import java.io.File;
043import java.io.FileInputStream;
044import java.io.InputStream;
045import java.io.InputStreamReader;
046import java.io.IOException;
047import java.nio.charset.StandardCharsets;
048import java.text.ParseException;
049import java.util.ArrayList;
050import java.util.Collection;
051import java.util.Iterator;
052import java.util.HashSet;
053import java.util.LinkedHashMap;
054import java.util.List;
055import java.util.Set;
056import java.util.concurrent.BlockingQueue;
057import java.util.concurrent.ArrayBlockingQueue;
058import java.util.concurrent.TimeUnit;
059import java.util.concurrent.atomic.AtomicBoolean;
060import java.nio.charset.Charset;
061
062import com.unboundid.asn1.ASN1OctetString;
063import com.unboundid.ldap.matchingrules.CaseIgnoreStringMatchingRule;
064import com.unboundid.ldap.matchingrules.MatchingRule;
065import com.unboundid.ldap.sdk.Attribute;
066import com.unboundid.ldap.sdk.Control;
067import com.unboundid.ldap.sdk.Entry;
068import com.unboundid.ldap.sdk.Modification;
069import com.unboundid.ldap.sdk.ModificationType;
070import com.unboundid.ldap.sdk.LDAPException;
071import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition;
072import com.unboundid.ldap.sdk.schema.Schema;
073import com.unboundid.util.AggregateInputStream;
074import com.unboundid.util.Base64;
075import com.unboundid.util.Debug;
076import com.unboundid.util.LDAPSDKThreadFactory;
077import com.unboundid.util.NotNull;
078import com.unboundid.util.Nullable;
079import com.unboundid.util.StaticUtils;
080import com.unboundid.util.ThreadSafety;
081import com.unboundid.util.ThreadSafetyLevel;
082import com.unboundid.util.Validator;
083import com.unboundid.util.parallel.AsynchronousParallelProcessor;
084import com.unboundid.util.parallel.Result;
085import com.unboundid.util.parallel.ParallelProcessor;
086import com.unboundid.util.parallel.Processor;
087
088import static com.unboundid.ldif.LDIFMessages.*;
089
090/**
091 * This class provides an LDIF reader, which can be used to read and decode
092 * entries and change records from a data source using the LDAP Data Interchange
093 * Format as per <A HREF="http://www.ietf.org/rfc/rfc2849.txt">RFC 2849</A>.
094 * <BR>
095 * This class is not synchronized.  If multiple threads read from the
096 * LDIFReader, they must be synchronized externally.
097 * <BR><BR>
098 * <H2>Example</H2>
099 * The following example iterates through all entries contained in an LDIF file
100 * and attempts to add them to a directory server:
101 * <PRE>
102 * LDIFReader ldifReader = new LDIFReader(pathToLDIFFile);
103 *
104 * int entriesRead = 0;
105 * int entriesAdded = 0;
106 * int errorsEncountered = 0;
107 * while (true)
108 * {
109 *   Entry entry;
110 *   try
111 *   {
112 *     entry = ldifReader.readEntry();
113 *     if (entry == null)
114 *     {
115 *       // All entries have been read.
116 *       break;
117 *     }
118 *
119 *     entriesRead++;
120 *   }
121 *   catch (LDIFException le)
122 *   {
123 *     errorsEncountered++;
124 *     if (le.mayContinueReading())
125 *     {
126 *       // A recoverable error occurred while attempting to read a change
127 *       // record, at or near line number le.getLineNumber()
128 *       // The entry will be skipped, but we'll try to keep reading from the
129 *       // LDIF file.
130 *       continue;
131 *     }
132 *     else
133 *     {
134 *       // An unrecoverable error occurred while attempting to read an entry
135 *       // at or near line number le.getLineNumber()
136 *       // No further LDIF processing will be performed.
137 *       break;
138 *     }
139 *   }
140 *   catch (IOException ioe)
141 *   {
142 *     // An I/O error occurred while attempting to read from the LDIF file.
143 *     // No further LDIF processing will be performed.
144 *     errorsEncountered++;
145 *     break;
146 *   }
147 *
148 *   LDAPResult addResult;
149 *   try
150 *   {
151 *     addResult = connection.add(entry);
152 *     // If we got here, then the change should have been processed
153 *     // successfully.
154 *     entriesAdded++;
155 *   }
156 *   catch (LDAPException le)
157 *   {
158 *     // If we got here, then the change attempt failed.
159 *     addResult = le.toLDAPResult();
160 *     errorsEncountered++;
161 *   }
162 * }
163 *
164 * ldifReader.close();
165 * </PRE>
166 */
167@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
168public final class LDIFReader
169       implements Closeable
170{
171  /**
172   * The default buffer size (128KB) that will be used when reading from the
173   * data source.
174   */
175  public static final int DEFAULT_BUFFER_SIZE = 128 * 1024;
176
177
178
179  /*
180   * When processing asynchronously, this determines how many of the allocated
181   * worker threads are used to parse each batch of read entries.
182   */
183  private static final int ASYNC_MIN_PER_PARSING_THREAD = 3;
184
185
186
187  /**
188   * When processing asynchronously, this specifies the size of the pending and
189   * completed queues.
190   */
191  private static final int ASYNC_QUEUE_SIZE = 500;
192
193
194
195  /**
196   * Special entry used internally to signal that the LDIFReaderEntryTranslator
197   * has signalled that a read Entry should be skipped by returning null,
198   * which normally implies EOF.
199   */
200  @NotNull private static final Entry SKIP_ENTRY = new Entry("cn=skipped");
201
202
203
204  /**
205   * The default base path that will be prepended to relative paths.  It will
206   * end with a trailing slash.
207   */
208  @NotNull private static final String DEFAULT_RELATIVE_BASE_PATH;
209  static
210  {
211    final File currentDir;
212    final String currentDirString = StaticUtils.getSystemProperty("user.dir");
213    if (currentDirString == null)
214    {
215      currentDir = new File(".");
216    }
217    else
218    {
219      currentDir = new File(currentDirString);
220    }
221
222    final String currentDirAbsolutePath = currentDir.getAbsolutePath();
223    if (currentDirAbsolutePath.endsWith(File.separator))
224    {
225      DEFAULT_RELATIVE_BASE_PATH = currentDirAbsolutePath;
226    }
227    else
228    {
229      DEFAULT_RELATIVE_BASE_PATH = currentDirAbsolutePath + File.separator;
230    }
231  }
232
233
234
235  // The buffered reader that will be used to read LDIF data.
236  @NotNull private final BufferedReader reader;
237
238  // The behavior that should be exhibited when encountering duplicate attribute
239  // values.
240  @NotNull private volatile DuplicateValueBehavior duplicateValueBehavior;
241
242  // A line number counter.
243  private long lineNumberCounter = 0;
244
245  // The change record translator to use, if any.
246  @Nullable private final LDIFReaderChangeRecordTranslator
247       changeRecordTranslator;
248
249  // The entry translator to use, if any.
250  @Nullable private final LDIFReaderEntryTranslator entryTranslator;
251
252  // The schema that will be used when processing, if applicable.
253  @Nullable private Schema schema;
254
255  // Specifies the base path that will be prepended to relative paths for file
256  // URLs.
257  @NotNull private volatile String relativeBasePath;
258
259  // The behavior that should be exhibited with regard to illegal trailing
260  // spaces in attribute values.
261  @NotNull private volatile TrailingSpaceBehavior trailingSpaceBehavior;
262
263  // True iff we are processing asynchronously.
264  private final boolean isAsync;
265
266  //
267  // The following only apply to asynchronous processing.
268  //
269
270  // Parses entries asynchronously.
271  @Nullable private final
272       AsynchronousParallelProcessor<UnparsedLDIFRecord,LDIFRecord> asyncParser;
273
274  // Set to true when the end of the input is reached.
275  @Nullable private final AtomicBoolean asyncParsingComplete;
276
277  // The records that have been read and parsed.
278  @Nullable private final BlockingQueue<Result<UnparsedLDIFRecord,LDIFRecord>>
279       asyncParsedRecords;
280
281
282
283  /**
284   * Creates a new LDIF reader that will read data from the specified file.
285   *
286   * @param  path  The path to the file from which the data is to be read.  It
287   *               must not be {@code null}.
288   *
289   * @throws  IOException  If a problem occurs while opening the file for
290   *                       reading.
291   */
292  public LDIFReader(@NotNull final String path)
293         throws IOException
294  {
295    this(new FileInputStream(path));
296  }
297
298
299
300  /**
301   * Creates a new LDIF reader that will read data from the specified file
302   * and parses the LDIF records asynchronously using the specified number of
303   * threads.
304   *
305   * @param  path  The path to the file from which the data is to be read.  It
306   *               must not be {@code null}.
307   * @param  numParseThreads  If this value is greater than zero, then the
308   *                          specified number of threads will be used to
309   *                          asynchronously read and parse the LDIF file.
310   *
311   * @throws  IOException  If a problem occurs while opening the file for
312   *                       reading.
313   *
314   * @see #LDIFReader(BufferedReader, int, LDIFReaderEntryTranslator)
315   *      constructor for more details about asynchronous processing.
316   */
317  public LDIFReader(@NotNull final String path, final int numParseThreads)
318         throws IOException
319  {
320    this(new FileInputStream(path), numParseThreads);
321  }
322
323
324
325  /**
326   * Creates a new LDIF reader that will read data from the specified file.
327   *
328   * @param  file  The file from which the data is to be read.  It must not be
329   *               {@code null}.
330   *
331   * @throws  IOException  If a problem occurs while opening the file for
332   *                       reading.
333   */
334  public LDIFReader(@NotNull final File file)
335         throws IOException
336  {
337    this(new FileInputStream(file));
338  }
339
340
341
342  /**
343   * Creates a new LDIF reader that will read data from the specified file
344   * and optionally parses the LDIF records asynchronously using the specified
345   * number of threads.
346   *
347   * @param  file             The file from which the data is to be read.  It
348   *                          must not be {@code null}.
349   * @param  numParseThreads  If this value is greater than zero, then the
350   *                          specified number of threads will be used to
351   *                          asynchronously read and parse the LDIF file.
352   *
353   * @throws  IOException  If a problem occurs while opening the file for
354   *                       reading.
355   */
356  public LDIFReader(@NotNull final File file, final int numParseThreads)
357         throws IOException
358  {
359    this(new FileInputStream(file), numParseThreads);
360  }
361
362
363
364  /**
365   * Creates a new LDIF reader that will read data from the specified files in
366   * the order in which they are provided and optionally parses the LDIF records
367   * asynchronously using the specified number of threads.
368   *
369   * @param  files            The files from which the data is to be read.  It
370   *                          must not be {@code null} or empty.
371   * @param  numParseThreads  If this value is greater than zero, then the
372   *                          specified number of threads will be used to
373   *                          asynchronously read and parse the LDIF file.
374   * @param entryTranslator   The LDIFReaderEntryTranslator to apply to entries
375   *                          before they are returned.  This is normally
376   *                          {@code null}, which causes entries to be returned
377   *                          unaltered. This is particularly useful when
378   *                          parsing the input file in parallel because the
379   *                          entry translation is also done in parallel.
380   *
381   * @throws  IOException  If a problem occurs while opening the file for
382   *                       reading.
383   */
384  public LDIFReader(@NotNull final File[] files, final int numParseThreads,
385                    @Nullable final LDIFReaderEntryTranslator entryTranslator)
386         throws IOException
387  {
388    this(files, numParseThreads, entryTranslator, null);
389  }
390
391
392
393  /**
394   * Creates a new LDIF reader that will read data from the specified files in
395   * the order in which they are provided and optionally parses the LDIF records
396   * asynchronously using the specified number of threads.
397   *
398   * @param  files                   The files from which the data is to be
399   *                                 read.  It must not be {@code null} or
400   *                                 empty.
401   * @param  numParseThreads         If this value is greater than zero, then
402   *                                 the specified number of threads will be
403   *                                 used to asynchronously read and parse the
404   *                                 LDIF file.
405   * @param  entryTranslator         The LDIFReaderEntryTranslator to apply to
406   *                                 entries before they are returned.  This is
407   *                                 normally {@code null}, which causes entries
408   *                                 to be returned unaltered.  This is
409   *                                 particularly useful when parsing the input
410   *                                 file in parallel because the entry
411   *                                 translation is also done in parallel.
412   * @param  changeRecordTranslator  The LDIFReaderChangeRecordTranslator to
413   *                                 apply to change records before they are
414   *                                 returned.  This is normally {@code null},
415   *                                 which causes change records to be returned
416   *                                 unaltered.  This is particularly useful
417   *                                 when parsing the input file in parallel
418   *                                 because the change record translation is
419   *                                 also done in parallel.
420   *
421   * @throws  IOException  If a problem occurs while opening the file for
422   *                       reading.
423   */
424  public LDIFReader(@NotNull final File[] files, final int numParseThreads,
425       @Nullable final LDIFReaderEntryTranslator entryTranslator,
426       @Nullable final LDIFReaderChangeRecordTranslator changeRecordTranslator)
427       throws IOException
428  {
429    this(files, numParseThreads, entryTranslator, changeRecordTranslator,
430         "UTF-8");
431  }
432
433
434
435  /**
436   * Creates a new LDIF reader that will read data from the specified files in
437   * the order in which they are provided and optionally parses the LDIF records
438   * asynchronously using the specified number of threads.
439   *
440   * @param  files                   The files from which the data is to be
441   *                                 read.  It must not be {@code null} or
442   *                                 empty.
443   * @param  numParseThreads         If this value is greater than zero, then
444   *                                 the specified number of threads will be
445   *                                 used to asynchronously read and parse the
446   *                                 LDIF file.
447   * @param  entryTranslator         The LDIFReaderEntryTranslator to apply to
448   *                                 entries before they are returned.  This is
449   *                                 normally {@code null}, which causes entries
450   *                                 to be returned unaltered.  This is
451   *                                 particularly useful when parsing the input
452   *                                 file in parallel because the entry
453   *                                 translation is also done in parallel.
454   * @param  changeRecordTranslator  The LDIFReaderChangeRecordTranslator to
455   *                                 apply to change records before they are
456   *                                 returned.  This is normally {@code null},
457   *                                 which causes change records to be returned
458   *                                 unaltered.  This is particularly useful
459   *                                 when parsing the input file in parallel
460   *                                 because the change record translation is
461   *                                 also done in parallel.
462   * @param  characterSet            The character set to use when reading from
463   *                                 the input stream.  It must not be
464   *                                 {@code null}.
465   *
466   * @throws  IOException  If a problem occurs while opening the file for
467   *                       reading.
468   */
469  public LDIFReader(@NotNull final File[] files, final int numParseThreads,
470       @Nullable final LDIFReaderEntryTranslator entryTranslator,
471       @Nullable final LDIFReaderChangeRecordTranslator changeRecordTranslator,
472       @NotNull final String characterSet)
473       throws IOException
474  {
475    this(createAggregateInputStream(files), numParseThreads, entryTranslator,
476         changeRecordTranslator, characterSet);
477  }
478
479
480
481  /**
482   * Creates a new aggregate input stream that will read data from the specified
483   * files.  If there are multiple files, then a "padding" file will be inserted
484   * between them to ensure that there is at least one blank line between the
485   * end of one file and the beginning of another.
486   *
487   * @param  files  The files from which the data is to be read.  It must not be
488   *                {@code null} or empty.
489   *
490   * @return  The input stream to use to read data from the provided files.
491   *
492   * @throws  IOException  If a problem is encountered while attempting to
493   *                       create the input stream.
494   */
495  @NotNull()
496  private static InputStream createAggregateInputStream(
497                                  @NotNull final File... files)
498          throws IOException
499  {
500    if (files.length == 0)
501    {
502      throw new IOException(ERR_READ_NO_LDIF_FILES.get());
503    }
504    else
505    {
506      return new AggregateInputStream(true, files);
507    }
508  }
509
510
511
512  /**
513   * Creates a new LDIF reader that will read data from the provided input
514   * stream.
515   *
516   * @param  inputStream  The input stream from which the data is to be read.
517   *                      It must not be {@code null}.
518   */
519  public LDIFReader(@NotNull final InputStream inputStream)
520  {
521    this(inputStream, 0);
522  }
523
524
525
526  /**
527   * Creates a new LDIF reader that will read data from the specified stream
528   * and parses the LDIF records asynchronously using the specified number of
529   * threads.
530   *
531   * @param  inputStream  The input stream from which the data is to be read.
532   *                      It must not be {@code null}.
533   * @param  numParseThreads  If this value is greater than zero, then the
534   *                          specified number of threads will be used to
535   *                          asynchronously read and parse the LDIF file.
536   *
537   * @see #LDIFReader(BufferedReader, int, LDIFReaderEntryTranslator)
538   *      constructor for more details about asynchronous processing.
539   */
540  public LDIFReader(@NotNull final InputStream inputStream,
541                    final int numParseThreads)
542  {
543    // UTF-8 is required by RFC 2849.  Java guarantees it's always available.
544    this(new BufferedReader(
545         new InputStreamReader(inputStream, StandardCharsets.UTF_8),
546              DEFAULT_BUFFER_SIZE),
547         numParseThreads);
548  }
549
550
551
552  /**
553   * Creates a new LDIF reader that will read data from the specified stream
554   * and parses the LDIF records asynchronously using the specified number of
555   * threads.
556   *
557   * @param  inputStream  The input stream from which the data is to be read.
558   *                      It must not be {@code null}.
559   * @param  numParseThreads  If this value is greater than zero, then the
560   *                          specified number of threads will be used to
561   *                          asynchronously read and parse the LDIF file.
562   * @param entryTranslator  The LDIFReaderEntryTranslator to apply to read
563   *                         entries before they are returned.  This is normally
564   *                         {@code null}, which causes entries to be returned
565   *                         unaltered. This is particularly useful when parsing
566   *                         the input file in parallel because the entry
567   *                         translation is also done in parallel.
568   *
569   * @see #LDIFReader(BufferedReader, int, LDIFReaderEntryTranslator)
570   *      constructor for more details about asynchronous processing.
571   */
572  public LDIFReader(@NotNull final InputStream inputStream,
573                    final int numParseThreads,
574                    @Nullable final LDIFReaderEntryTranslator entryTranslator)
575  {
576    this(inputStream, numParseThreads, entryTranslator, null);
577  }
578
579
580
581  /**
582   * Creates a new LDIF reader that will read data from the specified stream
583   * and parses the LDIF records asynchronously using the specified number of
584   * threads.
585   *
586   * @param  inputStream             The input stream from which the data is to
587   *                                 be read.  It must not be {@code null}.
588   * @param  numParseThreads         If this value is greater than zero, then
589   *                                 the specified number of threads will be
590   *                                 used to asynchronously read and parse the
591   *                                 LDIF file.
592   * @param  entryTranslator         The LDIFReaderEntryTranslator to apply to
593   *                                 entries before they are returned.  This is
594   *                                 normally {@code null}, which causes entries
595   *                                 to be returned unaltered.  This is
596   *                                 particularly useful when parsing the input
597   *                                 file in parallel because the entry
598   *                                 translation is also done in parallel.
599   * @param  changeRecordTranslator  The LDIFReaderChangeRecordTranslator to
600   *                                 apply to change records before they are
601   *                                 returned.  This is normally {@code null},
602   *                                 which causes change records to be returned
603   *                                 unaltered.  This is particularly useful
604   *                                 when parsing the input file in parallel
605   *                                 because the change record translation is
606   *                                 also done in parallel.
607   *
608   * @see #LDIFReader(BufferedReader, int, LDIFReaderEntryTranslator)
609   *      constructor for more details about asynchronous processing.
610   */
611  public LDIFReader(@NotNull final InputStream inputStream,
612       final int numParseThreads,
613       @Nullable final LDIFReaderEntryTranslator entryTranslator,
614       @Nullable final LDIFReaderChangeRecordTranslator changeRecordTranslator)
615  {
616    // UTF-8 is required by RFC 2849.  Java guarantees it's always available.
617    this(inputStream, numParseThreads, entryTranslator, changeRecordTranslator,
618         "UTF-8");
619  }
620
621
622
623  /**
624   * Creates a new LDIF reader that will read data from the specified stream
625   * and parses the LDIF records asynchronously using the specified number of
626   * threads.
627   *
628   * @param  inputStream             The input stream from which the data is to
629   *                                 be read.  It must not be {@code null}.
630   * @param  numParseThreads         If this value is greater than zero, then
631   *                                 the specified number of threads will be
632   *                                 used to asynchronously read and parse the
633   *                                 LDIF file.
634   * @param  entryTranslator         The LDIFReaderEntryTranslator to apply to
635   *                                 entries before they are returned.  This is
636   *                                 normally {@code null}, which causes entries
637   *                                 to be returned unaltered.  This is
638   *                                 particularly useful when parsing the input
639   *                                 file in parallel because the entry
640   *                                 translation is also done in parallel.
641   * @param  changeRecordTranslator  The LDIFReaderChangeRecordTranslator to
642   *                                 apply to change records before they are
643   *                                 returned.  This is normally {@code null},
644   *                                 which causes change records to be returned
645   *                                 unaltered.  This is particularly useful
646   *                                 when parsing the input file in parallel
647   *                                 because the change record translation is
648   *                                 also done in parallel.
649   * @param  characterSet            The character set to use when reading from
650   *                                 the input stream.  It must not be
651   *                                 {@code null}.
652   *
653   * @see #LDIFReader(BufferedReader, int, LDIFReaderEntryTranslator)
654   *      constructor for more details about asynchronous processing.
655   */
656  public LDIFReader(@NotNull final InputStream inputStream,
657       final int numParseThreads,
658       @Nullable final LDIFReaderEntryTranslator entryTranslator,
659       @Nullable final LDIFReaderChangeRecordTranslator changeRecordTranslator,
660       @NotNull final String characterSet)
661  {
662    this(new BufferedReader(
663              new InputStreamReader(inputStream, Charset.forName(characterSet)),
664              DEFAULT_BUFFER_SIZE),
665         numParseThreads, entryTranslator, changeRecordTranslator);
666  }
667
668
669
670  /**
671   * Creates a new LDIF reader that will use the provided buffered reader to
672   * read the LDIF data.  The encoding of the underlying Reader must be set to
673   * "UTF-8" as required by RFC 2849.
674   *
675   * @param  reader  The buffered reader that will be used to read the LDIF
676   *                 data.  It must not be {@code null}.
677   */
678  public LDIFReader(@NotNull final BufferedReader reader)
679  {
680    this(reader, 0);
681  }
682
683
684
685  /**
686   * Creates a new LDIF reader that will read data from the specified buffered
687   * reader and parses the LDIF records asynchronously using the specified
688   * number of threads.  The encoding of the underlying Reader must be set to
689   * "UTF-8" as required by RFC 2849.
690   *
691   * @param reader The buffered reader that will be used to read the LDIF data.
692   *               It must not be {@code null}.
693   * @param  numParseThreads  If this value is greater than zero, then the
694   *                          specified number of threads will be used to
695   *                          asynchronously read and parse the LDIF file.
696   *
697   * @see #LDIFReader(BufferedReader, int, LDIFReaderEntryTranslator)
698   *      constructor for more details about asynchronous processing.
699   */
700  public LDIFReader(@NotNull final BufferedReader reader,
701                    final int numParseThreads)
702  {
703    this(reader, numParseThreads, null);
704  }
705
706
707
708  /**
709   * Creates a new LDIF reader that will read data from the specified buffered
710   * reader and parses the LDIF records asynchronously using the specified
711   * number of threads.  The encoding of the underlying Reader must be set to
712   * "UTF-8" as required by RFC 2849.
713   *
714   * @param reader The buffered reader that will be used to read the LDIF data.
715   *               It must not be {@code null}.
716   * @param  numParseThreads  If this value is greater than zero, then the
717   *                          specified number of threads will be used to
718   *                          asynchronously read and parse the LDIF file.
719   *                          This should only be set to greater than zero when
720   *                          performance analysis has demonstrated that reading
721   *                          and parsing the LDIF is a bottleneck.  The default
722   *                          synchronous processing is normally fast enough.
723   *                          There is little benefit in passing in a value
724   *                          greater than four (unless there is an
725   *                          LDIFReaderEntryTranslator that does time-consuming
726   *                          processing).  A value of zero implies the
727   *                          default behavior of reading and parsing LDIF
728   *                          records synchronously when one of the read
729   *                          methods is called.
730   * @param entryTranslator  The LDIFReaderEntryTranslator to apply to read
731   *                         entries before they are returned.  This is normally
732   *                         {@code null}, which causes entries to be returned
733   *                         unaltered. This is particularly useful when parsing
734   *                         the input file in parallel because the entry
735   *                         translation is also done in parallel.
736   */
737  public LDIFReader(@NotNull final BufferedReader reader,
738                    final int numParseThreads,
739                    @Nullable final LDIFReaderEntryTranslator entryTranslator)
740  {
741    this(reader, numParseThreads, entryTranslator, null);
742  }
743
744
745
746  /**
747   * Creates a new LDIF reader that will read data from the specified buffered
748   * reader and parses the LDIF records asynchronously using the specified
749   * number of threads.  The encoding of the underlying Reader must be set to
750   * "UTF-8" as required by RFC 2849.
751   *
752   * @param reader                   The buffered reader that will be used to
753   *                                 read the LDIF data.  It must not be
754   *                                 {@code null}.
755   * @param  numParseThreads         If this value is greater than zero, then
756   *                                 the specified number of threads will be
757   *                                 used to asynchronously read and parse the
758   *                                 LDIF file.
759   * @param  entryTranslator         The LDIFReaderEntryTranslator to apply to
760   *                                 entries before they are returned.  This is
761   *                                 normally {@code null}, which causes entries
762   *                                 to be returned unaltered.  This is
763   *                                 particularly useful when parsing the input
764   *                                 file in parallel because the entry
765   *                                 translation is also done in parallel.
766   * @param  changeRecordTranslator  The LDIFReaderChangeRecordTranslator to
767   *                                 apply to change records before they are
768   *                                 returned.  This is normally {@code null},
769   *                                 which causes change records to be returned
770   *                                 unaltered.  This is particularly useful
771   *                                 when parsing the input file in parallel
772   *                                 because the change record translation is
773   *                                 also done in parallel.
774   */
775  public LDIFReader(@NotNull final BufferedReader reader,
776       final int numParseThreads,
777       @Nullable final LDIFReaderEntryTranslator entryTranslator,
778       @Nullable final LDIFReaderChangeRecordTranslator changeRecordTranslator)
779  {
780    Validator.ensureNotNull(reader);
781    Validator.ensureTrue(numParseThreads >= 0,
782               "LDIFReader.numParseThreads must not be negative.");
783
784    this.reader = reader;
785    this.entryTranslator = entryTranslator;
786    this.changeRecordTranslator = changeRecordTranslator;
787
788    duplicateValueBehavior = DuplicateValueBehavior.STRIP;
789    trailingSpaceBehavior  = TrailingSpaceBehavior.REJECT;
790
791    relativeBasePath = DEFAULT_RELATIVE_BASE_PATH;
792
793    if (numParseThreads == 0)
794    {
795      isAsync = false;
796      asyncParser = null;
797      asyncParsingComplete = null;
798      asyncParsedRecords = null;
799    }
800    else
801    {
802      isAsync = true;
803      asyncParsingComplete = new AtomicBoolean(false);
804
805      // Decodes entries in parallel.
806      final LDAPSDKThreadFactory threadFactory =
807           new LDAPSDKThreadFactory("LDIFReader Worker", true, null);
808      final ParallelProcessor<UnparsedLDIFRecord, LDIFRecord> parallelParser =
809           new ParallelProcessor<>(
810                new RecordParser(), threadFactory, numParseThreads,
811                ASYNC_MIN_PER_PARSING_THREAD);
812
813      final BlockingQueue<UnparsedLDIFRecord> pendingQueue = new
814           ArrayBlockingQueue<>(ASYNC_QUEUE_SIZE);
815
816      // The output queue must be a little more than twice as big as the input
817      // queue to more easily handle being shutdown in the middle of processing
818      // when the queues are full and threads are blocked.
819      asyncParsedRecords = new ArrayBlockingQueue<>(2 * ASYNC_QUEUE_SIZE + 100);
820
821      asyncParser = new AsynchronousParallelProcessor<>(pendingQueue,
822           parallelParser, asyncParsedRecords);
823
824      final LineReaderThread lineReaderThread = new LineReaderThread();
825      lineReaderThread.start();
826    }
827  }
828
829
830
831  /**
832   * Reads entries from the LDIF file with the specified path and returns them
833   * as a {@code List}.  This is a convenience method that should only be used
834   * for data sets that are small enough so that running out of memory isn't a
835   * concern.
836   *
837   * @param  path  The path to the LDIF file containing the entries to be read.
838   *
839   * @return  A list of the entries read from the given LDIF file.
840   *
841   * @throws  IOException  If a problem occurs while attempting to read data
842   *                       from the specified file.
843   *
844   * @throws  LDIFException  If a problem is encountered while attempting to
845   *                         decode data read as LDIF.
846   */
847  @NotNull()
848  public static List<Entry> readEntries(@NotNull final String path)
849         throws IOException, LDIFException
850  {
851    return readEntries(new LDIFReader(path));
852  }
853
854
855
856  /**
857   * Reads entries from the specified LDIF file and returns them as a
858   * {@code List}.  This is a convenience method that should only be used for
859   * data sets that are small enough so that running out of memory isn't a
860   * concern.
861   *
862   * @param  file  A reference to the LDIF file containing the entries to be
863   *               read.
864   *
865   * @return  A list of the entries read from the given LDIF file.
866   *
867   * @throws  IOException  If a problem occurs while attempting to read data
868   *                       from the specified file.
869   *
870   * @throws  LDIFException  If a problem is encountered while attempting to
871   *                         decode data read as LDIF.
872   */
873  @NotNull()
874  public static List<Entry> readEntries(@NotNull final File file)
875         throws IOException, LDIFException
876  {
877    return readEntries(new LDIFReader(file));
878  }
879
880
881
882  /**
883   * Reads and decodes LDIF entries from the provided input stream and
884   * returns them as a {@code List}.  This is a convenience method that should
885   * only be used for data sets that are small enough so that running out of
886   * memory isn't a concern.
887   *
888   * @param  inputStream  The input stream from which the entries should be
889   *                      read.  The input stream will be closed before
890   *                      returning.
891   *
892   * @return  A list of the entries read from the given input stream.
893   *
894   * @throws  IOException  If a problem occurs while attempting to read data
895   *                       from the input stream.
896   *
897   * @throws  LDIFException  If a problem is encountered while attempting to
898   *                         decode data read as LDIF.
899   */
900  @NotNull()
901  public static List<Entry> readEntries(@NotNull final InputStream inputStream)
902         throws IOException, LDIFException
903  {
904    return readEntries(new LDIFReader(inputStream));
905  }
906
907
908
909  /**
910   * Reads entries from the provided LDIF reader and returns them as a list.
911   *
912   * @param  reader  The reader from which the entries should be read.  It will
913   *                 be closed before returning.
914   *
915   * @return  A list of the entries read from the provided reader.
916   *
917   * @throws  IOException  If a problem was encountered while attempting to read
918   *                       data from the LDIF data source.
919   *
920   * @throws  LDIFException  If a problem is encountered while attempting to
921   *                         decode data read as LDIF.
922   */
923  @NotNull()
924  private static List<Entry> readEntries(@NotNull final LDIFReader reader)
925          throws IOException, LDIFException
926  {
927    try
928    {
929      final ArrayList<Entry> entries = new ArrayList<>(10);
930      while (true)
931      {
932        final Entry e = reader.readEntry();
933        if (e == null)
934        {
935          break;
936        }
937
938        entries.add(e);
939      }
940
941      return entries;
942    }
943    finally
944    {
945      reader.close();
946    }
947  }
948
949
950
951  /**
952   * Closes this LDIF reader and the underlying LDIF source.
953   *
954   * @throws  IOException  If a problem occurs while closing the underlying LDIF
955   *                       source.
956   */
957  @Override()
958  public void close()
959         throws IOException
960  {
961    reader.close();
962
963    if (isAsync())
964    {
965      // Closing the reader will trigger the LineReaderThread to complete, but
966      // not if it's blocked submitting the next UnparsedLDIFRecord.  To avoid
967      // this, we clear out the completed output queue, which is larger than
968      // the input queue, so the LineReaderThread will stop reading and
969      // shutdown the asyncParser.
970      asyncParsedRecords.clear();
971    }
972  }
973
974
975
976  /**
977   * Indicates whether to ignore any duplicate values encountered while reading
978   * LDIF records.
979   *
980   * @return  {@code true} if duplicate values should be ignored, or
981   *          {@code false} if any LDIF records containing duplicate values
982   *          should be rejected.
983   *
984   * @deprecated  Use the {@link #getDuplicateValueBehavior} method instead.
985   */
986  @Deprecated()
987  public boolean ignoreDuplicateValues()
988  {
989    return (duplicateValueBehavior == DuplicateValueBehavior.STRIP);
990  }
991
992
993
994  /**
995   * Specifies whether to ignore any duplicate values encountered while reading
996   * LDIF records.
997   *
998   * @param  ignoreDuplicateValues  Indicates whether to ignore duplicate
999   *                                attribute values encountered while reading
1000   *                                LDIF records.
1001   *
1002   * @deprecated  Use the {@link #setDuplicateValueBehavior} method instead.
1003   */
1004  @Deprecated()
1005  public void setIgnoreDuplicateValues(final boolean ignoreDuplicateValues)
1006  {
1007    if (ignoreDuplicateValues)
1008    {
1009      duplicateValueBehavior = DuplicateValueBehavior.STRIP;
1010    }
1011    else
1012    {
1013      duplicateValueBehavior = DuplicateValueBehavior.REJECT;
1014    }
1015  }
1016
1017
1018
1019  /**
1020   * Retrieves the behavior that should be exhibited if the LDIF reader
1021   * encounters an entry with duplicate values.
1022   *
1023   * @return  The behavior that should be exhibited if the LDIF reader
1024   *          encounters an entry with duplicate values.
1025   */
1026  @NotNull()
1027  public DuplicateValueBehavior getDuplicateValueBehavior()
1028  {
1029    return duplicateValueBehavior;
1030  }
1031
1032
1033
1034  /**
1035   * Specifies the behavior that should be exhibited if the LDIF reader
1036   * encounters an entry with duplicate values.
1037   *
1038   * @param  duplicateValueBehavior  The behavior that should be exhibited if
1039   *                                 the LDIF reader encounters an entry with
1040   *                                 duplicate values.
1041   */
1042  public void setDuplicateValueBehavior(
1043                   @NotNull final DuplicateValueBehavior duplicateValueBehavior)
1044  {
1045    this.duplicateValueBehavior = duplicateValueBehavior;
1046  }
1047
1048
1049
1050  /**
1051   * Indicates whether to strip off any illegal trailing spaces that may appear
1052   * in LDIF records (e.g., after an entry DN or attribute value).  The LDIF
1053   * specification strongly recommends that any value which legitimately
1054   * contains trailing spaces be base64-encoded, and any spaces which appear
1055   * after the end of non-base64-encoded values may therefore be considered
1056   * invalid.  If any such trailing spaces are encountered in an LDIF record and
1057   * they are not to be stripped, then an {@link LDIFException} will be thrown
1058   * for that record.
1059   * <BR><BR>
1060   * Note that this applies only to spaces after the end of a value, and not to
1061   * spaces which may appear at the end of a line for a value that is wrapped
1062   * and continued on the next line.
1063   *
1064   * @return  {@code true} if illegal trailing spaces should be stripped off, or
1065   *          {@code false} if LDIF records containing illegal trailing spaces
1066   *          should be rejected.
1067   *
1068   * @deprecated  Use the {@link #getTrailingSpaceBehavior} method instead.
1069   */
1070  @Deprecated()
1071  public boolean stripTrailingSpaces()
1072  {
1073    return (trailingSpaceBehavior == TrailingSpaceBehavior.STRIP);
1074  }
1075
1076
1077
1078  /**
1079   * Specifies whether to strip off any illegal trailing spaces that may appear
1080   * in LDIF records (e.g., after an entry DN or attribute value).  The LDIF
1081   * specification strongly recommends that any value which legitimately
1082   * contains trailing spaces be base64-encoded, and any spaces which appear
1083   * after the end of non-base64-encoded values may therefore be considered
1084   * invalid.  If any such trailing spaces are encountered in an LDIF record and
1085   * they are not to be stripped, then an {@link LDIFException} will be thrown
1086   * for that record.
1087   * <BR><BR>
1088   * Note that this applies only to spaces after the end of a value, and not to
1089   * spaces which may appear at the end of a line for a value that is wrapped
1090   * and continued on the next line.
1091   *
1092   * @param  stripTrailingSpaces  Indicates whether to strip off any illegal
1093   *                              trailing spaces, or {@code false} if LDIF
1094   *                              records containing them should be rejected.
1095   *
1096   * @deprecated  Use the {@link #setTrailingSpaceBehavior} method instead.
1097   */
1098  @Deprecated()
1099  public void setStripTrailingSpaces(final boolean stripTrailingSpaces)
1100  {
1101    trailingSpaceBehavior = stripTrailingSpaces
1102         ? TrailingSpaceBehavior.STRIP
1103         : TrailingSpaceBehavior.REJECT;
1104  }
1105
1106
1107
1108  /**
1109   * Retrieves the behavior that should be exhibited when encountering attribute
1110   * values which are not base64-encoded but contain trailing spaces.  The LDIF
1111   * specification strongly recommends that any value which legitimately
1112   * contains trailing spaces be base64-encoded, but the LDAP SDK LDIF parser
1113   * may be configured to automatically strip these spaces, to preserve them, or
1114   * to reject any entry or change record containing them.
1115   *
1116   * @return  The behavior that should be exhibited when encountering attribute
1117   *          values which are not base64-encoded but contain trailing spaces.
1118   */
1119  @NotNull()
1120  public TrailingSpaceBehavior getTrailingSpaceBehavior()
1121  {
1122    return trailingSpaceBehavior;
1123  }
1124
1125
1126
1127  /**
1128   * Specifies the behavior that should be exhibited when encountering attribute
1129   * values which are not base64-encoded but contain trailing spaces.  The LDIF
1130   * specification strongly recommends that any value which legitimately
1131   * contains trailing spaces be base64-encoded, but the LDAP SDK LDIF parser
1132   * may be configured to automatically strip these spaces, to preserve them, or
1133   * to reject any entry or change record containing them.
1134   *
1135   * @param  trailingSpaceBehavior  The behavior that should be exhibited when
1136   *                                encountering attribute values which are not
1137   *                                base64-encoded but contain trailing spaces.
1138   */
1139  public void setTrailingSpaceBehavior(
1140                   @NotNull final TrailingSpaceBehavior trailingSpaceBehavior)
1141  {
1142    this.trailingSpaceBehavior = trailingSpaceBehavior;
1143  }
1144
1145
1146
1147  /**
1148   * Retrieves the base path that will be prepended to relative paths in order
1149   * to obtain an absolute path.  This will only be used for "file:" URLs that
1150   * have paths which do not begin with a slash.
1151   *
1152   * @return  The base path that will be prepended to relative paths in order to
1153   *          obtain an absolute path.
1154   */
1155  @NotNull()
1156  public String getRelativeBasePath()
1157  {
1158    return relativeBasePath;
1159  }
1160
1161
1162
1163  /**
1164   * Specifies the base path that will be prepended to relative paths in order
1165   * to obtain an absolute path.  This will only be used for "file:" URLs that
1166   * have paths which do not begin with a space.
1167   *
1168   * @param  relativeBasePath  The base path that will be prepended to relative
1169   *                           paths in order to obtain an absolute path.
1170   */
1171  public void setRelativeBasePath(@NotNull final String relativeBasePath)
1172  {
1173    setRelativeBasePath(new File(relativeBasePath));
1174  }
1175
1176
1177
1178  /**
1179   * Specifies the base path that will be prepended to relative paths in order
1180   * to obtain an absolute path.  This will only be used for "file:" URLs that
1181   * have paths which do not begin with a space.
1182   *
1183   * @param  relativeBasePath  The base path that will be prepended to relative
1184   *                           paths in order to obtain an absolute path.
1185   */
1186  public void setRelativeBasePath(@NotNull final File relativeBasePath)
1187  {
1188    final String path = relativeBasePath.getAbsolutePath();
1189    if (path.endsWith(File.separator))
1190    {
1191      this.relativeBasePath = path;
1192    }
1193    else
1194    {
1195      this.relativeBasePath = path + File.separator;
1196    }
1197  }
1198
1199
1200
1201  /**
1202   * Retrieves the schema that will be used when reading LDIF records, if
1203   * defined.
1204   *
1205   * @return  The schema that will be used when reading LDIF records, or
1206   *          {@code null} if no schema should be used and all attributes should
1207   *          be treated as case-insensitive strings.
1208   */
1209  @Nullable()
1210  public Schema getSchema()
1211  {
1212    return schema;
1213  }
1214
1215
1216
1217  /**
1218   * Specifies the schema that should be used when reading LDIF records.
1219   *
1220   * @param  schema  The schema that should be used when reading LDIF records,
1221   *                 or {@code null} if no schema should be used and all
1222   *                 attributes should be treated as case-insensitive strings.
1223   */
1224  public void setSchema(@Nullable final Schema schema)
1225  {
1226    this.schema = schema;
1227  }
1228
1229
1230
1231  /**
1232   * Reads a record from the LDIF source.  It may be either an entry or an LDIF
1233   * change record.
1234   *
1235   * @return  The record read from the LDIF source, or {@code null} if there are
1236   *          no more entries to be read.
1237   *
1238   * @throws  IOException  If a problem occurs while trying to read from the
1239   *                       LDIF source.
1240   *
1241   * @throws  LDIFException  If the data read could not be parsed as an entry or
1242   *                         an LDIF change record.
1243   */
1244  @Nullable()
1245  public LDIFRecord readLDIFRecord()
1246         throws IOException, LDIFException
1247  {
1248    if (isAsync())
1249    {
1250      return readLDIFRecordAsync();
1251    }
1252    else
1253    {
1254      return readLDIFRecordInternal();
1255    }
1256  }
1257
1258
1259
1260  /**
1261   * Reads an entry from the LDIF source.
1262   *
1263   * @return  The entry read from the LDIF source, or {@code null} if there are
1264   *          no more entries to be read.
1265   *
1266   * @throws  IOException  If a problem occurs while attempting to read from the
1267   *                       LDIF source.
1268   *
1269   * @throws  LDIFException  If the data read could not be parsed as an entry.
1270   */
1271  @Nullable()
1272  public Entry readEntry()
1273         throws IOException, LDIFException
1274  {
1275    if (isAsync())
1276    {
1277      return readEntryAsync();
1278    }
1279    else
1280    {
1281      return readEntryInternal();
1282    }
1283  }
1284
1285
1286
1287  /**
1288   * Reads an LDIF change record from the LDIF source.  The LDIF record must
1289   * have a changetype.
1290   *
1291   * @return  The change record read from the LDIF source, or {@code null} if
1292   *          there are no more records to be read.
1293   *
1294   * @throws  IOException  If a problem occurs while attempting to read from the
1295   *                       LDIF source.
1296   *
1297   * @throws  LDIFException  If the data read could not be parsed as an LDIF
1298   *                         change record.
1299   */
1300  @Nullable()
1301  public LDIFChangeRecord readChangeRecord()
1302         throws IOException, LDIFException
1303  {
1304    return readChangeRecord(false);
1305  }
1306
1307
1308
1309  /**
1310   * Reads an LDIF change record from the LDIF source.  Optionally, if the LDIF
1311   * record does not have a changetype, then it may be assumed to be an add
1312   * change record.
1313   *
1314   * @param  defaultAdd  Indicates whether an LDIF record not containing a
1315   *                     changetype should be retrieved as an add change record.
1316   *                     If this is {@code false} and the record read does not
1317   *                     include a changetype, then an {@link LDIFException}
1318   *                     will be thrown.
1319   *
1320   * @return  The change record read from the LDIF source, or {@code null} if
1321   *          there are no more records to be read.
1322   *
1323   * @throws  IOException  If a problem occurs while attempting to read from the
1324   *                       LDIF source.
1325   *
1326   * @throws  LDIFException  If the data read could not be parsed as an LDIF
1327   *                         change record.
1328   */
1329  @Nullable()
1330  public LDIFChangeRecord readChangeRecord(final boolean defaultAdd)
1331         throws IOException, LDIFException
1332  {
1333    if (isAsync())
1334    {
1335      return readChangeRecordAsync(defaultAdd);
1336    }
1337    else
1338    {
1339      return readChangeRecordInternal(defaultAdd);
1340    }
1341  }
1342
1343
1344
1345  /**
1346   * Reads the next {@code LDIFRecord}, which was read and parsed by a different
1347   * thread.
1348   *
1349   * @return  The next parsed record or {@code null} if there are no more
1350   *          records to read.
1351   *
1352   * @throws IOException  If IOException was thrown when reading or parsing
1353   *                      the record.
1354   *
1355   * @throws LDIFException If LDIFException was thrown parsing the record.
1356   */
1357  @Nullable()
1358  private LDIFRecord readLDIFRecordAsync()
1359          throws IOException, LDIFException
1360  {
1361    Result<UnparsedLDIFRecord, LDIFRecord> result;
1362    LDIFRecord record = null;
1363    while (record == null)
1364    {
1365      result = readLDIFRecordResultAsync();
1366      if (result == null)
1367      {
1368        return null;
1369      }
1370
1371      record = result.getOutput();
1372
1373      // This is a special value that means we should skip this Entry.  We have
1374      // to use something different than null because null means EOF.
1375      if (record == SKIP_ENTRY)
1376      {
1377        record = null;
1378      }
1379    }
1380    return record;
1381  }
1382
1383
1384
1385  /**
1386   * Reads an entry asynchronously from the LDIF source.
1387   *
1388   * @return The entry read from the LDIF source, or {@code null} if there are
1389   *         no more entries to be read.
1390   *
1391   * @throws IOException   If a problem occurs while attempting to read from the
1392   *                       LDIF source.
1393   * @throws LDIFException If the data read could not be parsed as an entry.
1394   */
1395  @Nullable()
1396  private Entry readEntryAsync()
1397          throws IOException, LDIFException
1398  {
1399    Result<UnparsedLDIFRecord, LDIFRecord> result = null;
1400    LDIFRecord record = null;
1401    while (record == null)
1402    {
1403      result = readLDIFRecordResultAsync();
1404      if (result == null)
1405      {
1406        return null;
1407      }
1408
1409      record = result.getOutput();
1410
1411      // This is a special value that means we should skip this Entry.  We have
1412      // to use something different than null because null means EOF.
1413      if (record == SKIP_ENTRY)
1414      {
1415        record = null;
1416      }
1417    }
1418
1419    if (record instanceof Entry)
1420    {
1421      return (Entry) record;
1422    }
1423    else if (record instanceof LDIFChangeRecord)
1424    {
1425      try
1426      {
1427        // Some LDIFChangeRecord can be converted to an Entry.  This is really
1428        // an edge case though.
1429        return ((LDIFChangeRecord)record).toEntry();
1430      }
1431      catch (final LDIFException e)
1432      {
1433        Debug.debugException(e);
1434        final long firstLineNumber = result.getInput().getFirstLineNumber();
1435        throw new LDIFException(e.getExceptionMessage(),
1436                                firstLineNumber, true, e);
1437      }
1438    }
1439
1440    throw new AssertionError("LDIFRecords must either be an Entry or an " +
1441                             "LDIFChangeRecord");
1442  }
1443
1444
1445
1446  /**
1447   * Reads an LDIF change record from the LDIF source asynchronously.
1448   * Optionally, if the LDIF record does not have a changetype, then it may be
1449   * assumed to be an add change record.
1450   *
1451   * @param defaultAdd Indicates whether an LDIF record not containing a
1452   *                   changetype should be retrieved as an add change record.
1453   *                   If this is {@code false} and the record read does not
1454   *                   include a changetype, then an {@link LDIFException} will
1455   *                   be thrown.
1456   *
1457   * @return The change record read from the LDIF source, or {@code null} if
1458   *         there are no more records to be read.
1459   *
1460   * @throws IOException   If a problem occurs while attempting to read from the
1461   *                       LDIF source.
1462   * @throws LDIFException If the data read could not be parsed as an LDIF
1463   *                       change record.
1464   */
1465  @Nullable()
1466  private LDIFChangeRecord readChangeRecordAsync(final boolean defaultAdd)
1467          throws IOException, LDIFException
1468  {
1469    Result<UnparsedLDIFRecord, LDIFRecord> result = null;
1470    LDIFRecord record = null;
1471    while (record == null)
1472    {
1473      result = readLDIFRecordResultAsync();
1474      if (result == null)
1475      {
1476        return null;
1477      }
1478
1479      record = result.getOutput();
1480
1481      // This is a special value that means we should skip this Entry.  We have
1482      // to use something different than null because null means EOF.
1483      if (record == SKIP_ENTRY)
1484      {
1485        record = null;
1486      }
1487    }
1488
1489    if (record instanceof LDIFChangeRecord)
1490    {
1491      return (LDIFChangeRecord) record;
1492    }
1493    else if (record instanceof Entry)
1494    {
1495      if (defaultAdd)
1496      {
1497        return new LDIFAddChangeRecord((Entry) record);
1498      }
1499      else
1500      {
1501        final long firstLineNumber = result.getInput().getFirstLineNumber();
1502        throw new LDIFException(
1503             ERR_READ_NOT_CHANGE_RECORD.get(firstLineNumber), firstLineNumber,
1504             true);
1505      }
1506    }
1507
1508    throw new AssertionError("LDIFRecords must either be an Entry or an " +
1509                             "LDIFChangeRecord");
1510  }
1511
1512
1513
1514  /**
1515   * Reads the next LDIF record, which was read and parsed asynchronously by
1516   * separate threads.
1517   *
1518   * @return  The next LDIF record or {@code null} if there are no more records.
1519   *
1520   * @throws  IOException  If a problem occurs while attempting to read from the
1521   *                       LDIF source.
1522   *
1523   * @throws  LDIFException  If the data read could not be parsed as an entry.
1524   */
1525  @Nullable()
1526  private Result<UnparsedLDIFRecord, LDIFRecord> readLDIFRecordResultAsync()
1527          throws IOException, LDIFException
1528  {
1529    Result<UnparsedLDIFRecord, LDIFRecord> result = null;
1530
1531    // If the asynchronous reading and parsing is complete, then we don't have
1532    // to block waiting for the next record to show up on the queue.  If there
1533    // isn't a record there, then return null (EOF) right away.
1534    if (asyncParsingComplete.get())
1535    {
1536      result = asyncParsedRecords.poll();
1537    }
1538    else
1539    {
1540      try
1541      {
1542        // We probably could just do a asyncParsedRecords.take() here, but
1543        // there are some edge case error scenarios where
1544        // asyncParsingComplete might be set without a special EOF sentinel
1545        // Result enqueued.  So to guard against this, we have a very cautious
1546        // polling interval of 1 second.  During normal processing, we never
1547        // have to wait for this to expire, when there is something to do
1548        // (like shutdown).
1549        while ((result == null) && (!asyncParsingComplete.get()))
1550        {
1551          result = asyncParsedRecords.poll(1, TimeUnit.SECONDS);
1552        }
1553
1554        // There's a very small chance that we missed the value, so double-check
1555        if (result == null)
1556        {
1557          result = asyncParsedRecords.poll();
1558        }
1559      }
1560      catch (final InterruptedException e)
1561      {
1562        Debug.debugException(e);
1563        Thread.currentThread().interrupt();
1564        throw new IOException(e);
1565      }
1566    }
1567    if (result == null)
1568    {
1569      return null;
1570    }
1571
1572    rethrow(result.getFailureCause());
1573
1574    // Check if we reached the end of the input
1575    final UnparsedLDIFRecord unparsedRecord = result.getInput();
1576    if (unparsedRecord.isEOF())
1577    {
1578      // This might have been set already by the LineReaderThread, but
1579      // just in case it hasn't gotten to it yet, do so here.
1580      asyncParsingComplete.set(true);
1581
1582      // Enqueue this EOF result again for any other thread that might be
1583      // blocked in asyncParsedRecords.take() even though having multiple
1584      // threads call this method concurrently breaks the contract of this
1585      // class.
1586      try
1587      {
1588        asyncParsedRecords.put(result);
1589      }
1590      catch (final InterruptedException e)
1591      {
1592        // We shouldn't ever get interrupted because the put won't ever block.
1593        // Once we are done reading, this is the only item left in the queue,
1594        // so we should always be able to re-enqueue it.
1595        Debug.debugException(e);
1596        Thread.currentThread().interrupt();
1597      }
1598      return null;
1599    }
1600
1601    return result;
1602  }
1603
1604
1605
1606  /**
1607   * Indicates whether this LDIF reader was constructed to perform asynchronous
1608   * processing.
1609   *
1610   * @return  {@code true} if this LDIFReader was constructed to perform
1611   *          asynchronous processing, or {@code false} if not.
1612   */
1613  private boolean isAsync()
1614  {
1615    return isAsync;
1616  }
1617
1618
1619
1620  /**
1621   * If not {@code null}, rethrows the specified Throwable as either an
1622   * IOException or LDIFException.
1623   *
1624   * @param t  The exception to rethrow.  If it's {@code null}, then nothing
1625   *           is thrown.
1626   *
1627   * @throws IOException   If t is an IOException or a checked Exception that
1628   *                       is not an LDIFException.
1629   * @throws LDIFException  If t is an LDIFException.
1630   */
1631  static void rethrow(@Nullable final Throwable t)
1632         throws IOException, LDIFException
1633  {
1634    if (t == null)
1635    {
1636      return;
1637    }
1638
1639    if (t instanceof IOException)
1640    {
1641      throw (IOException) t;
1642    }
1643    else if (t instanceof LDIFException)
1644    {
1645      throw (LDIFException) t;
1646    }
1647    else if (t instanceof RuntimeException)
1648    {
1649      throw (RuntimeException) t;
1650    }
1651    else if (t instanceof Error)
1652    {
1653      throw (Error) t;
1654    }
1655    else
1656    {
1657      throw new IOException(t);
1658    }
1659  }
1660
1661
1662
1663  /**
1664   * Reads a record from the LDIF source.  It may be either an entry or an LDIF
1665   * change record.
1666   *
1667   * @return The record read from the LDIF source, or {@code null} if there are
1668   *         no more entries to be read.
1669   *
1670   * @throws IOException   If a problem occurs while trying to read from the
1671   *                       LDIF source.
1672   * @throws LDIFException If the data read could not be parsed as an entry or
1673   *                       an LDIF change record.
1674   */
1675  @Nullable()
1676  private LDIFRecord readLDIFRecordInternal()
1677       throws IOException, LDIFException
1678  {
1679    final UnparsedLDIFRecord unparsedRecord = readUnparsedRecord();
1680    return decodeRecord(unparsedRecord, relativeBasePath, schema);
1681  }
1682
1683
1684
1685  /**
1686   * Reads an entry from the LDIF source.
1687   *
1688   * @return The entry read from the LDIF source, or {@code null} if there are
1689   *         no more entries to be read.
1690   *
1691   * @throws IOException   If a problem occurs while attempting to read from the
1692   *                       LDIF source.
1693   * @throws LDIFException If the data read could not be parsed as an entry.
1694   */
1695  @Nullable()
1696  private Entry readEntryInternal()
1697       throws IOException, LDIFException
1698  {
1699    Entry e = null;
1700    while (e == null)
1701    {
1702      final UnparsedLDIFRecord unparsedRecord = readUnparsedRecord();
1703      if (unparsedRecord.isEOF())
1704      {
1705        return null;
1706      }
1707
1708      e = decodeEntry(unparsedRecord, relativeBasePath);
1709      Debug.debugLDIFRead(e);
1710
1711      if (entryTranslator != null)
1712      {
1713        e = entryTranslator.translate(e, unparsedRecord.getFirstLineNumber());
1714      }
1715    }
1716    return e;
1717  }
1718
1719
1720
1721  /**
1722   * Reads an LDIF change record from the LDIF source.  Optionally, if the LDIF
1723   * record does not have a changetype, then it may be assumed to be an add
1724   * change record.
1725   *
1726   * @param defaultAdd Indicates whether an LDIF record not containing a
1727   *                   changetype should be retrieved as an add change record.
1728   *                   If this is {@code false} and the record read does not
1729   *                   include a changetype, then an {@link LDIFException} will
1730   *                   be thrown.
1731   *
1732   * @return The change record read from the LDIF source, or {@code null} if
1733   *         there are no more records to be read.
1734   *
1735   * @throws IOException   If a problem occurs while attempting to read from the
1736   *                       LDIF source.
1737   * @throws LDIFException If the data read could not be parsed as an LDIF
1738   *                       change record.
1739   */
1740  @Nullable()
1741  private LDIFChangeRecord readChangeRecordInternal(final boolean defaultAdd)
1742       throws IOException, LDIFException
1743  {
1744    LDIFChangeRecord r = null;
1745    while (r == null)
1746    {
1747      final UnparsedLDIFRecord unparsedRecord = readUnparsedRecord();
1748      if (unparsedRecord.isEOF())
1749      {
1750        return null;
1751      }
1752
1753      r = decodeChangeRecord(unparsedRecord, relativeBasePath, defaultAdd,
1754           schema);
1755      Debug.debugLDIFRead(r);
1756
1757      if (changeRecordTranslator != null)
1758      {
1759        r = changeRecordTranslator.translate(r,
1760             unparsedRecord.getFirstLineNumber());
1761      }
1762    }
1763    return r;
1764  }
1765
1766
1767
1768  /**
1769   * Reads a record (either an entry or a change record) from the LDIF source
1770   * and places it in the line list.
1771   *
1772   * @return  The unparsed record that was read.
1773   *
1774   * @throws  IOException  If a problem occurs while attempting to read from the
1775   *                       LDIF source.
1776   *
1777   * @throws  LDIFException  If the data read could not be parsed as a valid
1778   *                         LDIF record.
1779   */
1780  @NotNull()
1781  private UnparsedLDIFRecord readUnparsedRecord()
1782         throws IOException, LDIFException
1783  {
1784    final ArrayList<StringBuilder> lineList = new ArrayList<>(20);
1785    boolean lastWasComment = false;
1786    long firstLineNumber = lineNumberCounter + 1;
1787    while (true)
1788    {
1789      final String line = reader.readLine();
1790      lineNumberCounter++;
1791
1792      if (line == null)
1793      {
1794        // We've hit the end of the LDIF source.  If we haven't read any entry
1795        // data, then return null.  Otherwise, the last entry wasn't followed by
1796        // a blank line, which is OK, and we should decode that entry.
1797        if (lineList.isEmpty())
1798        {
1799          return new UnparsedLDIFRecord(new ArrayList<StringBuilder>(0),
1800               duplicateValueBehavior, trailingSpaceBehavior, schema, -1);
1801        }
1802        else
1803        {
1804          break;
1805        }
1806      }
1807
1808      if (line.isEmpty())
1809      {
1810        // It's a blank line.  If we have read entry data, then this signals the
1811        // end of the entry.  Otherwise, it's an extra space between entries,
1812        // which is OK.
1813        lastWasComment = false;
1814        if (lineList.isEmpty())
1815        {
1816          firstLineNumber++;
1817          continue;
1818        }
1819        else
1820        {
1821          break;
1822        }
1823      }
1824
1825      if (line.charAt(0) == ' ')
1826      {
1827        // The line starts with a space, which means that it must be a
1828        // continuation of the previous line.  This is true even if the last
1829        // line was a comment.
1830        if (lastWasComment)
1831        {
1832          // What we've read is part of a comment, so we don't care about its
1833          // content.
1834        }
1835        else if (lineList.isEmpty())
1836        {
1837          throw new LDIFException(
1838                         ERR_READ_UNEXPECTED_FIRST_SPACE.get(lineNumberCounter),
1839                         lineNumberCounter, false);
1840        }
1841        else
1842        {
1843          lineList.get(lineList.size() - 1).append(line.substring(1));
1844          lastWasComment = false;
1845        }
1846      }
1847      else if (line.charAt(0) == '#')
1848      {
1849        lastWasComment = true;
1850      }
1851      else
1852      {
1853        // We want to make sure that we skip over the "version:" line if it
1854        // exists, but that should only occur at the beginning of an entry where
1855        // it can't be confused with a possible "version" attribute.
1856        if (lineList.isEmpty() && line.startsWith("version:"))
1857        {
1858          lastWasComment = true;
1859        }
1860        else
1861        {
1862          lineList.add(new StringBuilder(line));
1863          lastWasComment = false;
1864        }
1865      }
1866    }
1867
1868    return new UnparsedLDIFRecord(lineList, duplicateValueBehavior,
1869         trailingSpaceBehavior, schema, firstLineNumber);
1870  }
1871
1872
1873
1874  /**
1875   * Decodes the provided set of LDIF lines as an entry.  The provided set of
1876   * lines must contain exactly one entry.  Long lines may be wrapped as per the
1877   * LDIF specification, and it is acceptable to have one or more blank lines
1878   * following the entry. A default trailing space behavior of
1879   * {@link TrailingSpaceBehavior#REJECT} will be used.
1880   *
1881   * @param  ldifLines  The set of lines that comprise the LDIF representation
1882   *                    of the entry.  It must not be {@code null} or empty.
1883   *
1884   * @return  The entry read from LDIF.
1885   *
1886   * @throws  LDIFException  If the provided LDIF data cannot be decoded as an
1887   *                         entry.
1888   */
1889  @NotNull()
1890  public static Entry decodeEntry(@NotNull final String... ldifLines)
1891         throws LDIFException
1892  {
1893    final Entry e = decodeEntry(prepareRecord(DuplicateValueBehavior.STRIP,
1894         TrailingSpaceBehavior.REJECT, null, ldifLines),
1895         DEFAULT_RELATIVE_BASE_PATH);
1896    Debug.debugLDIFRead(e);
1897    return e;
1898  }
1899
1900
1901
1902  /**
1903   * Decodes the provided set of LDIF lines as an entry.  The provided set of
1904   * lines must contain exactly one entry.  Long lines may be wrapped as per the
1905   * LDIF specification, and it is acceptable to have one or more blank lines
1906   * following the entry. A default trailing space behavior of
1907   * {@link TrailingSpaceBehavior#REJECT} will be used.
1908   *
1909   * @param  ignoreDuplicateValues  Indicates whether to ignore duplicate
1910   *                                attribute values encountered while parsing.
1911   * @param  schema                 The schema to use when parsing the record,
1912   *                                if applicable.
1913   * @param  ldifLines              The set of lines that comprise the LDIF
1914   *                                representation of the entry.  It must not be
1915   *                                {@code null} or empty.
1916   *
1917   * @return  The entry read from LDIF.
1918   *
1919   * @throws  LDIFException  If the provided LDIF data cannot be decoded as an
1920   *                         entry.
1921   */
1922  @NotNull()
1923  public static Entry decodeEntry(final boolean ignoreDuplicateValues,
1924                                  @Nullable final Schema schema,
1925                                  @NotNull final String... ldifLines)
1926         throws LDIFException
1927  {
1928    return decodeEntry(ignoreDuplicateValues, TrailingSpaceBehavior.REJECT,
1929         schema, ldifLines);
1930  }
1931
1932
1933
1934  /**
1935   * Decodes the provided set of LDIF lines as an entry.  The provided set of
1936   * lines must contain exactly one entry.  Long lines may be wrapped as per the
1937   * LDIF specification, and it is acceptable to have one or more blank lines
1938   * following the entry.
1939   *
1940   * @param  ignoreDuplicateValues  Indicates whether to ignore duplicate
1941   *                                attribute values encountered while parsing.
1942   * @param  trailingSpaceBehavior  The behavior that should be exhibited when
1943   *                                encountering attribute values which are not
1944   *                                base64-encoded but contain trailing spaces.
1945   *                                It must not be {@code null}.
1946   * @param  schema                 The schema to use when parsing the record,
1947   *                                if applicable.
1948   * @param  ldifLines              The set of lines that comprise the LDIF
1949   *                                representation of the entry.  It must not be
1950   *                                {@code null} or empty.
1951   *
1952   * @return  The entry read from LDIF.
1953   *
1954   * @throws  LDIFException  If the provided LDIF data cannot be decoded as an
1955   *                         entry.
1956   */
1957  @NotNull()
1958  public static Entry decodeEntry(
1959                     final boolean ignoreDuplicateValues,
1960                     @NotNull final TrailingSpaceBehavior trailingSpaceBehavior,
1961                     @Nullable final Schema schema,
1962                     @NotNull final String... ldifLines)
1963         throws LDIFException
1964  {
1965    final Entry e = decodeEntry(prepareRecord(
1966              (ignoreDuplicateValues
1967                   ? DuplicateValueBehavior.STRIP
1968                   : DuplicateValueBehavior.REJECT),
1969         trailingSpaceBehavior, schema, ldifLines),
1970         DEFAULT_RELATIVE_BASE_PATH);
1971    Debug.debugLDIFRead(e);
1972    return e;
1973  }
1974
1975
1976
1977  /**
1978   * Decodes the provided set of LDIF lines as an LDIF change record.  The
1979   * provided set of lines must contain exactly one change record and it must
1980   * include a changetype.  Long lines may be wrapped as per the LDIF
1981   * specification, and it is acceptable to have one or more blank lines
1982   * following the entry.
1983   *
1984   * @param  ldifLines  The set of lines that comprise the LDIF representation
1985   *                    of the change record.  It must not be {@code null} or
1986   *                    empty.
1987   *
1988   * @return  The change record read from LDIF.
1989   *
1990   * @throws  LDIFException  If the provided LDIF data cannot be decoded as a
1991   *                         change record.
1992   */
1993  @NotNull()
1994  public static LDIFChangeRecord decodeChangeRecord(
1995                                      @NotNull final String... ldifLines)
1996         throws LDIFException
1997  {
1998    return decodeChangeRecord(false, ldifLines);
1999  }
2000
2001
2002
2003  /**
2004   * Decodes the provided set of LDIF lines as an LDIF change record.  The
2005   * provided set of lines must contain exactly one change record.  Long lines
2006   * may be wrapped as per the LDIF specification, and it is acceptable to have
2007   * one or more blank lines following the entry.
2008   *
2009   * @param  defaultAdd  Indicates whether an LDIF record not containing a
2010   *                     changetype should be retrieved as an add change record.
2011   *                     If this is {@code false} and the record read does not
2012   *                     include a changetype, then an {@link LDIFException}
2013   *                     will be thrown.
2014   * @param  ldifLines  The set of lines that comprise the LDIF representation
2015   *                    of the change record.  It must not be {@code null} or
2016   *                    empty.
2017   *
2018   * @return  The change record read from LDIF.
2019   *
2020   * @throws  LDIFException  If the provided LDIF data cannot be decoded as a
2021   *                         change record.
2022   */
2023  @NotNull()
2024  public static LDIFChangeRecord decodeChangeRecord(final boolean defaultAdd,
2025                                      @NotNull final String... ldifLines)
2026         throws LDIFException
2027  {
2028    final LDIFChangeRecord r =
2029         decodeChangeRecord(
2030              prepareRecord(DuplicateValueBehavior.STRIP,
2031                   TrailingSpaceBehavior.REJECT, null, ldifLines),
2032              DEFAULT_RELATIVE_BASE_PATH, defaultAdd, null);
2033    Debug.debugLDIFRead(r);
2034    return r;
2035  }
2036
2037
2038
2039  /**
2040   * Decodes the provided set of LDIF lines as an LDIF change record.  The
2041   * provided set of lines must contain exactly one change record.  Long lines
2042   * may be wrapped as per the LDIF specification, and it is acceptable to have
2043   * one or more blank lines following the entry.
2044   *
2045   * @param  ignoreDuplicateValues  Indicates whether to ignore duplicate
2046   *                                attribute values encountered while parsing.
2047   * @param  schema                 The schema to use when processing the change
2048   *                                record, or {@code null} if no schema should
2049   *                                be used and all values should be treated as
2050   *                                case-insensitive strings.
2051   * @param  defaultAdd             Indicates whether an LDIF record not
2052   *                                containing a changetype should be retrieved
2053   *                                as an add change record.  If this is
2054   *                                {@code false} and the record read does not
2055   *                                include a changetype, then an
2056   *                                {@link LDIFException} will be thrown.
2057   * @param  ldifLines              The set of lines that comprise the LDIF
2058   *                                representation of the change record.  It
2059   *                                must not be {@code null} or empty.
2060   *
2061   * @return  The change record read from LDIF.
2062   *
2063   * @throws  LDIFException  If the provided LDIF data cannot be decoded as a
2064   *                         change record.
2065   */
2066  @NotNull()
2067  public static LDIFChangeRecord decodeChangeRecord(
2068                     final boolean ignoreDuplicateValues,
2069                     @Nullable final Schema schema,
2070                     final boolean defaultAdd,
2071                     @NotNull final String... ldifLines)
2072         throws LDIFException
2073  {
2074    return decodeChangeRecord(ignoreDuplicateValues,
2075         TrailingSpaceBehavior.REJECT, schema, defaultAdd, ldifLines);
2076  }
2077
2078
2079
2080  /**
2081   * Decodes the provided set of LDIF lines as an LDIF change record.  The
2082   * provided set of lines must contain exactly one change record.  Long lines
2083   * may be wrapped as per the LDIF specification, and it is acceptable to have
2084   * one or more blank lines following the entry.
2085   *
2086   * @param  ignoreDuplicateValues  Indicates whether to ignore duplicate
2087   *                                attribute values encountered while parsing.
2088   * @param  trailingSpaceBehavior  The behavior that should be exhibited when
2089   *                                encountering attribute values which are not
2090   *                                base64-encoded but contain trailing spaces.
2091   *                                It must not be {@code null}.
2092   * @param  schema                 The schema to use when processing the change
2093   *                                record, or {@code null} if no schema should
2094   *                                be used and all values should be treated as
2095   *                                case-insensitive strings.
2096   * @param  defaultAdd             Indicates whether an LDIF record not
2097   *                                containing a changetype should be retrieved
2098   *                                as an add change record.  If this is
2099   *                                {@code false} and the record read does not
2100   *                                include a changetype, then an
2101   *                                {@link LDIFException} will be thrown.
2102   * @param  ldifLines              The set of lines that comprise the LDIF
2103   *                                representation of the change record.  It
2104   *                                must not be {@code null} or empty.
2105   *
2106   * @return  The change record read from LDIF.
2107   *
2108   * @throws  LDIFException  If the provided LDIF data cannot be decoded as a
2109   *                         change record.
2110   */
2111  @NotNull()
2112  public static LDIFChangeRecord decodeChangeRecord(
2113                     final boolean ignoreDuplicateValues,
2114                     @NotNull final TrailingSpaceBehavior trailingSpaceBehavior,
2115                     @Nullable final Schema schema,
2116                     final boolean defaultAdd,
2117                     @NotNull final String... ldifLines)
2118         throws LDIFException
2119  {
2120    final LDIFChangeRecord r = decodeChangeRecord(
2121         prepareRecord(
2122              (ignoreDuplicateValues
2123                   ? DuplicateValueBehavior.STRIP
2124                   : DuplicateValueBehavior.REJECT),
2125              trailingSpaceBehavior, schema, ldifLines),
2126         DEFAULT_RELATIVE_BASE_PATH, defaultAdd, null);
2127    Debug.debugLDIFRead(r);
2128    return r;
2129  }
2130
2131
2132
2133  /**
2134   * Parses the provided set of lines into a list of {@code StringBuilder}
2135   * objects suitable for decoding into an entry or LDIF change record.
2136   * Comments will be ignored and wrapped lines will be unwrapped.
2137   *
2138   * @param  duplicateValueBehavior  The behavior that should be exhibited if
2139   *                                 the LDIF reader encounters an entry with
2140   *                                 duplicate values.
2141   * @param  trailingSpaceBehavior   The behavior that should be exhibited when
2142   *                                 encountering attribute values which are not
2143   *                                 base64-encoded but contain trailing spaces.
2144   * @param  schema                  The schema to use when parsing the record,
2145   *                                 if applicable.
2146   * @param  ldifLines               The set of lines that comprise the record
2147   *                                 to decode.  It must not be {@code null} or
2148   *                                 empty.
2149   *
2150   * @return  The prepared list of {@code StringBuilder} objects ready to be
2151   *          decoded.
2152   *
2153   * @throws  LDIFException  If the provided lines do not contain valid LDIF
2154   *                         content.
2155   */
2156  @NotNull()
2157  private static UnparsedLDIFRecord prepareRecord(
2158               @NotNull final DuplicateValueBehavior duplicateValueBehavior,
2159               @NotNull final TrailingSpaceBehavior trailingSpaceBehavior,
2160               @Nullable final Schema schema,
2161               @NotNull final String... ldifLines)
2162          throws LDIFException
2163  {
2164    Validator.ensureNotNull(ldifLines);
2165    Validator.ensureFalse(ldifLines.length == 0,
2166         "LDIFReader.prepareRecord.ldifLines must not be empty.");
2167
2168    boolean lastWasComment = false;
2169    final ArrayList<StringBuilder> lineList = new ArrayList<>(ldifLines.length);
2170    for (int i=0; i < ldifLines.length; i++)
2171    {
2172      final String line = ldifLines[i];
2173      if (line.isEmpty())
2174      {
2175        // This is only acceptable if there are no more non-empty lines in the
2176        // array.
2177        for (int j=i+1; j < ldifLines.length; j++)
2178        {
2179          if (! ldifLines[j].isEmpty())
2180          {
2181            throw new LDIFException(ERR_READ_UNEXPECTED_BLANK.get(i), i, true,
2182                                    ldifLines, null);
2183          }
2184
2185          // If we've gotten here, then we know that we're at the end of the
2186          // entry.  If we have read data, then we can decode it as an entry.
2187          // Otherwise, there was no real data in the provided LDIF lines.
2188          if (lineList.isEmpty())
2189          {
2190            throw new LDIFException(ERR_READ_ONLY_BLANKS.get(), 0, true,
2191                                    ldifLines, null);
2192          }
2193          else
2194          {
2195            return new UnparsedLDIFRecord(lineList, duplicateValueBehavior,
2196                 trailingSpaceBehavior, schema, 0);
2197          }
2198        }
2199      }
2200
2201      if (line.charAt(0) == ' ')
2202      {
2203        if (i > 0)
2204        {
2205          if (! lastWasComment)
2206          {
2207            lineList.get(lineList.size() - 1).append(line.substring(1));
2208          }
2209        }
2210        else
2211        {
2212          throw new LDIFException(
2213                         ERR_READ_UNEXPECTED_FIRST_SPACE_NO_NUMBER.get(), 0,
2214                         true, ldifLines, null);
2215        }
2216      }
2217      else if (line.charAt(0) == '#')
2218      {
2219        lastWasComment = true;
2220      }
2221      else
2222      {
2223        lineList.add(new StringBuilder(line));
2224        lastWasComment = false;
2225      }
2226    }
2227
2228    if (lineList.isEmpty())
2229    {
2230      throw new LDIFException(ERR_READ_NO_DATA.get(), 0, true, ldifLines, null);
2231    }
2232    else
2233    {
2234      return new UnparsedLDIFRecord(lineList, duplicateValueBehavior,
2235           trailingSpaceBehavior, schema, 0);
2236    }
2237  }
2238
2239
2240
2241  /**
2242   * Decodes the unparsed record that was read from the LDIF source.  It may be
2243   * either an entry or an LDIF change record.
2244   *
2245   * @param  unparsedRecord    The unparsed LDIF record that was read from the
2246   *                           input.  It must not be {@code null} or empty.
2247   * @param  relativeBasePath  The base path that will be prepended to relative
2248   *                           paths in order to obtain an absolute path.
2249   * @param  schema            The schema to use when parsing.
2250   *
2251   * @return  The parsed record, or {@code null} if there are no more entries to
2252   *          be read.
2253   *
2254   * @throws  LDIFException  If the data read could not be parsed as an entry or
2255   *                         an LDIF change record.
2256   */
2257  @NotNull()
2258  private static LDIFRecord decodeRecord(
2259                      @NotNull final UnparsedLDIFRecord unparsedRecord,
2260                      @NotNull final String relativeBasePath,
2261                      @Nullable final Schema schema)
2262       throws LDIFException
2263  {
2264    // If there was an error reading from the input, then we rethrow it here.
2265    final Exception readError = unparsedRecord.getFailureCause();
2266    if (readError != null)
2267    {
2268      if (readError instanceof LDIFException)
2269      {
2270        // If the error was an LDIFException, which will normally be the case,
2271        // then rethrow it with all of the same state.  We could just
2272        //   throw (LDIFException) readError;
2273        // but that's considered bad form.
2274        final LDIFException ldifEx = (LDIFException) readError;
2275        throw new LDIFException(ldifEx.getMessage(),
2276                                ldifEx.getLineNumber(),
2277                                ldifEx.mayContinueReading(),
2278                                ldifEx.getDataLines(),
2279                                ldifEx.getCause());
2280      }
2281      else
2282      {
2283        throw new LDIFException(StaticUtils.getExceptionMessage(readError),
2284             -1, true, readError);
2285      }
2286    }
2287
2288    if (unparsedRecord.isEOF())
2289    {
2290      return null;
2291    }
2292
2293    final ArrayList<StringBuilder> lineList = unparsedRecord.getLineList();
2294    if (unparsedRecord.getLineList() == null)
2295    {
2296      return null;  // We can get here if there was an error reading the lines.
2297    }
2298
2299    final LDIFRecord r;
2300    if (lineList.size() == 1)
2301    {
2302      r = decodeEntry(unparsedRecord, relativeBasePath);
2303    }
2304    else
2305    {
2306      final String lowerSecondLine =
2307           StaticUtils.toLowerCase(lineList.get(1).toString());
2308      if (lowerSecondLine.startsWith("control:") ||
2309          lowerSecondLine.startsWith("changetype:"))
2310      {
2311        r = decodeChangeRecord(unparsedRecord, relativeBasePath, true, schema);
2312      }
2313      else
2314      {
2315        r = decodeEntry(unparsedRecord, relativeBasePath);
2316      }
2317    }
2318
2319    Debug.debugLDIFRead(r);
2320    return r;
2321  }
2322
2323
2324
2325  /**
2326   * Decodes the provided set of LDIF lines as an entry.  The provided list must
2327   * not contain any blank lines or comments, and lines are not allowed to be
2328   * wrapped.
2329   *
2330   * @param  unparsedRecord   The unparsed LDIF record that was read from the
2331   *                          input.  It must not be {@code null} or empty.
2332   * @param  relativeBasePath  The base path that will be prepended to relative
2333   *                           paths in order to obtain an absolute path.
2334   *
2335   * @return  The entry read from LDIF.
2336   *
2337   * @throws  LDIFException  If the provided LDIF data cannot be read as an
2338   *                         entry.
2339   */
2340  @NotNull()
2341  private static Entry decodeEntry(
2342                            @NotNull final UnparsedLDIFRecord unparsedRecord,
2343                            @NotNull final String relativeBasePath)
2344          throws LDIFException
2345  {
2346    final ArrayList<StringBuilder> ldifLines = unparsedRecord.getLineList();
2347    final long firstLineNumber = unparsedRecord.getFirstLineNumber();
2348
2349    final Iterator<StringBuilder> iterator = ldifLines.iterator();
2350
2351    // The first line must start with either "version:" or "dn:".  If the first
2352    // line starts with "version:" then the second must start with "dn:".
2353    StringBuilder line = iterator.next();
2354    handleTrailingSpaces(line, null, firstLineNumber,
2355         unparsedRecord.getTrailingSpaceBehavior());
2356    int colonPos = line.indexOf(":");
2357    if ((colonPos > 0) &&
2358        line.substring(0, colonPos).equalsIgnoreCase("version"))
2359    {
2360      // The first line is "version:".  Under most conditions, this will be
2361      // handled by the LDIF reader, but this can happen if you call
2362      // decodeEntry with a set of data that includes a version.  At any rate,
2363      // read the next line, which must specify the DN.
2364      line = iterator.next();
2365      handleTrailingSpaces(line, null, firstLineNumber,
2366           unparsedRecord.getTrailingSpaceBehavior());
2367    }
2368
2369    colonPos = line.indexOf(":");
2370    if ((colonPos < 0) ||
2371         (! line.substring(0, colonPos).equalsIgnoreCase("dn")))
2372    {
2373      throw new LDIFException(
2374           ERR_READ_DN_LINE_DOESNT_START_WITH_DN.get(firstLineNumber),
2375           firstLineNumber, true, ldifLines, null);
2376    }
2377
2378    final String dn;
2379    final int length = line.length();
2380    if (length == (colonPos+1))
2381    {
2382      // The colon was the last character on the line.  This is acceptable and
2383      // indicates that the entry has the null DN.
2384      dn = "";
2385    }
2386    else if (line.charAt(colonPos+1) == ':')
2387    {
2388      // Skip over any spaces leading up to the value, and then the rest of the
2389      // string is the base64-encoded DN.
2390      int pos = colonPos+2;
2391      while ((pos < length) && (line.charAt(pos) == ' '))
2392      {
2393        pos++;
2394      }
2395
2396      try
2397      {
2398        final byte[] dnBytes = Base64.decode(line.substring(pos));
2399        dn = StaticUtils.toUTF8String(dnBytes);
2400      }
2401      catch (final ParseException pe)
2402      {
2403        Debug.debugException(pe);
2404        throw new LDIFException(
2405                       ERR_READ_CANNOT_BASE64_DECODE_DN.get(firstLineNumber,
2406                                                            pe.getMessage()),
2407                       firstLineNumber, true, ldifLines, pe);
2408      }
2409      catch (final Exception e)
2410      {
2411        Debug.debugException(e);
2412        throw new LDIFException(
2413                       ERR_READ_CANNOT_BASE64_DECODE_DN.get(firstLineNumber, e),
2414                       firstLineNumber, true, ldifLines, e);
2415      }
2416    }
2417    else
2418    {
2419      // Skip over any spaces leading up to the value, and then the rest of the
2420      // string is the DN.
2421      int pos = colonPos+1;
2422      while ((pos < length) && (line.charAt(pos) == ' '))
2423      {
2424        pos++;
2425      }
2426
2427      dn = line.substring(pos);
2428    }
2429
2430
2431    // The remaining lines must be the attributes for the entry.  However, we
2432    // will allow the case in which an entry does not have any attributes, to be
2433    // able to support reading search result entries in which no attributes were
2434    // returned.
2435    if (! iterator.hasNext())
2436    {
2437      return new Entry(dn, unparsedRecord.getSchema());
2438    }
2439
2440    return new Entry(dn, unparsedRecord.getSchema(),
2441         parseAttributes(dn, unparsedRecord.getDuplicateValueBehavior(),
2442              unparsedRecord.getTrailingSpaceBehavior(),
2443              unparsedRecord.getSchema(), ldifLines, iterator, relativeBasePath,
2444              firstLineNumber));
2445  }
2446
2447
2448
2449  /**
2450   * Decodes the provided set of LDIF lines as a change record.  The provided
2451   * list must not contain any blank lines or comments, and lines are not
2452   * allowed to be wrapped.
2453   *
2454   * @param  unparsedRecord    The unparsed LDIF record that was read from the
2455   *                           input.  It must not be {@code null} or empty.
2456   * @param  relativeBasePath  The base path that will be prepended to relative
2457   *                           paths in order to obtain an absolute path.
2458   * @param  defaultAdd        Indicates whether an LDIF record not containing a
2459   *                           changetype should be retrieved as an add change
2460   *                           record.  If this is {@code false} and the record
2461   *                           read does not include a changetype, then an
2462   *                           {@link LDIFException} will be thrown.
2463   * @param  schema            The schema to use in parsing.
2464   *
2465   * @return  The change record read from LDIF.
2466   *
2467   * @throws  LDIFException  If the provided LDIF data cannot be decoded as a
2468   *                         change record.
2469   */
2470  @NotNull()
2471  private static LDIFChangeRecord decodeChangeRecord(
2472                      @NotNull final UnparsedLDIFRecord unparsedRecord,
2473                      @NotNull final String relativeBasePath,
2474                      final boolean defaultAdd,
2475                      @Nullable final Schema schema)
2476          throws LDIFException
2477  {
2478    final ArrayList<StringBuilder> ldifLines = unparsedRecord.getLineList();
2479    final long firstLineNumber = unparsedRecord.getFirstLineNumber();
2480
2481    Iterator<StringBuilder> iterator = ldifLines.iterator();
2482
2483    // The first line must start with either "version:" or "dn:".  If the first
2484    // line starts with "version:" then the second must start with "dn:".
2485    StringBuilder line = iterator.next();
2486    handleTrailingSpaces(line, null, firstLineNumber,
2487         unparsedRecord.getTrailingSpaceBehavior());
2488    int colonPos = line.indexOf(":");
2489    int linesRead = 1;
2490    if ((colonPos > 0) &&
2491        line.substring(0, colonPos).equalsIgnoreCase("version"))
2492    {
2493      // The first line is "version:".  Under most conditions, this will be
2494      // handled by the LDIF reader, but this can happen if you call
2495      // decodeEntry with a set of data that includes a version.  At any rate,
2496      // read the next line, which must specify the DN.
2497      line = iterator.next();
2498      linesRead++;
2499      handleTrailingSpaces(line, null, firstLineNumber,
2500           unparsedRecord.getTrailingSpaceBehavior());
2501    }
2502
2503    colonPos = line.indexOf(":");
2504    if ((colonPos < 0) ||
2505         (! line.substring(0, colonPos).equalsIgnoreCase("dn")))
2506    {
2507      throw new LDIFException(
2508           ERR_READ_DN_LINE_DOESNT_START_WITH_DN.get(firstLineNumber),
2509           firstLineNumber, true, ldifLines, null);
2510    }
2511
2512    final String dn;
2513    final int length = line.length();
2514    if (length == (colonPos+1))
2515    {
2516      // The colon was the last character on the line.  This is acceptable and
2517      // indicates that the entry has the null DN.
2518      dn = "";
2519    }
2520    else if (line.charAt(colonPos+1) == ':')
2521    {
2522      // Skip over any spaces leading up to the value, and then the rest of the
2523      // string is the base64-encoded DN.
2524      int pos = colonPos+2;
2525      while ((pos < length) && (line.charAt(pos) == ' '))
2526      {
2527        pos++;
2528      }
2529
2530      try
2531      {
2532        final byte[] dnBytes = Base64.decode(line.substring(pos));
2533        dn = StaticUtils.toUTF8String(dnBytes);
2534      }
2535      catch (final ParseException pe)
2536      {
2537        Debug.debugException(pe);
2538        throw new LDIFException(
2539                       ERR_READ_CR_CANNOT_BASE64_DECODE_DN.get(firstLineNumber,
2540                                                               pe.getMessage()),
2541                       firstLineNumber, true, ldifLines, pe);
2542      }
2543      catch (final Exception e)
2544      {
2545        Debug.debugException(e);
2546        throw new LDIFException(
2547                       ERR_READ_CR_CANNOT_BASE64_DECODE_DN.get(firstLineNumber,
2548                                                               e),
2549                       firstLineNumber, true, ldifLines, e);
2550      }
2551    }
2552    else
2553    {
2554      // Skip over any spaces leading up to the value, and then the rest of the
2555      // string is the DN.
2556      int pos = colonPos+1;
2557      while ((pos < length) && (line.charAt(pos) == ' '))
2558      {
2559        pos++;
2560      }
2561
2562      dn = line.substring(pos);
2563    }
2564
2565
2566    // An LDIF change record may contain zero or more controls, with the end of
2567    // the controls signified by the changetype.  The changetype element must be
2568    // present, unless defaultAdd is true in which case the first thing that is
2569    // neither control or changetype will trigger the start of add attribute
2570    // parsing.
2571    if (! iterator.hasNext())
2572    {
2573      throw new LDIFException(ERR_READ_CR_TOO_SHORT.get(firstLineNumber),
2574                              firstLineNumber, true, ldifLines, null);
2575    }
2576
2577    String changeType;
2578    ArrayList<Control> controls = null;
2579    while (true)
2580    {
2581      line = iterator.next();
2582      handleTrailingSpaces(line, dn, firstLineNumber,
2583           unparsedRecord.getTrailingSpaceBehavior());
2584      colonPos = line.indexOf(":");
2585      if (colonPos < 0)
2586      {
2587        throw new LDIFException(
2588             ERR_READ_CR_SECOND_LINE_MISSING_COLON.get(firstLineNumber),
2589             firstLineNumber, true, ldifLines, null);
2590      }
2591
2592      final String token = StaticUtils.toLowerCase(line.substring(0, colonPos));
2593      if (token.equals("control"))
2594      {
2595        if (controls == null)
2596        {
2597          controls = new ArrayList<>(5);
2598        }
2599
2600        controls.add(decodeControl(line, colonPos, firstLineNumber, ldifLines,
2601             relativeBasePath));
2602      }
2603      else if (token.equals("changetype"))
2604      {
2605        changeType =
2606             decodeChangeType(line, colonPos, firstLineNumber, ldifLines);
2607        break;
2608      }
2609      else if (defaultAdd)
2610      {
2611        // The line we read wasn't a control or changetype declaration, so we'll
2612        // assume it's an attribute in an add record.  However, we're not ready
2613        // for that yet, and since we can't rewind an iterator we'll create a
2614        // new one that hasn't yet gotten to this line.
2615        changeType = "add";
2616        iterator = ldifLines.iterator();
2617        for (int i=0; i < linesRead; i++)
2618        {
2619          iterator.next();
2620        }
2621        break;
2622      }
2623      else
2624      {
2625        throw new LDIFException(
2626             ERR_READ_CR_CT_LINE_DOESNT_START_WITH_CONTROL_OR_CT.get(
2627                  firstLineNumber),
2628             firstLineNumber, true, ldifLines, null);
2629      }
2630
2631      linesRead++;
2632    }
2633
2634
2635    // Make sure that the change type is acceptable and then decode the rest of
2636    // the change record accordingly.
2637    final String lowerChangeType = StaticUtils.toLowerCase(changeType);
2638    if (lowerChangeType.equals("add"))
2639    {
2640      // There must be at least one more line.  If not, then that's an error.
2641      // Otherwise, parse the rest of the data as attribute-value pairs.
2642      if (iterator.hasNext())
2643      {
2644        final Collection<Attribute> attrs =
2645             parseAttributes(dn, unparsedRecord.getDuplicateValueBehavior(),
2646                  unparsedRecord.getTrailingSpaceBehavior(),
2647                  unparsedRecord.getSchema(), ldifLines, iterator,
2648                  relativeBasePath, firstLineNumber);
2649        final Attribute[] attributes = new Attribute[attrs.size()];
2650        final Iterator<Attribute> attrIterator = attrs.iterator();
2651        for (int i=0; i < attributes.length; i++)
2652        {
2653          attributes[i] = attrIterator.next();
2654        }
2655
2656        return new LDIFAddChangeRecord(dn, attributes, controls);
2657      }
2658      else
2659      {
2660        throw new LDIFException(ERR_READ_CR_NO_ATTRIBUTES.get(firstLineNumber),
2661                                firstLineNumber, true, ldifLines, null);
2662      }
2663    }
2664    else if (lowerChangeType.equals("delete"))
2665    {
2666      // There shouldn't be any more data.  If there is, then that's an error.
2667      // Otherwise, we can just return the delete change record with what we
2668      // already know.
2669      if (iterator.hasNext())
2670      {
2671        throw new LDIFException(
2672                       ERR_READ_CR_EXTRA_DELETE_DATA.get(firstLineNumber),
2673                       firstLineNumber, true, ldifLines, null);
2674      }
2675      else
2676      {
2677        return new LDIFDeleteChangeRecord(dn, controls);
2678      }
2679    }
2680    else if (lowerChangeType.equals("modify"))
2681    {
2682      // There must be at least one more line.  If not, then that's an error.
2683      // Otherwise, parse the rest of the data as a set of modifications.
2684      if (iterator.hasNext())
2685      {
2686        final Modification[] mods = parseModifications(dn,
2687             unparsedRecord.getTrailingSpaceBehavior(), ldifLines, iterator,
2688             firstLineNumber, schema);
2689        return new LDIFModifyChangeRecord(dn, mods, controls);
2690      }
2691      else
2692      {
2693        throw new LDIFException(ERR_READ_CR_NO_MODS.get(firstLineNumber),
2694                                firstLineNumber, true, ldifLines, null);
2695      }
2696    }
2697    else if (lowerChangeType.equals("moddn") ||
2698             lowerChangeType.equals("modrdn"))
2699    {
2700      // There must be at least one more line.  If not, then that's an error.
2701      // Otherwise, parse the rest of the data as a set of modifications.
2702      if (iterator.hasNext())
2703      {
2704        return parseModifyDNChangeRecord(ldifLines, iterator, dn, controls,
2705             unparsedRecord.getTrailingSpaceBehavior(), firstLineNumber);
2706      }
2707      else
2708      {
2709        throw new LDIFException(ERR_READ_CR_NO_NEWRDN.get(firstLineNumber),
2710                                firstLineNumber, true, ldifLines, null);
2711      }
2712    }
2713    else
2714    {
2715      throw new LDIFException(ERR_READ_CR_INVALID_CT.get(changeType,
2716                                                         firstLineNumber),
2717                              firstLineNumber, true, ldifLines, null);
2718    }
2719  }
2720
2721
2722
2723  /**
2724   * Decodes information about a control from the provided line.
2725   *
2726   * @param  line              The line to process.
2727   * @param  colonPos          The position of the colon that separates the
2728   *                           control token string from tbe encoded control.
2729   * @param  firstLineNumber   The line number for the start of the record.
2730   * @param  ldifLines         The lines that comprise the LDIF representation
2731   *                           of the full record being parsed.
2732   * @param  relativeBasePath  The base path that will be prepended to relative
2733   *                           paths in order to obtain an absolute path.
2734   *
2735   * @return  The decoded control.
2736   *
2737   * @throws  LDIFException  If a problem is encountered while trying to decode
2738   *                         the changetype.
2739   */
2740  @NotNull()
2741  private static Control decodeControl(@NotNull final StringBuilder line,
2742                              final int colonPos, final long firstLineNumber,
2743                              @NotNull final ArrayList<StringBuilder> ldifLines,
2744                              @NotNull final String relativeBasePath)
2745          throws LDIFException
2746  {
2747    final String controlString;
2748    int length = line.length();
2749    if (length == (colonPos+1))
2750    {
2751      // The colon was the last character on the line.  This is not
2752      // acceptable.
2753      throw new LDIFException(
2754           ERR_READ_CONTROL_LINE_NO_CONTROL_VALUE.get(firstLineNumber),
2755           firstLineNumber, true, ldifLines, null);
2756    }
2757    else if (line.charAt(colonPos+1) == ':')
2758    {
2759      // Skip over any spaces leading up to the value, and then the rest of
2760      // the string is the base64-encoded control representation.  This is
2761      // unusual and unnecessary, but is nevertheless acceptable.
2762      int pos = colonPos+2;
2763      while ((pos < length) && (line.charAt(pos) == ' '))
2764      {
2765        pos++;
2766      }
2767
2768      try
2769      {
2770        final byte[] controlBytes = Base64.decode(line.substring(pos));
2771        controlString =  StaticUtils.toUTF8String(controlBytes);
2772      }
2773      catch (final ParseException pe)
2774      {
2775        Debug.debugException(pe);
2776        throw new LDIFException(
2777                       ERR_READ_CANNOT_BASE64_DECODE_CONTROL.get(
2778                            firstLineNumber, pe.getMessage()),
2779                       firstLineNumber, true, ldifLines, pe);
2780      }
2781      catch (final Exception e)
2782      {
2783        Debug.debugException(e);
2784        throw new LDIFException(
2785             ERR_READ_CANNOT_BASE64_DECODE_CONTROL.get(firstLineNumber, e),
2786             firstLineNumber, true, ldifLines, e);
2787      }
2788    }
2789    else
2790    {
2791      // Skip over any spaces leading up to the value, and then the rest of
2792      // the string is the encoded control.
2793      int pos = colonPos+1;
2794      while ((pos < length) && (line.charAt(pos) == ' '))
2795      {
2796        pos++;
2797      }
2798
2799      controlString = line.substring(pos);
2800    }
2801
2802    // If the resulting control definition is empty, then that's invalid.
2803    if (controlString.isEmpty())
2804    {
2805      throw new LDIFException(
2806           ERR_READ_CONTROL_LINE_NO_CONTROL_VALUE.get(firstLineNumber),
2807           firstLineNumber, true, ldifLines, null);
2808    }
2809
2810
2811    // The first element of the control must be the OID, and it must be followed
2812    // by a space (to separate it from the criticality), a colon (to separate it
2813    // from the value and indicate a default criticality of false), or the end
2814    // of the line (to indicate a default criticality of false and no value).
2815    String oid = null;
2816    boolean hasCriticality = false;
2817    boolean hasValue = false;
2818    int pos = 0;
2819    length = controlString.length();
2820    while (pos < length)
2821    {
2822      final char c = controlString.charAt(pos);
2823      if (c == ':')
2824      {
2825        // This indicates that there is no criticality and that the value
2826        // immediately follows the OID.
2827        oid = controlString.substring(0, pos++);
2828        hasValue = true;
2829        break;
2830      }
2831      else if (c == ' ')
2832      {
2833        // This indicates that there is a criticality.  We don't know anything
2834        // about the presence of a value yet.
2835        oid = controlString.substring(0, pos++);
2836        hasCriticality = true;
2837        break;
2838      }
2839      else
2840      {
2841        pos++;
2842      }
2843    }
2844
2845    if (oid == null)
2846    {
2847      // This indicates that the string representation of the control is only
2848      // the OID.
2849      return new Control(controlString, false);
2850    }
2851
2852
2853    // See if we need to read the criticality.  If so, then do so now.
2854    // Otherwise, assume a default criticality of false.
2855    final boolean isCritical;
2856    if (hasCriticality)
2857    {
2858      // Skip over any spaces before the criticality.
2859      while (controlString.charAt(pos) == ' ')
2860      {
2861        pos++;
2862      }
2863
2864      // Read until we find a colon or the end of the string.
2865      final int criticalityStartPos = pos;
2866      while (pos < length)
2867      {
2868        final char c = controlString.charAt(pos);
2869        if (c == ':')
2870        {
2871          hasValue = true;
2872          break;
2873        }
2874        else
2875        {
2876          pos++;
2877        }
2878      }
2879
2880      final String criticalityString =
2881           StaticUtils.toLowerCase(controlString.substring(criticalityStartPos,
2882                pos));
2883      if (criticalityString.equals("true"))
2884      {
2885        isCritical = true;
2886      }
2887      else if (criticalityString.equals("false"))
2888      {
2889        isCritical = false;
2890      }
2891      else
2892      {
2893        throw new LDIFException(
2894             ERR_READ_CONTROL_LINE_INVALID_CRITICALITY.get(criticalityString,
2895                  firstLineNumber),
2896             firstLineNumber, true, ldifLines, null);
2897      }
2898
2899      if (hasValue)
2900      {
2901        pos++;
2902      }
2903    }
2904    else
2905    {
2906      isCritical = false;
2907    }
2908
2909    // See if we need to read the value.  If so, then do so now.  It may be
2910    // a string, or it may be base64-encoded.  It could conceivably even be read
2911    // from a URL.
2912    final ASN1OctetString value;
2913    if (hasValue)
2914    {
2915      // The character immediately after the colon that precedes the value may
2916      // be one of the following:
2917      // - A second colon (optionally followed by a single space) to indicate
2918      //   that the value is base64-encoded.
2919      // - A less-than symbol to indicate that the value should be read from a
2920      //   location specified by a URL.
2921      // - A single space that precedes the non-base64-encoded value.
2922      // - The first character of the non-base64-encoded value.
2923      switch (controlString.charAt(pos))
2924      {
2925        case ':':
2926          try
2927          {
2928            if (controlString.length() == (pos+1))
2929            {
2930              value = new ASN1OctetString();
2931            }
2932            else if (controlString.charAt(pos+1) == ' ')
2933            {
2934              value = new ASN1OctetString(
2935                   Base64.decode(controlString.substring(pos+2)));
2936            }
2937            else
2938            {
2939              value = new ASN1OctetString(
2940                   Base64.decode(controlString.substring(pos+1)));
2941            }
2942          }
2943          catch (final Exception e)
2944          {
2945            Debug.debugException(e);
2946            throw new LDIFException(
2947                 ERR_READ_CONTROL_LINE_CANNOT_BASE64_DECODE_VALUE.get(
2948                      firstLineNumber, StaticUtils.getExceptionMessage(e)),
2949                 firstLineNumber, true, ldifLines, e);
2950          }
2951          break;
2952        case '<':
2953          try
2954          {
2955            final String urlString;
2956            if (controlString.charAt(pos+1) == ' ')
2957            {
2958              urlString = controlString.substring(pos+2);
2959            }
2960            else
2961            {
2962              urlString = controlString.substring(pos+1);
2963            }
2964            value = new ASN1OctetString(retrieveURLBytes(urlString,
2965                 relativeBasePath, firstLineNumber));
2966          }
2967          catch (final Exception e)
2968          {
2969            Debug.debugException(e);
2970            throw new LDIFException(
2971                 ERR_READ_CONTROL_LINE_CANNOT_RETRIEVE_VALUE_FROM_URL.get(
2972                      firstLineNumber, StaticUtils.getExceptionMessage(e)),
2973                 firstLineNumber, true, ldifLines, e);
2974          }
2975          break;
2976        case ' ':
2977          value = new ASN1OctetString(controlString.substring(pos+1));
2978          break;
2979        default:
2980          value = new ASN1OctetString(controlString.substring(pos));
2981          break;
2982      }
2983    }
2984    else
2985    {
2986      value = null;
2987    }
2988
2989    return new Control(oid, isCritical, value);
2990  }
2991
2992
2993
2994  /**
2995   * Decodes the changetype element from the provided line.
2996   *
2997   * @param  line             The line to process.
2998   * @param  colonPos         The position of the colon that separates the
2999   *                          changetype string from its value.
3000   * @param  firstLineNumber  The line number for the start of the record.
3001   * @param  ldifLines        The lines that comprise the LDIF representation of
3002   *                          the full record being parsed.
3003   *
3004   * @return  The decoded changetype string.
3005   *
3006   * @throws  LDIFException  If a problem is encountered while trying to decode
3007   *                         the changetype.
3008   */
3009  @NotNull()
3010  private static String decodeChangeType(@NotNull final StringBuilder line,
3011                             final int colonPos, final long firstLineNumber,
3012                             @NotNull final ArrayList<StringBuilder> ldifLines)
3013          throws LDIFException
3014  {
3015    final int length = line.length();
3016    if (length == (colonPos+1))
3017    {
3018      // The colon was the last character on the line.  This is not
3019      // acceptable.
3020      throw new LDIFException(
3021           ERR_READ_CT_LINE_NO_CT_VALUE.get(firstLineNumber), firstLineNumber,
3022           true, ldifLines, null);
3023    }
3024    else if (line.charAt(colonPos+1) == ':')
3025    {
3026      // Skip over any spaces leading up to the value, and then the rest of
3027      // the string is the base64-encoded changetype.  This is unusual and
3028      // unnecessary, but is nevertheless acceptable.
3029      int pos = colonPos+2;
3030      while ((pos < length) && (line.charAt(pos) == ' '))
3031      {
3032        pos++;
3033      }
3034
3035      try
3036      {
3037        final byte[] changeTypeBytes = Base64.decode(line.substring(pos));
3038        return StaticUtils.toUTF8String(changeTypeBytes);
3039      }
3040      catch (final ParseException pe)
3041      {
3042        Debug.debugException(pe);
3043        throw new LDIFException(
3044                       ERR_READ_CANNOT_BASE64_DECODE_CT.get(firstLineNumber,
3045                                                            pe.getMessage()),
3046                       firstLineNumber, true, ldifLines, pe);
3047      }
3048      catch (final Exception e)
3049      {
3050        Debug.debugException(e);
3051        throw new LDIFException(
3052             ERR_READ_CANNOT_BASE64_DECODE_CT.get(firstLineNumber, e),
3053             firstLineNumber, true, ldifLines, e);
3054      }
3055    }
3056    else
3057    {
3058      // Skip over any spaces leading up to the value, and then the rest of
3059      // the string is the changetype.
3060      int pos = colonPos+1;
3061      while ((pos < length) && (line.charAt(pos) == ' '))
3062      {
3063        pos++;
3064      }
3065
3066      return line.substring(pos);
3067    }
3068  }
3069
3070
3071
3072  /**
3073   * Parses the data available through the provided iterator as a collection of
3074   * attributes suitable for use in an entry or an add change record.
3075   *
3076   * @param  dn                      The DN of the record being read.
3077   * @param  duplicateValueBehavior  The behavior that should be exhibited if
3078   *                                 the LDIF reader encounters an entry with
3079   *                                 duplicate values.
3080   * @param  trailingSpaceBehavior   The behavior that should be exhibited when
3081   *                                 encountering attribute values which are not
3082   *                                 base64-encoded but contain trailing spaces.
3083   * @param  schema                  The schema to use when parsing the
3084   *                                 attributes, or {@code null} if none is
3085   *                                 needed.
3086   * @param  ldifLines               The lines that comprise the LDIF
3087   *                                 representation of the full record being
3088   *                                 parsed.
3089   * @param  iterator                The iterator to use to access the attribute
3090   *                                 lines.
3091   * @param  relativeBasePath        The base path that will be prepended to
3092   *                                 relative paths in order to obtain an
3093   *                                 absolute path.
3094   * @param  firstLineNumber         The line number for the start of the
3095   *                                 record.
3096   *
3097   * @return  The collection of attributes that were read.
3098   *
3099   * @throws  LDIFException  If the provided LDIF data cannot be decoded as a
3100   *                         set of attributes.
3101   */
3102  @NotNull()
3103  private static ArrayList<Attribute> parseAttributes(@NotNull final String dn,
3104               @NotNull final DuplicateValueBehavior duplicateValueBehavior,
3105               @NotNull final TrailingSpaceBehavior trailingSpaceBehavior,
3106               @Nullable final Schema schema,
3107               @NotNull final ArrayList<StringBuilder> ldifLines,
3108               @NotNull final Iterator<StringBuilder> iterator,
3109               @NotNull final String relativeBasePath,
3110               final long firstLineNumber)
3111          throws LDIFException
3112  {
3113    final LinkedHashMap<String,Object> attributes =
3114         new LinkedHashMap<>(StaticUtils.computeMapCapacity(ldifLines.size()));
3115    while (iterator.hasNext())
3116    {
3117      final StringBuilder line = iterator.next();
3118      handleTrailingSpaces(line, dn, firstLineNumber, trailingSpaceBehavior);
3119      final int colonPos = line.indexOf(":");
3120      if (colonPos <= 0)
3121      {
3122        throw new LDIFException(ERR_READ_NO_ATTR_COLON.get(firstLineNumber),
3123                                firstLineNumber, true, ldifLines, null);
3124      }
3125
3126      final String attributeName = line.substring(0, colonPos);
3127      final String lowerName     = StaticUtils.toLowerCase(attributeName);
3128
3129      final MatchingRule matchingRule;
3130      if (schema == null)
3131      {
3132        matchingRule = CaseIgnoreStringMatchingRule.getInstance();
3133      }
3134      else
3135      {
3136        matchingRule =
3137             MatchingRule.selectEqualityMatchingRule(attributeName, schema);
3138      }
3139
3140      Attribute attr;
3141      final LDIFAttribute ldifAttr;
3142      final Object attrObject = attributes.get(lowerName);
3143      if (attrObject == null)
3144      {
3145        attr     = null;
3146        ldifAttr = null;
3147      }
3148      else
3149      {
3150        if (attrObject instanceof Attribute)
3151        {
3152          attr     = (Attribute) attrObject;
3153          ldifAttr = new LDIFAttribute(attr.getName(), matchingRule,
3154                                       attr.getRawValues()[0]);
3155          attributes.put(lowerName, ldifAttr);
3156        }
3157        else
3158        {
3159          attr     = null;
3160          ldifAttr = (LDIFAttribute) attrObject;
3161        }
3162      }
3163
3164      final int length = line.length();
3165      if (length == (colonPos+1))
3166      {
3167        // This means that the attribute has a zero-length value, which is
3168        // acceptable.
3169        if (attrObject == null)
3170        {
3171          attr = new Attribute(attributeName, matchingRule, "");
3172          attributes.put(lowerName, attr);
3173        }
3174        else
3175        {
3176          try
3177          {
3178            if (! ldifAttr.addValue(new ASN1OctetString(),
3179                       duplicateValueBehavior))
3180            {
3181              if (duplicateValueBehavior != DuplicateValueBehavior.STRIP)
3182              {
3183                throw new LDIFException(ERR_READ_DUPLICATE_VALUE.get(dn,
3184                     firstLineNumber, attributeName), firstLineNumber, true,
3185                     ldifLines, null);
3186              }
3187            }
3188          }
3189          catch (final LDAPException le)
3190          {
3191            throw new LDIFException(
3192                 ERR_READ_VALUE_SYNTAX_VIOLATION.get(dn, firstLineNumber,
3193                      attributeName, StaticUtils.getExceptionMessage(le)),
3194                 firstLineNumber, true, ldifLines, le);
3195          }
3196        }
3197      }
3198      else if (line.charAt(colonPos+1) == ':')
3199      {
3200        // Skip over any spaces leading up to the value, and then the rest of
3201        // the string is the base64-encoded attribute value.
3202        int pos = colonPos+2;
3203        while ((pos < length) && (line.charAt(pos) == ' '))
3204        {
3205          pos++;
3206        }
3207
3208        try
3209        {
3210          final byte[] valueBytes = Base64.decode(line.substring(pos));
3211          if (attrObject == null)
3212          {
3213            attr = new Attribute(attributeName, matchingRule, valueBytes);
3214            attributes.put(lowerName, attr);
3215          }
3216          else
3217          {
3218            try
3219            {
3220              if (! ldifAttr.addValue(new ASN1OctetString(valueBytes),
3221                         duplicateValueBehavior))
3222              {
3223                if (duplicateValueBehavior != DuplicateValueBehavior.STRIP)
3224                {
3225                  throw new LDIFException(ERR_READ_DUPLICATE_VALUE.get(dn,
3226                       firstLineNumber, attributeName), firstLineNumber, true,
3227                       ldifLines, null);
3228                }
3229              }
3230            }
3231            catch (final LDAPException le)
3232            {
3233              throw new LDIFException(
3234                   ERR_READ_VALUE_SYNTAX_VIOLATION.get(dn, firstLineNumber,
3235                        attributeName, StaticUtils.getExceptionMessage(le)),
3236                   firstLineNumber, true, ldifLines, le);
3237            }
3238          }
3239        }
3240        catch (final ParseException pe)
3241        {
3242          Debug.debugException(pe);
3243          throw new LDIFException(
3244               ERR_READ_CANNOT_BASE64_DECODE_ATTR.get(attributeName,
3245                    firstLineNumber, pe.getMessage()),
3246               firstLineNumber, true, ldifLines, pe);
3247        }
3248      }
3249      else if (line.charAt(colonPos+1) == '<')
3250      {
3251        // Skip over any spaces leading up to the value, and then the rest of
3252        // the string is a URL that indicates where to get the real content.
3253        // At the present time, we'll only support the file URLs.
3254        int pos = colonPos+2;
3255        while ((pos < length) && (line.charAt(pos) == ' '))
3256        {
3257          pos++;
3258        }
3259
3260        final byte[] urlBytes;
3261        final String urlString = line.substring(pos);
3262        try
3263        {
3264          urlBytes =
3265               retrieveURLBytes(urlString, relativeBasePath, firstLineNumber);
3266        }
3267        catch (final Exception e)
3268        {
3269          Debug.debugException(e);
3270          throw new LDIFException(
3271               ERR_READ_URL_EXCEPTION.get(attributeName, urlString,
3272                    firstLineNumber, e),
3273               firstLineNumber, true, ldifLines, e);
3274        }
3275
3276        if (attrObject == null)
3277        {
3278          attr = new Attribute(attributeName, matchingRule, urlBytes);
3279          attributes.put(lowerName, attr);
3280        }
3281        else
3282        {
3283          try
3284          {
3285            if (! ldifAttr.addValue(new ASN1OctetString(urlBytes),
3286                 duplicateValueBehavior))
3287            {
3288              if (duplicateValueBehavior != DuplicateValueBehavior.STRIP)
3289              {
3290                throw new LDIFException(ERR_READ_DUPLICATE_VALUE.get(dn,
3291                     firstLineNumber, attributeName), firstLineNumber, true,
3292                     ldifLines, null);
3293              }
3294            }
3295          }
3296          catch (final LDIFException le)
3297          {
3298            Debug.debugException(le);
3299            throw le;
3300          }
3301          catch (final Exception e)
3302          {
3303            Debug.debugException(e);
3304            throw new LDIFException(
3305                 ERR_READ_URL_EXCEPTION.get(attributeName, urlString,
3306                      firstLineNumber, e),
3307                 firstLineNumber, true, ldifLines, e);
3308          }
3309        }
3310      }
3311      else
3312      {
3313        // Skip over any spaces leading up to the value, and then the rest of
3314        // the string is the value.
3315        int pos = colonPos+1;
3316        while ((pos < length) && (line.charAt(pos) == ' '))
3317        {
3318          pos++;
3319        }
3320
3321        final String valueString = line.substring(pos);
3322        if (attrObject == null)
3323        {
3324          attr = new Attribute(attributeName, matchingRule, valueString);
3325          attributes.put(lowerName, attr);
3326        }
3327        else
3328        {
3329          try
3330          {
3331            if (! ldifAttr.addValue(new ASN1OctetString(valueString),
3332                       duplicateValueBehavior))
3333            {
3334              if (duplicateValueBehavior != DuplicateValueBehavior.STRIP)
3335              {
3336                throw new LDIFException(ERR_READ_DUPLICATE_VALUE.get(dn,
3337                     firstLineNumber, attributeName), firstLineNumber, true,
3338                     ldifLines, null);
3339              }
3340            }
3341          }
3342          catch (final LDAPException le)
3343          {
3344            throw new LDIFException(
3345                 ERR_READ_VALUE_SYNTAX_VIOLATION.get(dn, firstLineNumber,
3346                      attributeName, StaticUtils.getExceptionMessage(le)),
3347                 firstLineNumber, true, ldifLines, le);
3348          }
3349        }
3350      }
3351    }
3352
3353    final ArrayList<Attribute> attrList = new ArrayList<>(attributes.size());
3354    for (final Object o : attributes.values())
3355    {
3356      if (o instanceof Attribute)
3357      {
3358        attrList.add((Attribute) o);
3359      }
3360      else
3361      {
3362        attrList.add(((LDIFAttribute) o).toAttribute());
3363      }
3364    }
3365
3366    return attrList;
3367  }
3368
3369
3370
3371  /**
3372   * Retrieves the bytes that make up the file referenced by the given URL.
3373   *
3374   * @param  urlString         The string representation of the URL to retrieve.
3375   * @param  relativeBasePath  The base path that will be prepended to relative
3376   *                           paths in order to obtain an absolute path.
3377   * @param  firstLineNumber   The line number for the start of the record.
3378   *
3379   * @return  The bytes contained in the specified file, or an empty array if
3380   *          the specified file is empty.
3381   *
3382   * @throws  LDIFException  If the provided URL is malformed or references a
3383   *                         nonexistent file.
3384   *
3385   * @throws  IOException  If a problem is encountered while attempting to read
3386   *                       from the target file.
3387   */
3388  @NotNull()
3389  private static byte[] retrieveURLBytes(@NotNull final String urlString,
3390                                         @NotNull final String relativeBasePath,
3391                                         final long firstLineNumber)
3392          throws LDIFException, IOException
3393  {
3394    int pos;
3395    final String path;
3396    final String lowerURLString = StaticUtils.toLowerCase(urlString);
3397    if (lowerURLString.startsWith("file:/"))
3398    {
3399      pos = 6;
3400      while ((pos < urlString.length()) && (urlString.charAt(pos) == '/'))
3401      {
3402        pos++;
3403      }
3404
3405      path = urlString.substring(pos-1);
3406    }
3407    else if (lowerURLString.startsWith("file:"))
3408    {
3409      // A file: URL that doesn't include a slash will be interpreted as a
3410      // relative path.
3411      path = relativeBasePath + urlString.substring(5);
3412    }
3413    else
3414    {
3415      throw new LDIFException(ERR_READ_URL_INVALID_SCHEME.get(urlString),
3416           firstLineNumber, true);
3417    }
3418
3419    final File f = new File(path);
3420    if (! f.exists())
3421    {
3422      throw new LDIFException(
3423           ERR_READ_URL_NO_SUCH_FILE.get(urlString, f.getAbsolutePath()),
3424           firstLineNumber, true);
3425    }
3426
3427    // In order to conserve memory, we'll only allow values to be read from
3428    // files no larger than 10 megabytes.
3429    final long fileSize = f.length();
3430    if (fileSize > (10 * 1024 * 1024))
3431    {
3432      throw new LDIFException(
3433           ERR_READ_URL_FILE_TOO_LARGE.get(urlString, f.getAbsolutePath(),
3434                (10*1024*1024)),
3435           firstLineNumber, true);
3436    }
3437
3438    int fileBytesRemaining = (int) fileSize;
3439    final byte[] fileData = new byte[(int) fileSize];
3440    final FileInputStream fis = new FileInputStream(f);
3441    try
3442    {
3443      int fileBytesRead = 0;
3444      while (fileBytesRead < fileSize)
3445      {
3446        final int bytesRead =
3447             fis.read(fileData, fileBytesRead, fileBytesRemaining);
3448        if (bytesRead < 0)
3449        {
3450          // We hit the end of the file before we expected to.  This shouldn't
3451          // happen unless the file size changed since we first looked at it,
3452          // which we won't allow.
3453          throw new LDIFException(
3454               ERR_READ_URL_FILE_SIZE_CHANGED.get(urlString,
3455                    f.getAbsolutePath()),
3456               firstLineNumber, true);
3457        }
3458
3459        fileBytesRead      += bytesRead;
3460        fileBytesRemaining -= bytesRead;
3461      }
3462
3463      if (fis.read() != -1)
3464      {
3465        // There is still more data to read.  This shouldn't happen unless the
3466        // file size changed since we first looked at it, which we won't allow.
3467        throw new LDIFException(
3468             ERR_READ_URL_FILE_SIZE_CHANGED.get(urlString, f.getAbsolutePath()),
3469             firstLineNumber, true);
3470      }
3471    }
3472    finally
3473    {
3474      fis.close();
3475    }
3476
3477    return fileData;
3478  }
3479
3480
3481
3482  /**
3483   * Parses the data available through the provided iterator into an array of
3484   * modifications suitable for use in a modify change record.
3485   *
3486   * @param  dn                     The DN of the entry being parsed.
3487   * @param  trailingSpaceBehavior  The behavior that should be exhibited when
3488   *                                encountering attribute values which are not
3489   *                                base64-encoded but contain trailing spaces.
3490   * @param  ldifLines              The lines that comprise the LDIF
3491   *                                representation of the full record being
3492   *                                parsed.
3493   * @param  iterator               The iterator to use to access the
3494   *                                modification data.
3495   * @param  firstLineNumber        The line number for the start of the record.
3496   * @param  schema                 The schema to use in processing.
3497   *
3498   * @return  An array containing the modifications that were read.
3499   *
3500   * @throws  LDIFException  If the provided LDIF data cannot be decoded as a
3501   *                         set of modifications.
3502   */
3503  @NotNull()
3504  private static Modification[] parseModifications(@NotNull final String dn,
3505               @NotNull final TrailingSpaceBehavior trailingSpaceBehavior,
3506               @NotNull final ArrayList<StringBuilder> ldifLines,
3507               @NotNull final Iterator<StringBuilder> iterator,
3508               final long firstLineNumber, @Nullable final Schema schema)
3509          throws LDIFException
3510  {
3511    final ArrayList<Modification> modList = new ArrayList<>(ldifLines.size());
3512
3513    while (iterator.hasNext())
3514    {
3515      // The first line must start with "add:", "delete:", "replace:", or
3516      // "increment:" followed by an attribute name.
3517      StringBuilder line = iterator.next();
3518      handleTrailingSpaces(line, dn, firstLineNumber, trailingSpaceBehavior);
3519      int colonPos = line.indexOf(":");
3520      if (colonPos < 0)
3521      {
3522        throw new LDIFException(ERR_READ_MOD_CR_NO_MODTYPE.get(firstLineNumber),
3523                                firstLineNumber, true, ldifLines, null);
3524      }
3525
3526      final ModificationType modType;
3527      final String modTypeStr =
3528           StaticUtils.toLowerCase(line.substring(0, colonPos));
3529      if (modTypeStr.equals("add"))
3530      {
3531        modType = ModificationType.ADD;
3532      }
3533      else if (modTypeStr.equals("delete"))
3534      {
3535        modType = ModificationType.DELETE;
3536      }
3537      else if (modTypeStr.equals("replace"))
3538      {
3539        modType = ModificationType.REPLACE;
3540      }
3541      else if (modTypeStr.equals("increment"))
3542      {
3543        modType = ModificationType.INCREMENT;
3544      }
3545      else
3546      {
3547        throw new LDIFException(ERR_READ_MOD_CR_INVALID_MODTYPE.get(modTypeStr,
3548                                     firstLineNumber),
3549                                firstLineNumber, true, ldifLines, null);
3550      }
3551
3552      String attributeName;
3553      int length = line.length();
3554      if (length == (colonPos+1))
3555      {
3556        // The colon was the last character on the line.  This is not
3557        // acceptable.
3558        throw new LDIFException(ERR_READ_MOD_CR_MODTYPE_NO_ATTR.get(
3559                                     firstLineNumber),
3560                                firstLineNumber, true, ldifLines, null);
3561      }
3562      else if (line.charAt(colonPos+1) == ':')
3563      {
3564        // Skip over any spaces leading up to the value, and then the rest of
3565        // the string is the base64-encoded attribute name.
3566        int pos = colonPos+2;
3567        while ((pos < length) && (line.charAt(pos) == ' '))
3568        {
3569          pos++;
3570        }
3571
3572        try
3573        {
3574          final byte[] dnBytes = Base64.decode(line.substring(pos));
3575          attributeName = StaticUtils.toUTF8String(dnBytes);
3576        }
3577        catch (final ParseException pe)
3578        {
3579          Debug.debugException(pe);
3580          throw new LDIFException(
3581               ERR_READ_MOD_CR_MODTYPE_CANNOT_BASE64_DECODE_ATTR.get(
3582                    firstLineNumber, pe.getMessage()),
3583               firstLineNumber, true, ldifLines, pe);
3584        }
3585        catch (final Exception e)
3586        {
3587          Debug.debugException(e);
3588          throw new LDIFException(
3589               ERR_READ_MOD_CR_MODTYPE_CANNOT_BASE64_DECODE_ATTR.get(
3590                    firstLineNumber, e),
3591               firstLineNumber, true, ldifLines, e);
3592        }
3593      }
3594      else
3595      {
3596        // Skip over any spaces leading up to the value, and then the rest of
3597        // the string is the attribute name.
3598        int pos = colonPos+1;
3599        while ((pos < length) && (line.charAt(pos) == ' '))
3600        {
3601          pos++;
3602        }
3603
3604        attributeName = line.substring(pos);
3605      }
3606
3607      if (attributeName.isEmpty())
3608      {
3609        throw new LDIFException(ERR_READ_MOD_CR_MODTYPE_NO_ATTR.get(
3610                                     firstLineNumber),
3611                                firstLineNumber, true, ldifLines, null);
3612      }
3613
3614
3615      // The next zero or more lines may be the set of attribute values.  Keep
3616      // reading until we reach the end of the iterator or until we find a line
3617      // with just a "-".
3618      final ArrayList<ASN1OctetString> valueList =
3619           new ArrayList<>(ldifLines.size());
3620      while (iterator.hasNext())
3621      {
3622        line = iterator.next();
3623        handleTrailingSpaces(line, dn, firstLineNumber, trailingSpaceBehavior);
3624        if (line.toString().equals("-"))
3625        {
3626          break;
3627        }
3628
3629        colonPos = line.indexOf(":");
3630        if (colonPos < 0)
3631        {
3632          throw new LDIFException(ERR_READ_NO_ATTR_COLON.get(firstLineNumber),
3633                                  firstLineNumber, true, ldifLines, null);
3634        }
3635        else if (! line.substring(0, colonPos).equalsIgnoreCase(attributeName))
3636        {
3637          // There are a couple of cases in which this might be acceptable:
3638          // - If the two names are logically equivalent, but have an alternate
3639          //   name (or OID) for the target attribute type, or if there are
3640          //   attribute options and the options are just in a different order.
3641          // - If this is the first value for the target attribute and the
3642          //   alternate name includes a "binary" option that the original
3643          //   attribute name did not have.  In this case, all subsequent values
3644          //   will also be required to have the binary option.
3645          final String alternateName = line.substring(0, colonPos);
3646
3647
3648          // Check to see if the base names are equivalent.
3649          boolean baseNameEquivalent = false;
3650          final String expectedBaseName = Attribute.getBaseName(attributeName);
3651          final String alternateBaseName = Attribute.getBaseName(alternateName);
3652          if (alternateBaseName.equalsIgnoreCase(expectedBaseName))
3653          {
3654            baseNameEquivalent = true;
3655          }
3656          else
3657          {
3658            if (schema != null)
3659            {
3660              final AttributeTypeDefinition expectedAT =
3661                   schema.getAttributeType(expectedBaseName);
3662              final AttributeTypeDefinition alternateAT =
3663                   schema.getAttributeType(alternateBaseName);
3664              if ((expectedAT != null) && (alternateAT != null) &&
3665                  expectedAT.equals(alternateAT))
3666              {
3667                baseNameEquivalent = true;
3668              }
3669            }
3670          }
3671
3672
3673          // Check to see if the attribute options are equivalent.
3674          final Set<String> expectedOptions =
3675               Attribute.getOptions(attributeName);
3676          final Set<String> lowerExpectedOptions = new HashSet<>(
3677               StaticUtils.computeMapCapacity(expectedOptions.size()));
3678          for (final String s : expectedOptions)
3679          {
3680            lowerExpectedOptions.add(StaticUtils.toLowerCase(s));
3681          }
3682
3683          final Set<String> alternateOptions =
3684               Attribute.getOptions(alternateName);
3685          final Set<String> lowerAlternateOptions = new HashSet<>(
3686               StaticUtils.computeMapCapacity(alternateOptions.size()));
3687          for (final String s : alternateOptions)
3688          {
3689            lowerAlternateOptions.add(StaticUtils.toLowerCase(s));
3690          }
3691
3692          final boolean optionsEquivalent =
3693               lowerAlternateOptions.equals(lowerExpectedOptions);
3694
3695
3696          if (baseNameEquivalent && optionsEquivalent)
3697          {
3698            // This is fine.  The two attribute descriptions are logically
3699            // equivalent.  We'll continue using the attribute description that
3700            // was provided first.
3701          }
3702          else if (valueList.isEmpty() && baseNameEquivalent &&
3703                   lowerAlternateOptions.remove("binary") &&
3704                   lowerAlternateOptions.equals(lowerExpectedOptions))
3705          {
3706            // This means that the provided value is the first value for the
3707            // attribute, and that the only significant difference is that the
3708            // provided attribute description included an unexpected "binary"
3709            // option.  We'll accept this, but will require any additional
3710            // values for this modification to also include the binary option,
3711            // and we'll use the binary option in the attribute that is
3712            // eventually created.
3713            attributeName = alternateName;
3714          }
3715          else
3716          {
3717            // This means that either the base names are different or the sets
3718            // of options are incompatible.  This is not acceptable.
3719            throw new LDIFException(ERR_READ_MOD_CR_ATTR_MISMATCH.get(
3720                                         firstLineNumber,
3721                                         line.substring(0, colonPos),
3722                                         attributeName),
3723                                    firstLineNumber, true, ldifLines, null);
3724          }
3725        }
3726
3727        length = line.length();
3728        final ASN1OctetString value;
3729        if (length == (colonPos+1))
3730        {
3731          // The colon was the last character on the line.  This is fine.
3732          value = new ASN1OctetString();
3733        }
3734        else if (line.charAt(colonPos+1) == ':')
3735        {
3736          // Skip over any spaces leading up to the value, and then the rest of
3737          // the string is the base64-encoded value.  This is unusual and
3738          // unnecessary, but is nevertheless acceptable.
3739          int pos = colonPos+2;
3740          while ((pos < length) && (line.charAt(pos) == ' '))
3741          {
3742            pos++;
3743          }
3744
3745          try
3746          {
3747            value = new ASN1OctetString(Base64.decode(line.substring(pos)));
3748          }
3749          catch (final ParseException pe)
3750          {
3751            Debug.debugException(pe);
3752            throw new LDIFException(ERR_READ_CANNOT_BASE64_DECODE_ATTR.get(
3753                 attributeName, firstLineNumber, pe.getMessage()),
3754                 firstLineNumber, true, ldifLines, pe);
3755          }
3756          catch (final Exception e)
3757          {
3758            Debug.debugException(e);
3759            throw new LDIFException(ERR_READ_CANNOT_BASE64_DECODE_ATTR.get(
3760                                         firstLineNumber, e),
3761                                    firstLineNumber, true, ldifLines, e);
3762          }
3763        }
3764        else
3765        {
3766          // Skip over any spaces leading up to the value, and then the rest of
3767          // the string is the value.
3768          int pos = colonPos+1;
3769          while ((pos < length) && (line.charAt(pos) == ' '))
3770          {
3771            pos++;
3772          }
3773
3774          value = new ASN1OctetString(line.substring(pos));
3775        }
3776
3777        valueList.add(value);
3778      }
3779
3780      final ASN1OctetString[] values = new ASN1OctetString[valueList.size()];
3781      valueList.toArray(values);
3782
3783      // If it's an add modification type, then there must be at least one
3784      // value.
3785      if ((modType.intValue() == ModificationType.ADD.intValue()) &&
3786          (values.length == 0))
3787      {
3788        throw new LDIFException(ERR_READ_MOD_CR_NO_ADD_VALUES.get(attributeName,
3789                                     firstLineNumber),
3790                                firstLineNumber, true, ldifLines, null);
3791      }
3792
3793      // If it's an increment modification type, then there must be exactly one
3794      // value.
3795      if ((modType.intValue() == ModificationType.INCREMENT.intValue()) &&
3796          (values.length != 1))
3797      {
3798        throw new LDIFException(ERR_READ_MOD_CR_INVALID_INCR_VALUE_COUNT.get(
3799                                     firstLineNumber, attributeName),
3800                                firstLineNumber, true, ldifLines, null);
3801      }
3802
3803      modList.add(new Modification(modType, attributeName, values));
3804    }
3805
3806    final Modification[] mods = new Modification[modList.size()];
3807    modList.toArray(mods);
3808    return mods;
3809  }
3810
3811
3812
3813  /**
3814   * Parses the data available through the provided iterator as the body of a
3815   * modify DN change record (i.e., the newrdn, deleteoldrdn, and optional
3816   * newsuperior lines).
3817   *
3818   * @param  ldifLines              The lines that comprise the LDIF
3819   *                                representation of the full record being
3820   *                                parsed.
3821   * @param  iterator               The iterator to use to access the modify DN
3822   *                                data.
3823   * @param  dn                     The current DN of the entry.
3824   * @param  controls               The set of controls to include in the change
3825   *                                record.
3826   * @param  trailingSpaceBehavior  The behavior that should be exhibited when
3827   *                                encountering attribute values which are not
3828   *                                base64-encoded but contain trailing spaces.
3829   * @param  firstLineNumber        The line number for the start of the record.
3830   *
3831   * @return  The decoded modify DN change record.
3832   *
3833   * @throws  LDIFException  If the provided LDIF data cannot be decoded as a
3834   *                         modify DN change record.
3835   */
3836  @NotNull()
3837  private static LDIFModifyDNChangeRecord parseModifyDNChangeRecord(
3838               @NotNull final ArrayList<StringBuilder> ldifLines,
3839               @NotNull final Iterator<StringBuilder> iterator,
3840               @NotNull final String dn,
3841               @Nullable final List<Control> controls,
3842               @NotNull final TrailingSpaceBehavior trailingSpaceBehavior,
3843               final long firstLineNumber)
3844          throws LDIFException
3845  {
3846    // The next line must be the new RDN, and it must start with "newrdn:".
3847    StringBuilder line = iterator.next();
3848    handleTrailingSpaces(line, dn, firstLineNumber, trailingSpaceBehavior);
3849    int colonPos = line.indexOf(":");
3850    if ((colonPos < 0) ||
3851        (! line.substring(0, colonPos).equalsIgnoreCase("newrdn")))
3852    {
3853      throw new LDIFException(ERR_READ_MODDN_CR_NO_NEWRDN_COLON.get(
3854                                   firstLineNumber),
3855                              firstLineNumber, true, ldifLines, null);
3856    }
3857
3858    final String newRDN;
3859    int length = line.length();
3860    if (length == (colonPos+1))
3861    {
3862      // The colon was the last character on the line.  This is not acceptable.
3863      throw new LDIFException(ERR_READ_MODDN_CR_NO_NEWRDN_VALUE.get(
3864                                   firstLineNumber),
3865                              firstLineNumber, true, ldifLines, null);
3866    }
3867    else if (line.charAt(colonPos+1) == ':')
3868    {
3869      // Skip over any spaces leading up to the value, and then the rest of the
3870      // string is the base64-encoded new RDN.
3871      int pos = colonPos+2;
3872      while ((pos < length) && (line.charAt(pos) == ' '))
3873      {
3874        pos++;
3875      }
3876
3877      try
3878      {
3879        final byte[] dnBytes = Base64.decode(line.substring(pos));
3880        newRDN = StaticUtils.toUTF8String(dnBytes);
3881      }
3882      catch (final ParseException pe)
3883      {
3884        Debug.debugException(pe);
3885        throw new LDIFException(
3886             ERR_READ_MODDN_CR_CANNOT_BASE64_DECODE_NEWRDN.get(firstLineNumber,
3887                                                               pe.getMessage()),
3888             firstLineNumber, true, ldifLines, pe);
3889      }
3890      catch (final Exception e)
3891      {
3892        Debug.debugException(e);
3893        throw new LDIFException(
3894             ERR_READ_MODDN_CR_CANNOT_BASE64_DECODE_NEWRDN.get(firstLineNumber,
3895                                                               e),
3896             firstLineNumber, true, ldifLines, e);
3897      }
3898    }
3899    else
3900    {
3901      // Skip over any spaces leading up to the value, and then the rest of the
3902      // string is the new RDN.
3903      int pos = colonPos+1;
3904      while ((pos < length) && (line.charAt(pos) == ' '))
3905      {
3906        pos++;
3907      }
3908
3909      newRDN = line.substring(pos);
3910    }
3911
3912    if (newRDN.isEmpty())
3913    {
3914      throw new LDIFException(ERR_READ_MODDN_CR_NO_NEWRDN_VALUE.get(
3915                                   firstLineNumber),
3916                              firstLineNumber, true, ldifLines, null);
3917    }
3918
3919
3920    // The next line must be the deleteOldRDN flag, and it must start with
3921    // 'deleteoldrdn:'.
3922    if (! iterator.hasNext())
3923    {
3924      throw new LDIFException(ERR_READ_MODDN_CR_NO_DELOLDRDN_COLON.get(
3925                                   firstLineNumber),
3926                              firstLineNumber, true, ldifLines, null);
3927    }
3928
3929    line = iterator.next();
3930    handleTrailingSpaces(line, dn, firstLineNumber, trailingSpaceBehavior);
3931    colonPos = line.indexOf(":");
3932    if ((colonPos < 0) ||
3933        (! line.substring(0, colonPos).equalsIgnoreCase("deleteoldrdn")))
3934    {
3935      throw new LDIFException(ERR_READ_MODDN_CR_NO_DELOLDRDN_COLON.get(
3936                                   firstLineNumber),
3937                              firstLineNumber, true, ldifLines, null);
3938    }
3939
3940    final String deleteOldRDNStr;
3941    length = line.length();
3942    if (length == (colonPos+1))
3943    {
3944      // The colon was the last character on the line.  This is not acceptable.
3945      throw new LDIFException(ERR_READ_MODDN_CR_NO_DELOLDRDN_VALUE.get(
3946                                   firstLineNumber),
3947                              firstLineNumber, true, ldifLines, null);
3948    }
3949    else if (line.charAt(colonPos+1) == ':')
3950    {
3951      // Skip over any spaces leading up to the value, and then the rest of the
3952      // string is the base64-encoded value.  This is unusual and
3953      // unnecessary, but is nevertheless acceptable.
3954      int pos = colonPos+2;
3955      while ((pos < length) && (line.charAt(pos) == ' '))
3956      {
3957        pos++;
3958      }
3959
3960      try
3961      {
3962        final byte[] changeTypeBytes = Base64.decode(line.substring(pos));
3963        deleteOldRDNStr = StaticUtils.toUTF8String(changeTypeBytes);
3964      }
3965      catch (final ParseException pe)
3966      {
3967        Debug.debugException(pe);
3968        throw new LDIFException(
3969             ERR_READ_MODDN_CR_CANNOT_BASE64_DECODE_DELOLDRDN.get(
3970                  firstLineNumber, pe.getMessage()),
3971             firstLineNumber, true, ldifLines, pe);
3972      }
3973      catch (final Exception e)
3974      {
3975        Debug.debugException(e);
3976        throw new LDIFException(
3977             ERR_READ_MODDN_CR_CANNOT_BASE64_DECODE_DELOLDRDN.get(
3978                  firstLineNumber, e),
3979             firstLineNumber, true, ldifLines, e);
3980      }
3981    }
3982    else
3983    {
3984      // Skip over any spaces leading up to the value, and then the rest of the
3985      // string is the value.
3986      int pos = colonPos+1;
3987      while ((pos < length) && (line.charAt(pos) == ' '))
3988      {
3989        pos++;
3990      }
3991
3992      deleteOldRDNStr = line.substring(pos);
3993    }
3994
3995    final boolean deleteOldRDN;
3996    if (deleteOldRDNStr.equals("0"))
3997    {
3998      deleteOldRDN = false;
3999    }
4000    else if (deleteOldRDNStr.equals("1"))
4001    {
4002      deleteOldRDN = true;
4003    }
4004    else if (deleteOldRDNStr.equalsIgnoreCase("false") ||
4005             deleteOldRDNStr.equalsIgnoreCase("no"))
4006    {
4007      // This is technically illegal, but we'll allow it.
4008      deleteOldRDN = false;
4009    }
4010    else if (deleteOldRDNStr.equalsIgnoreCase("true") ||
4011             deleteOldRDNStr.equalsIgnoreCase("yes"))
4012    {
4013      // This is also technically illegal, but we'll allow it.
4014      deleteOldRDN = false;
4015    }
4016    else
4017    {
4018      throw new LDIFException(ERR_READ_MODDN_CR_INVALID_DELOLDRDN.get(
4019                                   deleteOldRDNStr, firstLineNumber),
4020                              firstLineNumber, true, ldifLines, null);
4021    }
4022
4023
4024    // If there is another line, then it must be the new superior DN and it must
4025    // start with "newsuperior:".  If this is absent, then it's fine.
4026    final String newSuperiorDN;
4027    if (iterator.hasNext())
4028    {
4029      line = iterator.next();
4030      handleTrailingSpaces(line, dn, firstLineNumber, trailingSpaceBehavior);
4031      colonPos = line.indexOf(":");
4032      if ((colonPos < 0) ||
4033          (! line.substring(0, colonPos).equalsIgnoreCase("newsuperior")))
4034      {
4035        throw new LDIFException(ERR_READ_MODDN_CR_NO_NEWSUPERIOR_COLON.get(
4036                                     firstLineNumber),
4037                                firstLineNumber, true, ldifLines, null);
4038      }
4039
4040      length = line.length();
4041      if (length == (colonPos+1))
4042      {
4043        // The colon was the last character on the line.  This is fine.
4044        newSuperiorDN = "";
4045      }
4046      else if (line.charAt(colonPos+1) == ':')
4047      {
4048        // Skip over any spaces leading up to the value, and then the rest of
4049        // the string is the base64-encoded new superior DN.
4050        int pos = colonPos+2;
4051        while ((pos < length) && (line.charAt(pos) == ' '))
4052        {
4053          pos++;
4054        }
4055
4056        try
4057        {
4058          final byte[] dnBytes = Base64.decode(line.substring(pos));
4059          newSuperiorDN = StaticUtils.toUTF8String(dnBytes);
4060        }
4061        catch (final ParseException pe)
4062        {
4063          Debug.debugException(pe);
4064          throw new LDIFException(
4065               ERR_READ_MODDN_CR_CANNOT_BASE64_DECODE_NEWSUPERIOR.get(
4066                    firstLineNumber, pe.getMessage()),
4067               firstLineNumber, true, ldifLines, pe);
4068        }
4069        catch (final Exception e)
4070        {
4071          Debug.debugException(e);
4072          throw new LDIFException(
4073               ERR_READ_MODDN_CR_CANNOT_BASE64_DECODE_NEWSUPERIOR.get(
4074                    firstLineNumber, e),
4075               firstLineNumber, true, ldifLines, e);
4076        }
4077      }
4078      else
4079      {
4080        // Skip over any spaces leading up to the value, and then the rest of
4081        // the string is the new superior DN.
4082        int pos = colonPos+1;
4083        while ((pos < length) && (line.charAt(pos) == ' '))
4084        {
4085          pos++;
4086        }
4087
4088        newSuperiorDN = line.substring(pos);
4089      }
4090    }
4091    else
4092    {
4093      newSuperiorDN = null;
4094    }
4095
4096
4097    // There must not be any more lines.
4098    if (iterator.hasNext())
4099    {
4100      throw new LDIFException(ERR_READ_CR_EXTRA_MODDN_DATA.get(firstLineNumber),
4101                              firstLineNumber, true, ldifLines, null);
4102    }
4103
4104    return new LDIFModifyDNChangeRecord(dn, newRDN, deleteOldRDN,
4105         newSuperiorDN, controls);
4106  }
4107
4108
4109
4110  /**
4111   * Examines the line contained in the provided buffer to determine whether it
4112   * may contain one or more illegal trailing spaces.  If it does, then those
4113   * spaces will either be stripped out or an exception will be thrown to
4114   * indicate that they are illegal.
4115   *
4116   * @param  buffer                 The buffer to be examined.
4117   * @param  dn                     The DN of the LDIF record being parsed.  It
4118   *                                may be {@code null} if the DN is not yet
4119   *                                known (e.g., because the provided line is
4120   *                                expected to contain that DN).
4121   * @param  firstLineNumber        The approximate line number in the LDIF
4122   *                                source on which the LDIF record begins.
4123   * @param  trailingSpaceBehavior  The behavior that should be exhibited when
4124   *                                encountering attribute values which are not
4125   *                                base64-encoded but contain trailing spaces.
4126   *
4127   * @throws  LDIFException  If the line contained in the provided buffer ends
4128   *                         with one or more illegal trailing spaces and
4129   *                         {@code stripTrailingSpaces} was provided with a
4130   *                         value of {@code false}.
4131   */
4132  private static void handleTrailingSpaces(@NotNull final StringBuilder buffer,
4133               @Nullable final String dn, final long firstLineNumber,
4134               @NotNull final TrailingSpaceBehavior trailingSpaceBehavior)
4135          throws LDIFException
4136  {
4137    int pos = buffer.length() - 1;
4138    boolean trailingFound = false;
4139    while ((pos >= 0) && (buffer.charAt(pos) == ' '))
4140    {
4141      trailingFound = true;
4142      pos--;
4143    }
4144
4145    if (trailingFound && (buffer.charAt(pos) != ':'))
4146    {
4147      switch (trailingSpaceBehavior)
4148      {
4149        case STRIP:
4150          buffer.setLength(pos+1);
4151          break;
4152
4153        case REJECT:
4154          if (dn == null)
4155          {
4156            throw new LDIFException(
4157                 ERR_READ_ILLEGAL_TRAILING_SPACE_WITHOUT_DN.get(firstLineNumber,
4158                      buffer.toString()),
4159                 firstLineNumber, true);
4160          }
4161          else
4162          {
4163            throw new LDIFException(
4164                 ERR_READ_ILLEGAL_TRAILING_SPACE_WITH_DN.get(dn,
4165                      firstLineNumber, buffer.toString()),
4166                 firstLineNumber, true);
4167          }
4168
4169        case RETAIN:
4170        default:
4171          // No action will be taken.
4172          break;
4173      }
4174    }
4175  }
4176
4177
4178
4179  /**
4180   * This represents an unparsed LDIFRecord.  It stores the line number of the
4181   * first line of the record and each line of the record.
4182   */
4183  private static final class UnparsedLDIFRecord
4184  {
4185    @Nullable private final ArrayList<StringBuilder> lineList;
4186    private final long firstLineNumber;
4187    @Nullable private final Exception failureCause;
4188    private final boolean isEOF;
4189    @NotNull private final DuplicateValueBehavior duplicateValueBehavior;
4190    @Nullable private final Schema schema;
4191    @NotNull private final TrailingSpaceBehavior trailingSpaceBehavior;
4192
4193
4194
4195    /**
4196     * Creates a new instance of this record.
4197     *
4198     * @param  lineList                The lines that comprise the LDIF record.
4199     * @param  duplicateValueBehavior  The behavior to exhibit if the entry
4200     *                                 contains duplicate attribute values.
4201     * @param  trailingSpaceBehavior   Specifies the behavior to exhibit when
4202     *                                 encountering trailing spaces in
4203     *                                 non-base64-encoded attribute values.
4204     * @param  schema                  The schema to use when parsing, if
4205     *                                 applicable.
4206     * @param  firstLineNumber         The first line number of the LDIF record.
4207     */
4208    private UnparsedLDIFRecord(@NotNull final ArrayList<StringBuilder> lineList,
4209                 @NotNull final DuplicateValueBehavior duplicateValueBehavior,
4210                 @NotNull final TrailingSpaceBehavior trailingSpaceBehavior,
4211                 @Nullable final Schema schema, final long firstLineNumber)
4212    {
4213      this.lineList               = lineList;
4214      this.firstLineNumber        = firstLineNumber;
4215      this.duplicateValueBehavior = duplicateValueBehavior;
4216      this.trailingSpaceBehavior  = trailingSpaceBehavior;
4217      this.schema                 = schema;
4218
4219      failureCause = null;
4220      isEOF =
4221           (firstLineNumber < 0) || ((lineList != null) && lineList.isEmpty());
4222    }
4223
4224
4225
4226    /**
4227     * Creates a new instance of this record.
4228     *
4229     * @param failureCause  The Exception thrown when reading from the input.
4230     */
4231    private UnparsedLDIFRecord(@NotNull final Exception failureCause)
4232    {
4233      this.failureCause = failureCause;
4234
4235      lineList               = null;
4236      firstLineNumber        = 0;
4237      duplicateValueBehavior = DuplicateValueBehavior.REJECT;
4238      trailingSpaceBehavior  = TrailingSpaceBehavior.REJECT;
4239      schema                 = null;
4240      isEOF                  = false;
4241    }
4242
4243
4244
4245    /**
4246     * Return the lines that comprise the LDIF record.
4247     *
4248     * @return  The lines that comprise the LDIF record, or {@code null} if this
4249     *          is a failure record.
4250     */
4251    @Nullable()
4252    private ArrayList<StringBuilder> getLineList()
4253    {
4254      return lineList;
4255    }
4256
4257
4258
4259    /**
4260     * Retrieves the behavior to exhibit when encountering duplicate attribute
4261     * values.
4262     *
4263     * @return  The behavior to exhibit when encountering duplicate attribute
4264     *          values.
4265     */
4266    @NotNull()
4267    private DuplicateValueBehavior getDuplicateValueBehavior()
4268    {
4269      return duplicateValueBehavior;
4270    }
4271
4272
4273
4274    /**
4275     * Retrieves the behavior that should be exhibited when encountering
4276     * attribute values which are not base64-encoded but contain trailing
4277     * spaces.  The LDIF specification strongly recommends that any value which
4278     * legitimately contains trailing spaces be base64-encoded, but the LDAP SDK
4279     * LDIF parser may be configured to automatically strip these spaces, to
4280     * preserve them, or to reject any entry or change record containing them.
4281     *
4282     * @return  The behavior that should be exhibited when encountering
4283     *          attribute values which are not base64-encoded but contain
4284     *          trailing spaces.
4285     */
4286    @NotNull()
4287    private TrailingSpaceBehavior getTrailingSpaceBehavior()
4288    {
4289      return trailingSpaceBehavior;
4290    }
4291
4292
4293
4294    /**
4295     * Retrieves the schema that should be used when parsing the record, if
4296     * applicable.
4297     *
4298     * @return  The schema that should be used when parsing the record, or
4299     *          {@code null} if none should be used.
4300     */
4301    @Nullable()
4302    private Schema getSchema()
4303    {
4304      return schema;
4305    }
4306
4307
4308
4309    /**
4310     * Return the first line number of the LDIF record.
4311     *
4312     * @return  The first line number of the LDIF record.
4313     */
4314    private long getFirstLineNumber()
4315    {
4316      return firstLineNumber;
4317    }
4318
4319
4320
4321    /**
4322     * Return {@code true} iff the end of the input was reached.
4323     *
4324     * @return  {@code true} iff the end of the input was reached.
4325     */
4326    private boolean isEOF()
4327    {
4328      return isEOF;
4329    }
4330
4331
4332
4333    /**
4334     * Returns the reason that reading the record lines failed.  This normally
4335     * is only non-null if something bad happened to the input stream (like
4336     * a disk read error).
4337     *
4338     * @return  The reason that reading the record lines failed.
4339     */
4340    @Nullable()
4341    private Exception getFailureCause()
4342    {
4343      return failureCause;
4344    }
4345  }
4346
4347
4348  /**
4349   * When processing in asynchronous mode, this thread is responsible for
4350   * reading the raw unparsed records from the input and submitting them for
4351   * processing.
4352   */
4353  private final class LineReaderThread
4354       extends Thread
4355  {
4356    /**
4357     * Creates a new instance o fthis thread.
4358     */
4359    private LineReaderThread()
4360    {
4361      super("Asynchronous LDIF line reader");
4362      setDaemon(true);
4363    }
4364
4365
4366
4367    /**
4368     * Reads raw, unparsed records from the input and submits them for
4369     * processing until the input is finished or closed.
4370     */
4371    @Override()
4372    public void run()
4373    {
4374      try
4375      {
4376        boolean stopProcessing = false;
4377        while (!stopProcessing)
4378        {
4379          UnparsedLDIFRecord unparsedRecord;
4380          try
4381          {
4382            unparsedRecord = readUnparsedRecord();
4383          }
4384          catch (final IOException e)
4385          {
4386            Debug.debugException(e);
4387            unparsedRecord = new UnparsedLDIFRecord(e);
4388            stopProcessing = true;
4389          }
4390          catch (final Exception e)
4391          {
4392            Debug.debugException(e);
4393            unparsedRecord = new UnparsedLDIFRecord(e);
4394          }
4395
4396          try
4397          {
4398            asyncParser.submit(unparsedRecord);
4399          }
4400          catch (final InterruptedException e)
4401          {
4402            Debug.debugException(e);
4403            // If this thread is interrupted, then someone wants us to stop
4404            // processing, so that's what we'll do.
4405            Thread.currentThread().interrupt();
4406            stopProcessing = true;
4407          }
4408
4409          if ((unparsedRecord == null) || unparsedRecord.isEOF())
4410          {
4411            stopProcessing = true;
4412          }
4413        }
4414      }
4415      finally
4416      {
4417        try
4418        {
4419          asyncParser.shutdown();
4420        }
4421        catch (final InterruptedException e)
4422        {
4423          Debug.debugException(e);
4424          Thread.currentThread().interrupt();
4425        }
4426        finally
4427        {
4428          asyncParsingComplete.set(true);
4429        }
4430      }
4431    }
4432  }
4433
4434
4435
4436  /**
4437   * Used to parse Records asynchronously.
4438   */
4439  private final class RecordParser implements Processor<UnparsedLDIFRecord,
4440                                                        LDIFRecord>
4441  {
4442    /**
4443     * {@inheritDoc}
4444     */
4445    @Override()
4446    public LDIFRecord process(@NotNull final UnparsedLDIFRecord input)
4447           throws LDIFException
4448    {
4449      LDIFRecord record = decodeRecord(input, relativeBasePath, schema);
4450
4451      if ((record instanceof Entry) && (entryTranslator != null))
4452      {
4453        record = entryTranslator.translate((Entry) record,
4454             input.getFirstLineNumber());
4455
4456        if (record == null)
4457        {
4458          record = SKIP_ENTRY;
4459        }
4460      }
4461      if ((record instanceof LDIFChangeRecord) &&
4462          (changeRecordTranslator != null))
4463      {
4464        record = changeRecordTranslator.translate((LDIFChangeRecord) record,
4465             input.getFirstLineNumber());
4466
4467        if (record == null)
4468        {
4469          record = SKIP_ENTRY;
4470        }
4471      }
4472      return record;
4473    }
4474  }
4475}