mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

davidely
17.28.2007 13243b7d59ccb89dbd12fdf50b6eb56e16b07f26
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.
1 files added
16 files modified
1544 ■■■■ changed files
opends/build.xml 29 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/controls/MatchedValuesFilter.java 9 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/core/BackendConfigManager.java 1 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/core/ConnectionHandlerConfigManager.java 1 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/core/PluginConfigManager.java 1 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/core/SynchronizationProviderConfigManager.java 1 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/extensions/TraditionalWorkQueue.java 3 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/loggers/DirectoryDebugLogger.java 2 ●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/loggers/LoggerThread.java 7 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/protocols/ldap/LDAPFilter.java 3 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/schema/DoubleMetaphoneApproximateMatchingRule.java 141 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/types/Attribute.java 4 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/types/AttributeValue.java 9 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/types/SearchFilter.java 172 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/util/StaticUtils.java 19 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/util/Validator.java 2 ●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/types/SearchFilterTests.java 1140 ●●●●● patch | view | raw | blame | history
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>
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)
          {
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.*;
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.*;
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.*;
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.*;
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
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());
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);
      }
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)
        {
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))
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;
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
      {
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.
   *
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
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>
opends/tests/unit-tests-testng/src/server/org/opends/server/types/SearchFilterTests.java
New file
@@ -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);
  }
}