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