From 339cfd02f1ac4ae204ef5bdff53c9b56aef96add Mon Sep 17 00:00:00 2001
From: Nicolas Capponi <nicolas.capponi@forgerock.com>
Date: Tue, 28 Oct 2014 15:27:32 +0000
Subject: [PATCH] OPENDJ-1585 Add CompactDn class as compact representation of a DN

---
 opendj-sdk/opendj-core/clirr-ignored-api-changes.xml                                                                 |   11 +
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIF.java                                             |   40 +++----
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/CompactDnTestCase.java                                |  115 +++++++++++++++++++++++
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleImpl.java |    2 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateExactMatchingRuleImpl.java          |   13 +-
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java                                               |  113 +++++++++++++++++++++-
 6 files changed, 256 insertions(+), 38 deletions(-)

diff --git a/opendj-sdk/opendj-core/clirr-ignored-api-changes.xml b/opendj-sdk/opendj-core/clirr-ignored-api-changes.xml
index f074429..8b7b89b 100644
--- a/opendj-sdk/opendj-core/clirr-ignored-api-changes.xml
+++ b/opendj-sdk/opendj-core/clirr-ignored-api-changes.xml
@@ -22,7 +22,7 @@
   ! CDDL HEADER END
   !
   !      Copyright 2014 ForgeRock AS
-  !    
+  !
 -->
 <differences>
 <!--
@@ -190,7 +190,7 @@
     <differenceType>8001</differenceType>
     <justification>Renamed SchemaValidationPolicy.Policy to SchemaValidationPolicy.Action</justification>
   </difference>
-  
+
   <difference>
     <className>org/forgerock/opendj/ldap/LDAPListenerOptions</className>
     <differenceType>7002</differenceType>
@@ -383,4 +383,11 @@
     <to>org.forgerock.util.promise.Function</to>
     <justification>OPENDJ-1550 Replace SDK Function with Function from forgerock-util</justification>
   </difference>
+  <difference>
+    <className>org/forgerock/opendj/ldap/DN</className>
+    <differenceType>7002</differenceType>
+    <method>*toNormalizedString()</method>
+    <to>*toIrreversibleNormalizedByteString()</to>
+    <justification>OPENDJ-1585 Function has been renamed to avoid abuse</justification>
+  </difference>
 </differences>
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java
index 4ed0fa0..1cd8d2d 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java
@@ -30,6 +30,7 @@
 import static com.forgerock.opendj.util.StaticUtils.getBytes;
 import static com.forgerock.opendj.ldap.CoreMessages.ERR_DN_TYPE_NOT_FOUND;
 
+import java.util.Arrays;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -911,14 +912,18 @@
     }
 
     /**
-     * Returns the normalized string representation of a DN.
+     * Returns the irreversible normalized byte string representation of a DN, suitable for equality and comparisons,
+     * and providing a natural hierarchical ordering but not usable as a valid DN nor reversible to a valid DN.
+     * <p>
+     * This representation should be used only when a byte string representation is needed and when no reversibility to
+     * a valid DN is needed. Always consider using a {@code CompactDn} as an alternative.
      *
-     * @return The normalized string representation of the provided DN.
+     * @return The normalized string representation of the provided DN, not usable as a valid DN
      */
-    public String toNormalizedString() {
+    public ByteString toIrreversibleNormalizedByteString() {
         final StringBuilder builder = new StringBuilder(size());
         if (rdn() == null) {
-            return builder.toString();
+            return ByteString.empty();
         }
 
         int i = size() - 1;
@@ -931,7 +936,105 @@
             }
             normalizeRDN(builder, rdn);
         }
-        return builder.toString();
+        return ByteString.valueOf(builder.toString());
+    }
+
+    /**
+     * A compact representation of a DN, suitable for equality and comparisons, and providing a natural hierarchical
+     * ordering.
+     * <p>
+     * This representation should be used when it is important to reduce memory usage. The memory consumption compared
+     * to a regular DN object is minimal. Prototypical usage is for static groups implementation where large groups of
+     * DNs must be recorded and must be converted back to DNs.
+     * <p>
+     * This representation can be created either eagerly or lazily.
+     * <ul>
+     *   <li>eagerly: the normalized value is computed immediately at creation time.</li>
+     *   <li>lazily: the normalized value is computed only the first time it is needed.</li>
+     * </ul>
+     */
+    public static final class CompactDn implements Comparable<CompactDn> {
+
+        /** Original string corresponding to the DN. */
+        private final byte[] originalValue;
+
+        /**
+         * Normalized byte string, suitable for equality and comparisons, and providing a natural hierarchical ordering,
+         * but not usable as a valid DN.
+         */
+        private byte[] normalizedValue;
+
+        private CompactDn(final DN dn, final boolean initializeNormalizedValueEagerly) {
+            this.originalValue = dn.stringValue != null ? getBytes(dn.stringValue) : new byte[0];
+            if (initializeNormalizedValueEagerly) {
+                normalizedValue = dn.toIrreversibleNormalizedByteString().toByteArray();
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int compareTo(final CompactDn other) {
+            return ByteString.wrap(getNormalizedValue()).compareTo(ByteString.wrap(other.getNormalizedValue()));
+        }
+
+        /**
+         * Returns the DN corresponding to this compact representation.
+         *
+         *  @return the DN
+         */
+        public DN toDn() {
+            return DN.valueOf(ByteString.wrap(originalValue).toString());
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int hashCode() {
+            return Arrays.hashCode(getNormalizedValue());
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            } else if (obj instanceof CompactDn) {
+                final CompactDn other = (CompactDn) obj;
+                return Arrays.equals(getNormalizedValue(), other.getNormalizedValue());
+            } else {
+                return false;
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public String toString() {
+            return ByteString.wrap(originalValue).toString();
+        }
+
+        private byte[] getNormalizedValue() {
+            if (normalizedValue == null) {
+                normalizedValue = toDn().toIrreversibleNormalizedByteString().toByteArray();
+            }
+            return normalizedValue;
+        }
+    }
+
+    /**
+     * Returns a compact representation of this DN, with lazy evaluation of the normalized value.
+     *
+     * @return the DN compact representation
+     */
+    public CompactDn compact() {
+        return new CompactDn(this, false);
+    }
+
+    /**
+     * Returns a compact representation of this DN, with eager evaluation of the normalized value.
+     *
+     * @return the DN compact representation
+     */
+    public CompactDn compactEagerly() {
+        return new CompactDn(this, true);
     }
 
     /**
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateExactMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateExactMatchingRuleImpl.java
index af27b44..a1f8724 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateExactMatchingRuleImpl.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CertificateExactMatchingRuleImpl.java
@@ -110,7 +110,7 @@
             return value.toByteString();
         }
 
-        final String certificateIssuer = normalizeDN(schema, dnstring);
+        final ByteString certificateIssuer = normalizeDN(schema, dnstring);
         return createEncodedValue(serialNumber, certificateIssuer);
     }
 
@@ -183,14 +183,14 @@
             throw DecodeException.error(message);
         }
 
-        final String certificateIssuer = normalizeDN(schema, dnstring);
+        final ByteString certificateIssuer = normalizeDN(schema, dnstring);
         return DefaultAssertion.equality(createEncodedValue(serialNumber, certificateIssuer));
     }
 
-    private String normalizeDN(final Schema schema, final String dnstring) throws DecodeException {
+    private ByteString normalizeDN(final Schema schema, final String dnstring) throws DecodeException {
         try {
             DN dn = DN.valueOf(dnstring, schema.asNonStrictSchema());
-            return dn.toNormalizedString();
+            return dn.toIrreversibleNormalizedByteString();
         } catch (Exception e) {
             logger.traceException(e);
 
@@ -210,10 +210,9 @@
      *
      * @return the encoded ByteString
      */
-    private static ByteString createEncodedValue(BigInteger serial,
-            String issuerDN) {
+    private static ByteString createEncodedValue(BigInteger serial, ByteString issuerDN) {
         ByteStringBuilder builder = new ByteStringBuilder();
-        builder.append(StaticUtils.getBytes(issuerDN));
+        builder.append(issuerDN);
         builder.append((byte) 0); // Separator
         builder.append(serial.toByteArray());
         return builder.toByteString();
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleImpl.java
index ad87cce..71a36f5 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleImpl.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/DistinguishedNameEqualityMatchingRuleImpl.java
@@ -43,7 +43,7 @@
             throws DecodeException {
         try {
             DN dn = DN.valueOf(value.toString(), schema.asNonStrictSchema());
-            return ByteString.valueOf(dn.toNormalizedString());
+            return dn.toIrreversibleNormalizedByteString();
         } catch (final LocalizedIllegalArgumentException e) {
             throw DecodeException.error(e.getMessageObject());
         }
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIF.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIF.java
index 3486995..989eb25 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIF.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/LDIF.java
@@ -26,7 +26,6 @@
 package org.forgerock.opendj.ldif;
 
 import static com.forgerock.opendj.ldap.CoreMessages.*;
-import static com.forgerock.opendj.util.StaticUtils.getBytes;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -448,7 +447,7 @@
         while (patch.hasNext()) {
             final ChangeRecord change = patch.readChangeRecord();
             final DN changeDN = change.getName();
-            final byte[] changeNormDN = getBytes(change.getName().toNormalizedString());
+            final byte[] changeNormDN = toNormalizedByteArray(change.getName());
 
             final DecodeException de =
                     change.accept(new ChangeRecordVisitor<DecodeException, Void>() {
@@ -462,8 +461,7 @@
                                 try {
                                     final Entry entry =
                                             listener.handleDuplicateEntry(change, existingEntry);
-                                    entries.put(getBytes(entry.getName().toNormalizedString()),
-                                            encodeEntry(entry)[1]);
+                                    entries.put(toNormalizedByteArray(entry.getName()), encodeEntry(entry)[1]);
                                 } catch (final DecodeException e) {
                                     return e;
                                 }
@@ -489,9 +487,8 @@
                                     if (change.getControl(SubtreeDeleteRequestControl.DECODER,
                                             new DecodeOptions()) != null) {
                                         entries.subMap(
-                                                getBytes(change.getName().toNormalizedString()),
-                                                getBytes(change.getName().child(RDN.maxValue())
-                                                        .toNormalizedString())).clear();
+                                            toNormalizedByteArray(change.getName()),
+                                            toNormalizedByteArray(change.getName().child(RDN.maxValue()))).clear();
                                     } else {
                                         entries.remove(changeNormDN);
                                     }
@@ -535,8 +532,7 @@
                                 // @formatter:off
                                 final Iterator<Map.Entry<byte[], byte[]>> i =
                                     entries.subMap(changeNormDN,
-                                        getBytes(changeDN.child(RDN.maxValue()).
-                                                toNormalizedString())).entrySet().iterator();
+                                        toNormalizedByteArray(changeDN.child(RDN.maxValue()))).entrySet().iterator();
                                 // @formatter:on
 
                                 while (i.hasNext()) {
@@ -544,8 +540,7 @@
                                     final Entry entry = decodeEntry(e.getValue());
                                     final DN renamedDN = entry.getName().rename(oldDN, newDN);
                                     entry.setName(renamedDN);
-                                    renamedEntries.put(getBytes(renamedDN.toNormalizedString()),
-                                            encodeEntry(entry)[1]);
+                                    renamedEntries.put(toNormalizedByteArray(renamedDN), encodeEntry(entry)[1]);
                                     i.remove();
                                 }
 
@@ -562,18 +557,16 @@
                                     targetEntry.addAttribute(ava.toAttribute());
                                 }
 
-                                renamedEntries.remove(getBytes(targetEntry.getName()
-                                        .toNormalizedString()));
-                                renamedEntries.put(getBytes(targetEntry.getName()
-                                        .toNormalizedString()), encodeEntry(targetEntry)[1]);
+                                renamedEntries.remove(toNormalizedByteArray(targetEntry.getName()));
+                                renamedEntries.put(toNormalizedByteArray(targetEntry.getName()),
+                                        encodeEntry(targetEntry)[1]);
 
                                 // Add the renamed entries.
                                 final Iterator<byte[]> j = renamedEntries.values().iterator();
                                 while (j.hasNext()) {
                                     final Entry renamedEntry = decodeEntry(j.next());
                                     final byte[] existingEntryDn =
-                                            entries.get(getBytes(renamedEntry.getName()
-                                                    .toNormalizedString()));
+                                            entries.get(toNormalizedByteArray(renamedEntry.getName()));
 
                                     if (existingEntryDn != null) {
                                         final Entry existingEntry = decodeEntry(existingEntryDn);
@@ -581,15 +574,12 @@
                                             final Entry tmp =
                                                     listener.handleDuplicateEntry(change,
                                                             existingEntry, renamedEntry);
-                                            entries.put(
-                                                    getBytes(tmp.getName().toNormalizedString()),
-                                                    encodeEntry(tmp)[1]);
+                                            entries.put(toNormalizedByteArray(tmp.getName()), encodeEntry(tmp)[1]);
                                         } catch (final DecodeException e) {
                                             return e;
                                         }
                                     } else {
-                                        entries.put(getBytes(renamedEntry.getName()
-                                                .toNormalizedString()),
+                                        entries.put(toNormalizedByteArray(renamedEntry.getName()),
                                                 encodeEntry(renamedEntry)[1]);
                                     }
                                 }
@@ -839,10 +829,14 @@
         }
     }
 
+    private static byte[] toNormalizedByteArray(DN dn) {
+        return dn.toIrreversibleNormalizedByteString().toByteArray();
+    }
+
     private static byte[][] encodeEntry(final Entry entry) {
         final byte[][] bEntry = new byte[2][];
         // Store normalized DN
-        bEntry[0] = getBytes(entry.getName().toNormalizedString());
+        bEntry[0] = toNormalizedByteArray(entry.getName());
         try {
             // Store ASN1 representation of the entry.
             final ByteStringBuilder bsb = new ByteStringBuilder();
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/CompactDnTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/CompactDnTestCase.java
new file mode 100644
index 0000000..7c52578
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/CompactDnTestCase.java
@@ -0,0 +1,115 @@
+/*
+ * 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 2014 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import org.forgerock.opendj.ldap.DN.CompactDn;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.fest.assertions.Assertions.*;
+
+/**
+ * This class defines a set of tests for the org.forgerock.opendj.ldap.DN.CompactDn class.
+ */
+@SuppressWarnings("javadoc")
+public class CompactDnTestCase extends SdkTestCase {
+
+    /**
+     * DN test data provider.
+     *
+     * @return The array of test DN strings.
+     */
+    @DataProvider
+    public Object[][] equivalentDnRepresentations() {
+        return new Object[][] {
+            { "" , "" },
+            { "   " , "" },
+            { "cn=" , "cn=" },
+            { "cn= " , "cn=" },
+            { "cn =" , "cn=" },
+            { "cn = " , "cn=" },
+            { "dc=com" , "dc=com" },
+            { "dc=com+o=com" , "dc=com+o=com" },
+            { "DC=COM" , "DC=COM" },
+            { "dc = com" , "dc=com" },
+            { " dc = com " , "dc=com" },
+            { "dc=example,dc=com" , "dc=example,dc=com" },
+            { "dc=example, dc=com" , "dc=example,dc=com" },
+            { "dc=example ,dc=com" , "dc=example,dc=com" },
+            { "dc =example , dc  =   com" , "dc=example,dc=com" },
+            { "givenName=John+cn=Doe,ou=People,dc=example,dc=com",
+                "givenName=John+cn=Doe,ou=People,dc=example,dc=com" },
+            { "givenName=John\\+cn=Doe,ou=People,dc=example,dc=com",
+                "givenName=John\\+cn=Doe,ou=People,dc=example,dc=com" },
+            { "cn=Doe\\, John,ou=People,dc=example,dc=com", "cn=Doe\\, John,ou=People,dc=example,dc=com" },
+            { "UID=jsmith,DC=example,DC=net", "UID=jsmith,DC=example,DC=net" },
+            { "OU=Sales+CN=J. Smith,DC=example,DC=net",
+                "OU=Sales+CN=J. Smith,DC=example,DC=net" },
+            { "CN=James \\\"Jim\\\" Smith\\, III,DC=example,DC=net",
+                "CN=James \\\"Jim\\\" Smith\\, III,DC=example,DC=net" },
+            { "CN=John Smith\\2C III,DC=example,DC=net", "CN=John Smith\\, III,DC=example,DC=net" },
+            { "CN=\\23John Smith\\20,DC=example,DC=net", "CN=\\#John Smith\\ ,DC=example,DC=net" },
+            { "CN=Before\\0dAfter,DC=example,DC=net",
+                // \0d is a hex representation of Carriage return. It is mapped
+                // to a SPACE as defined in the MAP ( RFC 4518)
+                "CN=Before\\0dAfter,DC=example,DC=net" },
+            { "2.5.4.3=#04024869",
+                // Unicode codepoints from 0000-0008 are mapped to nothing.
+                "2.5.4.3=\\04\\02Hi" },
+            { "1.1.1=" , "1.1.1=" },
+            { "CN=Lu\\C4\\8Di\\C4\\87" , "CN=Lu\u010di\u0107" },
+            { "ou=\\e5\\96\\b6\\e6\\a5\\ad\\e9\\83\\a8,o=Airius", "ou=\u55b6\u696d\u90e8,o=Airius" },
+            { "photo=\\ john \\ ,dc=com" , "photo=\\ john \\ ,dc=com" },
+            { "AB-global=" , "AB-global=" },
+            { "OU= Sales + CN = J. Smith ,DC=example,DC=net", "OU=Sales+CN=J. Smith,DC=example,DC=net" },
+            { "cn=John+a=b" , "cn=John+a=b" },
+            { "O=\"Sue, Grabbit and Runn\",C=US", "O=Sue\\, Grabbit and Runn,C=US" }, };
+    }
+
+    @Test(dataProvider = "equivalentDnRepresentations")
+    public void testEquals(String dn, String otherDn) throws Exception {
+        CompactDn compactDn = DN.valueOf(dn).compact();
+        CompactDn compactDnEager = DN.valueOf(dn).compactEagerly();
+        CompactDn compactOtherDn = DN.valueOf(otherDn).compact();
+        CompactDn compactOtherDnEager = DN.valueOf(otherDn).compactEagerly();
+
+        assertThat(compactDn).isEqualTo(compactOtherDn);
+        assertThat(compactDn).isEqualTo(compactOtherDnEager);
+        assertThat(compactDnEager).isEqualTo(compactOtherDn);
+    }
+
+    @Test(dataProvider = "equivalentDnRepresentations")
+    public void testCompareTo(String dn, String otherDn) throws Exception {
+        CompactDn compactDn = DN.valueOf(dn).compact();
+        CompactDn compactDnEager = DN.valueOf(dn).compactEagerly();
+        CompactDn compactOtherDn = DN.valueOf(otherDn).compact();
+        CompactDn compactOtherDnEager = DN.valueOf(otherDn).compactEagerly();
+
+        assertThat(compactDn.compareTo(compactOtherDn)).isEqualTo(0);
+        assertThat(compactDn.compareTo(compactOtherDnEager)).isEqualTo(0);
+        assertThat(compactDnEager.compareTo(compactOtherDn)).isEqualTo(0);
+    }
+}

--
Gitblit v1.10.0