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