From b0d7717c48b14cd74dbe264bafd0bdb0c9c097c2 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Wed, 28 Nov 2012 14:37:13 +0000
Subject: [PATCH] Fix OPENDJ-656: Add support for generating LDAP filters from printf style templates

---
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Filter.java         |   81 +++++++++++++++++++++++++++
 opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/FilterTestCase.java |   71 +++++++++++++++++++++++
 2 files changed, 152 insertions(+), 0 deletions(-)

diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Filter.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Filter.java
index 35b639a..875068d 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Filter.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Filter.java
@@ -544,6 +544,50 @@
     }
 
     /**
+     * Returns the LDAP string representation of the provided filter assertion
+     * value in a form suitable for substitution directly into a filter string.
+     * This method may be useful in cases where a filter is to be constructed
+     * from a filter template using {@code String#format(String, Object...)}.
+     * The following example illustrates two approaches to constructing an
+     * equality filter:
+     *
+     * <pre>
+     * // This may contain user input.
+     * String assertionValue = ...;
+     *
+     * // Using the equality filter constructor:
+     * Filter filter = Filter.equality("cn", assertionValue);
+     *
+     * // Using a String template:
+     * String filterTemplate = "(cn=%s)";
+     * String filterString = String.format(filterTemplate,
+     *                                     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.
+     */
+    public static String escapeAssertionValue(final Object assertionValue) {
+        Validator.ensureNotNull(assertionValue);
+        final ByteString bytes = ByteString.valueOf(assertionValue);
+        final StringBuilder builder = new StringBuilder(bytes.length());
+        valueToFilterString(builder, bytes);
+        return builder.toString();
+    }
+
+    /**
      * Creates a new {@code extensible match} filter.
      * <p>
      * If {@code assertionValue} is not an instance of {@code ByteString} then
@@ -871,6 +915,43 @@
         }
     }
 
+    /**
+     * Creates a new filter using the provided filter template and unescaped
+     * assertion values. This method first escapes each of the assertion values
+     * and then substitutes them into the template using
+     * {@link String#format(String, Object...)}. Finally, the formatted string
+     * is parsed as an LDAP filter using {@link #valueOf(String)}.
+     * <p>
+     * This method may be useful in cases where the structure of a filter is not
+     * known at compile time, for example, it may be obtained from a
+     * configuration file. Example usage:
+     *
+     * <pre>
+     * String template = &quot;(|(cn=%s)(uid=user.%s))&quot;;
+     * Filter filter = Filter.valueOf(template, &quot;alice&quot;, 123);
+     * </pre>
+     *
+     * Any assertion values which are not instances of {@code ByteString} will
+     * be converted using the {@link ByteString#valueOf(Object)} method.
+     *
+     * @param template
+     *            The filter template.
+     * @param assertionValues
+     *            The assertion values to be substituted into the template.
+     * @return The formatted template parsed as a {@code Filter}.
+     * @throws LocalizedIllegalArgumentException
+     *             If the formatted template is not a valid LDAP string
+     *             representation of a filter.
+     */
+    public static Filter valueOf(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]);
+        }
+        final String filterString = String.format(template, (Object[]) assertionValueStrings);
+        return valueOf(filterString);
+    }
+
     // Converts an assertion value to a substring filter.
     private static Filter assertionValue2SubstringFilter(final String filterString,
             final String attrType, final int equalPos, final int endPos) {
diff --git a/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/FilterTestCase.java b/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/FilterTestCase.java
index 20d35f2..d6a0725 100644
--- a/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/FilterTestCase.java
+++ b/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/FilterTestCase.java
@@ -27,11 +27,13 @@
 
 package org.forgerock.opendj.ldap;
 
+import static java.util.Arrays.asList;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertTrue;
 
 import java.util.ArrayList;
+import java.util.List;
 
 import org.forgerock.i18n.LocalizedIllegalArgumentException;
 import org.testng.annotations.DataProvider;
@@ -264,4 +266,73 @@
         final Matcher matcher = equal.matcher();
         assertTrue(matcher.matches(entry).toBoolean());
     }
+
+    @DataProvider
+    public Object[][] getAssertionValues() {
+        // Use List for assertion values instead of an array because a List has a
+        // String representation which can be displayed by debuggers, etc.
+
+        // @formatter:off
+        return new Object[][] {
+            {
+                "(objectClass=*)", asList(), "(objectClass=*)"
+            },
+            {
+                "(objectClass=*)", asList("dummy"), "(objectClass=*)"
+            },
+            {
+                "(objectClass=*)", asList("dummy", "dummy"), "(objectClass=*)"
+            },
+            {
+                "(cn=%s)", asList("dummy"), "(cn=dummy)"
+            },
+            {
+                "(|(cn=%s)(uid=user.%s))", asList("alice", (Object) 1234), "(|(cn=alice)(uid=user.1234))"
+            },
+            {
+                "(|(cn=%1$s)(sn=%1$s))", asList("alice"), "(|(cn=alice)(sn=alice))"
+            },
+            // Check escaping.
+            {
+                "(cn=%s)", asList("*"), "(cn=\\2A)"
+            },
+            {
+                "(|(cn=%1$s)(sn=%1$s))", asList("alice)(objectClass=*"),
+                "(|(cn=alice\\29\\28objectClass=\\2A)(sn=alice\\29\\28objectClass=\\2A))"
+            },
+        };
+        // @formatter:on
+    }
+
+    @Test(dataProvider = "getAssertionValues")
+    public void testValueOfTemplate(String template, List<?> assertionValues, String expected)
+            throws Exception {
+        Filter filter = Filter.valueOf(template, assertionValues.toArray());
+        assertEquals(filter.toString(), expected);
+    }
+
+    @DataProvider
+    public Object[][] getEscapeAssertionValues() {
+        // @formatter:off
+        return new Object[][] {
+            {
+                "dummy", "dummy"
+            },
+            {
+                1234, "1234"
+            },
+            {
+                "*", "\\2A"
+            },
+            {
+                "alice)(objectClass=*", "alice\\29\\28objectClass=\\2A"
+            },
+        };
+        // @formatter:on
+    }
+
+    @Test(dataProvider = "getEscapeAssertionValues")
+    public void testEscapeAssertionValue(Object unescaped, String expected) throws Exception {
+        assertEquals(Filter.escapeAssertionValue(unescaped), expected);
+    }
 }

--
Gitblit v1.10.0