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}