/*
 * 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 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 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 2006-2008 Sun Microsystems, Inc.
 *      Portions Copyright 2014-2015 ForgeRock AS
 */
package org.opends.server.util;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ModificationType;
import org.opends.server.TestCaseUtils;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.*;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/**
 * This class defines a set of tests for the
 * {@link org.opends.server.util.LDIFReader} class.
 */
public final class TestLDIFReader extends UtilTestCase {
  /** Top object class. */
  private ObjectClass OC_TOP;
  /** Person object class. */
  private ObjectClass OC_PERSON;

  /** Object class attribute type. */
  private AttributeType AT_OC;
  /** Common name attribute type. */
  private AttributeType AT_CN;
  /** Surname attribute type. */
  private AttributeType AT_SN;
  /** Description attribute type. */
  private AttributeType AT_DESCR;
  /** Telephone number attribute type. */
  private AttributeType AT_TELN;

  /** Temporary file containing an attribute value. */
  private File TEMP_FILE;

  /** Temporary file content. */
  private static final String TEMP_FILE_STRING = "hello world";
  /** Temporary file LDIF. */
  private static final String TEMP_FILE_LDIF = "dn: cn=john smith, dc=com\n"
      + "changetype: add\n" + "objectClass: top\n"
      + "objectClass: person\n" + "cn: john\n" + "sn: smith\n"
      + "description:< file:///";


  /**
   * String of valid LDIF change records.
   *
   * Take from example 6 in the LDIF RFC + a couple of additions.
   */
  private static final String VALID_LDIF = "version: 1\n"
      + "# Add a new entry\n"
      + "dn: cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com\n"
      + "changetype: add\n"
      + "objectclass: top\n"
      + "objectclass: person\n"
      + "objectclass: organizationalPerson\n"
      + "cn: Fiona Jensen\n"
      + "sn: Jensen\n"
      + "uid: fiona\n"
      + "telephonenumber: +1 408 555 1212\n"
      + "\n"
      + "# Delete an existing entry\n"
      + "dn: cn=Robert Jensen, ou=Marketing, dc=airius, dc=com\n"
      + "changetype: delete\n"
      + "\n"
      + "# Modify an entry's relative distinguished name\n"
      + "dn: cn=Paul Jensen, ou=Product Development, dc=airius, dc=com\n"
      + "changetype: modrdn\n"
      + "newrdn: cn=Paula Jensen\n"
      + "deleteoldrdn: 1\n"
      + "\n"
      + "# Rename an entry and move all of its children to a new location in\n"
      + "# the directory tree (only implemented by LDAPv3 servers).\n"
      + "dn: ou=PD Accountants, ou=Product Development, dc=airius, dc=com\n"
      + "changetype: modrdn\n"
      + "newrdn: ou=Product Development Accountants\n"
      + "deleteoldrdn: 0\n"
      + "newsuperior: ou=Accounting, dc=airius, dc=com\n"
      + "\n"
      + "# Modify an entry: add an additional value to the postaladdress\n"
      + "# attribute, completely delete the description attribute, replace\n"
      + "# the telephonenumber attribute with two values, and delete a specific\n"
      + "# value from the facsimiletelephonenumber attribute\n"
      + "dn: cn=Paula Jensen, ou=Product Development, dc=airius, dc=com\n"
      + "changetype: modify\n"
      + "add: postaladdress\n"
      + "postaladdress: 123 Anystreet $ Sunnyvale, CA $ 94086\n"
      + "-\n"
      + "delete: description\n"
      + "-\n"
      + "replace: telephonenumber\n"
      + "telephonenumber: +1 408 555 1234\n"
      + "telephonenumber: +1 408 555 5678\n"
      + "-\n"
      + "delete: facsimiletelephonenumber\n"
      + "facsimiletelephonenumber: +1 408 555 9876\n"
      + "-\n"
      + "\n"
      + "# Modify an entry: replace the postaladdress attribute with an empty\n"
      + "# set of values (which will cause the attribute to be removed), and\n"
      + "# delete the entire description attribute. Note that the first will\n"
      + "# always succeed, while the second will only succeed if at least\n"
      + "# one value for the description attribute is present.\n"
      + "dn: cn=Ingrid Jensen, ou=Product Support, dc=airius, dc=com\n"
      + "changetype: modify\n"
      + "replace: postaladdress\n"
      + "-\n"
      + "delete: description\n"
      + "-\n"
      + "\n"
      + "# Modify rootDSE.\n"
      + "dn: \n"
      + "changetype: modify\n"
      + "delete: description\n"
      + "-\n"
      + "\n"
      + "# Modify base64 DN.\n"
      + "dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz\n"
      + "# dn:: uid=<uid>,ou=<JapaneseOU>,o=Airius\n"
      + "changetype:: bW9kaWZ5\n"
      + "delete: description\n"
      + "-\n"
      + "\n";

  /**
   * Once-only initialization.
   *
   * @throws Exception
   *           If an unexpected error occurred.
   */
  @BeforeClass
  public void setUp() throws Exception {
    // This test suite depends on having the schema available, so we'll
    // start the server.
    TestCaseUtils.startServer();

    // Initialize schema bits.
    OC_TOP = DirectoryServer.getObjectClass("top");
    OC_PERSON = DirectoryServer.getObjectClass("person");

    AT_OC = DirectoryServer.getObjectClassAttributeType();
    AT_CN = DirectoryServer.getAttributeTypeOrNull("cn");
    AT_SN = DirectoryServer.getAttributeTypeOrNull("sn");
    AT_DESCR = DirectoryServer.getAttributeTypeOrNull("description");
    AT_TELN = DirectoryServer.getAttributeTypeOrNull("telephonenumber");

    // Create a temporary file containing an attribute value.
    TEMP_FILE = File.createTempFile("tmp", "txt");
    OutputStream out = null;
    try {
      out = new FileOutputStream(TEMP_FILE);
      out.write(TEMP_FILE_STRING.getBytes("UTF-8"));
    } finally {
      if (out != null) {
        out.close();
      }
    }
  }

  /**
   * Once-only tear-down.
   *
   * @throws Exception
   *           If an unexpected error occurred.
   */
  @AfterClass
  public void tearDown() throws Exception {
    if (TEMP_FILE != null) {
      TEMP_FILE.delete();
    }
  }

  /**
   * Check the initial state of an LDIF reader.
   *
   * @throws Exception
   *           If the test failed unexpectedly.
   */
  @Test
  public void testInitialState() throws Exception {
    LDIFReader reader = createLDIFReader("");

    try {
      Assert.assertEquals(reader.getEntriesIgnored(), 0);
      Assert.assertEquals(reader.getEntriesRead(), 0);
      Assert.assertEquals(reader.getEntriesRejected(), 0);
      Assert.assertEquals(reader.getLastEntryLineNumber(), -1);
    } finally {
      reader.close();
    }
  }

  /**
   * Attempt to read an entry from an empty LDIF stream.
   *
   * @throws Exception
   *           If the test failed unexpectedly.
   */
  @Test
  public void testReadEntryEmptyStream() throws Exception {
    LDIFReader reader = createLDIFReader("");

    try {
      Entry entry = reader.readEntry();

      Assert.assertNull(entry);

      Assert.assertEquals(reader.getEntriesIgnored(), 0);
      Assert.assertEquals(reader.getEntriesRead(), 0);
      Assert.assertEquals(reader.getEntriesRejected(), 0);
      Assert.assertEquals(reader.getLastEntryLineNumber(), -1);
    } finally {
      reader.close();
    }
  }

  /**
   * Attempt to read an entry from an empty LDIF stream containing just
   * the LDIF version.
   *
   * @throws Exception
   *           If the test failed unexpectedly.
   */
  @Test(dependsOnMethods = { "testReadEntryEmptyStream" })
  public void testReadEntryEmptyStreamVersion() throws Exception {
    LDIFReader reader = createLDIFReader("version: 1\n");

    try {
      Entry entry = reader.readEntry();

      Assert.assertNull(entry);

      Assert.assertEquals(reader.getEntriesIgnored(), 0);
      Assert.assertEquals(reader.getEntriesRead(), 0);
      Assert.assertEquals(reader.getEntriesRejected(), 0);
      Assert.assertEquals(reader.getLastEntryLineNumber(), 1);
    } finally {
      reader.close();
    }
  }

  /**
   * Attempt to read a change record from an empty LDIF stream.
   *
   * @throws Exception
   *           If the test failed unexpectedly.
   */
  @Test
  public void testChangeRecordEmptyStream() throws Exception {
    LDIFReader reader = createLDIFReader("");
    try {
      ChangeRecordEntry change = reader.readChangeRecord(true);

      Assert.assertNull(change);

      Assert.assertEquals(reader.getEntriesIgnored(), 0);
      Assert.assertEquals(reader.getEntriesRead(), 0);
      Assert.assertEquals(reader.getEntriesRejected(), 0);
      Assert.assertEquals(reader.getLastEntryLineNumber(), -1);
    } finally {
      reader.close();
    }
  }

  /**
   * Attempt to read a single entry.
   *
   * @throws Exception
   *           If the test failed unexpectedly.
   */
  @Test(dependsOnMethods = { "testReadEntryEmptyStream" })
  public void testReadEntrySingle() throws Exception {
    final String ldifString = "dn: cn=john, dc=foo, dc=com\n"
        + "objectClass: top\n" + "objectClass: person\n" + "cn: john\n"
        + "sn: smith\n";

    LDIFReader reader = createLDIFReader(ldifString);

    try {
      Entry entry = reader.readEntry();
      Assert.assertNotNull(entry);

      Assert.assertEquals(entry.getName(), DN
          .valueOf("cn=john, dc=foo, dc=com"));
      Assert.assertTrue(entry.hasObjectClass(OC_TOP));
      Assert.assertTrue(entry.hasObjectClass(OC_PERSON));
      Assert.assertTrue(entry.hasValue(AT_CN, null, ByteString.valueOfUtf8("john")));
      Assert.assertTrue(entry.hasValue(AT_SN, null, ByteString.valueOfUtf8("smith")));

      Assert.assertNull(reader.readEntry());

      Assert.assertEquals(reader.getEntriesIgnored(), 0);
      Assert.assertEquals(reader.getEntriesRead(), 1);
      Assert.assertEquals(reader.getEntriesRejected(), 0);
      Assert.assertEquals(reader.getLastEntryLineNumber(), 1);
    } finally {
      reader.close();
    }
  }

  /**
   * Attempt to read an entry containing a folded line.
   *
   * @throws Exception
   *           If the test failed unexpectedly.
   */
  @Test(dependsOnMethods = { "testReadEntrySingle" })
  public void testReadEntryFoldedLine() throws Exception {
    final String ldifString = "dn: cn=john, dc=foo, dc=com\n"
        + "objectClass: top\n" + "objectClass: person\n" + "cn: john\n"
        + "sn: smith\n" + "description: once upon a time\n"
        + "  in the west\n";

    LDIFReader reader = createLDIFReader(ldifString);

    try {
      Entry entry = reader.readEntry();
      Assert.assertNotNull(entry);
      Assert.assertTrue(entry.hasValue(AT_DESCR, null, ByteString.valueOfUtf8("once upon a time in the west")));
    } finally {
      reader.close();
    }
  }

  /**
   * Attempt to read an entry containing a base64 line.
   *
   * @throws Exception
   *           If the test failed unexpectedly.
   */
  @Test(dependsOnMethods = { "testReadEntrySingle" })
  public void testReadEntryBase64() throws Exception {
    final String ldifString = "dn: cn=john, dc=foo, dc=com\n"
        + "objectClass: top\n" + "objectClass: person\n" + "cn: john\n"
        + "sn: smith\n"
        + "description:: b25jZSB1cG9uIGEgdGltZSBpbiB0aGUgd2VzdA==\n";

    LDIFReader reader = createLDIFReader(ldifString);

    try {
      Entry entry = reader.readEntry();
      Assert.assertNotNull(entry);
      Assert.assertTrue(entry.hasValue(AT_DESCR, null, ByteString.valueOfUtf8("once upon a time in the west")));
    } finally {
      reader.close();
    }
  }

  /**
   * Attempt to read multiple entries.
   *
   * @throws Exception
   *           If the test failed unexpectedly.
   */
  @Test(dependsOnMethods = { "testReadEntrySingle" })
  public void testReadEntryMultiple() throws Exception {
    final String ldifString = "dn: cn=john, dc=foo, dc=com\n"
        + "objectClass: top\n" + "objectClass: person\n" + "cn: john\n"
        + "sn: smith\n" + "\n" + "dn: cn=anne, dc=foo, dc=com\n"
        + "objectClass: top\n" + "objectClass: person\n" + "cn: anne\n"
        + "sn: other\n" + "\n";

    LDIFReader reader = createLDIFReader(ldifString);

    try {
      reader.readEntry();
      Entry entry = reader.readEntry();

      Assert.assertNotNull(entry);

      Assert.assertEquals(entry.getName(), DN
          .valueOf("cn=anne, dc=foo, dc=com"));
      Assert.assertTrue(entry.hasObjectClass(OC_TOP));
      Assert.assertTrue(entry.hasObjectClass(OC_PERSON));
      Assert.assertTrue(entry.hasValue(AT_CN, null, ByteString.valueOfUtf8("anne")));
      Assert.assertTrue(entry.hasValue(AT_SN, null, ByteString.valueOfUtf8("other")));

      Assert.assertNull(reader.readEntry());

      Assert.assertEquals(reader.getEntriesIgnored(), 0);
      Assert.assertEquals(reader.getEntriesRead(), 2);
      Assert.assertEquals(reader.getEntriesRejected(), 0);
      Assert.assertEquals(reader.getLastEntryLineNumber(), 7);
    } finally {
      reader.close();
    }
  }

  /**
   * Attempt to read multiple changes.
   *
   * @throws Exception
   *           If the test failed unexpectedly.
   */
  @Test(dependsOnMethods = { "testChangeRecordEmptyStream" })
  public void testReadChangeMultiple() throws Exception {
    LDIFReader reader = createLDIFReader(VALID_LDIF);

    try {
      ChangeRecordEntry change;
      AddChangeRecordEntry add;
      DeleteChangeRecordEntry delete;
      ModifyChangeRecordEntry modify;
      ModifyDNChangeRecordEntry modifyDN;
      DN dn;
      RDN rdn;
      Iterator<RawModification> i;
      Modification mod;
      Attribute attr;

      // Change record #1.
      change = reader.readChangeRecord(false);
      Assert.assertTrue(change instanceof AddChangeRecordEntry);
      add = (AddChangeRecordEntry) change;

      dn = DN.valueOf("cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com");
      Assert.assertEquals(add.getDN(), dn);

      List<Attribute> attrs = new ArrayList<>();
      AttributeBuilder builder = new AttributeBuilder(AT_OC, "objectclass");
      builder.add("top");
      builder.add("person");
      builder.add("organizationalPerson");

      attrs.add(builder.toAttribute());
      attrs.add(Attributes.create("cn", "Fiona Jensen"));
      attrs.add(Attributes.create("sn", "Jensen"));
      attrs.add(Attributes.create("uid", "fiona"));
      attrs.add(Attributes.create("telephonenumber", "+1 408 555 1212"));
      Assert.assertTrue(add.getAttributes().containsAll(attrs));

      // Change record #2.
      change = reader.readChangeRecord(false);
      Assert.assertTrue(change instanceof DeleteChangeRecordEntry);
      delete = (DeleteChangeRecordEntry) change;

      dn = DN.valueOf("cn=Robert Jensen, ou=Marketing, dc=airius, dc=com");
      Assert.assertEquals(delete.getDN(), dn);

      // Change record #3.
      change = reader.readChangeRecord(false);
      Assert.assertTrue(change instanceof ModifyDNChangeRecordEntry);
      modifyDN = (ModifyDNChangeRecordEntry) change;

      dn = DN
          .valueOf("cn=Paul Jensen, ou=Product Development, dc=airius, dc=com");
      Assert.assertEquals(modifyDN.getDN(), dn);

      rdn = RDN.decode("cn=paula jensen");
      Assert.assertEquals(modifyDN.getNewRDN(), rdn);
      Assert.assertNull(modifyDN.getNewSuperiorDN());
      Assert.assertTrue(modifyDN.deleteOldRDN());

      // Change record #4.
      change = reader.readChangeRecord(false);
      Assert.assertTrue(change instanceof ModifyDNChangeRecordEntry);
      modifyDN = (ModifyDNChangeRecordEntry) change;

      dn = DN
          .valueOf("ou=PD Accountants, ou=Product Development, dc=airius, dc=com");
      Assert.assertEquals(modifyDN.getDN(), dn);

      rdn = RDN.decode("ou=Product Development Accountants");
      Assert.assertEquals(modifyDN.getNewRDN(), rdn);
      dn = DN.valueOf("ou=Accounting, dc=airius, dc=com");
      Assert.assertEquals(modifyDN.getNewSuperiorDN(), dn);
      Assert.assertFalse(modifyDN.deleteOldRDN());

      // Change record #5.
      change = reader.readChangeRecord(false);
      Assert.assertTrue(change instanceof ModifyChangeRecordEntry);
      modify = (ModifyChangeRecordEntry) change;

      dn = DN
          .valueOf("cn=Paula Jensen, ou=Product Development, dc=airius, dc=com");
      Assert.assertEquals(modify.getDN(), dn);

      i = modify.getModifications().iterator();

      Assert.assertTrue(i.hasNext());
      mod = i.next().toModification();
      Assert.assertEquals(mod.getModificationType(), ModificationType.ADD);
      attr = Attributes.create("postaladdress",
          "123 Anystreet $ Sunnyvale, CA $ 94086");
      Assert.assertEquals(mod.getAttribute(), attr);

      Assert.assertTrue(i.hasNext());
      mod = i.next().toModification();
      Assert.assertEquals(mod.getModificationType(),
          ModificationType.DELETE);
      attr = Attributes.empty(AT_DESCR);
      Assert.assertEquals(mod.getAttribute(), attr);

      Assert.assertTrue(i.hasNext());
      mod = i.next().toModification();
      Assert.assertEquals(mod.getModificationType(),
          ModificationType.REPLACE);
      builder = new AttributeBuilder(AT_TELN, "telephonenumber");
      builder.add("+1 408 555 1234");
      builder.add("+1 408 555 5678");
      Assert.assertEquals(mod.getAttribute(), builder.toAttribute());

      Assert.assertTrue(i.hasNext());
      mod = i.next().toModification();
      Assert.assertEquals(mod.getModificationType(),
          ModificationType.DELETE);
      attr = Attributes.create("facsimiletelephonenumber", "+1 408 555 9876");
      Assert.assertEquals(mod.getAttribute(), attr);

      Assert.assertFalse(i.hasNext());

      // Change record #6.
      change = reader.readChangeRecord(false);
      Assert.assertTrue(change instanceof ModifyChangeRecordEntry);
      modify = (ModifyChangeRecordEntry) change;

      dn = DN.valueOf("cn=Ingrid Jensen, ou=Product Support, dc=airius, dc=com");
      Assert.assertEquals(modify.getDN(), dn);

      i = modify.getModifications().iterator();

      Assert.assertTrue(i.hasNext());
      mod = i.next().toModification();
      Assert.assertEquals(mod.getModificationType(), ModificationType.REPLACE);
      attr = Attributes.empty(DirectoryServer.getAttributeTypeOrNull("postaladdress"));
      Assert.assertEquals(mod.getAttribute(), attr);

      // Change record #7.
      change = reader.readChangeRecord(false);
      Assert.assertTrue(change instanceof ModifyChangeRecordEntry);
      modify = (ModifyChangeRecordEntry) change;

      Assert.assertTrue(modify.getDN().isRootDN());

      i = modify.getModifications().iterator();

      Assert.assertTrue(i.hasNext());
      mod = i.next().toModification();
      Assert.assertEquals(mod.getModificationType(),
          ModificationType.DELETE);
      attr = Attributes.empty(AT_DESCR);
      Assert.assertEquals(mod.getAttribute(), attr);

      // Change record #8.
      change = reader.readChangeRecord(false);
      Assert.assertTrue(change instanceof ModifyChangeRecordEntry);
      modify = (ModifyChangeRecordEntry) change;

      dn = DN.valueOf("uid=rogasawara, ou=\u55b6\u696d\u90e8, o=airius");
      Assert.assertEquals(modify.getDN(), dn);

      i = modify.getModifications().iterator();

      Assert.assertTrue(i.hasNext());
      mod = i.next().toModification();
      Assert.assertEquals(mod.getModificationType(),
          ModificationType.DELETE);
      attr = Attributes.empty(AT_DESCR);
      Assert.assertEquals(mod.getAttribute(), attr);

      Assert.assertFalse(i.hasNext());

      // Check final state.

      Assert.assertNull(reader.readChangeRecord(false));

      Assert.assertEquals(reader.getEntriesIgnored(), 0);
      Assert.assertEquals(reader.getEntriesRead(), 0);
      Assert.assertEquals(reader.getEntriesRejected(), 0);
      Assert.assertEquals(reader.getLastEntryLineNumber(), 72);
    } finally {
      reader.close();
    }
  }

  /**
   * Attempt to read multiple changes and rejects one.
   *
   * @throws Exception
   *           If the test failed unexpectedly.
   */
  @Test(dependsOnMethods = { "testReadChangeMultiple" })
  public void testReadChangeMultipleAndReject() throws Exception {
    LDIFReader reader = createLDIFReader(VALID_LDIF);

    try {
      reader.readChangeRecord(false);
      reader.readChangeRecord(false);
      reader.readChangeRecord(false);

      reader.readChangeRecord(false);
      reader.rejectLastEntry(LocalizableMessage.raw("Rejected"));

      reader.readChangeRecord(false);
      reader.rejectLastEntry(LocalizableMessage.raw("Rejected"));

      reader.readChangeRecord(false);
      reader.readChangeRecord(false);
      reader.readChangeRecord(false);

      // Check final state.
      Assert.assertNull(reader.readChangeRecord(false));
      Assert.assertEquals(reader.getEntriesRejected(), 2);
    } finally {
      reader.close();
    }
  }

  /**
   * Attempt to read a change containing a file-based attribute.
   *
   * @throws Exception
   *           If the test failed unexpectedly.
   */
  @Test(dependsOnMethods = { "testReadChangeMultiple" })
  public void testReadChangeWithFileBaseAttribute() throws Exception {
    StringBuilder buffer = new StringBuilder(TEMP_FILE_LDIF);
    buffer.append(TEMP_FILE.getCanonicalPath());
    buffer.append("\n");
    LDIFReader reader = createLDIFReader(buffer.toString());

    try {
      ChangeRecordEntry change = reader.readChangeRecord(false);
      Assert.assertTrue(change instanceof AddChangeRecordEntry);
      AddChangeRecordEntry add = (AddChangeRecordEntry) change;

      DN dn = DN.valueOf("cn=john smith, dc=com");
      Assert.assertEquals(add.getDN(), dn);

      Attribute attr = Attributes.create("description", TEMP_FILE_STRING);
      Assert.assertTrue(add.getAttributes().contains(attr));

      // Check final state.
      Assert.assertNull(reader.readChangeRecord(false));
    } finally {
      reader.close();
    }
  }

  /**
   * LDIF change reader - invalid data provider.
   *
   * @return Returns an array of invalid LDIF change records.
   */
  @DataProvider(name = "invalidLDIFChangeRecords")
  public Object[][] createInvalidLDIFChangeRecords() {
    return new Object[][] {
        {
          "changetype: add\n" +
          "objectClass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "dn: cn=john smith, dc=com\n" +
          "changetype: add\n" +
          "objectClass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n"
        },
        {
          ": cn=john smith, dc=com\n" +
          "changetype: add\n" +
          "objectClass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n"
        },
        {
          "x: cn=john smith, dc=com\n" +
          "changetype: add\n" +
          "objectClass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n"
        },
        {
          "dn: foo\n" +
          "changetype: add\n" +
          "objectClass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n"
        },
        {
          "dn: cn,=john smith, dc=com\n" +
          "changetype: add\n" +
          "objectClass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "chaxxngetype: add\n" +
          "objectClass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "changetype: foo\n" +
          "objectClass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "changetype:: add\n" +
          "objectClass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          ": add\n" +
          "objectClass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "changetype:\n" +
          "objectClass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "\n" +
          "objectClass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "changetype: add\n" +
          "xxxx\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "changetype: add\n" +
          "objectClass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "cn: john\n" +
          "sn: smith\n"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "changetype: add\n" +
          "objectClass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n" +
          "cn: john\n"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "changetype: add\n" +
          ": top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "objectClass: person\n" +
          "sn: smith\n"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "changetype: add\n" +
          "objectclass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n" +
          "description:: YnJva2VuIGJhc2U2NA*=="
        },
        {
          "dn:: YnJva2VuIGJhc2U2NA*==" +
          "changetype: add\n" +
          "objectclass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n"
        },
        {
          "dn:: Y249YnJva2VuIGJhc2U2NCBkbix4" +
          "changetype: add\n" +
          "objectclass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "changetype: add\n" +
          "objectclass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n" +
          "description:< brok@n:///bad/url"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "changetype: add\n" +
          "objectclass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n" +
          "description:< file:///bad/path/name"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "changetype: delete\n" +
          "objectclass: top\n" +
          "objectClass: person\n" +
          "cn: john\n" +
          "sn: smith\n" +
          "description:< file:///bad/path/name"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "changetype: modrdn\n"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "changetype: modrdn\n" +
          "newrdn: x\n"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "changetype: modrdn\n" +
          "newrdn: cn=foo\n"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "changetype: modrdn\n" +
          "newrdn: cn=foo\n" +
          "deleteoldxx: xxx\n"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "changetype: modrdn\n" +
          "newrdn: cn=foo\n" +
          "deleteoldrdn: xxx\n"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "changetype: modrdn\n" +
          "newrdn: cn=foo\n" +
          "deleteoldrdn: 0\n" +
          "newsu: xxxx\n"
        },
        {
          "dn: cn=john smith, dc=com\n" +
          "changetype: modrdn\n" +
          "newrdn: cn=foo\n" +
          "deleteoldrdn: 0\n" +
          "newsuperior: xxxx\n"
        },
    };
  }

  /**
   * Tests the read change record method against invalid LDIF records.
   *
   * @param ldifString
   *          The invalid LDIF change record.
   * @throws Exception
   *           If the test failed unexpectedly.
   */
  @Test(dataProvider = "invalidLDIFChangeRecords",
      expectedExceptions = { LDIFException.class })
  public void testReadChangeInvalidData(String ldifString) throws Exception {
    LDIFReader reader = createLDIFReader(ldifString);
    ChangeRecordEntry change = null;

    try {
      change = reader.readChangeRecord(false);
    } finally {
      reader.close();
    }

    Assert.fail("Expected exception but got result: "
        + change.getChangeOperationType() + " - " + change.getDN());
  }

  /**
   * Create an LDIF reader from a string of LDIF.
   *
   * @param ldifString
   *          The string of LDIF. *
   * @return Returns the LDIF reader.
   * @throws Exception
   *           If an error occurred.
   */
  private LDIFReader createLDIFReader(String ldifString) throws Exception {
    byte[] bytes = StaticUtils.getBytes(ldifString);

    LDIFReader reader = new LDIFReader(new LDIFImportConfig(
        new ByteArrayInputStream(bytes)));

    return reader;
  }
}
