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