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