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