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 2009-2010 Sun Microsystems, Inc. 025 * Portions copyright 2011-2015 ForgeRock AS 026 */ 027 028package org.forgerock.opendj.ldif; 029 030import static com.forgerock.opendj.ldap.CoreMessages.*; 031 032import java.io.IOException; 033import java.io.InputStream; 034import java.io.Reader; 035import java.util.Arrays; 036import java.util.LinkedList; 037import java.util.List; 038import java.util.NoSuchElementException; 039 040import org.forgerock.i18n.LocalizableMessage; 041import org.forgerock.i18n.LocalizedIllegalArgumentException; 042import org.forgerock.opendj.ldap.AttributeDescription; 043import org.forgerock.opendj.ldap.DN; 044import org.forgerock.opendj.ldap.DecodeException; 045import org.forgerock.opendj.ldap.Entry; 046import org.forgerock.opendj.ldap.LinkedHashMapEntry; 047import org.forgerock.opendj.ldap.Matcher; 048import org.forgerock.opendj.ldap.schema.Schema; 049import org.forgerock.opendj.ldap.schema.SchemaValidationPolicy; 050import org.forgerock.util.Reject; 051import org.forgerock.util.Utils; 052 053/** 054 * An LDIF entry reader reads attribute value records (entries) using the LDAP 055 * Data Interchange Format (LDIF) from a user defined source. 056 * 057 * @see <a href="http://tools.ietf.org/html/rfc2849">RFC 2849 - The LDAP Data 058 * Interchange Format (LDIF) - Technical Specification </a> 059 */ 060public final class LDIFEntryReader extends AbstractLDIFReader implements EntryReader { 061 /** Poison used to indicate end of LDIF. */ 062 private static final Entry EOF = new LinkedHashMapEntry(); 063 064 /** 065 * Parses the provided array of LDIF lines as a single LDIF entry. 066 * 067 * @param ldifLines 068 * The lines of LDIF to be parsed. 069 * @return The parsed LDIF entry. 070 * @throws LocalizedIllegalArgumentException 071 * If {@code ldifLines} did not contain an LDIF entry, if it 072 * contained multiple entries, if contained malformed LDIF, or 073 * if the entry could not be decoded using the default schema. 074 * @throws NullPointerException 075 * If {@code ldifLines} was {@code null}. 076 */ 077 public static Entry valueOfLDIFEntry(final String... ldifLines) { 078 final LDIFEntryReader reader = new LDIFEntryReader(ldifLines); 079 try { 080 if (!reader.hasNext()) { 081 // No change record found. 082 final LocalizableMessage message = 083 WARN_READ_LDIF_RECORD_NO_CHANGE_RECORD_FOUND.get(); 084 throw new LocalizedIllegalArgumentException(message); 085 } 086 087 final Entry entry = reader.readEntry(); 088 089 if (reader.hasNext()) { 090 // Multiple change records found. 091 final LocalizableMessage message = 092 WARN_READ_LDIF_RECORD_MULTIPLE_CHANGE_RECORDS_FOUND.get(); 093 throw new LocalizedIllegalArgumentException(message); 094 } 095 096 return entry; 097 } catch (final DecodeException e) { 098 // Badly formed LDIF. 099 throw new LocalizedIllegalArgumentException(e.getMessageObject()); 100 } catch (final IOException e) { 101 // This should never happen for a String based reader. 102 final LocalizableMessage message = 103 WARN_READ_LDIF_RECORD_UNEXPECTED_IO_ERROR.get(e.getMessage()); 104 throw new LocalizedIllegalArgumentException(message); 105 } finally { 106 Utils.closeSilently(reader); 107 } 108 } 109 110 private Entry nextEntry; 111 112 /** 113 * Creates a new LDIF entry reader whose source is the provided input 114 * stream. 115 * 116 * @param in 117 * The input stream to use. 118 * @throws NullPointerException 119 * If {@code in} was {@code null}. 120 */ 121 public LDIFEntryReader(final InputStream in) { 122 super(in); 123 } 124 125 /** 126 * Creates a new LDIF entry reader which will read lines of LDIF from the 127 * provided list of LDIF lines. 128 * 129 * @param ldifLines 130 * The lines of LDIF to be read. 131 * @throws NullPointerException 132 * If {@code ldifLines} was {@code null}. 133 */ 134 public LDIFEntryReader(final List<String> ldifLines) { 135 super(ldifLines); 136 } 137 138 /** 139 * Creates a new LDIF entry reader whose source is the provided character 140 * stream reader. 141 * 142 * @param reader 143 * The character stream reader to use. 144 * @throws NullPointerException 145 * If {@code reader} was {@code null}. 146 */ 147 public LDIFEntryReader(final Reader reader) { 148 super(reader); 149 } 150 151 /** 152 * Creates a new LDIF entry reader which will read lines of LDIF from the 153 * provided array of LDIF lines. 154 * 155 * @param ldifLines 156 * The lines of LDIF to be read. 157 * @throws NullPointerException 158 * If {@code ldifLines} was {@code null}. 159 */ 160 public LDIFEntryReader(final String... ldifLines) { 161 super(Arrays.asList(ldifLines)); 162 } 163 164 /** {@inheritDoc} */ 165 @Override 166 public void close() throws IOException { 167 close0(); 168 } 169 170 /** 171 * {@inheritDoc} 172 * 173 * @throws DecodeException 174 * If the entry could not be decoded because it was malformed. 175 */ 176 @Override 177 public boolean hasNext() throws DecodeException, IOException { 178 return getNextEntry() != EOF; 179 } 180 181 /** 182 * {@inheritDoc} 183 * 184 * @throws DecodeException 185 * If the entry could not be decoded because it was malformed. 186 */ 187 @Override 188 public Entry readEntry() throws DecodeException, IOException { 189 if (!hasNext()) { 190 // LDIF reader has completed successfully. 191 throw new NoSuchElementException(); 192 } 193 194 final Entry entry = nextEntry; 195 nextEntry = null; 196 return entry; 197 } 198 199 /** 200 * Specifies whether or not all operational attributes should be excluded 201 * from any entries that are read from LDIF. The default is {@code false}. 202 * 203 * @param excludeOperationalAttributes 204 * {@code true} if all operational attributes should be excluded, 205 * or {@code false} otherwise. 206 * @return A reference to this {@code LDIFEntryReader}. 207 */ 208 public LDIFEntryReader setExcludeAllOperationalAttributes( 209 final boolean excludeOperationalAttributes) { 210 this.excludeOperationalAttributes = excludeOperationalAttributes; 211 return this; 212 } 213 214 /** 215 * Specifies whether or not all user attributes should be excluded from any 216 * entries that are read from LDIF. The default is {@code false}. 217 * 218 * @param excludeUserAttributes 219 * {@code true} if all user attributes should be excluded, or 220 * {@code false} otherwise. 221 * @return A reference to this {@code LDIFEntryReader}. 222 */ 223 public LDIFEntryReader setExcludeAllUserAttributes(final boolean excludeUserAttributes) { 224 this.excludeUserAttributes = excludeUserAttributes; 225 return this; 226 } 227 228 /** 229 * Excludes the named attribute from any entries that are read from LDIF. By 230 * default all attributes are included unless explicitly excluded. 231 * 232 * @param attributeDescription 233 * The name of the attribute to be excluded. 234 * @return A reference to this {@code LDIFEntryReader}. 235 */ 236 public LDIFEntryReader setExcludeAttribute(final AttributeDescription attributeDescription) { 237 Reject.ifNull(attributeDescription); 238 excludeAttributes.add(attributeDescription); 239 return this; 240 } 241 242 /** 243 * Excludes all entries beneath the named entry (inclusive) from being read 244 * from LDIF. By default all entries are written unless explicitly excluded 245 * or included by branches or filters. 246 * 247 * @param excludeBranch 248 * The distinguished name of the branch to be excluded. 249 * @return A reference to this {@code LDIFEntryReader}. 250 */ 251 public LDIFEntryReader setExcludeBranch(final DN excludeBranch) { 252 Reject.ifNull(excludeBranch); 253 excludeBranches.add(excludeBranch); 254 return this; 255 } 256 257 /** 258 * Excludes all entries which match the provided filter matcher from being 259 * read from LDIF. By default all entries are read unless explicitly 260 * excluded or included by branches or filters. 261 * 262 * @param excludeFilter 263 * The filter matcher. 264 * @return A reference to this {@code LDIFEntryReader}. 265 */ 266 public LDIFEntryReader setExcludeFilter(final Matcher excludeFilter) { 267 Reject.ifNull(excludeFilter); 268 excludeFilters.add(excludeFilter); 269 return this; 270 } 271 272 /** 273 * Ensures that the named attribute is not excluded from any entries that 274 * are read from LDIF. By default all attributes are included unless 275 * explicitly excluded. 276 * 277 * @param attributeDescription 278 * The name of the attribute to be included. 279 * @return A reference to this {@code LDIFEntryReader}. 280 */ 281 public LDIFEntryReader setIncludeAttribute(final AttributeDescription attributeDescription) { 282 Reject.ifNull(attributeDescription); 283 includeAttributes.add(attributeDescription); 284 return this; 285 } 286 287 /** 288 * Ensures that all entries beneath the named entry (inclusive) are read 289 * from LDIF. By default all entries are written unless explicitly excluded 290 * or included by branches or filters. 291 * 292 * @param includeBranch 293 * The distinguished name of the branch to be included. 294 * @return A reference to this {@code LDIFEntryReader}. 295 */ 296 public LDIFEntryReader setIncludeBranch(final DN includeBranch) { 297 Reject.ifNull(includeBranch); 298 includeBranches.add(includeBranch); 299 return this; 300 } 301 302 /** 303 * Ensures that all entries which match the provided filter matcher are read 304 * from LDIF. By default all entries are read unless explicitly excluded or 305 * included by branches or filters. 306 * 307 * @param includeFilter 308 * The filter matcher. 309 * @return A reference to this {@code LDIFEntryReader}. 310 */ 311 public LDIFEntryReader setIncludeFilter(final Matcher includeFilter) { 312 Reject.ifNull(includeFilter); 313 includeFilters.add(includeFilter); 314 return this; 315 } 316 317 /** 318 * Sets the rejected record listener which should be notified whenever an 319 * LDIF record is skipped, malformed, or fails schema validation. 320 * <p> 321 * By default the {@link RejectedLDIFListener#FAIL_FAST} listener is used. 322 * 323 * @param listener 324 * The rejected record listener. 325 * @return A reference to this {@code LDIFEntryReader}. 326 */ 327 public LDIFEntryReader setRejectedLDIFListener(final RejectedLDIFListener listener) { 328 this.rejectedRecordListener = listener; 329 return this; 330 } 331 332 /** 333 * Sets the schema which should be used for decoding entries that are read 334 * from LDIF. The default schema is used if no other is specified. 335 * 336 * @param schema 337 * The schema which should be used for decoding entries that are 338 * read from LDIF. 339 * @return A reference to this {@code LDIFEntryReader}. 340 */ 341 public LDIFEntryReader setSchema(final Schema schema) { 342 Reject.ifNull(schema); 343 this.schema = schemaValidationPolicy.adaptSchemaForValidation(schema); 344 return this; 345 } 346 347 /** 348 * Specifies the schema validation which should be used when reading LDIF 349 * entry records. If attribute value validation is enabled then all checks 350 * will be performed. 351 * <p> 352 * Schema validation is disabled by default. 353 * <p> 354 * <b>NOTE:</b> this method copies the provided policy so changes made to it 355 * after this method has been called will have no effect. 356 * 357 * @param policy 358 * The schema validation which should be used when reading LDIF 359 * entry records. 360 * @return A reference to this {@code LDIFEntryReader}. 361 */ 362 public LDIFEntryReader setSchemaValidationPolicy(final SchemaValidationPolicy policy) { 363 this.schemaValidationPolicy = SchemaValidationPolicy.copyOf(policy); 364 this.schema = schemaValidationPolicy.adaptSchemaForValidation(schema); 365 return this; 366 } 367 368 private Entry getNextEntry() throws DecodeException, IOException { 369 while (nextEntry == null) { 370 // Read the set of lines that make up the next entry. 371 final LDIFRecord record = readLDIFRecord(); 372 if (record == null) { 373 nextEntry = EOF; 374 break; 375 } 376 377 try { 378 /* 379 * Read the DN of the entry and see if it is one that should be 380 * included in the import. 381 */ 382 final DN entryDN = readLDIFRecordDN(record); 383 if (entryDN == null) { 384 // Skip version record. 385 continue; 386 } 387 388 // Skip if branch containing the entry DN is excluded. 389 if (isBranchExcluded(entryDN)) { 390 final LocalizableMessage message = 391 ERR_LDIF_ENTRY_EXCLUDED_BY_DN 392 .get(record.lineNumber, entryDN.toString()); 393 handleSkippedRecord(record, message); 394 continue; 395 } 396 397 // Use an Entry for the AttributeSequence. 398 final Entry entry = new LinkedHashMapEntry(entryDN); 399 boolean schemaValidationFailure = false; 400 final List<LocalizableMessage> schemaErrors = new LinkedList<>(); 401 while (record.iterator.hasNext()) { 402 final String ldifLine = record.iterator.next(); 403 if (!readLDIFRecordAttributeValue(record, ldifLine, entry, schemaErrors)) { 404 schemaValidationFailure = true; 405 } 406 } 407 408 // Skip if the entry is excluded by any filters. 409 if (isEntryExcluded(entry)) { 410 final LocalizableMessage message = 411 ERR_LDIF_ENTRY_EXCLUDED_BY_FILTER.get(record.lineNumber, entryDN 412 .toString()); 413 handleSkippedRecord(record, message); 414 continue; 415 } 416 417 if (!schema.validateEntry(entry, schemaValidationPolicy, schemaErrors)) { 418 schemaValidationFailure = true; 419 } 420 421 if (schemaValidationFailure) { 422 handleSchemaValidationFailure(record, schemaErrors); 423 continue; 424 } 425 426 if (!schemaErrors.isEmpty()) { 427 handleSchemaValidationWarning(record, schemaErrors); 428 } 429 430 nextEntry = entry; 431 } catch (final DecodeException e) { 432 handleMalformedRecord(record, e.getMessageObject()); 433 continue; 434 } 435 } 436 437 return nextEntry; 438 } 439 440}