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

Matthew Swift
15.42.2016 a08c81f677247ec9eb7721a86250c663065e9930
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));
    }
}