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

Matthew Swift
22.31.2016 4f99c1a3cd5ee7b2d61a0e259d98b4b9fd85f9b2
OPENDJ-2877: support hasSubordinates/numSubordinates in MemoryBackend

These virtual attributes were required during the development of the
LDAP key store unit tests. They are enabled by calling
MemoryBackend.enableVirtualAttributes().

The attributes have also been added to the core schema.
5 files modified
224 ■■■■ changed files
opendj-core/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java 108 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchema.java 26 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchemaImpl.java 25 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GenerateCoreSchema.java 39 ●●●●● patch | view | raw | blame | history
opendj-core/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java 26 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java
@@ -15,6 +15,7 @@
 */
package org.forgerock.opendj.ldap;
import static org.forgerock.opendj.ldap.AttributeDescription.create;
import static org.forgerock.opendj.ldap.Attributes.singletonAttribute;
import static org.forgerock.opendj.ldap.Entries.modifyEntry;
import static org.forgerock.opendj.ldap.LdapException.newLdapException;
@@ -22,10 +23,13 @@
import static org.forgerock.opendj.ldap.responses.Responses.newCompareResult;
import static org.forgerock.opendj.ldap.responses.Responses.newResult;
import static org.forgerock.opendj.ldap.responses.Responses.newSearchResultEntry;
import static org.forgerock.opendj.ldap.schema.CoreSchema.getHasSubordinatesAttributeType;
import static org.forgerock.opendj.ldap.schema.CoreSchema.getNumSubordinatesAttributeType;
import java.io.IOException;
import java.util.Collection;
import java.util.NavigableMap;
import java.util.SortedSet;
import java.util.concurrent.ConcurrentSkipListMap;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
@@ -91,10 +95,14 @@
 * </pre>
 */
public final class MemoryBackend implements RequestHandler<RequestContext> {
    private static final AttributeDescription HAS_SUBORDINATES = create(getHasSubordinatesAttributeType());
    private static final AttributeDescription NUM_SUBORDINATES = create(getNumSubordinatesAttributeType());
    private final DecodeOptions decodeOptions;
    private final ConcurrentSkipListMap<DN, Entry> entries = new ConcurrentSkipListMap<>();
    private final Schema schema;
    private final Object writeLock = new Object();
    private boolean enableVirtualAttributes;
    /**
     * Creates a new empty memory backend which will use the default schema.
@@ -141,12 +149,24 @@
     *             or if duplicate entries are detected.
     */
    public MemoryBackend(final Schema schema, final EntryReader reader) throws IOException {
        this.schema = schema;
        this.decodeOptions = new DecodeOptions().setSchema(schema);
        this(schema);
        load(reader, false);
    }
    /**
     * Indicates whether search responses should include the {@code hasSubordinates} and {@code numSubordinates}
     * virtual attributes if requested.
     *
     * @param enabled
     *         {@code true} if the virtual attributes should be included.
     * @return This memory backend.
     */
    public MemoryBackend enableVirtualAttributes(final boolean enabled) {
        this.enableVirtualAttributes = enabled;
        return this;
    }
    /**
     * Clears the contents of this memory backend so that it does not contain
     * any entries.
     *
@@ -219,6 +239,65 @@
        return entries.values();
    }
    /**
     * Returns {@code true} if the named entry exists and has at least one child entry.
     *
     * @param dn
     *         The name of the entry.
     * @return {@code true} if the named entry exists and has at least one child entry.
     */
    public boolean hasSubordinates(final String dn) {
        return hasSubordinates(DN.valueOf(dn, schema));
    }
    /**
     * Returns {@code true} if the named entry exists and has at least one child entry.
     *
     * @param dn
     *         The name of the entry.
     * @return {@code true} if the named entry exists and has at least one child entry.
     */
    public boolean hasSubordinates(final DN dn) {
        if (!contains(dn)) {
            return false;
        }
        final DN next = entries.higherKey(dn);
        return next != null && next.isChildOf(dn);
    }
    /**
     * Returns the number of entries which are immediately subordinate to the named entry, or {@code 0} if the named
     * entry does not exist.
     *
     * @param dn
     *         The name of the entry.
     * @return The number of entries which are immediately subordinate to the named entry.
     */
    public int numSubordinates(final String dn) {
        return numSubordinates(DN.valueOf(dn, schema));
    }
    /**
     * Returns the number of entries which are immediately subordinate to the named entry, or {@code 0} if the named
     * entry does not exist.
     *
     * @param dn
     *         The name of the entry.
     * @return The number of entries which are immediately subordinate to the named entry.
     */
    public int numSubordinates(final DN dn) {
        final DN start = dn.child(RDN.minValue());
        final DN end = dn.child(RDN.maxValue());
        final SortedSet<DN> subtree = entries.keySet().subSet(start, end);
        int numSubordinates = 0;
        for (DN subordinate : subtree) {
            if (subordinate.isChildOf(dn)) {
                numSubordinates++;
            }
        }
        return numSubordinates;
    }
    @Override
    public void handleAdd(final RequestContext requestContext, final AddRequest request,
            final IntermediateResponseHandler intermediateResponseHandler,
@@ -261,7 +340,7 @@
                            "non-SIMPLE authentication not supported: " + request.getAuthenticationType());
                }
                entry = getRequiredEntry(null, username);
                if (!entry.containsAttribute("userPassword", password)) {
                if (!entry.containsAttribute("userPassword", (Object) password)) {
                    throw newLdapException(ResultCode.INVALID_CREDENTIALS, "Wrong password");
                }
            }
@@ -381,8 +460,9 @@
            switch (scope.asEnum()) {
            case BASE_OBJECT:
                final Entry baseEntry = getRequiredEntry(request, dn);
                if (matcher.matches(baseEntry).toBoolean()) {
                    sendEntry(attributeFilter, entryHandler, baseEntry);
                final Entry augmentedEntry = addVirtualAttributesIfNeeded(baseEntry);
                if (matcher.matches(augmentedEntry).toBoolean()) {
                    sendEntry(attributeFilter, entryHandler, augmentedEntry);
                }
                resultHandler.handleResult(newResult(ResultCode.SUCCESS));
                break;
@@ -406,6 +486,17 @@
        }
    }
    private Entry addVirtualAttributesIfNeeded(final Entry entry) {
        if (!enableVirtualAttributes) {
            return entry;
        }
        final Entry augmentedEntry = new LinkedHashMapEntry(entry);
        final int numSubordinates = numSubordinates(entry.getName());
        augmentedEntry.addAttribute(singletonAttribute(NUM_SUBORDINATES, numSubordinates));
        augmentedEntry.addAttribute(singletonAttribute(HAS_SUBORDINATES, numSubordinates > 0));
        return augmentedEntry;
    }
    /**
     * Returns {@code true} if this memory backend does not contain any entries.
     *
@@ -482,7 +573,7 @@
    private void searchWithSubordinates(final RequestContext requestContext, final SearchResultHandler entryHandler,
            final LdapResultHandler<Result> resultHandler, final DN dn, final Matcher matcher,
            final AttributeFilter attributeFilter, final int sizeLimit, SearchScope scope,
            SimplePagedResultsControl pagedResults) throws CancelledResultException, LdapException {
            SimplePagedResultsControl pagedResults) throws LdapException {
        final NavigableMap<DN, Entry> subtree = entries.subMap(dn, dn.child(RDN.maxValue()));
        if (subtree.isEmpty() || !dn.equals(subtree.firstKey())) {
            throw newLdapException(newResult(ResultCode.NO_SUCH_OBJECT));
@@ -497,7 +588,8 @@
            requestContext.checkIfCancelled(false);
            if (scope.equals(SearchScope.WHOLE_SUBTREE) || entry.getName().isChildOf(dn)
                    || (scope.equals(SearchScope.SUBORDINATES) && !entry.getName().equals(dn))) {
                if (matcher.matches(entry).toBoolean()) {
                final Entry augmentedEntry = addVirtualAttributesIfNeeded(entry);
                if (matcher.matches(augmentedEntry).toBoolean()) {
                    /*
                     * This entry is going to be returned to the client so it
                     * counts towards the size limit and any paging criteria.
@@ -514,7 +606,7 @@
                    }
                    // Send the entry back to the client.
                    if (!sendEntry(attributeFilter, entryHandler, entry)) {
                    if (!sendEntry(attributeFilter, entryHandler, augmentedEntry)) {
                        // Client has disconnected or cancelled.
                        break;
                    }
opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchema.java
@@ -12,7 +12,7 @@
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2009 Sun Microsystems, Inc.
 * Portions copyright 2014 ForgeRock AS.
 * Portions copyright 2014-2016 ForgeRock AS.
 */
package org.forgerock.opendj.ldap.schema;
@@ -275,6 +275,8 @@
        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.42");
    private static final AttributeType GOVERNING_STRUCTURE_RULE_ATTRIBUTE_TYPE =
        CoreSchemaImpl.getInstance().getAttributeType("2.5.21.10");
    private static final AttributeType HAS_SUBORDINATES_ATTRIBUTE_TYPE =
        CoreSchemaImpl.getInstance().getAttributeType("2.5.18.9");
    private static final AttributeType HOUSE_IDENTIFIER_ATTRIBUTE_TYPE =
        CoreSchemaImpl.getInstance().getAttributeType("2.5.4.51");
    private static final AttributeType INITIALS_ATTRIBUTE_TYPE =
@@ -301,6 +303,8 @@
        CoreSchemaImpl.getInstance().getAttributeType("2.5.21.7");
    private static final AttributeType NAMING_CONTEXTS_ATTRIBUTE_TYPE =
        CoreSchemaImpl.getInstance().getAttributeType("1.3.6.1.4.1.1466.101.120.5");
    private static final AttributeType NUM_SUBORDINATES_ATTRIBUTE_TYPE =
        CoreSchemaImpl.getInstance().getAttributeType("1.3.6.1.4.1.453.16.2.103");
    private static final AttributeType OBJECT_CLASSES_ATTRIBUTE_TYPE =
        CoreSchemaImpl.getInstance().getAttributeType("2.5.21.6");
    private static final AttributeType OBJECT_CLASS_ATTRIBUTE_TYPE =
@@ -1571,6 +1575,16 @@
    }
    /**
     * Returns a reference to the {@code hasSubordinates} Attribute Type
     * which has the OID {@code 2.5.18.9}.
     *
     * @return A reference to the {@code hasSubordinates} Attribute Type.
     */
    public static AttributeType getHasSubordinatesAttributeType() {
        return HAS_SUBORDINATES_ATTRIBUTE_TYPE;
    }
    /**
     * Returns a reference to the {@code houseIdentifier} Attribute Type
     * which has the OID {@code 2.5.4.51}.
     *
@@ -1701,6 +1715,16 @@
    }
    /**
     * Returns a reference to the {@code numSubordinates} Attribute Type
     * which has the OID {@code 1.3.6.1.4.1.453.16.2.103}.
     *
     * @return A reference to the {@code numSubordinates} Attribute Type.
     */
    public static AttributeType getNumSubordinatesAttributeType() {
        return NUM_SUBORDINATES_ATTRIBUTE_TYPE;
    }
    /**
     * Returns a reference to the {@code objectClasses} Attribute Type
     * which has the OID {@code 2.5.21.6}.
     *
opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchemaImpl.java
@@ -12,7 +12,7 @@
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2009-2010 Sun Microsystems, Inc.
 * Portions copyright 2013-2015 ForgeRock AS.
 * Portions copyright 2013-2016 ForgeRock AS.
 * Portions copyright 2014 Manuel Gaupp
 */
package org.forgerock.opendj.ldap.schema;
@@ -33,6 +33,7 @@
import java.util.Map.Entry;
import java.util.TreeMap;
/** Minimal set of LDAP standard schema elements. */
final class CoreSchemaImpl {
    private static final Map<String, List<String>> X500_ORIGIN = Collections.singletonMap(
            SCHEMA_PROPERTY_ORIGIN, Collections.singletonList("X.500"));
@@ -1099,6 +1100,28 @@
               .extraProperties(RFC4512_ORIGIN)
               .addToSchema();
        builder.buildAttributeType("2.5.18.9")
               .names("hasSubordinates")
               .equalityMatchingRule(EMR_BOOLEAN_NAME)
               .syntax(SYNTAX_INTEGER_OID)
               .singleValue(true)
               .noUserModification(true)
               .usage(AttributeUsage.DIRECTORY_OPERATION)
               .extraProperties(SCHEMA_PROPERTY_ORIGIN, "X.501")
               .addToSchema();
        builder.buildAttributeType("1.3.6.1.4.1.453.16.2.103")
               .names("numSubordinates")
               .description("Count of immediate subordinates")
               .equalityMatchingRule(EMR_INTEGER_NAME)
               .orderingMatchingRule(OMR_INTEGER_NAME)
               .syntax(SYNTAX_INTEGER_OID)
               .singleValue(true)
               .noUserModification(true)
               .usage(AttributeUsage.DIRECTORY_OPERATION)
               .extraProperties(SCHEMA_PROPERTY_ORIGIN, "draft-ietf-boreham-numsubordinates")
               .addToSchema();
        builder.buildAttributeType("2.5.18.10")
               .names("subschemaSubentry")
               .equalityMatchingRule(EMR_DN_NAME)
opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GenerateCoreSchema.java
@@ -13,7 +13,7 @@
 *
 * Copyright 2009 Sun Microsystems, Inc.
 * Portions Copyright 2014 Manuel Gaupp
 * Portions Copyright 2015 ForgeRock AS.
 * Portions Copyright 2015-2016 ForgeRock AS.
 */
package org.forgerock.opendj.ldap.schema;
@@ -115,26 +115,21 @@
        out.println("// It is automatically generated using GenerateCoreSchema class.");
        out.println();
        out.println("/**");
        out.println(" * The OpenDJ SDK core schema contains standard LDAP "
                + "RFC schema elements. These include:");
        out.println(" * The OpenDJ SDK core schema contains standard LDAP RFC schema elements. These include:");
        out.println(" * <ul>");
        out.println(" * <li><a href=\"http://tools.ietf.org/html/rfc4512\">RFC 4512 -");
        out
                .println(" * Lightweight Directory Access Protocol (LDAP): Directory Information");
        out.println(" * Lightweight Directory Access Protocol (LDAP): Directory Information");
        out.println(" * Models </a>");
        out.println(" * <li><a href=\"http://tools.ietf.org/html/rfc4517\">RFC 4517 -");
        out
                .println(" * Lightweight Directory Access Protocol (LDAP): Syntaxes and Matching");
        out.println(" * Lightweight Directory Access Protocol (LDAP): Syntaxes and Matching");
        out.println(" * Rules </a>");
        out.println(" * <li><a href=\"http://tools.ietf.org/html/rfc4519\">RFC 4519 -");
        out.println(" * Lightweight Directory Access Protocol (LDAP): Schema for User");
        out.println(" * Applications </a>");
        out.println(" * <li><a href=\"http://tools.ietf.org/html/rfc4530\">RFC 4530 -");
        out
                .println(" * Lightweight Directory Access Protocol (LDAP): entryUUID Operational");
        out.println(" * Lightweight Directory Access Protocol (LDAP): entryUUID Operational");
        out.println(" * Attribute </a>");
        out
                .println(" * <li><a href=\"http://tools.ietf.org/html/rfc3045\">RFC 3045 - Storing");
        out.println(" * <li><a href=\"http://tools.ietf.org/html/rfc3045\">RFC 3045 - Storing");
        out.println(" * Vendor Information in the LDAP Root DSE </a>");
        out.println(" * <li><a href=\"http://tools.ietf.org/html/rfc3112\">RFC 3112 - LDAP");
        out.println(" * Authentication Password Schema </a>");
@@ -149,15 +144,13 @@
        out.println("    // Core Syntaxes");
        for (final Map.Entry<String, Syntax> syntax : syntaxes.entrySet()) {
            out.println("    private static final Syntax " + syntax.getKey() + " =");
            out.println("        CoreSchemaImpl.getInstance().getSyntax(\""
                    + syntax.getValue().getOID() + "\");");
            out.println("        CoreSchemaImpl.getInstance().getSyntax(\"" + syntax.getValue().getOID() + "\");");
        }
        out.println();
        out.println("    // Core Matching Rules");
        for (final Map.Entry<String, MatchingRule> matchingRule : matchingRules.entrySet()) {
            out.println("    private static final MatchingRule " + matchingRule.getKey()
                    + " =");
            out.println("    private static final MatchingRule " + matchingRule.getKey() + " =");
            out.println("        CoreSchemaImpl.getInstance().getMatchingRule(\""
                    + matchingRule.getValue().getOID() + "\");");
        }
@@ -165,8 +158,7 @@
        out.println();
        out.println("    // Core Attribute Types");
        for (final Map.Entry<String, AttributeType> attributeType : attributeTypes.entrySet()) {
            out.println("    private static final AttributeType " + attributeType.getKey()
                    + " =");
            out.println("    private static final AttributeType " + attributeType.getKey() + " =");
            out.println("        CoreSchemaImpl.getInstance().getAttributeType(\""
                    + attributeType.getValue().getOID() + "\");");
        }
@@ -203,11 +195,9 @@
                            + " Syntax");
            out.println("    /**");
            out.println("     * Returns a reference to the " + description);
            out.println("     * which has the OID "
                    + toCodeJavaDoc(syntax.getValue().getOID()) + ".");
            out.println("     * which has the OID " + toCodeJavaDoc(syntax.getValue().getOID()) + ".");
            out.println("     *");
            out.println("     * @return A reference to the " + description + ".");
            out.println("     */");
            out.println("    public static Syntax get" + toJavaName(syntax.getKey()) + "() {");
            out.println("        return " + syntax.getKey() + ";");
@@ -224,7 +214,6 @@
                    + toCodeJavaDoc(matchingRule.getValue().getOID()) + ".");
            out.println("     *");
            out.println("     * @return A reference to the " + description + " Matching Rule.");
            out.println("     */");
            out.println("    public static MatchingRule get" + toJavaName(matchingRule.getKey()) + "() {");
            out.println("        return " + matchingRule.getKey() + ";");
@@ -241,10 +230,8 @@
                    + toCodeJavaDoc(attributeType.getValue().getOID()) + ".");
            out.println("     *");
            out.println("     * @return A reference to the " + description + " Attribute Type.");
            out.println("     */");
            out.println("    public static AttributeType get"
                    + toJavaName(attributeType.getKey()) + "() {");
            out.println("    public static AttributeType get" + toJavaName(attributeType.getKey()) + "() {");
            out.println("        return " + attributeType.getKey() + ";");
            out.println("    }");
        }
@@ -259,10 +246,8 @@
                    + toCodeJavaDoc(objectClass.getValue().getOID()) + ".");
            out.println("     *");
            out.println("     * @return A reference to the " + description + " Object Class.");
            out.println("     */");
            out.println("    public static ObjectClass get" + toJavaName(objectClass.getKey())
                    + "() {");
            out.println("    public static ObjectClass get" + toJavaName(objectClass.getKey()) + "() {");
            out.println("        return " + objectClass.getKey() + ";");
            out.println("    }");
        }
opendj-core/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java
@@ -590,7 +590,30 @@
                getUser1Entry());
    }
    @Test
    public void testHasSubordinates() throws Exception {
        final MemoryBackend backend = getMemoryBackend();
        assertThat(backend.hasSubordinates("dc=com")).isTrue();
        assertThat(backend.hasSubordinates("dc=example,dc=com")).isTrue();
        assertThat(backend.hasSubordinates("uid=test1,ou=people,dc=example,dc=com")).isFalse(); // leaf
        assertThat(backend.hasSubordinates("dc=c,dc=b,dc=a")).isFalse(); // doesn't exist
    }
    @Test
    public void testNumSubordinates() throws Exception {
        final MemoryBackend backend = getMemoryBackend();
        assertThat(backend.numSubordinates("dc=com")).isEqualTo(2);
        assertThat(backend.numSubordinates("dc=example,dc=com")).isEqualTo(1);
        assertThat(backend.numSubordinates("ou=people,dc=example,dc=com")).isEqualTo(5);
        assertThat(backend.numSubordinates("uid=test1,ou=people,dc=example,dc=com")).isEqualTo(0); // leaf
        assertThat(backend.numSubordinates("dc=c,dc=b,dc=a")).isEqualTo(0); // doesn't exist
    }
    private Connection getConnection() throws IOException {
        return newInternalConnection(getMemoryBackend());
    }
    private MemoryBackend getMemoryBackend() throws IOException {
        // @formatter:off
        String[] ldifEntries = new String[] {
            "dn: dc=com",
@@ -659,8 +682,7 @@
        };
        // @formatter:on
        numberOfEntriesInBackend = getNumberOfEntries(ldifEntries);
        final MemoryBackend backend = new MemoryBackend(new LDIFEntryReader(ldifEntries));
        return newInternalConnection(backend);
        return new MemoryBackend(new LDIFEntryReader(ldifEntries));
    }
    private int getNumberOfEntries(String[] ldifEntries) {