001 /* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at 010 * trunk/opends/resource/legal-notices/OpenDS.LICENSE 011 * or https://OpenDS.dev.java.net/OpenDS.LICENSE. 012 * See the License for the specific language governing permissions 013 * and limitations under the License. 014 * 015 * When distributing Covered Code, include this CDDL HEADER in each 016 * file and include the License file at 017 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, 018 * add the following below this CDDL HEADER, with the fields enclosed 019 * by brackets "[]" replaced with your own identifying information: 020 * Portions Copyright [yyyy] [name of copyright owner] 021 * 022 * CDDL HEADER END 023 * 024 * 025 * Copyright 2006-2008 Sun Microsystems, Inc. 026 */ 027 package org.opends.server.util; 028 import org.opends.messages.Message; 029 import org.opends.messages.MessageBuilder; 030 031 032 import static org.opends.server.loggers.debug.DebugLogger.*; 033 import org.opends.server.loggers.debug.DebugTracer; 034 import static org.opends.server.loggers.ErrorLogger.logError; 035 import static org.opends.messages.UtilityMessages.*; 036 import static org.opends.server.util.StaticUtils.toLowerCase; 037 import static org.opends.server.util.Validator.*; 038 039 import java.io.BufferedReader; 040 import java.io.BufferedWriter; 041 import java.io.ByteArrayOutputStream; 042 import java.io.IOException; 043 import java.io.InputStream; 044 import java.net.URL; 045 import java.util.ArrayList; 046 import java.util.HashMap; 047 import java.util.LinkedHashSet; 048 import java.util.LinkedList; 049 import java.util.List; 050 051 import org.opends.server.core.DirectoryServer; 052 import org.opends.server.core.PluginConfigManager; 053 import org.opends.server.protocols.asn1.ASN1OctetString; 054 import org.opends.server.protocols.ldap.LDAPAttribute; 055 import org.opends.server.protocols.ldap.LDAPModification; 056 import org.opends.server.types.AcceptRejectWarn; 057 import org.opends.server.types.Attribute; 058 import org.opends.server.types.AttributeType; 059 import org.opends.server.types.AttributeValue; 060 import org.opends.server.types.DirectoryException; 061 import org.opends.server.types.DN; 062 import org.opends.server.types.DebugLogLevel; 063 import org.opends.server.types.Entry; 064 065 066 import org.opends.server.types.LDIFImportConfig; 067 import org.opends.server.types.ModificationType; 068 import org.opends.server.types.ObjectClass; 069 import org.opends.server.types.RawModification; 070 import org.opends.server.types.RDN; 071 import org.opends.server.api.plugin.PluginResult; 072 073 074 /** 075 * This class provides the ability to read information from an LDIF file. It 076 * provides support for both standard entries and change entries (as would be 077 * used with a tool like ldapmodify). 078 */ 079 @org.opends.server.types.PublicAPI( 080 stability=org.opends.server.types.StabilityLevel.UNCOMMITTED, 081 mayInstantiate=true, 082 mayExtend=false, 083 mayInvoke=true) 084 public final class LDIFReader 085 { 086 /** 087 * The tracer object for the debug logger. 088 */ 089 private static final DebugTracer TRACER = getTracer(); 090 091 // The reader that will be used to read the data. 092 private BufferedReader reader; 093 094 // The buffer to use to read data from a URL. 095 private byte[] buffer; 096 097 // The import configuration that specifies what should be imported. 098 private LDIFImportConfig importConfig; 099 100 // The lines that comprise the body of the last entry read. 101 private LinkedList<StringBuilder> lastEntryBodyLines; 102 103 // The lines that comprise the header (DN and any comments) for the last entry 104 // read. 105 private LinkedList<StringBuilder> lastEntryHeaderLines; 106 107 // The number of entries that have been ignored by this LDIF reader because 108 // they didn't match the criteria. 109 private long entriesIgnored; 110 111 // The number of entries that have been read by this LDIF reader, including 112 // those that were ignored because they didn't match the criteria, and 113 // including those that were rejected because they were invalid in some way. 114 private long entriesRead; 115 116 // The number of entries that have been rejected by this LDIF reader. 117 private long entriesRejected; 118 119 // The line number on which the last entry started. 120 private long lastEntryLineNumber; 121 122 // The line number of the last line read from the LDIF file, starting with 1. 123 private long lineNumber; 124 125 // The plugin config manager that will be used if we are to invoke plugins 126 // on the entries as they are read. 127 private PluginConfigManager pluginConfigManager; 128 129 130 131 /** 132 * Creates a new LDIF reader that will read information from the specified 133 * file. 134 * 135 * @param importConfig The import configuration for this LDIF reader. It 136 * must not be <CODE>null</CODE>. 137 * 138 * @throws IOException If a problem occurs while opening the LDIF file for 139 * reading. 140 */ 141 public LDIFReader(LDIFImportConfig importConfig) 142 throws IOException 143 { 144 ensureNotNull(importConfig); 145 this.importConfig = importConfig; 146 147 reader = importConfig.getReader(); 148 buffer = new byte[4096]; 149 entriesRead = 0; 150 entriesIgnored = 0; 151 entriesRejected = 0; 152 lineNumber = 0; 153 lastEntryLineNumber = -1; 154 lastEntryBodyLines = new LinkedList<StringBuilder>(); 155 lastEntryHeaderLines = new LinkedList<StringBuilder>(); 156 pluginConfigManager = DirectoryServer.getPluginConfigManager(); 157 } 158 159 160 161 /** 162 * Reads the next entry from the LDIF source. 163 * 164 * @return The next entry read from the LDIF source, or <CODE>null</CODE> if 165 * the end of the LDIF data is reached. 166 * 167 * @throws IOException If an I/O problem occurs while reading from the file. 168 * 169 * @throws LDIFException If the information read cannot be parsed as an LDIF 170 * entry. 171 */ 172 public Entry readEntry() 173 throws IOException, LDIFException 174 { 175 return readEntry(importConfig.validateSchema()); 176 } 177 178 179 180 /** 181 * Reads the next entry from the LDIF source. 182 * 183 * @param checkSchema Indicates whether this reader should perform schema 184 * checking on the entry before returning it to the 185 * caller. Note that some basic schema checking (like 186 * refusing multiple values for a single-valued 187 * attribute) may always be performed. 188 * 189 * 190 * @return The next entry read from the LDIF source, or <CODE>null</CODE> if 191 * the end of the LDIF data is reached. 192 * 193 * @throws IOException If an I/O problem occurs while reading from the file. 194 * 195 * @throws LDIFException If the information read cannot be parsed as an LDIF 196 * entry. 197 */ 198 public Entry readEntry(boolean checkSchema) 199 throws IOException, LDIFException 200 { 201 while (true) 202 { 203 // Read the set of lines that make up the next entry. 204 LinkedList<StringBuilder> lines = readEntryLines(); 205 if (lines == null) 206 { 207 return null; 208 } 209 lastEntryBodyLines = lines; 210 lastEntryHeaderLines = new LinkedList<StringBuilder>(); 211 212 213 // Read the DN of the entry and see if it is one that should be included 214 // in the import. 215 DN entryDN = readDN(lines); 216 if (entryDN == null) 217 { 218 // This should only happen if the LDIF starts with the "version:" line 219 // and has a blank line immediately after that. In that case, simply 220 // read and return the next entry. 221 continue; 222 } 223 else if (!importConfig.includeEntry(entryDN)) 224 { 225 if (debugEnabled()) 226 { 227 TRACER.debugInfo("Skipping entry %s because the DN is not one that " + 228 "should be included based on the include and exclude branches.", 229 entryDN); 230 } 231 entriesRead++; 232 Message message = ERR_LDIF_SKIP.get(String.valueOf(entryDN)); 233 logToSkipWriter(lines, message); 234 entriesIgnored++; 235 continue; 236 } 237 else 238 { 239 entriesRead++; 240 } 241 242 // Read the set of attributes from the entry. 243 HashMap<ObjectClass,String> objectClasses = 244 new HashMap<ObjectClass,String>(); 245 HashMap<AttributeType,List<Attribute>> userAttributes = 246 new HashMap<AttributeType,List<Attribute>>(); 247 HashMap<AttributeType,List<Attribute>> operationalAttributes = 248 new HashMap<AttributeType,List<Attribute>>(); 249 try 250 { 251 for (StringBuilder line : lines) 252 { 253 readAttribute(lines, line, entryDN, objectClasses, userAttributes, 254 operationalAttributes, checkSchema); 255 } 256 } 257 catch (LDIFException e) 258 { 259 entriesRejected++; 260 throw e; 261 } 262 263 // Create the entry and see if it is one that should be included in the 264 // import. 265 Entry entry = new Entry(entryDN, objectClasses, userAttributes, 266 operationalAttributes); 267 TRACER.debugProtocolElement(DebugLogLevel.VERBOSE, entry); 268 269 try 270 { 271 if (! importConfig.includeEntry(entry)) 272 { 273 if (debugEnabled()) 274 { 275 TRACER.debugInfo("Skipping entry %s because the DN is not one " + 276 "that should be included based on the include and exclude " + 277 "filters.", entryDN); 278 } 279 Message message = ERR_LDIF_SKIP.get(String.valueOf(entryDN)); 280 logToSkipWriter(lines, message); 281 entriesIgnored++; 282 continue; 283 } 284 } 285 catch (Exception e) 286 { 287 if (debugEnabled()) 288 { 289 TRACER.debugCaught(DebugLogLevel.ERROR, e); 290 } 291 292 Message message = ERR_LDIF_COULD_NOT_EVALUATE_FILTERS_FOR_IMPORT. 293 get(String.valueOf(entry.getDN()), lastEntryLineNumber, 294 String.valueOf(e)); 295 throw new LDIFException(message, lastEntryLineNumber, true, e); 296 } 297 298 299 // If we should invoke import plugins, then do so. 300 if (importConfig.invokeImportPlugins()) 301 { 302 PluginResult.ImportLDIF pluginResult = 303 pluginConfigManager.invokeLDIFImportPlugins(importConfig, entry); 304 if (! pluginResult.continueProcessing()) 305 { 306 Message m; 307 Message rejectMessage = pluginResult.getErrorMessage(); 308 if (rejectMessage == null) 309 { 310 m = ERR_LDIF_REJECTED_BY_PLUGIN_NOMESSAGE.get( 311 String.valueOf(entryDN)); 312 } 313 else 314 { 315 m = ERR_LDIF_REJECTED_BY_PLUGIN.get(String.valueOf(entryDN), 316 rejectMessage); 317 } 318 319 logToRejectWriter(lines, m); 320 entriesRejected++; 321 continue; 322 } 323 } 324 325 326 // Make sure that the entry is valid as per the server schema if it is 327 // appropriate to do so. 328 if (checkSchema) 329 { 330 MessageBuilder invalidReason = new MessageBuilder(); 331 if (! entry.conformsToSchema(null, false, true, false, invalidReason)) 332 { 333 Message message = ERR_LDIF_SCHEMA_VIOLATION.get( 334 String.valueOf(entryDN), 335 lastEntryLineNumber, 336 invalidReason.toString()); 337 logToRejectWriter(lines, message); 338 entriesRejected++; 339 throw new LDIFException(message, lastEntryLineNumber, true); 340 } 341 } 342 343 344 // The entry should be included in the import, so return it. 345 return entry; 346 } 347 } 348 349 /** 350 * Reads the next change record from the LDIF source. 351 * 352 * @param defaultAdd Indicates whether the change type should default to 353 * "add" if none is explicitly provided. 354 * 355 * @return The next change record from the LDIF source, or <CODE>null</CODE> 356 * if the end of the LDIF data is reached. 357 * 358 * @throws IOException If an I/O problem occurs while reading from the file. 359 * 360 * @throws LDIFException If the information read cannot be parsed as an LDIF 361 * entry. 362 */ 363 public ChangeRecordEntry readChangeRecord(boolean defaultAdd) 364 throws IOException, LDIFException 365 { 366 while (true) 367 { 368 // Read the set of lines that make up the next entry. 369 LinkedList<StringBuilder> lines = readEntryLines(); 370 if (lines == null) 371 { 372 return null; 373 } 374 375 376 // Read the DN of the entry and see if it is one that should be included 377 // in the import. 378 DN entryDN = readDN(lines); 379 if (entryDN == null) 380 { 381 // This should only happen if the LDIF starts with the "version:" line 382 // and has a blank line immediately after that. In that case, simply 383 // read and return the next entry. 384 continue; 385 } 386 387 String changeType = readChangeType(lines); 388 389 ChangeRecordEntry entry = null; 390 391 if(changeType != null) 392 { 393 if(changeType.equals("add")) 394 { 395 entry = parseAddChangeRecordEntry(entryDN, lines); 396 } else if (changeType.equals("delete")) 397 { 398 entry = parseDeleteChangeRecordEntry(entryDN, lines); 399 } else if (changeType.equals("modify")) 400 { 401 entry = parseModifyChangeRecordEntry(entryDN, lines); 402 } else if (changeType.equals("modrdn")) 403 { 404 entry = parseModifyDNChangeRecordEntry(entryDN, lines); 405 } else if (changeType.equals("moddn")) 406 { 407 entry = parseModifyDNChangeRecordEntry(entryDN, lines); 408 } else 409 { 410 Message message = ERR_LDIF_INVALID_CHANGETYPE_ATTRIBUTE.get( 411 changeType, "add, delete, modify, moddn, modrdn"); 412 throw new LDIFException(message, lastEntryLineNumber, false); 413 } 414 } else 415 { 416 // default to "add"? 417 if(defaultAdd) 418 { 419 entry = parseAddChangeRecordEntry(entryDN, lines); 420 } else 421 { 422 Message message = ERR_LDIF_INVALID_CHANGETYPE_ATTRIBUTE.get( 423 null, "add, delete, modify, moddn, modrdn"); 424 throw new LDIFException(message, lastEntryLineNumber, false); 425 } 426 } 427 428 return entry; 429 } 430 } 431 432 433 434 /** 435 * Reads a set of lines from the next entry in the LDIF source. 436 * 437 * @return A set of lines from the next entry in the LDIF source. 438 * 439 * @throws IOException If a problem occurs while reading from the LDIF 440 * source. 441 * 442 * @throws LDIFException If the information read is not valid LDIF. 443 */ 444 private LinkedList<StringBuilder> readEntryLines() 445 throws IOException, LDIFException 446 { 447 // Read the entry lines into a buffer. 448 LinkedList<StringBuilder> lines = new LinkedList<StringBuilder>(); 449 int lastLine = -1; 450 451 while (true) 452 { 453 String line = reader.readLine(); 454 lineNumber++; 455 456 if (line == null) 457 { 458 // This must mean that we have reached the end of the LDIF source. 459 // If the set of lines read so far is empty, then move onto the next 460 // file or return null. Otherwise, break out of this loop. 461 if (lines.isEmpty()) 462 { 463 reader = importConfig.nextReader(); 464 if (reader == null) 465 { 466 return null; 467 } 468 else 469 { 470 return readEntryLines(); 471 } 472 } 473 else 474 { 475 break; 476 } 477 } 478 else if (line.length() == 0) 479 { 480 // This is a blank line. If the set of lines read so far is empty, 481 // then just skip over it. Otherwise, break out of this loop. 482 if (lines.isEmpty()) 483 { 484 continue; 485 } 486 else 487 { 488 break; 489 } 490 } 491 else if (line.charAt(0) == '#') 492 { 493 // This is a comment. Ignore it. 494 continue; 495 } 496 else if ((line.charAt(0) == ' ') || (line.charAt(0) == '\t')) 497 { 498 // This is a continuation of the previous line. If there is no 499 // previous line, then that's a problem. Note that while RFC 2849 500 // technically only allows a space in this position, both OpenLDAP and 501 // the Sun Java System Directory Server allow a tab as well, so we will 502 // too for compatibility reasons. See issue #852 for details. 503 if (lastLine >= 0) 504 { 505 lines.get(lastLine).append(line.substring(1)); 506 } 507 else 508 { 509 Message message = 510 ERR_LDIF_INVALID_LEADING_SPACE.get(lineNumber, line); 511 logToRejectWriter(lines, message); 512 throw new LDIFException(message, lineNumber, false); 513 } 514 } 515 else 516 { 517 // This is a new line. 518 if (lines.isEmpty()) 519 { 520 lastEntryLineNumber = lineNumber; 521 } 522 lines.add(new StringBuilder(line)); 523 lastLine++; 524 } 525 } 526 527 528 return lines; 529 } 530 531 532 533 /** 534 * Reads the DN of the entry from the provided list of lines. The DN must be 535 * the first line in the list, unless the first line starts with "version", 536 * in which case the DN should be the second line. 537 * 538 * @param lines The set of lines from which the DN should be read. 539 * 540 * @return The decoded entry DN. 541 * 542 * @throws LDIFException If DN is not the first element in the list (or the 543 * second after the LDIF version), or if a problem 544 * occurs while trying to parse it. 545 */ 546 private DN readDN(LinkedList<StringBuilder> lines) 547 throws LDIFException 548 { 549 if (lines.isEmpty()) 550 { 551 // This is possible if the contents of the first "entry" were just 552 // the version identifier. If that is the case, then return null and 553 // use that as a signal to the caller to go ahead and read the next entry. 554 return null; 555 } 556 557 StringBuilder line = lines.remove(); 558 lastEntryHeaderLines.add(line); 559 int colonPos = line.indexOf(":"); 560 if (colonPos <= 0) 561 { 562 Message message = 563 ERR_LDIF_NO_ATTR_NAME.get(lastEntryLineNumber, line.toString()); 564 565 logToRejectWriter(lines, message); 566 567 throw new LDIFException(message, lastEntryLineNumber, true); 568 } 569 570 String attrName = toLowerCase(line.substring(0, colonPos)); 571 if (attrName.equals("version")) 572 { 573 // This is the version line, and we can skip it. 574 return readDN(lines); 575 } 576 else if (! attrName.equals("dn")) 577 { 578 Message message = 579 ERR_LDIF_NO_DN.get(lastEntryLineNumber, line.toString()); 580 581 logToRejectWriter(lines, message); 582 583 throw new LDIFException(message, lastEntryLineNumber, true); 584 } 585 586 587 // Look at the character immediately after the colon. If there is none, 588 // then assume the null DN. If it is another colon, then the DN must be 589 // base64-encoded. Otherwise, it may be one or more spaces. 590 int length = line.length(); 591 if (colonPos == (length-1)) 592 { 593 return DN.nullDN(); 594 } 595 596 if (line.charAt(colonPos+1) == ':') 597 { 598 // The DN is base64-encoded. Find the first non-blank character and 599 // take the rest of the line, base64-decode it, and parse it as a DN. 600 int pos = colonPos+2; 601 while ((pos < length) && (line.charAt(pos) == ' ')) 602 { 603 pos++; 604 } 605 606 String encodedDNStr = line.substring(pos); 607 608 String dnStr; 609 try 610 { 611 dnStr = new String(Base64.decode(encodedDNStr), "UTF-8"); 612 } 613 catch (Exception e) 614 { 615 // The value did not have a valid base64-encoding. 616 if (debugEnabled()) 617 { 618 TRACER.debugCaught(DebugLogLevel.ERROR, e); 619 } 620 621 Message message = 622 ERR_LDIF_COULD_NOT_BASE64_DECODE_DN.get( 623 lastEntryLineNumber, line, 624 String.valueOf(e)); 625 626 logToRejectWriter(lines, message); 627 628 throw new LDIFException(message, lastEntryLineNumber, true, e); 629 } 630 631 try 632 { 633 return DN.decode(dnStr); 634 } 635 catch (DirectoryException de) 636 { 637 if (debugEnabled()) 638 { 639 TRACER.debugCaught(DebugLogLevel.ERROR, de); 640 } 641 642 Message message = ERR_LDIF_INVALID_DN.get( 643 lastEntryLineNumber, line.toString(), 644 de.getMessageObject()); 645 646 logToRejectWriter(lines, message); 647 648 throw new LDIFException(message, lastEntryLineNumber, true, de); 649 } 650 catch (Exception e) 651 { 652 if (debugEnabled()) 653 { 654 TRACER.debugCaught(DebugLogLevel.ERROR, e); 655 } 656 657 Message message = ERR_LDIF_INVALID_DN.get( 658 lastEntryLineNumber, line.toString(), 659 String.valueOf(e)); 660 661 logToRejectWriter(lines, message); 662 663 throw new LDIFException(message, lastEntryLineNumber, true, e); 664 } 665 } 666 else 667 { 668 // The rest of the value should be the DN. Skip over any spaces and 669 // attempt to decode the rest of the line as the DN. 670 int pos = colonPos+1; 671 while ((pos < length) && (line.charAt(pos) == ' ')) 672 { 673 pos++; 674 } 675 676 String dnString = line.substring(pos); 677 678 try 679 { 680 return DN.decode(dnString); 681 } 682 catch (DirectoryException de) 683 { 684 if (debugEnabled()) 685 { 686 TRACER.debugCaught(DebugLogLevel.ERROR, de); 687 } 688 689 Message message = ERR_LDIF_INVALID_DN.get( 690 lastEntryLineNumber, line.toString(), de.getMessageObject()); 691 692 logToRejectWriter(lines, message); 693 694 throw new LDIFException(message, lastEntryLineNumber, true, de); 695 } 696 catch (Exception e) 697 { 698 if (debugEnabled()) 699 { 700 TRACER.debugCaught(DebugLogLevel.ERROR, e); 701 } 702 703 Message message = ERR_LDIF_INVALID_DN.get( 704 lastEntryLineNumber, line.toString(), 705 String.valueOf(e)); 706 707 logToRejectWriter(lines, message); 708 709 throw new LDIFException(message, lastEntryLineNumber, true, e); 710 } 711 } 712 } 713 714 715 716 /** 717 * Reads the changetype of the entry from the provided list of lines. If 718 * there is no changetype attribute then an add is assumed. 719 * 720 * @param lines The set of lines from which the DN should be read. 721 * 722 * @return The decoded entry DN. 723 * 724 * @throws LDIFException If DN is not the first element in the list (or the 725 * second after the LDIF version), or if a problem 726 * occurs while trying to parse it. 727 */ 728 private String readChangeType(LinkedList<StringBuilder> lines) 729 throws LDIFException 730 { 731 if (lines.isEmpty()) 732 { 733 // Error. There must be other entries. 734 return null; 735 } 736 737 StringBuilder line = lines.get(0); 738 lastEntryHeaderLines.add(line); 739 int colonPos = line.indexOf(":"); 740 if (colonPos <= 0) 741 { 742 Message message = ERR_LDIF_NO_ATTR_NAME.get( 743 lastEntryLineNumber, line.toString()); 744 logToRejectWriter(lines, message); 745 throw new LDIFException(message, lastEntryLineNumber, true); 746 } 747 748 String attrName = toLowerCase(line.substring(0, colonPos)); 749 if (! attrName.equals("changetype")) 750 { 751 // No changetype attribute - return null 752 return null; 753 } else 754 { 755 // Remove the line 756 lines.remove(); 757 } 758 759 760 // Look at the character immediately after the colon. If there is none, 761 // then no value was specified. Throw an exception 762 int length = line.length(); 763 if (colonPos == (length-1)) 764 { 765 Message message = ERR_LDIF_INVALID_CHANGETYPE_ATTRIBUTE.get( 766 null, "add, delete, modify, moddn, modrdn"); 767 throw new LDIFException(message, lastEntryLineNumber, false ); 768 } 769 770 if (line.charAt(colonPos+1) == ':') 771 { 772 // The change type is base64-encoded. Find the first non-blank 773 // character and 774 // take the rest of the line, and base64-decode it. 775 int pos = colonPos+2; 776 while ((pos < length) && (line.charAt(pos) == ' ')) 777 { 778 pos++; 779 } 780 781 String encodedChangeTypeStr = line.substring(pos); 782 783 String changeTypeStr; 784 try 785 { 786 changeTypeStr = new String(Base64.decode(encodedChangeTypeStr), 787 "UTF-8"); 788 } 789 catch (Exception e) 790 { 791 // The value did not have a valid base64-encoding. 792 if (debugEnabled()) 793 { 794 TRACER.debugCaught(DebugLogLevel.ERROR, e); 795 } 796 797 Message message = ERR_LDIF_COULD_NOT_BASE64_DECODE_DN.get( 798 lastEntryLineNumber, line, 799 String.valueOf(e)); 800 logToRejectWriter(lines, message); 801 throw new LDIFException(message, lastEntryLineNumber, true, e); 802 } 803 804 return changeTypeStr; 805 } 806 else 807 { 808 // The rest of the value should be the changetype. 809 // Skip over any spaces and 810 // attempt to decode the rest of the line as the changetype string. 811 int pos = colonPos+1; 812 while ((pos < length) && (line.charAt(pos) == ' ')) 813 { 814 pos++; 815 } 816 817 String changeTypeString = line.substring(pos); 818 819 return changeTypeString; 820 } 821 } 822 823 824 /** 825 * Decodes the provided line as an LDIF attribute and adds it to the 826 * appropriate hash. 827 * 828 * @param lines The full set of lines that comprise the 829 * entry (used for writing reject information). 830 * @param line The line to decode. 831 * @param entryDN The DN of the entry being decoded. 832 * @param objectClasses The set of objectclasses decoded so far for 833 * the current entry. 834 * @param userAttributes The set of user attributes decoded so far 835 * for the current entry. 836 * @param operationalAttributes The set of operational attributes decoded so 837 * far for the current entry. 838 * @param checkSchema Indicates whether to perform schema 839 * validation for the attribute. 840 * 841 * @throws LDIFException If a problem occurs while trying to decode the 842 * attribute contained in the provided entry. 843 */ 844 private void readAttribute(LinkedList<StringBuilder> lines, 845 StringBuilder line, DN entryDN, 846 HashMap<ObjectClass,String> objectClasses, 847 HashMap<AttributeType,List<Attribute>> userAttributes, 848 HashMap<AttributeType,List<Attribute>> operationalAttributes, 849 boolean checkSchema) 850 throws LDIFException 851 { 852 // Parse the attribute type description. 853 int colonPos = parseColonPosition(lines, line); 854 String attrDescr = line.substring(0, colonPos); 855 Attribute attribute = parseAttrDescription(attrDescr); 856 String attrName = attribute.getName(); 857 String lowerName = toLowerCase(attrName); 858 LinkedHashSet<String> options = attribute.getOptions(); 859 860 // Now parse the attribute value. 861 ASN1OctetString value = parseSingleValue(lines, line, entryDN, 862 colonPos, attrName); 863 864 // See if this is an objectclass or an attribute. Then get the 865 // corresponding definition and add the value to the appropriate hash. 866 if (lowerName.equals("objectclass")) 867 { 868 if (! importConfig.includeObjectClasses()) 869 { 870 if (debugEnabled()) 871 { 872 TRACER.debugVerbose("Skipping objectclass %s for entry %s due to " + 873 "the import configuration.", value, entryDN); 874 } 875 return; 876 } 877 878 String ocName = value.stringValue(); 879 String lowerOCName = toLowerCase(ocName); 880 881 ObjectClass objectClass = DirectoryServer.getObjectClass(lowerOCName); 882 if (objectClass == null) 883 { 884 objectClass = DirectoryServer.getDefaultObjectClass(ocName); 885 } 886 887 if (objectClasses.containsKey(objectClass)) 888 { 889 logError(WARN_LDIF_DUPLICATE_OBJECTCLASS.get( 890 String.valueOf(entryDN), lastEntryLineNumber, ocName)); 891 } 892 else 893 { 894 objectClasses.put(objectClass, ocName); 895 } 896 } 897 else 898 { 899 AttributeType attrType = DirectoryServer.getAttributeType(lowerName); 900 if (attrType == null) 901 { 902 attrType = DirectoryServer.getDefaultAttributeType(attrName); 903 } 904 905 906 if (! importConfig.includeAttribute(attrType)) 907 { 908 if (debugEnabled()) 909 { 910 TRACER.debugVerbose("Skipping attribute %s for entry %s due to the " + 911 "import configuration.", attrName, entryDN); 912 } 913 return; 914 } 915 916 if (checkSchema && 917 (DirectoryServer.getSyntaxEnforcementPolicy() != 918 AcceptRejectWarn.ACCEPT)) 919 { 920 MessageBuilder invalidReason = new MessageBuilder(); 921 if (! attrType.getSyntax().valueIsAcceptable(value, invalidReason)) 922 { 923 Message message = WARN_LDIF_VALUE_VIOLATES_SYNTAX.get( 924 String.valueOf(entryDN), 925 lastEntryLineNumber, value.stringValue(), 926 attrName, invalidReason.toString()); 927 if (DirectoryServer.getSyntaxEnforcementPolicy() == 928 AcceptRejectWarn.WARN) 929 { 930 logError(message); 931 } 932 else 933 { 934 logToRejectWriter(lines, message); 935 throw new LDIFException(message, lastEntryLineNumber, 936 true); 937 } 938 } 939 } 940 941 AttributeValue attributeValue = new AttributeValue(attrType, value); 942 List<Attribute> attrList; 943 if (attrType.isOperational()) 944 { 945 attrList = operationalAttributes.get(attrType); 946 if (attrList == null) 947 { 948 LinkedHashSet<AttributeValue> valueSet = 949 new LinkedHashSet<AttributeValue>(); 950 valueSet.add(attributeValue); 951 952 attrList = new ArrayList<Attribute>(); 953 attrList.add(new Attribute(attrType, attrName, options, valueSet)); 954 operationalAttributes.put(attrType, attrList); 955 return; 956 } 957 } 958 else 959 { 960 attrList = userAttributes.get(attrType); 961 if (attrList == null) 962 { 963 LinkedHashSet<AttributeValue> valueSet = 964 new LinkedHashSet<AttributeValue>(); 965 valueSet.add(attributeValue); 966 967 attrList = new ArrayList<Attribute>(); 968 attrList.add(new Attribute(attrType, attrName, options, valueSet)); 969 userAttributes.put(attrType, attrList); 970 return; 971 } 972 } 973 974 975 // Check to see if any of the attributes in the list have the same set of 976 // options. If so, then try to add a value to that attribute. 977 for (Attribute a : attrList) 978 { 979 if (a.optionsEqual(options)) 980 { 981 LinkedHashSet<AttributeValue> valueSet = a.getValues(); 982 if (valueSet.contains(attributeValue)) 983 { 984 if (! checkSchema) 985 { 986 // If we're not doing schema checking, then it is possible that 987 // the attribute type should use case-sensitive matching and the 988 // values differ in capitalization. Only reject the proposed 989 // value if we find another value that is exactly the same as the 990 // one that was provided. 991 for (AttributeValue v : valueSet) 992 { 993 if (v.getValue().equals(attributeValue.getValue())) 994 { 995 Message message = WARN_LDIF_DUPLICATE_ATTR.get( 996 String.valueOf(entryDN), 997 lastEntryLineNumber, attrName, 998 value.stringValue()); 999 logToRejectWriter(lines, message); 1000 throw new LDIFException(message, lastEntryLineNumber, 1001 true); 1002 } 1003 } 1004 } 1005 else 1006 { 1007 Message message = WARN_LDIF_DUPLICATE_ATTR.get( 1008 String.valueOf(entryDN), 1009 lastEntryLineNumber, attrName, 1010 value.stringValue()); 1011 logToRejectWriter(lines, message); 1012 throw new LDIFException(message, lastEntryLineNumber, 1013 true); 1014 } 1015 } 1016 1017 if (attrType.isSingleValue() && (! valueSet.isEmpty()) && checkSchema) 1018 { 1019 Message message = ERR_LDIF_MULTIPLE_VALUES_FOR_SINGLE_VALUED_ATTR 1020 .get(String.valueOf(entryDN), 1021 lastEntryLineNumber, attrName); 1022 logToRejectWriter(lines, message); 1023 throw new LDIFException(message, lastEntryLineNumber, true); 1024 } 1025 1026 valueSet.add(attributeValue); 1027 return; 1028 } 1029 } 1030 1031 1032 // No set of matching options was found, so create a new one and add it to 1033 // the list. 1034 LinkedHashSet<AttributeValue> valueSet = 1035 new LinkedHashSet<AttributeValue>(); 1036 valueSet.add(attributeValue); 1037 attrList.add(new Attribute(attrType, attrName, options, valueSet)); 1038 return; 1039 } 1040 } 1041 1042 1043 1044 /** 1045 * Decodes the provided line as an LDIF attribute and returns the 1046 * Attribute (name and values) for the specified attribute name. 1047 * 1048 * @param lines The full set of lines that comprise the 1049 * entry (used for writing reject information). 1050 * @param line The line to decode. 1051 * @param entryDN The DN of the entry being decoded. 1052 * @param attributeName The name and options of the attribute to 1053 * return the values for. 1054 * 1055 * @return The attribute in octet string form. 1056 * @throws LDIFException If a problem occurs while trying to decode 1057 * the attribute contained in the provided 1058 * entry or if the parsed attribute name does 1059 * not match the specified attribute name. 1060 */ 1061 private Attribute readSingleValueAttribute( 1062 LinkedList<StringBuilder> lines, StringBuilder line, DN entryDN, 1063 String attributeName) throws LDIFException 1064 { 1065 // Parse the attribute type description. 1066 int colonPos = parseColonPosition(lines, line); 1067 String attrDescr = line.substring(0, colonPos); 1068 Attribute attribute = parseAttrDescription(attrDescr); 1069 String attrName = attribute.getName(); 1070 1071 if (attributeName != null) 1072 { 1073 Attribute expectedAttr = parseAttrDescription(attributeName); 1074 1075 if (!attribute.equals(expectedAttr)) 1076 { 1077 Message message = ERR_LDIF_INVALID_CHANGERECORD_ATTRIBUTE.get( 1078 attrDescr, attributeName); 1079 throw new LDIFException(message, lastEntryLineNumber, false); 1080 } 1081 } 1082 1083 // Now parse the attribute value. 1084 ASN1OctetString value = parseSingleValue(lines, line, entryDN, 1085 colonPos, attrName); 1086 1087 AttributeType attrType = attribute.getAttributeType(); 1088 AttributeValue attributeValue = new AttributeValue(attrType, value); 1089 attribute.getValues().add(attributeValue); 1090 1091 return attribute; 1092 } 1093 1094 1095 /** 1096 * Retrieves the starting line number for the last entry read from the LDIF 1097 * source. 1098 * 1099 * @return The starting line number for the last entry read from the LDIF 1100 * source. 1101 */ 1102 public long getLastEntryLineNumber() 1103 { 1104 return lastEntryLineNumber; 1105 } 1106 1107 1108 1109 /** 1110 * Rejects the last entry read from the LDIF. This method is intended for use 1111 * by components that perform their own validation of entries (e.g., backends 1112 * during import processing) in which the entry appeared valid to the LDIF 1113 * reader but some other problem was encountered. 1114 * 1115 * @param message A human-readable message providing the reason that the 1116 * last entry read was not acceptable. 1117 */ 1118 public void rejectLastEntry(Message message) 1119 { 1120 entriesRejected++; 1121 1122 BufferedWriter rejectWriter = importConfig.getRejectWriter(); 1123 if (rejectWriter != null) 1124 { 1125 try 1126 { 1127 if ((message != null) && (message.length() > 0)) 1128 { 1129 rejectWriter.write("# "); 1130 rejectWriter.write(message.toString()); 1131 rejectWriter.newLine(); 1132 } 1133 1134 for (StringBuilder sb : lastEntryHeaderLines) 1135 { 1136 rejectWriter.write(sb.toString()); 1137 rejectWriter.newLine(); 1138 } 1139 1140 for (StringBuilder sb : lastEntryBodyLines) 1141 { 1142 rejectWriter.write(sb.toString()); 1143 rejectWriter.newLine(); 1144 } 1145 1146 rejectWriter.newLine(); 1147 } 1148 catch (Exception e) 1149 { 1150 if (debugEnabled()) 1151 { 1152 TRACER.debugCaught(DebugLogLevel.ERROR, e); 1153 } 1154 } 1155 } 1156 } 1157 1158 1159 1160 /** 1161 * Closes this LDIF reader and the underlying file or input stream. 1162 */ 1163 public void close() 1164 { 1165 importConfig.close(); 1166 } 1167 1168 1169 1170 /** 1171 * Parse an AttributeDescription (an attribute type name and its options). 1172 * @param attrDescr The attribute description to be parsed. 1173 * @return A new attribute with no values, representing the attribute type 1174 * and its options. 1175 */ 1176 private static Attribute parseAttrDescription(String attrDescr) 1177 { 1178 String attrName; 1179 String lowerName; 1180 LinkedHashSet<String> options; 1181 int semicolonPos = attrDescr.indexOf(';'); 1182 if (semicolonPos > 0) 1183 { 1184 attrName = attrDescr.substring(0, semicolonPos); 1185 options = new LinkedHashSet<String>(); 1186 int nextPos = attrDescr.indexOf(';', semicolonPos+1); 1187 while (nextPos > 0) 1188 { 1189 String option = attrDescr.substring(semicolonPos+1, nextPos); 1190 if (option.length() > 0) 1191 { 1192 options.add(option); 1193 semicolonPos = nextPos; 1194 nextPos = attrDescr.indexOf(';', semicolonPos+1); 1195 } 1196 } 1197 1198 String option = attrDescr.substring(semicolonPos+1); 1199 if (option.length() > 0) 1200 { 1201 options.add(option); 1202 } 1203 } 1204 else 1205 { 1206 attrName = attrDescr; 1207 options = null; 1208 } 1209 1210 lowerName = toLowerCase(attrName); 1211 AttributeType attrType = DirectoryServer.getAttributeType(lowerName); 1212 if (attrType == null) 1213 { 1214 attrType = DirectoryServer.getDefaultAttributeType(attrName); 1215 } 1216 1217 return new Attribute(attrType, attrName, options, null); 1218 } 1219 1220 1221 1222 /** 1223 * Retrieves the total number of entries read so far by this LDIF reader, 1224 * including those that have been ignored or rejected. 1225 * 1226 * @return The total number of entries read so far by this LDIF reader. 1227 */ 1228 public long getEntriesRead() 1229 { 1230 return entriesRead; 1231 } 1232 1233 1234 1235 /** 1236 * Retrieves the total number of entries that have been ignored so far by this 1237 * LDIF reader because they did not match the import criteria. 1238 * 1239 * @return The total number of entries ignored so far by this LDIF reader. 1240 */ 1241 public long getEntriesIgnored() 1242 { 1243 return entriesIgnored; 1244 } 1245 1246 1247 1248 /** 1249 * Retrieves the total number of entries rejected so far by this LDIF reader. 1250 * This includes both entries that were rejected because of internal 1251 * validation failure (e.g., they didn't conform to the defined server 1252 * schema) or an external validation failure (e.g., the component using this 1253 * LDIF reader didn't accept the entry because it didn't have a parent). 1254 * 1255 * @return The total number of entries rejected so far by this LDIF reader. 1256 */ 1257 public long getEntriesRejected() 1258 { 1259 return entriesRejected; 1260 } 1261 1262 1263 1264 /** 1265 * Parse a modifyDN change record entry from LDIF. 1266 * 1267 * @param entryDN 1268 * The name of the entry being modified. 1269 * @param lines 1270 * The lines to parse. 1271 * @return Returns the parsed modifyDN change record entry. 1272 * @throws LDIFException 1273 * If there was an error when parsing the change record. 1274 */ 1275 private ChangeRecordEntry parseModifyDNChangeRecordEntry(DN entryDN, 1276 LinkedList<StringBuilder> lines) throws LDIFException { 1277 1278 DN newSuperiorDN = null; 1279 RDN newRDN = null; 1280 boolean deleteOldRDN = false; 1281 1282 if(lines.isEmpty()) 1283 { 1284 Message message = ERR_LDIF_NO_MOD_DN_ATTRIBUTES.get(); 1285 throw new LDIFException(message, lineNumber, true); 1286 } 1287 1288 StringBuilder line = lines.remove(); 1289 String rdnStr = getModifyDNAttributeValue(lines, line, entryDN, "newrdn"); 1290 1291 try 1292 { 1293 newRDN = RDN.decode(rdnStr); 1294 } catch (DirectoryException de) 1295 { 1296 if (debugEnabled()) 1297 { 1298 TRACER.debugCaught(DebugLogLevel.ERROR, de); 1299 } 1300 Message message = ERR_LDIF_INVALID_DN.get( 1301 lineNumber, line.toString(), de.getMessageObject()); 1302 throw new LDIFException(message, lineNumber, true); 1303 } catch (Exception e) 1304 { 1305 if (debugEnabled()) 1306 { 1307 TRACER.debugCaught(DebugLogLevel.ERROR, e); 1308 } 1309 Message message = 1310 ERR_LDIF_INVALID_DN.get(lineNumber, line.toString(), e.getMessage()); 1311 throw new LDIFException(message, lineNumber, true); 1312 } 1313 1314 if(lines.isEmpty()) 1315 { 1316 Message message = ERR_LDIF_NO_DELETE_OLDRDN_ATTRIBUTE.get(); 1317 throw new LDIFException(message, lineNumber, true); 1318 } 1319 lineNumber++; 1320 1321 line = lines.remove(); 1322 String delStr = getModifyDNAttributeValue(lines, line, 1323 entryDN, "deleteoldrdn"); 1324 1325 if(delStr.equalsIgnoreCase("false") || 1326 delStr.equalsIgnoreCase("no") || 1327 delStr.equalsIgnoreCase("0")) 1328 { 1329 deleteOldRDN = false; 1330 } else if(delStr.equalsIgnoreCase("true") || 1331 delStr.equalsIgnoreCase("yes") || 1332 delStr.equalsIgnoreCase("1")) 1333 { 1334 deleteOldRDN = true; 1335 } else 1336 { 1337 Message message = ERR_LDIF_INVALID_DELETE_OLDRDN_ATTRIBUTE.get(delStr); 1338 throw new LDIFException(message, lineNumber, true); 1339 } 1340 1341 if(!lines.isEmpty()) 1342 { 1343 lineNumber++; 1344 1345 line = lines.remove(); 1346 1347 String dnStr = getModifyDNAttributeValue(lines, line, 1348 entryDN, "newsuperior"); 1349 try 1350 { 1351 newSuperiorDN = DN.decode(dnStr); 1352 } catch (DirectoryException de) 1353 { 1354 if (debugEnabled()) 1355 { 1356 TRACER.debugCaught(DebugLogLevel.ERROR, de); 1357 } 1358 Message message = ERR_LDIF_INVALID_DN.get( 1359 lineNumber, line.toString(), de.getMessageObject()); 1360 throw new LDIFException(message, lineNumber, true); 1361 } catch (Exception e) 1362 { 1363 if (debugEnabled()) 1364 { 1365 TRACER.debugCaught(DebugLogLevel.ERROR, e); 1366 } 1367 Message message = ERR_LDIF_INVALID_DN.get( 1368 lineNumber, line.toString(), e.getMessage()); 1369 throw new LDIFException(message, lineNumber, true); 1370 } 1371 } 1372 1373 return new ModifyDNChangeRecordEntry(entryDN, newRDN, deleteOldRDN, 1374 newSuperiorDN); 1375 } 1376 1377 1378 1379 /** 1380 * Return the string value for the specified attribute name which only 1381 * has one value. 1382 * 1383 * @param lines 1384 * The set of lines for this change record entry. 1385 * @param line 1386 * The line currently being examined. 1387 * @param entryDN 1388 * The name of the entry being modified. 1389 * @param attributeName 1390 * The attribute name 1391 * @return the string value for the attribute name. 1392 * @throws LDIFException 1393 * If a problem occurs while attempting to determine the 1394 * attribute value. 1395 */ 1396 1397 private String getModifyDNAttributeValue(LinkedList<StringBuilder> lines, 1398 StringBuilder line, 1399 DN entryDN, 1400 String attributeName) throws LDIFException 1401 { 1402 Attribute attr = 1403 readSingleValueAttribute(lines, line, entryDN, attributeName); 1404 LinkedHashSet<AttributeValue> values = attr.getValues(); 1405 1406 // Get the attribute value 1407 Object[] vals = values.toArray(); 1408 return (((AttributeValue)vals[0]).getStringValue()); 1409 } 1410 1411 1412 1413 /** 1414 * Parse a modify change record entry from LDIF. 1415 * 1416 * @param entryDN 1417 * The name of the entry being modified. 1418 * @param lines 1419 * The lines to parse. 1420 * @return Returns the parsed modify change record entry. 1421 * @throws LDIFException 1422 * If there was an error when parsing the change record. 1423 */ 1424 private ChangeRecordEntry parseModifyChangeRecordEntry(DN entryDN, 1425 LinkedList<StringBuilder> lines) throws LDIFException { 1426 1427 List<RawModification> modifications = new ArrayList<RawModification>(); 1428 while(!lines.isEmpty()) 1429 { 1430 ModificationType modType = null; 1431 1432 StringBuilder line = lines.remove(); 1433 Attribute attr = 1434 readSingleValueAttribute(lines, line, entryDN, null); 1435 String name = attr.getName(); 1436 LinkedHashSet<AttributeValue> values = attr.getValues(); 1437 1438 // Get the attribute description 1439 String attrDescr = values.iterator().next().getStringValue(); 1440 1441 String lowerName = toLowerCase(name); 1442 if(lowerName.equals("add")) 1443 { 1444 modType = ModificationType.ADD; 1445 } else if(lowerName.equals("delete")) 1446 { 1447 modType = ModificationType.DELETE; 1448 } else if(lowerName.equals("replace")) 1449 { 1450 modType = ModificationType.REPLACE; 1451 } else if(lowerName.equals("increment")) 1452 { 1453 modType = ModificationType.INCREMENT; 1454 } else 1455 { 1456 // Invalid attribute name. 1457 Message message = ERR_LDIF_INVALID_MODIFY_ATTRIBUTE.get( 1458 name, "add, delete, replace, increment"); 1459 throw new LDIFException(message, lineNumber, true); 1460 } 1461 1462 // Now go through the rest of the attributes till the "-" line is 1463 // reached. 1464 Attribute modAttr = LDIFReader.parseAttrDescription(attrDescr); 1465 while (! lines.isEmpty()) 1466 { 1467 line = lines.remove(); 1468 if(line.toString().equals("-")) 1469 { 1470 break; 1471 } 1472 Attribute a = 1473 readSingleValueAttribute(lines, line, entryDN, attrDescr); 1474 modAttr.getValues().addAll(a.getValues()); 1475 } 1476 1477 LDAPAttribute ldapAttr = new LDAPAttribute(modAttr); 1478 LDAPModification mod = new LDAPModification(modType, ldapAttr); 1479 modifications.add(mod); 1480 } 1481 1482 return new ModifyChangeRecordEntry(entryDN, modifications); 1483 } 1484 1485 1486 1487 /** 1488 * Parse a delete change record entry from LDIF. 1489 * 1490 * @param entryDN 1491 * The name of the entry being deleted. 1492 * @param lines 1493 * The lines to parse. 1494 * @return Returns the parsed delete change record entry. 1495 * @throws LDIFException 1496 * If there was an error when parsing the change record. 1497 */ 1498 private ChangeRecordEntry parseDeleteChangeRecordEntry(DN entryDN, 1499 LinkedList<StringBuilder> lines) throws LDIFException { 1500 1501 if (!lines.isEmpty()) 1502 { 1503 Message message = ERR_LDIF_INVALID_DELETE_ATTRIBUTES.get(); 1504 throw new LDIFException(message, lineNumber, true); 1505 } 1506 1507 return new DeleteChangeRecordEntry(entryDN); 1508 } 1509 1510 1511 1512 /** 1513 * Parse an add change record entry from LDIF. 1514 * 1515 * @param entryDN 1516 * The name of the entry being added. 1517 * @param lines 1518 * The lines to parse. 1519 * @return Returns the parsed add change record entry. 1520 * @throws LDIFException 1521 * If there was an error when parsing the change record. 1522 */ 1523 private ChangeRecordEntry parseAddChangeRecordEntry(DN entryDN, 1524 LinkedList<StringBuilder> lines) throws LDIFException { 1525 1526 HashMap<ObjectClass,String> objectClasses = 1527 new HashMap<ObjectClass,String>(); 1528 HashMap<AttributeType,List<Attribute>> attributes = 1529 new HashMap<AttributeType, List<Attribute>>(); 1530 for(StringBuilder line : lines) 1531 { 1532 readAttribute(lines, line, entryDN, objectClasses, 1533 attributes, attributes, importConfig.validateSchema()); 1534 } 1535 1536 // Reconstruct the object class attribute. 1537 AttributeType ocType = DirectoryServer.getObjectClassAttributeType(); 1538 LinkedHashSet<AttributeValue> ocValues = 1539 new LinkedHashSet<AttributeValue>(objectClasses.size()); 1540 for (String value : objectClasses.values()) { 1541 AttributeValue av = new AttributeValue(ocType, value); 1542 ocValues.add(av); 1543 } 1544 Attribute ocAttr = new Attribute(ocType, "objectClass", ocValues); 1545 List<Attribute> ocAttrList = new ArrayList<Attribute>(1); 1546 ocAttrList.add(ocAttr); 1547 attributes.put(ocType, ocAttrList); 1548 1549 return new AddChangeRecordEntry(entryDN, attributes); 1550 } 1551 1552 1553 1554 /** 1555 * Parse colon position in an attribute description. 1556 * 1557 * @param lines 1558 * The current set of lines. 1559 * @param line 1560 * The current line. 1561 * @return The colon position. 1562 * @throws LDIFException 1563 * If the colon was badly placed or not found. 1564 */ 1565 private int parseColonPosition(LinkedList<StringBuilder> lines, 1566 StringBuilder line) throws LDIFException { 1567 1568 int colonPos = line.indexOf(":"); 1569 if (colonPos <= 0) 1570 { 1571 Message message = ERR_LDIF_NO_ATTR_NAME.get( 1572 lastEntryLineNumber, line.toString()); 1573 logToRejectWriter(lines, message); 1574 throw new LDIFException(message, lastEntryLineNumber, true); 1575 } 1576 return colonPos; 1577 } 1578 1579 1580 1581 /** 1582 * Parse a single attribute value from a line of LDIF. 1583 * 1584 * @param lines 1585 * The current set of lines. 1586 * @param line 1587 * The current line. 1588 * @param entryDN 1589 * The DN of the entry being parsed. 1590 * @param colonPos 1591 * The position of the separator colon in the line. 1592 * @param attrName 1593 * The name of the attribute being parsed. 1594 * @return The parsed attribute value. 1595 * @throws LDIFException 1596 * If an error occurred when parsing the attribute value. 1597 */ 1598 private ASN1OctetString parseSingleValue( 1599 LinkedList<StringBuilder> lines, 1600 StringBuilder line, 1601 DN entryDN, 1602 int colonPos, 1603 String attrName) throws LDIFException { 1604 1605 // Look at the character immediately after the colon. If there is 1606 // none, then assume an attribute with an empty value. If it is another 1607 // colon, then the value must be base64-encoded. If it is a less-than 1608 // sign, then assume that it is a URL. Otherwise, it is a regular value. 1609 int length = line.length(); 1610 ASN1OctetString value; 1611 if (colonPos == (length-1)) 1612 { 1613 value = new ASN1OctetString(); 1614 } 1615 else 1616 { 1617 char c = line.charAt(colonPos+1); 1618 if (c == ':') 1619 { 1620 // The value is base64-encoded. Find the first non-blank 1621 // character, take the rest of the line, and base64-decode it. 1622 int pos = colonPos+2; 1623 while ((pos < length) && (line.charAt(pos) == ' ')) 1624 { 1625 pos++; 1626 } 1627 1628 try 1629 { 1630 value = new ASN1OctetString(Base64.decode(line.substring(pos))); 1631 } 1632 catch (Exception e) 1633 { 1634 // The value did not have a valid base64-encoding. 1635 if (debugEnabled()) 1636 { 1637 TRACER.debugCaught(DebugLogLevel.ERROR, e); 1638 } 1639 1640 Message message = ERR_LDIF_COULD_NOT_BASE64_DECODE_ATTR.get( 1641 String.valueOf(entryDN), 1642 lastEntryLineNumber, line, 1643 String.valueOf(e)); 1644 logToRejectWriter(lines, message); 1645 throw new LDIFException(message, lastEntryLineNumber, true, e); 1646 } 1647 } 1648 else if (c == '<') 1649 { 1650 // Find the first non-blank character, decode the rest of the 1651 // line as a URL, and read its contents. 1652 int pos = colonPos+2; 1653 while ((pos < length) && (line.charAt(pos) == ' ')) 1654 { 1655 pos++; 1656 } 1657 1658 URL contentURL; 1659 try 1660 { 1661 contentURL = new URL(line.substring(pos)); 1662 } 1663 catch (Exception e) 1664 { 1665 // The URL was malformed or had an invalid protocol. 1666 if (debugEnabled()) 1667 { 1668 TRACER.debugCaught(DebugLogLevel.ERROR, e); 1669 } 1670 1671 Message message = ERR_LDIF_INVALID_URL.get(String.valueOf(entryDN), 1672 lastEntryLineNumber, 1673 String.valueOf(attrName), 1674 String.valueOf(e)); 1675 logToRejectWriter(lines, message); 1676 throw new LDIFException(message, lastEntryLineNumber, true, e); 1677 } 1678 1679 1680 InputStream inputStream = null; 1681 ByteArrayOutputStream outputStream = null; 1682 try 1683 { 1684 outputStream = new ByteArrayOutputStream(); 1685 inputStream = contentURL.openConnection().getInputStream(); 1686 1687 int bytesRead; 1688 while ((bytesRead = inputStream.read(buffer)) > 0) 1689 { 1690 outputStream.write(buffer, 0, bytesRead); 1691 } 1692 1693 value = new ASN1OctetString(outputStream.toByteArray()); 1694 } 1695 catch (Exception e) 1696 { 1697 // We were unable to read the contents of that URL for some 1698 // reason. 1699 if (debugEnabled()) 1700 { 1701 TRACER.debugCaught(DebugLogLevel.ERROR, e); 1702 } 1703 1704 Message message = ERR_LDIF_URL_IO_ERROR.get(String.valueOf(entryDN), 1705 lastEntryLineNumber, 1706 String.valueOf(attrName), 1707 String.valueOf(contentURL), 1708 String.valueOf(e)); 1709 logToRejectWriter(lines, message); 1710 throw new LDIFException(message, lastEntryLineNumber, true, e); 1711 } 1712 finally 1713 { 1714 if (outputStream != null) 1715 { 1716 try 1717 { 1718 outputStream.close(); 1719 } catch (Exception e) {} 1720 } 1721 1722 if (inputStream != null) 1723 { 1724 try 1725 { 1726 inputStream.close(); 1727 } catch (Exception e) {} 1728 } 1729 } 1730 } 1731 else 1732 { 1733 // The rest of the line should be the value. Skip over any 1734 // spaces and take the rest of the line as the value. 1735 int pos = colonPos+1; 1736 while ((pos < length) && (line.charAt(pos) == ' ')) 1737 { 1738 pos++; 1739 } 1740 1741 value = new ASN1OctetString(line.substring(pos)); 1742 } 1743 } 1744 return value; 1745 } 1746 1747 /** 1748 * Log a message to the reject writer if one is configured. 1749 * 1750 * @param lines 1751 * The set of rejected lines. 1752 * @param message 1753 * The associated error message. 1754 */ 1755 private void logToRejectWriter(LinkedList<StringBuilder> lines, 1756 Message message) { 1757 1758 BufferedWriter rejectWriter = importConfig.getRejectWriter(); 1759 if (rejectWriter != null) 1760 { 1761 logToWriter(rejectWriter, lines, message); 1762 } 1763 } 1764 1765 /** 1766 * Log a message to the reject writer if one is configured. 1767 * 1768 * @param lines 1769 * The set of rejected lines. 1770 * @param message 1771 * The associated error message. 1772 */ 1773 private void logToSkipWriter(LinkedList<StringBuilder> lines, 1774 Message message) { 1775 1776 BufferedWriter skipWriter = importConfig.getSkipWriter(); 1777 if (skipWriter != null) 1778 { 1779 logToWriter(skipWriter, lines, message); 1780 } 1781 } 1782 1783 /** 1784 * Log a message to the given writer. 1785 * 1786 * @param writer 1787 * The writer to write to. 1788 * @param lines 1789 * The set of rejected lines. 1790 * @param message 1791 * The associated error message. 1792 */ 1793 private void logToWriter(BufferedWriter writer, 1794 LinkedList<StringBuilder> lines, 1795 Message message) 1796 { 1797 if (writer != null) 1798 { 1799 try 1800 { 1801 writer.write("# "); 1802 writer.write(String.valueOf(message)); 1803 writer.newLine(); 1804 for (StringBuilder sb : lines) 1805 { 1806 writer.write(sb.toString()); 1807 writer.newLine(); 1808 } 1809 1810 writer.newLine(); 1811 } 1812 catch (Exception e) 1813 { 1814 if (debugEnabled()) 1815 { 1816 TRACER.debugCaught(DebugLogLevel.ERROR, e); 1817 } 1818 } 1819 } 1820 } 1821 1822 } 1823