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

Matthew Swift
10.31.2013 507e00fb190713b1654579123d284bcd3d750abe
opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json
@@ -134,12 +134,13 @@
        // The REST APIs and their LDAP attribute mappings.
        "mappings" : {
            "/users" : {
                "baseDN" : "ou=people,dc=example,dc=com",
                "readOnUpdatePolicy" : "controls",
                "useSubtreeDelete" : true,
                "etagAttribute" : "etag",
                "namingStrategy" : {
                    "strategy" : "clientDNNaming",
                "baseDN"              : "ou=people,dc=example,dc=com",
                "readOnUpdatePolicy"  : "controls",
                "useSubtreeDelete"    : true,
                "usePermissiveModify" : true,
                "etagAttribute"       : "etag",
                "namingStrategy"      : {
                    "strategy"    : "clientDNNaming",
                    "dnAttribute" : "uid"
                },
                "additionalLDAPAttributes" : [
@@ -192,12 +193,13 @@
                }
            },
            "/groups" : {
                "baseDN" : "ou=groups,dc=example,dc=com",
                "readOnUpdatePolicy" : "controls",
                "useSubtreeDelete" : true,
                "etagAttribute" : "etag",
                "namingStrategy" : {
                    "strategy" : "clientDNNaming",
                "baseDN"              : "ou=groups,dc=example,dc=com",
                "readOnUpdatePolicy"  : "controls",
                "useSubtreeDelete"    : true,
                "usePermissiveModify" : true,
                "etagAttribute"       : "etag",
                "namingStrategy"      : {
                    "strategy"    : "clientDNNaming",
                    "dnAttribute" : "cn"
                },
                "additionalLDAPAttributes" : [
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
New file
@@ -0,0 +1,217 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2013 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.forgerock.opendj.ldap.Attributes.emptyAttribute;
import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResultHandler;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.AttributeDescription;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.LinkedAttribute;
import org.forgerock.opendj.ldap.Modification;
import org.forgerock.opendj.ldap.ModificationType;
/**
 * An abstract LDAP attribute mapper which provides a simple mapping from a JSON
 * value to a single LDAP attribute.
 */
abstract class AbstractLDAPAttributeMapper<T extends AbstractLDAPAttributeMapper<T>> extends
        AttributeMapper {
    Object defaultJSONValue = null;
    List<Object> defaultJSONValues = emptyList();
    final AttributeDescription ldapAttributeName;
    private boolean isRequired = false;
    private boolean isSingleValued = false;
    private WritabilityPolicy writabilityPolicy = READ_WRITE;
    AbstractLDAPAttributeMapper(final AttributeDescription ldapAttributeName) {
        this.ldapAttributeName = ldapAttributeName;
    }
    /**
     * Indicates that the LDAP attribute is mandatory and must be provided
     * during create requests.
     *
     * @return This attribute mapper.
     */
    public final T isRequired() {
        this.isRequired = true;
        return getThis();
    }
    /**
     * Indicates that multi-valued LDAP attribute should be represented as a
     * single-valued JSON value, rather than an array of values.
     *
     * @return This attribute mapper.
     */
    public final T isSingleValued() {
        this.isSingleValued = true;
        return getThis();
    }
    /**
     * Indicates whether or not the LDAP attribute supports updates. The default
     * is {@link WritabilityPolicy#READ_WRITE}.
     *
     * @param policy
     *            The writability policy.
     * @return This attribute mapper.
     */
    public final T writability(final WritabilityPolicy policy) {
        this.writabilityPolicy = policy;
        return getThis();
    }
    boolean attributeIsRequired() {
        return isRequired && defaultJSONValue == null;
    }
    boolean attributeIsSingleValued() {
        return isSingleValued || ldapAttributeName.getAttributeType().isSingleValue();
    }
    @Override
    void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath,
            final Set<String> ldapAttributes) {
        ldapAttributes.add(ldapAttributeName.toString());
    }
    abstract void getNewLDAPAttributes(Context c, JsonPointer path, List<Object> newValues,
            ResultHandler<Attribute> h);
    abstract T getThis();
    @Override
    void toLDAP(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
            final ResultHandler<List<Modification>> h) {
        // Make following code readable.
        final boolean isUpdateRequest = e != null;
        final boolean isCreateRequest = !isUpdateRequest;
        // Get the existing LDAP attribute.
        final Attribute oldLDAPAttribute;
        if (isCreateRequest) {
            oldLDAPAttribute = emptyAttribute(ldapAttributeName);
        } else {
            final Attribute tmp = e.getAttribute(ldapAttributeName);
            oldLDAPAttribute = tmp != null ? tmp : emptyAttribute(ldapAttributeName);
        }
        if (v != null && v.isList() && attributeIsSingleValued()) {
            // Single-valued field violation.
            h.handleError(new BadRequestException(i18n(
                    "The request cannot be processed because an array of values was "
                            + "provided for the single valued field '%s'", path)));
        } else {
            getNewLDAPAttributes(c, path, asList(v), new ResultHandler<Attribute>() {
                @Override
                public void handleError(final ResourceException error) {
                    h.handleError(error);
                }
                @Override
                public void handleResult(final Attribute newLDAPAttribute) {
                    /*
                     * If the attribute is read-only then handle the following
                     * cases:
                     *
                     * 1) new values are provided and they are the same as the
                     * existing values
                     *
                     * 2) no new values are provided.
                     */
                    if (isCreateRequest && !writabilityPolicy.canCreate(ldapAttributeName)
                            || isUpdateRequest && !writabilityPolicy.canWrite(ldapAttributeName)) {
                        if (newLDAPAttribute.isEmpty()
                                || (isUpdateRequest && newLDAPAttribute.equals(oldLDAPAttribute))
                                || writabilityPolicy.discardWrites()) {
                            // No change.
                            h.handleResult(Collections.<Modification> emptyList());
                        } else {
                            h.handleError(new BadRequestException(i18n(
                                    "The request cannot be processed because it attempts to modify "
                                            + "the read-only field '%s'", path)));
                        }
                    } else {
                        // Compute the changes to the attribute.
                        final List<Modification> modifications;
                        if (oldLDAPAttribute.isEmpty() && newLDAPAttribute.isEmpty()) {
                            // No change.
                            modifications = Collections.<Modification> emptyList();
                        } else if (oldLDAPAttribute.isEmpty() || newLDAPAttribute.isEmpty()) {
                            // Delete or add.
                            modifications =
                                    singletonList(new Modification(ModificationType.REPLACE,
                                            newLDAPAttribute));
                        } else {
                            /*
                             * We could do a replace, but try to save bandwidth
                             * and send diffs instead. Perform deletes first in
                             * case we don't have an appropriate normalizer:
                             * permissive add(x) followed by delete(x) is
                             * destructive, whereas delete(x) followed by add(x)
                             * is idempotent when adding/removing the same
                             * value.
                             */
                            modifications = new ArrayList<Modification>(2);
                            final Attribute deletedValues = new LinkedAttribute(oldLDAPAttribute);
                            deletedValues.removeAll(newLDAPAttribute);
                            if (!deletedValues.isEmpty()) {
                                modifications.add(new Modification(ModificationType.DELETE,
                                        deletedValues));
                            }
                            final Attribute addedValues = new LinkedAttribute(newLDAPAttribute);
                            addedValues.removeAll(oldLDAPAttribute);
                            if (!addedValues.isEmpty()) {
                                modifications.add(new Modification(ModificationType.ADD,
                                        addedValues));
                            }
                        }
                        h.handleResult(modifications);
                    }
                }
            });
        }
    }
    private List<Object> asList(final JsonValue v) {
        if (v == null || v.isNull() || (v.isList() && v.size() == 0)) {
            return defaultJSONValues;
        } else if (v.isList()) {
            return v.asList();
        } else {
            return singletonList(v.getObject());
        }
    }
}
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
@@ -21,9 +21,9 @@
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.ResultHandler;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Modification;
/**
 * An attribute mapper is responsible for converting JSON values to and from
@@ -56,7 +56,8 @@
     *            The set into which the required LDAP attribute names should be
     *            put.
     */
    abstract void getLDAPAttributes(Context c, JsonPointer jsonAttribute, Set<String> ldapAttributes);
    abstract void getLDAPAttributes(Context c, JsonPointer path, JsonPointer subPath,
            Set<String> ldapAttributes);
    /**
     * Transforms the provided REST comparison filter parameters to an LDAP
@@ -86,7 +87,7 @@
     * @param h
     *            The result handler.
     */
    abstract void getLDAPFilter(Context c, FilterType type, JsonPointer jsonAttribute,
    abstract void getLDAPFilter(Context c, JsonPointer path, JsonPointer subPath, FilterType type,
            String operator, Object valueAssertion, ResultHandler<Filter> h);
    /**
@@ -113,7 +114,7 @@
     * @param h
     *            The result handler.
     */
    abstract void toJSON(Context c, Entry e, ResultHandler<JsonValue> h);
    abstract void toJSON(Context c, JsonPointer path, Entry e, ResultHandler<JsonValue> h);
    /**
     * Maps a JSON value to one or more LDAP attributes, invoking a completion
@@ -139,7 +140,8 @@
     * @param h
     *            The result handler.
     */
    abstract void toLDAP(Context c, JsonValue v, ResultHandler<List<Attribute>> h);
    abstract void toLDAP(Context c, JsonPointer path, Entry e, JsonValue v,
            ResultHandler<List<Modification>> h);
    // TODO: methods for obtaining schema information (e.g. name, description,
    // type information).
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java
@@ -16,6 +16,7 @@
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull;
import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.Context;
@@ -88,7 +89,7 @@
    AuthenticatedConnectionContext(final JsonValue savedContext, final PersistenceConfig config)
            throws ResourceException {
        super(savedContext, config);
        throw new InternalServerErrorException("Cached LDAP connections cannot be restored");
        throw new InternalServerErrorException(i18n("Cached LDAP connections cannot be restored"));
    }
    /**
@@ -98,7 +99,7 @@
    protected void saveToJson(final JsonValue savedContext, final PersistenceConfig config)
            throws ResourceException {
        super.saveToJson(savedContext, config);
        throw new InternalServerErrorException("Cached LDAP connections cannot be persisted");
        throw new InternalServerErrorException(i18n("Cached LDAP connections cannot be persisted"));
    }
    /**
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java
@@ -16,6 +16,7 @@
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.isJSONPrimitive;
import java.util.ArrayList;
@@ -53,8 +54,8 @@
                DN.valueOf(authzId.substring(3), schema);
            } catch (final IllegalArgumentException e) {
                throw new ForbiddenException(
                        "The request could not be authorized because the required security principal "
                                + " was not a valid LDAP DN");
                        i18n("The request could not be authorized because the required "
                                + "security principal was not a valid LDAP DN"));
            }
            return authzId;
        }
@@ -135,15 +136,13 @@
            if (isJSONPrimitive(value)) {
                values[i] = String.valueOf(value);
            } else if (value == null) {
                // FIXME: i18n.
                throw new ForbiddenException(
                        "The request could not be authorized because the required security principal "
                                + key + " could not be determined");
                throw new ForbiddenException(i18n(
                        "The request could not be authorized because the required "
                                + "security principal '%s' could not be determined", key));
            } else {
                // FIXME: i18n.
                throw new ForbiddenException(
                        "The request could not be authorized because the required security principal "
                                + key + " had an invalid data type");
                throw new ForbiddenException(i18n(
                        "The request could not be authorized because the required "
                                + "security principal '%s' had an invalid data type", key));
            }
        }
        return values;
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
@@ -30,15 +30,17 @@
    private final ReadOnUpdatePolicy readOnUpdatePolicy;
    private final Schema schema;
    private final boolean useSubtreeDelete;
    private final boolean usePermissiveModify;
    Config(final ConnectionFactory factory, final ReadOnUpdatePolicy readOnUpdatePolicy,
            final AuthorizationPolicy authzPolicy, final AuthzIdTemplate proxiedAuthzTemplate,
            final boolean useSubtreeDelete, final Schema schema) {
            final boolean useSubtreeDelete, final boolean usePermissiveModify, final Schema schema) {
        this.factory = factory;
        this.readOnUpdatePolicy = readOnUpdatePolicy;
        this.authzPolicy = authzPolicy;
        this.proxiedAuthzTemplate = proxiedAuthzTemplate;
        this.useSubtreeDelete = useSubtreeDelete;
        this.usePermissiveModify = usePermissiveModify;
        this.schema = schema;
        this.options = new DecodeOptions().setSchema(schema);
    }
@@ -88,6 +90,17 @@
    }
    /**
     * Returns {@code true} if modify requests should include the permissive
     * modify control.
     *
     * @return {@code true} if modify requests should include the permissive
     *         modify control.
     */
    boolean usePermissiveModify() {
        return usePermissiveModify;
    }
    /**
     * Returns {@code true} if delete requests should include the subtree delete
     * control.
     *
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
@@ -27,9 +27,9 @@
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.ResultHandler;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Modification;
/**
 * An attribute mapper which maps a single JSON attribute to a fixed value.
@@ -42,16 +42,17 @@
    }
    @Override
    void getLDAPAttributes(final Context c, final JsonPointer jsonAttribute,
    void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath,
            final Set<String> ldapAttributes) {
        // Nothing to do.
    }
    @Override
    void getLDAPFilter(final Context c, final FilterType type, final JsonPointer jsonAttribute,
            final String operator, final Object valueAssertion, final ResultHandler<Filter> h) {
    void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath,
            final FilterType type, final String operator, final Object valueAssertion,
            final ResultHandler<Filter> h) {
        final Filter filter;
        final JsonValue subValue = value.get(jsonAttribute);
        final JsonValue subValue = value.get(subPath);
        if (subValue == null) {
            filter = alwaysFalse();
        } else if (type == FilterType.PRESENT) {
@@ -86,14 +87,16 @@
    }
    @Override
    void toJSON(final Context c, final Entry e, final ResultHandler<JsonValue> h) {
    void toJSON(final Context c, final JsonPointer path, final Entry e,
            final ResultHandler<JsonValue> h) {
        h.handleResult(value.copy());
    }
    @Override
    void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) {
    void toLDAP(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
            final ResultHandler<List<Modification>> h) {
        // FIXME: should we check if the provided value matches the constant?
        h.handleResult(Collections.<Attribute> emptyList());
        h.handleResult(Collections.<Modification> emptyList());
    }
    private <T extends Comparable<T>> Filter compare(final Context c, final FilterType type,
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
@@ -17,9 +17,14 @@
import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
import static org.forgerock.opendj.ldap.Filter.alwaysTrue;
import static org.forgerock.opendj.ldap.requests.Requests.newAddRequest;
import static org.forgerock.opendj.ldap.requests.Requests.newDeleteRequest;
import static org.forgerock.opendj.ldap.requests.Requests.newModifyRequest;
import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
import static org.forgerock.opendj.rest2ldap.Utils.accumulate;
import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.toFilter;
import static org.forgerock.opendj.rest2ldap.Utils.transform;
@@ -39,8 +44,10 @@
import org.forgerock.json.resource.CollectionResourceProvider;
import org.forgerock.json.resource.CreateRequest;
import org.forgerock.json.resource.DeleteRequest;
import org.forgerock.json.resource.InternalServerErrorException;
import org.forgerock.json.resource.NotSupportedException;
import org.forgerock.json.resource.PatchRequest;
import org.forgerock.json.resource.PreconditionFailedException;
import org.forgerock.json.resource.QueryFilter;
import org.forgerock.json.resource.QueryFilterVisitor;
import org.forgerock.json.resource.QueryRequest;
@@ -61,16 +68,19 @@
import org.forgerock.opendj.ldap.ErrorResultException;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Function;
import org.forgerock.opendj.ldap.Modification;
import org.forgerock.opendj.ldap.ModificationType;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.controls.AssertionRequestControl;
import org.forgerock.opendj.ldap.controls.PermissiveModifyRequestControl;
import org.forgerock.opendj.ldap.controls.PostReadRequestControl;
import org.forgerock.opendj.ldap.controls.PostReadResponseControl;
import org.forgerock.opendj.ldap.controls.PreReadRequestControl;
import org.forgerock.opendj.ldap.controls.PreReadResponseControl;
import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl;
import org.forgerock.opendj.ldap.requests.AddRequest;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
@@ -126,22 +136,31 @@
            @Override
            public void run() {
                // Calculate entry content.
                attributeMapper.toLDAP(c, request.getContent(),
                        new ResultHandler<List<Attribute>>() {
                attributeMapper.toLDAP(c, new JsonPointer(), null, request.getContent(),
                        new ResultHandler<List<Modification>>() {
                            @Override
                            public void handleError(final ResourceException error) {
                                h.handleError(error);
                            }
                            @Override
                            public void handleResult(final List<Attribute> result) {
                            public void handleResult(final List<Modification> result) {
                                // Perform add operation.
                                final AddRequest addRequest = Requests.newAddRequest(DN.rootDN());
                                final AddRequest addRequest = newAddRequest(DN.rootDN());
                                for (final Attribute attribute : additionalLDAPAttributes) {
                                    addRequest.addAttribute(attribute);
                                }
                                for (final Attribute attribute : result) {
                                    addRequest.addAttribute(attribute);
                                for (final Modification modification : result) {
                                    if (modification.getModificationType() == ModificationType.ADD
                                            || modification.getModificationType() == ModificationType.REPLACE) {
                                        addRequest.addAttribute(modification.getAttribute());
                                    } else {
                                        // Attribute mappers must return add/replace updates.
                                        h.handleError(new InternalServerErrorException(
                                                i18n("Attribute mapper returned a modification which "
                                                        + "does not add an attribute")));
                                        return;
                                    }
                                }
                                try {
                                    nameStrategy.setResourceId(c, getBaseDN(c), request
@@ -175,9 +194,12 @@
            @Override
            public void run() {
                // Find the entry and then delete it.
                final String ldapAttribute =
                        (etagAttribute != null && request.getRevision() != null) ? etagAttribute
                                .toString() : "1.1";
                final SearchRequest searchRequest =
                        nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId).addAttribute(
                                "1.1");
                                ldapAttribute);
                c.getConnection().searchSingleEntryAsync(searchRequest,
                        new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() {
                            @Override
@@ -188,9 +210,12 @@
                            @Override
                            public void handleResult(final SearchResultEntry entry) {
                                try {
                                    // Fail-fast if there is a version mismatch.
                                    ensureMVCCVersionMatches(entry, request.getRevision());
                                    // Perform delete operation.
                                    final ChangeRecord deleteRequest =
                                            Requests.newDeleteRequest(entry.getName());
                                            newDeleteRequest(entry.getName());
                                    if (config.readOnUpdatePolicy() == CONTROLS) {
                                        final String[] attributes =
                                                getLDAPAttributes(c, request.getFields());
@@ -225,7 +250,10 @@
        final Context c = wrap(context);
        final QueryResultHandler h = wrap(c, handler);
        // Get the connection, then calculate the search filter, then perform the search.
        /*
         * Get the connection, then calculate the search filter, then perform
         * the search.
         */
        c.run(h, new Runnable() {
            @Override
            public void run() {
@@ -238,17 +266,19 @@
                    @Override
                    public void handleResult(final Filter ldapFilter) {
                        // Avoid performing a search if the filter could not be mapped or if it will never match.
                        /*
                         * Avoid performing a search if the filter could not be
                         * mapped or if it will never match.
                         */
                        if (ldapFilter == null || ldapFilter == alwaysFalse()) {
                            h.handleResult(new QueryResult());
                        } else {
                            // Perform the search.
                            final String[] attributes = getLDAPAttributes(c, request.getFields());
                            final SearchRequest request =
                                    Requests.newSearchRequest(getBaseDN(c),
                                            SearchScope.SINGLE_LEVEL, ldapFilter == Filter
                                                    .alwaysTrue() ? Filter.objectClassPresent()
                                                    : ldapFilter, attributes);
                                    newSearchRequest(getBaseDN(c), SearchScope.SINGLE_LEVEL,
                                            ldapFilter == Filter.alwaysTrue() ? Filter
                                                    .objectClassPresent() : ldapFilter, attributes);
                            c.getConnection().searchAsync(request, null, new SearchResultHandler() {
                                private final AtomicInteger pendingResourceCount =
                                        new AtomicInteger();
@@ -291,7 +321,7 @@
                                            };
                                    pendingResourceCount.incrementAndGet();
                                    attributeMapper.toJSON(c, entry, mapHandler);
                                    attributeMapper.toJSON(c, new JsonPointer(), entry, mapHandler);
                                    return true;
                                }
@@ -373,31 +403,132 @@
    @Override
    public void updateInstance(final ServerContext context, final String resourceId,
            final UpdateRequest request, final ResultHandler<Resource> handler) {
        handler.handleError(new NotSupportedException("Not yet implemented"));
        /*
         * Update operations are a bit awkward because there is not direct
         * mapping to LDAP. We need to convert the update request into an LDAP
         * modify operation which means reading the current LDAP entry,
         * generating the new entry content, then comparing the two in order to
         * obtain a set of changes. We also need to handle read-only fields
         * correctly: if a read-only field is included with the new resource
         * then it must match exactly the value of the existing field.
         */
        final Context c = wrap(context);
        final ResultHandler<Resource> h = wrap(c, handler);
        // Get connection then perform the search.
        c.run(h, new Runnable() {
            @Override
            public void run() {
                // First of all read the existing entry.
                final String[] attributes =
                        getLDAPAttributes(c, Collections.<JsonPointer> emptyList());
                final SearchRequest searchRequest =
                        nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId).addAttribute(
                                attributes);
                c.getConnection().searchSingleEntryAsync(searchRequest,
                        new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() {
                            @Override
                            public void handleErrorResult(final ErrorResultException error) {
                                h.handleError(asResourceException(error));
                            }
                            @Override
                            public void handleResult(final SearchResultEntry entry) {
                                try {
                                    // Fail-fast if there is a version mismatch.
                                    ensureMVCCVersionMatches(entry, request.getRevision());
                                    //  Create the modify request.
                                    final ModifyRequest modifyRequest =
                                            newModifyRequest(entry.getName());
                                    if (config.readOnUpdatePolicy() == CONTROLS) {
                                        final String[] attributes =
                                                getLDAPAttributes(c, request.getFields());
                                        modifyRequest.addControl(PostReadRequestControl.newControl(
                                                false, attributes));
                                    }
                                    if (config.usePermissiveModify()) {
                                        modifyRequest.addControl(PermissiveModifyRequestControl
                                                .newControl(true));
                                    }
                                    addAssertionControl(modifyRequest, request.getRevision());
                                    /*
                                     * Determine the set of changes that need to
                                     * be performed.
                                     */
                                    attributeMapper.toLDAP(c, new JsonPointer(), entry, request
                                            .getNewContent(),
                                            new ResultHandler<List<Modification>>() {
                                                @Override
                                                public void handleError(
                                                        final ResourceException error) {
                                                    h.handleError(error);
                                                }
                                                @Override
                                                public void handleResult(
                                                        final List<Modification> result) {
                                                    // Perform the modify operation.
                                                    modifyRequest.getModifications().addAll(result);
                                                    c.getConnection().applyChangeAsync(
                                                            modifyRequest, null,
                                                            postUpdateHandler(c, h));
                                                }
                                            });
                                } catch (final Exception e) {
                                    h.handleError(asResourceException(e));
                                }
                            }
                        });
            }
        });
    }
    private void adaptEntry(final Context c, final Entry entry,
            final ResultHandler<Resource> handler) {
        final String actualResourceId = nameStrategy.getResourceId(c, entry);
        final String revision = getRevisionFromEntry(entry);
        attributeMapper.toJSON(c, entry, transform(new Function<JsonValue, Resource, Void>() {
            @Override
            public Resource apply(final JsonValue value, final Void p) {
                return new Resource(actualResourceId, revision, new JsonValue(value));
            }
        }, handler));
        attributeMapper.toJSON(c, new JsonPointer(), entry, transform(
                new Function<JsonValue, Resource, Void>() {
                    @Override
                    public Resource apply(final JsonValue value, final Void p) {
                        return new Resource(actualResourceId, revision, new JsonValue(value));
                    }
                }, handler));
    }
    private void addAssertionControl(final ChangeRecord request, final String revision)
            throws NotSupportedException {
        if (revision != null) {
            if (etagAttribute != null) {
                request.addControl(AssertionRequestControl.newControl(true, Filter.equality(
                        etagAttribute.toString(), revision)));
            } else {
                // FIXME: i18n
                throw new NotSupportedException(
                        "Multi-version concurrency control is not supported by this resource");
    private void addAssertionControl(final ChangeRecord request, final String expectedRevision)
            throws ResourceException {
        if (expectedRevision != null) {
            ensureMVCCSupported();
            request.addControl(AssertionRequestControl.newControl(true, Filter.equality(
                    etagAttribute.toString(), expectedRevision)));
        }
    }
    private void ensureMVCCSupported() throws NotSupportedException {
        if (etagAttribute == null) {
            throw new NotSupportedException(
                    i18n("Multi-version concurrency control is not supported by this resource"));
        }
    }
    private void ensureMVCCVersionMatches(final Entry entry, final String expectedRevision)
            throws ResourceException {
        if (expectedRevision != null) {
            ensureMVCCSupported();
            final String actualRevision = entry.parseAttribute(etagAttribute).asString();
            if (actualRevision == null) {
                throw new PreconditionFailedException(i18n(
                        "The resource could not be accessed because it did not contain any "
                                + "version information, when the version '%s' was expected",
                        expectedRevision));
            } else if (!expectedRevision.equals(actualRevision)) {
                throw new PreconditionFailedException(i18n(
                        "The resource could not be accessed because the expected version '%s' "
                                + "does not match the current version '%s'", expectedRevision,
                        actualRevision));
            }
        }
    }
@@ -422,12 +553,14 @@
        if (requestedAttributes.isEmpty()) {
            // Full read.
            requestedLDAPAttributes = new LinkedHashSet<String>();
            attributeMapper.getLDAPAttributes(c, new JsonPointer(), requestedLDAPAttributes);
            attributeMapper.getLDAPAttributes(c, new JsonPointer(), new JsonPointer(),
                    requestedLDAPAttributes);
        } else {
            // Partial read.
            requestedLDAPAttributes = new LinkedHashSet<String>(requestedAttributes.size());
            for (final JsonPointer requestedAttribute : requestedAttributes) {
                attributeMapper.getLDAPAttributes(c, requestedAttribute, requestedLDAPAttributes);
                attributeMapper.getLDAPAttributes(c, new JsonPointer(), requestedAttribute,
                        requestedLDAPAttributes);
            }
        }
@@ -488,16 +621,16 @@
                    @Override
                    public Void visitContainsFilter(final ResultHandler<Filter> p,
                            final JsonPointer field, final Object valueAssertion) {
                        attributeMapper.getLDAPFilter(c, FilterType.CONTAINS, field, null,
                                valueAssertion, p);
                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
                                FilterType.CONTAINS, null, valueAssertion, p);
                        return null;
                    }
                    @Override
                    public Void visitEqualsFilter(final ResultHandler<Filter> p,
                            final JsonPointer field, final Object valueAssertion) {
                        attributeMapper.getLDAPFilter(c, FilterType.EQUAL_TO, field, null,
                                valueAssertion, p);
                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
                                FilterType.EQUAL_TO, null, valueAssertion, p);
                        return null;
                    }
@@ -505,40 +638,40 @@
                    public Void visitExtendedMatchFilter(final ResultHandler<Filter> p,
                            final JsonPointer field, final String operator,
                            final Object valueAssertion) {
                        attributeMapper.getLDAPFilter(c, FilterType.EXTENDED, field, operator,
                                valueAssertion, p);
                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
                                FilterType.EXTENDED, operator, valueAssertion, p);
                        return null;
                    }
                    @Override
                    public Void visitGreaterThanFilter(final ResultHandler<Filter> p,
                            final JsonPointer field, final Object valueAssertion) {
                        attributeMapper.getLDAPFilter(c, FilterType.GREATER_THAN, field, null,
                                valueAssertion, p);
                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
                                FilterType.GREATER_THAN, null, valueAssertion, p);
                        return null;
                    }
                    @Override
                    public Void visitGreaterThanOrEqualToFilter(final ResultHandler<Filter> p,
                            final JsonPointer field, final Object valueAssertion) {
                        attributeMapper.getLDAPFilter(c, FilterType.GREATER_THAN_OR_EQUAL_TO,
                                field, null, valueAssertion, p);
                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
                                FilterType.GREATER_THAN_OR_EQUAL_TO, null, valueAssertion, p);
                        return null;
                    }
                    @Override
                    public Void visitLessThanFilter(final ResultHandler<Filter> p,
                            final JsonPointer field, final Object valueAssertion) {
                        attributeMapper.getLDAPFilter(c, FilterType.LESS_THAN, field, null,
                                valueAssertion, p);
                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
                                FilterType.LESS_THAN, null, valueAssertion, p);
                        return null;
                    }
                    @Override
                    public Void visitLessThanOrEqualToFilter(final ResultHandler<Filter> p,
                            final JsonPointer field, final Object valueAssertion) {
                        attributeMapper.getLDAPFilter(c, FilterType.LESS_THAN_OR_EQUAL_TO, field,
                                null, valueAssertion, p);
                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
                                FilterType.LESS_THAN_OR_EQUAL_TO, null, valueAssertion, p);
                        return null;
                    }
@@ -598,20 +731,24 @@
                    @Override
                    public Void visitPresentFilter(final ResultHandler<Filter> p,
                            final JsonPointer field) {
                        attributeMapper.getLDAPFilter(c, FilterType.PRESENT, field, null, null, p);
                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
                                FilterType.PRESENT, null, null, p);
                        return null;
                    }
                    @Override
                    public Void visitStartsWithFilter(final ResultHandler<Filter> p,
                            final JsonPointer field, final Object valueAssertion) {
                        attributeMapper.getLDAPFilter(c, FilterType.STARTS_WITH, field, null,
                                valueAssertion, p);
                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
                                FilterType.STARTS_WITH, null, valueAssertion, p);
                        return null;
                    }
                };
        // Note that the returned LDAP filter may be null if it could not be mapped by any attribute mappers.
        /*
         * Note that the returned LDAP filter may be null if it could not be
         * mapped by any attribute mappers.
         */
        queryFilter.accept(visitor, h);
    }
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
@@ -17,6 +17,7 @@
import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
import static org.forgerock.opendj.rest2ldap.Utils.accumulate;
import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.toLowerCase;
import static org.forgerock.opendj.rest2ldap.Utils.transform;
@@ -32,10 +33,10 @@
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.ResultHandler;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Function;
import org.forgerock.opendj.ldap.Modification;
/**
 * An attribute mapper which maps JSON objects to LDAP attributes.
@@ -50,6 +51,11 @@
            this.name = name;
            this.mapper = mapper;
        }
        @Override
        public String toString() {
            return name + " -> " + mapper;
        }
    }
    private final Map<String, Mapping> mappings = new LinkedHashMap<String, Mapping>();
@@ -74,43 +80,50 @@
    }
    @Override
    void getLDAPAttributes(final Context c, final JsonPointer jsonAttribute,
    void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath,
            final Set<String> ldapAttributes) {
        if (jsonAttribute.isEmpty()) {
        if (subPath.isEmpty()) {
            // Request all subordinate mappings.
            for (final Mapping mapping : mappings.values()) {
                mapping.mapper.getLDAPAttributes(c, jsonAttribute, ldapAttributes);
                mapping.mapper.getLDAPAttributes(c, path.child(mapping.name), subPath,
                        ldapAttributes);
            }
        } else {
            // Request single subordinate mapping.
            final Mapping mapping = getMapping(jsonAttribute);
            final Mapping mapping = getMapping(subPath);
            if (mapping != null) {
                final JsonPointer relativePointer = jsonAttribute.relativePointer();
                mapping.mapper.getLDAPAttributes(c, relativePointer, ldapAttributes);
                mapping.mapper.getLDAPAttributes(c, path.child(subPath.get(0)), subPath
                        .relativePointer(), ldapAttributes);
            }
        }
    }
    @Override
    void getLDAPFilter(final Context c, final FilterType type, final JsonPointer jsonAttribute,
            final String operator, final Object valueAssertion, final ResultHandler<Filter> h) {
        final Mapping mapping = getMapping(jsonAttribute);
    void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath,
            final FilterType type, final String operator, final Object valueAssertion,
            final ResultHandler<Filter> h) {
        final Mapping mapping = getMapping(subPath);
        if (mapping != null) {
            final JsonPointer relativePointer = jsonAttribute.relativePointer();
            mapping.mapper.getLDAPFilter(c, type, relativePointer, operator, valueAssertion, h);
            mapping.mapper.getLDAPFilter(c, path.child(subPath.get(0)), subPath.relativePointer(),
                    type, operator, valueAssertion, h);
        } 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.
            /*
             * 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.
             */
            h.handleResult(alwaysFalse());
        }
    }
    @Override
    void toJSON(final Context c, final Entry e, final ResultHandler<JsonValue> h) {
        // 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.
    void toJSON(final Context c, final JsonPointer path, final Entry e,
            final ResultHandler<JsonValue> h) {
        /*
         * 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 ResultHandler<Map.Entry<String, JsonValue>> handler =
                accumulate(mappings.size(), transform(
                        new Function<List<Map.Entry<String, JsonValue>>, JsonValue, Void>() {
@@ -118,11 +131,16 @@
                            public JsonValue apply(final List<Map.Entry<String, JsonValue>> value,
                                    final Void p) {
                                if (value.isEmpty()) {
                                    // No subordinate attributes, so omit the entire JSON object
                                    // from the resource.
                                    /*
                                     * No subordinate attributes, so omit the
                                     * entire JSON object from the resource.
                                     */
                                    return null;
                                } else {
                                    // Combine the sub-attributes into a single JSON object.
                                    /*
                                     * Combine the sub-attributes into a single
                                     * JSON object.
                                     */
                                    final Map<String, Object> result =
                                            new LinkedHashMap<String, Object>(value.size());
                                    for (final Map.Entry<String, JsonValue> e : value) {
@@ -134,7 +152,7 @@
                        }, h));
        for (final Mapping mapping : mappings.values()) {
            mapping.mapper.toJSON(c, e, transform(
            mapping.mapper.toJSON(c, path.child(mapping.name), e, transform(
                    new Function<JsonValue, Map.Entry<String, JsonValue>, Void>() {
                        @Override
                        public Map.Entry<String, JsonValue> apply(final JsonValue value,
@@ -147,30 +165,39 @@
    }
    @Override
    void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) {
        // Fail immediately if the JSON value has the wrong type or contains unknown attributes.
    void toLDAP(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
            final ResultHandler<List<Modification>> h) {
        /*
         * Fail immediately if the JSON value has the wrong type or contains
         * unknown attributes.
         */
        final Map<String, Mapping> missingMappings = new LinkedHashMap<String, Mapping>(mappings);
        if (v != null && !v.isNull()) {
            if (v.isMap()) {
                for (final String attribute : v.asMap().keySet()) {
                    if (missingMappings.remove(toLowerCase(attribute)) == null) {
                        h.handleError(new BadRequestException("unrecognized attribute '"
                                + attribute + "'"));
                        h.handleError(new BadRequestException(i18n(
                                "The request cannot be processed because the JSON resource "
                                        + "contains an unrecognized field '%s'", path
                                        .child(attribute))));
                        return;
                    }
                }
            } else {
                h.handleError(new BadRequestException("JSON object expected"));
                h.handleError(new BadRequestException(i18n(
                        "The request cannot be processed because the JSON resource "
                                + "contains the field '%s' whose value is the wrong type: "
                                + "an object is expected", path)));
                return;
            }
        }
        // Accumulate the results of the subordinate mappings.
        final ResultHandler<List<Attribute>> handler =
        final ResultHandler<List<Modification>> handler =
                accumulate(mappings.size(), transform(
                        new Function<List<List<Attribute>>, List<Attribute>, Void>() {
                        new Function<List<List<Modification>>, List<Modification>, Void>() {
                            @Override
                            public List<Attribute> apply(final List<List<Attribute>> value,
                            public List<Modification> apply(final List<List<Modification>> value,
                                    final Void p) {
                                switch (value.size()) {
                                case 0:
@@ -178,9 +205,9 @@
                                case 1:
                                    return value.get(0);
                                default:
                                    final List<Attribute> attributes =
                                            new ArrayList<Attribute>(value.size());
                                    for (final List<Attribute> a : value) {
                                    final List<Modification> attributes =
                                            new ArrayList<Modification>(value.size());
                                    for (final List<Modification> a : value) {
                                        attributes.addAll(a);
                                    }
                                    return attributes;
@@ -190,16 +217,16 @@
        // Invoke mappings for which there are values provided.
        if (v != null && !v.isNull()) {
            for (final Map.Entry<String, Object> e : v.asMap().entrySet()) {
                final Mapping mapping = getMapping(e.getKey());
                final JsonValue subValue = new JsonValue(e.getValue());
                mapping.mapper.toLDAP(c, subValue, handler);
            for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
                final Mapping mapping = getMapping(me.getKey());
                final JsonValue subValue = new JsonValue(me.getValue());
                mapping.mapper.toLDAP(c, path.child(me.getKey()), e, subValue, handler);
            }
        }
        // Invoke mappings for which there were no values provided.
        for (final Mapping mapping : missingMappings.values()) {
            mapping.mapper.toLDAP(c, null, handler);
            mapping.mapper.toLDAP(c, path.child(mapping.name), e, null, handler);
        }
    }
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
@@ -15,16 +15,15 @@
 */
package org.forgerock.opendj.rest2ldap;
import static java.util.Collections.singletonList;
import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
import static org.forgerock.opendj.rest2ldap.Utils.accumulate;
import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull;
import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.transform;
import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
@@ -39,6 +38,7 @@
import org.forgerock.json.resource.ResultHandler;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.AttributeDescription;
import org.forgerock.opendj.ldap.Attributes;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
@@ -47,11 +47,11 @@
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Function;
import org.forgerock.opendj.ldap.LinkedAttribute;
import org.forgerock.opendj.ldap.Modification;
import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
@@ -61,7 +61,8 @@
 * An attribute mapper which provides a mapping from a JSON value to a single DN
 * valued LDAP attribute.
 */
public final class ReferenceAttributeMapper extends AttributeMapper {
public final class ReferenceAttributeMapper extends
        AbstractLDAPAttributeMapper<ReferenceAttributeMapper> {
    /**
     * The maximum number of candidate references to allow in search filters.
     */
@@ -69,45 +70,19 @@
    private final DN baseDN;
    private Filter filter = null;
    private boolean isRequired = false;
    private boolean isSingleValued = false;
    private final AttributeDescription ldapAttributeName;
    private final AttributeMapper mapper;
    private final AttributeDescription primaryKey;
    private SearchScope scope = SearchScope.WHOLE_SUBTREE;
    private WritabilityPolicy writabilityPolicy = READ_WRITE;
    ReferenceAttributeMapper(final AttributeDescription ldapAttributeName, final DN baseDN,
            final AttributeDescription primaryKey, final AttributeMapper mapper) {
        this.ldapAttributeName = ldapAttributeName;
        super(ldapAttributeName);
        this.baseDN = baseDN;
        this.primaryKey = primaryKey;
        this.mapper = mapper;
    }
    /**
     * Indicates that the LDAP attribute is mandatory and must be provided
     * during create requests.
     *
     * @return This attribute mapper.
     */
    public ReferenceAttributeMapper isRequired() {
        this.isRequired = true;
        return this;
    }
    /**
     * Indicates that multi-valued LDAP attribute should be represented as a
     * single-valued JSON value, rather than an array of values.
     *
     * @return This attribute mapper.
     */
    public ReferenceAttributeMapper isSingleValued() {
        this.isSingleValued = true;
        return this;
    }
    /**
     * Sets the filter which should be used when searching for referenced LDAP
     * entries. The default is {@code (objectClass=*)}.
     *
@@ -148,30 +123,12 @@
        return this;
    }
    /**
     * Indicates whether or not the LDAP attribute supports updates. The default
     * is {@link WritabilityPolicy#READ_WRITE}.
     *
     * @param policy
     *            The writability policy.
     * @return This attribute mapper.
     */
    public ReferenceAttributeMapper writability(final WritabilityPolicy policy) {
        this.writabilityPolicy = policy;
        return this;
    }
    @Override
    void getLDAPAttributes(final Context c, final JsonPointer jsonAttribute,
            final Set<String> ldapAttributes) {
        ldapAttributes.add(ldapAttributeName.toString());
    }
    @Override
    void getLDAPFilter(final Context c, final FilterType type, final JsonPointer jsonAttribute,
            final String operator, final Object valueAssertion, final ResultHandler<Filter> h) {
    void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath,
            final FilterType type, final String operator, final Object valueAssertion,
            final ResultHandler<Filter> h) {
        // Construct a filter which can be used to find referenced resources.
        mapper.getLDAPFilter(c, type, jsonAttribute, operator, valueAssertion,
        mapper.getLDAPFilter(c, path, subPath, type, operator, valueAssertion,
                new ResultHandler<Filter>() {
                    @Override
                    public void handleError(final ResourceException error) {
@@ -224,86 +181,27 @@
    }
    @Override
    void toJSON(final Context c, final Entry e, final ResultHandler<JsonValue> h) {
        final Attribute attribute = e.getAttribute(ldapAttributeName);
        if (attribute == null || attribute.isEmpty()) {
            h.handleResult(null);
        } else if (attributeIsSingleValued()) {
            try {
                final DN dn = attribute.parse().usingSchema(c.getConfig().schema()).asDN();
                readEntry(c, dn, h);
            } catch (final Exception ex) {
                // The LDAP attribute could not be decoded.
                h.handleError(asResourceException(ex));
            }
        } else {
            try {
                final Set<DN> dns =
                        attribute.parse().usingSchema(c.getConfig().schema()).asSetOfDN();
                final ResultHandler<JsonValue> handler =
                        accumulate(dns.size(), transform(
                                new Function<List<JsonValue>, JsonValue, Void>() {
                                    @Override
                                    public JsonValue apply(final List<JsonValue> value, final Void p) {
                                        if (value.isEmpty()) {
                                            // No values, so omit the entire JSON object from the resource.
                                            return null;
                                        } else {
                                            // Combine values into a single JSON array.
                                            final List<Object> result =
                                                    new ArrayList<Object>(value.size());
                                            for (final JsonValue e : value) {
                                                result.add(e.getObject());
                                            }
                                            return new JsonValue(result);
                                        }
                                    }
                                }, h));
                for (final DN dn : dns) {
                    readEntry(c, dn, handler);
                }
            } catch (final Exception ex) {
                // The LDAP attribute could not be decoded.
                h.handleError(asResourceException(ex));
            }
    void getNewLDAPAttributes(final Context c, final JsonPointer path,
            final List<Object> newValues, final ResultHandler<Attribute> h) {
        // No need to do anything if there are no values.
        if (newValues.isEmpty()) {
            h.handleResult(Attributes.emptyAttribute(ldapAttributeName));
            return;
        }
    }
    @Override
    void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) {
        try {
            if (v == null || v.isNull()) {
                if (attributeIsRequired()) {
                    // FIXME: improve error message.
                    throw new BadRequestException("no value provided");
                } else {
                    h.handleResult(Collections.<Attribute> emptyList());
                }
            } else if (v.isList() && attributeIsSingleValued()) {
                // FIXME: improve error message.
                throw new BadRequestException("expected single value, but got multiple values");
            } else if (!writabilityPolicy.canCreate(ldapAttributeName)) {
                if (writabilityPolicy.discardWrites()) {
                    h.handleResult(Collections.<Attribute> emptyList());
                } else {
                    // FIXME: improve error message.
                    throw new BadRequestException("attempted to create a read-only value");
                }
            } else {
                /*
                 * For each value use the subordinate mapper to obtain the LDAP
                 * primary key, the perform a search for each one to find the
                 * corresponding entries.
                 */
                final JsonValue valueList =
                        v.isList() ? v : new JsonValue(singletonList(v.getObject()));
                final Attribute reference = new LinkedAttribute(ldapAttributeName);
                final AtomicInteger pendingSearches = new AtomicInteger(valueList.size());
                final AtomicReference<ResourceException> exception =
                        new AtomicReference<ResourceException>();
        /*
         * For each value use the subordinate mapper to obtain the LDAP primary
         * key, the perform a search for each one to find the corresponding
         * entries.
         */
        final Attribute newLDAPAttribute = new LinkedAttribute(ldapAttributeName);
        final AtomicInteger pendingSearches = new AtomicInteger(newValues.size());
        final AtomicReference<ResourceException> exception =
                new AtomicReference<ResourceException>();
                for (final JsonValue value : valueList) {
                    mapper.toLDAP(c, value, new ResultHandler<List<Attribute>>() {
        for (final Object value : newValues) {
            mapper.toLDAP(c, path, null /* force create */, new JsonValue(value),
                    new ResultHandler<List<Modification>>() {
                        @Override
                        public void handleError(final ResourceException error) {
@@ -311,32 +209,29 @@
                        }
                        @Override
                        public void handleResult(final List<Attribute> result) {
                        public void handleResult(final List<Modification> result) {
                            Attribute primaryKeyAttribute = null;
                            for (final Attribute attribute : result) {
                                if (attribute.getAttributeDescription().equals(primaryKey)) {
                                    primaryKeyAttribute = attribute;
                            for (final Modification modification : result) {
                                if (modification.getAttribute().getAttributeDescription().equals(
                                        primaryKey)) {
                                    primaryKeyAttribute = modification.getAttribute();
                                    break;
                                }
                            }
                            if (primaryKeyAttribute == null) {
                                // FIXME: improve error message.
                                h.handleError(new BadRequestException(
                                        "reference primary key attribute is missing"));
                                return;
                            }
                            if (primaryKeyAttribute.isEmpty()) {
                                // FIXME: improve error message.
                            if (primaryKeyAttribute == null || primaryKeyAttribute.isEmpty()) {
                                h.handleError(new BadRequestException(
                                        "reference primary key attribute is empty"));
                                        i18n("The request cannot be processed because the reference "
                                                + "field '%s' contains a value which does not contain "
                                                + "a primary key", path)));
                                return;
                            }
                            if (primaryKeyAttribute.size() > 1) {
                                // FIXME: improve error message.
                                h.handleError(new BadRequestException(
                                        "reference primary key attribute contains multiple values"));
                                        i18n("The request cannot be processed because the reference "
                                                + "field '%s' contains a value which contains multiple "
                                                + "primary keys", path)));
                                return;
                            }
@@ -356,7 +251,7 @@
                                                    ResourceException re;
                                                    try {
                                                        throw error;
                                                    } catch (EntryNotFoundException e) {
                                                    } catch (final EntryNotFoundException e) {
                                                        // FIXME: improve error message.
                                                        re =
                                                                new BadRequestException(
@@ -364,7 +259,7 @@
                                                                                + primaryKeyValue
                                                                                        .toString()
                                                                                + "' does not exist");
                                                    } catch (MultipleEntriesFoundException e) {
                                                    } catch (final MultipleEntriesFoundException e) {
                                                        // FIXME: improve error message.
                                                        re =
                                                                new BadRequestException(
@@ -372,7 +267,7 @@
                                                                                + primaryKeyValue
                                                                                        .toString()
                                                                                + "' is ambiguous");
                                                    } catch (ErrorResultException e) {
                                                    } catch (final ErrorResultException e) {
                                                        re = asResourceException(e);
                                                    }
                                                    exception.compareAndSet(null, re);
@@ -382,8 +277,8 @@
                                                @Override
                                                public void handleResult(
                                                        final SearchResultEntry result) {
                                                    synchronized (reference) {
                                                        reference.add(result.getName());
                                                    synchronized (newLDAPAttribute) {
                                                        newLDAPAttribute.add(result.getName());
                                                    }
                                                    completeIfNecessary();
                                                }
@@ -393,40 +288,80 @@
                        private void completeIfNecessary() {
                            if (pendingSearches.decrementAndGet() == 0) {
                                if (exception.get() == null) {
                                    h.handleResult(singletonList(reference));
                                    h.handleResult(newLDAPAttribute);
                                } else {
                                    h.handleError(exception.get());
                                }
                            }
                        }
                    });
                }
            }
        } catch (final ResourceException e) {
            h.handleError(e);
        } catch (final Exception e) {
            // FIXME: improve error message.
            h.handleError(new BadRequestException(e.getMessage()));
        }
    }
    private boolean attributeIsRequired() {
        return isRequired;
    @Override
    ReferenceAttributeMapper getThis() {
        return this;
    }
    private boolean attributeIsSingleValued() {
        return isSingleValued || ldapAttributeName.getAttributeType().isSingleValue();
    @Override
    void toJSON(final Context c, final JsonPointer path, final Entry e,
            final ResultHandler<JsonValue> h) {
        final Attribute attribute = e.getAttribute(ldapAttributeName);
        if (attribute == null || attribute.isEmpty()) {
            h.handleResult(null);
        } else if (attributeIsSingleValued()) {
            try {
                final DN dn = attribute.parse().usingSchema(c.getConfig().schema()).asDN();
                readEntry(c, path, dn, h);
            } catch (final Exception ex) {
                // The LDAP attribute could not be decoded.
                h.handleError(asResourceException(ex));
            }
        } else {
            try {
                final Set<DN> dns =
                        attribute.parse().usingSchema(c.getConfig().schema()).asSetOfDN();
                final ResultHandler<JsonValue> handler =
                        accumulate(dns.size(), transform(
                                new Function<List<JsonValue>, JsonValue, Void>() {
                                    @Override
                                    public JsonValue apply(final List<JsonValue> value, final Void p) {
                                        if (value.isEmpty()) {
                                            /*
                                             * No values, so omit the entire
                                             * JSON object from the resource.
                                             */
                                            return null;
                                        } else {
                                            // Combine values into a single JSON array.
                                            final List<Object> result =
                                                    new ArrayList<Object>(value.size());
                                            for (final JsonValue e : value) {
                                                result.add(e.getObject());
                                            }
                                            return new JsonValue(result);
                                        }
                                    }
                                }, h));
                for (final DN dn : dns) {
                    readEntry(c, path, dn, handler);
                }
            } catch (final Exception ex) {
                // The LDAP attribute could not be decoded.
                h.handleError(asResourceException(ex));
            }
        }
    }
    private SearchRequest createSearchRequest(final Filter result) {
        final Filter searchFilter = filter != null ? Filter.and(filter, result) : result;
        final SearchRequest request = Requests.newSearchRequest(baseDN, scope, searchFilter, "1.1");
        return request;
        return newSearchRequest(baseDN, scope, searchFilter, "1.1");
    }
    private void readEntry(final Context c, final DN dn, final ResultHandler<JsonValue> handler) {
    private void readEntry(final Context c, final JsonPointer path, final DN dn,
            final ResultHandler<JsonValue> handler) {
        final Set<String> requestedLDAPAttributes = new LinkedHashSet<String>();
        mapper.getLDAPAttributes(c, new JsonPointer(), requestedLDAPAttributes);
        mapper.getLDAPAttributes(c, path, new JsonPointer(), requestedLDAPAttributes);
        c.getConnection().readEntryAsync(dn, requestedLDAPAttributes,
                new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() {
@@ -435,14 +370,17 @@
                        if (!(error instanceof EntryNotFoundException)) {
                            handler.handleError(asResourceException(error));
                        } else {
                            // The referenced entry does not exist so ignore it since it cannot be mapped.
                            /*
                             * The referenced entry does not exist so ignore it
                             * since it cannot be mapped.
                             */
                            handler.handleResult(null);
                        }
                    }
                    @Override
                    public void handleResult(final SearchResultEntry result) {
                        mapper.toJSON(c, result, handler);
                        mapper.toJSON(c, path, result, handler);
                    }
                });
    }
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
@@ -44,6 +44,7 @@
import org.forgerock.opendj.ldap.ConnectionException;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.Connections;
import org.forgerock.opendj.ldap.ConstraintViolationException;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.EntryNotFoundException;
@@ -86,6 +87,7 @@
        private AttributeMapper rootMapper;
        private Schema schema = Schema.getDefaultSchema();
        private boolean useSubtreeDelete;
        private boolean usePermissiveModify;
        Builder() {
            useEtagAttribute();
@@ -144,7 +146,7 @@
            }
            return new LDAPCollectionResourceProvider(baseDN, rootMapper, nameStrategy,
                    etagAttribute, new Config(factory, readOnUpdatePolicy, authzPolicy,
                            proxiedAuthzTemplate, useSubtreeDelete, schema),
                            proxiedAuthzTemplate, useSubtreeDelete, usePermissiveModify, schema),
                    additionalLDAPAttributes);
        }
@@ -196,10 +198,14 @@
                useEtagAttribute(etagAttribute.asString());
            }
            if (configuration.get("useSubtreeDelete").required().asBoolean()) {
            if (configuration.get("useSubtreeDelete").defaultTo(false).asBoolean()) {
                useSubtreeDelete();
            }
            if (configuration.get("usePermissiveModify").defaultTo(false).asBoolean()) {
                usePermissiveModify();
            }
            mapper(configureObjectMapper(configuration.get("attributes").required()));
            return this;
@@ -305,6 +311,11 @@
            return this;
        }
        public Builder usePermissiveModify() {
            this.usePermissiveModify = true;
            return this;
        }
        private AttributeDescription ad(final String attribute) {
            return AttributeDescription.valueOf(attribute, schema);
        }
@@ -323,9 +334,6 @@
                if (config.isDefined("defaultJSONValue")) {
                    s.defaultJSONValue(config.get("defaultJSONValue").getObject());
                }
                if (config.isDefined("defaultLDAPValue")) {
                    s.defaultLDAPValue(config.get("defaultLDAPValue").getObject());
                }
                if (config.get("isBinary").defaultTo(false).asBoolean()) {
                    s.isBinary();
                }
@@ -507,6 +515,14 @@
            return e;
        } catch (final AssertionFailureException e) {
            resourceResultCode = ResourceException.VERSION_MISMATCH;
        } catch (final ConstraintViolationException e) {
            final ResultCode rc = e.getResult().getResultCode();
            if (rc.equals(ResultCode.ENTRY_ALREADY_EXISTS)) {
                resourceResultCode = ResourceException.VERSION_MISMATCH; // Consistent with MVCC.
            } else {
                // Schema violation, etc.
                resourceResultCode = ResourceException.BAD_REQUEST;
            }
        } catch (final AuthenticationException e) {
            resourceResultCode = 401;
        } catch (final AuthorizationException e) {
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
@@ -16,8 +16,6 @@
package org.forgerock.opendj.rest2ldap;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
import static org.forgerock.opendj.ldap.Functions.fixedFunction;
@@ -25,20 +23,17 @@
import static org.forgerock.opendj.rest2ldap.Utils.base64ToByteString;
import static org.forgerock.opendj.rest2ldap.Utils.byteStringToBase64;
import static org.forgerock.opendj.rest2ldap.Utils.byteStringToJson;
import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.jsonToAttribute;
import static org.forgerock.opendj.rest2ldap.Utils.jsonToByteString;
import static org.forgerock.opendj.rest2ldap.Utils.toFilter;
import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResultHandler;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.AttributeDescription;
@@ -46,25 +41,18 @@
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Function;
import org.forgerock.opendj.ldap.LinkedAttribute;
/**
 * An attribute mapper which provides a simple mapping from a JSON value to a
 * single LDAP attribute.
 */
public final class SimpleAttributeMapper extends AttributeMapper {
public final class SimpleAttributeMapper extends AbstractLDAPAttributeMapper<SimpleAttributeMapper> {
    private Function<ByteString, ?, Void> decoder = null;
    private Object defaultJSONValue = null;
    private Collection<Object> defaultJSONValues = Collections.emptySet();
    private ByteString defaultLDAPValue = null;
    private Function<Object, ByteString, Void> encoder = null;
    private boolean isRequired = false;
    private boolean isSingleValued = false;
    private final AttributeDescription ldapAttributeName;
    private WritabilityPolicy writabilityPolicy = READ_WRITE;
    SimpleAttributeMapper(final AttributeDescription ldapAttributeName) {
        this.ldapAttributeName = ldapAttributeName;
        super(ldapAttributeName);
    }
    /**
@@ -90,20 +78,7 @@
     */
    public SimpleAttributeMapper defaultJSONValue(final Object defaultValue) {
        this.defaultJSONValue = defaultValue;
        this.defaultJSONValues = defaultValue != null ? singleton(defaultValue) : emptySet();
        return this;
    }
    /**
     * Sets the default LDAP value which should be substituted when the JSON
     * attribute is not found in the JSON value.
     *
     * @param defaultValue
     *            The default LDAP value.
     * @return This attribute mapper.
     */
    public SimpleAttributeMapper defaultLDAPValue(final Object defaultValue) {
        this.defaultLDAPValue = defaultValue != null ? ByteString.valueOf(defaultValue) : null;
        this.defaultJSONValues = defaultValue != null ? singletonList(defaultValue) : emptyList();
        return this;
    }
@@ -137,61 +112,21 @@
        return this;
    }
    /**
     * Indicates that the LDAP attribute is mandatory and must be provided
     * during create requests.
     *
     * @return This attribute mapper.
     */
    public SimpleAttributeMapper isRequired() {
        this.isRequired = true;
        return this;
    }
    /**
     * Indicates that multi-valued LDAP attribute should be represented as a
     * single-valued JSON value, rather than an array of values.
     *
     * @return This attribute mapper.
     */
    public SimpleAttributeMapper isSingleValued() {
        this.isSingleValued = true;
        return this;
    }
    /**
     * Indicates whether or not the LDAP attribute supports updates. The default
     * is {@link WritabilityPolicy#READ_WRITE}.
     *
     * @param policy
     *            The writability policy.
     * @return This attribute mapper.
     */
    public SimpleAttributeMapper writability(final WritabilityPolicy policy) {
        this.writabilityPolicy = policy;
        return this;
    }
    @Override
    void getLDAPAttributes(final Context c, final JsonPointer jsonAttribute,
            final Set<String> ldapAttributes) {
        ldapAttributes.add(ldapAttributeName.toString());
    }
    @Override
    void getLDAPFilter(final Context c, final FilterType type, final JsonPointer jsonAttribute,
            final String operator, final Object valueAssertion, final ResultHandler<Filter> h) {
        if (jsonAttribute.isEmpty()) {
    void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath,
            final FilterType type, final String operator, final Object valueAssertion,
            final ResultHandler<Filter> h) {
        if (subPath.isEmpty()) {
            try {
                final ByteString va =
                        valueAssertion != null ? encoder().apply(valueAssertion, null) : null;
                h.handleResult(toFilter(c, type, ldapAttributeName.toString(), va));
            } catch (Exception e) {
            } catch (final Exception e) {
                // Invalid assertion value - bad request.
                // FIXME: improve error message.
                h.handleError(new BadRequestException("Invalid filter assertion value '"
                        + String.valueOf(valueAssertion) + "'", e));
                h.handleError(new BadRequestException(i18n(
                        "The request cannot be processed because it contained an "
                                + "illegal filter assertion value '%s' for field '%s'", String
                                .valueOf(valueAssertion), path), e));
            }
        } else {
            // This attribute mapper does not support partial filtering.
@@ -200,7 +135,25 @@
    }
    @Override
    void toJSON(final Context c, final Entry e, final ResultHandler<JsonValue> h) {
    void getNewLDAPAttributes(final Context c, final JsonPointer path,
            final List<Object> newValues, final ResultHandler<Attribute> h) {
        try {
            h.handleResult(jsonToAttribute(newValues, ldapAttributeName, encoder()));
        } catch (final Exception ex) {
            h.handleError(new BadRequestException(i18n(
                    "The request cannot be processed because an error occurred while "
                            + "encoding the values for the field '%s': %s", path, ex.getMessage())));
        }
    }
    @Override
    SimpleAttributeMapper getThis() {
        return this;
    }
    @Override
    void toJSON(final Context c, final JsonPointer path, final Entry e,
            final ResultHandler<JsonValue> h) {
        try {
            final Object value;
            if (attributeIsSingleValued()) {
@@ -211,66 +164,12 @@
                value = s.isEmpty() ? null : s;
            }
            h.handleResult(value != null ? new JsonValue(value) : null);
        } catch (Exception ex) {
        } catch (final Exception ex) {
            // The LDAP attribute could not be decoded.
            h.handleError(asResourceException(ex));
        }
    }
    @Override
    void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) {
        try {
            final List<Attribute> result;
            if (v == null || v.isNull()) {
                if (attributeIsRequired()) {
                    // FIXME: improve error message.
                    throw new BadRequestException("no value provided");
                } else if (defaultLDAPValue != null) {
                    result =
                            singletonList((Attribute) new LinkedAttribute(ldapAttributeName,
                                    defaultLDAPValue));
                } else {
                    result = emptyList();
                }
            } else if (v.isList() && attributeIsSingleValued()) {
                // FIXME: improve error message.
                throw new BadRequestException("expected single value, but got multiple values");
            } else if (!writabilityPolicy.canCreate(ldapAttributeName)) {
                if (writabilityPolicy.discardWrites()) {
                    result = emptyList();
                } else {
                    // FIXME: improve error message.
                    throw new BadRequestException("attempted to create a read-only value");
                }
            } else {
                final Object value = v.getObject();
                if (value != null) {
                    result = singletonList(jsonToAttribute(value, ldapAttributeName, encoder()));
                } else if (defaultLDAPValue != null) {
                    result =
                            singletonList((Attribute) new LinkedAttribute(ldapAttributeName,
                                    defaultLDAPValue));
                } else {
                    result = emptyList();
                }
            }
            h.handleResult(result);
        } catch (final ResourceException e) {
            h.handleError(e);
        } catch (final Exception e) {
            // FIXME: improve error message.
            h.handleError(new BadRequestException(e.getMessage()));
        }
    }
    private boolean attributeIsRequired() {
        return isRequired && defaultJSONValue == null;
    }
    private boolean attributeIsSingleValued() {
        return isSingleValued || ldapAttributeName.getAttributeType().isSingleValue();
    }
    private Function<ByteString, ? extends Object, Void> decoder() {
        return decoder == null ? fixedFunction(byteStringToJson(), ldapAttributeName) : decoder;
    }
opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
@@ -19,20 +19,27 @@
import static org.fest.assertions.Fail.fail;
import static org.forgerock.json.resource.Requests.newDeleteRequest;
import static org.forgerock.json.resource.Requests.newReadRequest;
import static org.forgerock.json.resource.Requests.newUpdateRequest;
import static org.forgerock.json.resource.Resources.newCollection;
import static org.forgerock.json.resource.Resources.newInternalConnection;
import static org.forgerock.opendj.ldap.Connections.newInternalConnectionFactory;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.object;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.simple;
import static org.forgerock.opendj.rest2ldap.TestUtils.asResource;
import static org.forgerock.opendj.rest2ldap.TestUtils.content;
import static org.forgerock.opendj.rest2ldap.TestUtils.ctx;
import static org.forgerock.opendj.rest2ldap.TestUtils.field;
import static org.forgerock.opendj.rest2ldap.TestUtils.object;
import java.io.IOException;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.Connection;
import org.forgerock.json.resource.NotFoundException;
import org.forgerock.json.resource.PreconditionFailedException;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.Resource;
import org.forgerock.json.resource.RootContext;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.MemoryBackend;
import org.forgerock.opendj.ldif.LDIFEntryReader;
@@ -54,10 +61,10 @@
    public void testDelete() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        final Resource resource = connection.delete(c(), newDeleteRequest("/test1"));
        checkTestUser1(resource);
        final Resource resource = connection.delete(ctx(), newDeleteRequest("/test1"));
        checkResourcesAreEqual(resource, getTestUser1(12345));
        try {
            connection.read(c(), newReadRequest("/test1"));
            connection.read(ctx(), newReadRequest("/test1"));
            fail("Read succeeded unexpectedly");
        } catch (final NotFoundException e) {
            // Expected.
@@ -69,10 +76,10 @@
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        final Resource resource =
                connection.delete(c(), newDeleteRequest("/test1").setRevision("12345"));
        checkTestUser1(resource);
                connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12345"));
        checkResourcesAreEqual(resource, getTestUser1(12345));
        try {
            connection.read(c(), newReadRequest("/test1"));
            connection.read(ctx(), newReadRequest("/test1"));
            fail("Read succeeded unexpectedly");
        } catch (final NotFoundException e) {
            // Expected.
@@ -83,50 +90,50 @@
    public void testDeleteMVCCNoMatch() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        connection.delete(c(), newDeleteRequest("/test1").setRevision("12346"));
        connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12346"));
    }
    @Test(expectedExceptions = NotFoundException.class)
    public void testDeleteNotFound() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        connection.delete(c(), newDeleteRequest("/missing"));
        connection.delete(ctx(), newDeleteRequest("/missing"));
    }
    @Test
    public void testRead() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Resource resource =
                newInternalConnection(handler).read(c(), newReadRequest("/test1"));
        checkTestUser1(resource);
                newInternalConnection(handler).read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource, getTestUser1(12345));
    }
    @Test(expectedExceptions = NotFoundException.class)
    public void testReadNotFound() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        newInternalConnection(handler).read(c(), newReadRequest("/missing"));
        newInternalConnection(handler).read(ctx(), newReadRequest("/missing"));
    }
    @Test
    public void testReadSelectAllFields() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Resource resource =
                newInternalConnection(handler).read(c(), newReadRequest("/test1").addField("/"));
        checkTestUser1(resource);
                newInternalConnection(handler).read(ctx(), newReadRequest("/test1").addField("/"));
        checkResourcesAreEqual(resource, getTestUser1(12345));
    }
    @Test
    public void testReadSelectPartial() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Resource resource =
                newInternalConnection(handler).read(c(),
                newInternalConnection(handler).read(ctx(),
                        newReadRequest("/test1").addField("surname"));
        assertThat(resource.getId()).isEqualTo("test1");
        assertThat(resource.getRevision()).isEqualTo("12345");
        assertThat(resource.getContent().get("id").asString()).isNull();
        assertThat(resource.getContent().get("_id").asString()).isNull();
        assertThat(resource.getContent().get("displayName").asString()).isNull();
        assertThat(resource.getContent().get("surname").asString()).isEqualTo("user 1");
        assertThat(resource.getContent().get("rev").asString()).isNull();
        assertThat(resource.getContent().get("_rev").asString()).isNull();
    }
    // Disabled - see CREST-86 (Should JSON resource fields be case insensitive?)
@@ -134,14 +141,60 @@
    public void testReadSelectPartialInsensitive() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Resource resource =
                newInternalConnection(handler).read(c(),
                newInternalConnection(handler).read(ctx(),
                        newReadRequest("/test1").addField("SURNAME"));
        assertThat(resource.getId()).isEqualTo("test1");
        assertThat(resource.getRevision()).isEqualTo("12345");
        assertThat(resource.getContent().get("id").asString()).isNull();
        assertThat(resource.getContent().get("_id").asString()).isNull();
        assertThat(resource.getContent().get("displayName").asString()).isNull();
        assertThat(resource.getContent().get("surname").asString()).isEqualTo("user 1");
        assertThat(resource.getContent().get("rev").asString()).isNull();
        assertThat(resource.getContent().get("_rev").asString()).isNull();
    }
    @Test
    public void testUpdate() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        final Resource resource1 =
                connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)));
        checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
    }
    @Test
    public void testUpdateMVCCMatch() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        final Resource resource1 =
                connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345))
                        .setRevision("12345"));
        checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
    }
    @Test(expectedExceptions = PreconditionFailedException.class)
    public void testUpdateMVCCNoMatch() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345))
                .setRevision("12346"));
    }
    @Test(expectedExceptions = NotFoundException.class)
    public void testUpdateNotFound() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        connection.update(ctx(), newUpdateRequest("/missing", getTestUser1Updated(12345)));
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testUpdateReadOnlyAttribute() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        // Etag is read-only.
        connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(99999)));
    }
    private Builder builder() throws IOException {
@@ -149,24 +202,23 @@
                .useEtagAttribute().useClientDNNaming("uid").readOnUpdatePolicy(
                        ReadOnUpdatePolicy.CONTROLS).authorizationPolicy(AuthorizationPolicy.NONE)
                .additionalLDAPAttribute("objectClass", "top", "person").mapper(
                        object().attribute("id", simple("uid").isSingleValued().isRequired())
                                .attribute("displayName",
                                        simple("cn").isSingleValued().isRequired()).attribute(
                                        "surname", simple("sn").isSingleValued().isRequired())
                                .attribute("rev", simple("etag").isSingleValued().isRequired()));
                        object().attribute(
                                "_id",
                                simple("uid").isSingleValued().isRequired().writability(
                                        WritabilityPolicy.CREATE_ONLY)).attribute("displayName",
                                simple("cn").isSingleValued().isRequired()).attribute("surname",
                                simple("sn").isSingleValued().isRequired()).attribute(
                                "_rev",
                                simple("etag").isSingleValued().isRequired().writability(
                                        WritabilityPolicy.READ_ONLY)));
    }
    private RootContext c() {
        return new RootContext();
    }
    private void checkTestUser1(final Resource resource) {
        assertThat(resource.getId()).isEqualTo("test1");
        assertThat(resource.getRevision()).isEqualTo("12345");
        assertThat(resource.getContent().get("id").asString()).isEqualTo("test1");
        assertThat(resource.getContent().get("displayName").asString()).isEqualTo("test user 1");
        assertThat(resource.getContent().get("surname").asString()).isEqualTo("user 1");
        assertThat(resource.getContent().get("rev").asString()).isEqualTo("12345");
    private void checkResourcesAreEqual(final Resource actual, final JsonValue expected) {
        final Resource expectedResource = asResource(expected);
        assertThat(actual.getId()).isEqualTo(expectedResource.getId());
        assertThat(actual.getRevision()).isEqualTo(expectedResource.getRevision());
        assertThat(actual.getContent().getObject()).isEqualTo(
                expectedResource.getContent().getObject());
    }
    private ConnectionFactory getConnectionFactory() throws IOException {
@@ -200,4 +252,14 @@
        return newInternalConnectionFactory(backend);
    }
    private JsonValue getTestUser1(final int rev) {
        return content(object(field("_id", "test1"), field("_rev", String.valueOf(rev)), field(
                "displayName", "test user 1"), field("surname", "user 1")));
    }
    private JsonValue getTestUser1Updated(final int rev) {
        return content(object(field("_id", "test1"), field("_rev", String.valueOf(rev)), field(
                "displayName", "changed"), field("surname", "user 1")));
    }
}
opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java
New file
@@ -0,0 +1,143 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2013 ForgeRock Inc.
 */
package org.forgerock.opendj.rest2ldap;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.Resource;
import org.forgerock.json.resource.RootContext;
/**
 * Unit test utility methods, including fluent methods for creating JSON
 * objects.
 */
public final class TestUtils {
    /**
     * Creates a JSON array object.
     *
     * @param objects
     *            The array elements.
     * @return A JSON array.
     */
    public static Object array(final Object... objects) {
        return Arrays.asList(objects);
    }
    /**
     * Returns a {@code Resource} containing the provided JSON content. The ID
     * and revision will be taken from the "_id" and "_rev" fields respectively.
     *
     * @param content
     *            The JSON content.
     * @return A {@code Resource} containing the provided JSON content.
     */
    public static Resource asResource(final JsonValue content) {
        return new Resource(content.get("_id").asString(), content.get("_rev").asString(), content);
    }
    /**
     * Creates a JSON value for the provided object.
     *
     * @param object
     *            The object.
     * @return The JSON value.
     */
    public static JsonValue content(final Object object) {
        return new JsonValue(object);
    }
    /**
     * Creates a root context to be passed in with client requests.
     *
     * @return The root context.
     */
    public static RootContext ctx() {
        return new RootContext();
    }
    /**
     * Creates a JSON value for the provided object. This is the same as
     * {@link #content(Object)} but can yield more readable test data in data
     * providers.
     *
     * @param object
     *            The object.
     * @return The JSON value.
     */
    public static JsonValue expected(final Object object) {
        return content(object);
    }
    /**
     * Creates a JSON field for inclusion in a JSON object using
     * {@link #object(java.util.Map.Entry...)}.
     *
     * @param key
     *            The JSON field name.
     * @param value
     *            The JSON field value.
     * @return The JSON field for inclusion in a JSON object.
     */
    public static Map.Entry<String, Object> field(final String key, final Object value) {
        return new AbstractMap.SimpleImmutableEntry<String, Object>(key, value);
    }
    /**
     * Creates a list of JSON pointers from the provided string representations.
     *
     * @param fields
     *            The list of JSON pointer strings.
     * @return The list of parsed JSON pointers.
     */
    public static List<JsonPointer> filter(final String... fields) {
        final List<JsonPointer> result = new ArrayList<JsonPointer>(fields.length);
        for (final String field : fields) {
            result.add(new JsonPointer(field));
        }
        return result;
    }
    /**
     * Creates a JSON object comprised of the provided JSON
     * {@link #field(String, Object) fields}.
     *
     * @param fields
     *            The list of {@link #field(String, Object) fields} to include
     *            in the JSON object.
     * @return The JSON object.
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static Object object(final Map.Entry... fields) {
        final Map<String, Object> object = new LinkedHashMap<String, Object>(fields.length);
        for (final Map.Entry<String, Object> field : fields) {
            object.put(field.getKey(), field.getValue());
        }
        return object;
    }
    private TestUtils() {
        // Prevent instantiation.
    }
}