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

Matthew Swift
01.12.2013 4ca1423e387874accc55c1d0ffcada3eddb833c5
Partial fix for CREST-3: Add patch support

* initial commit of Rest2LDAP support
* includes basic unit tests (more to come)
* need to re-align memory backend implementation and check against patch operation spec in Javadoc.
8 files modified
989 ■■■■ changed files
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java 303 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java 90 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java 35 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java 186 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java 191 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java 58 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java 3 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java 123 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
@@ -18,8 +18,10 @@
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.Rest2LDAP.asResourceException;
import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.isNullOrEmpty;
import static org.forgerock.opendj.rest2ldap.Utils.transform;
import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE;
import java.util.ArrayList;
@@ -30,12 +32,14 @@
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.NotSupportedException;
import org.forgerock.json.resource.PatchOperation;
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.Attributes;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Function;
import org.forgerock.opendj.ldap.LinkedAttribute;
import org.forgerock.opendj.ldap.Modification;
import org.forgerock.opendj.ldap.ModificationType;
@@ -96,6 +100,12 @@
    }
    @Override
    void create(final Context c, final JsonPointer path, final JsonValue v,
            final ResultHandler<List<Attribute>> h) {
        getNewLDAPAttributes(c, path, v, createAttributeHandler(path, h));
    }
    @Override
    void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath,
            final Set<String> ldapAttributes) {
        ldapAttributes.add(ldapAttributeName.toString());
@@ -107,28 +117,244 @@
    abstract T getThis();
    @Override
    void toLDAP(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
    void patch(final Context c, final JsonPointer path, final PatchOperation operation,
            final ResultHandler<List<Modification>> h) {
        // Make following code readable.
        final boolean isUpdateRequest = e != null;
        final boolean isCreateRequest = !isUpdateRequest;
        try {
            final JsonPointer field = operation.getField();
            final JsonValue v = operation.getValue();
        // 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);
            /*
             * Reject any attempts to patch this field if it is read-only, even
             * if it is configured to discard writes.
             */
            if (!writabilityPolicy.canWrite(ldapAttributeName)) {
                throw new BadRequestException(i18n(
                        "The request cannot be processed because it attempts to modify "
                                + "the read-only field '%s'", path));
        }
        if (v != null && v.isList() && attributeIsSingleValued()) {
            switch (field.size()) {
            case 0:
                /*
                 * The patch operation targets the entire mapping. If this
                 * mapping is multi-valued, then the patch value must be a list
                 * of values to be added, removed, or replaced. If it is
                 * single-valued then the patch value must not be a list.
                 */
                if (attributeIsSingleValued()) {
                    if (v != null && v.isList()) {
            // Single-valued field violation.
            h.handleError(new BadRequestException(i18n(
                        throw new BadRequestException(i18n(
                    "The request cannot be processed because an array of values was "
                            + "provided for the single valued field '%s'", path)));
                                        + "provided for the single valued field '%s'", path));
                    }
                } else if (v != null && !v.isList() && !operation.isIncrement()
                        && !(v.isNull() && (operation.isReplace() || operation.isRemove()))) {
                    // Multi-valued field violation.
                    throw new BadRequestException(i18n(
                            "The request cannot be processed because an array of values was "
                                    + "not provided for the multi-valued field '%s'", path));
                }
                break;
            case 1:
                /*
                 * The patch operation targets a sub-field. If the sub-field
                 * name is a number then it is an attempt to patch a single
                 * value at a specific index. Rest2LDAP cannot support indexed
                 * updates because LDAP attribute values are unordered. We will,
                 * however, support the special index "-" indicating that a
                 * value should be appended.
                 */
                final String fieldName = field.get(0);
                if (fieldName.equals("-") && operation.isAdd()) {
                    // Append a single value.
                    if (attributeIsSingleValued()) {
                        throw new BadRequestException(i18n(
                                "The request cannot be processed because it attempts to append a "
                                        + "value to the single valued field '%s'", path));
                    } else if (v.isList()) {
                        throw new BadRequestException(i18n(
                                "The request cannot be processed because it attempts to "
                                        + "perform an indexed append of an array of values to "
                                        + "the multi-valued field '%s'", path.child(fieldName)));
                    }
                } else if (fieldName.matches("[0-9]+")) {
                    // Array index - not allowed.
                    throw new NotSupportedException(i18n(
                            "The request cannot be processed because it included "
                                    + "an indexed patch operation '%s' which is not supported "
                                    + "by this resource provider", path.child(fieldName)));
        } else {
            final ResultHandler<Attribute> attributeHandler = new ResultHandler<Attribute>() {
                    throw new BadRequestException(i18n(
                            "The request cannot be processed because it included "
                                    + "an unrecognized field '%s'", path.child(fieldName)));
                }
                break;
            default:
                /*
                 * The patch operation targets the child of a sub-field. This is
                 * not possible for a LDAP attribute mapper.
                 */
                throw new BadRequestException(i18n(
                        "The request cannot be processed because it included "
                                + "an unrecognized field '%s'", path.child(field.get(0))));
            }
            // Check that the values are compatible with the type of patch operation.
            final List<Object> newValues = asList(v, Collections.emptyList());
            final ModificationType modType;
            if (operation.isAdd()) {
                /*
                 * Use a replace for single valued fields in case the underlying
                 * LDAP attribute is multi-valued, or the attribute already
                 * contains a value.
                 */
                modType =
                        attributeIsSingleValued() ? ModificationType.REPLACE : ModificationType.ADD;
                if (newValues.isEmpty()) {
                    throw new BadRequestException(i18n(
                            "The request cannot be processed because it included "
                                    + "an add patch operation but no value(s) for field '%s'", path
                                    .child(field.get(0))));
                }
            } else if (operation.isRemove()) {
                modType = ModificationType.DELETE;
            } else if (operation.isReplace()) {
                modType = ModificationType.REPLACE;
            } else if (operation.isIncrement()) {
                modType = ModificationType.INCREMENT;
                if (newValues.isEmpty()) {
                    throw new BadRequestException(i18n(
                            "The request cannot be processed because it included "
                                    + "an increment patch operation but no value for field '%s'",
                            path.child(field.get(0))));
                } else if (newValues.size() > 1) {
                    throw new BadRequestException(
                            i18n("The request cannot be processed because it included "
                                    + "an increment patch operation with multiple values for field '%s'",
                                    path.child(field.get(0))));
                }
            } else {
                throw new NotSupportedException(i18n(
                        "The request cannot be processed because it included "
                                + "an unsupported type of patch operation '%s'", operation
                                .getOperation()));
            }
            // Create the modification.
            if (newValues.isEmpty()) {
                // Deleting the attribute.
                if (isRequired) {
                    h.handleError(new BadRequestException(i18n(
                            "The request cannot be processed because it attempts to remove "
                                    + "the required field '%s'", path)));
                } else {
                    h.handleResult(singletonList(new Modification(modType,
                            emptyAttribute(ldapAttributeName))));
                }
            } else {
                getNewLDAPAttributes(c, path, newValues, transform(
                        new Function<Attribute, List<Modification>, Void>() {
                            @Override
                            public List<Modification> apply(final Attribute value, final Void p) {
                                return singletonList(new Modification(modType, value));
                            }
                        }, h));
            }
        } catch (final Exception e) {
            h.handleError(asResourceException(e));
        }
    }
    @Override
    void update(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
            final ResultHandler<List<Modification>> h) {
        getNewLDAPAttributes(c, path, v, updateAttributeHandler(path, e, h));
    }
    private List<Object> asList(final JsonValue v, final List<Object> defaultValues) {
        if (isNullOrEmpty(v)) {
            return defaultValues;
        } else if (v.isList()) {
            return v.asList();
        } else {
            return singletonList(v.getObject());
        }
    }
    private void checkSchema(final JsonPointer path, final JsonValue v) throws BadRequestException {
        if (attributeIsSingleValued()) {
            if (v != null && v.isList()) {
                // Single-valued field violation.
                throw new BadRequestException(i18n(
                        "The request cannot be processed because an array of values was "
                                + "provided for the single valued field '%s'", path));
            }
        } else if (v != null && !v.isList()) {
            // Multi-valued field violation.
            throw new BadRequestException(i18n(
                    "The request cannot be processed because an array of values was "
                            + "not provided for the multi-valued field '%s'", path));
        }
    }
    private ResultHandler<Attribute> createAttributeHandler(final JsonPointer path,
            final ResultHandler<List<Attribute>> h) {
        return new ResultHandler<Attribute>() {
            @Override
            public void handleError(final ResourceException error) {
                h.handleError(error);
            }
            @Override
            public void handleResult(final Attribute newLDAPAttribute) {
                if (!writabilityPolicy.canCreate(ldapAttributeName)) {
                    if (newLDAPAttribute.isEmpty() || writabilityPolicy.discardWrites()) {
                        h.handleResult(Collections.<Attribute> emptyList());
                    } else {
                        h.handleError(new BadRequestException(i18n(
                                "The request cannot be processed because it attempts to create "
                                        + "the read-only field '%s'", path)));
                    }
                } else if (newLDAPAttribute.isEmpty()) {
                    if (isRequired) {
                        h.handleError(new BadRequestException(i18n(
                                "The request cannot be processed because it attempts to remove "
                                        + "the required field '%s'", path)));
                        return;
                    } else {
                        h.handleResult(Collections.<Attribute> emptyList());
                    }
                } else {
                    h.handleResult(singletonList(newLDAPAttribute));
                }
            }
        };
    }
    private void getNewLDAPAttributes(final Context c, final JsonPointer path, final JsonValue v,
            final ResultHandler<Attribute> attributeHandler) {
        try {
            // Ensure that the value is of the correct type.
            checkSchema(path, v);
            final List<Object> newValues = asList(v, defaultJSONValues);
            if (newValues.isEmpty()) {
                // Skip sub-class implementation if there are no values.
                attributeHandler.handleResult(emptyAttribute(ldapAttributeName));
            } else {
                getNewLDAPAttributes(c, path, newValues, attributeHandler);
            }
        } catch (final Exception ex) {
            attributeHandler.handleError(asResourceException(ex));
        }
    }
    private ResultHandler<Attribute> updateAttributeHandler(final JsonPointer path, final Entry e,
            final ResultHandler<List<Modification>> h) {
        // Get the existing LDAP attribute.
        final Attribute tmp = e.getAttribute(ldapAttributeName);
        final Attribute oldLDAPAttribute = tmp != null ? tmp : emptyAttribute(ldapAttributeName);
        return new ResultHandler<Attribute>() {
                @Override
                public void handleError(final ResourceException error) {
                    h.handleError(error);
@@ -145,10 +371,8 @@
                     *
                     * 2) no new values are provided.
                     */
                    if (isCreateRequest && !writabilityPolicy.canCreate(ldapAttributeName)
                            || isUpdateRequest && !writabilityPolicy.canWrite(ldapAttributeName)) {
                        if (newLDAPAttribute.isEmpty()
                                || (isUpdateRequest && newLDAPAttribute.equals(oldLDAPAttribute))
                if (!writabilityPolicy.canWrite(ldapAttributeName)) {
                    if (newLDAPAttribute.isEmpty() || newLDAPAttribute.equals(oldLDAPAttribute)
                                || writabilityPolicy.discardWrites()) {
                            // No change.
                            h.handleResult(Collections.<Modification> emptyList());
@@ -170,8 +394,8 @@
                                            newLDAPAttribute));
                        } else if (newLDAPAttribute.isEmpty()) {
                            /*
                             * The attribute is being deleted - this is not
                             * allowed if the attribute is required.
                         * The attribute is being deleted - this is not allowed
                         * if the attribute is required.
                             */
                            if (isRequired) {
                                h.handleError(new BadRequestException(i18n(
@@ -185,13 +409,12 @@
                            }
                        } 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.
                         * 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);
@@ -205,32 +428,12 @@
                            final Attribute addedValues = new LinkedAttribute(newLDAPAttribute);
                            addedValues.removeAll(oldLDAPAttribute);
                            if (!addedValues.isEmpty()) {
                                modifications.add(new Modification(ModificationType.ADD,
                                        addedValues));
                            modifications.add(new Modification(ModificationType.ADD, addedValues));
                            }
                        }
                        h.handleResult(modifications);
                    }
                }
            };
            final List<Object> newValues = asList(v);
            if (newValues.isEmpty()) {
                // Skip sub-class implementation if there are no values.
                attributeHandler.handleResult(Attributes.emptyAttribute(ldapAttributeName));
            } else {
                getNewLDAPAttributes(c, path, asList(v), attributeHandler);
            }
        }
    }
    private List<Object> asList(final JsonValue v) {
        if (isNullOrEmpty(v)) {
            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
@@ -20,7 +20,9 @@
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.PatchOperation;
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;
@@ -40,6 +42,33 @@
    }
    /**
     * Maps a JSON value to one or more LDAP attributes, invoking a completion
     * handler once the transformation has completed. This method is invoked
     * when a REST resource is created using a create request.
     * <p>
     * If the JSON value corresponding to this mapper is not present in the
     * resource then this method will be invoked with a value of {@code null}.
     * It is the responsibility of the mapper implementation to take appropriate
     * action in this case, perhaps by substituting default LDAP values, or by
     * rejecting the update by invoking the result handler's
     * {@link ResultHandler#handleError handleError} method.
     *
     * @param c
     *            The context.
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            attribute mapper. This may be used when constructing error
     *            messages.
     * @param v
     *            The JSON value to be converted to LDAP attributes, which may
     *            be {@code null} indicating that the JSON value was not present
     *            in the resource.
     * @param h
     *            The result handler.
     */
    abstract void create(Context c, JsonPointer path, JsonValue v, ResultHandler<List<Attribute>> h);
    /**
     * Adds the names of the LDAP attributes required by this attribute mapper
     * to the provided set.
     * <p>
@@ -48,10 +77,14 @@
     *
     * @param c
     *            The context.
     * @param jsonAttribute
     *            The name of the requested sub-attribute within this mapper or
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            attribute mapper. This may be used when constructing error
     *            messages.
     * @param subPath
     *            The targeted JSON field relative to this attribute mapper, or
     *            root if all attributes associated with this mapper have been
     *            requested.
     *            targeted.
     * @param ldapAttributes
     *            The set into which the required LDAP attribute names should be
     *            put.
@@ -71,12 +104,16 @@
     *
     * @param c
     *            The context.
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            attribute mapper. This may be used when constructing error
     *            messages.
     * @param subPath
     *            The targeted JSON field relative to this attribute mapper, or
     *            root if all attributes associated with this mapper have been
     *            targeted.
     * @param type
     *            The type of REST comparison filter.
     * @param jsonAttribute
     *            The name of the targeted sub-attribute within this mapper or
     *            root if all attributes associated with this mapper have been
     *            targeted by the filter.
     * @param operator
     *            The name of the extended operator to use for the comparison,
     *            or {@code null} if {@code type} is not
@@ -91,6 +128,28 @@
            String operator, Object valueAssertion, ResultHandler<Filter> h);
    /**
     * Maps a JSON patch operation to one or more LDAP modifications, invoking a
     * completion handler once the transformation has completed. This method is
     * invoked when a REST resource is modified using a patch request.
     *
     * @param c
     *            The context.
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            attribute mapper. This may be used when constructing error
     *            messages.
     * @param operation
     *            The JSON patch operation to be converted to LDAP
     *            modifications. The targeted JSON field will be relative to
     *            this attribute mapper, or root if all attributes associated
     *            with this mapper have been targeted.
     * @param h
     *            The result handler.
     */
    abstract void patch(Context c, JsonPointer path, PatchOperation operation,
            ResultHandler<List<Modification>> h);
    /**
     * Maps one or more LDAP attributes to their JSON representation, invoking a
     * completion handler once the transformation has completed.
     * <p>
@@ -109,20 +168,21 @@
     *
     * @param c
     *            The context.
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            attribute mapper. This may be used when constructing error
     *            messages.
     * @param e
     *            The LDAP entry to be converted to JSON.
     * @param h
     *            The result handler.
     */
    abstract void toJSON(Context c, JsonPointer path, Entry e, ResultHandler<JsonValue> h);
    abstract void read(Context c, JsonPointer path, Entry e, ResultHandler<JsonValue> h);
    /**
     * Maps a JSON value to one or more LDAP attributes, invoking a completion
     * handler once the transformation has completed.
     * <p>
     * This method is invoked whenever a REST resource is converted to an LDAP
     * entry or LDAP modification, i.e. when performing create, put, or patch
     * requests.
     * Maps a JSON value to one or more LDAP modifications, invoking a
     * completion handler once the transformation has completed. This method is
     * invoked when a REST resource is modified using an update request.
     * <p>
     * If the JSON value corresponding to this mapper is not present in the
     * resource then this method will be invoked with a value of {@code null}.
@@ -140,7 +200,7 @@
     * @param h
     *            The result handler.
     */
    abstract void toLDAP(Context c, JsonPointer path, Entry e, JsonValue v,
    abstract void update(Context c, JsonPointer path, Entry e, JsonValue v,
            ResultHandler<List<Modification>> h);
    // TODO: methods for obtaining schema information (e.g. name, description,
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
@@ -29,7 +29,9 @@
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.PatchOperation;
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;
@@ -45,6 +47,18 @@
    }
    @Override
    void create(final Context c, final JsonPointer path, final JsonValue v,
            final ResultHandler<List<Attribute>> h) {
        if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) {
            h.handleError(new BadRequestException(i18n(
                    "The request cannot be processed because it attempts to create "
                            + "the read-only field '%s'", path)));
        } else {
            h.handleResult(Collections.<Attribute> emptyList());
        }
    }
    @Override
    void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath,
            final Set<String> ldapAttributes) {
        // Nothing to do.
@@ -90,25 +104,30 @@
    }
    @Override
    void toJSON(final Context c, final JsonPointer path, final Entry e,
    void patch(final Context c, final JsonPointer path, final PatchOperation operation,
            final ResultHandler<List<Modification>> h) {
        h.handleError(new BadRequestException(i18n(
                "The request cannot be processed because it attempts to patch "
                        + "the read-only field '%s'", path)));
    }
    @Override
    void read(final Context c, final JsonPointer path, final Entry e,
            final ResultHandler<JsonValue> h) {
        h.handleResult(value.copy());
    }
    @Override
    void toLDAP(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
    void update(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
            final ResultHandler<List<Modification>> h) {
        if (!isNullOrEmpty(v)) {
            // A value was provided so it must match.
            if (!v.getObject().equals(value.getObject())) {
        if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) {
                h.handleError(new BadRequestException(i18n(
                        "The request cannot be processed because it attempts to modify "
                                + "the read-only field '%s'", path)));
                return;
            }
        }
        } else {
        h.handleResult(Collections.<Modification> emptyList());
    }
    }
    private <T extends Comparable<T>> Filter compare(final Context c, final FilterType type,
            final T v1, final T v2) {
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
@@ -44,8 +44,8 @@
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.PatchOperation;
import org.forgerock.json.resource.PatchRequest;
import org.forgerock.json.resource.PreconditionFailedException;
import org.forgerock.json.resource.QueryFilter;
@@ -69,7 +69,6 @@
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;
@@ -136,31 +135,22 @@
            @Override
            public void run() {
                // Calculate entry content.
                attributeMapper.toLDAP(c, new JsonPointer(), null, request.getContent(),
                        new ResultHandler<List<Modification>>() {
                attributeMapper.create(c, new JsonPointer(), request.getContent(),
                        new ResultHandler<List<Attribute>>() {
                            @Override
                            public void handleError(final ResourceException error) {
                                h.handleError(error);
                            }
                            @Override
                            public void handleResult(final List<Modification> result) {
                            public void handleResult(final List<Attribute> result) {
                                // Perform add operation.
                                final AddRequest addRequest = newAddRequest(DN.rootDN());
                                for (final Attribute attribute : additionalLDAPAttributes) {
                                    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;
                                    }
                                for (final Attribute attribute : result) {
                                    addRequest.addAttribute(attribute);
                                }
                                try {
                                    nameStrategy.setResourceId(c, getBaseDN(c), request
@@ -189,59 +179,104 @@
        final Context c = wrap(context);
        final ResultHandler<Resource> h = wrap(c, handler);
        // Get connection then perform the search.
        c.run(h, new Runnable() {
        // Get connection, search if needed, then delete.
        c.run(h, doUpdate(c, resourceId, request.getRevision(), new ResultHandler<DN>() {
            @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(
                                ldapAttribute);
                c.getConnection().searchSingleEntryAsync(searchRequest,
                        new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() {
                            @Override
                            public void handleErrorResult(final ErrorResultException error) {
                                h.handleError(asResourceException(error));
            public void handleError(final ResourceException error) {
                h.handleError(error);
                            }
                            @Override
                            public void handleResult(final SearchResultEntry entry) {
            public void handleResult(final DN dn) {
                                try {
                                    // Fail-fast if there is a version mismatch.
                                    ensureMVCCVersionMatches(entry, request.getRevision());
                    final ChangeRecord deleteRequest = newDeleteRequest(dn);
                    if (config.readOnUpdatePolicy() == CONTROLS) {
                        final String[] attributes = getLDAPAttributes(c, request.getFields());
                        deleteRequest.addControl(PreReadRequestControl
                                .newControl(false, attributes));
                    }
                    if (config.useSubtreeDelete()) {
                        deleteRequest.addControl(SubtreeDeleteRequestControl.newControl(true));
                    }
                    addAssertionControl(deleteRequest, request.getRevision());
                    c.getConnection()
                            .applyChangeAsync(deleteRequest, null, postUpdateHandler(c, h));
                } catch (final Exception e) {
                    h.handleError(asResourceException(e));
                }
            }
        }));
    }
                                    // Perform delete operation.
                                    final ChangeRecord deleteRequest =
                                            newDeleteRequest(entry.getName());
    @Override
    public void patchInstance(final ServerContext context, final String resourceId,
            final PatchRequest request, final ResultHandler<Resource> handler) {
        final Context c = wrap(context);
        final ResultHandler<Resource> h = wrap(c, handler);
        /*
         * Get the connection, search if needed, then determine modifications,
         * then perform modify.
         */
        c.run(h, doUpdate(c, resourceId, request.getRevision(), new ResultHandler<DN>() {
            @Override
            public void handleError(final ResourceException error) {
                h.handleError(error);
            }
            @Override
            public void handleResult(final DN dn) {
                //  Convert the patch operations to LDAP modifications.
                final ResultHandler<List<Modification>> handler =
                        accumulate(request.getPatchOperations().size(),
                                new ResultHandler<List<List<Modification>>>() {
                                    @Override
                                    public void handleError(final ResourceException error) {
                                        h.handleError(error);
                                    }
                                    @Override
                                    public void handleResult(final List<List<Modification>> result) {
                                        //  The patch operations have been converted successfully.
                                        try {
                                            final ModifyRequest modifyRequest =
                                                    newModifyRequest(dn);
                                    if (config.readOnUpdatePolicy() == CONTROLS) {
                                        final String[] attributes =
                                                getLDAPAttributes(c, request.getFields());
                                        deleteRequest.addControl(PreReadRequestControl.newControl(
                                                false, attributes));
                                                modifyRequest.addControl(PostReadRequestControl
                                                        .newControl(false, attributes));
                                    }
                                    if (config.useSubtreeDelete()) {
                                        deleteRequest.addControl(SubtreeDeleteRequestControl
                                            if (config.usePermissiveModify()) {
                                                modifyRequest
                                                        .addControl(PermissiveModifyRequestControl
                                                .newControl(true));
                                    }
                                    addAssertionControl(deleteRequest, request.getRevision());
                                    c.getConnection().applyChangeAsync(deleteRequest, null,
                                            addAssertionControl(modifyRequest, request
                                                    .getRevision());
                                            // Add the modifications.
                                            for (final List<Modification> modifications : result) {
                                                if (modifications != null) {
                                                    modifyRequest.getModifications().addAll(
                                                            modifications);
                                                }
                                            }
                                            // Perform the modify request.
                                            c.getConnection().applyChangeAsync(modifyRequest, null,
                                            postUpdateHandler(c, h));
                                } catch (final Exception e) {
                                    h.handleError(asResourceException(e));
                                }
                            }
                        });
            }
        });
    }
    @Override
    public void patchInstance(final ServerContext context, final String resourceId,
            final PatchRequest request, final ResultHandler<Resource> handler) {
        handler.handleError(new NotSupportedException("Not yet implemented"));
                for (final PatchOperation operation : request.getPatchOperations()) {
                    attributeMapper.patch(c, new JsonPointer(), operation, handler);
                }
            }
        }));
    }
    @Override
@@ -321,7 +356,7 @@
                                            };
                                    pendingResourceCount.incrementAndGet();
                                    attributeMapper.toJSON(c, new JsonPointer(), entry, mapHandler);
                                    attributeMapper.read(c, new JsonPointer(), entry, mapHandler);
                                    return true;
                                }
@@ -404,7 +439,7 @@
    public void updateInstance(final ServerContext context, final String resourceId,
            final UpdateRequest request, final ResultHandler<Resource> handler) {
        /*
         * Update operations are a bit awkward because there is not direct
         * Update operations are a bit awkward because there is no 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
@@ -415,11 +450,10 @@
        final Context c = wrap(context);
        final ResultHandler<Resource> h = wrap(c, handler);
        // Get connection then perform the search.
        // Get connection then, search for the existing entry, then modify.
        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 =
@@ -457,7 +491,7 @@
                                     * Determine the set of changes that need to
                                     * be performed.
                                     */
                                    attributeMapper.toLDAP(c, new JsonPointer(), entry, request
                                    attributeMapper.update(c, new JsonPointer(), entry, request
                                            .getNewContent(),
                                            new ResultHandler<List<Modification>>() {
                                                @Override
@@ -489,7 +523,7 @@
            final ResultHandler<Resource> handler) {
        final String actualResourceId = nameStrategy.getResourceId(c, entry);
        final String revision = getRevisionFromEntry(entry);
        attributeMapper.toJSON(c, new JsonPointer(), entry, transform(
        attributeMapper.read(c, new JsonPointer(), entry, transform(
                new Function<JsonValue, Resource, Void>() {
                    @Override
                    public Resource apply(final JsonValue value, final Void p) {
@@ -507,6 +541,46 @@
        }
    }
    private Runnable doUpdate(final Context c, final String resourceId, final String revision,
            final ResultHandler<DN> updateHandler) {
        return new Runnable() {
            @Override
            public void run() {
                final String ldapAttribute =
                        (etagAttribute != null && revision != null) ? etagAttribute.toString()
                                : "1.1";
                final SearchRequest searchRequest =
                        nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId).addAttribute(
                                ldapAttribute);
                if (searchRequest.getScope().equals(SearchScope.BASE_OBJECT)) {
                    // There's no point in doing a search because we already know the DN.
                    updateHandler.handleResult(searchRequest.getName());
                } else {
                    c.getConnection().searchSingleEntryAsync(searchRequest,
                            new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() {
                                @Override
                                public void handleErrorResult(final ErrorResultException error) {
                                    updateHandler.handleError(asResourceException(error));
                                }
                                @Override
                                public void handleResult(final SearchResultEntry entry) {
                                    try {
                                        // Fail-fast if there is a version mismatch.
                                        ensureMVCCVersionMatches(entry, revision);
                                        // Perform update operation.
                                        updateHandler.handleResult(entry.getName());
                                    } catch (final Exception e) {
                                        updateHandler.handleError(asResourceException(e));
                                    }
                                }
                            });
                }
            }
        };
    }
    private void ensureMVCCSupported() throws NotSupportedException {
        if (etagAttribute == null) {
            throw new NotSupportedException(
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
@@ -15,7 +15,9 @@
 */
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.json.resource.PatchOperation.operation;
import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
import static org.forgerock.opendj.rest2ldap.Utils.accumulate;
import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.toLowerCase;
@@ -32,7 +34,10 @@
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.PatchOperation;
import org.forgerock.json.resource.ResourceException;
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;
@@ -80,6 +85,37 @@
    }
    @Override
    void create(final Context c, final JsonPointer path, final JsonValue v,
            final ResultHandler<List<Attribute>> h) {
        try {
            /*
             * First check that the JSON value is an object and that the fields
             * it contains are known by this mapper.
             */
            final Map<String, Mapping> missingMappings = checkMapping(path, v);
            // Accumulate the results of the subordinate mappings.
            final ResultHandler<List<Attribute>> handler = accumulator(h);
            // Invoke mappings for which there are values provided.
            if (v != null && !v.isNull()) {
                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.create(c, path.child(me.getKey()), subValue, handler);
                }
            }
            // Invoke mappings for which there were no values provided.
            for (final Mapping mapping : missingMappings.values()) {
                mapping.mapper.create(c, path.child(mapping.name), null, handler);
            }
        } catch (final Exception e) {
            h.handleError(asResourceException(e));
        }
    }
    @Override
    void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath,
            final Set<String> ldapAttributes) {
        if (subPath.isEmpty()) {
@@ -117,7 +153,55 @@
    }
    @Override
    void toJSON(final Context c, final JsonPointer path, final Entry e,
    void patch(final Context c, final JsonPointer path, final PatchOperation operation,
            final ResultHandler<List<Modification>> h) {
        try {
            final JsonPointer field = operation.getField();
            final JsonValue v = operation.getValue();
            if (field.isEmpty()) {
                /*
                 * The patch operation applies to this object. We'll handle this
                 * by allowing the JSON value to be a partial object and
                 * add/remove/replace only the provided values.
                 */
                checkMapping(path, operation.getValue());
                // Accumulate the results of the subordinate mappings.
                final ResultHandler<List<Modification>> handler = accumulator(h);
                // Invoke the sub-mappers using a new patch operation targeted at each field.
                for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
                    final Mapping mapping = getMapping(me.getKey());
                    final JsonValue subValue = new JsonValue(me.getValue());
                    final PatchOperation subOperation =
                            operation(operation.getOperation(), field /* empty */, subValue);
                    mapping.mapper.patch(c, path.child(me.getKey()), subOperation, handler);
                }
            } else {
                /*
                 * The patch operation targets a subordinate field. Create a new
                 * patch operation targeting the field and forward it to the
                 * appropriate mapper.
                 */
                final String fieldName = field.get(0);
                final Mapping mapping = getMapping(fieldName);
                if (mapping == null) {
                    throw new BadRequestException(i18n(
                            "The request cannot be processed because it included "
                                    + "an unrecognized field '%s'", path.child(fieldName)));
                }
                final PatchOperation subOperation =
                        operation(operation.getOperation(), field.relativePointer(), v);
                mapping.mapper.patch(c, path.child(fieldName), subOperation, h);
            }
        } catch (final Exception ex) {
            h.handleError(asResourceException(ex));
        }
    }
    @Override
    void read(final Context c, final JsonPointer path, final Entry e,
            final ResultHandler<JsonValue> h) {
        /*
         * Use an accumulator which will aggregate the results from the
@@ -152,7 +236,7 @@
                        }, h));
        for (final Mapping mapping : mappings.values()) {
            mapping.mapper.toJSON(c, path.child(mapping.name), e, transform(
            mapping.mapper.read(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,
@@ -165,69 +249,80 @@
    }
    @Override
    void toLDAP(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
    void update(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
            final ResultHandler<List<Modification>> h) {
        try {
        /*
         * Fail immediately if the JSON value has the wrong type or contains
         * unknown attributes.
             * First check that the JSON value is an object and that the fields
             * it contains are known by this mapper.
         */
        final Map<String, Mapping> missingMappings = 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(i18n(
                                "The request cannot be processed because the JSON resource "
                                        + "contains an unrecognized field '%s'", path
                                        .child(attribute))));
                        return;
                    }
                }
            } else {
                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;
            }
        }
            final Map<String, Mapping> missingMappings = checkMapping(path, v);
        // Accumulate the results of the subordinate mappings.
        final ResultHandler<List<Modification>> handler =
                accumulate(mappings.size(), transform(
                        new Function<List<List<Modification>>, List<Modification>, Void>() {
                            @Override
                            public List<Modification> apply(final List<List<Modification>> value,
                                    final Void p) {
                                switch (value.size()) {
                                case 0:
                                    return Collections.emptyList();
                                case 1:
                                    return value.get(0);
                                default:
                                    final List<Modification> attributes =
                                            new ArrayList<Modification>(value.size());
                                    for (final List<Modification> a : value) {
                                        attributes.addAll(a);
                                    }
                                    return attributes;
                                }
                            }
                        }, h));
            final ResultHandler<List<Modification>> handler = accumulator(h);
        // Invoke mappings for which there are values provided.
        if (v != null && !v.isNull()) {
            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);
                    mapping.mapper.update(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, path.child(mapping.name), e, null, handler);
                mapping.mapper.update(c, path.child(mapping.name), e, null, handler);
        }
        } catch (final Exception ex) {
            h.handleError(asResourceException(ex));
        }
    }
    private <T> ResultHandler<List<T>> accumulator(final ResultHandler<List<T>> h) {
        return accumulate(mappings.size(), transform(new Function<List<List<T>>, List<T>, Void>() {
            @Override
            public List<T> apply(final List<List<T>> value, final Void p) {
                switch (value.size()) {
                case 0:
                    return Collections.emptyList();
                case 1:
                    return value.get(0);
                default:
                    final List<T> attributes = new ArrayList<T>(value.size());
                    for (final List<T> a : value) {
                        attributes.addAll(a);
                    }
                    return attributes;
                }
            }
        }, h));
    }
    /*
     * Fail immediately if the JSON value has the wrong type or contains unknown
     * attributes.
     */
    private Map<String, Mapping> checkMapping(final JsonPointer path, final JsonValue v)
            throws ResourceException {
        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) {
                        throw new BadRequestException(i18n(
                                "The request cannot be processed because it included "
                                        + "an unrecognized field '%s'", path.child(attribute)));
                    }
                }
            } else {
                throw new BadRequestException(i18n(
                        "The request cannot be processed because it included "
                                + "the field '%s' whose value is the wrong type: "
                                + "an object is expected", path));
            }
        }
        return missingMappings;
    }
    private Mapping getMapping(final JsonPointer jsonAttribute) {
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
@@ -46,7 +46,6 @@
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;
@@ -193,8 +192,7 @@
                new AtomicReference<ResourceException>();
        for (final Object value : newValues) {
            mapper.toLDAP(c, path, null /* force create */, new JsonValue(value),
                    new ResultHandler<List<Modification>>() {
            mapper.create(c, path, new JsonValue(value), new ResultHandler<List<Attribute>>() {
                        @Override
                        public void handleError(final ResourceException error) {
@@ -202,27 +200,26 @@
                        }
                        @Override
                        public void handleResult(final List<Modification> result) {
                public void handleResult(final List<Attribute> result) {
                            Attribute primaryKeyAttribute = null;
                            for (final Modification modification : result) {
                                if (modification.getAttribute().getAttributeDescription().equals(
                                        primaryKey)) {
                                    primaryKeyAttribute = modification.getAttribute();
                    for (final Attribute attribute : result) {
                        if (attribute.getAttributeDescription().equals(primaryKey)) {
                            primaryKeyAttribute = attribute;
                                    break;
                                }
                            }
                            if (primaryKeyAttribute == null || primaryKeyAttribute.isEmpty()) {
                                h.handleError(new BadRequestException(
                                        i18n("The request cannot be processed because the reference "
                        h.handleError(new BadRequestException(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) {
                                h.handleError(new BadRequestException(
                                        i18n("The request cannot be processed because the reference "
                        h.handleError(new BadRequestException(i18n(
                                "The request cannot be processed because the reference "
                                                + "field '%s' contains a value which contains multiple "
                                                + "primary keys", path)));
                                return;
@@ -230,40 +227,32 @@
                            // Now search for the referenced entry in to get its DN.
                            final ByteString primaryKeyValue = primaryKeyAttribute.firstValue();
                            final Filter filter =
                                    Filter.equality(primaryKey.toString(), primaryKeyValue);
                    final Filter filter = Filter.equality(primaryKey.toString(), primaryKeyValue);
                            final SearchRequest search = createSearchRequest(filter);
                            c.getConnection()
                                    .searchSingleEntryAsync(
                                            search,
                    c.getConnection().searchSingleEntryAsync(search,
                                            new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() {
                                                @Override
                                                public void handleErrorResult(
                                                        final ErrorResultException error) {
                                public void handleErrorResult(final ErrorResultException error) {
                                                    ResourceException re;
                                                    try {
                                                        throw error;
                                                    } catch (final EntryNotFoundException e) {
                                                        re =
                                                                new BadRequestException(
                                                                        i18n("The request cannot be processed "
                                                new BadRequestException(i18n(
                                                        "The request cannot be processed "
                                                                                + "because the resource '%s' "
                                                                                + "referenced in field '%s' does "
                                                                                + "not exist",
                                                                                primaryKeyValue
                                                                                        .toString(),
                                                                                path));
                                                                + "not exist", primaryKeyValue
                                                                .toString(), path));
                                                    } catch (final MultipleEntriesFoundException e) {
                                                        re =
                                                                new BadRequestException(
                                                                        i18n("The request cannot be processed "
                                                new BadRequestException(i18n(
                                                        "The request cannot be processed "
                                                                                + "because the resource '%s' "
                                                                                + "referenced in field '%s' is "
                                                                                + "ambiguous",
                                                                                primaryKeyValue
                                                                                        .toString(),
                                                                                path));
                                                                + "ambiguous", primaryKeyValue
                                                                .toString(), path));
                                                    } catch (final ErrorResultException e) {
                                                        re = asResourceException(e);
                                                    }
@@ -272,8 +261,7 @@
                                                }
                                                @Override
                                                public void handleResult(
                                                        final SearchResultEntry result) {
                                public void handleResult(final SearchResultEntry result) {
                                                    synchronized (newLDAPAttribute) {
                                                        newLDAPAttribute.add(result.getName());
                                                    }
@@ -301,7 +289,7 @@
    }
    @Override
    void toJSON(final Context c, final JsonPointer path, final Entry e,
    void read(final Context c, final JsonPointer path, final Entry e,
            final ResultHandler<JsonValue> h) {
        final Attribute attribute = e.getAttribute(ldapAttributeName);
        if (attribute == null || attribute.isEmpty()) {
@@ -377,7 +365,7 @@
                    @Override
                    public void handleResult(final SearchResultEntry result) {
                        mapper.toJSON(c, path, result, handler);
                        mapper.read(c, path, result, handler);
                    }
                });
    }
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
@@ -49,7 +49,6 @@
 */
public final class SimpleAttributeMapper extends AbstractLDAPAttributeMapper<SimpleAttributeMapper> {
    private Function<ByteString, ?, Void> decoder = null;
    private Function<Object, ByteString, Void> encoder = null;
    SimpleAttributeMapper(final AttributeDescription ldapAttributeName) {
@@ -152,7 +151,7 @@
    }
    @Override
    void toJSON(final Context c, final JsonPointer path, final Entry e,
    void read(final Context c, final JsonPointer path, final Entry e,
            final ResultHandler<JsonValue> h) {
        try {
            final Object value;
opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
@@ -20,7 +20,10 @@
import static org.fest.assertions.Fail.fail;
import static org.forgerock.json.fluent.JsonValue.field;
import static org.forgerock.json.fluent.JsonValue.object;
import static org.forgerock.json.resource.PatchOperation.add;
import static org.forgerock.json.resource.PatchOperation.remove;
import static org.forgerock.json.resource.Requests.newDeleteRequest;
import static org.forgerock.json.resource.Requests.newPatchRequest;
import static org.forgerock.json.resource.Requests.newReadRequest;
import static org.forgerock.json.resource.Requests.newUpdateRequest;
import static org.forgerock.json.resource.Resources.newCollection;
@@ -103,6 +106,126 @@
    }
    @Test
    public void testPatch() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        final Resource resource1 =
                connection.patch(ctx(), newPatchRequest("/test1", add("displayName", "changed")));
        checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
    }
    @Test
    public void testPatchAddOptionalAttribute() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        final JsonValue newContent = getTestUser1(12345);
        newContent.put("description", asList("one", "two"));
        final Resource resource1 =
                connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one",
                        "two"))));
        checkResourcesAreEqual(resource1, newContent);
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, newContent);
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testPatchConstantAttribute() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        connection.patch(ctx(), newPatchRequest("/test1", add("/schemas", asList("junk"))));
    }
    @Test
    public void testPatchDeleteOptionalAttribute() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        connection.patch(ctx(),
                newPatchRequest("/test1", add("/description", asList("one", "two"))));
        final Resource resource1 =
                connection.patch(ctx(), newPatchRequest("/test1", remove("/description")));
        checkResourcesAreEqual(resource1, getTestUser1(12345));
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, getTestUser1(12345));
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testPatchMissingRequiredAttribute() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        connection.patch(ctx(), newPatchRequest("/test1", remove("/surname")));
    }
    @Test
    public void testPatchModifyOptionalAttribute() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        connection.patch(ctx(),
                newPatchRequest("/test1", add("/description", asList("one", "two"))));
        final Resource resource1 =
                connection.patch(ctx(), newPatchRequest("/test1", add("/description",
                        asList("three"))));
        final JsonValue newContent = getTestUser1(12345);
        newContent.put("description", asList("one", "two", "three"));
        checkResourcesAreEqual(resource1, newContent);
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, newContent);
    }
    @Test
    public void testPatchMVCCMatch() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        final Resource resource1 =
                connection.patch(ctx(), newPatchRequest("/test1", add("displayName", "changed"))
                        .setRevision("12345"));
        checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
        checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
    }
    @Test(expectedExceptions = PreconditionFailedException.class)
    public void testPatchMVCCNoMatch() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        connection.patch(ctx(), newPatchRequest("/test1", add("displayName", "changed"))
                .setRevision("12346"));
    }
    @Test(expectedExceptions = NotFoundException.class)
    public void testPatchNotFound() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        connection.patch(ctx(), newPatchRequest("/missing", add("displayName", "changed")));
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testPatchReadOnlyAttribute() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        // Etag is read-only.
        connection.patch(ctx(), newPatchRequest("/test1", add("_rev", "99999")));
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testPatchSingleValuedAttributeWithMultipleValues() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        connection.patch(ctx(),
                newPatchRequest("/test1", add("/surname", asList("black", "white"))));
    }
    @Test(expectedExceptions = BadRequestException.class)
    public void testPatchUnknownAttribute() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Connection connection = newInternalConnection(handler);
        final JsonValue newContent = getTestUser1Updated(12345);
        newContent.add("dummy", "junk");
        connection.patch(ctx(), newPatchRequest("/test1", add("/dummy", "junk")));
    }
    @Test
    public void testRead() throws Exception {
        final RequestHandler handler = newCollection(builder().build());
        final Resource resource =