From 4f99c1a3cd5ee7b2d61a0e259d98b4b9fd85f9b2 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Thu, 22 Sep 2016 22:59:55 +0000
Subject: [PATCH] OPENDJ-2877: support hasSubordinates/numSubordinates in MemoryBackend

---
 opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GenerateCoreSchema.java |   39 +++------
 opendj-core/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java             |  108 +++++++++++++++++++++++++--
 opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchemaImpl.java     |   25 ++++++
 opendj-core/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java     |   26 ++++++
 opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchema.java         |   26 ++++++
 5 files changed, 185 insertions(+), 39 deletions(-)

diff --git a/opendj-core/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java b/opendj-core/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java
index 8415c49..3060bc1 100644
--- a/opendj-core/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java
+++ b/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;
                     }
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchema.java b/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchema.java
index 663cf3e..01a4cb7 100644
--- a/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchema.java
+++ b/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}.
      *
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchemaImpl.java b/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchemaImpl.java
index 01fb4c2..ef018b8 100644
--- a/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/CoreSchemaImpl.java
+++ b/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)
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GenerateCoreSchema.java b/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GenerateCoreSchema.java
index d074f3b..66ef15c 100644
--- a/opendj-core/src/main/java/org/forgerock/opendj/ldap/schema/GenerateCoreSchema.java
+++ b/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("    }");
         }
diff --git a/opendj-core/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java b/opendj-core/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java
index c5c867a..ef9ffd0 100644
--- a/opendj-core/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java
+++ b/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) {

--
Gitblit v1.10.0