From 13243b7d59ccb89dbd12fdf50b6eb56e16b07f26 Mon Sep 17 00:00:00 2001
From: davidely <davidely@localhost>
Date: Wed, 17 Jan 2007 04:28:07 +0000
Subject: [PATCH] Fixes for several small SearchFilter and Attribute matching issues (730, 695, 688, 689, 693).  This also includes tests for the SearchFilter class.  I've also elimiated a race condition from the operation test cases.

---
 opends/src/server/org/opends/server/types/AttributeValue.java                            |    9 
 opends/src/server/org/opends/server/util/StaticUtils.java                                |   19 
 opends/src/server/org/opends/server/core/BackendConfigManager.java                       |    1 
 opends/src/server/org/opends/server/types/Attribute.java                                 |    4 
 opends/src/server/org/opends/server/loggers/LoggerThread.java                            |    7 
 opends/src/server/org/opends/server/extensions/TraditionalWorkQueue.java                 |    3 
 opends/tests/unit-tests-testng/src/server/org/opends/server/types/SearchFilterTests.java | 1140 +++++++++++++++++++++++++++++++++++++++++++
 opends/src/server/org/opends/server/core/PluginConfigManager.java                        |    1 
 opends/src/server/org/opends/server/schema/DoubleMetaphoneApproximateMatchingRule.java   |  141 ++--
 opends/src/server/org/opends/server/types/SearchFilter.java                              |  172 +-----
 opends/src/server/org/opends/server/util/Validator.java                                  |    2 
 opends/src/server/org/opends/server/core/SynchronizationProviderConfigManager.java       |    1 
 opends/src/server/org/opends/server/protocols/ldap/LDAPFilter.java                       |    3 
 opends/src/server/org/opends/server/loggers/DirectoryDebugLogger.java                    |    2 
 opends/build.xml                                                                         |   29 
 opends/src/server/org/opends/server/core/ConnectionHandlerConfigManager.java             |    1 
 opends/src/server/org/opends/server/controls/MatchedValuesFilter.java                    |    9 
 17 files changed, 1,314 insertions(+), 230 deletions(-)

diff --git a/opends/build.xml b/opends/build.xml
index f633ecb..9d430ef 100644
--- a/opends/build.xml
+++ b/opends/build.xml
@@ -32,8 +32,6 @@
   </description>
 
 
-
-
   <!-- General server-wide properties                               -->
   <property name="src.dir"        location="src/server"              />
   <property name="build.dir"      location="build"                   />
@@ -120,11 +118,9 @@
   <property name="dynconstants.stubfile"
         location="${resource.dir}/DynamicConstants.java.stubs" />
 
+
   <property file="PRODUCT"                                                />
 
-
-
-
   <!-- Create a package bundle containing the DSML library. -->
   <target name="dsml" depends="predsml,package"
        description="Build a Directory Server package bundle with DSML.">
@@ -807,6 +803,12 @@
       </not>
     </condition>
 
+    <condition property="test.diff.verbose" value="false">
+      <not>
+        <isset property="test.diff.verbose" />
+      </not>
+    </condition>
+
     <condition property="test.diff.enabled" value="false">
       <isset property="test.diff.disable" />
     </condition>
@@ -827,7 +829,8 @@
                   outputpath="${cvgdiff.report.dir}"
                   diffpath="${basedir}/${test.diff.srcpath}"
                   svnpath="${test.diff.svnpath}" 
-                  enabled="${test.diff.enabled}" />
+                  enabled="${test.diff.enabled}"
+                  verbose="${test.diff.verbose}" />
 
   </target> 
 
@@ -837,39 +840,37 @@
   </target>
 
   <!-- Execute Directory Server TestNG unit tests specified from CLI -->
-  <target name="testcustom"
-          description="Execute the Directory Server TestNG unit tests specified from CLI.">
+  <target name="testcustom">
     <echo message="This target is deprecated. Please use the test target as it now supports the test.* properties." /> 
   </target>
 
   <!-- Execute all of the Directory Server TestNG unit tests in text mode. -->
   <target name="testall"
           depends="enableTestNGAssertions,prepdefaultalltest,testinit,runtests"
-          description="Run all of the TestNG tests.">
+          description="Run all of the TestNG tests with assertions enabled.  See 'testwithcoverage' for properties you can set.">
   </target>
 
 
   <!-- Execute the Directory Server TestNG unit tests in text mode. -->
   <target name="test"
           depends="testinit,runtests"
-          description="Execute the Directory Server TestNG unit tests in text mode.">
+          description="Execute the Directory Server TestNG unit tests in text mode.  Set '-Dorg.opends.test.suppressOutput=true' to suppress output from the unit tests.">
   </target>
 
   <!-- Execute the Directory Server TestNG unit tests in text mode with a coverage report. -->
   <target name="testwithcoverage"
           depends="coverage,test,coveragediff"
-          description="Execute the Directory Server TestNG unit tests in text mode with a coverage report.">
+          description="Execute the Directory Server TestNG unit tests in text mode with a coverage report.  Use -Dtest.packages, -Dtest.classes, or -Dtest.methods to control which unit tests are run.  Use -Dtest.diff.srcpath to control which src files show up in the coverage diff.  See the 'test' package for other properties you can set.">
   </target>
 
   <!-- Execute the Directory Server TestNG unit tests in text mode with a coverage report and slow tests. -->
   <target name="testallwithcoverage"
           depends="coverage,testall,coveragediff"
-          description="Execute the Directory Server TestNG unit tests in text mode with a coverage report.">
+          description="The same as 'testwithcoverage' except 'testall' is run instead of 'test'.">
   </target>
 
   <!-- Execute the Directory Server TestNG unit tests specified from CLI in text mode with a coverage report. -->
-  <target name="testcustomwithcoverage"
-          description="Execute the Directory Server TestNG unit tests specified from CLI in text mode with a coverage report.">
+  <target name="testcustomwithcoverage">
     <echo message="This target is deprecated. Please use the testwithcoverage target as it now supports the test.* properties." />
   </target>
 
diff --git a/opends/src/server/org/opends/server/controls/MatchedValuesFilter.java b/opends/src/server/org/opends/server/controls/MatchedValuesFilter.java
index f818c3f..be8965b 100644
--- a/opends/src/server/org/opends/server/controls/MatchedValuesFilter.java
+++ b/opends/src/server/org/opends/server/controls/MatchedValuesFilter.java
@@ -1844,9 +1844,12 @@
         {
           try
           {
-            return approximateMatchingRule.approximatelyMatch(
-                        assertionValue.getNormalizedValue(),
-                        value.getNormalizedValue());
+            ByteString nv1 =  approximateMatchingRule.normalizeValue(
+                    assertionValue.getNormalizedValue());
+            ByteString nv2 =  approximateMatchingRule.normalizeValue(
+                    value.getNormalizedValue());
+
+            return approximateMatchingRule.approximatelyMatch(nv1, nv2);
           }
           catch (Exception e)
           {
diff --git a/opends/src/server/org/opends/server/core/BackendConfigManager.java b/opends/src/server/org/opends/server/core/BackendConfigManager.java
index 32b13ed..f4e4160 100644
--- a/opends/src/server/org/opends/server/core/BackendConfigManager.java
+++ b/opends/src/server/org/opends/server/core/BackendConfigManager.java
@@ -62,7 +62,6 @@
 import static org.opends.server.loggers.Error.*;
 import static org.opends.server.messages.ConfigMessages.*;
 import static org.opends.server.messages.MessageHandler.*;
-import static org.opends.server.util.ServerConstants.*;
 import static org.opends.server.util.StaticUtils.*;
 
 
diff --git a/opends/src/server/org/opends/server/core/ConnectionHandlerConfigManager.java b/opends/src/server/org/opends/server/core/ConnectionHandlerConfigManager.java
index 65ecb12..d547141 100644
--- a/opends/src/server/org/opends/server/core/ConnectionHandlerConfigManager.java
+++ b/opends/src/server/org/opends/server/core/ConnectionHandlerConfigManager.java
@@ -56,7 +56,6 @@
 import static org.opends.server.messages.ConfigMessages.*;
 import static org.opends.server.messages.CoreMessages.*;
 import static org.opends.server.messages.MessageHandler.*;
-import static org.opends.server.util.ServerConstants.*;
 import static org.opends.server.util.StaticUtils.*;
 
 
diff --git a/opends/src/server/org/opends/server/core/PluginConfigManager.java b/opends/src/server/org/opends/server/core/PluginConfigManager.java
index 130e695..f5065b5 100644
--- a/opends/src/server/org/opends/server/core/PluginConfigManager.java
+++ b/opends/src/server/org/opends/server/core/PluginConfigManager.java
@@ -78,7 +78,6 @@
 import static org.opends.server.messages.ConfigMessages.*;
 import static org.opends.server.messages.MessageHandler.*;
 import static org.opends.server.messages.PluginMessages.*;
-import static org.opends.server.util.ServerConstants.*;
 import static org.opends.server.util.StaticUtils.*;
 
 
diff --git a/opends/src/server/org/opends/server/core/SynchronizationProviderConfigManager.java b/opends/src/server/org/opends/server/core/SynchronizationProviderConfigManager.java
index a5c60b7..420102d 100644
--- a/opends/src/server/org/opends/server/core/SynchronizationProviderConfigManager.java
+++ b/opends/src/server/org/opends/server/core/SynchronizationProviderConfigManager.java
@@ -52,7 +52,6 @@
 import static org.opends.server.loggers.Error.*;
 import static org.opends.server.messages.ConfigMessages.*;
 import static org.opends.server.messages.MessageHandler.*;
-import static org.opends.server.util.ServerConstants.*;
 import static org.opends.server.util.StaticUtils.*;
 
 
diff --git a/opends/src/server/org/opends/server/extensions/TraditionalWorkQueue.java b/opends/src/server/org/opends/server/extensions/TraditionalWorkQueue.java
index ba635fa..c607e8f 100644
--- a/opends/src/server/org/opends/server/extensions/TraditionalWorkQueue.java
+++ b/opends/src/server/org/opends/server/extensions/TraditionalWorkQueue.java
@@ -553,7 +553,8 @@
     }
     catch (InterruptedException ie)
     {
-      assert debugException(CLASS_NAME, "retryNextOperation", ie);
+      // This is somewhat expected so don't log.
+      //      assert debugException(CLASS_NAME, "retryNextOperation", ie);
 
       // If this occurs, then the worker thread must have been interrupted for
       // some reason.  This could be because the Directory Server is shutting
diff --git a/opends/src/server/org/opends/server/loggers/DirectoryDebugLogger.java b/opends/src/server/org/opends/server/loggers/DirectoryDebugLogger.java
index 1ad02d0..7ea6470 100644
--- a/opends/src/server/org/opends/server/loggers/DirectoryDebugLogger.java
+++ b/opends/src/server/org/opends/server/loggers/DirectoryDebugLogger.java
@@ -481,7 +481,7 @@
     {
       StringBuilder buffer = new StringBuilder();
       buffer.append("classname=").append(className).
-        append(" methodname=").append(methodName).append(' ').
+        append(" methodname=").append(methodName).append("\n").
         append( StaticUtils.stackTraceToString(exception));
 
       debugLogger.log(DirectoryLogLevel.ERROR, buffer.toString());
diff --git a/opends/src/server/org/opends/server/loggers/LoggerThread.java b/opends/src/server/org/opends/server/loggers/LoggerThread.java
index b2ab23f..74be7f8 100644
--- a/opends/src/server/org/opends/server/loggers/LoggerThread.java
+++ b/opends/src/server/org/opends/server/loggers/LoggerThread.java
@@ -97,7 +97,12 @@
       try
       {
         sleep(time);
-      } catch(Exception e)
+      }
+      catch(InterruptedException e)
+      {
+        // We expect this to happen.
+      }
+      catch(Exception e)
       {
         assert debugException(CLASS_NAME, "run", e);
       }
diff --git a/opends/src/server/org/opends/server/protocols/ldap/LDAPFilter.java b/opends/src/server/org/opends/server/protocols/ldap/LDAPFilter.java
index be16f37..7673611 100644
--- a/opends/src/server/org/opends/server/protocols/ldap/LDAPFilter.java
+++ b/opends/src/server/org/opends/server/protocols/ldap/LDAPFilter.java
@@ -34,6 +34,7 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.StringTokenizer;
+import java.util.Collection;
 
 import org.opends.server.api.MatchingRule;
 import org.opends.server.core.DirectoryServer;
@@ -175,7 +176,7 @@
     {
       case AND:
       case OR:
-        List<SearchFilter> comps = filter.getFilterComponents();
+        Collection<SearchFilter> comps = filter.getFilterComponents();
         filterComponents = new ArrayList<LDAPFilter>(comps.size());
         for (SearchFilter f : comps)
         {
diff --git a/opends/src/server/org/opends/server/schema/DoubleMetaphoneApproximateMatchingRule.java b/opends/src/server/org/opends/server/schema/DoubleMetaphoneApproximateMatchingRule.java
index 50acb91..bdc1240 100644
--- a/opends/src/server/org/opends/server/schema/DoubleMetaphoneApproximateMatchingRule.java
+++ b/opends/src/server/org/opends/server/schema/DoubleMetaphoneApproximateMatchingRule.java
@@ -276,7 +276,7 @@
           // neither an 'E' nor an 'I' except in "BACHER" and "MACHER".
           if ((pos > 1) &&
               (! isVowel(posMinusTwo = valueString.charAt(pos-2))) &&
-              hasSubstring(valueString, pos-1, pos+2, "ACH") &&
+              hasSubstring(valueString, pos-1, "ACH") &&
               ((posPlusTwo = valueString.charAt(pos+2)) != 'I') &&
               ((posPlusTwo != 'E') ||
                ((valueString.charAt(pos+3) == 'R') &&
@@ -289,7 +289,7 @@
 
 
           // Check for a special case of "caesar", which will be maped to 'S'.
-          if ((pos == 0) && hasSubstring(valueString, pos+1, pos+5, "AESAR"))
+          if ((pos == 0) && hasSubstring(valueString, pos+1, "AESAR"))
           {
             metaphone.append("S");
             pos += 2;
@@ -301,7 +301,7 @@
           if ((posPlusOne = valueString.charAt(pos+1)) == 'H')
           {
             // Check for "chia" as in "chianti" and map to 'K'.
-            if (hasSubstring(valueString, pos+2, pos+4, "IA"))
+            if (hasSubstring(valueString, pos+2, "IA"))
             {
               metaphone.append("K");
               pos += 2;
@@ -309,7 +309,7 @@
             }
 
             // Check for "chae" as in "michael" and map to 'K'.
-            if (hasSubstring(valueString, pos+2, pos+4, "AE"))
+            if (hasSubstring(valueString, pos+2, "AE"))
             {
               metaphone.append("K");
               pos += 2;
@@ -318,13 +318,13 @@
 
             // Check for a Greek root at the beginning of the value like
             // chemistry or chorus and map to 'K'.
-            if ((pos == 0) && (! hasSubstring(valueString, 2, 5, "ORE")) &&
-                (hasSubstring(valueString, 2, 6, "ARAC") ||
-                 hasSubstring(valueString, 2, 6, "ARIS") ||
-                 hasSubstring(valueString, 2, 4, "OR") ||
-                 hasSubstring(valueString, 2, 4, "YM") ||
-                 hasSubstring(valueString, 2, 4, "IA") ||
-                 hasSubstring(valueString, 2, 4, "EM")))
+            if ((pos == 0) && (! hasSubstring(valueString, 2, "ORE")) &&
+                (hasSubstring(valueString, 2, "ARAC") ||
+                 hasSubstring(valueString, 2, "ARIS") ||
+                 hasSubstring(valueString, 2, "OR") ||
+                 hasSubstring(valueString, 2, "YM") ||
+                 hasSubstring(valueString, 2, "IA") ||
+                 hasSubstring(valueString, 2, "EM")))
             {
               metaphone.append("K");
               pos += 2;
@@ -335,9 +335,9 @@
             // Check for "CH" values that produce a "KH" sound that will be
             // mapped to 'K'.
             if (isGermanic(valueString) ||
-                hasSubstring(valueString, pos-2, pos+4, "ORCHES") ||
-                hasSubstring(valueString, pos-2, pos+4, "ARCHIT") ||
-                hasSubstring(valueString, pos-2, pos+4, "ORCHID") ||
+                hasSubstring(valueString, pos-2, "ORCHES") ||
+                hasSubstring(valueString, pos-2, "ARCHIT") ||
+                hasSubstring(valueString, pos-2, "ORCHID") ||
                 ((posPlusTwo = valueString.charAt(pos+2)) == 'T') ||
                 (posPlusTwo == 'S') ||
                 (((pos == 0) ||
@@ -359,7 +359,7 @@
             // All other "CH" values.
             if (pos > 0)
             {
-              if (hasSubstring(valueString, 0, 2, "MC"))
+              if (hasSubstring(valueString, 0, "MC"))
               {
                 metaphone.append("K");
               }
@@ -380,7 +380,7 @@
 
           // Check for "CZ" as in "czerny" but not "wicz" and map to 'S'.
           if ((posPlusOne == 'Z') &&
-              (! hasSubstring(valueString, pos-2, pos, "WI")))
+              (! hasSubstring(valueString, pos-2, "WI")))
           {
             metaphone.append("S");
             pos += 2;
@@ -406,8 +406,8 @@
                 (! ((posPlusTwo == 'H') && valueString.charAt(pos+3) == 'U')))
             {
               if (((pos == 1) && (valueString.charAt(pos-1) == 'A')) ||
-                  hasSubstring(valueString, pos-1, pos+3, "UCCEE") ||
-                  hasSubstring(valueString, pos-1, pos+3, "UCCES"))
+                  hasSubstring(valueString, pos-1, "UCCEE") ||
+                  hasSubstring(valueString, pos-1, "UCCES"))
               {
                 // Values like "accident", "accede", and "succeed".
                 metaphone.append("K");
@@ -619,7 +619,7 @@
             }
             else
             {
-              if ((! hasSubstring(valueString, pos+2, pos+4, "EY")) &&
+              if ((! hasSubstring(valueString, pos+2, "EY")) &&
                   (! isSlavoGermanic(valueString)))
               {
                 metaphone.append("N");
@@ -666,11 +666,11 @@
               (posPlusOne == 'Y')) &&
               ((posMinusOne = valueString.charAt(pos-1)) != 'E') &&
               (posMinusOne != 'I') &&
-              (! hasSubstring(valueString, 0, 6, "DANGER")) &&
-              (! hasSubstring(valueString, 0, 6, "RANGER")) &&
-              (! hasSubstring(valueString, 0, 6, "MANGER")) &&
-              (! hasSubstring(valueString, pos-1, pos+2, "RGY")) &&
-              (! hasSubstring(valueString, pos-1, pos+2, "OGY")))
+              (! hasSubstring(valueString, 0, "DANGER")) &&
+              (! hasSubstring(valueString, 0, "RANGER")) &&
+              (! hasSubstring(valueString, 0, "MANGER")) &&
+              (! hasSubstring(valueString, pos-1, "RGY")) &&
+              (! hasSubstring(valueString, pos-1, "OGY")))
           {
             metaphone.append("K");
             pos += 2;
@@ -681,12 +681,12 @@
           // Check for Italian uses like 'biaggi" and map to 'J'.
           if ((posPlusOne == 'E') || (posPlusOne == 'I') ||
               (posPlusOne == 'Y') ||
-              hasSubstring(valueString, pos-1, pos+3, "AGGI") ||
-              hasSubstring(valueString, pos-1, pos+3, "OGGI"))
+              hasSubstring(valueString, pos-1, "AGGI") ||
+              hasSubstring(valueString, pos-1, "OGGI"))
           {
             // Germanic uses will be mapped to 'K'.
             if (isGermanic(valueString) ||
-                hasSubstring(valueString, pos+1, pos+3, "ET"))
+                hasSubstring(valueString, pos+1, "ET"))
             {
               metaphone.append("K");
             }
@@ -732,14 +732,14 @@
 
         case 'J':
           // Take care of obvious Spanish uses that should map to 'H'.
-          if (hasSubstring(valueString, 0, 4, "SAN "))
+          if (hasSubstring(valueString, 0, "SAN "))
           {
             metaphone.append("H");
             pos++;
             break;
           }
 
-          if (hasSubstring(valueString, pos, pos+4, "JOSE"))
+          if (hasSubstring(valueString, pos, "JOSE"))
           {
             if ((pos == 0) && (valueString.charAt(pos+4) == ' '))
             {
@@ -803,10 +803,10 @@
           {
             pos++;
           }
-          else if (hasSubstring(valueString, pos-1, pos+2, "UMB"))
+          else if (hasSubstring(valueString, pos-1, "UMB"))
           {
             if (((pos+1) == last) ||
-                hasSubstring(valueString, pos+2, pos+4, "ER"))
+                hasSubstring(valueString, pos+2, "ER"))
             {
               pos++;
             }
@@ -868,9 +868,9 @@
         case 'R':
           // Ignore R at the end of French words.
           if ((pos == last) && (! isSlavoGermanic(valueString)) &&
-              hasSubstring(valueString, pos-2, pos, "IE") &&
-              (! hasSubstring(valueString, pos-4, pos-2, "ME")) &&
-              (! hasSubstring(valueString, pos-4, pos-2, "MA")))
+              hasSubstring(valueString, pos-2, "IE") &&
+              (! hasSubstring(valueString, pos-4, "ME")) &&
+              (! hasSubstring(valueString, pos-4, "MA")))
           {
             pos++;
             break;
@@ -891,8 +891,8 @@
 
         case 'S':
           // Special cases like isle and carlysle will be silent.
-          if (hasSubstring(valueString, pos-1, pos+2, "ISL") ||
-              hasSubstring(valueString, pos-1, pos+2, "YSL"))
+          if (hasSubstring(valueString, pos-1, "ISL") ||
+              hasSubstring(valueString, pos-1, "YSL"))
           {
             pos++;
             break;
@@ -900,7 +900,7 @@
 
 
           // Special case of sugar mapped to 'X'.
-          if (hasSubstring(valueString, pos+1, pos+5, "UGAR"))
+          if (hasSubstring(valueString, pos+1, "UGAR"))
           {
             metaphone.append("X");
             pos++;
@@ -911,10 +911,10 @@
           // SH is generally mapped to 'X', but not in Germanic cases.
           if ((posPlusOne = valueString.charAt(pos+1)) == 'H')
           {
-            if (hasSubstring(valueString, pos+1, pos+5, "HEIM") ||
-                hasSubstring(valueString, pos+1, pos+5, "HOEK") ||
-                hasSubstring(valueString, pos+1, pos+5, "HOLM") ||
-                hasSubstring(valueString, pos+1, pos+5, "HOLZ"))
+            if (hasSubstring(valueString, pos+1, "HEIM") ||
+                hasSubstring(valueString, pos+1, "HOEK") ||
+                hasSubstring(valueString, pos+1, "HOLM") ||
+                hasSubstring(valueString, pos+1, "HOLZ"))
             {
               metaphone.append("S");
             }
@@ -929,8 +929,8 @@
 
 
           // Italian and Armenian cases will map to "S".
-          if (hasSubstring(valueString, pos+1, pos+3, "IO") ||
-              hasSubstring(valueString, pos+1, pos+3, "IA"))
+          if (hasSubstring(valueString, pos+1, "IO") ||
+              hasSubstring(valueString, pos+1, "IA"))
           {
             metaphone.append("S");
             pos += 3;
@@ -964,10 +964,10 @@
           {
             if ((posPlusTwo = valueString.charAt(pos+2)) == 'H')
             {
-              if (hasSubstring(valueString, pos+3, pos+5, "OO") ||
-                  hasSubstring(valueString, pos+3, pos+5, "UY") ||
-                  hasSubstring(valueString, pos+3, pos+5, "ED") ||
-                  hasSubstring(valueString, pos+3, pos+5, "EM"))
+              if (hasSubstring(valueString, pos+3, "OO") ||
+                  hasSubstring(valueString, pos+3, "UY") ||
+                  hasSubstring(valueString, pos+3, "ED") ||
+                  hasSubstring(valueString, pos+3, "EM"))
               {
                 metaphone.append("SK");
               }
@@ -997,8 +997,8 @@
           // Ignore a trailing S in French words.  All others will be mapped to
           // 'S'.
           if (! ((pos == last) &&
-                 (hasSubstring(valueString, pos-2, pos, "AI") ||
-                  hasSubstring(valueString, pos-2, pos, "OI"))))
+                 (hasSubstring(valueString, pos-2, "AI") ||
+                  hasSubstring(valueString, pos-2, "OI"))))
           {
             metaphone.append("S");
           }
@@ -1014,9 +1014,9 @@
 
         case 'T':
           // "TION", "TIA", and "TCH" will be mapped to 'X'.
-          if (hasSubstring(valueString, pos, pos+4, "TION") ||
-              hasSubstring(valueString, pos, pos+3, "TIA") ||
-              hasSubstring(valueString, pos, pos+3, "TCH"))
+          if (hasSubstring(valueString, pos, "TION") ||
+              hasSubstring(valueString, pos, "TIA") ||
+              hasSubstring(valueString, pos, "TCH"))
           {
             metaphone.append("X");
             pos += 3;
@@ -1030,8 +1030,8 @@
               ((posPlusOne == 'T') && (valueString.charAt(pos+2) == 'H')))
           {
             if (isGermanic(valueString) ||
-                hasSubstring(valueString, pos+2, pos+4, "OM") ||
-                hasSubstring(valueString, pos+2, pos+4, "AM"))
+                hasSubstring(valueString, pos+2, "OM") ||
+                hasSubstring(valueString, pos+2, "AM"))
             {
               metaphone.append("T");
             }
@@ -1092,8 +1092,8 @@
 
 
           // A Polish value like WICZ or WITZ should be mapped to TS.
-          if (hasSubstring(valueString, pos+1, pos+4, "WICZ") ||
-              hasSubstring(valueString, pos+1, pos+4, "WITZ"))
+          if (hasSubstring(valueString, pos+1, "WICZ") ||
+              hasSubstring(valueString, pos+1, "WITZ"))
           {
             metaphone.append("TS");
             pos += 4;
@@ -1109,10 +1109,10 @@
         case 'X':
           // X maps to KS except at the end of French words.
           if (! ((pos == last) &&
-                 (hasSubstring(valueString, pos-3, pos, "IAU") ||
-                  hasSubstring(valueString, pos-3, pos, "EAU") ||
-                  hasSubstring(valueString, pos-2, pos, "AU") ||
-                  hasSubstring(valueString, pos-2, pos, "OU"))))
+                 (hasSubstring(valueString, pos-3, "IAU") ||
+                  hasSubstring(valueString, pos-3, "EAU") ||
+                  hasSubstring(valueString, pos-2, "AU") ||
+                  hasSubstring(valueString, pos-2, "OU"))))
           {
             metaphone.append("KS");
           }
@@ -1206,24 +1206,35 @@
    *                    determination.
    * @param  start      The position in the value at which to start the
    *                    comparison.
-   * @param  end        The position in the value at which to stop the
-   *                    comparison.  This character will not actually be
-   *                    compared against the provided substring.
    * @param  substring  The substring to compare against the specified value
    *                    range.
    *
    * @return  <CODE>true</CODE> if the specified portion of the value matches
    *          the given substring, or <CODE>false</CODE> if it does not.
    */
-  private boolean hasSubstring(String value, int start, int end,
+  private boolean hasSubstring(String value, int start,
                                String substring)
   {
     assert debugEnter(CLASS_NAME, "hasSubstring", String.valueOf(value),
-                      String.valueOf(start), String.valueOf(end),
+                      String.valueOf(start),
                       String.valueOf(substring));
 
     try
     {
+      // This can happen since a lot of the rules "look behind" and
+      // rightfully don't check if it's the first character
+      if (start < 0) {
+        return false;
+      }
+
+      int end = start + substring.length();
+
+      // value isn't big enough to do the comparison
+      if (end > value.length())
+      {
+        return false;
+      }
+
       for (int i=0,pos=start; pos < end; i++,pos++)
       {
         if (value.charAt(pos) != substring.charAt(i))
diff --git a/opends/src/server/org/opends/server/types/Attribute.java b/opends/src/server/org/opends/server/types/Attribute.java
index fec73c5..8e4a567 100644
--- a/opends/src/server/org/opends/server/types/Attribute.java
+++ b/opends/src/server/org/opends/server/types/Attribute.java
@@ -787,7 +787,7 @@
     ByteString normalizedValue;
     try
     {
-      normalizedValue = value.getNormalizedValue();
+      normalizedValue = matchingRule.normalizeValue(value.getValue());
     }
     catch (Exception e)
     {
@@ -803,7 +803,7 @@
     {
       try
       {
-        ByteString nv = v.getNormalizedValue();
+        ByteString nv = matchingRule.normalizeValue(v.getValue());
         if (matchingRule.approximatelyMatch(nv, normalizedValue))
         {
           return ConditionResult.TRUE;
diff --git a/opends/src/server/org/opends/server/types/AttributeValue.java b/opends/src/server/org/opends/server/types/AttributeValue.java
index ba9ddd2..288bafd 100644
--- a/opends/src/server/org/opends/server/types/AttributeValue.java
+++ b/opends/src/server/org/opends/server/types/AttributeValue.java
@@ -347,7 +347,14 @@
     {
       if (attributeType == null)
       {
-        return normalizedValue.hashCode();
+        if (normalizedValue != null)
+        {
+          return normalizedValue.hashCode();
+        }
+        else
+        {
+          return value.hashCode();
+        }
       }
       else
       {
diff --git a/opends/src/server/org/opends/server/types/SearchFilter.java b/opends/src/server/org/opends/server/types/SearchFilter.java
index 1d91d29..f3ce839 100644
--- a/opends/src/server/org/opends/server/types/SearchFilter.java
+++ b/opends/src/server/org/opends/server/types/SearchFilter.java
@@ -34,6 +34,9 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
+import java.util.LinkedHashSet;
+import java.util.Collection;
+import java.util.Collections;
 
 import org.opends.server.api.MatchingRule;
 import org.opends.server.core.DirectoryServer;
@@ -64,38 +67,38 @@
 
 
   // The attribute type for this filter.
-  private AttributeType attributeType;
+  private final AttributeType attributeType;
 
   // The assertion value for this filter.
-  private AttributeValue assertionValue;
+  private final AttributeValue assertionValue;
 
   // Indicates whether to match on DN attributes for extensible match
   // filters.
-  private boolean dnAttributes;
+  private final boolean dnAttributes;
 
   // The subFinal element for substring filters.
-  private ByteString subFinalElement;
+  private final ByteString subFinalElement;
 
   // The subInitial element for substring filters.
-  private ByteString subInitialElement;
+  private final ByteString subInitialElement;
 
   // The search filter type for this filter.
-  private FilterType filterType;
+  private final FilterType filterType;
 
   // The set of subAny components for substring filters.
-  private List<ByteString> subAnyElements;
+  private final List<ByteString> subAnyElements;
 
   // The set of filter components for AND and OR filters.
-  private List<SearchFilter> filterComponents;
+  private final LinkedHashSet<SearchFilter> filterComponents;
 
   // The not filter component for this search filter.
-  private SearchFilter notComponent;
+  private final SearchFilter notComponent;
 
   // The set of options for the attribute type in this filter.
-  private Set<String> attributeOptions;
+  private final Set<String> attributeOptions;
 
   // The matching rule ID for this search filter.
-  private String matchingRuleID;
+  private final String matchingRuleID;
 
 
 
@@ -122,9 +125,11 @@
    * @param  dnAttributes       Indicates whether to match on DN
    *                            attributes for extensible match
    *                            filters.
+   *
+   * FIXME: this should be private.
    */
   public SearchFilter(FilterType filterType,
-                      List<SearchFilter> filterComponents,
+                      Collection<SearchFilter> filterComponents,
                       SearchFilter notComponent,
                       AttributeType attributeType,
                       Set<String> attributeOptions,
@@ -150,9 +155,21 @@
                               String.valueOf(dnAttributes)
                             });
 
+    // This used to happen in getSubAnyElements, but we do it here
+    // so that we can make this.subAnyElements final.
+    if (subAnyElements == null) {
+      subAnyElements = new ArrayList<ByteString>(0);
+    }
+
+    // This used to happen in getFilterComponents, but we do it here
+    // so that we can make this.filterComponents final.
+    if (filterComponents == null) {
+      filterComponents = Collections.emptyList();
+    }
 
     this.filterType        = filterType;
-    this.filterComponents  = filterComponents;
+    this.filterComponents  =
+            new LinkedHashSet<SearchFilter>(filterComponents);
     this.notComponent      = notComponent;
     this.attributeType     = attributeType;
     this.attributeOptions  = attributeOptions;
@@ -165,7 +182,6 @@
   }
 
 
-
   /**
    * Creates a new AND search filter with the provided information.
    *
@@ -2185,19 +2201,14 @@
 
   /**
    * Retrieves the set of filter components for this AND or OR filter.
-   * The returned list may be modified by the caller.
+   * The returned list can be modified by the caller.
    *
    * @return  The set of filter components for this AND or OR filter.
    */
-  public List<SearchFilter> getFilterComponents()
+  public Set<SearchFilter> getFilterComponents()
   {
     assert debugEnter(CLASS_NAME, "getFilterComponents");
 
-    if (filterComponents == null)
-    {
-      filterComponents = new ArrayList<SearchFilter>(0);
-    }
-
     return filterComponents;
   }
 
@@ -2234,21 +2245,6 @@
 
 
   /**
-   * Specifies the attribute type for this filter.
-   *
-   * @param  attributeType  The attribute type for this filter.
-   */
-  public void setAttributeType(AttributeType attributeType)
-  {
-    assert debugEnter(CLASS_NAME, "setAttributeType",
-                      String.valueOf(attributeType));
-
-    this.attributeType = attributeType;
-  }
-
-
-
-  /**
    * Retrieves the assertion value for this filter.
    *
    * @return  The assertion value for this filter, or
@@ -2264,21 +2260,6 @@
 
 
   /**
-   * Specifies the assertion value for this filter.
-   *
-   * @param  assertionValue  The assertion value for this filter.
-   */
-  public void setAssertionValue(AttributeValue assertionValue)
-  {
-    assert debugEnter(CLASS_NAME, "setAssertionValue",
-                      String.valueOf(assertionValue));
-
-    this.assertionValue = assertionValue;
-  }
-
-
-
-  /**
    * Retrieves the subInitial element for this substring filter.
    *
    * @return  The subInitial element for this substring filter, or
@@ -2294,22 +2275,6 @@
 
 
   /**
-   * Specifies the subInitial element for this substring filter.
-   *
-   * @param  subInitialElement  The subInitial element for this
-   *                            substring filter.
-   */
-  public void setSubInitialElement(ByteString subInitialElement)
-  {
-    assert debugEnter(CLASS_NAME, "setSubInitialElement",
-                      String.valueOf(subInitialElement));
-
-    this.subInitialElement = subInitialElement;
-  }
-
-
-
-  /**
    * Retrieves the set of subAny elements for this substring filter.
    * The returned list may be altered by the caller.
    *
@@ -2319,11 +2284,6 @@
   {
     assert debugEnter(CLASS_NAME, "getSubAnyElements");
 
-    if (subAnyElements == null)
-    {
-      subAnyElements = new ArrayList<ByteString>(0);
-    }
-
     return subAnyElements;
   }
 
@@ -2344,22 +2304,6 @@
 
 
   /**
-   * Specifies the subFinal element for this substring filter.
-   *
-   * @param  subFinalElement  The subFinal element for this substring
-   *                          filter.
-   */
-  public void setSubFinalElement(ByteString subFinalElement)
-  {
-    assert debugEnter(CLASS_NAME, "setSubFinalElement",
-                      String.valueOf(subFinalElement));
-
-    this.subFinalElement = subFinalElement;
-  }
-
-
-
-  /**
    * Retrieves the matching rule ID for this extensible matching
    * filter.
    *
@@ -2376,23 +2320,6 @@
 
 
   /**
-   * Specifies the matching rule ID for this extensible matching
-   * filter.
-   *
-   * @param  matchingRuleID  The matching rule ID for this extensible
-   *                         matching filter.
-   */
-  public void setMatchingRuleID(String matchingRuleID)
-  {
-    assert debugEnter(CLASS_NAME, "setMatchingRuleID",
-                      String.valueOf(matchingRuleID));
-
-    this.matchingRuleID = matchingRuleID;
-  }
-
-
-
-  /**
    * Retrieves the dnAttributes flag for this extensible matching
    * filter.
    *
@@ -2409,23 +2336,6 @@
 
 
   /**
-   * Specifies the value of the dnAttributes flag for this extensible
-   * matching filter.
-   *
-   * @param  dnAttributes  The value of the dnAttributes flag for this
-   *                       extensible matching filter.
-   */
-  public void setDNAttributes(boolean dnAttributes)
-  {
-    assert debugEnter(CLASS_NAME, "setDNAttributes",
-                      String.valueOf(dnAttributes));
-
-    this.dnAttributes = dnAttributes;
-  }
-
-
-
-  /**
    * Indicates whether this search filter matches the provided entry.
    *
    * @param  entry  The entry for which to make the determination.
@@ -3619,7 +3529,6 @@
   }
 
 
-
   /**
    * Indicates whether this search filter is equal to the provided
    * object.
@@ -3721,18 +3630,12 @@
           return false;
         }
 
-outerSubAnyLoop:
-        for (ByteString outer : subAnyElements)
-        {
-          for (ByteString inner : f.subAnyElements)
-          {
-            if (outer.equals(inner))
-            {
-              continue outerSubAnyLoop;
-            }
+        for (int i = 0; i < subAnyElements.size(); i++) {
+          ByteString sub1 = subAnyElements.get(i);
+          ByteString sub2 = f.subAnyElements.get(i);
+          if (!sub1.equals(sub2)) {
+            return false;
           }
-
-          return false;
         }
 
         return true;
@@ -3805,7 +3708,6 @@
   }
 
 
-
   /**
    * Retrieves the hash code for this search filter.
    *
diff --git a/opends/src/server/org/opends/server/util/StaticUtils.java b/opends/src/server/org/opends/server/util/StaticUtils.java
index d14109b..cce743e 100644
--- a/opends/src/server/org/opends/server/util/StaticUtils.java
+++ b/opends/src/server/org/opends/server/util/StaticUtils.java
@@ -1376,6 +1376,24 @@
   }
 
 
+  /**
+   * Return true if and only if o1 and o2 are both null or o1.equals(o2).
+   *
+   * @param o1 the first object to compare
+   * @param o2 the second object to compare
+   * @return true iff o1 and o2 are equal
+   */
+  public static boolean objectsAreEqual(Object o1, Object o2)
+  {
+    if (o1 == null)
+    {
+      return (o2 == null);
+    }
+    else
+    {
+      return o1.equals(o2);
+    }
+  }
 
   /**
    * Retrieves a stack trace from the provided exception as a single-line
@@ -3115,7 +3133,6 @@
   }
 
 
-
   /**
    * Attempts to delete the specified file or directory.  If it is a directory,
    * then any files or subdirectories that it contains will be recursively
diff --git a/opends/src/server/org/opends/server/util/Validator.java b/opends/src/server/org/opends/server/util/Validator.java
index c357927..152adc1 100644
--- a/opends/src/server/org/opends/server/util/Validator.java
+++ b/opends/src/server/org/opends/server/util/Validator.java
@@ -65,7 +65,7 @@
  * happens before the method is invoked cannot be eliminated, e.g.
  * <code>Validator.ensureTrue(someExpensiveCheck())</code> will always invoke
  * <code>someExpensiveCheck()</code>.  When this code is on the critical path,
- * and we do not expect the validation to failure, you can guard the call with
+ * and we do not expect the validation to fail, you can guard the call with
  * an <code>assert</code> because each method returns true, and this code will
  * only be executed when asserts are enabled.
  * <p>
diff --git a/opends/tests/unit-tests-testng/src/server/org/opends/server/types/SearchFilterTests.java b/opends/tests/unit-tests-testng/src/server/org/opends/server/types/SearchFilterTests.java
new file mode 100644
index 0000000..0b3d437
--- /dev/null
+++ b/opends/tests/unit-tests-testng/src/server/org/opends/server/types/SearchFilterTests.java
@@ -0,0 +1,1140 @@
+/*
+ * 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
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE
+ * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
+ * 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
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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
+ *
+ *
+ *      Portions Copyright 2006 Sun Microsystems, Inc.
+ */
+package org.opends.server.types;
+
+import org.opends.server.DirectoryServerTestCase;
+import org.opends.server.TestCaseUtils;
+import org.opends.server.protocols.asn1.ASN1OctetString;
+import org.opends.server.util.StaticUtils;
+import org.opends.server.types.DirectoryException;
+import org.opends.server.core.DirectoryServer;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+import org.testng.annotations.BeforeClass;
+import org.testng.Assert;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+
+import static java.util.Arrays.asList;
+import static org.opends.server.util.StaticUtils.*;
+import static org.testng.Assert.*;
+
+/**
+ * Tests for the org.opends.server.types.SearchFilter class
+ *
+ * This class covers the SearchFilter class fairly well.  The main gaps are
+ * with extensible match, attribute options, and there is a lot of code
+ * that is not reachable because it's in exception handling code that
+ * is not exercisable externally.
+   */
+public class SearchFilterTests extends DirectoryServerTestCase {
+
+  @BeforeClass
+  public void setupClass() throws Exception {
+    TestCaseUtils.startServer();
+  }
+
+  ////////////////////////////////////////////////////////////////////////////
+  ////////////////////////////////////////////////////////////////////////////
+  //
+  // createFilterFromString
+  //
+  ////////////////////////////////////////////////////////////////////////////
+  ////////////////////////////////////////////////////////////////////////////
+
+  // -------------------------------------------------------------------------
+  //
+  // Test valid filters.
+  //
+  // -------------------------------------------------------------------------
+
+  // These are valid filters.
+  @DataProvider(name = "paramsCreateFilterFromStringValidFilters")
+  public Object[][] paramsCreateFilterFromStringValidFilters() {
+    return new Object[][]{
+            {"(&)", "(&)"},
+            {"(|)", "(|)"},
+            {"(sn=test)", "(sn=test)"},
+            {"(sn=*)", "(sn=*)"},
+            {"(sn=)", "(sn=)"},
+            {"(sn=*test*)", "(sn=*test*)"},
+
+            {"(!(sn=test))", "(!(sn=test))"},
+            {"(|(sn=test)(sn=test2))", "(|(sn=test)(sn=test2))"},
+
+            {"(&(sn=test))", "(&(sn=test))"},
+            {"(|(sn=test))", "(|(sn=test))"},
+    };
+  }
+
+  @Test(dataProvider = "paramsCreateFilterFromStringValidFilters")
+  public void testCreateFilterFromStringValidFilters(
+          String originalFilter,
+          String expectedToStringFilter
+  ) throws DirectoryException {
+    runRecreateFilterTest(originalFilter, expectedToStringFilter);
+  }
+
+  private void runRecreateFilterTest(
+          String originalFilter,
+          String expectedToStringFilter
+  ) throws DirectoryException {
+    String regenerated = SearchFilter.createFilterFromString(originalFilter).toString();
+    Assert.assertEquals(regenerated, expectedToStringFilter, "original=" + originalFilter + ", expected=" + expectedToStringFilter);
+  }
+
+  // These are valid filters.
+  @DataProvider(name = "escapeSequenceFilters")
+  public Object[][] escapeSequenceFilters() {
+    final char[] CHAR_NIBBLES = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+                                 'a', 'b', 'c', 'd', 'e', 'f',
+                                 'A', 'B', 'C', 'D', 'E', 'F'};
+
+    final byte[] BYTE_NIBBLES = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
+                                 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
+                                 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F};
+
+    List<String[]> allParameters = new ArrayList<String[]>();
+    for (int i = 0; i < CHAR_NIBBLES.length; i++) {
+      char highNibble = CHAR_NIBBLES[i];
+      byte highByteNibble = BYTE_NIBBLES[i];
+      for (int j = 0; j < CHAR_NIBBLES.length; j++) {
+        char lowNibble = CHAR_NIBBLES[j];
+        byte lowByteNibble = BYTE_NIBBLES[j];
+        String inputChar = "\\" + highNibble + lowNibble;
+        byte byteValue = (byte)((((int)highByteNibble) << 4) | lowByteNibble);
+        String outputChar = getFilterValueForChar(byteValue);
+
+        // Exact match
+        String inputFilter = "(sn=" + inputChar + ")";
+        String outputFilter = "(sn=" + outputChar + ")";
+        allParameters.add(new String[]{inputFilter, outputFilter});
+
+        // Substring
+        inputFilter = "(sn=" + inputChar + "*" + inputChar + "*" + inputChar + ")";
+        outputFilter = "(sn=" + outputChar + "*" + outputChar + "*" + outputChar + ")";
+        allParameters.add(new String[]{inputFilter, outputFilter});
+
+        // <=
+        inputFilter = "(sn<=" + inputChar + ")";
+        outputFilter = "(sn<=" + outputChar + ")";
+        allParameters.add(new String[]{inputFilter, outputFilter});
+
+        // >=
+        inputFilter = "(sn>=" + inputChar + ")";
+        outputFilter = "(sn>=" + outputChar + ")";
+        allParameters.add(new String[]{inputFilter, outputFilter});
+
+        // =~
+        inputFilter = "(sn>=" + inputChar + ")";
+        outputFilter = "(sn>=" + outputChar + ")";
+        allParameters.add(new String[]{inputFilter, outputFilter});
+
+        // =~
+        inputFilter = "(sn:caseExactMatch:=" + inputChar + ")";
+        outputFilter = "(sn:caseExactMatch:=" + outputChar + ")";
+        allParameters.add(new String[]{inputFilter, outputFilter});
+      }
+    }
+
+    return (Object[][]) allParameters.toArray(new String[][]{});
+  }
+
+
+  // These are filters with invalid escape sequences.
+  @DataProvider(name = "invalidEscapeSequenceFilters")
+  public Object[][] invalidEscapeSequenceFilters() {
+    final char[] VALID_NIBBLES = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+                                 'a', 'b', 'c', 'd', 'e', 'f',
+                                 'A', 'B', 'C', 'D', 'E', 'F'};
+
+    final char[] INVALID_NIBBBLES = {'g', 'z', 'G', 'Z', '-', '=', '+', '\00', ')',
+                                     'n', 't', '\\'};
+
+    List<String> invalidEscapeSequences = new ArrayList<String>();
+
+    for (int i = 0; i < VALID_NIBBLES.length; i++) {
+      char validNibble = VALID_NIBBLES[i];
+      for (int j = 0; j < INVALID_NIBBBLES.length; j++) {
+        char invalidNibble = INVALID_NIBBBLES[j];
+
+        invalidEscapeSequences.add("\\" + validNibble + invalidNibble);
+        invalidEscapeSequences.add("\\" + invalidNibble + validNibble);
+      }
+      // Also do a test case where we only have one character in the escape sequence.
+      invalidEscapeSequences.add("\\" + validNibble);
+    }
+
+    List<String[]> allParameters = new ArrayList<String[]>();
+    for (String invalidEscape : invalidEscapeSequences) {
+      // Exact match
+      allParameters.add(new String[]{"(sn=" + invalidEscape + ")"});
+      allParameters.add(new String[]{"(sn=" + invalidEscape});
+
+      // Substring
+      allParameters.add(new String[]{"(sn=" + invalidEscape + "*" + invalidEscape + "*" + invalidEscape + ")"});
+      allParameters.add(new String[]{"(sn=" + invalidEscape + "*" + invalidEscape + "*" + invalidEscape});
+
+      // <=
+      allParameters.add(new String[]{"(sn<=" + invalidEscape + ")"});
+      allParameters.add(new String[]{"(sn<=" + invalidEscape});
+
+      // >=
+      allParameters.add(new String[]{"(sn>=" + invalidEscape + ")"});
+      allParameters.add(new String[]{"(sn>=" + invalidEscape});
+
+      // =~
+      allParameters.add(new String[]{"(sn>=" + invalidEscape + ")"});
+      allParameters.add(new String[]{"(sn>=" + invalidEscape});
+
+      // =~
+      allParameters.add(new String[]{"(sn:caseExactMatch:=" + invalidEscape + ")"});
+      allParameters.add(new String[]{"(sn:caseExactMatch:=" + invalidEscape});
+    }
+
+    return (Object[][]) allParameters.toArray(new String[][]{});
+  }
+
+
+  /**
+   * @return a value that can be used in an LDAP filter.
+   */
+  private String getFilterValueForChar(byte value) {
+    if (((value & 0x7F) != value) ||  // Not 7-bit clean
+         (value <= 0x1F) ||           // Below the printable character range
+         (value == 0x28) ||           // Open parenthesis
+         (value == 0x29) ||           // Close parenthesis
+         (value == 0x2A) ||           // Asterisk
+         (value == 0x5C) ||           // Backslash
+         (value == 0x7F))             // Delete character
+    {
+      return "\\" + StaticUtils.byteToHex(value);
+    } else {
+      return "" + ((char)value);
+    }
+  }
+
+  @Test(dataProvider = "escapeSequenceFilters")
+  public void testRecreateFilterWithEscape(
+          String originalFilter,
+          String expectedToStringFilter
+  ) throws DirectoryException {
+    runRecreateFilterTest(originalFilter, expectedToStringFilter);
+  }
+
+  @Test(dataProvider = "invalidEscapeSequenceFilters",
+        expectedExceptions = DirectoryException.class)
+  public void testFilterWithInvalidEscape(
+          String filterWithInvalidEscape)
+          throws DirectoryException {
+    // This should fail with a parse error.
+    SearchFilter.createFilterFromString(filterWithInvalidEscape);
+  }
+
+
+  // -------------------------------------------------------------------------
+  //
+  // Test invalid filters.
+  //
+  // -------------------------------------------------------------------------
+
+  //
+  // Invalid filters that are detected.
+  //
+
+  @DataProvider(name = "invalidFilters")
+  public Object[][] invalidFilters() {
+    return new Object[][]{
+            {null},
+            {"(cn)"},
+            {"()"},
+            {"("},
+            {"(&(sn=test)"},
+            {"(|(sn=test)"},
+            {"(!(sn=test)"},
+            {"(&(sn=test)))"},
+            {"(|(sn=test)))"},
+            // TODO: open a bug for this.
+//            {"(!(sn=test)))"},
+            {"(sn=\\A)"},
+            {"(sn=\\1H)"},
+            {"(sn=\\H1)"},
+    };
+  }
+
+  @Test(dataProvider = "invalidFilters",
+        expectedExceptions = DirectoryException.class)
+  public void testCreateFilterFromStringInvalidFilters(String invalidFilter)
+          throws DirectoryException {
+    SearchFilter.createFilterFromString(invalidFilter).toString();
+  }
+
+  //
+  // This is more or less the same as what's above, but it's for invalid
+  // filters that are not currently detected by the parser.  To turn these
+  // on, remove them from the broken group.  As the code is modified to handle
+  // these cases, please add these test cases to the
+  // paramsCreateFilterFromStringInvalidFilters DataProvider.
+  //
+
+  @DataProvider(name = "uncaughtInvalidFilters")
+  public Object[][] paramsCreateFilterFromStringUncaughtInvalidFilters() {
+    return new Object[][]{
+            {"(cn=**)"},
+            {"( sn = test )"},
+            {"&(cn=*)"},
+            {"(!(sn=test)(sn=test2))"},
+            {"(objectclass=**)"},
+    };
+  }
+
+  @Test(dataProvider = "uncaughtInvalidFilters",
+        expectedExceptions = DirectoryException.class,
+        // FIXME:  These currently aren't detected
+        enabled = false)
+  public void testCreateFilterFromStringUncaughtInvalidFilters(String invalidFilter)
+          throws DirectoryException {
+    SearchFilter.createFilterFromString(invalidFilter).toString();
+  }
+
+
+  ////////////////////////////////////////////////////////////////////////////
+  ////////////////////////////////////////////////////////////////////////////
+  //
+  // matches
+  //
+  ////////////////////////////////////////////////////////////////////////////
+  ////////////////////////////////////////////////////////////////////////////
+
+  private static final String JOHN_SMITH_LDIF = TestCaseUtils.makeLdif(
+          "dn: cn=John Smith,dc=example,dc=com",
+          "objectclass: inetorgperson",
+          "cn: John Smith",
+          "cn;lang-en: Jonathan Smith",
+          "sn: Smith",
+          "givenname: John",
+          "internationaliSDNNumber: 12345",
+          "displayName: *",
+          "title: tattoos",
+          "labeledUri: http://opends.org/john"
+          );
+
+  @DataProvider(name = "matchesParams")
+  public Object[][] matchesParams() {
+    return new Object[][]{
+            {JOHN_SMITH_LDIF, "(objectclass=inetorgperson)", true},
+            {JOHN_SMITH_LDIF, "(objectclass=iNetOrgPeRsOn)", true},
+            {JOHN_SMITH_LDIF, "(objectclass=*)", true},
+            {JOHN_SMITH_LDIF, "(objectclass=person)", false},
+
+            {JOHN_SMITH_LDIF, "(cn=John Smith)", true},
+            {JOHN_SMITH_LDIF, "(cn=Jonathan Smith)", true},
+            {JOHN_SMITH_LDIF, "(cn=JOHN SmITh)", true},
+            {JOHN_SMITH_LDIF, "(cn=*)", true},
+            {JOHN_SMITH_LDIF, "(cn=*John Smith*)", true},
+            {JOHN_SMITH_LDIF, "(cn=*Jo*ith*)", true},
+            {JOHN_SMITH_LDIF, "(cn=*Jo*i*th*)", true},
+            {JOHN_SMITH_LDIF, "(cn=*Joh*ohn*)", false},  // this shouldn't match
+            {JOHN_SMITH_LDIF, "(internationaliSDNNumber=*23*34*)", false},  // this shouldn't match
+
+            {JOHN_SMITH_LDIF, "(cn=*o*n*)", true},
+            {JOHN_SMITH_LDIF, "(cn=*n*o*)", false},
+
+            // attribute options
+            {JOHN_SMITH_LDIF, "(cn;lang-en=Jonathan Smith)", true},
+            {JOHN_SMITH_LDIF, "(cn;lang-en=Jonathan Smithe)", false},
+            {JOHN_SMITH_LDIF, "(cn;lang-fr=Jonathan Smith)", false},
+            {JOHN_SMITH_LDIF, "(cn;lang-en=*jon*an*)", true},
+
+            // attribute subtypes.  Enable this once 593 is fixed.
+//            {JOHN_SMITH_LDIF, "(name=John Smith)", true},
+//            {JOHN_SMITH_LDIF, "(name=*Smith*)", true},
+//            {JOHN_SMITH_LDIF, "(name;lang-en=Jonathan Smith)", true},  // ? maybe not
+//            {JOHN_SMITH_LDIF, "(name;lang-en=*Jonathan*)", true},  // ? maybe not
+
+            // Enable this once
+//            {JOHN_SMITH_LDIF, "(cn=*Jo**i*th*)", true},
+
+            {JOHN_SMITH_LDIF, "(cn=\\4Aohn*)", true}, // \4A = J
+            {JOHN_SMITH_LDIF, "(|(cn=Jane Smith)(cn=John Smith))", true},
+
+            {JOHN_SMITH_LDIF, "(title~=tattoos)", true},
+            {JOHN_SMITH_LDIF, "(title~=tattos)", true},
+
+            {JOHN_SMITH_LDIF, "(labeledUri=http://opends.org/john)", true},
+            {JOHN_SMITH_LDIF, "(labeledUri=http://opends.org/JOHN)", false},
+            {JOHN_SMITH_LDIF, "(labeledUri=http://*/john)", true},
+            {JOHN_SMITH_LDIF, "(labeledUri=http://*/JOHN)", false},
+
+            {JOHN_SMITH_LDIF, "(cn>=John Smith)", true},
+            {JOHN_SMITH_LDIF, "(cn>=J)", true},
+            {JOHN_SMITH_LDIF, "(cn<=J)", false},
+
+            {JOHN_SMITH_LDIF, "(cn=Jane Smith)", false},
+
+            {JOHN_SMITH_LDIF, "(displayName=\\2A)", true}, // \2A = *
+
+            // 2.5.4.4 is Smith
+            {JOHN_SMITH_LDIF, "(2.5.4.4=Smith)", true},
+
+            {JOHN_SMITH_LDIF, "(sn:caseExactMatch:=Smith)", true},
+            {JOHN_SMITH_LDIF, "(sn:caseExactMatch:=smith)", false},
+
+            // Test cases for 730
+            {JOHN_SMITH_LDIF, "(internationaliSDNNumber=*12*45*)", true},
+            {JOHN_SMITH_LDIF, "(internationaliSDNNumber=*45*12*)", false},
+
+            // TODO: open a bug for all of these.
+//            {JOHN_SMITH_LDIF, "(:caseExactMatch:=Smith)", true},
+//            {JOHN_SMITH_LDIF, "(:caseExactMatch:=NotSmith)", false},
+
+            // Look at 4515 for some more examples.  Ask Neil.
+//            {JOHN_SMITH_LDIF, "(:dn:caseExactMatch:=example)", true},
+//            {JOHN_SMITH_LDIF, "(:dn:caseExactMatch:=notexample)", false},
+    };
+  }
+
+  @Test(dataProvider = "matchesParams")
+  public void testMatches(String ldifEntry, String filterStr, boolean expectMatch) throws Exception {
+    runMatchTest(ldifEntry, filterStr, expectMatch);
+  }
+
+  private void runMatchTest(String ldifEntry, String filterStr, boolean expectMatch) throws Exception {
+    Entry entry = TestCaseUtils.entryFromLdifString(ldifEntry);
+
+    runSingleMatchTest(entry, filterStr, expectMatch);
+    runSingleMatchTest(entry, "(|" + filterStr + ")", expectMatch);
+    runSingleMatchTest(entry, "(&" + filterStr + ")", expectMatch);
+    runSingleMatchTest(entry, "(!" + filterStr + ")", !expectMatch);
+  }
+
+  private void runSingleMatchTest(Entry entry, String filterStr, boolean expectMatch) throws Exception {
+    final SearchFilter filter = SearchFilter.createFilterFromString(filterStr);
+    boolean matches = filter.matchesEntry(entry);
+    Assert.assertEquals(matches, expectMatch, "Filter=" + filter + "\nEntry=" + entry);
+  }
+
+  ////////////////////////////////////////////////////////////////////////////
+  ////////////////////////////////////////////////////////////////////////////
+  //
+  // Filter construction
+  //
+  ////////////////////////////////////////////////////////////////////////////
+  ////////////////////////////////////////////////////////////////////////////
+
+
+  /**
+   *
+   */
+  private static final String makeSimpleLdif(String givenname, String sn) {
+    String cn = givenname + " " + sn;
+    return TestCaseUtils.makeLdif(
+          "dn: cn=" + cn + ",dc=example,dc=com",
+          "objectclass: inetorgperson",
+          "cn: " + cn,
+          "sn: " + sn,
+          "givenname: " + givenname
+          );
+  }
+
+  private static final String JANE_SMITH_LDIF = makeSimpleLdif("Jane", "Smith");
+  private static final String JANE_AUSTIN_LDIF = makeSimpleLdif("Jane", "Austin");
+  private static final String JOE_SMITH_LDIF = makeSimpleLdif("Joe", "Smith");
+  private static final String JOE_AUSTIN_LDIF = makeSimpleLdif("Joe", "Austin");
+
+  private static final List<String> ALL_ENTRIES_LDIF =
+          Collections.unmodifiableList(asList(JANE_SMITH_LDIF,
+                                              JANE_AUSTIN_LDIF,
+                                              JOE_SMITH_LDIF,
+                                              JOE_AUSTIN_LDIF));
+
+
+  /**
+   *
+   */
+  private List<String> getEntriesExcluding(List<String> matchedEntries) {
+    List<String> unmatched = new ArrayList<String>(ALL_ENTRIES_LDIF);
+    unmatched.removeAll(matchedEntries);
+    return unmatched;
+  }
+
+
+  /**
+   *
+   */
+  private static class FilterDescription {
+    private SearchFilter searchFilter;
+
+    private List<String> matchedEntriesLdif;
+    private List<String> unmatchedEntriesLdif;
+
+    private FilterType filterType;
+    private LinkedHashSet<SearchFilter> filterComponents = new LinkedHashSet<SearchFilter>();
+    private SearchFilter notComponent;
+    private AttributeValue assertionValue;
+    private AttributeType attributeType;
+    private ByteString subInitialElement;
+    private List<ByteString> subAnyElements = new ArrayList<ByteString>();
+    private ByteString subFinalElement;
+    private String matchingRuleId;
+    private boolean dnAttributes;
+
+
+    /**
+     *
+     */
+    public void validateFilterFields() throws AssertionError {
+      if (!searchFilter.getFilterType().equals(filterType)) {
+        throwUnequalError("filterTypes");
+      }
+
+      if (!searchFilter.getFilterComponents().equals(filterComponents)) {
+        throwUnequalError("filterComponents");
+      }
+
+      if (!objectsAreEqual(searchFilter.getNotComponent(), notComponent)) {
+        throwUnequalError("notComponent");
+      }
+
+      if (!objectsAreEqual(searchFilter.getAssertionValue(), assertionValue)) {
+        throwUnequalError("assertionValue");
+      }
+
+      if (!objectsAreEqual(searchFilter.getAttributeType(), attributeType)) {
+        throwUnequalError("attributeType");
+      }
+
+      if (!objectsAreEqual(searchFilter.getSubInitialElement(), subInitialElement)) {
+        throwUnequalError("subInitial");
+      }
+
+      if (!objectsAreEqual(searchFilter.getSubAnyElements(), subAnyElements)) {
+        throwUnequalError("subAny");
+      }
+
+      if (!objectsAreEqual(searchFilter.getSubFinalElement(), subFinalElement)) {
+        throwUnequalError("subFinal");
+      }
+
+      if (!objectsAreEqual(searchFilter.getMatchingRuleID(), matchingRuleId)) {
+        throwUnequalError("matchingRuleId");
+      }
+
+      if (searchFilter.getDNAttributes() != dnAttributes) {
+        throwUnequalError("dnAttributes");
+      }
+    }
+
+
+    /**
+     *
+     */
+    private void throwUnequalError(String message) throws AssertionError {
+      throw new AssertionError("Filter differs from what is expected '" + message + "' differ.\n" + toString());
+    }
+
+
+    /**
+     *
+     */
+    @Override
+    public String toString() {
+      return "FilterDescription: \n" +
+              "\tsearchFilter=" + searchFilter + "\n" +
+              "\tfilterType = " + filterType + "\n" +
+              "\tfilterComponents = " + filterComponents + "\n" +
+              "\tnotComponent = " + notComponent + "\n" +
+              "\tassertionValue = " + assertionValue + "\n" +
+              "\tattributeType = " + attributeType + "\n" +
+              "\tsubInitialElement = " + subInitialElement + "\n" +
+              "\tsubAnyElements = " + subAnyElements + "\n" +
+              "\tsubFinalElement = " + subFinalElement + "\n" +
+              "\tmatchingRuleId = " + dnAttributes + "\n";
+    }
+
+
+    /**
+     *
+     */
+    private FilterDescription negate() {
+      FilterDescription negation = new FilterDescription();
+      negation.searchFilter = SearchFilter.createNOTFilter(searchFilter);
+
+      // Flip-flop these
+      negation.matchedEntriesLdif = unmatchedEntriesLdif;
+      negation.unmatchedEntriesLdif = matchedEntriesLdif;
+
+      negation.filterType = FilterType.NOT;
+      negation.notComponent = searchFilter;
+
+      return negation;
+    }
+
+
+    /**
+     *
+     */
+    public FilterDescription clone() {
+      FilterDescription that = new FilterDescription();
+
+      that.searchFilter = this.searchFilter;
+      that.matchedEntriesLdif = this.matchedEntriesLdif;
+      that.unmatchedEntriesLdif = this.unmatchedEntriesLdif;
+      that.filterType = this.filterType;
+      that.filterComponents = this.filterComponents;
+      that.notComponent = this.notComponent;
+      that.assertionValue = this.assertionValue;
+      that.attributeType = this.attributeType;
+      that.subInitialElement = this.subInitialElement;
+      that.subAnyElements = this.subAnyElements;
+      that.subFinalElement = this.subFinalElement;
+      that.matchingRuleId = this.matchingRuleId;
+      that.dnAttributes = this.dnAttributes;
+
+      return that;
+    }
+  }
+
+
+  /**
+   *
+   */
+  private FilterDescription assertionFilterDescription(FilterType filterType,
+                                                       String attributeType,
+                                                       String attributeValue,
+                                                       List<String> matchedEntries) {
+    FilterDescription description = new FilterDescription();
+
+    description.filterType = filterType;
+    description.attributeType = DirectoryServer.getAttributeType(attributeType);
+    description.assertionValue = new AttributeValue(description.attributeType, attributeValue);
+
+    if (filterType == FilterType.EQUALITY) {
+      description.searchFilter = SearchFilter.createEqualityFilter(description.attributeType,
+                                                                   description.assertionValue);
+    } else if (filterType == FilterType.LESS_OR_EQUAL) {
+      description.searchFilter = SearchFilter.createLessOrEqualFilter(description.attributeType,
+                                                                      description.assertionValue);
+    } else if (filterType == FilterType.GREATER_OR_EQUAL) {
+      description.searchFilter = SearchFilter.createGreaterOrEqualFilter(description.attributeType,
+                                                                         description.assertionValue);
+    } else if (filterType == FilterType.APPROXIMATE_MATCH) {
+      description.searchFilter = SearchFilter.createApproximateFilter(description.attributeType,
+                                                                      description.assertionValue);
+    } else {
+      fail(filterType + " is not handled.");
+    }
+
+    description.matchedEntriesLdif = matchedEntries;
+    description.unmatchedEntriesLdif = getEntriesExcluding(matchedEntries);
+
+    return description;
+  }
+
+
+  /**
+   *
+   */
+  private FilterDescription equalityFilterDescription(String attributeType,
+                                                      String attributeValue,
+                                                      List<String> matchedEntries) {
+    return assertionFilterDescription(FilterType.EQUALITY, attributeType, attributeValue, matchedEntries);
+  }
+
+
+  /**
+   *
+   */
+  private FilterDescription lessEqualFilterDescription(String attributeType,
+                                                       String attributeValue,
+                                                       List<String> matchedEntries) {
+    return assertionFilterDescription(FilterType.LESS_OR_EQUAL, attributeType, attributeValue, matchedEntries);
+  }
+
+
+  /**
+   *
+   */
+  private FilterDescription greaterEqualFilterDescription(String attributeType,
+                                                          String attributeValue,
+                                                          List<String> matchedEntries) {
+    return assertionFilterDescription(FilterType.GREATER_OR_EQUAL, attributeType, attributeValue, matchedEntries);
+  }
+
+
+  /**
+   *
+   */
+  private FilterDescription approximateFilterDescription(String attributeType,
+                                                         String attributeValue,
+                                                         List<String> matchedEntries) {
+    return assertionFilterDescription(FilterType.APPROXIMATE_MATCH, attributeType, attributeValue, matchedEntries);
+  }
+
+
+  /**
+   *
+   */
+  private FilterDescription substringFilterDescription(String attributeType,
+                                                       String subInitial,
+                                                       List<String> subAny,
+                                                       String subFinal,
+                                                       List<String> matchedEntries) {
+    FilterDescription description = new FilterDescription();
+
+    description.filterType = FilterType.SUBSTRING;
+    description.attributeType = DirectoryServer.getAttributeType(attributeType);
+
+    description.subInitialElement = new ASN1OctetString(subInitial);
+    description.subAnyElements = new ArrayList<ByteString>();
+    for (int i = 0; (subAny != null) && (i < subAny.size()); i++) {
+      String s = subAny.get(i);
+      description.subAnyElements.add(new ASN1OctetString(s));
+    }
+    description.subFinalElement = new ASN1OctetString(subFinal);
+
+    description.searchFilter = SearchFilter.createSubstringFilter(description.attributeType,
+            description.subInitialElement,
+            description.subAnyElements,
+            description.subFinalElement);
+
+
+    description.matchedEntriesLdif = matchedEntries;
+    description.unmatchedEntriesLdif = getEntriesExcluding(matchedEntries);
+
+    return description;
+  }
+
+
+  /**
+   *
+   */
+  private List<FilterDescription> getNotFilters(List<FilterDescription> filters) {
+    List<FilterDescription> notFilters = new ArrayList<FilterDescription>();
+
+    for (FilterDescription filter: filters) {
+      notFilters.add(filter.negate());
+    }
+
+    return notFilters;
+  }
+
+
+  /**
+   *
+   */
+  private FilterDescription getAndFilter(List<FilterDescription> filters) {
+    FilterDescription andFilter = new FilterDescription();
+
+    List<String> matchedEntries = new ArrayList<String>(ALL_ENTRIES_LDIF);
+    List<SearchFilter> filterComponents = new ArrayList<SearchFilter>();
+
+    for (FilterDescription filter: filters) {
+      matchedEntries.retainAll(filter.matchedEntriesLdif);
+      filterComponents.add(filter.searchFilter);
+    }
+
+    andFilter.searchFilter = SearchFilter.createANDFilter(filterComponents);
+    andFilter.filterComponents = new LinkedHashSet<SearchFilter>(filterComponents);
+
+    andFilter.filterType = FilterType.AND;
+
+    andFilter.matchedEntriesLdif = matchedEntries;
+    andFilter.unmatchedEntriesLdif = getEntriesExcluding(matchedEntries);
+
+    return andFilter;
+  }
+
+
+  /**
+   *
+   */
+  private List<FilterDescription> getAndFilters(List<FilterDescription> filters) {
+    List<FilterDescription> andFilters = new ArrayList<FilterDescription>();
+
+    for (FilterDescription first: filters) {
+      for (FilterDescription second: filters) {
+        andFilters.add(getAndFilter(asList(first, second)));
+      }
+    }
+
+    return andFilters;
+  }
+
+
+  /**
+   *
+   */
+  private FilterDescription getOrFilter(List<FilterDescription> filters) {
+    FilterDescription orFilter = new FilterDescription();
+
+    List<String> unmatchedEntries = new ArrayList<String>(ALL_ENTRIES_LDIF);
+    List<SearchFilter> filterComponents = new ArrayList<SearchFilter>();
+
+    for (FilterDescription filter: filters) {
+      unmatchedEntries.retainAll(filter.unmatchedEntriesLdif);
+      filterComponents.add(filter.searchFilter);
+    }
+
+    orFilter.searchFilter = SearchFilter.createORFilter(filterComponents);
+    orFilter.filterComponents = new LinkedHashSet<SearchFilter>(filterComponents);
+
+    orFilter.filterType = FilterType.OR;
+
+    // Since we're not using Sets, we've whittled down unmatched entries from
+    // the full set instead of adding to matchedEntries, which would lead
+    // to duplicates.
+    orFilter.unmatchedEntriesLdif = unmatchedEntries;
+    orFilter.matchedEntriesLdif = getEntriesExcluding(unmatchedEntries);
+
+    return orFilter;
+  }
+
+
+  /**
+   *
+   */
+  private List<FilterDescription> getOrFilters(List<FilterDescription> filters) {
+    List<FilterDescription> orFilters = new ArrayList<FilterDescription>();
+
+    for (FilterDescription first: filters) {
+      for (FilterDescription second: filters) {
+        orFilters.add(getOrFilter(asList(first, second)));
+      }
+    }
+
+    return orFilters;
+  }
+
+
+  /**
+   *
+   */
+  private List<FilterDescription> getEqualityFilters() throws Exception {
+    List<FilterDescription> descriptions = new ArrayList<FilterDescription>();
+
+    descriptions.add(equalityFilterDescription("sn", "Smith",
+            asList(JANE_SMITH_LDIF, JOE_SMITH_LDIF)));
+
+    descriptions.add(equalityFilterDescription("givenname", "Jane",
+            asList(JANE_SMITH_LDIF, JANE_AUSTIN_LDIF)));
+
+    return descriptions;
+  }
+
+
+  /**
+   *
+   */
+  private List<FilterDescription> getApproximateFilters() throws Exception {
+    List<FilterDescription> descriptions = new ArrayList<FilterDescription>();
+
+    descriptions.add(approximateFilterDescription("sn", "Smythe",
+            asList(JANE_SMITH_LDIF, JOE_SMITH_LDIF)));
+
+    return descriptions;
+  }
+
+
+  /**
+   *
+   */
+  private List<FilterDescription> getSubstringFilters() throws Exception {
+    List<FilterDescription> descriptions = new ArrayList<FilterDescription>();
+
+    descriptions.add(substringFilterDescription(
+            "sn",
+            "S", asList("i"), "th", // S*i*th
+            asList(JANE_SMITH_LDIF, JOE_SMITH_LDIF)));
+
+    return descriptions;
+  }
+
+
+  /**
+   *
+   */
+  private List<FilterDescription> getInequalityFilters() throws Exception {
+    List<FilterDescription> descriptions = new ArrayList<FilterDescription>();
+
+    descriptions.add(lessEqualFilterDescription("sn", "Aus",
+            (List<String>)(new ArrayList<String>())));
+
+    descriptions.add(greaterEqualFilterDescription("sn", "Aus",
+            asList(JANE_AUSTIN_LDIF, JOE_AUSTIN_LDIF,
+                   JANE_SMITH_LDIF, JOE_SMITH_LDIF)));
+
+
+    descriptions.add(lessEqualFilterDescription("sn", "Smi",
+            asList(JANE_AUSTIN_LDIF, JOE_AUSTIN_LDIF)));
+
+    descriptions.add(greaterEqualFilterDescription("sn", "Smi",
+            asList(JANE_SMITH_LDIF, JOE_SMITH_LDIF)));
+
+
+    descriptions.add(lessEqualFilterDescription("sn", "Smith",
+            asList(JANE_AUSTIN_LDIF, JOE_AUSTIN_LDIF,
+                   JANE_SMITH_LDIF, JOE_SMITH_LDIF)));
+
+    descriptions.add(greaterEqualFilterDescription("sn", "Smith",
+            asList(JANE_SMITH_LDIF, JOE_SMITH_LDIF)));
+
+
+    return descriptions;
+  }
+
+
+  /**
+   * Updates to this should also be made in getMinimalFilterDescriptionList.
+   * @see #getMinimalFilterDescriptionList
+   */
+  private List<FilterDescription> getFilterDescriptionList() throws Exception {
+    List<FilterDescription> baseDescriptions = new ArrayList<FilterDescription>();
+
+    baseDescriptions.addAll(getEqualityFilters());
+    baseDescriptions.addAll(getInequalityFilters());
+    baseDescriptions.addAll(getApproximateFilters());
+    baseDescriptions.addAll(getSubstringFilters());
+    baseDescriptions.addAll(getNotFilters(baseDescriptions));
+
+    List<FilterDescription> allDescriptions = new ArrayList<FilterDescription>();
+
+    allDescriptions.addAll(getAndFilters(baseDescriptions));
+    allDescriptions.addAll(getOrFilters(baseDescriptions));
+    allDescriptions.addAll(baseDescriptions);
+
+    return allDescriptions;
+  }
+
+
+  /**
+   *
+   */
+  public List<FilterDescription> getMinimalFilterDescriptionList() throws Exception {
+    List<FilterDescription> baseDescriptions = new ArrayList<FilterDescription>();
+    List<FilterDescription> allDescriptions = new ArrayList<FilterDescription>();
+
+    baseDescriptions.addAll(getEqualityFilters().subList(0, 1));
+    baseDescriptions.addAll(getInequalityFilters().subList(0, 2));
+    baseDescriptions.addAll(getSubstringFilters().subList(0, 1));
+    baseDescriptions.addAll(getNotFilters(baseDescriptions).subList(0, 1));
+
+    allDescriptions.addAll(baseDescriptions);
+    allDescriptions.addAll(getAndFilters(baseDescriptions).subList(0, 2));
+    allDescriptions.addAll(getOrFilters(baseDescriptions).subList(0, 2));
+
+    return allDescriptions;
+  }
+
+
+
+  /**
+   *
+   */
+  @DataProvider(name = "filterDescriptions")
+  public Object[][] getFilterDescriptions() throws Exception {
+    List<FilterDescription> allDescriptions = getFilterDescriptionList();
+
+    // Now convert to [][]
+    FilterDescription[][] descriptionArray = new FilterDescription[allDescriptions.size()][];
+    for (int i = 0; i < allDescriptions.size(); i++) {
+      FilterDescription description = allDescriptions.get(i);
+      descriptionArray[i] = new FilterDescription[]{description};
+    }
+
+    return descriptionArray;
+  }
+
+
+  @Test(dataProvider = "filterDescriptions")
+  public void testFilterConstruction(FilterDescription description) throws Exception {
+    description.validateFilterFields();
+
+    for (String ldif: description.matchedEntriesLdif) {
+      Entry entry = TestCaseUtils.entryFromLdifString(ldif);
+      if (!description.searchFilter.matchesEntry(entry)) {
+        fail("Expected to match entry. " + description + entry);
+      }
+    }
+
+    for (String ldif: description.unmatchedEntriesLdif) {
+      Entry entry = TestCaseUtils.entryFromLdifString(ldif);
+      if (description.searchFilter.matchesEntry(entry)) {
+        fail("Should not have matched entry. " + description + entry);
+      }
+    }
+  }
+
+  // TODO: test more on extensible match and attribute options
+  // TODO: test that we fail when creating filters without specifying all of the parameters
+  // TODO: we need to test attribute options!
+  // TODO: test the audio attribute since it's octetStringMatch
+  // TODO: test the homePhone attribute since   EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch
+  // TODO: test labeledURI since it's  caseExactMatch SUBSTR caseExactSubstringsMatch
+  // TODO: test mail since it's EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch
+  // TODO: test secretary since it's distinguishedNameMatch
+  // TODO: test x500UniqueIdentifier since it's bitStringMatch
+
+
+  private static final Object[][] TEST_EQUALS_PARAMS = new Object[][]{
+          // These have duplicates, and their String representation should even reflect that.
+          {"(&(sn=Smith))", "(&(sn=Smith)(sn=Smith))", true, true},
+          {"(|(sn=Smith))", "(|(sn=Smith)(sn=Smith))", true, true},
+
+          // These are reordered, so they are equivalent, but their String representations will differ
+          {"(&(sn=Smith)(sn<=Aus))", "(&(sn<=Aus)(sn=Smith))", true, false},
+          {"(|(sn=Smith)(sn<=Aus))", "(|(sn<=Aus)(sn=Smith))", true, false},
+
+          // These should be case insensitive
+          {"(SN=Smith)", "(sn=Smith)", true, true},
+          {"(sn=smith)", "(sn=Smith)", true, false},
+          {"(SN=S*th)", "(sn=S*th)", true, true},
+
+          {"(sn:caseExactMatch:=Smith)", "(sn:caseExactMatch:=Smith)", true, true},
+
+          // This demonstrates bug 704.
+//          {"(sn:caseExactMatch:=Smith)", "(sn:caseExactMatch:=smith)", false, false},
+
+          // TODO: open a bug for this.
+//          {"(:dn:caseExactMatch:=example)", "(:DN:caseExactMatch:=example)", true, true}, // ? String not match
+
+          // 2.5.4.4 is 'sn'
+          {"(2.5.4.4=Smith)", "(2.5.4.4=Smith)", true, true},
+          {"(2.5.4.4=Smith)", "(sn=Smith)", true, true},
+
+          {"(sn;lang-en=Smith)", "(sn;lang-en=Smith)", true, true},
+
+          // This demonstrates bug 706
+//          {"(sn;lang-en=Smith)", "(sn=Smith)", false, false},
+
+
+          // This demonstrates bug 705.
+//          {"(sn=s*t*h)", "(sn=S*T*H)", true, false},
+
+          // These should be case sensitive
+          {"(labeledURI=http://opends.org)", "(labeledURI=http://OpenDS.org)", false, false},
+          {"(labeledURI=http://opends*)", "(labeledURI=http://OpenDS*)", false, false},
+
+          // These are WYSIWIG
+          {"(sn=*)", "(sn=*)", true, true},
+          {"(sn=S*)", "(sn=S*th)", false, false},
+          {"(sn=*S)", "(sn=S*th)", false, false},
+          {"(sn=S*t)", "(sn=S*th)", false, false},
+          {"(sn=*i*t*)", "(sn=*i*t*)", true, true},
+          {"(sn=*t*i*)", "(sn=*i*t*)", false, false},  // Test case for 695
+          {"(sn=S*i*t)", "(sn=S*th)", false, false},
+          {"(sn=Smith)", "(sn=Smith)", true, true},
+          {"(sn=Smith)", "(sn<=Aus)", false, false},
+          {"(sn=Smith)", "(sn>=Aus)", false, false},
+          {"(sn=Smith)", "(sn=S*i*th)", false, false},
+          {"(sn=Smith)", "(!(sn=Smith))", false, false},
+          {"(sn=Smith)", "(&(sn=Smith)(sn<=Aus))", false, false},
+          {"(sn=Smith)", "(|(sn=Smith)(sn<=Aus))", false, false},
+          {"(sn<=Aus)", "(sn<=Aus)", true, true},
+          {"(sn<=Aus)", "(sn>=Aus)", false, false},
+          {"(sn<=Aus)", "(sn=S*i*th)", false, false},
+          {"(sn<=Aus)", "(!(sn=Smith))", false, false},
+          {"(sn<=Aus)", "(&(sn=Smith)(sn=Smith))", false, false},
+          {"(sn<=Aus)", "(&(sn=Smith)(sn<=Aus))", false, false},
+          {"(sn<=Aus)", "(|(sn=Smith)(sn=Smith))", false, false},
+          {"(sn<=Aus)", "(|(sn=Smith)(sn<=Aus))", false, false},
+          {"(sn>=Aus)", "(sn>=Aus)", true, true},
+          {"(sn>=Aus)", "(sn=S*i*th)", false, false},
+          {"(sn>=Aus)", "(!(sn=Smith))", false, false},
+          {"(sn>=Aus)", "(&(sn=Smith)(sn=Smith))", false, false},
+          {"(sn>=Aus)", "(&(sn=Smith)(sn<=Aus))", false, false},
+          {"(sn>=Aus)", "(|(sn=Smith)(sn=Smith))", false, false},
+          {"(sn>=Aus)", "(|(sn=Smith)(sn<=Aus))", false, false},
+          {"(sn=S*i*th)", "(sn=S*i*th)", true, true},
+          {"(sn=S*i*th)", "(!(sn=Smith))", false, false},
+          {"(sn=S*i*th)", "(&(sn=Smith)(sn=Smith))", false, false},
+          {"(sn=S*i*th)", "(&(sn=Smith)(sn<=Aus))", false, false},
+          {"(sn=S*i*th)", "(|(sn=Smith)(sn=Smith))", false, false},
+          {"(sn=S*i*th)", "(|(sn=Smith)(sn<=Aus))", false, false},
+          {"(!(sn=Smith))", "(!(sn=Smith))", true, true},
+          {"(!(sn=Smith))", "(&(sn=Smith)(sn=Smith))", false, false},
+          {"(!(sn=Smith))", "(&(sn=Smith)(sn<=Aus))", false, false},
+          {"(!(sn=Smith))", "(|(sn=Smith)(sn=Smith))", false, false},
+          {"(!(sn=Smith))", "(|(sn=Smith)(sn<=Aus))", false, false},
+          {"(&(sn=Smith)(sn=Smith))", "(&(sn=Smith)(sn=Smith))", true, true},
+          {"(&(sn=Smith)(sn=Smith))", "(&(sn=Smith)(sn<=Aus))", false, false},
+          {"(&(sn=Smith)(sn=Smith))", "(|(sn=Smith)(sn=Smith))", false, false},
+          {"(&(sn=Smith)(sn=Smith))", "(|(sn=Smith)(sn<=Aus))", false, false},
+          {"(&(sn=Smith)(sn<=Aus))", "(&(sn=Smith)(sn<=Aus))", true, true},
+          {"(&(sn=Smith)(sn<=Aus))", "(|(sn=Smith)(sn=Smith))", false, false},
+          {"(&(sn=Smith)(sn<=Aus))", "(|(sn=Smith)(sn<=Aus))", false, false},
+          {"(|(sn=Smith)(sn=Smith))", "(|(sn=Smith)(sn=Smith))", true, true},
+          {"(|(sn=Smith)(sn=Smith))", "(|(sn=Smith)(sn<=Aus))", false, false},
+          {"(|(sn=Smith)(sn<=Aus))", "(|(sn=Smith)(sn<=Aus))", true, true},
+          {"(&(sn=Smith)(sn<=Aus))", "(&(sn=Smith)(sn>=Aus))", false, false},
+          {"(|(sn=Smith)(sn<=Aus))", "(|(sn=Smith)(sn>=Aus))", false, false},
+
+
+  };
+
+
+  /**
+   *
+   */
+  @DataProvider(name = "equalsTest")
+  public Object[][] getEqualsTests() throws Exception {
+    return TEST_EQUALS_PARAMS;
+  }
+
+
+  /**
+   *
+   */
+  @Test(dataProvider = "equalsTest")
+  public void testEquals(String stringFilter1, String stringFilter2, boolean expectEquals, boolean expectStringEquals) throws Exception {
+    SearchFilter filter1 = SearchFilter.createFilterFromString(stringFilter1);
+    SearchFilter filter2 = SearchFilter.createFilterFromString(stringFilter2);
+
+    boolean actualEquals = filter1.equals(filter2);
+    assertEquals(actualEquals, expectEquals,
+                 "Expected " + filter1 + (expectEquals ? " == " : " != ") + filter2);
+
+    // Test symmetry
+    actualEquals = filter2.equals(filter1);
+    assertEquals(actualEquals, expectEquals,
+                 "Expected " + filter1 + (expectEquals ? " == " : " != ") + filter2);
+
+    if (expectEquals) {
+      assertEquals(filter1.hashCode(), filter2.hashCode(),
+                   "Hash codes differ for " + filter1 + " and " + filter2);
+    }
+
+    // Test toString
+    actualEquals = filter2.toString().equals(filter1.toString());
+    assertEquals(actualEquals, expectStringEquals,
+                 "Expected " + filter1 + (expectStringEquals ? " == " : " != ") + filter2);
+  }
+}
+

--
Gitblit v1.10.0