From 6bcc78cb0b93450fc97be9ad9f40639543e69738 Mon Sep 17 00:00:00 2001
From: Nicolas Capponi <nicolas.capponi@forgerock.com>
Date: Thu, 15 Jan 2015 17:02:27 +0000
Subject: [PATCH] OPENDJ-1585 CR-5758 Fix DN normalization in backends.pluggable package

---
 opendj3-server-dev/src/server/org/opends/server/backends/pluggable/DN2URI.java                                |   63 +++-
 opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/backends/pluggable/TestJebFormat.java |  558 ++++++++++++++++++++++++++++++++++++++++++
 opendj3-server-dev/src/server/org/opends/server/backends/pluggable/EntryContainer.java                        |    2 
 opendj3-server-dev/src/server/org/opends/server/backends/pluggable/JebFormat.java                             |  100 +------
 4 files changed, 618 insertions(+), 105 deletions(-)

diff --git a/opendj3-server-dev/src/server/org/opends/server/backends/pluggable/DN2URI.java b/opendj3-server-dev/src/server/org/opends/server/backends/pluggable/DN2URI.java
index d25e7ec..fa9d68a 100644
--- a/opendj3-server-dev/src/server/org/opends/server/backends/pluggable/DN2URI.java
+++ b/opendj3-server-dev/src/server/org/opends/server/backends/pluggable/DN2URI.java
@@ -22,7 +22,7 @@
  *
  *
  *      Copyright 2006-2010 Sun Microsystems, Inc.
- *      Portions Copyright 2012-2014 ForgeRock AS
+ *      Portions Copyright 2012-2015 ForgeRock AS
  */
 package org.opends.server.backends.pluggable;
 
@@ -58,16 +58,22 @@
 import org.opends.server.types.SearchResultReference;
 import org.opends.server.util.StaticUtils;
 
+import com.forgerock.opendj.util.Pair;
+
 import static org.opends.messages.JebMessages.*;
 import static org.opends.server.util.ServerConstants.*;
 
 /**
  * This class represents the referral database which contains URIs from referral
- * entries.  The key is the DN of the referral entry and the value is that of a
- * labeled URI in the ref attribute for that entry. Duplicate keys are permitted
- * since a referral entry can contain multiple values of the ref attribute.  Key
- * order is the same as in the DN database so that all referrals in a subtree
- * can be retrieved by cursoring through a range of the records.
+ * entries.
+ * <p>
+ * The key is the DN of the referral entry and the value is that of a pair
+ * (DN, list of labeled URI in the ref attribute for that entry). The DN must be
+ * duplicated in the value because the key is suitable for comparisons but is
+ * not reversible to a valid DN. Duplicate keys are permitted since a referral
+ * entry can contain multiple values of the ref attribute. Key order is the same
+ * as in the DN database so that all referrals in a subtree can be retrieved by
+ * cursoring through a range of the records.
  */
 public class DN2URI extends DatabaseContainer
 {
@@ -112,11 +118,15 @@
     prefixRDNComponents = entryContainer.getBaseDN().size();
   }
 
-  private ByteSequence encode(Collection<String> col)
+  private ByteSequence encode(DN dn, Collection<String> col)
   {
     if (col != null)
     {
       ByteStringBuilder b = new ByteStringBuilder();
+      byte[] dnBytes = StaticUtils.getBytes(dn.toString());
+      b.append(dnBytes.length);
+      b.append(dnBytes);
+
       b.append(col.size());
       for (String s : col)
       {
@@ -129,21 +139,31 @@
     return ByteString.empty();
   }
 
-  private Collection<String> decode(ByteSequence bs)
+  private Pair<DN, List<String>> decode(ByteSequence bs) throws StorageRuntimeException
   {
     if (!bs.isEmpty())
     {
       ByteSequenceReader r = bs.asReader();
+      final int dnLength = r.getInt();
+      DN dn = null;
+      try
+      {
+        dn = DN.valueOf(r.getString(dnLength));
+      }
+      catch (DirectoryException e)
+      {
+        throw new StorageRuntimeException("Unable to decode DN from binary value", e);
+      }
       final int nbElems = r.getInt();
-      ArrayList<String> results = new ArrayList<String>(nbElems);
+      List<String> results = new ArrayList<String>(nbElems);
       for (int i = 0; i < nbElems; i++)
       {
         final int stringLength = r.getInt();
         results.add(r.getString(stringLength));
       }
-      return results;
+      return Pair.of(dn, results);
     }
-    return new ArrayList<String>();
+    return Pair.empty();
   }
 
   /**
@@ -162,15 +182,16 @@
     ByteString oldValue = read(txn, key, true);
     if (oldValue != null)
     {
-      final Collection<String> newUris = decode(oldValue);
+      final Pair<DN, List<String>> dnAndUris = decode(oldValue);
+      final Collection<String> newUris = dnAndUris.getSecond();
       if (newUris.addAll(labeledURIs))
       {
-        put(txn, key, encode(newUris));
+        put(txn, key, encode(dn, newUris));
       }
     }
     else
     {
-      txn.putIfAbsent(treeName, key, encode(labeledURIs));
+      txn.putIfAbsent(treeName, key, encode(dn, labeledURIs));
     }
     containsReferrals = ConditionResult.TRUE;
   }
@@ -214,10 +235,11 @@
     ByteString oldValue = read(txn, key, true);
     if (oldValue != null)
     {
-      final Collection<String> oldUris = decode(oldValue);
+      final Pair<DN, List<String>> dnAndUris = decode(oldValue);
+      final Collection<String> oldUris = dnAndUris.getSecond();
       if (oldUris.removeAll(labeledURIs))
       {
-        put(txn, key, encode(oldUris));
+        put(txn, key, encode(dn, oldUris));
         containsReferrals = containsReferrals(txn);
         return true;
       }
@@ -522,7 +544,8 @@
           if (cursor.positionToKey(toKey(dn)))
           {
             // Construct a set of all the labeled URIs in the referral.
-            Collection<String> labeledURIs = decode(cursor.getValue());
+            final Pair<DN, List<String>> dnAndUris = decode(cursor.getValue());
+            Collection<String> labeledURIs = dnAndUris.getSecond();
             throwReferralException(targetDN, dn, labeledURIs, searchScope);
           }
         }
@@ -591,8 +614,6 @@
         while (success && cursor.getKey().compareTo(end) < 0)
         {
           // We have found a subordinate referral.
-          DN dn = JebFormat.dnFromDNKey(cursor.getKey(), entryContainer.getBaseDN());
-
           // Make sure the referral is within scope.
           if (searchOp.getScope() == SearchScope.SINGLE_LEVEL
               && JebFormat.findDNKeyParent(cursor.getKey()) != baseDN.length())
@@ -601,7 +622,9 @@
           }
 
           // Construct a list of all the URIs in the referral.
-          Collection<String> labeledURIs = decode(cursor.getValue());
+          final Pair<DN, List<String>> dnAndUris = decode(cursor.getValue());
+          final DN dn = dnAndUris.getFirst();
+          final Collection<String> labeledURIs = dnAndUris.getSecond();
           SearchResultReference reference = toSearchResultReference(dn, labeledURIs, searchOp.getScope());
           if (!searchOp.returnReference(dn, reference))
           {
diff --git a/opendj3-server-dev/src/server/org/opends/server/backends/pluggable/EntryContainer.java b/opendj3-server-dev/src/server/org/opends/server/backends/pluggable/EntryContainer.java
index 9bab1ca..b31e75c 100644
--- a/opendj3-server-dev/src/server/org/opends/server/backends/pluggable/EntryContainer.java
+++ b/opendj3-server-dev/src/server/org/opends/server/backends/pluggable/EntryContainer.java
@@ -1749,7 +1749,7 @@
                   if (!pluginResult.continueProcessing())
                   {
                     LocalizableMessage message =
-                        ERR_JEB_DELETE_ABORTED_BY_SUBORDINATE_PLUGIN.get(dnFromDNKey(cursor.getKey(), getBaseDN()));
+                        ERR_JEB_DELETE_ABORTED_BY_SUBORDINATE_PLUGIN.get(subordinateEntry.getName().toString());
                     throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message);
                   }
                 }
diff --git a/opendj3-server-dev/src/server/org/opends/server/backends/pluggable/JebFormat.java b/opendj3-server-dev/src/server/org/opends/server/backends/pluggable/JebFormat.java
index 89c2de3..e90c91b 100644
--- a/opendj3-server-dev/src/server/org/opends/server/backends/pluggable/JebFormat.java
+++ b/opendj3-server-dev/src/server/org/opends/server/backends/pluggable/JebFormat.java
@@ -33,7 +33,6 @@
 import org.forgerock.opendj.ldap.ByteString;
 import org.forgerock.opendj.ldap.ByteStringBuilder;
 import org.opends.server.types.DN;
-import org.opends.server.types.DirectoryException;
 import org.opends.server.types.RDN;
 import org.opends.server.util.StaticUtils;
 
@@ -51,59 +50,14 @@
   public static final byte TAG_DIRECTORY_SERVER_ENTRY = 0x61;
 
   /**
-   * Decode a DN value from its database key representation.
+   * Find the length of bytes that represents the superior DN of the given DN
+   * key. The superior DN is represented by the initial bytes of the DN key.
    *
-   * @param dnKey The database key value of the DN.
-   * @param prefix The DN to prefix the decoded DN value.
-   * @return The decoded DN value.
-   * @throws DirectoryException if an error occurs while decoding the DN value.
-   * @see #dnToDNKey(DN, int)
+   * @param dnKey
+   *          The database key value of the DN.
+   * @return The length of the superior DN or -1 if the given dn is the root DN
+   *         or 0 if the superior DN is removed.
    */
-  public static DN dnFromDNKey(ByteSequence dnKey, DN prefix) throws DirectoryException
-  {
-    DN dn = prefix;
-    boolean escaped = false;
-    ByteStringBuilder buffer = new ByteStringBuilder();
-    for(int i = 0; i < dnKey.length(); i++)
-    {
-      if(dnKey.byteAt(i) == 0x5C)
-      {
-        escaped = true;
-        continue;
-      }
-      else if(!escaped && dnKey.byteAt(i) == 0x01)
-      {
-        buffer.append(0x01);
-        escaped = false;
-        continue;
-      }
-      else if(!escaped && dnKey.byteAt(i) == 0x00)
-      {
-        if(buffer.length() > 0)
-        {
-          dn = dn.child(RDN.decode(buffer.toString()));
-          buffer.clear();
-        }
-      }
-      else
-      {
-        if(escaped)
-        {
-          buffer.append(0x5C);
-          escaped = false;
-        }
-        buffer.append(dnKey.byteAt(i));
-      }
-    }
-
-    if(buffer.length() > 0)
-    {
-      dn = dn.child(RDN.decode(buffer.toString()));
-    }
-
-    return dn;
-  }
-
   public static int findDNKeyParent(ByteSequence dnKey)
   {
     if (dnKey.length() == 0)
@@ -112,10 +66,11 @@
       return -1;
     }
 
-    // We will walk backwards through the buffer and find the first unescaped comma
+    // We will walk backwards through the buffer
+    // and find the first unescaped NORMALIZED_RDN_SEPARATOR
     for (int i = dnKey.length() - 1; i >= 0; i--)
     {
-      if (dnKey.byteAt(i) == 0x00 && i - 1 >= 0 && dnKey.byteAt(i - 1) != 0x5C)
+      if (dnKey.byteAt(i) == DN.NORMALIZED_RDN_SEPARATOR && i - 1 >= 0 && dnKey.byteAt(i - 1) != DN.NORMALIZED_ESC_BYTE)
       {
         return i;
       }
@@ -125,6 +80,7 @@
 
   /**
    * Create a DN database key from an entry DN.
+   *
    * @param dn The entry DN.
    * @param prefixRDNs The number of prefix RDNs to remove from the encoded
    *                   representation.
@@ -132,39 +88,15 @@
    */
   public static ByteString dnToDNKey(DN dn, int prefixRDNs)
   {
-    StringBuilder buffer = new StringBuilder();
-    for (int i = dn.size() - prefixRDNs - 1; i >= 0; i--)
+    ByteStringBuilder builder = new ByteStringBuilder();
+    int startSize = dn.size() - prefixRDNs - 1;
+    for (int i = startSize; i >= 0; i--)
     {
-      buffer.append('\u0000');
-      formatRDNKey(dn.getRDN(i), buffer);
+        builder.append(DN.NORMALIZED_RDN_SEPARATOR);
+        dn.getRDN(i).toNormalizedByteString(builder);
     }
 
-    return ByteString.wrap(StaticUtils.getBytes(buffer.toString()));
+    return builder.toByteString();
   }
 
-  private static void formatRDNKey(RDN rdn, StringBuilder buffer)
-  {
-    if (!rdn.isMultiValued())
-    {
-      rdn.toString(buffer);
-    }
-    else
-    {
-      TreeSet<String> rdnElementStrings = new TreeSet<String>();
-
-      for (int i=0; i < rdn.getNumValues(); i++)
-      {
-        StringBuilder b2 = new StringBuilder();
-        rdnElementStrings.add(b2.toString());
-      }
-
-      Iterator<String> iterator = rdnElementStrings.iterator();
-      buffer.append(iterator.next().replace("\u0001", "\\\u0001"));
-      while (iterator.hasNext())
-      {
-        buffer.append('\u0001');
-        buffer.append(iterator.next().replace("\u0001", "\\\u0001"));
-      }
-    }
-  }
 }
diff --git a/opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/backends/pluggable/TestJebFormat.java b/opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/backends/pluggable/TestJebFormat.java
new file mode 100644
index 0000000..b3a75fa
--- /dev/null
+++ b/opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/backends/pluggable/TestJebFormat.java
@@ -0,0 +1,558 @@
+/*
+ * 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-2009 Sun Microsystems, Inc.
+ *      Portions Copyright 2014-2015 ForgeRock AS
+ */
+package org.opends.server.backends.pluggable;
+
+import java.io.ByteArrayInputStream;
+import java.util.List;
+import java.util.Map;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.opends.server.DirectoryServerTestCase;
+import org.opends.server.TestCaseUtils;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.types.*;
+import org.opends.server.util.LDIFReader;
+import org.opends.server.util.StaticUtils;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.opends.server.util.StaticUtils.*;
+import static org.testng.Assert.*;
+
+/**
+ * JebFormat Tester.
+ */
+@SuppressWarnings("javadoc")
+public class TestJebFormat extends DirectoryServerTestCase {
+
+  private static final String ldifString =
+    "dn: uid=user.1,ou=People,dc=example,dc=com\n"
+      + "objectClass: top\n"
+      + "objectClass: person\n"
+      + "objectClass: organizationalPerson\n"
+      + "objectClass: inetOrgPerson\n"
+      + "uid: user.1\n"
+      + "homePhone: 951-245-7634\n"
+      + "description: This is the description for Aaccf Amar.\n"
+      + "st: NC\n"
+      + "mobile: 027-085-0537\n"
+      + "postalAddress: Aaccf Amar$17984 Thirteenth Street"
+      + "$Rockford, NC  85762\n"
+      + "mail: user.1@example.com\n"
+      + "cn: Aaccf Amar\n"
+      + "l: Rockford\n"
+      + "pager: 508-763-4246\n"
+      + "street: 17984 Thirteenth Street\n"
+      + "telephoneNumber: 216-564-6748\n"
+      + "employeeNumber: 1\n"
+      + "sn: Amar\n"
+      + "givenName: Aaccf\n"
+      + "postalCode: 85762\n"
+      + "userPassword: password\n"
+      + "initials: AA\n"
+      + "\n"
+      + "dn:: b3U95Za25qWt6YOoLG89QWlyaXVz\n"
+      + "# dn:: ou=<JapaneseOU>,o=Airius\n"
+      + "objectclass: top\n"
+      + "objectclass: organizationalUnit\n"
+      + "ou:: 5Za25qWt6YOo\n"
+      + "# ou:: <JapaneseOU>\n"
+      + "ou;lang-ja:: 5Za25qWt6YOo\n"
+      + "# ou;lang-ja:: <JapaneseOU>\n"
+      + "ou;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2\n"
+      + "# ou;lang-ja:: <JapaneseOU_in_phonetic_representation>\n"
+      + "ou;lang-en: Sales\n"
+      + "description: Japanese office\n"
+      + "\n"
+      + "dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz\n"
+      + "# dn:: uid=<uid>,ou=<JapaneseOU>,o=Airius\n"
+      + "userpassword: {SHA}O3HSv1MusyL4kTjP+HKI5uxuNoM=\n"
+      + "objectclass: top\n"
+      + "objectclass: person\n"
+      + "objectclass: organizationalPerson\n"
+      + "objectclass: inetOrgPerson\n"
+      + "uid: rogasawara\n"
+      + "mail: rogasawara@airius.co.jp\n"
+      + "givenname;lang-ja:: 44Ot44OJ44OL44O8\n"
+      + "# givenname;lang-ja:: <JapaneseGivenname>\n"
+      + "sn;lang-ja:: 5bCP56yg5Y6f\n"
+      + "# sn;lang-ja:: <JapaneseSn>\n"
+      + "cn;lang-ja:: 5bCP56yg5Y6fIOODreODieODi+ODvA==\n"
+      + "# cn;lang-ja:: <JapaneseCn>\n"
+      + "title;lang-ja:: 5Za25qWt6YOoIOmDqOmVtw==\n"
+      + "# title;lang-ja:: <JapaneseTitle>\n"
+      + "preferredlanguage: ja\n"
+      + "givenname:: 44Ot44OJ44OL44O8\n"
+      + "# givenname:: <JapaneseGivenname>\n"
+      + "sn:: 5bCP56yg5Y6f\n"
+      + "# sn:: <JapaneseSn>\n"
+      + "cn:: 5bCP56yg5Y6fIOODreODieODi+ODvA==\n"
+      + "# cn:: <JapaneseCn>\n"
+      + "title:: 5Za25qWt6YOoIOmDqOmVtw==\n"
+      + "# title:: <JapaneseTitle>\n"
+      + "givenname;lang-ja;phonetic:: 44KN44Gp44Gr44O8\n"
+      + "# givenname;lang-ja;phonetic:: "
+      + "<JapaneseGivenname_in_phonetic_representation_kana>\n"
+      + "sn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJ\n"
+      + "# sn;lang-ja;phonetic:: "
+      + "<JapaneseSn_in_phonetic_representation_kana>\n"
+      + "cn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJIOOCjeOBqeOBq+ODvA==\n"
+      + "# cn;lang-ja;phonetic:: "
+      + "<JapaneseCn_in_phonetic_representation_kana>\n"
+      + "title;lang-ja;phonetic:: "
+      + ""
+      + "44GI44GE44GO44KH44GG44G2IOOBtuOBoeOCh+OBhg==\n"
+      + "# title;lang-ja;phonetic::\n"
+      + "# <JapaneseTitle_in_phonetic_representation_kana>\n"
+      + "givenname;lang-en: Rodney\n"
+      + "sn;lang-en: Ogasawara\n"
+      + "cn;lang-en: Rodney Ogasawara\n"
+      + "title;lang-en: Sales, Director\n" + "\n" + "";
+
+
+  /**
+   * Encodes this entry using the V3 encoding.
+   *
+   * @param  buffer  The buffer to encode into.
+   *
+   * @throws  DirectoryException  If a problem occurs while attempting
+   *                              to encode the entry.
+   */
+  private void encodeV1(Entry entry, ByteStringBuilder buffer)
+         throws DirectoryException
+  {
+    // The version number will be one byte.
+    buffer.append((byte)0x01);
+
+    // TODO: Can we encode the DN directly into buffer?
+    byte[] dnBytes  = getBytes(entry.getName().toString());
+    buffer.appendBERLength(dnBytes.length);
+    buffer.append(dnBytes);
+
+
+    // Encode number of OCs and 0 terminated names.
+    int i = 1;
+    ByteStringBuilder bsb = new ByteStringBuilder();
+    for (String ocName : entry.getObjectClasses().values())
+    {
+      bsb.append(ocName);
+      if(i < entry.getObjectClasses().values().size())
+      {
+        bsb.append((byte)0x00);
+      }
+      i++;
+    }
+    buffer.appendBERLength(bsb.length());
+    buffer.append(bsb);
+
+
+    // Encode the user attributes in the appropriate manner.
+    encodeV1Attributes(buffer, entry.getUserAttributes());
+
+
+    // The operational attributes will be encoded in the same way as
+    // the user attributes.
+    encodeV1Attributes(buffer, entry.getOperationalAttributes());
+  }
+
+  private void encodeV1Attributes(ByteStringBuilder buffer,
+                                Map<AttributeType,List<Attribute>> attributes)
+      throws DirectoryException
+  {
+    int numAttributes = 0;
+
+    // First count how many attributes are there to encode.
+    for (List<Attribute> attrList : attributes.values())
+    {
+      for (Attribute a : attrList)
+      {
+        if (a.isVirtual() || a.isEmpty())
+        {
+          continue;
+        }
+
+        numAttributes++;
+      }
+    }
+
+    // Encoded one-to-five byte number of attributes
+    buffer.appendBERLength(numAttributes);
+
+    append(buffer, attributes);
+  }
+
+    /**
+   * Encodes this entry using the V3 encoding.
+   *
+   * @param  buffer  The buffer to encode into.
+   *
+   * @throws  DirectoryException  If a problem occurs while attempting
+   *                              to encode the entry.
+   */
+  private void encodeV2(Entry entry, ByteStringBuilder buffer,
+                        EntryEncodeConfig config)
+         throws DirectoryException
+  {
+    // The version number will be one byte.
+    buffer.append((byte)0x02);
+
+    // Get the encoded respresentation of the config.
+    config.encode(buffer);
+
+    // If we should include the DN, then it will be encoded as a
+    // one-to-five byte length followed by the UTF-8 byte
+    // representation.
+    if (! config.excludeDN())
+    {
+      // TODO: Can we encode the DN directly into buffer?
+      byte[] dnBytes  = getBytes(entry.getName().toString());
+      buffer.appendBERLength(dnBytes.length);
+      buffer.append(dnBytes);
+    }
+
+
+    // Encode the object classes in the appropriate manner.
+    if (config.compressObjectClassSets())
+    {
+      config.getCompressedSchema().encodeObjectClasses(buffer,
+          entry.getObjectClasses());
+    }
+    else
+    {
+      // Encode number of OCs and 0 terminated names.
+      int i = 1;
+      ByteStringBuilder bsb = new ByteStringBuilder();
+      for (String ocName : entry.getObjectClasses().values())
+      {
+        bsb.append(ocName);
+        if(i < entry.getObjectClasses().values().size())
+        {
+          bsb.append((byte)0x00);
+        }
+        i++;
+      }
+      buffer.appendBERLength(bsb.length());
+      buffer.append(bsb);
+    }
+
+
+    // Encode the user attributes in the appropriate manner.
+    encodeV2Attributes(buffer, entry.getUserAttributes(), config);
+
+
+    // The operational attributes will be encoded in the same way as
+    // the user attributes.
+    encodeV2Attributes(buffer, entry.getOperationalAttributes(), config);
+  }
+
+  private void encodeV2Attributes(ByteStringBuilder buffer,
+                                Map<AttributeType,List<Attribute>> attributes,
+                                EntryEncodeConfig config)
+      throws DirectoryException
+  {
+    int numAttributes = 0;
+
+    // First count how many attributes are there to encode.
+    for (List<Attribute> attrList : attributes.values())
+    {
+      for (Attribute a : attrList)
+      {
+        if (a.isVirtual() || a.isEmpty())
+        {
+          continue;
+        }
+
+        numAttributes++;
+      }
+    }
+
+    // Encoded one-to-five byte number of attributes
+    buffer.appendBERLength(numAttributes);
+
+    if (config.compressAttributeDescriptions())
+    {
+      for (List<Attribute> attrList : attributes.values())
+      {
+        for (Attribute a : attrList)
+        {
+          if (a.isVirtual() || a.isEmpty())
+          {
+            continue;
+          }
+
+          ByteStringBuilder bsb = new ByteStringBuilder();
+          config.getCompressedSchema().encodeAttribute(bsb, a);
+          buffer.appendBERLength(bsb.length());
+          buffer.append(bsb);
+        }
+      }
+    }
+    else
+    {
+      append(buffer, attributes);
+    }
+  }
+
+  /**
+   * The attributes will be encoded as a sequence of:
+   * - A UTF-8 byte representation of the attribute name.
+   * - A zero delimiter
+   * - A one-to-five byte number of values for the attribute
+   * - A sequence of:
+   *   - A one-to-five byte length for the value
+   *   - A UTF-8 byte representation for the value
+   */
+  private void append(ByteStringBuilder buffer,
+      Map<AttributeType, List<Attribute>> attributes)
+  {
+    for (List<Attribute> attrList : attributes.values())
+    {
+      for (Attribute a : attrList)
+      {
+        byte[] nameBytes = getBytes(a.getNameWithOptions());
+        buffer.append(nameBytes);
+        buffer.append((byte)0x00);
+
+        buffer.appendBERLength(a.size());
+        for (ByteString v : a)
+        {
+          buffer.appendBERLength(v.length());
+          buffer.append(v);
+        }
+      }
+    }
+  }
+
+  /**
+   * Test entry.
+   *
+   * @throws Exception
+   *           If the test failed unexpectedly.
+   */
+  @Test()
+  public void testEntryToAndFromDatabase() throws Exception {
+    ensureTheServerIsUpAndRunning();
+
+    // Convert the test LDIF string to a byte array
+    byte[] originalLDIFBytes = StaticUtils.getBytes(ldifString);
+
+    LDIFReader reader = new LDIFReader(new LDIFImportConfig(
+        new ByteArrayInputStream(originalLDIFBytes)));
+
+    Entry entryBefore, entryAfter;
+    while ((entryBefore = reader.readEntry(false)) != null) {
+      ByteString bytes = ID2Entry.entryToDatabase(entryBefore,
+          new DataConfig(false, false, null));
+
+      entryAfter = ID2Entry.entryFromDatabase(bytes,
+                        DirectoryServer.getDefaultCompressedSchema());
+
+      // check DN and number of attributes
+      assertEquals(entryBefore.getAttributes().size(), entryAfter
+          .getAttributes().size());
+
+      assertEquals(entryBefore.getName(), entryAfter.getName());
+
+      // check the object classes were not changed
+      for (String ocBefore : entryBefore.getObjectClasses().values()) {
+        ObjectClass objectClass = DirectoryServer.getObjectClass(ocBefore
+            .toLowerCase());
+        if (objectClass == null) {
+          objectClass = DirectoryServer.getDefaultObjectClass(ocBefore);
+        }
+        String ocAfter = entryAfter.getObjectClasses().get(objectClass);
+
+        assertEquals(ocBefore, ocAfter);
+      }
+
+      // check the user attributes were not changed
+      for (AttributeType attrType : entryBefore.getUserAttributes()
+          .keySet()) {
+        List<Attribute> listBefore = entryBefore.getAttribute(attrType);
+        List<Attribute> listAfter = entryAfter.getAttribute(attrType);
+
+        assertTrue(listAfter != null);
+
+        assertEquals(listBefore.size(), listAfter.size());
+
+        for (Attribute attrBefore : listBefore) {
+          boolean found = false;
+
+          for (Attribute attrAfter : listAfter) {
+            if (attrAfter.optionsEqual(attrBefore.getOptions())) {
+              // Found the corresponding attribute
+
+              assertEquals(attrBefore, attrAfter);
+              found = true;
+            }
+          }
+
+          assertTrue(found);
+        }
+      }
+    }
+    reader.close();
+  }
+
+  /**
+   * Tests the entry encoding and decoding process the version 1 encoding.
+   *
+   * @throws Exception
+   *           If the test failed unexpectedly.
+   */
+  @Test()
+  public void testEntryToAndFromDatabaseV1() throws Exception {
+    ensureTheServerIsUpAndRunning();
+
+    // Convert the test LDIF string to a byte array
+    byte[] originalLDIFBytes = StaticUtils.getBytes(ldifString);
+
+    LDIFReader reader = new LDIFReader(new LDIFImportConfig(
+        new ByteArrayInputStream(originalLDIFBytes)));
+
+    Entry entryBefore, entryAfterV1;
+    while ((entryBefore = reader.readEntry(false)) != null) {
+      ByteStringBuilder bsb = new ByteStringBuilder();
+      encodeV1(entryBefore, bsb);
+      entryAfterV1 = Entry.decode(bsb.asReader());
+
+      assertEquals(entryBefore, entryAfterV1);
+    }
+    reader.close();
+  }
+
+  /**
+   * Retrieves a set of entry encode configurations that may be used to test the
+   * entry encoding and decoding capabilities.
+   */
+  @DataProvider(name = "encodeConfigs")
+  public Object[][] getEntryEncodeConfigs()
+  {
+    return new Object[][]
+    {
+      new Object[] { new EntryEncodeConfig() },
+      new Object[] { new EntryEncodeConfig(false, false, false) },
+      new Object[] { new EntryEncodeConfig(true, false, false) },
+      new Object[] { new EntryEncodeConfig(false, true, false) },
+      new Object[] { new EntryEncodeConfig(false, false, true) },
+      new Object[] { new EntryEncodeConfig(true, true, false) },
+      new Object[] { new EntryEncodeConfig(true, false, true) },
+      new Object[] { new EntryEncodeConfig(false, true, true) },
+      new Object[] { new EntryEncodeConfig(true, true, true) },
+    };
+  }
+
+  /**
+   * Tests the entry encoding and decoding process the version 1 encoding.
+   *
+   * @throws Exception
+   *           If the test failed unexpectedly.
+   */
+  @Test(dataProvider = "encodeConfigs")
+  public void testEntryToAndFromDatabaseV2(EntryEncodeConfig config)
+         throws Exception {
+    ensureTheServerIsUpAndRunning();
+
+    // Convert the test LDIF string to a byte array
+    byte[] originalLDIFBytes = StaticUtils.getBytes(ldifString);
+
+    LDIFReader reader = new LDIFReader(new LDIFImportConfig(
+        new ByteArrayInputStream(originalLDIFBytes)));
+
+    Entry entryBefore, entryAfterV2;
+    while ((entryBefore = reader.readEntry(false)) != null) {
+      ByteStringBuilder bsb = new ByteStringBuilder();
+      encodeV2(entryBefore, bsb, config);
+      entryAfterV2 = Entry.decode(bsb.asReader());
+      if (config.excludeDN())
+      {
+        entryAfterV2.setDN(entryBefore.getName());
+      }
+      assertEquals(entryBefore, entryAfterV2);
+    }
+    reader.close();
+  }
+
+  /**
+   * Tests the entry encoding and decoding process the version 1 encoding.
+   *
+   * @throws Exception
+   *           If the test failed unexpectedly.
+   */
+  @Test(dataProvider = "encodeConfigs")
+  public void testEntryToAndFromDatabaseV3(EntryEncodeConfig config)
+         throws Exception {
+    ensureTheServerIsUpAndRunning();
+
+    // Convert the test LDIF string to a byte array
+    byte[] originalLDIFBytes = StaticUtils.getBytes(ldifString);
+
+    LDIFReader reader = new LDIFReader(new LDIFImportConfig(
+        new ByteArrayInputStream(originalLDIFBytes)));
+
+    Entry entryBefore, entryAfterV3;
+    while ((entryBefore = reader.readEntry(false)) != null) {
+      ByteStringBuilder bsb = new ByteStringBuilder();
+      entryBefore.encode(bsb, config);
+      entryAfterV3 = Entry.decode(bsb.asReader());
+      if (config.excludeDN())
+      {
+        entryAfterV3.setDN(entryBefore.getName());
+      }
+      assertEquals(entryBefore, entryAfterV3);
+    }
+    reader.close();
+  }
+
+  @DataProvider
+  private Object[][] findDnKeyParentData()
+  {
+    return new Object[][]
+    {
+      // dn, expected length of parent
+      { "dc=example", 0 },
+      { "dc=example,dc=com", 7 },
+      { "dc=example,dc=com\\,org", 11 },
+
+    };
+  }
+
+  @Test(dataProvider="findDnKeyParentData")
+  public void testFindDnKeyParent(String dn, int expectedLength) throws Exception
+  {
+    ensureTheServerIsUpAndRunning();
+    ByteString dnKey = JebFormat.dnToDNKey(DN.valueOf(dn), 0);
+    assertThat(JebFormat.findDNKeyParent(dnKey)).isEqualTo(expectedLength);
+  }
+
+  private void ensureTheServerIsUpAndRunning() throws Exception
+  {
+    TestCaseUtils.startServer();
+  }
+}

--
Gitblit v1.10.0