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