From c657fa9ccde2ac75a4ee0cddbbb4e28bc7e904c0 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Tue, 21 Jun 2011 20:52:08 +0000
Subject: [PATCH] Partial fix for OPENDJ-205: Add support for rejecting and skipping records to the LDIF readers

---
 opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/ConnectionFactoryTestCase.java                        |    2 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleImpl.java |    2 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/Schema.java                                    |   50 +
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFStream.java                               |    4 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java                           |  238 ++++++----
 opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/schema/SchemaCompatOptionsTest.java                   |    4 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchemaImpl.java                            |    2 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties                                  |   84 ++-
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIFEntryReader.java                                  |  105 ++-
 opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldif/LDIFChangeRecordReaderTestCase.java                   |  471 +++++++++++++++++++++
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/EntryWriter.java                                      |    2 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/ChangeRecordWriter.java                               |    2 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFReader.java                               |  135 +++--
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/RejectedRecordListener.java                           |  141 ++++++
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeDescription.java                             |   29 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFWriter.java                               |    4 
 16 files changed, 1,018 insertions(+), 257 deletions(-)

diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeDescription.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeDescription.java
index 875da3b..c7e21d1 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeDescription.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeDescription.java
@@ -784,6 +784,9 @@
    * @param attributeDescription
    *          The LDAP string representation of an attribute description.
    * @return The parsed attribute description.
+   * @throws UnknownSchemaElementException
+   *           If {@code attributeDescription} contains an attribute type which
+   *           is not contained in the default schema and the schema is strict.
    * @throws LocalizedIllegalArgumentException
    *           If {@code attributeDescription} is not a valid LDAP string
    *           representation of an attribute description.
@@ -791,7 +794,8 @@
    *           If {@code attributeDescription} was {@code null}.
    */
   public static AttributeDescription valueOf(final String attributeDescription)
-      throws LocalizedIllegalArgumentException, NullPointerException
+      throws UnknownSchemaElementException, LocalizedIllegalArgumentException,
+      NullPointerException
   {
     return valueOf(attributeDescription, Schema.getDefaultSchema());
   }
@@ -807,17 +811,20 @@
    * @param schema
    *          The schema to use when parsing the attribute description.
    * @return The parsed attribute description.
+   * @throws UnknownSchemaElementException
+   *           If {@code attributeDescription} contains an attribute type which
+   *           is not contained in the provided schema and the schema is strict.
    * @throws LocalizedIllegalArgumentException
    *           If {@code attributeDescription} is not a valid LDAP string
    *           representation of an attribute description.
    * @throws NullPointerException
-   *           If {@code attributeDescription} or {@code schema} was {@code
-   *           null}.
+   *           If {@code attributeDescription} or {@code schema} was
+   *           {@code null}.
    */
   @SuppressWarnings("serial")
   public static AttributeDescription valueOf(final String attributeDescription,
-      final Schema schema) throws LocalizedIllegalArgumentException,
-      NullPointerException
+      final Schema schema) throws UnknownSchemaElementException,
+      LocalizedIllegalArgumentException, NullPointerException
   {
     Validator.ensureNotNull(attributeDescription, schema);
 
@@ -1005,17 +1012,7 @@
     }
 
     // Get the attribute type from the schema.
-    final AttributeType attributeType;
-    try
-    {
-      attributeType = schema.getAttributeType(oid);
-    }
-    catch (final UnknownSchemaElementException e)
-    {
-      final LocalizableMessage message = ERR_ATTRIBUTE_DESCRIPTION_TYPE_NOT_FOUND
-          .get(attributeDescription, e.getMessageObject());
-      throw new LocalizedIllegalArgumentException(message);
-    }
+    final AttributeType attributeType = schema.getAttributeType(oid);
 
     // If we're already at the end of the attribute description then it
     // does not contain any options.
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchemaImpl.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchemaImpl.java
index 939eac6..86bb032 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchemaImpl.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchemaImpl.java
@@ -91,7 +91,7 @@
     addRFC3112(builder);
     addSunProprietary(builder);
 
-    SINGLETON = builder.toSchema().nonStrict();
+    SINGLETON = builder.toSchema().asNonStrictSchema();
   }
 
 
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleImpl.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleImpl.java
index f36b605..5e55697 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleImpl.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleImpl.java
@@ -54,7 +54,7 @@
   {
     try
     {
-      DN dn = DN.valueOf(value.toString(), schema.nonStrict());
+      DN dn = DN.valueOf(value.toString(), schema.asNonStrictSchema());
       StringBuilder builder = new StringBuilder(value.length());
       return ByteString.valueOf(normalizeDN(builder, dn));
     }
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/Schema.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/Schema.java
index 1759a1e..f64b02e 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/Schema.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/Schema.java
@@ -2446,12 +2446,16 @@
 
 
   /**
-   * Indicates whether or not this schema is strict. Attribute type queries in
-   * non-strict schema always succeed: if the requested attribute type is not
-   * found then a temporary attribute type is created automatically having the
-   * Octet String syntax and associated matching rules. Strict schema, on the
-   * other hand, throw an {@link UnknownSchemaElementException} whenever an
-   * attempt is made to retrieve a non-existent attribute type.
+   * Indicates whether or not this schema is strict.
+   * <p>
+   * Attribute type queries against non-strict schema always succeed: if the
+   * requested attribute type is not found then a temporary attribute type is
+   * created automatically having the Octet String syntax and associated
+   * matching rules.
+   * <p>
+   * Strict schema, on the other hand, throw an
+   * {@link UnknownSchemaElementException} whenever an attempt is made to
+   * retrieve a non-existent attribute type.
    *
    * @return {@code true} if this schema is strict.
    */
@@ -2463,16 +2467,14 @@
 
 
   /**
-   * Returns a non-strict view of this schema. Attribute type queries in
-   * non-strict schema always succeed: if the requested attribute type is not
-   * found then a temporary attribute type is created automatically having the
-   * Octet String syntax and associated matching rules. Strict schema, on the
-   * other hand, throw an {@link UnknownSchemaElementException} whenever an
-   * attempt is made to retrieve a non-existent attribute type.
+   * Returns a non-strict view of this schema.
+   * <p>
+   * See the description of {@link #isStrict()} for more details.
    *
    * @return A non-strict view of this schema.
+   * @see Schema#isStrict()
    */
-  public Schema nonStrict()
+  public Schema asNonStrictSchema()
   {
     if (impl.isStrict())
     {
@@ -2487,6 +2489,28 @@
 
 
   /**
+   * Returns a strict view of this schema.
+   * <p>
+   * See the description of {@link #isStrict()} for more details.
+   *
+   * @return A strict view of this schema.
+   * @see Schema#isStrict()
+   */
+  public Schema asStrictSchema()
+  {
+    if (impl.isStrict())
+    {
+      return this;
+    }
+    else
+    {
+      return new Schema(((NonStrictImpl) impl).strictImpl);
+    }
+  }
+
+
+
+  /**
    * Adds the definitions of all the schema elements contained in this schema to
    * the provided subschema subentry. Any existing attributes (including schema
    * definitions) contained in the provided entry will be preserved.
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFReader.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFReader.java
index d2fb8c8..4230d08 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFReader.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFReader.java
@@ -46,6 +46,8 @@
 import org.forgerock.i18n.LocalizableMessageBuilder;
 import org.forgerock.i18n.LocalizedIllegalArgumentException;
 import org.forgerock.opendj.ldap.*;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
 
 import com.forgerock.opendj.util.Base64;
 import com.forgerock.opendj.util.Validator;
@@ -155,7 +157,7 @@
 
 
     /**
-     *{@inheritDoc}
+     * {@inheritDoc}
      */
     public String readLine() throws IOException
     {
@@ -209,7 +211,7 @@
 
 
     /**
-     *{@inheritDoc}
+     * {@inheritDoc}
      */
     public String readLine() throws IOException
     {
@@ -226,7 +228,11 @@
 
 
 
-  boolean validateSchema = true;
+  RejectedRecordListener rejectedRecordListener = RejectedRecordListener.FAIL_FAST;
+
+  Schema schema = Schema.getDefaultSchema().asNonStrictSchema();
+
+  boolean validateSchema = false;
 
   private final LDIFReaderImpl impl;
 
@@ -322,8 +328,8 @@
         {
           // The value did not have a valid base64-encoding.
           final LocalizableMessage message = ERR_LDIF_COULD_NOT_BASE64_DECODE_ATTR
-              .get(entryDN.toString(), record.lineNumber, ldifLine, e
-                  .getMessageObject());
+              .get(entryDN.toString(), record.lineNumber, ldifLine,
+                  e.getMessageObject());
           throw DecodeException.error(message);
         }
       }
@@ -345,8 +351,9 @@
         catch (final Exception e)
         {
           // The URL was malformed or had an invalid protocol.
-          final LocalizableMessage message = ERR_LDIF_INVALID_URL.get(entryDN
-              .toString(), record.lineNumber, attrName, String.valueOf(e));
+          final LocalizableMessage message = ERR_LDIF_INVALID_URL.get(
+              entryDN.toString(), record.lineNumber, attrName,
+              String.valueOf(e));
           throw DecodeException.error(message);
         }
 
@@ -370,9 +377,9 @@
         {
           // We were unable to read the contents of that URL for some
           // reason.
-          final LocalizableMessage message = ERR_LDIF_URL_IO_ERROR.get(entryDN
-              .toString(), record.lineNumber, attrName, String
-              .valueOf(contentURL), String.valueOf(e));
+          final LocalizableMessage message = ERR_LDIF_URL_IO_ERROR.get(
+              entryDN.toString(), record.lineNumber, attrName,
+              String.valueOf(contentURL), String.valueOf(e));
           throw DecodeException.error(message);
         }
         finally
@@ -583,7 +590,8 @@
 
 
   final void readLDIFRecordAttributeValue(final LDIFRecord record,
-      final String ldifLine, final Entry entry) throws DecodeException
+      final String ldifLine, final Entry entry,
+      final List<LocalizableMessage> schemaErrors) throws DecodeException
   {
     // Parse the attribute description.
     final int colonPos = parseColonPosition(record, ldifLine);
@@ -594,9 +602,25 @@
     {
       attributeDescription = AttributeDescription.valueOf(attrDescr, schema);
     }
+    catch (final UnknownSchemaElementException e)
+    {
+      final LocalizableMessage message = ERR_LDIF_UNKNOWN_ATTRIBUTE_TYPE.get(
+          record.lineNumber, entry.getName().toString(), attrDescr);
+      if (validateSchema)
+      {
+        schemaErrors.add(message);
+        return;
+      }
+      else
+      {
+        throw DecodeException.error(message);
+      }
+    }
     catch (final LocalizedIllegalArgumentException e)
     {
-      throw DecodeException.error(e.getMessageObject());
+      final LocalizableMessage message = ERR_LDIF_MALFORMED_ATTRIBUTE_NAME.get(
+          record.lineNumber, entry.getName().toString(), attrDescr);
+      throw DecodeException.error(message);
     }
 
     // Now parse the attribute value.
@@ -617,9 +641,10 @@
     {
       if (validateSchema && attributeDescription.containsOption("binary"))
       {
-        final LocalizableMessage message = ERR_LDIF_INVALID_ATTR_OPTION.get(
-            entry.getName().toString(), record.lineNumber, attrDescr);
-        throw DecodeException.error(message);
+        final LocalizableMessage message = ERR_LDIF_UNEXPECTED_BINARY_OPTION
+            .get(record.lineNumber, entry.getName().toString(), attrDescr);
+        schemaErrors.add(message);
+        return;
       }
     }
     else
@@ -636,10 +661,11 @@
         if (!attributeDescription.getAttributeType().getSyntax()
             .valueIsAcceptable(value, invalidReason))
         {
-          final LocalizableMessage message = WARN_LDIF_VALUE_VIOLATES_SYNTAX
-              .get(entry.getName().toString(), record.lineNumber, value
-                  .toString(), attrDescr, invalidReason);
-          throw DecodeException.error(message);
+          final LocalizableMessage message = WARN_LDIF_MALFORMED_ATTRIBUTE_VALUE
+              .get(record.lineNumber, entry.getName().toString(),
+                  value.toString(), attrDescr, invalidReason);
+          schemaErrors.add(message);
+          return;
         }
       }
 
@@ -654,25 +680,29 @@
         if (!attributeDescription.getAttributeType().getSyntax()
             .valueIsAcceptable(value, invalidReason))
         {
-          final LocalizableMessage message = WARN_LDIF_VALUE_VIOLATES_SYNTAX
-              .get(entry.getName().toString(), record.lineNumber, value
-                  .toString(), attrDescr, invalidReason);
-          throw DecodeException.error(message);
+          final LocalizableMessage message = WARN_LDIF_MALFORMED_ATTRIBUTE_VALUE
+              .get(record.lineNumber, entry.getName().toString(),
+                  value.toString(), attrDescr, invalidReason);
+          schemaErrors.add(message);
+          return;
         }
 
-        if (!attribute.add(value))
+        if (!attribute.add(value) && validateSchema)
         {
-          final LocalizableMessage message = WARN_LDIF_DUPLICATE_ATTR.get(entry
-              .getName().toString(), record.lineNumber, attrDescr, value
-              .toString());
-          throw DecodeException.error(message);
+          final LocalizableMessage message = WARN_LDIF_DUPLICATE_ATTRIBUTE_VALUE
+              .get(record.lineNumber, entry.getName().toString(), attrDescr,
+                  value.toString());
+          schemaErrors.add(message);
+          return;
         }
 
-        if (attributeDescription.getAttributeType().isSingleValue())
+        if (validateSchema
+            && attributeDescription.getAttributeType().isSingleValue())
         {
-          final LocalizableMessage message = ERR_LDIF_MULTIPLE_VALUES_FOR_SINGLE_VALUED_ATTR
-              .get(entry.getName().toString(), record.lineNumber, attrDescr);
-          throw DecodeException.error(message);
+          final LocalizableMessage message = ERR_LDIF_MULTI_VALUED_SINGLE_VALUED_ATTRIBUTE
+              .get(record.lineNumber, entry.getName().toString(), attrDescr);
+          schemaErrors.add(message);
+          return;
         }
       }
       else
@@ -787,15 +817,13 @@
 
   final String readLDIFRecordKeyValuePair(final LDIFRecord record,
       final KeyValuePair pair, final boolean allowBase64)
-      throws DecodeException
   {
     final String ldifLine = record.iterator.next();
     final int colonPos = ldifLine.indexOf(":");
     if (colonPos <= 0)
     {
-      final LocalizableMessage message = ERR_LDIF_NO_ATTR_NAME.get(
-          record.lineNumber, ldifLine);
-      throw DecodeException.error(message);
+      pair.key = null;
+      return ldifLine;
     }
     pair.key = ldifLine.substring(0, colonPos);
 
@@ -804,10 +832,8 @@
     final int length = ldifLine.length();
     if (colonPos == length - 1)
     {
-      // FIXME: improve error.
-      final LocalizableMessage message = LocalizableMessage
-          .raw("Malformed changetype attribute");
-      throw DecodeException.error(message);
+      pair.key = null;
+      return ldifLine;
     }
 
     if (allowBase64 && ldifLine.charAt(colonPos + 1) == ':')
@@ -826,11 +852,8 @@
       }
       catch (final LocalizedIllegalArgumentException e)
       {
-        // The value did not have a valid base64-encoding.
-        // FIXME: improve error.
-        final LocalizableMessage message = LocalizableMessage
-            .raw("Malformed base64 changetype attribute");
-        throw DecodeException.error(message);
+        pair.key = null;
+        return ldifLine;
       }
     }
     else
@@ -852,19 +875,29 @@
 
 
 
-  final void rejectLDIFRecord(final LDIFRecord record,
+  final void handleMalformedRecord(final LDIFRecord record,
       final LocalizableMessage message) throws DecodeException
   {
-    // FIXME: not yet implemented.
-    throw DecodeException.error(message);
+    rejectedRecordListener.handleMalformedRecord(record.lineNumber,
+        record.ldifLines, message);
   }
 
 
 
-  final void skipLDIFRecord(final LDIFRecord record,
-      final LocalizableMessage message)
+  final void handleSchemaValidationFailure(final LDIFRecord record,
+      final List<LocalizableMessage> messages) throws DecodeException
   {
-    // FIXME: not yet implemented.
+    rejectedRecordListener.handleSchemaValidationFailure(record.lineNumber,
+        record.ldifLines, messages);
+  }
+
+
+
+  final void handleSkippedRecord(final LDIFRecord record,
+      final LocalizableMessage message) throws DecodeException
+  {
+    rejectedRecordListener.handleSkippedRecord(record.lineNumber,
+        record.ldifLines, message);
   }
 
 
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFStream.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFStream.java
index 4ae83d8..6c7abe9 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFStream.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFStream.java
@@ -23,6 +23,7 @@
  *
  *
  *      Copyright 2009 Sun Microsystems, Inc.
+ *      Portions copyright 2011 ForgeRock AS
  */
 
 package org.forgerock.opendj.ldif;
@@ -39,7 +40,6 @@
 import org.forgerock.opendj.ldap.Entry;
 import org.forgerock.opendj.ldap.Matcher;
 import org.forgerock.opendj.ldap.schema.AttributeType;
-import org.forgerock.opendj.ldap.schema.Schema;
 
 
 
@@ -57,8 +57,6 @@
 
   final Set<AttributeDescription> includeAttributes = new HashSet<AttributeDescription>();
 
-  Schema schema = Schema.getDefaultSchema();
-
   final Set<DN> includeBranches = new HashSet<DN>();
 
   final Set<DN> excludeBranches = new HashSet<DN>();
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFWriter.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFWriter.java
index 16de300..0a866a7 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFWriter.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/AbstractLDIFWriter.java
@@ -23,6 +23,7 @@
  *
  *
  *      Copyright 2009 Sun Microsystems, Inc.
+ *      Portions copyright 2011 ForgeRock AS
  */
 
 package org.forgerock.opendj.ldif;
@@ -39,6 +40,7 @@
 import org.forgerock.opendj.ldap.ByteSequence;
 import org.forgerock.opendj.ldap.ByteString;
 import org.forgerock.opendj.ldap.controls.Control;
+import org.forgerock.opendj.ldap.schema.Schema;
 
 import com.forgerock.opendj.util.Base64;
 import com.forgerock.opendj.util.Validator;
@@ -253,6 +255,8 @@
 
   private final StringBuilder builder = new StringBuilder(80);
 
+  Schema schema = Schema.getDefaultSchema();
+
 
 
   /**
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/ChangeRecordWriter.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/ChangeRecordWriter.java
index a23a36a..0dfac63 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/ChangeRecordWriter.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/ChangeRecordWriter.java
@@ -43,8 +43,6 @@
 /**
  * An interface for writing change records to a data source, typically an LDIF
  * file.
- * <p>
- * TODO: FilteredChangeRecordWriter
  */
 public interface ChangeRecordWriter extends Closeable, Flushable
 {
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/EntryWriter.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/EntryWriter.java
index afd60c0..b24b4df 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/EntryWriter.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/EntryWriter.java
@@ -39,8 +39,6 @@
 
 /**
  * An interface for writing entries to a data source, typically an LDIF file.
- * <p>
- * TODO: FilteredChangeRecordWriter
  */
 public interface EntryWriter extends Closeable, Flushable
 {
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java
index 0d0e7ed..df050fa 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIFChangeRecordReader.java
@@ -35,10 +35,7 @@
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.NoSuchElementException;
+import java.util.*;
 
 import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.i18n.LocalizedIllegalArgumentException;
@@ -47,6 +44,7 @@
 import org.forgerock.opendj.ldap.requests.ModifyRequest;
 import org.forgerock.opendj.ldap.requests.Requests;
 import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException;
 
 import com.forgerock.opendj.util.Validator;
 
@@ -335,6 +333,25 @@
 
 
   /**
+   * Sets the rejected record listener which should be notified whenever a
+   * record is skipped, malformed, or fails schema validation.
+   * <p>
+   * By default the {@link RejectedRecordListener#FAIL_FAST} listener is used.
+   *
+   * @param listener
+   *          The rejected record listener.
+   * @return A reference to this {@code LDIFChangeRecordReader}.
+   */
+  public LDIFChangeRecordReader setRejectedRecordListener(
+      final RejectedRecordListener listener)
+  {
+    this.rejectedRecordListener = listener;
+    return this;
+  }
+
+
+
+  /**
    * Sets the schema which should be used for decoding change records that are
    * read from LDIF. The default schema is used if no other is specified.
    *
@@ -346,7 +363,8 @@
   public LDIFChangeRecordReader setSchema(final Schema schema)
   {
     Validator.ensureNotNull(schema);
-    this.schema = schema;
+    this.schema = validateSchema ? schema.asStrictSchema() : schema
+        .asNonStrictSchema();
     return this;
   }
 
@@ -354,7 +372,12 @@
 
   /**
    * Specifies whether or not schema validation should be performed for change
-   * records that are read from LDIF. The default is {@code true} .
+   * records that are read from LDIF.
+   * <p>
+   * When enabled, the LDIF reader will implicitly use a strict schema so that
+   * unrecognized attribute types will be detected.
+   * <p>
+   * Schema validation is disabled by default.
    *
    * @param validateSchema
    *          {@code true} if schema validation should be performed, or
@@ -364,6 +387,8 @@
   public LDIFChangeRecordReader setValidateSchema(final boolean validateSchema)
   {
     this.validateSchema = validateSchema;
+    this.schema = validateSchema ? schema.asStrictSchema() : schema
+        .asNonStrictSchema();
     return this;
   }
 
@@ -374,104 +399,92 @@
   {
     while (nextChangeRecord == null)
     {
-      LDIFRecord record = null;
-
       // Read the set of lines that make up the next entry.
-      record = readLDIFRecord();
+      final LDIFRecord record = readLDIFRecord();
       if (record == null)
       {
         nextChangeRecord = EOF;
         break;
       }
 
-      // Read the DN of the entry and see if it is one that should be
-      // included in the import.
-      DN entryDN;
       try
       {
-        entryDN = readLDIFRecordDN(record);
+        // Read the DN of the entry and see if it is one that should be
+        // included in the import.
+        final DN entryDN = readLDIFRecordDN(record);
         if (entryDN == null)
         {
           // Skip version record.
           continue;
         }
-      }
-      catch (final DecodeException e)
-      {
-        rejectLDIFRecord(record, e.getMessageObject());
-        continue;
-      }
 
-      // Skip if branch containing the entry DN is excluded.
-      if (isBranchExcluded(entryDN))
-      {
-        final LocalizableMessage message = LocalizableMessage
-            .raw("Skipping entry because it is in excluded branch");
-        skipLDIFRecord(record, message);
-        continue;
-      }
+        // Skip if branch containing the entry DN is excluded.
+        if (isBranchExcluded(entryDN))
+        {
+          final LocalizableMessage message = ERR_LDIF_CHANGE_EXCLUDED_BY_DN
+              .get(record.lineNumber, entryDN.toString());
+          handleSkippedRecord(record, message);
+          continue;
+        }
 
-      ChangeRecord changeRecord = null;
-      try
-      {
         if (!record.iterator.hasNext())
         {
-          // FIXME: improve error.
-          final LocalizableMessage message = LocalizableMessage
-              .raw("Missing changetype");
+          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);
+        }
 
         if (!toLowerCase(pair.key).equals("changetype"))
         {
           // Default to add change record.
-          changeRecord = parseAddChangeRecordEntry(entryDN, ldifLine, record);
+          nextChangeRecord = parseAddChangeRecordEntry(entryDN, ldifLine,
+              record);
         }
         else
         {
           final String changeType = toLowerCase(pair.value);
           if (changeType.equals("add"))
           {
-            changeRecord = parseAddChangeRecordEntry(entryDN, null, record);
+            nextChangeRecord = parseAddChangeRecordEntry(entryDN, null, record);
           }
           else if (changeType.equals("delete"))
           {
-            changeRecord = parseDeleteChangeRecordEntry(entryDN, record);
+            nextChangeRecord = parseDeleteChangeRecordEntry(entryDN, record);
           }
           else if (changeType.equals("modify"))
           {
-            changeRecord = parseModifyChangeRecordEntry(entryDN, record);
+            nextChangeRecord = parseModifyChangeRecordEntry(entryDN, record);
           }
           else if (changeType.equals("modrdn"))
           {
-            changeRecord = parseModifyDNChangeRecordEntry(entryDN, record);
+            nextChangeRecord = parseModifyDNChangeRecordEntry(entryDN, record);
           }
           else if (changeType.equals("moddn"))
           {
-            changeRecord = parseModifyDNChangeRecordEntry(entryDN, record);
+            nextChangeRecord = parseModifyDNChangeRecordEntry(entryDN, record);
           }
           else
           {
-            // FIXME: improve error.
-            final LocalizableMessage message = ERR_LDIF_INVALID_CHANGETYPE_ATTRIBUTE
-                .get(pair.value, "add, delete, modify, moddn, modrdn");
+            final LocalizableMessage message = ERR_LDIF_BAD_CHANGE_TYPE.get(
+                record.lineNumber, entryDN.toString(), pair.value);
             throw DecodeException.error(message);
           }
         }
       }
       catch (final DecodeException e)
       {
-        rejectLDIFRecord(record, e.getMessageObject());
+        handleMalformedRecord(record, e.getMessageObject());
         continue;
       }
-
-      if (changeRecord != null)
-      {
-        nextChangeRecord = changeRecord;
-      }
     }
     return nextChangeRecord;
   }
@@ -484,17 +497,24 @@
   {
     // Use an Entry for the AttributeSequence.
     final Entry entry = new LinkedHashMapEntry(entryDN);
+    final List<LocalizableMessage> schemaErrors = new LinkedList<LocalizableMessage>();
 
     if (lastLDIFLine != null)
     {
       // This line was read when looking for the change type.
-      readLDIFRecordAttributeValue(record, lastLDIFLine, entry);
+      readLDIFRecordAttributeValue(record, lastLDIFLine, entry, schemaErrors);
     }
 
     while (record.iterator.hasNext())
     {
       final String ldifLine = record.iterator.next();
-      readLDIFRecordAttributeValue(record, ldifLine, entry);
+      readLDIFRecordAttributeValue(record, ldifLine, entry, schemaErrors);
+    }
+
+    if (!schemaErrors.isEmpty())
+    {
+      handleSchemaValidationFailure(record, schemaErrors);
+      return null;
     }
 
     return Requests.newAddRequest(entry);
@@ -507,9 +527,8 @@
   {
     if (record.iterator.hasNext())
     {
-      // FIXME: include line number in error.
-      final LocalizableMessage message = ERR_LDIF_INVALID_DELETE_ATTRIBUTES
-          .get();
+      final LocalizableMessage message = ERR_LDIF_MALFORMED_DELETE.get(
+          record.lineNumber, entryDN.toString());
       throw DecodeException.error(message);
     }
 
@@ -525,10 +544,18 @@
 
     final KeyValuePair pair = new KeyValuePair();
     final List<ByteString> attributeValues = new ArrayList<ByteString>();
+    final List<LocalizableMessage> schemaErrors = new LinkedList<LocalizableMessage>();
 
     while (record.iterator.hasNext())
     {
-      readLDIFRecordKeyValuePair(record, pair, false);
+      String ldifLine = readLDIFRecordKeyValuePair(record, pair, false);
+      if (pair.key == null)
+      {
+        final LocalizableMessage message = ERR_LDIF_MALFORMED_MODIFICATION_TYPE
+            .get(record.lineNumber, entryDN.toString(), ldifLine);
+        throw DecodeException.error(message);
+      }
+
       final String changeType = toLowerCase(pair.key);
 
       ModificationType modType;
@@ -550,9 +577,8 @@
       }
       else
       {
-        // FIXME: improve error.
-        final LocalizableMessage message = ERR_LDIF_INVALID_MODIFY_ATTRIBUTE
-            .get(pair.key, "add, delete, replace, increment");
+        final LocalizableMessage message = ERR_LDIF_BAD_MODIFICATION_TYPE.get(
+            record.lineNumber, entryDN.toString(), pair.key);
         throw DecodeException.error(message);
       }
 
@@ -561,9 +587,25 @@
       {
         attributeDescription = AttributeDescription.valueOf(pair.value, schema);
       }
+      catch (final UnknownSchemaElementException e)
+      {
+        final LocalizableMessage message = ERR_LDIF_UNKNOWN_ATTRIBUTE_TYPE.get(
+            record.lineNumber, entryDN.toString(), pair.value);
+        if (validateSchema)
+        {
+          schemaErrors.add(message);
+          continue;
+        }
+        else
+        {
+          throw DecodeException.error(message);
+        }
+      }
       catch (final LocalizedIllegalArgumentException e)
       {
-        throw DecodeException.error(e.getMessageObject());
+        final LocalizableMessage message = ERR_LDIF_MALFORMED_ATTRIBUTE_NAME
+            .get(record.lineNumber, entryDN.toString(), pair.value);
+        throw DecodeException.error(message);
       }
 
       // Skip the attribute if requested before performing any schema
@@ -580,9 +622,10 @@
       {
         if (validateSchema && attributeDescription.containsOption("binary"))
         {
-          final LocalizableMessage message = ERR_LDIF_INVALID_ATTR_OPTION.get(
-              entryDN.toString(), record.lineNumber, pair.value);
-          throw DecodeException.error(message);
+          final LocalizableMessage message = ERR_LDIF_UNEXPECTED_BINARY_OPTION
+              .get(record.lineNumber, entryDN.toString(), pair.value);
+          schemaErrors.add(message);
+          continue;
         }
       }
       else
@@ -595,7 +638,7 @@
       attributeValues.clear();
       while (record.iterator.hasNext())
       {
-        final String ldifLine = record.iterator.next();
+        ldifLine = record.iterator.next();
         if (ldifLine.equals("-"))
         {
           break;
@@ -613,7 +656,11 @@
         }
         catch (final LocalizedIllegalArgumentException e)
         {
-          throw DecodeException.error(e.getMessageObject());
+          // No need to catch schema exception here because it implies that the
+          // attribute name is wrong.
+          final LocalizableMessage message = ERR_LDIF_MALFORMED_ATTRIBUTE_NAME
+              .get(record.lineNumber, entryDN.toString(), attrDescr);
+          throw DecodeException.error(message);
         }
 
         // Ensure that the binary option is present if required.
@@ -625,9 +672,9 @@
 
         if (!attributeDescription2.equals(attributeDescription))
         {
-          // TODO: include line number.
-          final LocalizableMessage message = ERR_LDIF_INVALID_CHANGERECORD_ATTRIBUTE
-              .get(attributeDescription2.toString(),
+          final LocalizableMessage message = ERR_LDIF_ATTRIBUTE_NAME_MISMATCH
+              .get(record.lineNumber, entryDN.toString(),
+                  attributeDescription2.toString(),
                   attributeDescription.toString());
           throw DecodeException.error(message);
         }
@@ -642,6 +689,12 @@
       modifyRequest.addModification(change);
     }
 
+    if (!schemaErrors.isEmpty())
+    {
+      handleSchemaValidationFailure(record, schemaErrors);
+      return null;
+    }
+
     return modifyRequest;
   }
 
@@ -655,19 +708,18 @@
     // Parse the newrdn.
     if (!record.iterator.hasNext())
     {
-      // TODO: include line number.
-      final LocalizableMessage message = ERR_LDIF_NO_MOD_DN_ATTRIBUTES.get();
+      final LocalizableMessage message = ERR_LDIF_NO_NEW_RDN.get(
+          record.lineNumber, entryDN.toString());
       throw DecodeException.error(message);
     }
 
     final KeyValuePair pair = new KeyValuePair();
-    String ldifLine = record.iterator.next();
-    readLDIFRecordKeyValuePair(record, pair, true);
-    if (!toLowerCase(pair.key).equals("newrdn"))
+    String ldifLine = readLDIFRecordKeyValuePair(record, pair, true);
+
+    if (pair.key == null || !toLowerCase(pair.key).equals("newrdn"))
     {
-      // FIXME: improve error.
-      final LocalizableMessage message = LocalizableMessage
-          .raw("Missing newrdn");
+      final LocalizableMessage message = ERR_LDIF_MALFORMED_NEW_RDN.get(
+          record.lineNumber, entryDN.toString(), ldifLine);
       throw DecodeException.error(message);
     }
 
@@ -678,27 +730,24 @@
     }
     catch (final LocalizedIllegalArgumentException e)
     {
-      final LocalizableMessage message = ERR_LDIF_INVALID_DN.get(
-          record.lineNumber, ldifLine, e.getMessageObject());
+      final LocalizableMessage message = ERR_LDIF_MALFORMED_NEW_RDN.get(
+          record.lineNumber, entryDN.toString(), pair.value);
       throw DecodeException.error(message);
     }
 
     // Parse the deleteoldrdn.
     if (!record.iterator.hasNext())
     {
-      // TODO: include line number.
-      final LocalizableMessage message = ERR_LDIF_NO_DELETE_OLDRDN_ATTRIBUTE
-          .get();
+      final LocalizableMessage message = ERR_LDIF_NO_DELETE_OLD_RDN.get(
+          record.lineNumber, entryDN.toString());
       throw DecodeException.error(message);
     }
 
-    ldifLine = record.iterator.next();
-    readLDIFRecordKeyValuePair(record, pair, true);
-    if (!toLowerCase(pair.key).equals("deleteoldrdn"))
+    ldifLine = readLDIFRecordKeyValuePair(record, pair, true);
+    if (pair.key == null || !toLowerCase(pair.key).equals("deleteoldrdn"))
     {
-      // FIXME: improve error.
-      final LocalizableMessage message = LocalizableMessage
-          .raw("Missing deleteoldrdn");
+      final LocalizableMessage message = ERR_LDIF_MALFORMED_DELETE_OLD_RDN.get(
+          record.lineNumber, entryDN.toString(), ldifLine);
       throw DecodeException.error(message);
     }
 
@@ -714,22 +763,19 @@
     }
     else
     {
-      // FIXME: improve error.
-      final LocalizableMessage message = ERR_LDIF_INVALID_DELETE_OLDRDN_ATTRIBUTE
-          .get(pair.value);
+      final LocalizableMessage message = ERR_LDIF_MALFORMED_DELETE_OLD_RDN.get(
+          record.lineNumber, entryDN.toString(), pair.value);
       throw DecodeException.error(message);
     }
 
     // Parse the newsuperior if present.
     if (record.iterator.hasNext())
     {
-      ldifLine = record.iterator.next();
-      readLDIFRecordKeyValuePair(record, pair, true);
-      if (!toLowerCase(pair.key).equals("newsuperior"))
+      ldifLine = readLDIFRecordKeyValuePair(record, pair, true);
+      if (pair.key == null || !toLowerCase(pair.key).equals("newsuperior"))
       {
-        // FIXME: improve error.
-        final LocalizableMessage message = LocalizableMessage
-            .raw("Missing newsuperior");
+        final LocalizableMessage message = ERR_LDIF_MALFORMED_NEW_SUPERIOR.get(
+            record.lineNumber, entryDN.toString(), ldifLine);
         throw DecodeException.error(message);
       }
 
@@ -740,8 +786,8 @@
       }
       catch (final LocalizedIllegalArgumentException e)
       {
-        final LocalizableMessage message = ERR_LDIF_INVALID_DN.get(
-            record.lineNumber, ldifLine, e.getMessageObject());
+        final LocalizableMessage message = ERR_LDIF_MALFORMED_NEW_SUPERIOR.get(
+            record.lineNumber, entryDN.toString(), pair.value);
         throw DecodeException.error(message);
       }
     }
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIFEntryReader.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIFEntryReader.java
index 6a891c1..5f13925 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIFEntryReader.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/LDIFEntryReader.java
@@ -23,6 +23,7 @@
  *
  *
  *      Copyright 2009-2010 Sun Microsystems, Inc.
+ *      Portions copyright 2011 ForgeRock AS
  */
 
 package org.forgerock.opendj.ldif;
@@ -34,6 +35,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Arrays;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.NoSuchElementException;
 
@@ -361,6 +363,25 @@
 
 
   /**
+   * Sets the rejected record listener which should be notified whenever a
+   * record is skipped, malformed, or fails schema validation.
+   * <p>
+   * By default the {@link RejectedRecordListener#FAIL_FAST} listener is used.
+   *
+   * @param listener
+   *          The rejected record listener.
+   * @return A reference to this {@code LDIFEntryReader}.
+   */
+  public LDIFEntryReader setRejectedRecordListener(
+      final RejectedRecordListener listener)
+  {
+    this.rejectedRecordListener = listener;
+    return this;
+  }
+
+
+
+  /**
    * Sets the schema which should be used for decoding entries that are read
    * from LDIF. The default schema is used if no other is specified.
    *
@@ -372,7 +393,8 @@
   public LDIFEntryReader setSchema(final Schema schema)
   {
     Validator.ensureNotNull(schema);
-    this.schema = schema;
+    this.schema = validateSchema ? schema.asStrictSchema() : schema
+        .asNonStrictSchema();
     return this;
   }
 
@@ -380,7 +402,12 @@
 
   /**
    * Specifies whether or not schema validation should be performed for entries
-   * that are read from LDIF. The default is {@code true}.
+   * that are read from LDIF.
+   * <p>
+   * When enabled, this LDIF reader will implicitly use a strict schema so that
+   * unrecognized attribute types will be detected.
+   * <p>
+   * Schema validation is disabled by default.
    *
    * @param validateSchema
    *          {@code true} if schema validation should be performed, or
@@ -390,6 +417,8 @@
   public LDIFEntryReader setValidateSchema(final boolean validateSchema)
   {
     this.validateSchema = validateSchema;
+    this.schema = validateSchema ? schema.asStrictSchema() : schema
+        .asNonStrictSchema();
     return this;
   }
 
@@ -399,69 +428,65 @@
   {
     while (nextEntry == null)
     {
-      LDIFRecord record = null;
-
       // Read the set of lines that make up the next entry.
-      record = readLDIFRecord();
+      final LDIFRecord record = readLDIFRecord();
       if (record == null)
       {
         nextEntry = EOF;
         break;
       }
 
-      // Read the DN of the entry and see if it is one that should be
-      // included in the import.
-      DN entryDN;
       try
       {
-        entryDN = readLDIFRecordDN(record);
+        // Read the DN of the entry and see if it is one that should be
+        // included in the import.
+        final DN entryDN = readLDIFRecordDN(record);
         if (entryDN == null)
         {
           // Skip version record.
           continue;
         }
-      }
-      catch (final DecodeException e)
-      {
-        rejectLDIFRecord(record, e.getMessageObject());
-        continue;
-      }
 
-      // Skip if branch containing the entry DN is excluded.
-      if (isBranchExcluded(entryDN))
-      {
-        final LocalizableMessage message = LocalizableMessage
-            .raw("Skipping entry because it is in excluded branch");
-        skipLDIFRecord(record, message);
-        continue;
-      }
+        // Skip if branch containing the entry DN is excluded.
+        if (isBranchExcluded(entryDN))
+        {
+          final LocalizableMessage message = ERR_LDIF_ENTRY_EXCLUDED_BY_DN.get(
+              record.lineNumber, entryDN.toString());
+          handleSkippedRecord(record, message);
+          continue;
+        }
 
-      // Use an Entry for the AttributeSequence.
-      final Entry entry = new LinkedHashMapEntry(entryDN);
-      try
-      {
+        // Use an Entry for the AttributeSequence.
+        final Entry entry = new LinkedHashMapEntry(entryDN);
+        final List<LocalizableMessage> schemaErrors = new LinkedList<LocalizableMessage>();
         while (record.iterator.hasNext())
         {
           final String ldifLine = record.iterator.next();
-          readLDIFRecordAttributeValue(record, ldifLine, entry);
+          readLDIFRecordAttributeValue(record, ldifLine, entry, schemaErrors);
         }
+
+        // Skip if the entry is excluded by any filters.
+        if (isEntryExcluded(entry))
+        {
+          final LocalizableMessage message = ERR_LDIF_ENTRY_EXCLUDED_BY_FILTER
+              .get(record.lineNumber, entryDN.toString());
+          handleSkippedRecord(record, message);
+          continue;
+        }
+
+        if (!schemaErrors.isEmpty())
+        {
+          handleSchemaValidationFailure(record, schemaErrors);
+          continue;
+        }
+
+        nextEntry = entry;
       }
       catch (final DecodeException e)
       {
-        rejectLDIFRecord(record, e.getMessageObject());
+        handleMalformedRecord(record, e.getMessageObject());
         continue;
       }
-
-      // Skip if the entry is excluded by any filters.
-      if (isEntryExcluded(entry))
-      {
-        final LocalizableMessage message = LocalizableMessage
-            .raw("Skipping entry due to exclusing filters");
-        skipLDIFRecord(record, message);
-        continue;
-      }
-
-      nextEntry = entry;
     }
 
     return nextEntry;
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/RejectedRecordListener.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/RejectedRecordListener.java
new file mode 100644
index 0000000..79e167e
--- /dev/null
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldif/RejectedRecordListener.java
@@ -0,0 +1,141 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at
+ * trunk/opendj3/legal-notices/CDDLv1_0.txt
+ * or http://forgerock.org/license/CDDLv1.0.html.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at
+ * trunk/opendj3/legal-notices/CDDLv1_0.txt.  If applicable,
+ * add the following below this CDDL HEADER, with the fields enclosed
+ * by brackets "[]" replaced with your own identifying information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *      Copyright 2011 ForgeRock AS
+ */
+
+package org.forgerock.opendj.ldif;
+
+
+
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.DecodeException;
+
+
+
+/**
+ * A listener interface which is notified whenever records are skipped,
+ * malformed, or fail schema validation.
+ */
+public interface RejectedRecordListener
+{
+  /**
+   * The default handler which ignores skipped records but which terminates
+   * processing by throwing a {@code DecodeException} as soon as a record is
+   * found to be malformed or rejected due to a schema validation failure.
+   */
+  public static final RejectedRecordListener FAIL_FAST = new RejectedRecordListener()
+  {
+
+    @Override
+    public void handleMalformedRecord(final long lineNumber,
+        final List<String> ldifRecord, final LocalizableMessage reason)
+        throws DecodeException
+    {
+      // Fail fast.
+      throw DecodeException.error(reason);
+    }
+
+
+
+    @Override
+    public void handleSchemaValidationFailure(final long lineNumber,
+        final List<String> ldifRecord, final List<LocalizableMessage> reasons)
+        throws DecodeException
+    {
+      // Fail fast - just use first message.
+      throw DecodeException.error(reasons.get(0));
+    }
+
+
+
+    @Override
+    public void handleSkippedRecord(final long lineNumber,
+        final List<String> ldifRecord, final LocalizableMessage reason)
+        throws DecodeException
+    {
+      // Ignore skipped records.
+    }
+  };
+
+
+
+  /**
+   * Invoked when a record was rejected because it was malformed in some way and
+   * could not be decoded.
+   *
+   * @param lineNumber
+   *          The line number within the source location in which the malformed
+   *          record is located, if known, otherwise {@code -1}.
+   * @param ldifRecord
+   *          An LDIF representation of the malformed record.
+   * @param reason
+   *          The reason why the record is malformed.
+   * @throws DecodeException
+   *           If processing should terminate.
+   */
+  void handleMalformedRecord(long lineNumber, List<String> ldifRecord,
+      LocalizableMessage reason) throws DecodeException;
+
+
+
+  /**
+   * Invoked when a record was rejected because it does not conform to the
+   * schema and schema validation is enabled.
+   *
+   * @param lineNumber
+   *          The line number within the source location in which the rejected
+   *          record is located, if known, otherwise {@code -1}.
+   * @param ldifRecord
+   *          An LDIF representation of the record which failed schema
+   *          validation.
+   * @param reasons
+   *          The reasons why the record failed schema validation.
+   * @throws DecodeException
+   *           If processing should terminate.
+   */
+  void handleSchemaValidationFailure(long lineNumber, List<String> ldifRecord,
+      List<LocalizableMessage> reasons) throws DecodeException;
+
+
+
+  /**
+   * Invoked when a record was skipped because it did not match filter criteria
+   * defined by the reader.
+   *
+   * @param lineNumber
+   *          The line number within the source location in which the skipped
+   *          record is located, if known, otherwise {@code -1}.
+   * @param ldifRecord
+   *          An LDIF representation of the skipped record.
+   * @param reason
+   *          The reason why the record was skipped.
+   * @throws DecodeException
+   *           If processing should terminate.
+   */
+  void handleSkippedRecord(long lineNumber, List<String> ldifRecord,
+      LocalizableMessage reason) throws DecodeException;
+
+}
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties
index 160c302..6c6c410 100755
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties
@@ -477,8 +477,6 @@
  failed and will be removed from the schema: %s
 ERR_NO_SUBSCHEMA_SUBENTRY_ATTR=The entry %s does not include \
  a subschemaSubentry attribute 
-ERR_ATTRIBUTE_DESCRIPTION_TYPE_NOT_FOUND=The attribute description "%s" \
- could not be parsed due to the following reason: %s
 ERR_RDN_TYPE_NOT_FOUND=The RDN "%s" could not be parsed due to the \
  following reason: %s
 ERR_DN_TYPE_NOT_FOUND=The DN "%s" could not be parsed due to the \
@@ -880,34 +878,12 @@
 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
-WARN_LDIF_DUPLICATE_ATTR=Entry %s read from LDIF starting at line %d \
- includes a duplicate attribute %s with value %s.  The second occurrence of \
- that attribute value has been skipped
-ERR_LDIF_MULTIPLE_VALUES_FOR_SINGLE_VALUED_ATTR=Entry %s starting at \
- line %d includes multiple values for single-valued attribute %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 \
  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 \
  occurred while trying to read that content:  %s
-ERR_LDIF_INVALID_DELETE_ATTRIBUTES=Error in the LDIF change record \
- entry. Invalid attributes specified for the delete operation
-ERR_LDIF_NO_MOD_DN_ATTRIBUTES=Error in the LDIF change record \
- entry. No attributes specified for the mod DN operation
-ERR_LDIF_NO_DELETE_OLDRDN_ATTRIBUTE=Error in the LDIF change record \
- entry. No delete old RDN attribute specified for the mod DN operation
-ERR_LDIF_INVALID_DELETE_OLDRDN_ATTRIBUTE=Error in the LDIF change \
- record entry. Invalid value "%s" for the delete old RDN attribute specified \
- for the mod DN operation
-ERR_LDIF_INVALID_CHANGERECORD_ATTRIBUTE=Error in the LDIF change \
- record entry. Invalid attribute "%s" specified. Expecting attribute "%s"
-ERR_LDIF_INVALID_MODIFY_ATTRIBUTE=Error in the LDIF change record \
- entry. Invalid attribute "%s" specified. Expecting one of the following \
- attributes "%s"
-ERR_LDIF_INVALID_CHANGETYPE_ATTRIBUTE=Error in the LDIF change \
- record entry. Invalid value "%s" for the changetype specified. Expecting one \
- of the following values "%s"
 ERR_LDAPURL_NO_SCHEME=The provided string "%s" cannot be decoded \
  as an LDAP URL because it does not contain a protocol scheme
 ERR_LDAPURL_BAD_SCHEME=The provided string "%s" cannot be decoded \
@@ -930,11 +906,6 @@
 ERR_LDAPURL_INVALID_HEX_BYTE=The provided URL component "%s" could \
  not be decoded because the character at byte %d was not a valid hexadecimal \
  digit
-WARN_LDIF_VALUE_VIOLATES_SYNTAX=Entry %s read from LDIF starting at \
- line %d includes value "%s" for attribute %s that is invalid according to the \
- associated syntax:  %s
-ERR_LDIF_INVALID_ATTR_OPTION=Unable to parse LDIF entry %s starting \
- at line %d because it has an invalid binary option for attribute %s
 ERR_INVALID_ESCAPE_CHAR=The value %s cannot be decoded because %c \
  is not a valid escape character
 WARN_READ_LDIF_RECORD_NO_CHANGE_RECORD_FOUND=The provided LDIF \
@@ -1303,3 +1274,58 @@
  the optional attribute "%s" which is not defined in the schema
 WARN_ATTR_SYNTAX_NOT_IMPLEMENTED1=The "%s" syntax with OID %s is not \
  implemented. It will be substituted by the default syntax with OID %s
+ERR_LDIF_ENTRY_EXCLUDED_BY_DN=Skipping LDIF entry starting at line %d with \
+ distinguished name "%s" because it is within an excluded branch
+ERR_LDIF_ENTRY_EXCLUDED_BY_FILTER=Skipping LDIF entry starting at line %d with \
+ distinguished name "%s" because it does not meet the filter criteria
+ERR_LDIF_CHANGE_EXCLUDED_BY_DN=Skipping LDIF change record starting at line %d \
+ with distinguished name "%s" because it is within an excluded branch
+ERR_LDIF_NO_CHANGE_TYPE=Unable to parse LDIF change record starting at line %d \
+ with distinguished name "%s" because there was no change type
+ERR_LDIF_MALFORMED_CHANGE_TYPE=Unable to parse LDIF change record starting at line %d \
+ with distinguished name "%s" because it contained a malformed changetype \
+ "%s"
+ERR_LDIF_BAD_CHANGE_TYPE=Unable to parse LDIF change record starting at line %d \
+ with distinguished name "%s" because it contained an unrecognized changetype \
+ "%s". The changetype must be one of add, delete, modify, modrdn, or moddn
+ERR_LDIF_MALFORMED_DELETE=Unable to parse LDIF delete record starting at line %d \
+ with distinguished name "%s" because it contained additional lines after the \
+ changetype when none were expected
+ERR_LDIF_BAD_MODIFICATION_TYPE=Unable to parse LDIF modify record starting at line %d \
+ with distinguished name "%s" because it contained an unrecognized modification type \
+ "%s". The modification type must be one of add, delete, replace, or increment
+ERR_LDIF_MALFORMED_MODIFICATION_TYPE=Unable to parse LDIF modify record starting at line %d \
+ with distinguished name "%s" because it contained a malformed modification type \
+ "%s"
+ERR_LDIF_UNKNOWN_ATTRIBUTE_TYPE=Rejecting the LDIF change record \
+ starting at line %d with distinguished name "%s" because it contains an \
+ unrecognized attribute type "%s"
+ERR_LDIF_MALFORMED_ATTRIBUTE_NAME=Unable to parse LDIF change record \
+ starting at line %d with distinguished name "%s" because it contained a \
+ malformed attribute description "%s"
+ERR_LDIF_UNEXPECTED_BINARY_OPTION=Unable to parse LDIF change record starting \
+ at line %d with distinguished name "%s" because it has an unexpected binary \
+ option for attribute %s
+ERR_LDIF_ATTRIBUTE_NAME_MISMATCH=Unable to parse LDIF change record \
+ starting at line %d with distinguished name "%s" because it contained a \
+ an unexpected attribute "%s" when "%s" was expected
+WARN_LDIF_MALFORMED_ATTRIBUTE_VALUE=Rejecting the LDIF change record \
+ starting at line %d with distinguished name "%s" because it includes the \
+ value "%s" for attribute "%s" which is invalid according to the syntax: %s
+WARN_LDIF_DUPLICATE_ATTRIBUTE_VALUE=Rejecting the LDIF change record \
+ starting at line %d with distinguished name "%s" because it includes a \
+ duplicate attribute "%s" with value "%s"
+ERR_LDIF_MULTI_VALUED_SINGLE_VALUED_ATTRIBUTE=Rejecting the LDIF change record \
+ starting at line %d with distinguished name "%s" because it includes multiple \
+ values for single-valued attribute "%s"
+ERR_LDIF_NO_NEW_RDN=Unable to parse LDIF modify DN record starting at line %d \
+ with distinguished name "%s" because there was no new RDN
+ERR_LDIF_MALFORMED_NEW_RDN=Unable to parse LDIF modify DN record starting at line %d \
+ with distinguished name "%s" because it contained a malformed new RDN "%s"
+ERR_LDIF_NO_DELETE_OLD_RDN=Unable to parse LDIF modify DN record starting at line %d \
+ with distinguished name "%s" because there was no deleteoldrdn field
+ERR_LDIF_MALFORMED_DELETE_OLD_RDN=Unable to parse LDIF modify DN record starting at line %d \
+ with distinguished name "%s" because it contained a malformed deleteoldrdn "%s"
+ERR_LDIF_MALFORMED_NEW_SUPERIOR=Unable to parse LDIF modify DN record starting at line %d \
+ with distinguished name "%s" because it contained a malformed newsuperior "%s"
+
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/ConnectionFactoryTestCase.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/ConnectionFactoryTestCase.java
index fa053f4..0ba30b8 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/ConnectionFactoryTestCase.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/ConnectionFactoryTestCase.java
@@ -345,7 +345,7 @@
                   + "caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch "
                   + "SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )",
               false);
-      final Schema testSchema = builder.toSchema().nonStrict();
+      final Schema testSchema = builder.toSchema().asNonStrictSchema();
       assertThat(testSchema.getWarnings()).isEmpty();
       Schema.setDefaultSchema(testSchema);
 
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/schema/SchemaCompatOptionsTest.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/schema/SchemaCompatOptionsTest.java
index 22cbc8e..69d9e7d 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/schema/SchemaCompatOptionsTest.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/schema/SchemaCompatOptionsTest.java
@@ -87,7 +87,7 @@
     SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema())
         .setSchemaCompatOptions(SchemaCompatOptions.defaultOptions()
             .allowMalformedNamesAndOptions(allowIllegalCharacters));
-    Schema schema = builder.toSchema().nonStrict();
+    Schema schema = builder.toSchema().asNonStrictSchema();
     AttributeDescription.valueOf(atd, schema);
   }
 
@@ -136,7 +136,7 @@
     SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema())
         .setSchemaCompatOptions(SchemaCompatOptions.defaultOptions()
             .allowMalformedNamesAndOptions(allowIllegalCharacters));
-    Schema schema = builder.toSchema().nonStrict();
+    Schema schema = builder.toSchema().asNonStrictSchema();
     AttributeDescription.valueOf(atd, schema);
   }
 
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldif/LDIFChangeRecordReaderTestCase.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldif/LDIFChangeRecordReaderTestCase.java
new file mode 100644
index 0000000..cfbce2f
--- /dev/null
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldif/LDIFChangeRecordReaderTestCase.java
@@ -0,0 +1,471 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at
+ * trunk/opendj3/legal-notices/CDDLv1_0.txt
+ * or http://forgerock.org/license/CDDLv1.0.html.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at
+ * trunk/opendj3/legal-notices/CDDLv1_0.txt.  If applicable,
+ * add the following below this CDDL HEADER, with the fields enclosed
+ * by brackets "[]" replaced with your own identifying information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Copyright 2011 ForgeRock AS
+ */
+
+package org.forgerock.opendj.ldif;
+
+
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Arrays;
+import java.util.Iterator;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.*;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.testng.annotations.Test;
+
+
+
+/**
+ * This class tests the LDIFChangeRecordReader functionality.
+ */
+public final class LDIFChangeRecordReaderTestCase extends LDIFTestCase
+{
+  /**
+   * Tests reading a valid add change record with a changetype.
+   *
+   * @throws Exception
+   *           if an unexpected error occurred.
+   */
+  @Test
+  public void testReadAddRecordWithChangeType() throws Exception
+  {
+    // @formatter:off
+    LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+        "dn: dc=example,dc=com",
+        "changetype: add",
+        "objectClass: top",
+        "objectClass: domainComponent",
+        "dc: example"
+        );
+    // @formatter:on
+
+    assertThat(reader.hasNext()).isTrue();
+    ChangeRecord record = reader.readChangeRecord();
+    assertThat(record).isInstanceOf(AddRequest.class);
+    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);
+  }
+
+
+
+  /**
+   * Tests reading a valid add change record without a changetype.
+   *
+   * @throws Exception
+   *           if an unexpected error occurred.
+   */
+  @Test
+  public void testReadAddRecordWithoutChangeType() throws Exception
+  {
+    // @formatter:off
+    LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+        "dn: dc=example,dc=com",
+        "objectClass: top",
+        "objectClass: domainComponent",
+        "dc: example"
+        );
+    // @formatter:on
+
+    assertThat(reader.hasNext()).isTrue();
+    ChangeRecord record = reader.readChangeRecord();
+    assertThat(record).isInstanceOf(AddRequest.class);
+    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);
+  }
+
+
+
+  /**
+   * Tests reading a valid modify change record.
+   *
+   * @throws Exception
+   *           if an unexpected error occurred.
+   */
+  @Test
+  public void testReadModifyRecord() throws Exception
+  {
+    // @formatter:off
+    LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+        "dn: dc=example,dc=com",
+        "changetype: modify",
+        "add: description",
+        "-",
+        "add: description",
+        "description: value1",
+        "-",
+        "add: description",
+        "description: value1",
+        "description: value2",
+        "-",
+        "delete: description",
+        "-",
+        "delete: description",
+        "description: value1",
+        "-",
+        "delete: description",
+        "description: value1",
+        "description: value2",
+        "-",
+        "replace: description",
+        "-",
+        "replace: description",
+        "description: value1",
+        "-",
+        "replace: description",
+        "description: value1",
+        "description: value2",
+        "-",
+        "increment: description",
+        "description: 1"
+        );
+    // @formatter:on
+
+    assertThat(reader.hasNext()).isTrue();
+    ChangeRecord record = reader.readChangeRecord();
+    assertThat(record).isInstanceOf(ModifyRequest.class);
+    ModifyRequest modifyRequest = (ModifyRequest) record;
+    assertThat((Object) modifyRequest.getName()).isEqualTo(
+        DN.valueOf("dc=example,dc=com"));
+
+    Iterator<Modification> changes = modifyRequest.getModifications()
+        .iterator();
+    Modification modification;
+
+    modification = changes.next();
+    assertThat(modification.getModificationType()).isEqualTo(
+        ModificationType.ADD);
+    assertThat(modification.getAttribute()).isEqualTo(
+        new LinkedAttribute("description"));
+
+    modification = changes.next();
+    assertThat(modification.getModificationType()).isEqualTo(
+        ModificationType.ADD);
+    assertThat(modification.getAttribute()).isEqualTo(
+        new LinkedAttribute("description", "value1"));
+
+    modification = changes.next();
+    assertThat(modification.getModificationType()).isEqualTo(
+        ModificationType.ADD);
+    assertThat(modification.getAttribute()).isEqualTo(
+        new LinkedAttribute("description", "value1", "value2"));
+
+    modification = changes.next();
+    assertThat(modification.getModificationType()).isEqualTo(
+        ModificationType.DELETE);
+    assertThat(modification.getAttribute()).isEqualTo(
+        new LinkedAttribute("description"));
+
+    modification = changes.next();
+    assertThat(modification.getModificationType()).isEqualTo(
+        ModificationType.DELETE);
+    assertThat(modification.getAttribute()).isEqualTo(
+        new LinkedAttribute("description", "value1"));
+
+    modification = changes.next();
+    assertThat(modification.getModificationType()).isEqualTo(
+        ModificationType.DELETE);
+    assertThat(modification.getAttribute()).isEqualTo(
+        new LinkedAttribute("description", "value1", "value2"));
+
+    modification = changes.next();
+    assertThat(modification.getModificationType()).isEqualTo(
+        ModificationType.REPLACE);
+    assertThat(modification.getAttribute()).isEqualTo(
+        new LinkedAttribute("description"));
+
+    modification = changes.next();
+    assertThat(modification.getModificationType()).isEqualTo(
+        ModificationType.REPLACE);
+    assertThat(modification.getAttribute()).isEqualTo(
+        new LinkedAttribute("description", "value1"));
+
+    modification = changes.next();
+    assertThat(modification.getModificationType()).isEqualTo(
+        ModificationType.REPLACE);
+    assertThat(modification.getAttribute()).isEqualTo(
+        new LinkedAttribute("description", "value1", "value2"));
+
+    modification = changes.next();
+    assertThat(modification.getModificationType()).isEqualTo(
+        ModificationType.INCREMENT);
+    assertThat(modification.getAttribute()).isEqualTo(
+        new LinkedAttribute("description", "1"));
+
+    assertThat(changes.hasNext()).isFalse();
+  }
+
+
+
+  /**
+   * Tests reading a valid delete change record.
+   *
+   * @throws Exception
+   *           if an unexpected error occurred.
+   */
+  @Test
+  public void testReadDeleteRecord() throws Exception
+  {
+    // @formatter:off
+    LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+        "dn: dc=example,dc=com",
+        "changetype: delete"
+        );
+    // @formatter:on
+
+    assertThat(reader.hasNext()).isTrue();
+    ChangeRecord record = reader.readChangeRecord();
+    assertThat(record).isInstanceOf(DeleteRequest.class);
+    DeleteRequest deleteRequest = (DeleteRequest) record;
+    assertThat((Object) deleteRequest.getName()).isEqualTo(
+        DN.valueOf("dc=example,dc=com"));
+  }
+
+
+
+  /**
+   * Tests reading a valid moddn change record.
+   *
+   * @throws Exception
+   *           if an unexpected error occurred.
+   */
+  @Test
+  public void testReadModdnRecordWithoutNewSuperior() throws Exception
+  {
+    // @formatter:off
+    LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+        "dn: dc=example,dc=com",
+        "changetype: moddn",
+        "newrdn: dc=eggsample",
+        "deleteoldrdn: true"
+        );
+    // @formatter:on
+
+    assertThat(reader.hasNext()).isTrue();
+    ChangeRecord record = reader.readChangeRecord();
+    assertThat(record).isInstanceOf(ModifyDNRequest.class);
+    ModifyDNRequest modifyDNRequest = (ModifyDNRequest) record;
+    assertThat((Object) modifyDNRequest.getName()).isEqualTo(
+        DN.valueOf("dc=example,dc=com"));
+    assertThat((Object) modifyDNRequest.getNewRDN()).isEqualTo(
+        RDN.valueOf("dc=eggsample"));
+    assertThat(modifyDNRequest.isDeleteOldRDN()).isTrue();
+    assertThat(modifyDNRequest.getNewSuperior()).isNull();
+  }
+
+
+
+  /**
+   * Tests reading a valid moddn change record.
+   *
+   * @throws Exception
+   *           if an unexpected error occurred.
+   */
+  @Test
+  public void testReadModdnRecordWithNewSuperior() throws Exception
+  {
+    // @formatter:off
+    LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+        "dn: dc=example,dc=com",
+        "changetype: moddn",
+        "newrdn: dc=eggsample",
+        "deleteoldrdn: true",
+        "newsuperior: dc=org"
+        );
+    // @formatter:on
+
+    assertThat(reader.hasNext()).isTrue();
+    ChangeRecord record = reader.readChangeRecord();
+    assertThat(record).isInstanceOf(ModifyDNRequest.class);
+    ModifyDNRequest modifyDNRequest = (ModifyDNRequest) record;
+    assertThat((Object) modifyDNRequest.getName()).isEqualTo(
+        DN.valueOf("dc=example,dc=com"));
+    assertThat((Object) modifyDNRequest.getNewRDN()).isEqualTo(
+        RDN.valueOf("dc=eggsample"));
+    assertThat(modifyDNRequest.isDeleteOldRDN()).isTrue();
+    assertThat((Object) modifyDNRequest.getNewSuperior()).isEqualTo(
+        DN.valueOf("dc=org"));
+  }
+
+
+
+  /**
+   * Tests reading a malformed record invokes the rejected record listener.
+   *
+   * @throws Exception
+   *           if an unexpected error occurred.
+   */
+  @Test
+  public void testRejectedRecordListenerMalformedFirstRecord() throws Exception
+  {
+    RejectedRecordListener listener = mock(RejectedRecordListener.class);
+
+    // @formatter:off
+    LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+        "dn: baddn",
+        "changetype: add",
+        "objectClass: top",
+        "objectClass: domainComponent",
+        "dc: example"
+        ).setRejectedRecordListener(listener);
+    // @formatter:on
+
+    assertThat(reader.hasNext()).isFalse();
+
+    verify(listener).handleMalformedRecord(
+        eq(1L),
+        eq(Arrays.asList("dn: baddn", "changetype: add", "objectClass: top",
+            "objectClass: domainComponent", "dc: example")),
+        any(LocalizableMessage.class));
+  }
+
+
+
+  /**
+   * Tests reading a malformed record invokes the rejected record listener.
+   *
+   * @throws Exception
+   *           if an unexpected error occurred.
+   */
+  @Test
+  public void testRejectedRecordListenerMalformedSecondRecord()
+      throws Exception
+  {
+    RejectedRecordListener listener = mock(RejectedRecordListener.class);
+
+    // @formatter:off
+    LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+        "dn: dc=example,dc=com",
+        "changetype: add",
+        "objectClass: top",
+        "objectClass: domainComponent",
+        "dc: example",
+        "",
+        "dn: baddn",
+        "changetype: add",
+        "objectClass: top",
+        "objectClass: domainComponent",
+        "dc: example"
+        ).setRejectedRecordListener(listener);
+    // @formatter:on
+
+    reader.readChangeRecord(); // Skip good record.
+    assertThat(reader.hasNext()).isFalse();
+
+    verify(listener).handleMalformedRecord(
+        eq(7L),
+        eq(Arrays.asList("dn: baddn", "changetype: add", "objectClass: top",
+            "objectClass: domainComponent", "dc: example")),
+        any(LocalizableMessage.class));
+  }
+
+
+
+  /**
+   * Tests reading a skipped record invokes the rejected record listener.
+   *
+   * @throws Exception
+   *           if an unexpected error occurred.
+   */
+  @Test
+  public void testRejectedRecordListenerSkipsRecord() throws Exception
+  {
+    RejectedRecordListener listener = mock(RejectedRecordListener.class);
+
+    // @formatter:off
+    LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+        "dn: dc=example,dc=com",
+        "changetype: add",
+        "objectClass: top",
+        "objectClass: domainComponent",
+        "dc: example"
+        ).setRejectedRecordListener(listener).setExcludeBranch(DN.valueOf("dc=com"));
+    // @formatter:on
+
+    assertThat(reader.hasNext()).isFalse();
+
+    verify(listener)
+        .handleSkippedRecord(
+            eq(1L),
+            eq(Arrays.asList("dn: dc=example,dc=com", "changetype: add",
+                "objectClass: top", "objectClass: domainComponent",
+                "dc: example")), any(LocalizableMessage.class));
+  }
+
+
+
+  /**
+   * Tests reading a record which does not conform to the schema invokes the
+   * rejected record listener.
+   *
+   * @throws Exception
+   *           if an unexpected error occurred.
+   */
+  @Test
+  public void testRejectedRecordListenerRejectsBadSchemaRecord()
+      throws Exception
+  {
+    RejectedRecordListener listener = mock(RejectedRecordListener.class);
+
+    // @formatter:off
+    LDIFChangeRecordReader reader = new LDIFChangeRecordReader(
+        "dn: dc=example,dc=com",
+        "changetype: add",
+        "objectClass: top",
+        "objectClass: domainComponent",
+        "dc: example",
+        "xxx: unknown attribute"
+        ).setRejectedRecordListener(listener).setValidateSchema(true);
+    // @formatter:on
+
+    assertThat(reader.hasNext()).isFalse();
+
+    verify(listener).handleSchemaValidationFailure(
+        eq(1L),
+        eq(Arrays.asList("dn: dc=example,dc=com", "changetype: add",
+            "objectClass: top", "objectClass: domainComponent", "dc: example",
+            "xxx: unknown attribute")), anyListOf(LocalizableMessage.class));
+  }
+}

--
Gitblit v1.10.0