From c8f8eff4270d19e5022b6810a47ca0504de47208 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Fri, 30 Nov 2012 00:34:59 +0000
Subject: [PATCH] Fix OPENDJ-656: Add support for generating LDAP filters and DNs from printf style templates

---
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Filter.java         |   15 ++-
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AVA.java            |   58 +++++++-------
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/DN.java             |  109 +++++++++++++++++++++++++++
 opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/FilterTestCase.java |    2 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java     |   31 +++++++
 5 files changed, 181 insertions(+), 34 deletions(-)

diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AVA.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AVA.java
index e46452e..54d8a9e 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AVA.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AVA.java
@@ -148,6 +148,35 @@
         return new AVA(attribute, value);
     }
 
+    static void escapeAttributeValue(final String str, final StringBuilder builder) {
+        if (str.length() > 0) {
+            char c = str.charAt(0);
+            int startPos = 0;
+            if ((c == ' ') || (c == '#')) {
+                builder.append('\\');
+                builder.append(c);
+                startPos = 1;
+            }
+            final int length = str.length();
+            for (int si = startPos; si < length; si++) {
+                c = str.charAt(si);
+                if (c < ' ') {
+                    for (final byte b : getBytes(String.valueOf(c))) {
+                        builder.append('\\');
+                        builder.append(StaticUtils.byteToLowerHex(b));
+                    }
+                } else {
+                    if ((c == ' ' && si == length - 1)
+                            || (c == '"' || c == '+' || c == ',' || c == ';' || c == '<'
+                            || c == '=' || c == '>' || c == '\\' || c == '\u0000')) {
+                        builder.append('\\');
+                    }
+                    builder.append(c);
+                }
+            }
+        }
+    }
+
     private static void appendHexChars(final SubstringReader reader,
             final StringBuilder valueBuffer, final StringBuilder hexBuffer) throws DecodeException {
         final int length = hexBuffer.length();
@@ -720,34 +749,7 @@
                 builder.append("#");
                 StaticUtils.toHex(attributeValue, builder);
             } else {
-                final String str = attributeValue.toString();
-                if (str.length() == 0) {
-                    return builder;
-                }
-                char c = str.charAt(0);
-                int startPos = 0;
-                if ((c == ' ') || (c == '#')) {
-                    builder.append('\\');
-                    builder.append(c);
-                    startPos = 1;
-                }
-                final int length = str.length();
-                for (int si = startPos; si < length; si++) {
-                    c = str.charAt(si);
-                    if (c < ' ') {
-                        for (final byte b : getBytes(String.valueOf(c))) {
-                            builder.append('\\');
-                            builder.append(StaticUtils.byteToLowerHex(b));
-                        }
-                    } else {
-                        if ((c == ' ' && si == length - 1)
-                                || (c == '"' || c == '+' || c == ',' || c == ';' || c == '<'
-                                        || c == '=' || c == '>' || c == '\\' || c == '\u0000')) {
-                            builder.append('\\');
-                        }
-                        builder.append(c);
-                    }
-                }
+                escapeAttributeValue(attributeValue.toString(), builder);
             }
         }
         return builder;
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/DN.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/DN.java
index a32ce02..8c80bb7 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/DN.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/DN.java
@@ -86,6 +86,113 @@
             };
 
     /**
+     * Returns the LDAP string representation of the provided DN attribute value
+     * in a form suitable for substitution directly into a DN string. This
+     * method may be useful in cases where a DN is to be constructed from a DN
+     * template using {@code String#format(String, Object...)}. The following
+     * example illustrates two approaches to constructing a DN:
+     *
+     * <pre>
+     * // This may contain user input.
+     * String attributeValue = ...;
+     *
+     * // Using the equality filter constructor:
+     * DN dn = DN.valueOf("ou=people,dc=example,dc=com").child("uid", attributeValue);
+     *
+     * // Using a String template:
+     * String dnTemplate = "uid=%s,ou=people,dc=example,dc=com";
+     * String dnString = String.format(dnTemplate,
+     *                                 DN.escapeAttributeValue(attributeValue));
+     * DN dn = DN.valueOf(dnString);
+     * </pre>
+     *
+     * <b>Note:</b> attribute values do not and should not be escaped before
+     * passing them to constructors like {@link #child(String, Object)}.
+     * Escaping is only required when creating DN strings.
+     *
+     * @param attributeValue
+     *            The attribute value.
+     * @return The LDAP string representation of the provided filter assertion
+     *         value in a form suitable for substitution directly into a filter
+     *         string.
+     */
+    public static String escapeAttributeValue(final Object attributeValue) {
+        Validator.ensureNotNull(attributeValue);
+        final String s = String.valueOf(attributeValue);
+        final StringBuilder builder = new StringBuilder(s.length());
+        AVA.escapeAttributeValue(s, builder);
+        return builder.toString();
+    }
+
+    /**
+     * Creates a new DN using the provided DN template and unescaped attribute
+     * values using the default schema. This method first escapes each of the
+     * attribute values and then substitutes them into the template using
+     * {@link String#format(String, Object...)}. Finally, the formatted string
+     * is parsed as an LDAP DN using {@link #valueOf(String)}.
+     * <p>
+     * This method may be useful in cases where the structure of a DN is not
+     * known at compile time, for example, it may be obtained from a
+     * configuration file. Example usage:
+     *
+     * <pre>
+     * String template = &quot;uid=%s,ou=people,dc=example,dc=com&quot;;
+     * DN dn = DN.format(template, &quot;bjensen&quot;);
+     * </pre>
+     *
+     * @param template
+     *            The DN template.
+     * @param attributeValues
+     *            The attribute values to be substituted into the template.
+     * @return The formatted template parsed as a {@code DN}.
+     * @throws LocalizedIllegalArgumentException
+     *             If the formatted template is not a valid LDAP string
+     *             representation of a DN.
+     * @see #escapeAttributeValue(Object)
+     */
+    public static DN format(final String template, final Object... attributeValues) {
+        return format(template, Schema.getDefaultSchema(), attributeValues);
+    }
+
+    /**
+     * Creates a new DN using the provided DN template and unescaped attribute
+     * values using the provided schema. This method first escapes each of the
+     * attribute values and then substitutes them into the template using
+     * {@link String#format(String, Object...)}. Finally, the formatted string
+     * is parsed as an LDAP DN using {@link #valueOf(String)}.
+     * <p>
+     * This method may be useful in cases where the structure of a DN is not
+     * known at compile time, for example, it may be obtained from a
+     * configuration file. Example usage:
+     *
+     * <pre>
+     * String template = &quot;uid=%s,ou=people,dc=example,dc=com&quot;;
+     * DN dn = DN.format(template, &quot;bjensen&quot;);
+     * </pre>
+     *
+     * @param template
+     *            The DN template.
+     * @param schema
+     *            The schema to use when parsing the DN.
+     * @param attributeValues
+     *            The attribute values to be substituted into the template.
+     * @return The formatted template parsed as a {@code DN}.
+     * @throws LocalizedIllegalArgumentException
+     *             If the formatted template is not a valid LDAP string
+     *             representation of a DN.
+     * @see #escapeAttributeValue(Object)
+     */
+    public static DN format(final String template, final Schema schema,
+            final Object... attributeValues) {
+        final String[] attributeValueStrings = new String[attributeValues.length];
+        for (int i = 0; i < attributeValues.length; i++) {
+            attributeValueStrings[i] = escapeAttributeValue(attributeValues[i]);
+        }
+        final String dnString = String.format(template, (Object[]) attributeValueStrings);
+        return valueOf(dnString, schema);
+    }
+
+    /**
      * Returns the Root DN. The Root DN does not contain and RDN components and
      * is superior to all other DNs.
      *
@@ -107,6 +214,7 @@
      *             DN.
      * @throws NullPointerException
      *             If {@code dn} was {@code null}.
+     * @see #format(String, Object...)
      */
     public static DN valueOf(final String dn) {
         return valueOf(dn, Schema.getDefaultSchema());
@@ -126,6 +234,7 @@
      *             DN.
      * @throws NullPointerException
      *             If {@code dn} or {@code schema} was {@code null}.
+     * @see #format(String, Schema, Object...)
      */
     public static DN valueOf(final String dn, final Schema schema) {
         Validator.ensureNotNull(dn, schema);
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Filter.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Filter.java
index 875068d..eb2946b 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Filter.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Filter.java
@@ -66,6 +66,9 @@
  * import static org.forgerock.opendj.Filter.*;
  *
  * Filter filter = and(equality("cn", "bjensen"), greaterOrEqual("age", 21));
+ *
+ * // Alternatively use a filter template:
+ * Filter filter = Filter.format("(&(cn=%s)(age>=%s))", "bjensen", 21);
  * </pre>
  *
  * @see <a href="http://tools.ietf.org/html/rfc4511">RFC 4511 - Lightweight
@@ -564,20 +567,20 @@
      *                                     Filter.escapeAssertionValue(assertionValue));
      * Filter filter = Filter.valueOf(filterString);
      * </pre>
+     *
      * If {@code assertionValue} is not an instance of {@code ByteString} then
      * it will be converted using the {@link ByteString#valueOf(Object)} method.
-     *
      * <p>
      * <b>Note:</b> assertion values do not and should not be escaped before
      * passing them to constructors like {@link #equality(String, Object)}.
      * Escaping is only required when creating filter strings.
-     * <p>
-     * <b>Note:</b>
+     *
      * @param assertionValue
      *            The assertion value.
      * @return The LDAP string representation of the provided filter assertion
      *         value in a form suitable for substitution directly into a filter
      *         string.
+     * @see #format(String, Object...)
      */
     public static String escapeAssertionValue(final Object assertionValue) {
         Validator.ensureNotNull(assertionValue);
@@ -881,6 +884,7 @@
      * @throws LocalizedIllegalArgumentException
      *             If {@code string} is not a valid LDAP string representation
      *             of a filter.
+     * @see #format(String, Object...)
      */
     public static Filter valueOf(final String string) {
         Validator.ensureNotNull(string);
@@ -928,7 +932,7 @@
      *
      * <pre>
      * String template = &quot;(|(cn=%s)(uid=user.%s))&quot;;
-     * Filter filter = Filter.valueOf(template, &quot;alice&quot;, 123);
+     * Filter filter = Filter.format(template, &quot;alice&quot;, 123);
      * </pre>
      *
      * Any assertion values which are not instances of {@code ByteString} will
@@ -942,8 +946,9 @@
      * @throws LocalizedIllegalArgumentException
      *             If the formatted template is not a valid LDAP string
      *             representation of a filter.
+     * @see #escapeAssertionValue(Object)
      */
-    public static Filter valueOf(final String template, final Object... assertionValues) {
+    public static Filter format(final String template, final Object... assertionValues) {
         final String[] assertionValueStrings = new String[assertionValues.length];
         for (int i = 0; i < assertionValues.length; i++) {
             assertionValueStrings[i] = escapeAssertionValue(assertionValues[i]);
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java
index fef9be6..1c542b6 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/DNTestCase.java
@@ -979,4 +979,35 @@
         assertEquals(DN.valueOf(dn).rename(DN.valueOf(fromDN), DN.valueOf(toDN)), DN
                 .valueOf(expectedDN));
     }
+
+    /**
+     * Tests the {@link DN#format(String, Object...)} method.
+     */
+    @Test
+    public void testFormatNoEscape() {
+        DN actual = DN.format("deviceId=%s,uid=%s,dc=test", 123, "bjensen");
+        DN expected = DN.valueOf("dc=test").child("uid", "bjensen").child("deviceId", 123);
+        assertEquals(actual, expected);
+        assertEquals(actual.toString(), "deviceId=123,uid=bjensen,dc=test");
+    }
+
+    /**
+     * Tests the {@link DN#format(String, Object...)} method.
+     */
+    @Test
+    public void testFormatEscape() {
+        DN actual = DN.format("uid=%s,dc=test", "#cn=foo+sn=bar");
+        DN expected = DN.valueOf("dc=test").child("uid", "#cn=foo+sn=bar");
+        assertEquals(actual, expected);
+        assertEquals(actual.toString(), "uid=\\#cn\\=foo\\+sn\\=bar,dc=test");
+    }
+
+    /**
+     * Tests the {@link DN#escapeAttributeValue(Object)} method.
+     */
+    @Test
+    public void testEscapeAttributeValue() {
+        String actual = DN.escapeAttributeValue("#cn=foo+sn=bar");
+        assertEquals(actual, "\\#cn\\=foo\\+sn\\=bar");
+    }
 }
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/FilterTestCase.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/FilterTestCase.java
index d6a0725..2b79e13 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/FilterTestCase.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/FilterTestCase.java
@@ -307,7 +307,7 @@
     @Test(dataProvider = "getAssertionValues")
     public void testValueOfTemplate(String template, List<?> assertionValues, String expected)
             throws Exception {
-        Filter filter = Filter.valueOf(template, assertionValues.toArray());
+        Filter filter = Filter.format(template, assertionValues.toArray());
         assertEquals(filter.toString(), expected);
     }
 

--
Gitblit v1.10.0