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

Matthew Swift
31.38.2016 91a823b7845f825827463db27f759d12fe58f937
OPENDJ-2987 Fix OID equality and first component matching rules

Changes:

* improve reliability of OID equality matching rule normalization by
resolving OIDs to their primary name, not their numeric OID

* make OID indexes immune to changes in the schema

* added documentationion to the OID equality matching rule
explaining the design choices and algorithms used

* removed unused methods from Schema.
2 files added
3 files modified
538 ■■■■ changed files
opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectIdentifierEqualityMatchingRuleImpl.java 241 ●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectIdentifierFirstComponentEqualityMatchingRuleImpl.java 13 ●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/Schema.java 57 ●●●●● patch | view | raw | blame | history
opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/ObjectIdentifierEqualityMatchingRuleTest.java 153 ●●●●● patch | view | raw | blame | history
opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/ObjectIdentifierFirstComponentEqualityMatchingRuleTest.java 74 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectIdentifierEqualityMatchingRuleImpl.java
@@ -12,65 +12,232 @@
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2009 Sun Microsystems, Inc.
 * Portions copyright 2011-2015 ForgeRock AS.
 * Portions copyright 2011-2016 ForgeRock AS.
 */
package org.forgerock.opendj.ldap.schema;
import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
import static com.forgerock.opendj.util.StaticUtils.toLowerCase;
import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_OID_NAME;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.forgerock.opendj.ldap.Assertion;
import org.forgerock.opendj.ldap.ByteSequence;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ConditionResult;
import org.forgerock.opendj.ldap.DecodeException;
import com.forgerock.opendj.util.StaticUtils;
import com.forgerock.opendj.util.SubstringReader;
import org.forgerock.opendj.ldap.spi.IndexQueryFactory;
import org.forgerock.opendj.ldap.spi.Indexer;
import org.forgerock.opendj.ldap.spi.IndexingOptions;
/**
 * This class defines the objectIdentifierMatch matching rule defined in X.520
 * and referenced in RFC 2252. This expects to work on OIDs and will match
 * and referenced in RFC 4517. This expects to work on OIDs and will match
 * either an attribute/objectclass name or a numeric OID. NOTE: This matching
 * rule requires a schema to lookup object identifiers in the descriptor form.
 */
final class ObjectIdentifierEqualityMatchingRuleImpl extends AbstractEqualityMatchingRuleImpl {
final class ObjectIdentifierEqualityMatchingRuleImpl extends AbstractMatchingRuleImpl {
    /**
     * NOTE: this matching rule is used by the "objectClass" attribute type which is frequently used in filters and
     * which is also usually indexed for equality. Unfortunately, it is non-trivial to implement correctly and there
     * are a number of subtleties that need to be understood.
     *
     * Comparing two values for equality
     * =================================
     *
     * The LDAP RFCs describe the algorithm for comparing assertion values with attribute values, but never discusses
     * how to compare two attribute values for equality in order to detect duplicate or missing values during updates.
     *
     * Given two schema elements:
     *
     * - object class with OID 1.2.3 and name "xxx"
     * - attribute type with OID 4.5.6 and name "xxx"
     *
     * When:
     *
     * - an attribute "oids" whose syntax is OID and which has the value "xxx"
     *
     * Then:
     *
     * - the filter "(oids=1.2.3)" matches, as does "(oids=4.5.6)", as does "(oids=xxx)".
     *
     * However, if the attribute's value is "1.2.3" then only the filters "(oids=1.2.3)" and "(oids=xxx)" match. In
     * order to compare two values with each other when enforcing set semantics in an LDAP attribute we need a function
     * which is reflexive, symmetric, and transitive. While the first two are possible, the last is not:
     *
     * - reflexive: 1.2.3 == 1.2.3, xxx == xxx
     * - symmetric: 1.2.3 == xxx, xxx == 1.2.3
     * - transitive: 1.2.3 == xxx, xxx = 4.5.6, 1.2.3 != 4.5.6
     *
     * The implication is that it is impossible to implement a reliable normalization method. There are three
     * options:
     *
     * 1) avoid resolving OID names to numeric OIDs during normalization and, instead, convert the OID to lower-case.
     *    This approach has the undesirable effect of allowing users to add equivalent object classes to entries. For
     *    example, the OIDs "2.5.6.6" and "person" are equivalent, but would have different normalized representations
     *
     * 2) resolve OID names to their equivalent numeric OID. For example, the normalized representation of "Person"
     *    would be "2.5.6.6". Unfortunately, there are rare cases where two schema elements share the same name - one
     *    can imagine having a custom object class called "manager" which would clash with the standard attribute
     *    type "manager". In this situation, the algorithm must choose whether to convert the value to the object
     *    class's numeric OID or the attribute type's. Unfortunately, this approach suffers from the same problem as
     *    the first if it happens to prioritize the wrong type of schema element
     *
     * 3) as (2) but resolve numeric OIDs to their "primary" OID name. For example, the normalized representation of
     *    "2.5.6.6" would be "person". The drawback with this approach is that an OID valued attribute cannot
     *    contain two values having the same "primary" name. In the example above, the attribute "oids" cannot
     *    contain the values "1.2.3" and "4.5.6" because they both share the same normalized primary name "xxx". Such
     *    collisions will be extremely rare in practice because attributes rarely reference a heterogeneous set of
     *    schema elements, such as a mix of attribute types and object classes. For example, the "objectClass"
     *    attribute should only contain object class OIDs whose names should never collide.
     *
     * Option (3) has been chosen as the normalization strategy as it provides the best compromise.
     *
     * Indexes
     * =======
     *
     * Indexing poses a different problem because indexes are persistent and rebuilding them is a relatively
     * expensive operation. If index keys depend on the schema, as is the case for the normalization algorithm (3)
     * above, then changes to the schema, e.g. the addition or removal of schema elements, may require an index
     * rebuild. In the above example, changing the primary name of one of the schema elements from "xxx" to "yyy"
     * invalidates any indexes. To avoid the need to rebuild indexes whenever the schema changes we use a simple
     * normalization algorithm - option (1) above - for generating index keys and employ a more complex algorithm for
     * querying the indexes during search operations:
     *
     * 1) compute all the possible aliases of the assertion value's OID using the current schema. In the earlier
     *    example, a filter of the form "(oids=xxx)" would yield the keys "xxx", "1.2.3", and "4.5.6", whereas the
     *    filter "(oids=1.2.3)" would only yield the keys "1.2.3" and "xxx"
     *
     * 2) perform an exact match index query against the index for each key
     *
     * 3) perform a union of the combined results.
     *
     * This algorithm may yield false positives in very rare cases when the schema contains many schema elements
     * sharing the same name.
     */
    ObjectIdentifierEqualityMatchingRuleImpl() {
        super(EMR_OID_NAME);
        // Nothing to do.
    }
    static String resolveNames(final Schema schema, final String oidOrName) throws DecodeException {
        if (StaticUtils.isDigit(oidOrName.charAt(0))) {
            return oidOrName;
        }
        // Do a best effort attempt to normalize names to OIDs.
        final String lowerCaseName = StaticUtils.toLowerCase(oidOrName);
        try {
            final String oid = schema.getOIDForName(lowerCaseName);
            if (oid != null) {
                return oid;
    @Override
    public Assertion getAssertion(final Schema schema, final ByteSequence assertionValue) throws DecodeException {
        return getAssertion(schema, EMR_OID_NAME, assertionValue);
    }
    @Override
    public Collection<? extends Indexer> createIndexers(final IndexingOptions options) {
        return Collections.singleton(new Indexer() {
            @Override
            public String getIndexID() {
                return EMR_OID_NAME;
            }
        } catch (UnknownSchemaElementException e) {
            throw DecodeException.error(e.getMessageObject(), e);
            @Override
            public void createKeys(final Schema schema, final ByteSequence value, final Collection<ByteString> keys)
                    throws DecodeException {
                // TODO: optimize - avoid converting to/from string + validate syntax.
                final String oid = toLowerCase(value.toString()).trim();
                keys.add(ByteString.valueOfUtf8(oid));
            }
            @Override
            public String keyToHumanReadableString(final ByteSequence key) {
                return key.toByteString().toString();
            }
        });
    }
    static Assertion getAssertion(final Schema schema, final String indexId, final ByteSequence assertionValue) {
        return new Assertion() {
            // TODO: optimize.
            final String oid = toLowerCase(assertionValue.toString()).trim();
            final List<ByteString> candidates = getCandidates(schema, oid);
            @Override
            public ConditionResult matches(final ByteSequence normalizedAttributeValue) {
                return ConditionResult.valueOf(candidates.contains(normalizedAttributeValue.toByteString()));
            }
            @Override
            public <T> T createIndexQuery(final IndexQueryFactory<T> factory) throws DecodeException {
                final List<T> subQueries = new ArrayList<>(candidates.size());
                for (final ByteString candidate : candidates) {
                    subQueries.add(factory.createExactMatchQuery(indexId, candidate));
                }
                return factory.createUnionQuery(subQueries);
            }
        };
    }
    private static List<ByteString> getCandidates(final Schema schema, final String oid) {
        // TODO: optimize - avoid double lookups.
        // The set of candidates is likely to be small, usually 2 or 3, so avoid the memory overhead of using a Set
        // and instead store the candidates in a small array.
        final List<ByteString> candidates = new ArrayList<>(3);
        candidates.add(ByteString.valueOfUtf8(oid));
        if (schema.hasObjectClass(oid)) { // Careful of placeholders
            final ObjectClass oc = schema.getObjectClass(oid);
            addCandidates(candidates, oc.getOID(), oc.getNames());
        }
        return lowerCaseName;
        if (schema.hasAttributeType(oid)) { // Careful of placeholders
            final AttributeType at = schema.getAttributeType(oid);
            addCandidates(candidates, at.getOID(), at.getNames());
        }
        if (schema.hasSyntax(oid)) {
            final Syntax syntax = schema.getSyntax(oid);
            addCandidates(candidates, syntax.getOID(), Collections.<String>emptyList());
        }
        if (schema.hasMatchingRule(oid)) {
            final MatchingRule mr = schema.getMatchingRule(oid);
            addCandidates(candidates, mr.getOID(), mr.getNames());
        }
        if (schema.hasNameForm(oid)) {
            final NameForm nf = schema.getNameForm(oid);
            addCandidates(candidates, nf.getOID(), nf.getNames());
        }
        return candidates;
    }
    private static void addCandidates(final List<ByteString> candidates,
                                      final String numericOid,
                                      final List<String> names) {
        addIfNotPresent(candidates, ByteString.valueOfUtf8(numericOid));
        for (final String name : names) {
            addIfNotPresent(candidates, ByteString.valueOfUtf8(toLowerCase(name)));
        }
    }
    private static void addIfNotPresent(final List<ByteString> candidates, final ByteString candidate) {
        if (!candidates.contains(candidate)) {
            candidates.add(candidate);
        }
    }
    @Override
    public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) throws DecodeException {
        return normalizeAttributeValuePrivate(schema, value);
    }
    static ByteString normalizeAttributeValuePrivate(final Schema schema, final ByteSequence value)
            throws DecodeException {
        final String definition = value.toString();
        final SubstringReader reader = new SubstringReader(definition);
        final String oid = readOID(reader, schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS));
        return ByteString.valueOfUtf8(resolveNames(schema, oid));
    }
    @Override
    String keyToHumanReadableString(ByteSequence key) {
        return key.toByteString().toString();
        // TODO: optimize - avoid converting to/from string + validate syntax + avoid double lookups.
        final String oid = toLowerCase(value.toString()).trim();
        if (schema.hasObjectClass(oid)) { // Careful of placeholders
            return ByteString.valueOfUtf8(toLowerCase(schema.getObjectClass(oid).getNameOrOID()));
        }
        if (schema.hasAttributeType(oid)) { // Careful of placeholders
            return ByteString.valueOfUtf8(toLowerCase(schema.getAttributeType(oid).getNameOrOID()));
        }
        if (schema.hasSyntax(oid)) {
            return ByteString.valueOfUtf8(toLowerCase(schema.getSyntax(oid).getOID()));
        }
        if (schema.hasMatchingRule(oid)) {
            return ByteString.valueOfUtf8(toLowerCase(schema.getMatchingRule(oid).getNameOrOID()));
        }
        if (schema.hasNameForm(oid)) {
            return ByteString.valueOfUtf8(toLowerCase(schema.getNameForm(oid).getNameOrOID()));
        }
        return ByteString.valueOfUtf8(oid);
    }
}
opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/ObjectIdentifierFirstComponentEqualityMatchingRuleImpl.java
@@ -12,13 +12,12 @@
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2009 Sun Microsystems, Inc.
 * Portions copyright 2011-2015 ForgeRock AS.
 * Portions copyright 2011-2016 ForgeRock AS.
 */
package org.forgerock.opendj.ldap.schema;
import static com.forgerock.opendj.ldap.CoreMessages.*;
import static org.forgerock.opendj.ldap.schema.ObjectIdentifierEqualityMatchingRuleImpl.*;
import static com.forgerock.opendj.util.StaticUtils.toLowerCase;
import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
@@ -33,7 +32,7 @@
/**
 * This class implements the objectIdentifierFirstComponentMatch matching rule
 * defined in X.520 and referenced in RFC 2252. This rule is intended for use
 * defined in X.520 and referenced in RFC 4517. This rule is intended for use
 * with attributes whose values contain a set of parentheses enclosing a
 * space-delimited set of names and/or name-value pairs (like attribute type or
 * objectclass descriptions) in which the "first component" is the first item
@@ -47,7 +46,9 @@
    @Override
    public Assertion getAssertion(final Schema schema, final ByteSequence assertionValue) throws DecodeException {
        return defaultAssertion(normalizeAttributeValuePrivate(schema, assertionValue));
        return ObjectIdentifierEqualityMatchingRuleImpl.getAssertion(schema,
                                                                     EMR_OID_FIRST_COMPONENT_NAME,
                                                                     assertionValue);
    }
    @Override
@@ -77,6 +78,6 @@
        // The next set of characters must be the OID.
        final String oid = readOID(reader, schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS));
        return ByteString.valueOfUtf8(resolveNames(schema, oid));
        return ByteString.valueOfUtf8(toLowerCase(oid, new StringBuilder(oid.length())).toString().trim());
    }
}
opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/Schema.java
@@ -75,8 +75,6 @@
        Syntax getDefaultSyntax();
        String getOIDForName(String lowerCaseName);
        AttributeType getAttributeType(Schema schema, String nameOrOid);
        AttributeType getAttributeType(String nameOrOid, Syntax syntax);
@@ -189,11 +187,6 @@
        }
        @Override
        public String getOIDForName(final String lowerCaseName) {
            return strictImpl.getOIDForName(lowerCaseName);
        }
        @Override
        public AttributeType getAttributeType(final Schema schema, final String nameOrOid) {
            return getAttributeType0(nameOrOid, schema.getDefaultSyntax(), schema.getDefaultMatchingRule());
        }
@@ -502,45 +495,6 @@
        }
        @Override
        public String getOIDForName(String lowerCaseName) {
            AttributeType attributeType = getAttributeType0(lowerCaseName);
            if (attributeType != null) {
                return attributeType.getOID();
            }
            try {
                return getObjectClass(lowerCaseName).getOID();
            } catch (UnknownSchemaElementException ignore) {
                // try next schema element
            }
            try {
                return getSyntax(null, lowerCaseName).getOID();
            } catch (UnknownSchemaElementException ignore) {
                // try next schema element
            }
            try {
                return getMatchingRule(lowerCaseName).getOID();
            } catch (UnknownSchemaElementException ignore) {
                // try next schema element
            }
            try {
                return getNameForm(lowerCaseName).getOID();
            } catch (UnknownSchemaElementException ignore) {
                // try next schema element
            }
            try {
                return getDITContentRule(lowerCaseName).getStructuralClassOID();
            } catch (UnknownSchemaElementException ignore) {
                // try next schema element
            }
            try {
                return getMatchingRuleUse(lowerCaseName).getMatchingRuleOID();
            } catch (UnknownSchemaElementException ignore) {
                // try next schema element
            }
            return null;
        }
        @Override
        public AttributeType getAttributeType(String nameOrOid, Syntax syntax) {
            return getAttributeType(null, nameOrOid);
        }
@@ -1149,17 +1103,6 @@
    }
    /**
     * Return the numerical OID matching the lowerCaseName.
     * @param lowerCaseName The lower case name
     * @return OID matching the name or null if name doesn't match to an OID
     * @throws UnknownSchemaElementException if multiple OID are matching
     * lowerCaseName
     */
    String getOIDForName(String lowerCaseName) {
        return impl.getOIDForName(lowerCaseName);
    }
    /**
     * Returns the attribute type for the specified name or numeric OID.
     * <p>
     * If the requested attribute type is not registered in this schema and this
opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/ObjectIdentifierEqualityMatchingRuleTest.java
New file
@@ -0,0 +1,153 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2016 ForgeRock AS.
 *
 */
package org.forgerock.opendj.ldap.schema;
import static org.assertj.core.api.Assertions.assertThat;
import static org.forgerock.opendj.ldap.ConditionResult.FALSE;
import static org.forgerock.opendj.ldap.ConditionResult.TRUE;
import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_OID_NAME;
import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_OID_OID;
import static org.mockito.Matchers.anyCollection;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.forgerock.opendj.ldap.Assertion;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DecodeException;
import org.forgerock.opendj.ldap.spi.IndexQueryFactory;
import org.forgerock.opendj.ldap.spi.Indexer;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@Test
public class ObjectIdentifierEqualityMatchingRuleTest extends MatchingRuleTest {
    @Override
    @DataProvider(name = "matchingRuleInvalidAttributeValues")
    public Object[][] createMatchingRuleInvalidAttributeValues() {
        return new Object[][] {};
    }
    @Override
    @DataProvider(name = "matchingrules")
    public Object[][] createMatchingRuleTest() {
        return new Object[][] {
            // value, assertion, expected result
            // numeric OIDs match directly.
            { "2.5.4.3", "2.5.4.3", TRUE },
            { "2.5.4.3", "2.5.4.4", FALSE },   // OID is known
            { "2.5.4.3", "9.9.9.999", FALSE }, // OID is unknown
            { "9.9.9.999", "9.9.9.999", TRUE },
            { "9.9.9.999", "9.9.9.9999", FALSE },
            // OID 'descr' form where assertion values are known to the schema.
            { "2.5.4.3", "cn", TRUE },
            { "2.5.4.3", "commonName", TRUE },
            { "2.5.4.3", "l", FALSE },
            { "cn", "2.5.4.3", TRUE },
            { "commonName", "2.5.4.3", TRUE },
            { "l", "2.5.4.3", FALSE },
            { "cn", "cn", TRUE },
            { "commonName", "cn", TRUE },
            { "cn", "commonName", TRUE },
            { "commonName", "commonName", TRUE },
            { "l", "cn", FALSE },
            { "cn", "l", FALSE },
            /* THESE TESTS FAIL BECAUSE THE ACTUAL RESULT IS FALSE
            // These are undefined because assertion values are unknown to the server. See RFC 4517 4.2.26.
            { "2.5.4.3", "dummy", UNDEFINED },
            { "2.5.4.3", "xxx", UNDEFINED },
            { "9.9.9.999", "foo", UNDEFINED },
            */
            // Strictly speaking this should evaluate to UNDEFINED since 'DUMMY' is not recognized.
            { "dummy", "DUMMY", TRUE },
            // 2.5.4.3 is recognized (it's a numeric OID) so matching can be performed.
            { "dummy", "2.5.4.3", FALSE },
        };
    }
    @Override
    protected MatchingRule getRule() {
        return Schema.getCoreSchema().getMatchingRule(EMR_OID_OID);
    }
    @Test
    public void indexingShouldNotBeSensitiveToSchemaChanges() throws Exception {
        // 1) keys should be stable and not impacted by schema changes
        // 2) index queries should work even when the schema element has been removed.
        final String customDefinition = "( 9.9.9.999 NAME ( 'foo' 'bar' ) SUP name )";
        final Schema customSchema = new SchemaBuilder(Schema.getCoreSchema())
                .addAttributeType(customDefinition, true)
                .toSchema();
        final List<ByteString> attributeValues = Arrays.asList(b("9.9.9.999"), b("FOO"), b("cn"));
        // Indexing should be insensitive to the schema.
        checkIndexContainsExpectedKeys(Schema.getCoreSchema(), attributeValues, b("9.9.9.999"), b("foo"), b("cn"));
        checkIndexContainsExpectedKeys(customSchema,           attributeValues, b("9.9.9.999"), b("foo"), b("cn"));
        // Index queries should take advantage of the schema by testing alternative schema element names.
        final ByteString assertionValue = b("bar");
        checkKeysUsedForIndexQuerying(Schema.getCoreSchema(), assertionValue, b("bar"));
        checkKeysUsedForIndexQuerying(customSchema,           assertionValue, b("bar"), b("9.9.9.999"), b("foo"));
    }
    private void checkIndexContainsExpectedKeys(final Schema schema,
                                                final List<ByteString> attributeValues,
                                                final ByteString... expectedKeys) throws DecodeException {
        final MatchingRule mr = schema.getMatchingRule(EMR_OID_OID);
        final Indexer indexer = mr.createIndexers(null).iterator().next();
        final ArrayList<ByteString> keys = new ArrayList<>();
        for (final ByteString value : attributeValues) {
            indexer.createKeys(schema, value, keys);
        }
        assertThat(keys).containsExactly(expectedKeys);
    }
    private void checkKeysUsedForIndexQuerying(final Schema schema,
                                               final ByteString assertionValue,
                                               final ByteString... expectedKeys) throws DecodeException {
        final MatchingRule mr = schema.getMatchingRule(EMR_OID_OID);
        final Assertion assertion = mr.getAssertion(assertionValue);
        final IndexQueryFactory indexQueryFactory = mock(IndexQueryFactory.class);
        assertion.createIndexQuery(indexQueryFactory);
        for (final ByteString key : expectedKeys) {
            verify(indexQueryFactory).createExactMatchQuery(EMR_OID_NAME, key);
        }
        verify(indexQueryFactory).createUnionQuery(anyCollection());
        verifyNoMoreInteractions(indexQueryFactory);
    }
    private ByteString b(final String value) {
        return ByteString.valueOfUtf8(value);
    }
}
opendj-core/src/test/java/org/forgerock/opendj/ldap/schema/ObjectIdentifierFirstComponentEqualityMatchingRuleTest.java
New file
@@ -0,0 +1,74 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2016 ForgeRock AS.
 *
 */
package org.forgerock.opendj.ldap.schema;
import static org.forgerock.opendj.ldap.ConditionResult.FALSE;
import static org.forgerock.opendj.ldap.ConditionResult.TRUE;
import static org.forgerock.opendj.ldap.schema.SchemaConstants.EMR_OID_FIRST_COMPONENT_OID;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@Test
public class ObjectIdentifierFirstComponentEqualityMatchingRuleTest extends MatchingRuleTest {
    @Override
    @DataProvider(name = "matchingRuleInvalidAttributeValues")
    public Object[][] createMatchingRuleInvalidAttributeValues() {
        return new Object[][] {};
    }
    @Override
    @DataProvider(name = "matchingrules")
    public Object[][] createMatchingRuleTest() {
        // This is defined in the core schema so the names 'cn' and 'commonName' will be recognized. However 'dummy'
        // is not part of the core schema definition so it won't be recognized by the server.
        final String cnDefinition = "( 2.5.4.3 NAME ( 'cn' 'commonName' 'dummy' ) SUP name )";
        // Matching should be supported against the numeric OID, but the names are not in the core schema so cannot
        // be used in assertion values.
        final String customDefinition = "( 9.9.9.999 NAME ( 'foo' 'bar' ) SUP name )";
        return new Object[][] {
            // numeric OIDs match directly.
            { cnDefinition, "2.5.4.3", TRUE },
            { cnDefinition, "2.5.4.4", FALSE },   // OID is known
            { cnDefinition, "9.9.9.999", FALSE }, // OID is unknown, but it is numeric
            { customDefinition, "9.9.9.999", TRUE },
            { customDefinition, "9.9.9.9999", FALSE },
            // OID 'descr' form where assertion values are known to the schema.
            { cnDefinition, "cn", TRUE },
            { cnDefinition, "commonName", TRUE },
            { cnDefinition, "l", FALSE },
            { customDefinition, "cn", FALSE },
            /* THESE TESTS FAIL BECAUSE THE ACTUAL RESULT IS FALSE
            // These are undefined because the assertion values are unknown to the server. See RFC 4517 4.2.26.
            { cnDefinition, "dummy", UNDEFINED },
            { cnDefinition, "xxx", UNDEFINED },
            { customDefinition, "foo", UNDEFINED }
            */
        };
    }
    @Override
    protected MatchingRule getRule() {
        return Schema.getCoreSchema().getMatchingRule(EMR_OID_FIRST_COMPONENT_OID);
    }
}