mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Violette Roche-Montane
05.30.2012 2cf107f6e38ef84851a81c40c74feb66370ed098
Fix OPENDJ-185 : Add support for controls in SDK LDIFReader
4 files modified
749 ■■■■ changed files
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/ChangeRecord.java 61 ●●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java 76 ●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties 11 ●●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldif/LDIFChangeRecordReaderTestCase.java 601 ●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/ChangeRecord.java
@@ -22,11 +22,20 @@
 *
 *
 *      Copyright 2009 Sun Microsystems, Inc.
 *      Portions copyright 2012 ForgeRock AS
 */
package org.forgerock.opendj.ldif;
import java.util.List;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.DecodeException;
import org.forgerock.opendj.ldap.DecodeOptions;
import org.forgerock.opendj.ldap.controls.Control;
import org.forgerock.opendj.ldap.controls.ControlDecoder;
import org.forgerock.opendj.ldap.requests.Request;
/**
 * A request to modify the content of the Directory in some way. A change record
@@ -38,7 +47,7 @@
 * <li>An {@code ModifyDN} operation.
 * </ul>
 */
public interface ChangeRecord {
public interface ChangeRecord extends Request {
    /**
     * Applies a {@code ChangeRecordVisitor} to this {@code ChangeRecord}.
     *
@@ -62,4 +71,54 @@
     * @return The distinguished name of the entry being modified.
     */
    DN getName();
    /**
     * Sets the distinguished name of the entry to be updated. The server shall
     * not perform any alias dereferencing in determining the object to be
     * updated.
     *
     * @param dn
     *            The the distinguished name of the entry to be updated.
     * @return This change record.
     * @throws UnsupportedOperationException
     *             If this change record does not permit the distinguished name
     *             to be set.
     * @throws NullPointerException
     *             If {@code dn} was {@code null}.
     */
    ChangeRecord setName(DN dn);
    /**
     * Sets the distinguished name of the entry to be updated. The server shall
     * not perform any alias dereferencing in determining the object to be
     * updated.
     *
     * @param dn
     *            The the distinguished name of the entry to be updated.
     * @return This change record.
     * @throws LocalizedIllegalArgumentException
     *             If {@code dn} could not be decoded using the default schema.
     * @throws UnsupportedOperationException
     *             If this change record does not permit the distinguished name
     *             to be set.
     * @throws NullPointerException
     *             If {@code dn} was {@code null}.
     */
    ChangeRecord setName(String dn);
    /**
     * {@inheritDoc}
     */
    Request addControl(Control control);
    /**
     * {@inheritDoc}
     */
    <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
            throws DecodeException;
    /**
     * {@inheritDoc}
     */
    List<Control> getControls();
}
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java
@@ -37,6 +37,8 @@
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizableMessageBuilder;
@@ -51,6 +53,8 @@
import org.forgerock.opendj.ldap.Modification;
import org.forgerock.opendj.ldap.ModificationType;
import org.forgerock.opendj.ldap.RDN;
import org.forgerock.opendj.ldap.controls.Control;
import org.forgerock.opendj.ldap.controls.GenericControl;
import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.Requests;
@@ -64,7 +68,6 @@
/**
 * An LDIF change record reader reads change records using the LDAP Data
 * Interchange Format (LDIF) from a user defined source.
 *
 * <p>
 * The following example reads changes from LDIF, and writes the changes to the
 * directory server.
@@ -139,6 +142,9 @@
    // Poison used to indicate end of LDIF.
    private static final ChangeRecord EOF = Requests.newAddRequest(DN.rootDN());
    private static final Pattern CONTROL_REGEX =
            Pattern.compile("^\\s*(\\d+(.\\d+)*)(\\s+((true)|(false)))?\\s*(:(:)?\\s*?\\S+)?\\s*$");
    /**
     * Creates a new LDIF change record reader whose source is the provided
     * input stream.
@@ -389,19 +395,34 @@
                    continue;
                }
                if (!record.iterator.hasNext()) {
                    final LocalizableMessage message =
                            ERR_LDIF_NO_CHANGE_TYPE.get(record.lineNumber, entryDN.toString());
                    throw DecodeException.error(message);
                }
                KeyValuePair pair;
                String ldifLine;
                List<Control> controls = null;
                while (true) {
                    if (!record.iterator.hasNext()) {
                        final LocalizableMessage message =
                                ERR_LDIF_NO_CHANGE_TYPE.get(record.lineNumber, entryDN.toString());
                        throw DecodeException.error(message);
                    }
                final KeyValuePair pair = new KeyValuePair();
                final String ldifLine = readLDIFRecordKeyValuePair(record, pair, false);
                if (pair.key == null) {
                    final LocalizableMessage message =
                            ERR_LDIF_MALFORMED_CHANGE_TYPE.get(record.lineNumber, entryDN
                                    .toString(), ldifLine);
                    throw DecodeException.error(message);
                    pair = new KeyValuePair();
                    ldifLine = readLDIFRecordKeyValuePair(record, pair, false);
                    if (pair.key == null) {
                        final LocalizableMessage message =
                                ERR_LDIF_MALFORMED_CHANGE_TYPE.get(record.lineNumber, entryDN
                                        .toString(), ldifLine);
                        throw DecodeException.error(message);
                    }
                    if (!toLowerCase(pair.key).equals("control")) {
                        break;
                    }
                    if (controls == null) {
                        controls = new LinkedList<Control>();
                    }
                    controls.add(parseControl(entryDN, record, ldifLine, pair.value));
                }
                if (!toLowerCase(pair.key).equals("changetype")) {
@@ -425,6 +446,13 @@
                                        pair.value);
                        throw DecodeException.error(message);
                    }
                    // Add the controls to the record.
                    if (controls != null) {
                        for (Control control : controls) {
                            nextChangeRecord.addControl(control);
                        }
                    }
                }
            } catch (final DecodeException e) {
                handleMalformedRecord(record, e.getMessageObject());
@@ -434,6 +462,27 @@
        return nextChangeRecord;
    }
    private Control parseControl(DN entryDN, LDIFRecord record, String ldifLine, String value)
            throws DecodeException {
        Matcher matcher = CONTROL_REGEX.matcher(value);
        if (!matcher.matches()) {
            final LocalizableMessage message =
                    ERR_LDIF_MALFORMED_CONTROL.get(record.lineNumber, entryDN.toString(), ldifLine);
            throw DecodeException.error(message);
        }
        String oid = matcher.group(1);
        boolean isCritical = matcher.group(5) != null;
        String controlValueString = matcher.group(7);
        ByteString controlValue = null;
        if (controlValueString != null) {
            controlValue = parseSingleValue(record, ldifLine, entryDN,  ldifLine.indexOf(':', 8), oid);
        }
        return GenericControl.newControl(oid, isCritical, controlValue);
    }
    private ChangeRecord parseAddChangeRecordEntry(final DN entryDN, final String lastLDIFLine,
            final LDIFRecord record) throws DecodeException {
        // Use an Entry for the AttributeSequence.
@@ -467,7 +516,6 @@
        if (!schemaErrors.isEmpty()) {
            handleSchemaValidationWarning(record, schemaErrors);
        }
        return Requests.newAddRequest(entry);
    }
opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties
@@ -22,7 +22,7 @@
#
#
#      Copyright 2010 Sun Microsystems, Inc.
#      Portions copyright 2011 ForgeRock AS
#      Portions copyright 2011-2012 ForgeRock AS
#
ERR_ATTR_SYNTAX_UNKNOWN_APPROXIMATE_MATCHING_RULE=Unable to retrieve \
 approximate matching rule %s used as the default for the %s attribute syntax. \
@@ -875,12 +875,12 @@
 line "%s":  %s
ERR_LDIF_COULD_NOT_BASE64_DECODE_ATTR=Unable to parse LDIF entry %s \
 starting at line %d because it was not possible to base64-decode the \
 attribute on line "%s":  %s
 value on line "%s":  %s
ERR_LDIF_INVALID_URL=Unable to parse LDIF entry %s starting at line \
 %d because the value of attribute %s was to be read from a URL but the URL \
 %d because the value %s was to be read from a URL but the URL \
 was invalid:  %s
ERR_LDIF_URL_IO_ERROR=Unable to parse LDIF entry %s starting at line \
 %d because the value of attribute %s was to be read from URL %s but an error \
 %d because the value %s was to be read from URL %s but an error \
 occurred while trying to read that content:  %s
ERR_LDAPURL_NO_SCHEME=The provided string "%s" cannot be decoded \
 as an LDAP URL because it does not contain a protocol scheme
@@ -1402,3 +1402,6 @@
 integer
FUNCTIONS_TO_LONG_FAIL=The provided value "%s" could not be parsed as an \
 long
ERR_LDIF_MALFORMED_CONTROL=Unable to parse LDIF change record starting at line %d \
 with distinguished name "%s" because it contained a malformed control \
 "%s"
opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldif/LDIFChangeRecordReaderTestCase.java
@@ -279,6 +279,7 @@
                new LDIFChangeRecordReader(getStandardLDIFChangeRecord());
        reader.setExcludeAttribute(null);
        reader.close();
    }
    /**
@@ -350,6 +351,7 @@
                new LDIFChangeRecordReader("version: 1",
                        "dn: uid=scarter,ou=People,dc=example,dc=com");
        reader.setIncludeAttribute(null);
        reader.close();
    }
    /**
@@ -409,6 +411,7 @@
                new LDIFChangeRecordReader("version: 1",
                        "dn: uid=scarter,ou=People,dc=example,dc=com");
        reader.setIncludeBranch(null);
        reader.close();
    }
    /**
@@ -456,6 +459,40 @@
    }
    /**
     * LDIFChangeRecordReader setSchemaValidationPolicy. Validate the Change
     * Record depending of the selected policy. ChangeRecord is here NOT allowed
     * because it contains a uid attribute which is not allowed by the
     * SchemaValidationPolicy.
     *
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testSetSchemaValidationPolicyDefaultRejectsEntry() throws Exception {
        // @formatter:off
        String[] strChangeRecord = {
            "dn: uid=user.0,ou=People,dc=example,dc=com",
            "changetype: add",
            "sn: Carter",
            "objectClass: person",
            "objectClass: top",
            "cn: Aaccf Amar",
            "sn: Amar",
            "uid: user.0"
        };
        // @formatter:on
        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(strChangeRecord);
        reader.setSchema(Schema.getDefaultSchema());
        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
        try {
            reader.readChangeRecord();
        } finally {
            reader.close();
        }
    }
    /**
     * SetExcludeBranch doesn't allow null.
     *
     * @throws Exception
@@ -467,6 +504,7 @@
                new LDIFChangeRecordReader("version: 1",
                        "dn: uid=scarter,ou=People,dc=example,dc=com");
        reader.setExcludeBranch(null);
        reader.close();
    }
    /**
@@ -481,6 +519,7 @@
                new LDIFChangeRecordReader("version: 1",
                        "dn: uid=scarter,ou=People,dc=example,dc=com");
        reader.setSchema(null);
        reader.close();
    }
    /**
@@ -522,40 +561,6 @@
    }
    /**
     * LDIFChangeRecordReader setSchemaValidationPolicy. Validate the Change
     * Record depending of the selected policy. ChangeRecord is here NOT allowed
     * because it contains a uid attribute which is not allowed by the
     * SchemaValidationPolicy.
     *
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testSetSchemaValidationPolicyDefaultRejectsEntry() throws Exception {
        // @formatter:off
        String[] strChangeRecord = {
            "dn: uid=user.0,ou=People,dc=example,dc=com",
            "changetype: add",
            "sn: Carter",
            "objectClass: person",
            "objectClass: top",
            "cn: Aaccf Amar",
            "sn: Amar",
            "uid: user.0"
        };
        // @formatter:on
        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(strChangeRecord);
        reader.setSchema(Schema.getDefaultSchema());
        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
        try {
            reader.readChangeRecord();
        } finally {
            reader.close();
        }
    }
    /**
     * Test an LDIFRecordChange with an empty pair key. Must throw an exception.
     *
     * @throws Exception
@@ -621,8 +626,8 @@
        assertThat(reader.hasNext()).isTrue();
        ChangeRecord record = reader.readChangeRecord();
        assertThat(record).isInstanceOf(AddRequest.class);
        assertThat(record.getName().toString()).isEqualTo("dc=example,dc=com");
        AddRequest addRequest = (AddRequest) record;
        assertThat((Object) addRequest.getName()).isEqualTo(DN.valueOf("dc=example,dc=com"));
        assertThat(addRequest.containsAttribute("objectClass", "top", "domainComponent")).isTrue();
        assertThat(addRequest.containsAttribute("dc", "example")).isTrue();
        assertThat(addRequest.getAttributeCount()).isEqualTo(2);
@@ -650,8 +655,8 @@
        assertThat(reader.hasNext()).isTrue();
        ChangeRecord record = reader.readChangeRecord();
        assertThat(record).isInstanceOf(AddRequest.class);
        assertThat(record.getName().toString()).isEqualTo("dc=example,dc=com");
        AddRequest addRequest = (AddRequest) record;
        assertThat((Object) addRequest.getName()).isEqualTo(DN.valueOf("dc=example,dc=com"));
        assertThat(addRequest.containsAttribute("objectClass", "top", "domainComponent")).isTrue();
        assertThat(addRequest.containsAttribute("dc", "example")).isTrue();
        assertThat(addRequest.getAttributeCount()).isEqualTo(2);
@@ -705,8 +710,8 @@
        assertThat(reader.hasNext()).isTrue();
        ChangeRecord record = reader.readChangeRecord();
        assertThat(record).isInstanceOf(ModifyRequest.class);
        assertThat(record.getName().toString()).isEqualTo("dc=example,dc=com");
        ModifyRequest modifyRequest = (ModifyRequest) record;
        assertThat((Object) modifyRequest.getName()).isEqualTo(DN.valueOf("dc=example,dc=com"));
        Iterator<Modification> changes = modifyRequest.getModifications().iterator();
        Modification modification;
@@ -1040,11 +1045,15 @@
     */
    @Test()
    public void testReadFileContainingSerieOfChanges() throws Exception {
        final File file = File.createTempFile("sdk", ".png");
        final String url = file.toURI().toURL().toString();
        // @formatter:off
        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
                "version: 1",
                "# Add a new entry",
                "dn: cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com",
                "dn: cn=Fiona Jensen,",
                " ou=Marketing, dc=airius, dc=com", // data continued from previous line
                "changetype: add",
                "objectclass: top",
                "objectclass: person",
@@ -1053,7 +1062,7 @@
                "sn: Jensen",
                "uid: fiona",
                "telephonenumber: +1 408 555 1212",
                "jpegphoto:< http://www.forgerock.com/sites/default/files/forgerock_logo.png",
                "jpegphoto:< " + url,
                "",
                "# Delete an existing entry",
                "dn: cn=Robert Jensen, ou=Marketing, dc=airius, dc=com",
@@ -1106,17 +1115,23 @@
                DN.valueOf("ou=PD Accountants, ou=Product Development, dc=airius, dc=com"));
        assertThat(reader.hasNext()).isFalse();
        file.delete();
        reader.close();
    }
    /**
     * Test to read an entry containing a control. Not Yet implemented. TODO
     * OPENDJ-185 Add support for controls in SDK LDIFReader.
     * Test to read an entry containing a delete control.\ Control syntax is
     * like => control : OID (criticality - optional) :(value - optional). <br>
     * ex: control: 1.2.840.113556.1.4.805 true :cn <br>
     * control: 1.2.840.113556.1.4.805 true ::dGVzdGluZw== <br>
     * control: 1.2.840.113556.1.4.805 ::dGVzdGluZw== <br>
     * etc...
     *
     * @throws Exception
     */
    @Test(enabled = false)
    public void testLDIFCRRParseAddChangeRecordEntryWithAControl() throws Exception {
    @Test()
    public void testParseChangeRecordEntryWithDeleteControl() throws Exception {
        // @formatter:off
        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
                    "# Delete an entry. The operation will attach the LDAPv3",
@@ -1124,33 +1139,453 @@
                    "# field is \"true\" and the controlValue field is",
                    "# absent, as required by [9].",
                    "dn: ou=Product Development, dc=airius, dc=com",
                    "control: 1.2.840.113556.1.4.805 true",
                    "control: 1.2.840.113556.1.4.805 true :cn",
                    "changetype: delete"
        );
        // @formatter:on
        assertThat(reader.hasNext()).isTrue();
        reader.setSchema(Schema.getDefaultSchema());
        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
        // Read the record
        ChangeRecord record = reader.readChangeRecord();
        assertThat(record).isInstanceOf(DeleteRequest.class);
        assertThat(record.getName().toString()).isEqualTo(
                "ou=Product Development, dc=airius, dc=com");
        assertThat(record.getControls()).isNotEmpty();
        assertThat(record.getControls().get(0).getOID()).isEqualTo("1.2.840.113556.1.4.805");
        assertThat(record.getControls().get(0).getValue().toString()).isEqualTo("cn");
        reader.close();
    }
    /**
     * Test to read an record containing a add control.
     *
     * @throws Exception
     */
    @Test()
    public void testParseChangeRecordEntryWithAddControl() throws Exception {
        // @formatter:off
        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
                "dn: ou=Product Development, dc=airius, dc=com",
                "control: 1.3.6.1.1.13.1 false :cn",
                "changetype: add",
                "objectClass: top",
                "objectClass: organization",
                "o: testing"
        );
        // @formatter:on
        reader.setSchema(Schema.getDefaultSchema());
        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
        // Read the entry
        // Read the record
        ChangeRecord record = reader.readChangeRecord();
        assertThat(record).isInstanceOf(AddRequest.class);
        AddRequest request = (AddRequest) record;
        assertThat((Object) request.getName()).isEqualTo(
                DN.valueOf("ou=Product Development, dc=airius, dc=com"));
        assertThat(request.getControls()).isNotEmpty();
        assertThat(record.getName().toString()).isEqualTo(
                "ou=Product Development, dc=airius, dc=com");
        assertThat(record.getControls()).isNotEmpty();
        assertThat(record.getControls().get(0).getOID()).isEqualTo("1.3.6.1.1.13.1");
        assertThat(record.getControls().get(0).getValue().toString()).isEqualTo("cn");
        reader.close();
    }
    /**
     * Test to read an record containing a invalid control. Must throw an error.
     *
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testParseChangeRecordEntryWithAnInvalidControl() throws Exception {
        // @formatter:off
        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
                "dn: ou=Product Development, dc=airius, dc=com",
                "control: 1.2.3.4 0 value",
                "changetype: add",
                "objectClass: top",
                "objectClass: organization",
                "o: testing"
        );
        // @formatter:on
        reader.setSchema(Schema.getDefaultSchema());
        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
        // Read the record
        reader.readChangeRecord();
        reader.close();
    }
    /**
     * Test to read an record containing a add control.
     *
     * @throws Exception
     */
    @Test()
    public void testParseChangeRecordEntryWithAddControlWithoutSpacesBetweenCriticalityValue()
            throws Exception {
        // @formatter:off
        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
                "dn: ou=Product Development, dc=airius, dc=com",
                "control: 1.3.6.1.1.13.1 false:cn",
                "changetype: add",
                "objectClass: top",
                "objectClass: organization",
                "o: testing"
        );
        // @formatter:on
        reader.setSchema(Schema.getDefaultSchema());
        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
        // Read the record
        ChangeRecord record = reader.readChangeRecord();
        assertThat(record).isInstanceOf(AddRequest.class);
        assertThat(record.getName().toString()).isEqualTo(
                "ou=Product Development, dc=airius, dc=com");
        assertThat(record.getControls()).isNotEmpty();
        assertThat(record.getControls().get(0).getOID()).isEqualTo("1.3.6.1.1.13.1");
        assertThat(record.getControls().get(0).getValue().toString()).isEqualTo("cn");
        reader.close();
    }
    /**
     * Test to read an record containing a add control.
     *
     * @throws Exception
     */
    @Test()
    public void testParseChangeRecordEntryWithAddControlContainingWhiteSpaces() throws Exception {
        // @formatter:off
        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
                "dn: ou=Product Development, dc=airius, dc=com",
                "control: 1.3.6.1.1.13.1 true : sn",
                "changetype: add",
                "objectClass: top",
                "objectClass: organization",
                "o: testing"
        );
        // @formatter:on
        reader.setSchema(Schema.getDefaultSchema());
        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
        // Read the record
        ChangeRecord record = reader.readChangeRecord();
        assertThat(record).isInstanceOf(AddRequest.class);
        assertThat(record.getName().toString()).isEqualTo(
                "ou=Product Development, dc=airius, dc=com");
        assertThat(record.getControls()).isNotEmpty();
        assertThat(record.getControls().get(0).getOID()).isEqualTo("1.3.6.1.1.13.1");
        assertThat(record.getControls().get(0).getValue().toString()).isEqualTo("sn");
        reader.close();
    }
    /**
     * Record is containing a control with a base64 value.
     *
     * @throws Exception
     */
    @Test()
    public void testParseChangeRecordEntryWithAddControlContainingBase64Value() throws Exception {
        // @formatter:off
        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
            "# This record contains a base64 value",
            "dn: uid=scarter,ou=People,dc=example,dc=com",
            "control: 2.16.840.1.113730.3.4.3 true::ZGVzY3JpcHRpb24=",
            "changetype: delete"
        );
        // @formatter:on
        reader.setSchema(Schema.getDefaultSchema());
        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
        // Read the record
        ChangeRecord record = reader.readChangeRecord();
        assertThat(record).isInstanceOf(DeleteRequest.class);
        assertThat(record.getName().toString())
                .isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
        assertThat(record.getControls()).isNotEmpty();
        assertThat(record.getControls().get(0).getOID()).isEqualTo("2.16.840.1.113730.3.4.3");
        assertThat(record.getControls().get(0).getValue().toString()).isEqualTo("description");
        reader.close();
    }
    /**
     * Test to read an record containing a malformed base64 value.
     *
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testParseChangeRecordEntryWithAddControlMalformedBase64() throws Exception {
        // @formatter:off
        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
                "# This record contains a control",
                "dn: uid=scarter,ou=People,dc=example,dc=com",
                "control: 2.16.840.1.113730.3.4.3 true:: malformedBase64",
                "changetype: delete"
        );
        // @formatter:on
        reader.setSchema(Schema.getDefaultSchema());
        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
        // Read the record
        reader.readChangeRecord();
        reader.close();
    }
    /**
     * Record is containing a control with a URL.
     *
     * @throws Exception
     */
    @Test()
    public void testParseChangeRecordEntryWithAddControlContainingURL() throws Exception {
        final File file = File.createTempFile("sdk", ".png");
        final String url = file.toURI().toURL().toString();
        // @formatter:off
        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
            "# This record contains a base64 value",
            "dn: uid=scarter,ou=People,dc=example,dc=com",
            "control: 2.16.840.1.113730.3.4.3 true:<" + url,
            "changetype: delete"
        );
        // @formatter:on
        reader.setSchema(Schema.getDefaultSchema());
        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
        // Read the record
        ChangeRecord record = reader.readChangeRecord();
        assertThat(record).isInstanceOf(DeleteRequest.class);
        assertThat(record.getName().toString())
                .isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
        assertThat(record.getControls()).isNotEmpty();
        assertThat(record.getControls().get(0).getOID()).isEqualTo("2.16.840.1.113730.3.4.3");
        // URL is fine, but the content is empty ;)
        assertThat(record.getControls().get(0).getValue().toString()).isEmpty();
        file.delete();
        reader.close();
    }
    /**
     * Test to read an record containing a malformed URL.
     *
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testParseChangeRecordEntryWithAddControlMalformedURL() throws Exception {
        // @formatter:off
        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
                "# This record contains a control",
                "dn: uid=scarter,ou=People,dc=example,dc=com",
                "control: 2.16.840.1.113730.3.4.3 true:<malformedURL",
                "changetype: delete"
        );
        // @formatter:on
        reader.setSchema(Schema.getDefaultSchema());
        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
        // Read the record
        reader.readChangeRecord();
        reader.close();
    }
    /**
     * Test to read an record containing a add control without value.
     *
     * @throws Exception
     */
    @Test()
    public void testParseChangeRecordEntryWithAddControlWithoutValue() throws Exception {
        // @formatter:off
        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
                "dn: ou=Product Development, dc=airius, dc=com",
                "control: 1.3.6.1.1.13.1 false ", // space added
                "changetype: add",
                "objectClass: top",
                "objectClass: organization",
                "o: testing"
        );
        // @formatter:on
        reader.setSchema(Schema.getDefaultSchema());
        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
        // Read the record
        ChangeRecord record = reader.readChangeRecord();
        assertThat(record).isInstanceOf(AddRequest.class);
        assertThat(record.getName().toString()).isEqualTo(
                "ou=Product Development, dc=airius, dc=com");
        assertThat(record.getControls()).isNotEmpty();
        assertThat(record.getControls().get(0).getOID()).isEqualTo("1.3.6.1.1.13.1");
        assertThat(record.getControls().get(0).getValue()).isNull();
        reader.close();
    }
    /**
     * Test to read an record containing a add control without criticality.
     *
     * @throws Exception
     */
    @Test()
    public void testParseChangeRecordEntryWithAddControlWithoutCriticality() throws Exception {
        // @formatter:off
        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
                "dn: ou=Product Development, dc=airius, dc=com",
                "control:1.3.6.1.1.13.1 :description",
                "changetype: add",
                "objectClass: top",
                "objectClass: organization",
                "o: testing"
        );
        // @formatter:on
        reader.setSchema(Schema.getDefaultSchema());
        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
        // Read the record
        ChangeRecord record = reader.readChangeRecord();
        assertThat(record).isInstanceOf(AddRequest.class);
        assertThat(record.getName().toString()).isEqualTo(
                "ou=Product Development, dc=airius, dc=com");
        assertThat(record.getControls()).isNotEmpty();
        assertThat(record.getControls().get(0).getOID()).isEqualTo("1.3.6.1.1.13.1");
        assertThat(record.getControls().get(0).getValue().toString()).isEqualTo("description");
        reader.close();
    }
    /**
     * Test to read an record providing by our LDIFChangeRecordWriter.
     *
     * @throws Exception
     */
    @Test()
    public void testParseChangeRecordEntryWithAddControlProvidedByChangeRecordWriter()
            throws Exception {
        // @formatter:off
        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
                "# This record contains a control",
                "dn: uid=scarter,ou=People,dc=example,dc=com",
                "control: 2.16.840.1.113730.3.4.3 true:: MAkCAQ8BAf8BAf8=",
                "changetype: delete"
        );
        // @formatter:on
        reader.setSchema(Schema.getDefaultSchema());
        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
        // Read the record
        ChangeRecord record = reader.readChangeRecord();
        assertThat(record).isInstanceOf(DeleteRequest.class);
        assertThat(record.getName().toString())
                .isEqualTo("uid=scarter,ou=People,dc=example,dc=com");
        assertThat(record.getControls()).isNotEmpty();
        assertThat(record.getControls().get(0).getOID()).isEqualTo("2.16.840.1.113730.3.4.3");
        assertThat(record.getControls().get(0).getValue().toBase64String()).isEqualTo(
                "MAkCAQ8BAf8BAf8=");
        reader.close();
    }
    /**
     * Test to read an record containing a add control without value.
     *
     * @throws Exception
     */
    @Test()
    public void testParseChangeRecordEntryWithMultipleControls() throws Exception {
        // @formatter:off
        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
                "dn: ou=Product Development, dc=airius, dc=com",
                "control: 1.3.6.1.1.13.1 false",
                "changetype: add",
                "objectClass: top",
                "objectClass: organization",
                "o: testing",
                "",
                "# Modify an entry's relative distinguished name",
                "dn: cn=Paul Jensen, ou=Product Development, dc=airius, dc=com",
                "control: 1.3.6.1.1.13.13 false: cn", // with spaces between boolean & value
                "changetype: add",
                "objectClass: top",
                "objectClass: organization",
                "o: testing",
                "",
                "# Modify an entry's relative distinguished name",
                "dn: cn=Paula Jensen, ou=Product Development, dc=airius, dc=com",
                "control:1.3.6.1.1.13.13.16 :sn", // wihtout spaces between control and oid
                "changetype: add",
                "objectClass: top",
                "objectClass: organization",
                "o: testing"
        );
        // @formatter:on
        reader.setSchema(Schema.getDefaultSchema());
        reader.setSchemaValidationPolicy(SchemaValidationPolicy.defaultPolicy());
        // 1st
        ChangeRecord record = reader.readChangeRecord();
        assertThat(record.getName().toString()).isEqualTo(
                "ou=Product Development, dc=airius, dc=com");
        assertThat(record.getControls()).isNotEmpty();
        assertThat(record.getControls().get(0).getOID()).isEqualTo("1.3.6.1.1.13.1");
        assertThat(record.getControls().get(0).getValue()).isNull();
        //2nd
        record = reader.readChangeRecord();
        assertThat(record).isInstanceOf(AddRequest.class);
        assertThat(record.getName().toString()).isEqualTo(
                "cn=Paul Jensen, ou=Product Development, dc=airius, dc=com");
        assertThat(record.getControls()).isNotEmpty();
        assertThat(record.getControls().get(0).getOID()).isEqualTo("1.3.6.1.1.13.13");
        assertThat(record.getControls().get(0).getValue().toString()).isEqualTo("cn");
        //3rd
        record = reader.readChangeRecord();
        assertThat(record).isInstanceOf(AddRequest.class);
        assertThat(record.getName().toString()).isEqualTo(
                "cn=Paula Jensen, ou=Product Development, dc=airius, dc=com");
        assertThat(record.getControls()).isNotEmpty();
        assertThat(record.getControls().get(0).getOID()).isEqualTo("1.3.6.1.1.13.13.16");
        assertThat(record.getControls().get(0).getValue().toString()).isEqualTo("sn");
        reader.close();
    }
    /**
     * Test to read an record containing an invalid control. (pair.value is
     * null) Must throw a DecodeException.
     *
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testParseChangeRecordEntryWithAddControlPairKeyNull() throws Exception {
        // @formatter:off
        final  LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
                "dn: ou=Product Development, dc=airius, dc=com",
                "control:",
                "changetype: add",
                "objectClass: top",
                "objectClass: organization",
                "o: testing"
        );
        // @formatter:on
        try {
            reader.readChangeRecord();
        } finally {
            reader.close();
        }
    }
    /**
     * Test an add request malformed, changetype is erroneous (wrongchangetype)
     * Must throw an exception.
     *
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testLDIFCRRParseAddChangeRecordEntryLastLDIFLineIsNull() throws Exception {
    public void testParseAddChangeRecordEntryLastLDIFLineIsNull() throws Exception {
        // @formatter:off
        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
                "dn: uid=scarter,ou=People,dc=example,dc=com",
@@ -1176,7 +1611,7 @@
     *             if an unexpected error occurred.
     */
    @Test()
    public void testLDIFCRRparseDeleteChangeRecordEntry() throws Exception {
    public void testParseDeleteChangeRecordEntry() throws Exception {
        // @formatter:off
        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
            "dn: dc=example,dc=com",
@@ -1200,7 +1635,7 @@
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testLDIFCRRParseDeleteChangeRecordEntryMalformedDelete() throws Exception {
    public void testParseDeleteChangeRecordEntryMalformedDelete() throws Exception {
        // @formatter:off
        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
@@ -1213,6 +1648,7 @@
        );
        // @formatter:on
        reader.readChangeRecord();
        reader.close();
    }
    /**
@@ -1222,8 +1658,7 @@
     * @throws Exception
     */
    @Test()
    public void testLDIFCRRParseModifyChangeRecordEntryDeleteMultipleValuableAttributes()
            throws Exception {
    public void testParseModifyChangeRecordEntryDeleteMultipleValuableAttributes() throws Exception {
        // @formatter:off
        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
                "# Add new entry containing multiple attributes",
@@ -1246,6 +1681,7 @@
        assertThat((Object) req.getName()).isEqualTo(
                DN.valueOf("cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com"));
        assertThat(reader.hasNext()).isFalse();
        reader.close();
    }
    /**
@@ -1257,7 +1693,7 @@
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testLDIFCRRParseModifyChangeRecordEntryDeleteMultipleValuableAttributesMalformedLDIF()
    public void testParseModifyChangeRecordEntryDeleteMultipleValuableAttributesMalformedLDIF()
            throws Exception {
        // @formatter:off
        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
@@ -1289,7 +1725,7 @@
     * @throws Exception
     */
    @Test()
    public void testLDIFCRRParseModifyChangeRecordBEREncodingRequired() throws Exception {
    public void testParseModifyChangeRecordBEREncodingRequired() throws Exception {
        // @formatter:off
        String validcert1 = // a valid certificate but wrong can be used => no errors
                "MIICpTCCAg6gAwIBAgIJALeoA6I3ZC/cMA0GCSqGSIb3DQEBBQUAMFYxCzAJBgNV"
@@ -1348,7 +1784,7 @@
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testLDIFCRRParseModifyChangeRecordBEREncodingNotRequired() throws Exception {
    public void testParseModifyChangeRecordBEREncodingNotRequired() throws Exception {
        // @formatter:off
        final String[] strChangeRecord = {
            "version: 1",
@@ -1378,7 +1814,7 @@
     * @throws Exception
     */
    @Test()
    public void testLDIFCRRParseModifyChangeRecordEntryReplaceOk() throws Exception {
    public void testParseModifyChangeRecordEntryReplaceOk() throws Exception {
        // @formatter:off
        final String[] strChangeRecord = {
@@ -1414,7 +1850,7 @@
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testLDIFCRRParseModifyChangeRecordEntryReplaceKOPolicyReject() throws Exception {
    public void testParseModifyChangeRecordEntryReplaceKOPolicyReject() throws Exception {
        // @formatter:off
        final String[] strChangeRecord = {
@@ -1444,7 +1880,7 @@
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testLDIFCRRParseModifyChangeRecordEntryReplaceKOPolicyWarn() throws Exception {
    public void testParseModifyChangeRecordEntryReplaceKOPolicyWarn() throws Exception {
        // @formatter:off
        final String[] strChangeRecord = {
@@ -1475,7 +1911,7 @@
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testLDIFCRRParseModifyChangeRecordEntryReplaceLocalizedIllegalArgumentException()
    public void testParseModifyChangeRecordEntryReplaceLocalizedIllegalArgumentException()
            throws Exception {
        // @formatter:off
@@ -1506,7 +1942,7 @@
     * @throws Exception
     */
    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
    public void testLDIFCRRParseModifyChangeRecordEntryWithWrongChangetype() {
    public void testParseModifyChangeRecordEntryWithWrongChangetype() {
        // @formatter:off
        LDIFChangeRecordReader.valueOfLDIFChangeRecord(
            "version: 1",
@@ -1523,7 +1959,7 @@
     * @throws Exception
     */
    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
    public void testLDIFCRRParseModifyChangeRecordEntryWithNullPairKey() {
    public void testParseModifyChangeRecordEntryWithNullPairKey() {
        // @formatter:off
        LDIFChangeRecordReader.valueOfLDIFChangeRecord(
            "version: 1",
@@ -1540,7 +1976,7 @@
     * @throws Exception
     */
    @Test()
    public void testLDIFCRRParseModifyChangeRecordEntryIncrement() throws Exception {
    public void testParseModifyChangeRecordEntryIncrement() throws Exception {
        // @formatter:off
        final ChangeRecord cr = LDIFChangeRecordReader.valueOfLDIFChangeRecord(
            "version: 1",
@@ -1567,7 +2003,7 @@
     * @throws Exception
     */
    @Test()
    public void testLDIFCRRparseModifyDNChangeRecordEntryRecordBase64NewRDN() throws Exception {
    public void testParseModifyDNChangeRecordEntryRecordBase64NewRDN() throws Exception {
        // @formatter:off
        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
@@ -1596,7 +2032,7 @@
     * @throws Exception
     */
    @Test()
    public void testLDIFCRRParseModifyDNChangeRecordEntry() throws Exception {
    public void testParseModifyDNChangeRecordEntry() throws Exception {
        // @formatter:off
        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
            "version: 1",
@@ -1627,7 +2063,7 @@
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testLDIFCRRParseModifyDNChangeRecordEntryMalformedMissedNewRDN() throws Exception {
    public void testParseModifyDNChangeRecordEntryMalformedMissedNewRDN() throws Exception {
        // @formatter:off
        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
            "version: 1",
@@ -1650,7 +2086,7 @@
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testLDIFCRRParseModifyDNChangeRecordEntryKeyMalformedEmptyNewRDN() throws Exception {
    public void testParseModifyDNChangeRecordEntryKeyMalformedEmptyNewRDN() throws Exception {
        // @formatter:off
        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
@@ -1675,7 +2111,7 @@
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testLDIFCRRParseModifyDNChangeRecordEntryKeyValueMalformedRDN() throws Exception {
    public void testParseModifyDNChangeRecordEntryKeyValueMalformedRDN() throws Exception {
        // @formatter:off
        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
            "version: 1",
@@ -1699,8 +2135,7 @@
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testLDIFCRRParseModifyDNChangeRecordEntryKeyValueMalformedDeleteOldRDN()
            throws Exception {
    public void testParseModifyDNChangeRecordEntryKeyValueMalformedDeleteOldRDN() throws Exception {
        // @formatter:off
        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
@@ -1725,8 +2160,7 @@
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testLDIFCRRParseModifyDNChangeRecordEntryKeyValueMalformedDeleteOldRDN2()
            throws Exception {
    public void testParseModifyDNChangeRecordEntryKeyValueMalformedDeleteOldRDN2() throws Exception {
        // @formatter:off
        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
            "version: 1",
@@ -1749,8 +2183,7 @@
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testLDIFCRRParseModifyDNChangeRecordEntryKeyValueMalformedDeleteOldRDN3()
            throws Exception {
    public void testParseModifyDNChangeRecordEntryKeyValueMalformedDeleteOldRDN3() throws Exception {
        // @formatter:off
        final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
@@ -1774,7 +2207,7 @@
     * @throws Exception
     */
    @Test()
    public void testLDIFCRRparseModifyRecordEntryDeleteOldRDNFalse() throws Exception {
    public void testParseModifyRecordEntryDeleteOldRDNFalse() throws Exception {
        // @formatter:off
        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
@@ -1805,7 +2238,7 @@
     * @throws Exception
     */
    @Test()
    public void testLDIFCRRparseModifyRecordEntryNewSuperior() throws Exception {
    public void testParseModifyRecordEntryNewSuperior() throws Exception {
        // @formatter:off
        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
            "dn: cn=scarter,ou=People,dc=example,dc=com",
@@ -1836,7 +2269,7 @@
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testLDIFCRRparseModifyRecordEntryNewSuperiorMalformed() throws Exception {
    public void testParseModifyRecordEntryNewSuperiorMalformed() throws Exception {
        // @formatter:off
        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
            "dn: cn=scarter,ou=People,dc=example,dc=com",
@@ -1860,7 +2293,7 @@
     * @throws Exception
     */
    @Test(expectedExceptions = DecodeException.class)
    public void testLDIFCRRparseModifyRecordEntryNewSuperiorMalformed2() throws Exception {
    public void testParseModifyRecordEntryNewSuperiorMalformed2() throws Exception {
        // @formatter:off
        LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
@@ -1980,7 +2413,7 @@
     */
    @Test(expectedExceptions = NullPointerException.class)
    public void testChangeRecordReaderInpuStreamDoesntAllowNull() throws Exception {
        new LDIFChangeRecordReader((InputStream) null);
        new LDIFChangeRecordReader((InputStream) null).close();
    }
    /**
@@ -2002,6 +2435,10 @@
     */
    @Test(expectedExceptions = LocalizedIllegalArgumentException.class)
    public void testValueOfLDIFChangeRecordDoesntAllowMultipleChangeRecords() throws Exception {
        final File file = File.createTempFile("sdk", ".png");
        final String url = file.toURI().toURL().toString();
        file.delete();
        // @formatter:off
        LDIFChangeRecordReader.valueOfLDIFChangeRecord(
            "version: 1",
@@ -2015,7 +2452,7 @@
            "sn: Jensen",
            "uid: fiona",
            "telephonenumber: +1 408 555 1212",
            "jpegphotojpegphoto:< http://www.forgerock.com/sites/default/files/forgerock_logo.png",
            "jpegphotojpegphoto:< " + url,
            "",
            "# Delete an existing entry",
            "dn: cn=Robert Jensen, ou=Marketing, dc=airius, dc=com",