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