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 * Portions copyright 2016 Matthew Stevenson
017 */
018package org.forgerock.opendj.ldif;
019
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.Reader;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.LinkedList;
026import java.util.List;
027import java.util.NoSuchElementException;
028import java.util.regex.Matcher;
029import java.util.regex.Pattern;
030
031import org.forgerock.i18n.LocalizableMessage;
032import org.forgerock.i18n.LocalizableMessageBuilder;
033import org.forgerock.i18n.LocalizedIllegalArgumentException;
034import org.forgerock.opendj.ldap.AttributeDescription;
035import org.forgerock.opendj.ldap.ByteString;
036import org.forgerock.opendj.ldap.DN;
037import org.forgerock.opendj.ldap.DecodeException;
038import org.forgerock.opendj.ldap.Entry;
039import org.forgerock.opendj.ldap.LinkedAttribute;
040import org.forgerock.opendj.ldap.LinkedHashMapEntry;
041import org.forgerock.opendj.ldap.Modification;
042import org.forgerock.opendj.ldap.ModificationType;
043import org.forgerock.opendj.ldap.RDN;
044import org.forgerock.opendj.ldap.controls.Control;
045import org.forgerock.opendj.ldap.controls.GenericControl;
046import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
047import org.forgerock.opendj.ldap.requests.ModifyRequest;
048import org.forgerock.opendj.ldap.requests.Requests;
049import org.forgerock.opendj.ldap.schema.Schema;
050import org.forgerock.opendj.ldap.schema.SchemaValidationPolicy;
051import org.forgerock.opendj.ldap.schema.Syntax;
052import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
053import org.forgerock.util.Reject;
054
055import static com.forgerock.opendj.ldap.CoreMessages.*;
056import static com.forgerock.opendj.util.StaticUtils.*;
057
058/**
059 * An LDIF change record reader reads change records using the LDAP Data
060 * Interchange Format (LDIF) from a user defined source.
061 * <p>
062 * The following example reads changes from LDIF, and writes the changes to the
063 * directory server.
064 *
065 * <pre>
066 * InputStream ldif = ...;
067 * LDIFChangeRecordReader reader = new LDIFChangeRecordReader(ldif);
068 *
069 * Connection connection = ...;
070 * connection.bind(...);
071 *
072 * ConnectionChangeRecordWriter writer =
073 *         new ConnectionChangeRecordWriter(connection);
074 * while (reader.hasNext()) {
075 *     ChangeRecord changeRecord = reader.readChangeRecord();
076 *     writer.writeChangeRecord(changeRecord);
077 * }
078 * </pre>
079 *
080 * @see <a href="http://tools.ietf.org/html/rfc2849">RFC 2849 - The LDAP Data
081 *      Interchange Format (LDIF) - Technical Specification </a>
082 */
083public final class LDIFChangeRecordReader extends AbstractLDIFReader implements ChangeRecordReader {
084    private static final Pattern CONTROL_REGEX = Pattern
085            .compile("^\\s*(\\d+(.\\d+)*)(\\s+((true)|(false)))?\\s*(:(:)?\\s*?\\S+)?\\s*$");
086
087    /** Poison used to indicate end of LDIF. */
088    private static final ChangeRecord EOF = Requests.newAddRequest(DN.rootDN());
089
090    /**
091     * Parses the provided array of LDIF lines as a single LDIF change record.
092     *
093     * @param ldifLines
094     *            The lines of LDIF to be parsed.
095     * @return The parsed LDIF change record.
096     * @throws LocalizedIllegalArgumentException
097     *             If {@code ldifLines} did not contain an LDIF change record,
098     *             if it contained multiple change records, if contained
099     *             malformed LDIF, or if the change record could not be decoded
100     *             using the default schema.
101     * @throws NullPointerException
102     *             If {@code ldifLines} was {@code null}.
103     */
104    public static ChangeRecord valueOfLDIFChangeRecord(final String... ldifLines) {
105        // LDIF change record reader is tolerant to missing change types.
106        try (final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(ldifLines)) {
107            if (!reader.hasNext()) {
108                // No change record found.
109                final LocalizableMessage message =
110                        WARN_READ_LDIF_RECORD_NO_CHANGE_RECORD_FOUND.get();
111                throw new LocalizedIllegalArgumentException(message);
112            }
113
114            final ChangeRecord record = reader.readChangeRecord();
115
116            if (reader.hasNext()) {
117                // Multiple change records found.
118                final LocalizableMessage message =
119                        WARN_READ_LDIF_RECORD_MULTIPLE_CHANGE_RECORDS_FOUND.get();
120                throw new LocalizedIllegalArgumentException(message);
121            }
122
123            return record;
124        } catch (final DecodeException e) {
125            // Badly formed LDIF.
126            throw new LocalizedIllegalArgumentException(e.getMessageObject());
127        } catch (final IOException e) {
128            // This should never happen for a String based reader.
129            final LocalizableMessage message =
130                    WARN_READ_LDIF_RECORD_UNEXPECTED_IO_ERROR.get(e.getMessage());
131            throw new LocalizedIllegalArgumentException(message);
132        }
133    }
134
135    private ChangeRecord nextChangeRecord;
136
137    /**
138     * Creates a new LDIF change record reader whose source is the provided
139     * input stream.
140     *
141     * @param in
142     *            The input stream to use.
143     * @throws NullPointerException
144     *             If {@code in} was {@code null}.
145     */
146    public LDIFChangeRecordReader(final InputStream in) {
147        super(in);
148    }
149
150    /**
151     * Creates a new LDIF change record reader which will read lines of LDIF
152     * from the provided list of LDIF lines.
153     *
154     * @param ldifLines
155     *            The lines of LDIF to be read.
156     * @throws NullPointerException
157     *             If {@code ldifLines} was {@code null}.
158     */
159    public LDIFChangeRecordReader(final List<String> ldifLines) {
160        super(ldifLines);
161    }
162
163    /**
164     * Creates a new LDIF change record reader whose source is the provided
165     * character stream reader.
166     *
167     * @param reader
168     *            The character stream reader to use.
169     * @throws NullPointerException
170     *             If {@code reader} was {@code null}.
171     */
172    public LDIFChangeRecordReader(final Reader reader) {
173        super(reader);
174    }
175
176    /**
177     * Creates a new LDIF change record reader which will read lines of LDIF
178     * from the provided array of LDIF lines.
179     *
180     * @param ldifLines
181     *            The lines of LDIF to be read.
182     * @throws NullPointerException
183     *             If {@code ldifLines} was {@code null}.
184     */
185    public LDIFChangeRecordReader(final String... ldifLines) {
186        super(Arrays.asList(ldifLines));
187    }
188
189    @Override
190    public void close() throws IOException {
191        close0();
192    }
193
194    /**
195     * {@inheritDoc}
196     *
197     * @throws DecodeException
198     *             If the change record could not be decoded because it was
199     *             malformed.
200     */
201    @Override
202    public boolean hasNext() throws DecodeException, IOException {
203        return getNextChangeRecord() != EOF;
204    }
205
206    /**
207     * {@inheritDoc}
208     *
209     * @throws DecodeException
210     *             If the entry could not be decoded because it was malformed.
211     */
212    @Override
213    public ChangeRecord readChangeRecord() throws DecodeException, IOException {
214        if (!hasNext()) {
215            // LDIF reader has completed successfully.
216            throw new NoSuchElementException();
217        }
218
219        final ChangeRecord changeRecord = nextChangeRecord;
220        nextChangeRecord = null;
221        return changeRecord;
222    }
223
224    /**
225     * Specifies whether all operational attributes should be excluded
226     * from any change records that are read from LDIF. The default is
227     * {@code false}.
228     *
229     * @param excludeOperationalAttributes
230     *            {@code true} if all operational attributes should be excluded,
231     *            or {@code false} otherwise.
232     * @return A reference to this {@code LDIFChangeRecordReader}.
233     */
234    public LDIFChangeRecordReader setExcludeAllOperationalAttributes(
235            final boolean excludeOperationalAttributes) {
236        this.excludeOperationalAttributes = excludeOperationalAttributes;
237        return this;
238    }
239
240    /**
241     * Specifies whether all user attributes should be excluded from any
242     * change records that are read from LDIF. The default is {@code false}.
243     *
244     * @param excludeUserAttributes
245     *            {@code true} if all user attributes should be excluded, or
246     *            {@code false} otherwise.
247     * @return A reference to this {@code LDIFChangeRecordReader}.
248     */
249    public LDIFChangeRecordReader setExcludeAllUserAttributes(final boolean excludeUserAttributes) {
250        this.excludeUserAttributes = excludeUserAttributes;
251        return this;
252    }
253
254    /**
255     * Excludes the named attribute from any change records that are read from
256     * LDIF. By default all attributes are included unless explicitly excluded.
257     *
258     * @param attributeDescription
259     *            The name of the attribute to be excluded.
260     * @return A reference to this {@code LDIFChangeRecordReader}.
261     */
262    public LDIFChangeRecordReader setExcludeAttribute(
263            final AttributeDescription attributeDescription) {
264        Reject.ifNull(attributeDescription);
265        excludeAttributes.add(attributeDescription);
266        return this;
267    }
268
269    /**
270     * Excludes all change records which target entries beneath the named entry
271     * (inclusive) from being read from LDIF. By default all change records are
272     * read unless explicitly excluded or included.
273     *
274     * @param excludeBranch
275     *            The distinguished name of the branch to be excluded.
276     * @return A reference to this {@code LDIFChangeRecordReader}.
277     */
278    public LDIFChangeRecordReader setExcludeBranch(final DN excludeBranch) {
279        Reject.ifNull(excludeBranch);
280        excludeBranches.add(excludeBranch);
281        return this;
282    }
283
284    /**
285     * Ensures that the named attribute is not excluded from any change records
286     * that are read from LDIF. By default all attributes are included unless
287     * explicitly excluded.
288     *
289     * @param attributeDescription
290     *            The name of the attribute to be included.
291     * @return A reference to this {@code LDIFChangeRecordReader}.
292     */
293    public LDIFChangeRecordReader setIncludeAttribute(
294            final AttributeDescription attributeDescription) {
295        Reject.ifNull(attributeDescription);
296        includeAttributes.add(attributeDescription);
297        return this;
298    }
299
300    /**
301     * Ensures that all change records which target entries beneath the named
302     * entry (inclusive) are read from LDIF. By default all change records are
303     * read unless explicitly excluded or included.
304     *
305     * @param includeBranch
306     *            The distinguished name of the branch to be included.
307     * @return A reference to this {@code LDIFChangeRecordReader}.
308     */
309    public LDIFChangeRecordReader setIncludeBranch(final DN includeBranch) {
310        Reject.ifNull(includeBranch);
311        includeBranches.add(includeBranch);
312        return this;
313    }
314
315    /**
316     * Sets the rejected record listener which should be notified whenever an
317     * LDIF record is skipped, malformed, or fails schema validation.
318     * <p>
319     * By default the {@link RejectedLDIFListener#FAIL_FAST} listener is used.
320     *
321     * @param listener
322     *            The rejected record listener.
323     * @return A reference to this {@code LDIFChangeRecordReader}.
324     */
325    public LDIFChangeRecordReader setRejectedLDIFListener(final RejectedLDIFListener listener) {
326        this.rejectedRecordListener = listener;
327        return this;
328    }
329
330    /**
331     * Sets the schema which should be used for decoding change records that are
332     * read from LDIF. The default schema is used if no other is specified.
333     *
334     * @param schema
335     *            The schema which should be used for decoding change records
336     *            that are read from LDIF.
337     * @return A reference to this {@code LDIFChangeRecordReader}.
338     */
339    public LDIFChangeRecordReader setSchema(final Schema schema) {
340        Reject.ifNull(schema);
341        this.schema = schemaValidationPolicy.adaptSchemaForValidation(schema);
342        return this;
343    }
344
345    /**
346     * Specifies the schema validation which should be used when reading LDIF
347     * change records. If attribute value validation is enabled then all checks
348     * will be performed.
349     * <p>
350     * Schema validation is disabled by default.
351     * <p>
352     * <b>NOTE:</b> this method copies the provided policy so changes made to it
353     * after this method has been called will have no effect.
354     *
355     * @param policy
356     *            The schema validation which should be used when reading LDIF
357     *            change records.
358     * @return A reference to this {@code LDIFChangeRecordReader}.
359     */
360    public LDIFChangeRecordReader setSchemaValidationPolicy(final SchemaValidationPolicy policy) {
361        this.schemaValidationPolicy = SchemaValidationPolicy.copyOf(policy);
362        this.schema = schemaValidationPolicy.adaptSchemaForValidation(schema);
363        return this;
364    }
365
366    private ChangeRecord getNextChangeRecord() throws DecodeException, IOException {
367        while (nextChangeRecord == null) {
368            // Read the set of lines that make up the next entry.
369            final LDIFRecord record = readLDIFRecord();
370            if (record == null) {
371                nextChangeRecord = EOF;
372                break;
373            }
374
375            try {
376                /* Read the DN of the entry and see if it is one that should be included in the import. */
377                final DN entryDN = readLDIFRecordDN(record);
378                if (entryDN == null) {
379                    // Skip version record.
380                    continue;
381                }
382
383                // Skip if branch containing the entry DN is excluded.
384                if (isBranchExcluded(entryDN)) {
385                    final LocalizableMessage message =
386                            ERR_LDIF_CHANGE_EXCLUDED_BY_DN.get(record.lineNumber, entryDN);
387                    handleSkippedRecord(record, message);
388                    continue;
389                }
390
391                KeyValuePair pair;
392                String ldifLine;
393                List<Control> controls = null;
394                while (true) {
395                    if (!record.iterator.hasNext()) {
396                        throw DecodeException.error(
397                                ERR_LDIF_NO_CHANGE_TYPE.get(record.lineNumber, entryDN));
398                    }
399
400                    pair = new KeyValuePair();
401                    ldifLine = readLDIFRecordKeyValuePair(record, pair, false);
402                    if (pair.key == null) {
403                        throw DecodeException.error(
404                                ERR_LDIF_MALFORMED_CHANGE_TYPE.get(record.lineNumber, entryDN, ldifLine));
405                    }
406
407                    if (!"control".equals(toLowerCase(pair.key))) {
408                        break;
409                    }
410
411                    if (controls == null) {
412                        controls = new LinkedList<>();
413                    }
414
415                    controls.add(parseControl(entryDN, record, ldifLine, pair.value));
416                }
417
418                if (!"changetype".equals(toLowerCase(pair.key))) {
419                    // Default to add change record.
420                    nextChangeRecord = parseAddChangeRecordEntry(entryDN, ldifLine, record);
421                } else {
422                    final String changeType = toLowerCase(pair.value);
423                    if ("add".equals(changeType)) {
424                        nextChangeRecord = parseAddChangeRecordEntry(entryDN, null, record);
425                    } else if ("delete".equals(changeType)) {
426                        nextChangeRecord = parseDeleteChangeRecordEntry(entryDN, record);
427                    } else if ("modify".equals(changeType)) {
428                        nextChangeRecord = parseModifyChangeRecordEntry(entryDN, record);
429                    } else if ("modrdn".equals(changeType)) {
430                        nextChangeRecord = parseModifyDNChangeRecordEntry(entryDN, record);
431                    } else if ("moddn".equals(changeType)) {
432                        nextChangeRecord = parseModifyDNChangeRecordEntry(entryDN, record);
433                    } else {
434                        throw DecodeException.error(
435                                ERR_LDIF_BAD_CHANGE_TYPE.get(record.lineNumber, entryDN, pair.value));
436                    }
437
438                    // Add the controls to the record.
439                    if (controls != null) {
440                        for (final Control control : controls) {
441                            nextChangeRecord.addControl(control);
442                        }
443                    }
444                }
445            } catch (final DecodeException e) {
446                handleMalformedRecord(record, e.getMessageObject());
447                continue;
448            }
449        }
450        return nextChangeRecord;
451    }
452
453    private ChangeRecord parseAddChangeRecordEntry(final DN entryDN, final String lastLDIFLine,
454            final LDIFRecord record) throws DecodeException {
455        // Use an Entry for the AttributeSequence.
456        final Entry entry = new LinkedHashMapEntry(entryDN);
457        boolean schemaValidationFailure = false;
458        final List<LocalizableMessage> schemaErrors = new LinkedList<>();
459
460        if (lastLDIFLine != null
461                // This line was read when looking for the change type.
462                && !readLDIFRecordAttributeValue(record, lastLDIFLine, entry, schemaErrors)) {
463            schemaValidationFailure = true;
464        }
465
466        while (record.iterator.hasNext()) {
467            final String ldifLine = record.iterator.next();
468            if (!readLDIFRecordAttributeValue(record, ldifLine, entry, schemaErrors)) {
469                schemaValidationFailure = true;
470            }
471        }
472
473        if (!schema.validateEntry(entry, schemaValidationPolicy, schemaErrors)) {
474            schemaValidationFailure = true;
475        }
476
477        if (schemaValidationFailure) {
478            handleSchemaValidationFailure(record, schemaErrors);
479            return null;
480        }
481
482        if (!schemaErrors.isEmpty()) {
483            handleSchemaValidationWarning(record, schemaErrors);
484        }
485        return Requests.newAddRequest(entry);
486    }
487
488    private Control parseControl(final DN entryDN, final LDIFRecord record, final String ldifLine,
489            final String value) throws DecodeException {
490        final Matcher matcher = CONTROL_REGEX.matcher(value);
491        if (!matcher.matches()) {
492            throw DecodeException.error(ERR_LDIF_MALFORMED_CONTROL.get(record.lineNumber, entryDN, ldifLine));
493        }
494        final String oid = matcher.group(1);
495        final boolean isCritical = matcher.group(5) != null;
496        final String controlValueString = matcher.group(7);
497        ByteString controlValue = null;
498        if (controlValueString != null) {
499            controlValue =
500                    parseSingleValue(record, ldifLine, entryDN, ldifLine.indexOf(':', 8), oid);
501        }
502        return GenericControl.newControl(oid, isCritical, controlValue);
503    }
504
505    private ChangeRecord parseDeleteChangeRecordEntry(final DN entryDN, final LDIFRecord record)
506            throws DecodeException {
507        if (record.iterator.hasNext()) {
508            throw DecodeException.error(ERR_LDIF_MALFORMED_DELETE.get(record.lineNumber, entryDN));
509        }
510        return Requests.newDeleteRequest(entryDN);
511    }
512
513    private ChangeRecord parseModifyChangeRecordEntry(final DN entryDN, final LDIFRecord record)
514            throws DecodeException {
515        final ModifyRequest modifyRequest = Requests.newModifyRequest(entryDN);
516        final KeyValuePair pair = new KeyValuePair();
517        final List<ByteString> attributeValues = new ArrayList<>();
518        boolean schemaValidationFailure = false;
519        final List<LocalizableMessage> schemaErrors = new LinkedList<>();
520
521        while (record.iterator.hasNext()) {
522            String ldifLine = readLDIFRecordKeyValuePair(record, pair, false);
523            if (pair.key == null) {
524                throw DecodeException.error(
525                        ERR_LDIF_MALFORMED_MODIFICATION_TYPE.get(record.lineNumber, entryDN, ldifLine));
526            }
527
528            final String changeType = toLowerCase(pair.key);
529
530            ModificationType modType;
531            if ("add".equals(changeType)) {
532                modType = ModificationType.ADD;
533            } else if ("delete".equals(changeType)) {
534                modType = ModificationType.DELETE;
535            } else if ("replace".equals(changeType)) {
536                modType = ModificationType.REPLACE;
537            } else if ("increment".equals(changeType)) {
538                modType = ModificationType.INCREMENT;
539            } else {
540                throw DecodeException.error(
541                        ERR_LDIF_BAD_MODIFICATION_TYPE.get(record.lineNumber, entryDN, pair.key));
542            }
543
544            AttributeDescription attributeDescription;
545            try {
546                attributeDescription = AttributeDescription.valueOf(pair.value, schema);
547            } catch (final UnknownSchemaElementException e) {
548                final LocalizableMessage message =
549                        ERR_LDIF_UNKNOWN_ATTRIBUTE_TYPE.get(record.lineNumber, entryDN, pair.value);
550                switch (schemaValidationPolicy.checkAttributesAndObjectClasses()) {
551                case REJECT:
552                    schemaValidationFailure = true;
553                    schemaErrors.add(message);
554                    continue;
555                case WARN:
556                    schemaErrors.add(message);
557                    continue;
558                default: // Ignore
559                    /* This should not happen: we should be using a non-strict schema for this policy. */
560                    throw new IllegalStateException("Schema is not consistent with policy", e);
561                }
562            } catch (final LocalizedIllegalArgumentException e) {
563                throw DecodeException.error(
564                        ERR_LDIF_MALFORMED_ATTRIBUTE_NAME.get(record.lineNumber, entryDN, pair.value));
565            }
566
567            /*
568             * Skip the attribute if requested before performing any schema
569             * checking: the attribute may have been excluded because it is
570             * known to violate the schema.
571             */
572            if (isAttributeExcluded(attributeDescription)) {
573                continue;
574            }
575
576            final Syntax syntax = attributeDescription.getAttributeType().getSyntax();
577
578            // Ensure that the binary option is present if required.
579            if (!syntax.isBEREncodingRequired()) {
580                if (schemaValidationPolicy.checkAttributeValues().needsChecking()
581                        && attributeDescription.hasOption("binary")) {
582                    final LocalizableMessage message =
583                            ERR_LDIF_UNEXPECTED_BINARY_OPTION.get(record.lineNumber, entryDN, pair.value);
584                    if (schemaValidationPolicy.checkAttributeValues().isReject()) {
585                        schemaValidationFailure = true;
586                    }
587                    schemaErrors.add(message);
588                    continue;
589                }
590            } else {
591                attributeDescription = attributeDescription.withOption("binary");
592            }
593
594            /* Now go through the rest of the attributes until the "-" line is reached. */
595            attributeValues.clear();
596            while (record.iterator.hasNext()) {
597                ldifLine = record.iterator.next();
598                if ("-".equals(ldifLine)) {
599                    break;
600                }
601
602                // Parse the attribute description.
603                final int colonPos = parseColonPosition(record, ldifLine);
604                final String attrDescr = ldifLine.substring(0, colonPos);
605
606                AttributeDescription attributeDescription2;
607                try {
608                    attributeDescription2 = AttributeDescription.valueOf(attrDescr, schema);
609                } catch (final LocalizedIllegalArgumentException e) {
610                    /*
611                     * No need to catch schema exception here because it implies
612                     * that the attribute name is wrong and the record is
613                     * malformed.
614                     */
615                    throw DecodeException.error(
616                            ERR_LDIF_MALFORMED_ATTRIBUTE_NAME.get(record.lineNumber, entryDN, attrDescr));
617                }
618
619                // Ensure that the binary option is present if required.
620                if (attributeDescription.getAttributeType().getSyntax().isBEREncodingRequired()) {
621                    attributeDescription2 = attributeDescription2.withOption("binary");
622                }
623
624                if (!attributeDescription2.equals(attributeDescription)) {
625                    // Malformed record.
626                    throw DecodeException.error(ERR_LDIF_ATTRIBUTE_NAME_MISMATCH.get(
627                            record.lineNumber, entryDN, attributeDescription2, attributeDescription));
628                }
629
630                // Parse the attribute value and check it if needed.
631                final ByteString value =
632                        parseSingleValue(record, ldifLine, entryDN, colonPos, attrDescr);
633                if (schemaValidationPolicy.checkAttributeValues().needsChecking()) {
634                    final LocalizableMessageBuilder builder = new LocalizableMessageBuilder();
635                    if (!syntax.valueIsAcceptable(value, builder)) {
636                        /*
637                         * Just log a message, but don't skip the value since
638                         * this could change the semantics of the modification
639                         * (e.g. if all values in a delete are skipped then this
640                         * implies that the whole attribute should be removed).
641                         */
642                        if (schemaValidationPolicy.checkAttributeValues().isReject()) {
643                            schemaValidationFailure = true;
644                        }
645                        schemaErrors.add(builder.toMessage());
646                    }
647                }
648                attributeValues.add(value);
649            }
650
651            final Modification change =
652                    new Modification(modType, new LinkedAttribute(attributeDescription,
653                            attributeValues));
654            modifyRequest.addModification(change);
655        }
656
657        if (schemaValidationFailure) {
658            handleSchemaValidationFailure(record, schemaErrors);
659            return null;
660        }
661
662        if (!schemaErrors.isEmpty()) {
663            handleSchemaValidationWarning(record, schemaErrors);
664        }
665
666        return modifyRequest;
667    }
668
669    private ChangeRecord parseModifyDNChangeRecordEntry(final DN entryDN, final LDIFRecord record)
670            throws DecodeException {
671        // Parse the newrdn.
672        if (!record.iterator.hasNext()) {
673            throw DecodeException.error(ERR_LDIF_NO_NEW_RDN.get(record.lineNumber, entryDN));
674        }
675
676        final KeyValuePair pair = new KeyValuePair();
677        String ldifLine = readLDIFRecordKeyValuePair(record, pair, true);
678
679        if (pair.key == null || !"newrdn".equals(toLowerCase(pair.key))) {
680            throw DecodeException.error(
681                    ERR_LDIF_MALFORMED_NEW_RDN.get(record.lineNumber, entryDN, ldifLine));
682        }
683
684        final ModifyDNRequest modifyDNRequest;
685        try {
686            final RDN newRDN = RDN.valueOf(pair.value, schema);
687            modifyDNRequest = Requests.newModifyDNRequest(entryDN, newRDN);
688        } catch (final LocalizedIllegalArgumentException e) {
689            throw DecodeException.error(
690                    ERR_LDIF_MALFORMED_NEW_RDN.get(record.lineNumber, entryDN, pair.value));
691        }
692
693        // Parse the deleteoldrdn.
694        if (!record.iterator.hasNext()) {
695            final LocalizableMessage message =
696                    ERR_LDIF_NO_DELETE_OLD_RDN.get(record.lineNumber, entryDN.toString());
697            throw DecodeException.error(message);
698        }
699
700        ldifLine = readLDIFRecordKeyValuePair(record, pair, true);
701        if (pair.key == null || !"deleteoldrdn".equals(toLowerCase(pair.key))) {
702            final LocalizableMessage message =
703                    ERR_LDIF_MALFORMED_DELETE_OLD_RDN.get(record.lineNumber, entryDN.toString(),
704                            ldifLine);
705            throw DecodeException.error(message);
706        }
707
708        final String delStr = toLowerCase(pair.value);
709        if ("false".equals(delStr) || "no".equals(delStr) || "0".equals(delStr)) {
710            modifyDNRequest.setDeleteOldRDN(false);
711        } else if ("true".equals(delStr) || "yes".equals(delStr) || "1".equals(delStr)) {
712            modifyDNRequest.setDeleteOldRDN(true);
713        } else {
714            final LocalizableMessage message =
715                    ERR_LDIF_MALFORMED_DELETE_OLD_RDN.get(record.lineNumber, entryDN.toString(),
716                            pair.value);
717            throw DecodeException.error(message);
718        }
719
720        // Parse the newsuperior if present.
721        if (record.iterator.hasNext()) {
722            ldifLine = readLDIFRecordKeyValuePair(record, pair, true);
723            if (pair.key == null || !"newsuperior".equals(toLowerCase(pair.key)) || "".equals(pair.value)) {
724                throw DecodeException.error(
725                        ERR_LDIF_MALFORMED_NEW_SUPERIOR.get(record.lineNumber, entryDN, ldifLine));
726            }
727
728            try {
729                final DN newSuperiorDN = DN.valueOf(pair.value, schema);
730                modifyDNRequest.setNewSuperior(newSuperiorDN.toString());
731            } catch (final LocalizedIllegalArgumentException e) {
732                final LocalizableMessage message =
733                        ERR_LDIF_MALFORMED_NEW_SUPERIOR.get(record.lineNumber, entryDN.toString(),
734                                pair.value);
735                throw DecodeException.error(message);
736            }
737        }
738
739        return modifyDNRequest;
740    }
741}