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

Nicolas Capponi
06.50.2014 4cb8262e95fde77e6a0d2c84f1aa118e3b1ee850
opendj3-server-dev/src/server/org/opends/server/schema/CollationMatchingRuleFactory.java
@@ -26,55 +26,34 @@
 */
package org.opends.server.schema;
import java.nio.CharBuffer;
import java.text.CollationKey;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ByteSequence;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ByteStringBuilder;
import org.forgerock.opendj.ldap.ConditionResult;
import org.forgerock.opendj.ldap.DecodeException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.schema.CoreSchema;
import org.forgerock.opendj.ldap.schema.MatchingRule;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.ldap.spi.IndexQueryFactory;
import org.forgerock.opendj.ldap.spi.IndexingOptions;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.meta.CollationMatchingRuleCfgDefn.MatchingRuleType;
import org.opends.server.admin.std.server.CollationMatchingRuleCfg;
import org.opends.server.api.AbstractMatchingRule;
import org.opends.server.api.ExtensibleIndexer;
import org.opends.server.api.ExtensibleMatchingRule;
import org.opends.server.api.MatchingRule;
import org.opends.server.api.MatchingRuleFactory;
import org.opends.server.api.OrderingMatchingRule;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.ConfigChangeResult;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.InitializationException;
import org.opends.server.util.StaticUtils;
import static org.opends.messages.ConfigMessages.*;
import static org.opends.messages.CoreMessages.*;
import static org.opends.messages.SchemaMessages.*;
import static org.opends.server.schema.SchemaConstants.*;
import static org.opends.server.util.ServerConstants.*;
/**
 * This class is a factory class for Collation matching rules. It
@@ -87,25 +66,6 @@
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
  // Whether equality matching rules are enabled.
  private boolean equalityMatchingRuleType;
  // Whether less-than matching rules are enabled.
  private boolean lessThanMatchingRuleType;
  // Whether less-than-equal-to matching rules are enabled.
  private boolean lessThanEqualToMatchingRuleType;
  // Whether less-than-equal-to matching rules are enabled.
  private boolean greaterThanMatchingRuleType;
  // Whether greater-than matching rules are enabled.
  private boolean greaterThanEqualToMatchingRuleType;
  // Whether greater-than-equal-to matching rules are enabled.
  private boolean substringMatchingRuleType;
  // Stores the list of available locales on this JVM.
  private static final Set<Locale> supportedLocales = new HashSet<Locale>(
      Arrays.asList(Locale.getAvailableLocales()));
@@ -118,7 +78,6 @@
      new HashMap<String, MatchingRule>();
  /**
   * Creates a new instance of CollationMatchingRuleFactory.
   */
@@ -127,19 +86,15 @@
    super();
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public final Collection<MatchingRule> getMatchingRules()
  public final Collection<org.forgerock.opendj.ldap.schema.MatchingRule> getMatchingRules()
  {
    return Collections.unmodifiableCollection(matchingRules.values());
  }
  /**
   * Adds a new mapping of OID and MatchingRule.
   *
@@ -153,22 +108,6 @@
    matchingRules.put(oid, matchingRule);
  }
  /**
   * Returns the Matching rule for the specified OID.
   *
   * @param oid
   *          OID of the matching rule to be searched.
   * @return MatchingRule corresponding to an OID.
   */
  private MatchingRule getMatchingRule(String oid)
  {
    return matchingRules.get(oid);
  }
  /**
   * Clears the Map containing matching Rules.
   */
@@ -177,63 +116,6 @@
    matchingRules.clear();
  }
  /**
   * Reads the configuration and initializes matching rule types.
   *
   * @param ruleTypes
   *          The Set containing allowed matching rule types.
   */
  private void initializeMatchingRuleTypes(SortedSet<MatchingRuleType> ruleTypes)
  {
    for (MatchingRuleType type : ruleTypes)
    {
      switch (type)
      {
      case EQUALITY:
        equalityMatchingRuleType = true;
        break;
      case LESS_THAN:
        lessThanMatchingRuleType = true;
        break;
      case LESS_THAN_OR_EQUAL_TO:
        lessThanEqualToMatchingRuleType = true;
        break;
      case GREATER_THAN:
        greaterThanMatchingRuleType = true;
        break;
      case GREATER_THAN_OR_EQUAL_TO:
        greaterThanEqualToMatchingRuleType = true;
        break;
      case SUBSTRING:
        substringMatchingRuleType = true;
        break;
      default:
        // No default values allowed.
      }
    }
  }
  /**
   * Creates a new Collator instance.
   *
   * @param locale
   *          Locale for the collator
   * @return Returns a new Collator instance
   */
  private Collator createCollator(Locale locale)
  {
    Collator collator = Collator.getInstance(locale);
    collator.setStrength(Collator.PRIMARY);
    collator.setDecomposition(Collator.FULL_DECOMPOSITION);
    return collator;
  }
  /**
   * {@inheritDoc}
   */
@@ -241,7 +123,7 @@
  public void initializeMatchingRule(CollationMatchingRuleCfg configuration)
      throws ConfigException, InitializationException
  {
    initializeMatchingRuleTypes(configuration.getMatchingRuleType());
    final Schema coreSchema = CoreSchema.getInstance();
    for (String collation : configuration.getCollation())
    {
      CollationMapper mapper = new CollationMapper(collation);
@@ -257,12 +139,22 @@
      Locale locale = getLocale(languageTag);
      if (locale != null)
      {
        createLessThanMatchingRule(mapper, locale);
        createLessThanOrEqualToMatchingRule(mapper, locale);
        createEqualityMatchingRule(mapper, locale);
        createGreaterThanOrEqualToMatchingRule(mapper, locale);
        createGreaterThanMatchingRule(mapper, locale);
        createSubstringMatchingRule(mapper, locale);
        try
        {
          final int[] numericSuffixes = { 1, 2, 3, 4, 5, 6 };
          for (int suffix : numericSuffixes)
          {
            final String oid =  nOID + "." + suffix;
            addMatchingRule(oid, coreSchema.getMatchingRule(oid));
          }
          // the default (equality) matching rule
          addMatchingRule(nOID, coreSchema.getMatchingRule(nOID));
        }
        catch (Exception e)
        {
          logger.error(LocalizableMessage.raw("Error when adding a collation matching rule with oid %s, tag %s: %s",
              nOID, languageTag, e.getMessage()));
        }
      }
      else
      {
@@ -327,19 +219,13 @@
    // Clear the associated matching rules.
    resetRules();
    initializeMatchingRuleTypes(configuration.getMatchingRuleType());
    final Schema coreSchema = CoreSchema.getInstance();
    for (String collation : configuration.getCollation())
    {
      // validation has already been performed in isConfigurationChangeAcceptable()
      CollationMapper mapper = new CollationMapper(collation);
      String languageTag = mapper.getLanguageTag();
      Locale locale = getLocale(languageTag);
      createLessThanMatchingRule(mapper, locale);
      createLessThanOrEqualToMatchingRule(mapper, locale);
      createEqualityMatchingRule(mapper, locale);
      createGreaterThanOrEqualToMatchingRule(mapper, locale);
      createGreaterThanMatchingRule(mapper, locale);
      createSubstringMatchingRule(mapper, locale);
      String nOID = mapper.getNumericOID();
      addMatchingRule(nOID, coreSchema.getMatchingRule(nOID));
    }
    try
@@ -360,8 +246,6 @@
    return new ConfigChangeResult(resultCode, adminActionRequired, messages);
  }
  /**
   * {@inheritDoc}
   */
@@ -415,191 +299,6 @@
  }
  private Collection<String> copyNames(MatchingRule matchingRule)
  {
    Collection<String> defaultNames = new HashSet<String>();
    if (matchingRule != null)
    {
      defaultNames.addAll(matchingRule.getNames());
    }
    return defaultNames;
  }
  /**
   * Creates Less-than Matching Rule.
   *
   * @param mapper
   *          CollationMapper containing OID and the language Tag.
   * @param locale
   *          Locale value
   */
  private void createLessThanMatchingRule(CollationMapper mapper, Locale locale)
  {
    if (!lessThanMatchingRuleType) return;
    String oid = mapper.getNumericOID() + ".1";
    String lTag = mapper.getLanguageTag();
    Collection<String> names = copyNames(getMatchingRule(oid));
    names.add(lTag + ".lt");
    names.add(lTag + ".1");
    MatchingRule matchingRule =
        new CollationLessThanMatchingRule(oid, names, locale);
    addMatchingRule(oid, matchingRule);
  }
  /**
   * Creates Less-Than-Equal-To Matching Rule.
   *
   * @param mapper
   *          CollationMapper containing OID and the language Tag.
   * @param locale
   *          Locale value
   */
  private void createLessThanOrEqualToMatchingRule(
      CollationMapper mapper, Locale locale)
  {
    if (!lessThanEqualToMatchingRuleType) return;
    String oid = mapper.getNumericOID() + ".2";
    String lTag = mapper.getLanguageTag();
    Collection<String> names = copyNames(getMatchingRule(oid));
    names.add(lTag + ".lte");
    names.add(lTag + ".2");
    MatchingRule matchingRule =
        new CollationLessThanOrEqualToMatchingRule(oid, names, locale);
    addMatchingRule(oid, matchingRule);
  }
  /**
   * Creates Equality Matching Rule.
   *
   * @param mapper
   *          CollationMapper containing OID and the language Tag.
   * @param locale
   *          Locale value
   */
  private void createEqualityMatchingRule(CollationMapper mapper, Locale locale)
  {
    if (!equalityMatchingRuleType)
    {
      return;
    }
    // Register the default OID as equality matching rule.
    String lTag = mapper.getLanguageTag();
    String nOID = mapper.getNumericOID();
    Collection<String> defaultNames = copyNames(getMatchingRule(nOID));
    defaultNames.add(lTag);
    MatchingRule matchingRule =
        new CollationEqualityMatchingRule(nOID, defaultNames, locale);
    addMatchingRule(nOID, matchingRule);
    // Register OID.3 as the equality matching rule.
    String OID = mapper.getNumericOID() + ".3";
    Collection<String> names = copyNames(getMatchingRule(OID));
    names.add(lTag + ".eq");
    names.add(lTag + ".3");
    MatchingRule equalityMatchingRule =
        new CollationEqualityMatchingRule(OID, names, locale);
    addMatchingRule(OID, equalityMatchingRule);
  }
  /**
   * Creates Greater-than-equal-to Matching Rule.
   *
   * @param mapper
   *          CollationMapper containing OID and the language Tag.
   * @param locale
   *          Locale value
   */
  private void createGreaterThanOrEqualToMatchingRule(
      CollationMapper mapper, Locale locale)
  {
    if (!greaterThanEqualToMatchingRuleType) return;
    String oid = mapper.getNumericOID() + ".4";
    String lTag = mapper.getLanguageTag();
    Collection<String> names = copyNames(getMatchingRule(oid));
    names.add(lTag + ".gte");
    names.add(lTag + ".4");
    MatchingRule matchingRule =
        new CollationGreaterThanOrEqualToMatchingRule(oid, names, locale);
    addMatchingRule(oid, matchingRule);
  }
  /**
   * Creates Greater-than Matching Rule.
   *
   * @param mapper
   *          CollationMapper containing OID and the language Tag.
   * @param locale
   *          Locale value
   */
  private void createGreaterThanMatchingRule(CollationMapper mapper,
      Locale locale)
  {
    if (!greaterThanMatchingRuleType) return;
    String oid = mapper.getNumericOID() + ".5";
    String lTag = mapper.getLanguageTag();
    Collection<String> names = copyNames(getMatchingRule(oid));
    names.add(lTag + ".gt");
    names.add(lTag + ".5");
    MatchingRule matchingRule =
        new CollationGreaterThanMatchingRule(oid, names, locale);
    addMatchingRule(oid, matchingRule);
  }
  /**
   * Creates substring Matching Rule.
   *
   * @param mapper
   *          CollationMapper containing OID and the language Tag.
   * @param locale
   *          Locale value
   */
  private void createSubstringMatchingRule(CollationMapper mapper,
      Locale locale)
  {
    if (!substringMatchingRuleType) return;
    String oid = mapper.getNumericOID() + ".6";
    String lTag = mapper.getLanguageTag();
    Collection<String> names = copyNames(getMatchingRule(oid));
    names.add(lTag + ".sub");
    names.add(lTag + ".6");
    MatchingRule matchingRule =
        new CollationSubstringMatchingRule(oid, names, locale);
    addMatchingRule(oid, matchingRule);
  }
  /**
   * Verifies if the locale is supported by the JVM.
   *
@@ -648,1424 +347,6 @@
  /**
   * Evaluates and converts 2 consecutive characters of the provided string
   * starting at startPos and converts them into a single escaped char.
   *
   * @param hexString
   *          The hexadecimal string containing the escape sequence.
   * @param startPos
   *          The starting position of the hexadecimal escape sequence.
   * @return The escaped character
   * @throws DecodeException
   *           If the provided string contains invalid hexadecimal digits .
   */
  private static char hexToEscapedChar(String hexString, int startPos)
      throws DecodeException
  {
    // The two positions must be the hex characters that
    // comprise the escaped value.
    if ((startPos + 1) >= hexString.length())
    {
      LocalizableMessage message =
          ERR_SEARCH_FILTER_INVALID_ESCAPED_BYTE.get(hexString,
              startPos + 1);
      throw DecodeException.error(message);
    }
    byte byteValue = 0;
    switch (hexString.charAt(startPos))
    {
    case 0x30: // '0'
      break;
    case 0x31: // '1'
      byteValue = (byte) 0x10;
      break;
    case 0x32: // '2'
      byteValue = (byte) 0x20;
      break;
    case 0x33: // '3'
      byteValue = (byte) 0x30;
      break;
    case 0x34: // '4'
      byteValue = (byte) 0x40;
      break;
    case 0x35: // '5'
      byteValue = (byte) 0x50;
      break;
    case 0x36: // '6'
      byteValue = (byte) 0x60;
      break;
    case 0x37: // '7'
      byteValue = (byte) 0x70;
      break;
    case 0x38: // '8'
      byteValue = (byte) 0x80;
      break;
    case 0x39: // '9'
      byteValue = (byte) 0x90;
      break;
    case 0x41: // 'A'
    case 0x61: // 'a'
      byteValue = (byte) 0xA0;
      break;
    case 0x42: // 'B'
    case 0x62: // 'b'
      byteValue = (byte) 0xB0;
      break;
    case 0x43: // 'C'
    case 0x63: // 'c'
      byteValue = (byte) 0xC0;
      break;
    case 0x44: // 'D'
    case 0x64: // 'd'
      byteValue = (byte) 0xD0;
      break;
    case 0x45: // 'E'
    case 0x65: // 'e'
      byteValue = (byte) 0xE0;
      break;
    case 0x46: // 'F'
    case 0x66: // 'f'
      byteValue = (byte) 0xF0;
      break;
    default:
      LocalizableMessage message =
          ERR_SEARCH_FILTER_INVALID_ESCAPED_BYTE.get(hexString, startPos);
      throw DecodeException.error(message);
    }
    switch (hexString.charAt(++startPos))
    {
    case 0x30: // '0'
      break;
    case 0x31: // '1'
      byteValue |= (byte) 0x01;
      break;
    case 0x32: // '2'
      byteValue |= (byte) 0x02;
      break;
    case 0x33: // '3'
      byteValue |= (byte) 0x03;
      break;
    case 0x34: // '4'
      byteValue |= (byte) 0x04;
      break;
    case 0x35: // '5'
      byteValue |= (byte) 0x05;
      break;
    case 0x36: // '6'
      byteValue |= (byte) 0x06;
      break;
    case 0x37: // '7'
      byteValue |= (byte) 0x07;
      break;
    case 0x38: // '8'
      byteValue |= (byte) 0x08;
      break;
    case 0x39: // '9'
      byteValue |= (byte) 0x09;
      break;
    case 0x41: // 'A'
    case 0x61: // 'a'
      byteValue |= (byte) 0x0A;
      break;
    case 0x42: // 'B'
    case 0x62: // 'b'
      byteValue |= (byte) 0x0B;
      break;
    case 0x43: // 'C'
    case 0x63: // 'c'
      byteValue |= (byte) 0x0C;
      break;
    case 0x44: // 'D'
    case 0x64: // 'd'
      byteValue |= (byte) 0x0D;
      break;
    case 0x45: // 'E'
    case 0x65: // 'e'
      byteValue |= (byte) 0x0E;
      break;
    case 0x46: // 'F'
    case 0x66: // 'f'
      byteValue |= (byte) 0x0F;
      break;
    default:
      LocalizableMessage message =
          ERR_SEARCH_FILTER_INVALID_ESCAPED_BYTE.get(hexString, startPos);
      throw DecodeException.error(message);
    }
    return (char) byteValue;
  }
  /**
   * Collation Extensible matching rule.
   */
  private abstract class CollationMatchingRule
          extends AbstractMatchingRule
          implements ExtensibleMatchingRule
  {
    // Names for this class.
    private final Collection<String> names;
    // Collator for performing equality match.
    protected final Collator collator;
    // Numeric OID of the rule.
    private final String nOID;
    // Locale associated with this rule.
    private final Locale locale;
    // Indexer of this rule.
    protected ExtensibleIndexer indexer;
    /**
     * Constructs a new CollationMatchingRule.
     *
     * @param nOID
     *          OID of the collation matching rule
     * @param names
     *          names of this matching rule
     * @param locale
     *          Locale of the collation matching rule
     */
    private CollationMatchingRule(String nOID,
        Collection<String> names, Locale locale)
    {
      this.names = names;
      this.collator = createCollator(locale);
      this.locale = locale;
      this.nOID = nOID;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<String> getNames()
    {
      return Collections.unmodifiableCollection(names);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getOID()
    {
      return nOID;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getDescription()
    {
      // There is no standard description for this matching rule.
      return null;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getSyntaxOID()
    {
      return SYNTAX_DIRECTORY_STRING_OID;
    }
    /**
     * Returns the name of the index database for this matching rule. An
     * index name for this rule will be based upon the Locale. This will
     * ensure that multiple collation matching rules corresponding to
     * the same Locale can share the same index database.
     *
     * @return The name of the index for this matching rule.
     */
    public String getIndexName()
    {
      String language = locale.getLanguage();
      String country = locale.getCountry();
      String variant = locale.getVariant();
      StringBuilder builder = new StringBuilder(language);
      if (country != null && country.length() > 0)
      {
        builder.append("_");
        builder.append(locale.getCountry());
      }
      if (variant != null && variant.length() > 0)
      {
        builder.append("_");
        builder.append(locale.getVariant());
      }
      return builder.toString();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<ExtensibleIndexer> getIndexers()
    {
      if (indexer == null)
      {
        // The default implementation contains shared indexer and
        // doesn't use the config.
        indexer = new CollationSharedExtensibleIndexer(this);
      }
      return Collections.singletonList(indexer);
    }
  }
  /**
   * Collation rule for Equality matching rule.
   */
  private final class CollationEqualityMatchingRule
          extends CollationMatchingRule
          implements OrderingMatchingRule
  {
    /**
     * The serial version identifier required to satisfy the compiler because
     * this class implements the <CODE>java.io.Serializable</CODE> interface.
     * This value was generated using the <CODE>serialver</CODE> command-line
     * utility included with the Java SDK.
     */
    private static final long serialVersionUID = 3990778178484159862L;
    /**
     * Constructs a new CollationEqualityMatchingRule.
     *
     * @param nOID
     *          OID of the collation matching rule
     * @param names
     *          names of this matching rule
     * @param locale
     *          Locale of the collation matching rule
     */
    private CollationEqualityMatchingRule(String nOID,
        Collection<String> names, Locale locale)
    {
      super(nOID, names, locale);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public ByteString normalizeAttributeValue(ByteSequence value)
        throws DecodeException
    {
      CollationKey key = collator.getCollationKey(value.toString());
      return ByteString.wrap(key.toByteArray());
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public ConditionResult valuesMatch(ByteSequence attributeValue,
        ByteSequence assertionValue)
    {
      return ConditionResult.valueOf(assertionValue.equals(attributeValue));
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public <T> T createIndexQuery(ByteSequence assertionValue,
        IndexQueryFactory<T> factory) throws DecodeException
    {
      // Normalize the assertion value.
      return factory.createExactMatchQuery(indexer
          .getExtensibleIndexID(), normalizeAttributeValue(assertionValue));
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public int compare(byte[] arg0, byte[] arg1)
    {
      return StaticUtils.compare(arg0, arg1);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public int compareValues(ByteSequence value1, ByteSequence value2)
    {
      return value1.compareTo(value2);
    }
  }
  /**
   * Collation rule for Substring matching rule.
   */
  private final class CollationSubstringMatchingRule extends
      CollationMatchingRule
  {
    // Substring Indexer associated with this instance.
    private CollationSubstringExtensibleIndexer subIndexer;
    /**
     * Constructs a new CollationSubstringMatchingRule.
     *
     * @param nOID
     *          OID of the collation matching rule
     * @param names
     *          names of this matching rule
     * @param locale
     *          Locale of the collation matching rule
     */
    private CollationSubstringMatchingRule(String nOID,
        Collection<String> names, Locale locale)
    {
      super(nOID, names, locale);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public ByteString normalizeAttributeValue(ByteSequence value)
        throws DecodeException
    {
      CollationKey key = collator.getCollationKey(value.toString());
      return ByteString.wrap(key.toByteArray());
    }
    /**
     * Utility class which abstracts a substring assertion value.
     */
    private final class Assertion
    {
      // Initial part of the substring filter.
      private String subInitial;
      // any parts of the substring filter.
      private List<String> subAny;
      // Final part of the substring filter.
      private String subFinal;
      /**
       * Creates a new instance of Assertion.
       *
       * @param subInitial
       *          Initial part of the filter.
       * @param subAny
       *          Any part of the filter.
       * @param subFinal
       *          Final part of the filter.
       */
      private Assertion(String subInitial, List<String> subAny,
          String subFinal)
      {
        this.subInitial = subInitial;
        this.subAny = subAny;
        this.subFinal = subFinal;
      }
      /**
       * Returns the Initial part of the assertion.
       *
       * @return Initial part of assertion.
       */
      private String getInitial()
      {
        return subInitial;
      }
      /**
       * Returns the any part of the assertion.
       *
       * @return Any part of the assertion.
       */
      private List<String> getAny()
      {
        return subAny;
      }
      /**
       * Returns the final part of the assertion.
       *
       * @return Final part of the assertion.
       */
      private String getFinal()
      {
        return subFinal;
      }
    }
    private Assertion parseAssertion(ByteSequence value) throws DecodeException
    {
      // Get a string representation of the value.
      String filterString = value.toString();
      int endPos = filterString.length();
      // Find the locations of all the asterisks in the value. Also,
      // check to see if there are any escaped values, since they will
      // need special treatment.
      boolean hasEscape = false;
      LinkedList<Integer> asteriskPositions = new LinkedList<Integer>();
      for (int i = 0; i < endPos; i++)
      {
        if (filterString.charAt(i) == 0x2A) // The asterisk.
        {
          asteriskPositions.add(i);
        }
        else if (filterString.charAt(i) == 0x5C) // The backslash.
        {
          hasEscape = true;
        }
      }
      // If there were no asterisks, then this isn't a substring filter.
      if (asteriskPositions.isEmpty())
      {
        throw DecodeException.error(
            ERR_SEARCH_FILTER_SUBSTRING_NO_ASTERISKS.get(filterString, 0, endPos));
      }
      // If the value starts with an asterisk, then there is no
      // subInitial component. Otherwise, parse out the subInitial.
      String subInitial;
      int firstPos = asteriskPositions.removeFirst();
      if (firstPos == 0)
      {
        subInitial = null;
      }
      else
      {
        if (hasEscape)
        {
          CharBuffer buffer = CharBuffer.allocate(firstPos);
          for (int i = 0; i < firstPos; i++)
          {
            if (filterString.charAt(i) == 0x5C)
            {
              char escapeValue = hexToEscapedChar(filterString, i + 1);
              i += 2; // Move to the next sequence.
              buffer.put(escapeValue);
            }
            else
            {
              buffer.put(filterString.charAt(i));
            }
          }
          char[] subInitialChars = new char[buffer.position()];
          buffer.flip();
          buffer.get(subInitialChars);
          subInitial = new String(subInitialChars);
        }
        else
        {
          subInitial = filterString.substring(0, firstPos);
        }
      }
      // Next, process through the rest of the asterisks to get the
      // subAny values.
      List<String> subAny = new ArrayList<String>();
      for (int asteriskPos : asteriskPositions)
      {
        int length = asteriskPos - firstPos - 1;
        if (hasEscape)
        {
          CharBuffer buffer = CharBuffer.allocate(length);
          for (int i = firstPos + 1; i < asteriskPos; i++)
          {
            if (filterString.charAt(i) == 0x5C)
            {
              char escapeValue = hexToEscapedChar(filterString, i + 1);
              i += 2; // Move to the next sequence.
              buffer.put(escapeValue);
            }
            else
            {
              buffer.put(filterString.charAt(i));
            }
          }
          char[] subAnyChars = new char[buffer.position()];
          buffer.flip();
          buffer.get(subAnyChars);
          subAny.add(new String(subAnyChars));
        }
        else
        {
          subAny.add(filterString.substring(firstPos + 1, firstPos
              + length + 1));
        }
        firstPos = asteriskPos;
      }
      // Finally, see if there is anything after the last asterisk,
      // which would be the subFinal value.
      String subFinal;
      if (firstPos == (endPos - 1))
      {
        subFinal = null;
      }
      else
      {
        int length = endPos - firstPos - 1;
        if (hasEscape)
        {
          CharBuffer buffer = CharBuffer.allocate(length);
          for (int i = firstPos + 1; i < endPos; i++)
          {
            if (filterString.charAt(i) == 0x5C)
            {
              char escapeValue = hexToEscapedChar(filterString, i + 1);
              i += 2; // Move to the next sequence.
              buffer.put(escapeValue);
            }
            else
            {
              buffer.put(filterString.charAt(i));
            }
          }
          char[] subFinalChars = new char[buffer.position()];
          buffer.flip();
          buffer.get(subFinalChars);
          subFinal = new String(subFinalChars);
        }
        else
        {
          subFinal =
              filterString.substring(firstPos + 1, length + firstPos
                  + 1);
        }
      }
      return new Assertion(subInitial, subAny, subFinal);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public ByteString normalizeAssertionValue(ByteSequence value)
        throws DecodeException
    {
      Assertion assertion = parseAssertion(value);
      String subInitial = assertion.getInitial();
      // Normalize the Values in the following format:
      // initialLength, initial, numberofany, anyLength1, any1,
      // anyLength2, any2, ..., anyLengthn, anyn, finalLength, final
      List<Integer> normalizedList = new ArrayList<Integer>();
      if (subInitial == null)
      {
        normalizedList.add(0);
      }
      else
      {
        addLengthAndBytes(subInitial, normalizedList);
      }
      List<String> subAny = assertion.getAny();
      if (subAny.isEmpty())
      {
        normalizedList.add(0);
      }
      else
      {
        normalizedList.add(subAny.size());
        for (String any : subAny)
        {
          addLengthAndBytes(any, normalizedList);
        }
      }
      String subFinal = assertion.getFinal();
      if (subFinal == null)
      {
        normalizedList.add(0);
      }
      else
      {
        addLengthAndBytes(subFinal, normalizedList);
      }
      byte[] normalizedBytes = new byte[normalizedList.size()];
      for (int i = 0; i < normalizedList.size(); i++)
      {
        normalizedBytes[i] = normalizedList.get(i).byteValue();
      }
      return ByteString.wrap(normalizedBytes);
    }
    private void addLengthAndBytes(String substring,
        List<Integer> normalizedList)
    {
      CollationKey key = collator.getCollationKey(substring);
      byte[] substrBytes = key.toByteArray();
      // Last 4 bytes are 0s with PRIMARY strength.
      int length = substrBytes.length - 4;
      normalizedList.add(length);
      for (int i = 0; i < length; i++)
      {
        normalizedList.add((int) substrBytes[i]);
      }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public ConditionResult valuesMatch(ByteSequence attributeValue,
        ByteSequence assertionValue)
    { // FIXME Code similar to
      // AbstractSubstringMatchingRuleImpl.DefaultSubstringAssertion.matches()
      int valueLength = attributeValue.length() - 4;
      int valuePos = 0; // position in the value bytes array.
      // First byte is the length of subInitial.
      int subInitialLength = 0xFF & assertionValue.byteAt(0);
      if (subInitialLength != 0)
      {
        if (subInitialLength > valueLength)
        {
          return ConditionResult.FALSE;
        }
        for (; valuePos < subInitialLength; valuePos++)
        {
          if (attributeValue.byteAt(valuePos) != assertionValue
              .byteAt(valuePos + 1))
          {
            return ConditionResult.FALSE;
          }
        }
      }
      int assertPos = subInitialLength + 1;
      int anySize = 0xFF & assertionValue.byteAt(assertPos++);
      if (anySize != 0)
      {
        while (anySize-- > 0)
        {
          int anyLength = 0xFF & assertionValue.byteAt(assertPos++);
          int end = valueLength - anyLength;
          boolean match = false;
          for (; valuePos <= end; valuePos++)
          {
            if (assertionValue.byteAt(assertPos) == attributeValue
                .byteAt(valuePos))
            {
              boolean subMatch = true;
              for (int i = 1; i < anyLength; i++)
              {
                if (assertionValue.byteAt(assertPos + i) != attributeValue
                    .byteAt(valuePos + i))
                {
                  subMatch = false;
                  break;
                }
              }
              if (subMatch)
              {
                match = subMatch;
                break;
              }
            }
          }
          if (match)
          {
            valuePos += anyLength;
          }
          else
          {
            return ConditionResult.FALSE;
          }
          assertPos = assertPos + anyLength;
        }
      }
      int finalLength = 0xFF & assertionValue.byteAt(assertPos++);
      if (finalLength != 0)
      {
        if ((valueLength - finalLength) < valuePos)
        {
          return ConditionResult.FALSE;
        }
        if (finalLength != assertionValue.length() - assertPos)
        {
          // Some issue with the encoding.
          return ConditionResult.FALSE;
        }
        valuePos = valueLength - finalLength;
        for (int i = 0; i < finalLength; i++, valuePos++)
        {
          if (assertionValue.byteAt(assertPos + i) != attributeValue
              .byteAt(valuePos))
          {
            return ConditionResult.FALSE;
          }
        }
      }
      return ConditionResult.TRUE;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public final Collection<ExtensibleIndexer> getIndexers()
    {
      List<ExtensibleIndexer> indexers = new ArrayList<ExtensibleIndexer>();
      if (subIndexer == null)
      {
        subIndexer = new CollationSubstringExtensibleIndexer(this);
      }
      if (indexer == null)
      {
        indexer = new CollationSharedExtensibleIndexer(this);
      }
      indexers.add(subIndexer);
      indexers.add(indexer);
      return indexers;
    }
    /**
     * Makes a byte array representing a substring index key for one
     * substring of a value.
     *
     * @param value
     *          The String containing the value.
     * @param pos
     *          The starting position of the substring.
     * @param len
     *          The length of the substring.
     * @return A byte string containing a substring key.
     */
    private ByteString makeSubstringKey(String value, int pos, int len)
    {
      String sub = value.substring(pos, pos + len);
      CollationKey col = collator.getCollationKey(sub);
      byte[] key = col.toByteArray();
      // truncate the key
      return ByteString.wrap(key).subSequence(0, key.length - 4);
    }
    /**
     * Uses an equality index to retrieve the entry IDs that might
     * contain a given initial substring.
     *
     * @param bytes
     *          A normalized initial substring of an attribute value.
     * @return The candidate entry IDs.
     */
    private <T> T matchInitialSubstring(String value,
        IndexQueryFactory<T> factory)
    {
      // Use the shared equality indexer.
      return createRangeMatchQuery(value, factory, this.indexer);
    }
    private <T> T createRangeMatchQuery(String value,
        IndexQueryFactory<T> factory, ExtensibleIndexer indexer)
    { // FIXME Code similar to
      // AbstractSubstringMatchingRuleImpl.DefaultSubstringAssertion.rangeMatch()
      ByteString lower = makeSubstringKey(value, 0, value.length());
      ByteStringBuilder upper = new ByteStringBuilder(lower);
      for (int i = upper.length() - 1; i >= 0; i--)
      {
        if (upper.byteAt(i) == 0xFF)
        {
          // We have to carry the overflow to the more significant byte.
          upper.setByte(i, (byte) 0);
        }
        else
        {
          // No overflow, we can stop.
          upper.setByte(i, (byte) (upper.byteAt(i) + 1));
          break;
        }
      }
      // Read the range: lower <= keys < upper.
      return factory.createRangeMatchQuery(
          indexer.getExtensibleIndexID(), lower, upper, true, false);
    }
    /**
     * Retrieves the Index Records that might contain a given substring.
     *
     * @param value
     *          A String representing the attribute value.
     * @param factory
     *          An IndexQueryFactory which issues calls to the backend.
     * @param substrLength
     *          The length of the substring.
     * @return The candidate entry IDs.
     */
    private <T> T matchSubstring(String value, IndexQueryFactory<T> factory)
    { // FIXME Code similar to
      // AbstractSubstringMatchingRuleImpl.DefaultSubstringAssertion.substringMatch()
      int substrLength = factory.getIndexingOptions().substringKeySize();
      if (value.length() < substrLength)
      {
        return createRangeMatchQuery(value, factory, subIndexer);
      }
      List<T> queryList = new ArrayList<T>();
      Set<ByteString> set = new TreeSet<ByteString>();
      for (int first = 0, last = substrLength;
           last <= value.length();
           first++, last++)
      {
        set.add(makeSubstringKey(value, first, substrLength));
      }
      for (ByteString keyBytes : set)
      {
        queryList.add(factory.createExactMatchQuery(
            subIndexer.getExtensibleIndexID(), keyBytes));
      }
      return factory.createIntersectionQuery(queryList);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public <T> T createIndexQuery(ByteSequence assertionValue,
        IndexQueryFactory<T> factory) throws DecodeException
    { // FIXME Code similar to
      // AbstractSubstringMatchingRuleImpl.DefaultSubstringAssertion.createIndexQuery()?
      Assertion assertion = parseAssertion(assertionValue);
      String subInitial = assertion.getInitial();
      List<String> subAny = assertion.getAny();
      String subFinal = assertion.getFinal();
      List<T> queries = new ArrayList<T>();
      if (subInitial == null && subAny.isEmpty() && subFinal == null)
      {
        // Can happen with a filter like "cn:en.6:=*".
        // Just return an empty record.
        return factory.createMatchAllQuery();
      }
      List<String> elements = new ArrayList<String>();
      if (subInitial != null)
      {
        // Always use the shared indexer for initial match.
        queries.add(matchInitialSubstring(subInitial, factory));
      }
      if (subAny != null && subAny.size() > 0)
      {
        elements.addAll(subAny);
      }
      if (subFinal != null)
      {
        elements.add(subFinal);
      }
      for (String element : elements)
      {
        queries.add(matchSubstring(element, factory));
      }
      return factory.createIntersectionQuery(queries);
    }
  }
  /**
   * An abstract Collation rule for Ordering matching rule.
   */
  private abstract class CollationOrderingMatchingRule
          extends CollationMatchingRule
          implements OrderingMatchingRule
  {
    /**
     * The serial version identifier required to satisfy the compiler because
     * this class implements the <CODE>java.io.Serializable</CODE> interface.
     * This value was generated using the <CODE>serialver</CODE> command-line
     * utility included with the Java SDK.
     */
    private static final long serialVersionUID = 7354051060508436941L;
    /**
     * Constructs a new CollationOrderingMatchingRule.
     *
     * @param nOID
     *          OID of the collation matching rule
     * @param names
     *          names of this matching rule
     * @param locale
     *          Locale of the collation matching rule
     */
    private CollationOrderingMatchingRule(String nOID,
        Collection<String> names, Locale locale)
    {
      super(nOID, names, locale);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public ByteString normalizeAttributeValue(ByteSequence value)
        throws DecodeException
    {
      CollationKey key = collator.getCollationKey(value.toString());
      return ByteString.wrap(key.toByteArray());
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public int compare(byte[] arg0, byte[] arg1)
    {
      return StaticUtils.compare(arg0, arg1);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public int compareValues(ByteSequence value1, ByteSequence value2)
    {
      return value1.compareTo(value2);
    }
  }
  /**
   * Collation matching rule for Less-than matching rule.
   */
  private final class CollationLessThanMatchingRule extends
      CollationOrderingMatchingRule
  {
    /**
     * The serial version identifier required to satisfy the compiler because
     * this class implements the <CODE>java.io.Serializable</CODE> interface.
     * This value was generated using the <CODE>serialver</CODE> command-line
     * utility included with the Java SDK.
     */
    private static final long serialVersionUID = -7578406829946732713L;
    /**
     * Constructs a new CollationLessThanMatchingRule.
     *
     * @param nOID
     *          OID of the collation matching rule
     * @param names
     *          names of this matching rule
     * @param locale
     *          Locale of the collation matching rule
     */
    private CollationLessThanMatchingRule(String nOID,
        Collection<String> names, Locale locale)
    {
      super(nOID, names, locale);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public ConditionResult valuesMatch(ByteSequence attributeValue,
        ByteSequence assertionValue)
    {
      int ret = attributeValue.compareTo(assertionValue);
      return ConditionResult.valueOf(ret < 0);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public <T> T createIndexQuery(ByteSequence assertionValue,
        IndexQueryFactory<T> factory) throws DecodeException
    {
      return factory.createRangeMatchQuery(indexer
          .getExtensibleIndexID(), ByteString.empty(),
          normalizeAttributeValue(assertionValue), false, false);
    }
  }
  /**
   * Collation rule for less-than-equal-to matching rule.
   */
  private final class CollationLessThanOrEqualToMatchingRule extends
      CollationOrderingMatchingRule
  {
    /**
     * The serial version identifier required to satisfy the compiler because
     * this class implements the <CODE>java.io.Serializable</CODE> interface.
     * This value was generated using the <CODE>serialver</CODE> command-line
     * utility included with the Java SDK.
     */
    private static final long serialVersionUID = 7222067708233629974L;
    /**
     * Constructs a new CollationLessThanOrEqualToMatchingRule.
     *
     * @param nOID
     *          OID of the collation matching rule
     * @param names
     *          names of this matching rule
     * @param locale
     *          Locale of the collation matching rule
     */
    private CollationLessThanOrEqualToMatchingRule(String nOID,
        Collection<String> names, Locale locale)
    {
      super(nOID, names, locale);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public ConditionResult valuesMatch(ByteSequence attributeValue,
        ByteSequence assertionValue)
    {
      int ret = attributeValue.compareTo(assertionValue);
      return ConditionResult.valueOf(ret <= 0);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public <T> T createIndexQuery(ByteSequence assertionValue,
        IndexQueryFactory<T> factory) throws DecodeException
    {
      // Read the range: lower < keys <= upper.
      return factory.createRangeMatchQuery(indexer
          .getExtensibleIndexID(), ByteString.empty(),
          normalizeAttributeValue(assertionValue), false, true);
    }
  }
  /**
   * Collation rule for greater-than matching rule.
   */
  private final class CollationGreaterThanMatchingRule extends
      CollationOrderingMatchingRule
  {
    /**
     * The serial version identifier required to satisfy the compiler because
     * this class implements the <CODE>java.io.Serializable</CODE> interface.
     * This value was generated using the <CODE>serialver</CODE> command-line
     * utility included with the Java SDK.
     */
    private static final long serialVersionUID = 1204368277332957024L;
    /**
     * Constructs a new CollationGreaterThanMatchingRule.
     *
     * @param nOID
     *          OID of the collation matching rule
     * @param names
     *          names of this matching rule
     * @param locale
     *          Locale of the collation matching rule
     */
    private CollationGreaterThanMatchingRule(String nOID,
        Collection<String> names, Locale locale)
    {
      super(nOID, names, locale);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public ConditionResult valuesMatch(ByteSequence attributeValue,
        ByteSequence assertionValue)
    {
      int ret = attributeValue.compareTo(assertionValue);
      return ConditionResult.valueOf(ret > 0);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public <T> T createIndexQuery(ByteSequence assertionValue,
        IndexQueryFactory<T> factory) throws DecodeException
    {
      return factory.createRangeMatchQuery(indexer
          .getExtensibleIndexID(), normalizeAttributeValue(assertionValue),
          ByteString.empty(), false, false);
    }
  }
  /**
   * Collation rule for greater-than-equal-to matching rule.
   */
  private final class CollationGreaterThanOrEqualToMatchingRule extends
      CollationOrderingMatchingRule
  {
    /**
     * The serial version identifier required to satisfy the compiler because
     * this class implements the <CODE>java.io.Serializable</CODE> interface.
     * This value was generated using the <CODE>serialver</CODE> command-line
     * utility included with the Java SDK.
     */
    private static final long serialVersionUID = -5212358378014047933L;
    /**
     * Constructs a new CollationGreaterThanOrEqualToMatchingRule.
     *
     * @param nOID
     *          OID of the collation matching rule
     * @param names
     *          names of this matching rule
     * @param locale
     *          Locale of the collation matching rule
     */
    private CollationGreaterThanOrEqualToMatchingRule(String nOID,
        Collection<String> names, Locale locale)
    {
      super(nOID, names, locale);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public ConditionResult valuesMatch(ByteSequence attributeValue,
        ByteSequence assertionValue)
    {
      int ret = attributeValue.compareTo(assertionValue);
      return ConditionResult.valueOf(ret >= 0);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public <T> T createIndexQuery(ByteSequence assertionValue,
        IndexQueryFactory<T> factory) throws DecodeException
    {
      // Read the range: lower <= keys < upper.
      return factory.createRangeMatchQuery(indexer
          .getExtensibleIndexID(), normalizeAttributeValue(assertionValue),
          ByteString.empty(), true, false);
    }
  }
  /**
   * Extensible Indexer class for Collation Matching rules which share
   * the same index. This Indexer is shared by Equality and Ordering
   * Collation Matching Rules.
   */
  private final class CollationSharedExtensibleIndexer extends
      ExtensibleIndexer
  {
    /**
     * The Extensible Matching Rule.
     */
    private final CollationMatchingRule matchingRule;
    /**
     * Creates a new instance of CollationSharedExtensibleIndexer.
     *
     * @param matchingRule
     *          The Collation Matching Rule.
     */
    private CollationSharedExtensibleIndexer(
        CollationMatchingRule matchingRule)
    {
      this.matchingRule = matchingRule;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getExtensibleIndexID()
    {
      return EXTENSIBLE_INDEXER_ID_SHARED;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public final void createKeys(Schema schema, ByteSequence value,
        IndexingOptions options, Collection<ByteString> keys)
        throws DecodeException
    {
      keys.add(matchingRule.normalizeAttributeValue(value));
    }
    /** {@inheritDoc} */
    @Override
    public String getIndexID()
    {
      return matchingRule.getIndexName() + "." + getExtensibleIndexID();
    }
  }
  /**
   * Extensible Indexer class for Collation Substring Matching rules.
   * This Indexer is used by Substring Collation Matching Rules.
   */
  private final class CollationSubstringExtensibleIndexer extends
      ExtensibleIndexer
  {
    private final CollationSubstringMatchingRule matchingRule;
    /**
     * Creates a new instance of CollationSubstringExtensibleIndexer.
     *
     * @param matchingRule
     *          The CollationSubstringMatching Rule.
     */
    private CollationSubstringExtensibleIndexer(
        CollationSubstringMatchingRule matchingRule)
    {
      this.matchingRule = matchingRule;
    }
    /** {@inheritDoc} */
    @Override
    public void createKeys(Schema schema, ByteSequence value,
        IndexingOptions options, Collection<ByteString> keys)
    { // TODO merge with AbstractSubstringMatchingRuleImpl.SubstringIndexer.createKeys();
      String normValue = value.toString();
      int keyLength = options.substringKeySize();
      for (int i = 0, remain = normValue.length(); remain > 0; i++, remain--)
      {
        int len = Math.min(keyLength, remain);
        keys.add(matchingRule.makeSubstringKey(normValue, i, len));
      }
    }
    /** {@inheritDoc} */
    @Override
    public String getIndexID()
    {
      return matchingRule.getIndexName() + "." + getExtensibleIndexID();
    }
    /** {@inheritDoc} */
    @Override
    public String getExtensibleIndexID()
    {
      return EXTENSIBLE_INDEXER_ID_SUBSTRING;
    }
  }
  /**
   * A utility class for extracting the OID and Language Tag from the
   * configuration entry.
   */