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