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