From a08c81f677247ec9eb7721a86250c663065e9930 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Wed, 22 Jun 2016 22:12:03 +0000
Subject: [PATCH] OPENDJ-2871 Add support for sub-resources and inheritance
---
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectPropertyMapper.java | 264 ++++++++++++++++++++++++++++++++++++----------------
1 files changed, 180 insertions(+), 84 deletions(-)
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectPropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectPropertyMapper.java
index 47aab51..4aa4ab1 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectPropertyMapper.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectPropertyMapper.java
@@ -15,20 +15,25 @@
*/
package org.forgerock.opendj.rest2ldap;
+import static org.forgerock.opendj.rest2ldap.Rest2Ldap.simple;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
import static org.forgerock.json.resource.PatchOperation.operation;
import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException;
import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
import static org.forgerock.opendj.rest2ldap.Utils.toLowerCase;
+import static org.forgerock.util.Utils.joinAsString;
+import static org.forgerock.util.promise.Promises.newResultPromise;
-import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.TreeSet;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
@@ -40,12 +45,12 @@
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Modification;
import org.forgerock.util.Function;
+import org.forgerock.util.Pair;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.Promises;
/** An property mapper which maps JSON objects to LDAP attributes. */
public final class ObjectPropertyMapper extends PropertyMapper {
-
private static final class Mapping {
private final PropertyMapper mapper;
private final String name;
@@ -63,39 +68,80 @@
private final Map<String, Mapping> mappings = new LinkedHashMap<>();
+ private boolean includeAllUserAttributesByDefault = false;
+ private final Set<String> excludedDefaultUserAttributes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+
ObjectPropertyMapper() {
// Nothing to do.
}
/**
- * Creates a mapping for an attribute contained in the JSON object.
+ * Creates an explicit mapping for a property contained in the JSON object. When user attributes are
+ * {@link #includeAllUserAttributesByDefault included} by default, be careful to {@link
+ * #excludedDefaultUserAttributes exclude} any attributes which have explicit mappings defined using this method,
+ * otherwise they will be duplicated in the JSON representation.
*
* @param name
- * The name of the JSON attribute to be mapped.
+ * The name of the JSON property to be mapped.
* @param mapper
- * The property mapper responsible for mapping the JSON
- * attribute to LDAP attribute(s).
+ * The property mapper responsible for mapping the JSON attribute to LDAP attribute(s).
* @return A reference to this property mapper.
*/
- public ObjectPropertyMapper attribute(final String name, final PropertyMapper mapper) {
+ public ObjectPropertyMapper property(final String name, final PropertyMapper mapper) {
mappings.put(toLowerCase(name), new Mapping(name, mapper));
return this;
}
+ /**
+ * Specifies whether all LDAP user attributes should be mapped by default using the default schema based mapping
+ * rules. Individual attributes can be excluded using {@link #excludedDefaultUserAttributes} in order to prevent
+ * attributes with explicit mappings being mapped twice.
+ *
+ * @param include {@code true} if all LDAP user attributes be mapped by default.
+ * @return A reference to this property mapper.
+ */
+ public ObjectPropertyMapper includeAllUserAttributesByDefault(final boolean include) {
+ this.includeAllUserAttributesByDefault = include;
+ return this;
+ }
+
+ /**
+ * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when
+ * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be
+ * excluded in order to prevent duplication.
+ *
+ * @param attributeNames The list of attributes to be excluded.
+ * @return A reference to this property mapper.
+ */
+ public ObjectPropertyMapper excludedDefaultUserAttributes(final String... attributeNames) {
+ return excludedDefaultUserAttributes(Arrays.asList(attributeNames));
+ }
+
+ /**
+ * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when
+ * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be
+ * excluded in order to prevent duplication.
+ *
+ * @param attributeNames The list of attributes to be excluded.
+ * @return A reference to this property mapper.
+ */
+ public ObjectPropertyMapper excludedDefaultUserAttributes(final Collection<String> attributeNames) {
+ excludedDefaultUserAttributes.addAll(attributeNames);
+ return this;
+ }
+
@Override
public String toString() {
- return "object(" + mappings.values() + ")";
+ return "object(" + joinAsString(", ", mappings.values()) + ")";
}
@Override
- Promise<List<Attribute>, ResourceException> create(final Connection connection, final JsonPointer path,
- final JsonValue v) {
+ Promise<List<Attribute>, ResourceException> create(final Connection connection,
+ final Resource resource, final JsonPointer path,
+ final JsonValue v) {
try {
- /*
- * First check that the JSON value is an object and that the fields
- * it contains are known by this mapper.
- */
- final Map<String, Mapping> missingMappings = checkMapping(path, v);
+ // First check that the JSON value is an object and that the fields it contains are known by this mapper.
+ final Map<String, Mapping> missingMappings = validateJsonValue(path, v);
// Accumulate the results of the subordinate mappings.
final List<Promise<List<Attribute>, ResourceException>> promises = new ArrayList<>();
@@ -105,61 +151,70 @@
for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
final Mapping mapping = getMapping(me.getKey());
final JsonValue subValue = new JsonValue(me.getValue());
- promises.add(mapping.mapper.create(connection, path.child(me.getKey()), subValue));
+ promises.add(mapping.mapper.create(connection, resource, path.child(me.getKey()),
+ subValue));
}
}
// Invoke mappings for which there were no values provided.
for (final Mapping mapping : missingMappings.values()) {
- promises.add(mapping.mapper.create(connection, path.child(mapping.name), null));
+ promises.add(mapping.mapper.create(connection, resource, path.child(mapping.name), null));
}
return Promises.when(promises)
.then(this.<Attribute> accumulateResults());
} catch (final Exception e) {
- return Promises.newExceptionPromise(asResourceException(e));
+ return asResourceException(e).asPromise();
}
}
@Override
- void getLdapAttributes(final Connection connection, final JsonPointer path, final JsonPointer subPath,
- final Set<String> ldapAttributes) {
+ void getLdapAttributes(final JsonPointer path, final JsonPointer subPath, final Set<String> ldapAttributes) {
if (subPath.isEmpty()) {
// Request all subordinate mappings.
+ if (includeAllUserAttributesByDefault) {
+ ldapAttributes.add("*");
+ // Continue because there may be explicit mappings for operational attributes.
+ }
for (final Mapping mapping : mappings.values()) {
- mapping.mapper.getLdapAttributes(connection, path.child(mapping.name), subPath, ldapAttributes);
+ mapping.mapper.getLdapAttributes(path.child(mapping.name), subPath, ldapAttributes);
}
} else {
// Request single subordinate mapping.
- final Mapping mapping = getMapping(subPath);
+ final Mapping mapping = getMappingOrNull(subPath);
if (mapping != null) {
- mapping.mapper.getLdapAttributes(
- connection, path.child(subPath.get(0)), subPath.relativePointer(), ldapAttributes);
+ mapping.mapper.getLdapAttributes(path.child(subPath.get(0)), subPath.relativePointer(), ldapAttributes);
}
}
}
@Override
- Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final JsonPointer path,
- final JsonPointer subPath, final FilterType type,
- final String operator, final Object valueAssertion) {
- final Mapping mapping = getMapping(subPath);
+ Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final Resource resource,
+ final JsonPointer path, final JsonPointer subPath,
+ final FilterType type, final String operator,
+ final Object valueAssertion) {
+ final Mapping mapping = getMappingOrNull(subPath);
if (mapping != null) {
- return mapping.mapper.getLdapFilter(connection, path.child(subPath.get(0)),
- subPath.relativePointer(), type, operator, valueAssertion);
+ return mapping.mapper.getLdapFilter(connection,
+ resource,
+ path.child(subPath.get(0)),
+ subPath.relativePointer(),
+ type,
+ operator,
+ valueAssertion);
} else {
/*
* Either the filter targeted the entire object (i.e. it was "/"),
* or it targeted an unrecognized attribute within the object.
* Either way, the filter will never match.
*/
- return Promises.newResultPromise(alwaysFalse());
+ return newResultPromise(alwaysFalse());
}
}
@Override
- Promise<List<Modification>, ResourceException> patch(final Connection connection, final JsonPointer path,
- final PatchOperation operation) {
+ Promise<List<Modification>, ResourceException> patch(final Connection connection, final Resource resource,
+ final JsonPointer path, final PatchOperation operation) {
try {
final JsonPointer field = operation.getField();
final JsonValue v = operation.getValue();
@@ -170,7 +225,7 @@
* by allowing the JSON value to be a partial object and
* add/remove/replace only the provided values.
*/
- checkMapping(path, v);
+ validateJsonValue(path, v);
// Accumulate the results of the subordinate mappings.
final List<Promise<List<Modification>, ResourceException>> promises = new ArrayList<>();
@@ -182,7 +237,7 @@
final JsonValue subValue = new JsonValue(me.getValue());
final PatchOperation subOperation =
operation(operation.getOperation(), field /* empty */, subValue);
- promises.add(mapping.mapper.patch(connection, path.child(me.getKey()), subOperation));
+ promises.add(mapping.mapper.patch(connection, resource, path.child(me.getKey()), subOperation));
}
}
@@ -195,71 +250,93 @@
* appropriate mapper.
*/
final String fieldName = field.get(0);
- final Mapping mapping = getMapping(fieldName);
+ final Mapping mapping = getMappingOrNull(fieldName);
if (mapping == null) {
throw newBadRequestException(ERR_UNRECOGNIZED_FIELD.get(path.child(fieldName)));
}
final PatchOperation subOperation =
operation(operation.getOperation(), field.relativePointer(), v);
- return mapping.mapper.patch(connection, path.child(fieldName), subOperation);
+ return mapping.mapper.patch(connection, resource, path.child(fieldName), subOperation);
}
- } catch (final Exception ex) {
- return Promises.newExceptionPromise(asResourceException(ex));
+ } catch (final Exception e) {
+ return asResourceException(e).asPromise();
}
}
@Override
- Promise<JsonValue, ResourceException> read(final Connection connection, final JsonPointer path, final Entry e) {
+ Promise<JsonValue, ResourceException> read(final Connection connection, final Resource resource,
+ final JsonPointer path, final Entry e) {
/*
* Use an accumulator which will aggregate the results from the
* subordinate mappers into a single list. On completion, the
* accumulator combines the results into a single JSON map object.
*/
- final List<Promise<Map.Entry<String, JsonValue>, ResourceException>> promises =
+ final List<Promise<Pair<String, JsonValue>, ResourceException>> promises =
new ArrayList<>(mappings.size());
for (final Mapping mapping : mappings.values()) {
- promises.add(mapping.mapper.read(connection, path.child(mapping.name), e)
- .then(new Function<JsonValue, Map.Entry<String, JsonValue>, ResourceException>() {
- @Override
- public Map.Entry<String, JsonValue> apply(final JsonValue value) {
- return value != null ? new SimpleImmutableEntry<String, JsonValue>(mapping.name, value)
- : null;
- }
- }));
+ promises.add(mapping.mapper.read(connection, resource, path.child(mapping.name), e)
+ .then(toProperty(mapping.name)));
+ }
+
+ if (includeAllUserAttributesByDefault) {
+ // Map all user attributes using a default simple mapping. It would be nice if we could automatically
+ // detect which attributes have been mapped already using explicit mappings, but it would require us to
+ // track which attributes have been accessed in the entry. Instead, we'll rely on the user to exclude
+ // attributes which have explicit mappings.
+ for (final Attribute attribute : e.getAllAttributes()) {
+ // Don't include operational attributes. They must have explicit mappings.
+ if (attribute.getAttributeDescription().getAttributeType().isOperational()) {
+ continue;
+ }
+ // Filter out excluded attributes.
+ final String attributeName = attribute.getAttributeDescriptionAsString();
+ if (!excludedDefaultUserAttributes.isEmpty() && excludedDefaultUserAttributes.contains(attributeName)) {
+ continue;
+ }
+ // This attribute needs to be mapped.
+ final SimplePropertyMapper mapper = simple(attribute.getAttributeDescription());
+ promises.add(mapper.read(connection, resource, path.child(attributeName), e)
+ .then(toProperty(attributeName)));
+ }
}
return Promises.when(promises)
- .then(new Function<List<Map.Entry<String, JsonValue>>, JsonValue, ResourceException>() {
- @Override
- public JsonValue apply(final List<Map.Entry<String, JsonValue>> value) {
- if (value.isEmpty()) {
- /*
- * No subordinate attributes, so omit the entire
- * JSON object from the resource.
- */
- return null;
- } else {
- // Combine the sub-attributes into a single JSON object.
- final Map<String, Object> result = new LinkedHashMap<>(value.size());
- for (final Map.Entry<String, JsonValue> e : value) {
- if (e != null) {
- result.put(e.getKey(), e.getValue().getObject());
- }
- }
- return new JsonValue(result);
- }
- }
- });
+ .then(new Function<List<Pair<String, JsonValue>>, JsonValue, ResourceException>() {
+ @Override
+ public JsonValue apply(final List<Pair<String, JsonValue>> value) {
+ if (value.isEmpty()) {
+ // No subordinate attributes, so omit the entire JSON object from the resource.
+ return null;
+ } else {
+ // Combine the sub-attributes into a single JSON object.
+ final Map<String, Object> result = new LinkedHashMap<>(value.size());
+ for (final Pair<String, JsonValue> e : value) {
+ if (e != null) {
+ result.put(e.getFirst(), e.getSecond().getObject());
+ }
+ }
+ return new JsonValue(result);
+ }
+ }
+ });
+ }
+
+ private Function<JsonValue, Pair<String, JsonValue>, ResourceException> toProperty(final String name) {
+ return new Function<JsonValue, Pair<String, JsonValue>, ResourceException>() {
+ @Override
+ public Pair<String, JsonValue> apply(final JsonValue value) {
+ return value != null ? Pair.of(name, value) : null;
+ }
+ };
}
@Override
- Promise<List<Modification>, ResourceException> update(
- final Connection connection, final JsonPointer path, final Entry e, final JsonValue v) {
+ Promise<List<Modification>, ResourceException> update(final Connection connection, final Resource resource,
+ final JsonPointer path, final Entry e, final JsonValue v) {
try {
- // First check that the JSON value is an object and that the fields
- // it contains are known by this mapper.
- final Map<String, Mapping> missingMappings = checkMapping(path, v);
+ // First check that the JSON value is an object and that the fields it contains are known by this mapper.
+ final Map<String, Mapping> missingMappings = validateJsonValue(path, v);
// Accumulate the results of the subordinate mappings.
final List<Promise<List<Modification>, ResourceException>> promises = new ArrayList<>();
@@ -269,19 +346,19 @@
for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
final Mapping mapping = getMapping(me.getKey());
final JsonValue subValue = new JsonValue(me.getValue());
- promises.add(mapping.mapper.update(connection, path.child(me.getKey()), e, subValue));
+ promises.add(mapping.mapper.update(connection, resource, path.child(me.getKey()), e, subValue));
}
}
// Invoke mappings for which there were no values provided.
for (final Mapping mapping : missingMappings.values()) {
- promises.add(mapping.mapper.update(connection, path.child(mapping.name), e, null));
+ promises.add(mapping.mapper.update(connection, resource, path.child(mapping.name), e, null));
}
return Promises.when(promises)
.then(this.<Modification> accumulateResults());
} catch (final Exception ex) {
- return Promises.newExceptionPromise(asResourceException(ex));
+ return asResourceException(ex).asPromise();
}
}
@@ -306,13 +383,13 @@
}
/** Fail immediately if the JSON value has the wrong type or contains unknown attributes. */
- private Map<String, Mapping> checkMapping(final JsonPointer path, final JsonValue v)
- throws ResourceException {
+ private Map<String, Mapping> validateJsonValue(final JsonPointer path, final JsonValue v) throws ResourceException {
final Map<String, Mapping> missingMappings = new LinkedHashMap<>(mappings);
if (v != null && !v.isNull()) {
if (v.isMap()) {
for (final String attribute : v.asMap().keySet()) {
- if (missingMappings.remove(toLowerCase(attribute)) == null) {
+ if (missingMappings.remove(toLowerCase(attribute)) == null
+ && !isIncludedDefaultUserAttribute(attribute)) {
throw newBadRequestException(ERR_UNRECOGNIZED_FIELD.get(path.child(attribute)));
}
}
@@ -323,12 +400,31 @@
return missingMappings;
}
- private Mapping getMapping(final JsonPointer jsonAttribute) {
- return jsonAttribute.isEmpty() ? null : getMapping(jsonAttribute.get(0));
+ private Mapping getMappingOrNull(final JsonPointer jsonAttribute) {
+ return jsonAttribute.isEmpty() ? null : getMappingOrNull(jsonAttribute.get(0));
+ }
+
+ private Mapping getMappingOrNull(final String jsonAttribute) {
+ final Mapping mapping = mappings.get(toLowerCase(jsonAttribute));
+ if (mapping != null) {
+ return mapping;
+ }
+ if (isIncludedDefaultUserAttribute(jsonAttribute)) {
+ return new Mapping(jsonAttribute, simple(jsonAttribute));
+ }
+ return null;
}
private Mapping getMapping(final String jsonAttribute) {
- return mappings.get(toLowerCase(jsonAttribute));
+ final Mapping mappingOrNull = getMappingOrNull(jsonAttribute);
+ if (mappingOrNull != null) {
+ return mappingOrNull;
+ }
+ throw new IllegalStateException("Unexpected null mapping for jsonAttribute: " + jsonAttribute);
}
+ private boolean isIncludedDefaultUserAttribute(final String attributeName) {
+ return includeAllUserAttributesByDefault
+ && (excludedDefaultUserAttributes.isEmpty() || !excludedDefaultUserAttributes.contains(attributeName));
+ }
}
--
Gitblit v1.10.0