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