From a2bc68638f55ae0ad7b9e3a04c7a3c02d01384f8 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Wed, 13 Feb 2013 23:44:11 +0000
Subject: [PATCH] Partial fix for OPENDJ-758 : Implement configurable update policy for simple and default mappers

---
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/WritabilityPolicy.java              |   41 ++
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java                      |  121 +++----
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java          |  217 ++++++++++++
 opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Example.java                        |   43 +-
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java                         |   28 
 /dev/null                                                                                                 |   74 ----
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java          |  156 +++++---
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java                          |   10 
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java                |   49 +-
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/MVCCStrategy.java                   |   22 
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java    |   84 ++--
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java |  118 +++---
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java                   |    6 
 13 files changed, 594 insertions(+), 375 deletions(-)

diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
index cdb9c5c..70f699a 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
@@ -16,7 +16,6 @@
 package org.forgerock.opendj.rest2ldap;
 
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 import org.forgerock.json.fluent.JsonPointer;
@@ -42,8 +41,7 @@
 
     /**
      * Adds the names of the LDAP attributes required by this attribute mapper
-     * which are associated with the provided resource attribute to the provided
-     * set.
+     * to the provided set.
      * <p>
      * Implementations should only add the names of attributes found in the LDAP
      * entry directly associated with the resource.
@@ -51,7 +49,9 @@
      * @param c
      *            The context.
      * @param jsonAttribute
-     *            The name of the resource attribute requested by the client.
+     *            The name of the requested sub-attribute within this mapper or
+     *            root if all attributes associated with this mapper have been
+     *            requested.
      * @param ldapAttributes
      *            The set into which the required LDAP attribute names should be
      *            put.
@@ -63,11 +63,7 @@
      * filter representation, invoking a completion handler once the
      * transformation has completed.
      * <p>
-     * If this attribute mapper is not responsible for mapping the provided JSON
-     * attribute then the result handler's {@link ResultHandler#handleResult
-     * handleResult} method must be invoked with the value {@code null}. If this
-     * attribute mapper is responsible for mapping the JSON attribute, but an
-     * error occurred while constructing the LDAP filter, then the result
+     * If an error occurred while constructing the LDAP filter, then the result
      * handler's {@link ResultHandler#handleError handleError} method must be
      * invoked with an appropriate exception indicating the problem which
      * occurred.
@@ -77,7 +73,9 @@
      * @param type
      *            The type of REST comparison filter.
      * @param jsonAttribute
-     *            The name of the resource attribute to be filtered.
+     *            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
@@ -92,13 +90,21 @@
             String operator, Object valueAssertion, ResultHandler<Filter> h);
 
     /**
-     * Transforms attributes contained in the provided LDAP entry to JSON
-     * content, invoking a completion handler once the transformation has
-     * completed.
+     * Maps one or more LDAP attributes to their JSON representation, invoking a
+     * completion handler once the transformation has completed.
      * <p>
      * This method is invoked whenever an LDAP entry is converted to a REST
      * resource, i.e. when responding to read, query, create, put, or patch
      * requests.
+     * <p>
+     * If the LDAP attributes are not present in the entry, perhaps because they
+     * are optional, then implementations should invoke the result handler's
+     * {@link ResultHandler#handleResult handleResult} method with a result of
+     * {@code null}. If the LDAP attributes cannot be mapped for any other
+     * reason, perhaps because they are required but missing, or they contain
+     * unexpected content, then the result handler's
+     * {@link ResultHandler#handleError handleError} method must be invoked with
+     * an appropriate exception indicating the problem which occurred.
      *
      * @param c
      *            The context.
@@ -107,20 +113,29 @@
      * @param h
      *            The result handler.
      */
-    abstract void toJSON(Context c, Entry e, ResultHandler<Map<String, Object>> h);
+    abstract void toJSON(Context c, Entry e, ResultHandler<JsonValue> h);
 
     /**
-     * Transforms JSON content in the provided JSON value to LDAP attributes,
-     * invoking a completion handler once the transformation has completed.
+     * 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.
+     * <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 v
-     *            The JSON value to be converted to LDAP attributes.
+     *            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.
      */
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ComplexAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ComplexAttributeMapper.java
deleted file mode 100644
index a1ad982..0000000
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ComplexAttributeMapper.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * 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 2012-2013 ForgeRock AS.
- */
-package org.forgerock.opendj.rest2ldap;
-
-import static org.forgerock.opendj.rest2ldap.Utils.toLowerCase;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import org.forgerock.json.fluent.JsonPointer;
-import org.forgerock.json.fluent.JsonValue;
-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;
-
-/**
- * An attribute mapper which maps a single JSON attribute to the result of
- * another attribute mapper.
- */
-final class ComplexAttributeMapper extends AttributeMapper {
-
-    private final String jsonAttributeName;
-    private final AttributeMapper mapper;
-    private final String normalizedJsonAttributeName;
-
-    /**
-     * Creates a new complex attribute mapper which will wrap the results of the
-     * provided mapper as a complex JSON object.
-     *
-     * @param jsonAttributeName
-     *            The name of the complex attribute.
-     * @param mapper
-     *            The mapper which should be used to provide the contents of the
-     *            complex attribute.
-     */
-    ComplexAttributeMapper(final String jsonAttributeName, final AttributeMapper mapper) {
-        this.jsonAttributeName = jsonAttributeName;
-        this.mapper = mapper;
-        this.normalizedJsonAttributeName = toLowerCase(jsonAttributeName);
-    }
-
-    @Override
-    void getLDAPAttributes(final Context c, final JsonPointer jsonAttribute,
-            final Set<String> ldapAttributes) {
-        if (jsonAttribute.isEmpty() || matches(jsonAttribute)) {
-            final JsonPointer relativePointer = jsonAttribute.relativePointer();
-            mapper.getLDAPAttributes(c, relativePointer, ldapAttributes);
-        }
-    }
-
-    @Override
-    void getLDAPFilter(final Context c, final FilterType type, final JsonPointer jsonAttribute,
-            final String operator, final Object valueAssertion, final ResultHandler<Filter> h) {
-        if (matches(jsonAttribute)) {
-            final JsonPointer relativePointer = jsonAttribute.relativePointer();
-            mapper.getLDAPFilter(c, type, relativePointer, operator, valueAssertion, h);
-        } else {
-            // This attribute mapper cannot handle the provided filter component.
-            h.handleResult(null);
-        }
-    }
-
-    @Override
-    void toJSON(final Context c, final Entry e, final ResultHandler<Map<String, Object>> h) {
-        final ResultHandler<Map<String, Object>> wrapper =
-                new ResultHandler<Map<String, Object>>() {
-
-                    @Override
-                    public void handleError(final ResourceException e) {
-                        h.handleError(e);
-                    }
-
-                    @Override
-                    public void handleResult(final Map<String, Object> result) {
-                        final Map<String, Object> complexResult =
-                                Collections.singletonMap(jsonAttributeName, (Object) result);
-                        h.handleResult(complexResult);
-                    }
-                };
-        mapper.toJSON(c, e, wrapper);
-    }
-
-    @Override
-    void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) {
-        if (v.isDefined(jsonAttributeName)) {
-            mapper.toLDAP(c, v.get(jsonAttributeName), h);
-        } else {
-            mapper.toLDAP(c, new JsonValue(Collections.emptyMap()), h);
-        }
-    }
-
-    private boolean matches(final JsonPointer jsonAttribute) {
-        return !jsonAttribute.isEmpty()
-                && toLowerCase(jsonAttribute.get(0)).equals(normalizedJsonAttributeName);
-    }
-
-}
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/CompositeAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/CompositeAttributeMapper.java
deleted file mode 100644
index cf77ea8..0000000
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/CompositeAttributeMapper.java
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * 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 2012-2013 ForgeRock AS.
- */
-package org.forgerock.opendj.rest2ldap;
-
-import static org.forgerock.opendj.rest2ldap.Utils.accumulate;
-import static org.forgerock.opendj.rest2ldap.Utils.transform;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-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.Function;
-
-/**
- * An attribute mapper which combines the results of a set of subordinate
- * attribute mappers into a single JSON object.
- */
-final class CompositeAttributeMapper extends AttributeMapper {
-    private final List<AttributeMapper> attributeMappers;
-
-    /**
-     * Creates a new composite attribute mapper.
-     */
-    CompositeAttributeMapper(final Collection<AttributeMapper> mappers) {
-        this.attributeMappers = new ArrayList<AttributeMapper>(mappers);
-    }
-
-    @Override
-    void getLDAPAttributes(final Context c, final JsonPointer jsonAttribute,
-            final Set<String> ldapAttributes) {
-        for (final AttributeMapper attribute : attributeMappers) {
-            attribute.getLDAPAttributes(c, jsonAttribute, ldapAttributes);
-        }
-    }
-
-    @Override
-    void getLDAPFilter(final Context c, final FilterType type, final JsonPointer jsonAttribute,
-            final String operator, final Object valueAssertion, final ResultHandler<Filter> h) {
-        final ResultHandler<Filter> handler =
-                accumulate(attributeMappers.size(), transform(
-                        new Function<List<Filter>, Filter, Void>() {
-                            @Override
-                            public Filter apply(final List<Filter> value, final Void p) {
-                                // Remove unmapped filters and combine using logical-OR.
-                                final Iterator<Filter> i = value.iterator();
-                                while (i.hasNext()) {
-                                    final Filter f = i.next();
-                                    if (f == null) {
-                                        // No mapping so remove.
-                                        i.remove();
-                                    } else if (f == c.getConfig().falseFilter()) {
-                                        return c.getConfig().falseFilter();
-                                    } else if (f == c.getConfig().trueFilter()) {
-                                        return c.getConfig().trueFilter();
-                                    }
-                                }
-                                switch (value.size()) {
-                                case 0:
-                                    // No mappings found.
-                                    return null;
-                                case 1:
-                                    return value.get(0);
-                                default:
-                                    return Filter.or(value);
-                                }
-                            }
-                        }, h));
-        for (final AttributeMapper subMapper : attributeMappers) {
-            subMapper.getLDAPFilter(c, type, jsonAttribute, operator, valueAssertion, handler);
-        }
-    }
-
-    @Override
-    void toJSON(final Context c, final Entry e, final ResultHandler<Map<String, Object>> h) {
-        final ResultHandler<Map<String, Object>> handler =
-                accumulate(attributeMappers.size(), transform(
-                        new Function<List<Map<String, Object>>, Map<String, Object>, Void>() {
-                            @Override
-                            public Map<String, Object> apply(final List<Map<String, Object>> value,
-                                    final Void p) {
-                                switch (value.size()) {
-                                case 0:
-                                    return Collections.<String, Object> emptyMap();
-                                case 1:
-                                    return value.get(0) != null ? value.get(0) : Collections
-                                            .<String, Object> emptyMap();
-                                default:
-                                    return mergeJsonValues(value,
-                                            new LinkedHashMap<String, Object>());
-                                }
-                            }
-                        }, h));
-        for (final AttributeMapper mapper : attributeMappers) {
-            mapper.toJSON(c, e, handler);
-        }
-    }
-
-    @Override
-    void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) {
-        final ResultHandler<List<Attribute>> handler =
-                accumulate(attributeMappers.size(), transform(
-                        new Function<List<List<Attribute>>, List<Attribute>, Void>() {
-                            @Override
-                            public List<Attribute> apply(final List<List<Attribute>> value,
-                                    final Void p) {
-                                switch (value.size()) {
-                                case 0:
-                                    return Collections.emptyList();
-                                case 1:
-                                    return value.get(0) != null ? value.get(0) : Collections
-                                            .<Attribute> emptyList();
-                                default:
-                                    List<Attribute> attributes =
-                                            new ArrayList<Attribute>(value.size());
-                                    for (List<Attribute> a : value) {
-                                        attributes.addAll(a);
-                                    }
-                                    return attributes;
-                                }
-                            }
-                        }, h));
-        for (final AttributeMapper mapper : attributeMappers) {
-            mapper.toLDAP(c, v, handler);
-        }
-    }
-
-    @SuppressWarnings("unchecked")
-    private void mergeJsonValue(final Map<String, Object> srcValue,
-            final Map<String, Object> dstValue) {
-        for (final Map.Entry<String, Object> record : srcValue.entrySet()) {
-            final String key = record.getKey();
-            final Object newValue = record.getValue();
-            Object existingValue = dstValue.get(key);
-            if (existingValue == null) {
-                // Value is new, so just add it.
-                dstValue.put(key, newValue);
-            } else if ((existingValue instanceof Map) && (newValue instanceof Map)) {
-                // Merge two maps - create a new Map, in case the existing one
-                // is unmodifiable.
-                existingValue =
-                        new LinkedHashMap<String, Object>((Map<String, Object>) existingValue);
-                mergeJsonValue((Map<String, Object>) newValue, (Map<String, Object>) existingValue);
-            } else if ((existingValue instanceof List) && (newValue instanceof List)) {
-                // Merge two lists- create a new List, in case the existing one
-                // is unmodifiable.
-                final List<Object> tmp = new ArrayList<Object>((List<Object>) existingValue);
-                tmp.addAll((List<Object>) newValue);
-                existingValue = tmp;
-            }
-
-            // Replace the existing value.
-            dstValue.put(key, newValue);
-        }
-    }
-
-    private Map<String, Object> mergeJsonValues(final List<Map<String, Object>> srcValues,
-            final Map<String, Object> dstValue) {
-        for (final Map<String, Object> value : srcValues) {
-            if (value != null) {
-                mergeJsonValue(value, dstValue);
-            }
-        }
-        return dstValue;
-    }
-}
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
index 53076d2..e5a8b1b 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
@@ -25,10 +25,10 @@
 final class Config {
 
     private final Filter falseFilter;
-    private final Schema schema;
-    private final ReadOnUpdatePolicy readOnUpdatePolicy;
-    private final Filter trueFilter;
     private final DecodeOptions options;
+    private final ReadOnUpdatePolicy readOnUpdatePolicy;
+    private final Schema schema;
+    private final Filter trueFilter;
 
     Config(final Filter trueFilter, final Filter falseFilter,
             final ReadOnUpdatePolicy readOnUpdatePolicy, final Schema schema) {
@@ -40,17 +40,6 @@
     }
 
     /**
-     * Returns the schema which should be used when attribute types and
-     * controls.
-     *
-     * @return The schema which should be used when attribute types and
-     *         controls.
-     */
-    public Schema schema() {
-        return schema;
-    }
-
-    /**
      * Returns the decoding options which should be used when decoding controls
      * in responses.
      *
@@ -83,6 +72,17 @@
     }
 
     /**
+     * Returns the schema which should be used when attribute types and
+     * controls.
+     *
+     * @return The schema which should be used when attribute types and
+     *         controls.
+     */
+    public Schema schema() {
+        return schema;
+    }
+
+    /**
      * {@inheritDoc}
      */
     @Override
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DefaultAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DefaultAttributeMapper.java
deleted file mode 100644
index 96b68f7..0000000
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DefaultAttributeMapper.java
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * 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 2012-2013 ForgeRock AS.
- */
-package org.forgerock.opendj.rest2ldap;
-
-import static org.forgerock.opendj.rest2ldap.Utils.attributeToJson;
-import static org.forgerock.opendj.rest2ldap.Utils.getAttributeName;
-import static org.forgerock.opendj.rest2ldap.Utils.jsonToAttribute;
-import static org.forgerock.opendj.rest2ldap.Utils.toFilter;
-import static org.forgerock.opendj.rest2ldap.Utils.toLowerCase;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-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.ResultHandler;
-import org.forgerock.opendj.ldap.Attribute;
-import org.forgerock.opendj.ldap.AttributeDescription;
-import org.forgerock.opendj.ldap.Entry;
-import org.forgerock.opendj.ldap.Filter;
-
-/**
- * An attribute mapper that directly maps a configurable selection of attributes
- * to and from LDAP without any transformation.
- */
-final class DefaultAttributeMapper extends AttributeMapper {
-    // All user attributes by default.
-    private final Map<String, String> excludedAttributes = new LinkedHashMap<String, String>();
-    private final Map<String, String> includedAttributes = new LinkedHashMap<String, String>();
-
-    /**
-     * Creates a new default attribute mapper which will map all user attributes
-     * to JSON by default.
-     */
-    DefaultAttributeMapper() {
-        // No implementation required.
-    }
-
-    /**
-     * Excludes one or more LDAP attributes from this mapping.
-     *
-     * @param attributes
-     *            The attributes to be excluded.
-     * @return This attribute mapper.
-     */
-    DefaultAttributeMapper excludeAttribute(final String... attributes) {
-        for (final String attribute : attributes) {
-            excludedAttributes.put(toLowerCase(attribute), attribute);
-        }
-        return this;
-    }
-
-    @Override
-    void getLDAPAttributes(final Context c, final JsonPointer jsonAttribute,
-            final Set<String> ldapAttributes) {
-        switch (jsonAttribute.size()) {
-        case 0:
-            // Requested everything.
-            if (!includedAttributes.isEmpty()) {
-                ldapAttributes.addAll(includedAttributes.values());
-            } else {
-                // All user attributes.
-                ldapAttributes.add("*");
-            }
-            break;
-        default:
-            final String name = jsonAttribute.get(0);
-            if (isIncludedAttribute(name)) {
-                ldapAttributes.add(name);
-            }
-            break;
-        }
-    }
-
-    @Override
-    void getLDAPFilter(final Context c, final FilterType type, final JsonPointer jsonAttribute,
-            final String operator, final Object valueAssertion, final ResultHandler<Filter> h) {
-        if (jsonAttribute.size() == 1 && isIncludedAttribute(jsonAttribute.get(0))) {
-            h.handleResult(toFilter(c, type, jsonAttribute.get(0), valueAssertion));
-        } else {
-            // This attribute mapper cannot handle the provided filter component.
-            h.handleResult(null);
-        }
-    }
-
-    /**
-     * Includes one or more LDAP attributes in this mapping.
-     *
-     * @param attributes
-     *            The attributes to be included.
-     * @return This attribute mapper.
-     */
-    DefaultAttributeMapper includeAttribute(final String... attributes) {
-        for (final String attribute : attributes) {
-            includedAttributes.put(toLowerCase(attribute), attribute);
-        }
-        return this;
-    }
-
-    @Override
-    void toJSON(final Context c, final Entry e, final ResultHandler<Map<String, Object>> h) {
-        // FIXME: this will leave out attributes which were not included in the LDAP entry. We should
-        // ensure that JSON attributes are present, even if they are null or an empty array.
-        final Map<String, Object> result = new LinkedHashMap<String, Object>(e.getAttributeCount());
-        for (final Attribute a : e.getAllAttributes()) {
-            final String name = getAttributeName(a);
-            if (isIncludedAttribute(name)) {
-                result.put(name, attributeToJson(a));
-            }
-        }
-        h.handleResult(result);
-    }
-
-    @Override
-    void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) {
-        if (v.isMap()) {
-            final List<Attribute> result = new ArrayList<Attribute>(v.size());
-            for (final Map.Entry<String, Object> field : v.asMap().entrySet()) {
-                if (!isIncludedAttribute(field.getKey())) {
-                    continue;
-                }
-                try {
-                    final AttributeDescription ad =
-                            AttributeDescription.valueOf(field.getKey(), c.getConfig().schema());
-                    result.add(jsonToAttribute(field.getValue(), ad));
-                } catch (final Exception e) {
-                    // FIXME: improve error message.
-                    h.handleError(new BadRequestException("The field " + field.getKey()
-                            + " is invalid"));
-                    return;
-                }
-            }
-            h.handleResult(result);
-        } else {
-            h.handleResult(Collections.<Attribute> emptyList());
-        }
-    }
-
-    private boolean isIncludedAttribute(final String name) {
-        final String lowerName = toLowerCase(name);
-
-        // Ignore the requested attribute if it has been excluded.
-        if (excludedAttributes.containsKey(lowerName)) {
-            return false;
-        }
-
-        // Include all attributes by default.
-        if (includedAttributes.isEmpty() || includedAttributes.containsKey(lowerName)) {
-            return true;
-        }
-
-        return false;
-    }
-}
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
index fac2ae1..8c0c88d 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
@@ -19,7 +19,6 @@
 
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 import org.forgerock.json.fluent.JsonPointer;
@@ -33,12 +32,10 @@
  * An attribute mapper which maps a single JSON attribute to a fixed value.
  */
 final class JSONConstantAttributeMapper extends AttributeMapper {
-    private final String jsonAttributeName;
-    private final Object jsonAttributeValue;
+    private final JsonValue value;
 
-    JSONConstantAttributeMapper(final String attributeName, final Object attributeValue) {
-        this.jsonAttributeName = attributeName;
-        this.jsonAttributeValue = attributeValue;
+    JSONConstantAttributeMapper(final Object value) {
+        this.value = new JsonValue(value);
     }
 
     @Override
@@ -50,57 +47,52 @@
     @Override
     void getLDAPFilter(final Context c, final FilterType type, final JsonPointer jsonAttribute,
             final String operator, final Object valueAssertion, final ResultHandler<Filter> h) {
-        if (jsonAttribute.size() == 1 && jsonAttribute.get(0).equalsIgnoreCase(jsonAttributeName)) {
-            final Filter filter;
-            if (type == FilterType.PRESENT) {
-                filter = c.getConfig().trueFilter();
-            } else if (jsonAttributeValue instanceof String && valueAssertion instanceof String) {
-                final String v1 = toLowerCase((String) jsonAttributeValue);
-                final String v2 = toLowerCase((String) valueAssertion);
-                switch (type) {
-                case CONTAINS:
-                    filter =
-                            v1.contains(v2) ? c.getConfig().trueFilter() : c.getConfig()
-                                    .falseFilter();
-                    break;
-                case STARTS_WITH:
-                    filter =
-                            v1.startsWith(v2) ? c.getConfig().trueFilter() : c.getConfig()
-                                    .falseFilter();
-                    break;
-                default:
-                    filter = compare(c, type, v1, v2);
-                    break;
-                }
-            } else if (jsonAttributeValue instanceof Number && valueAssertion instanceof Number) {
-                final Double v1 = ((Number) jsonAttributeValue).doubleValue();
-                final Double v2 = ((Number) valueAssertion).doubleValue();
+        final Filter filter;
+        final JsonValue subValue = value.get(jsonAttribute);
+        if (subValue == null) {
+            filter = c.getConfig().falseFilter();
+        } else if (type == FilterType.PRESENT) {
+            filter = c.getConfig().trueFilter();
+        } else if (value.isString() && valueAssertion instanceof String) {
+            final String v1 = toLowerCase(value.asString());
+            final String v2 = toLowerCase((String) valueAssertion);
+            switch (type) {
+            case CONTAINS:
+                filter = v1.contains(v2) ? c.getConfig().trueFilter() : c.getConfig().falseFilter();
+                break;
+            case STARTS_WITH:
+                filter =
+                        v1.startsWith(v2) ? c.getConfig().trueFilter() : c.getConfig()
+                                .falseFilter();
+                break;
+            default:
                 filter = compare(c, type, v1, v2);
-            } else if (jsonAttributeValue instanceof Boolean && valueAssertion instanceof Boolean) {
-                final Boolean v1 = (Boolean) jsonAttributeValue;
-                final Boolean v2 = (Boolean) valueAssertion;
-                filter = compare(c, type, v1, v2);
-            } else {
-                // This attribute mapper is a candidate but it does not match.
-                filter = c.getConfig().falseFilter();
+                break;
             }
-            h.handleResult(filter);
+        } else if (value.isNumber() && valueAssertion instanceof Number) {
+            final Double v1 = value.asDouble();
+            final Double v2 = ((Number) valueAssertion).doubleValue();
+            filter = compare(c, type, v1, v2);
+        } else if (value.isBoolean() && valueAssertion instanceof Boolean) {
+            final Boolean v1 = value.asBoolean();
+            final Boolean v2 = (Boolean) valueAssertion;
+            filter = compare(c, type, v1, v2);
         } else {
-            // This attribute mapper cannot handle the provided filter component.
-            h.handleResult(null);
+            // This attribute mapper is a candidate but it does not match.
+            filter = c.getConfig().falseFilter();
         }
+        h.handleResult(filter);
     }
 
     @Override
-    void toJSON(final Context c, final Entry e, final ResultHandler<Map<String, Object>> h) {
-        // FIXME: how do we know if the user requested it???
-        h.handleResult(Collections.singletonMap(jsonAttributeName, jsonAttributeValue));
-
+    void toJSON(final Context c, 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) {
-        h.handleResult(Collections.<Attribute>emptyList());
+        // FIXME: should we check if the provided value matches the constant?
+        h.handleResult(Collections.<Attribute> emptyList());
     }
 
     private <T extends Comparable<T>> Filter compare(final Context c, final FilterType type,
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
index c0c4cd2..ffda24d 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
@@ -24,7 +24,6 @@
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -85,8 +84,8 @@
  */
 final class LDAPCollectionResourceProvider implements CollectionResourceProvider {
 
-    private abstract class AbstractRequestCompletionHandler<R,
-            H extends org.forgerock.opendj.ldap.ResultHandler<? super R>>
+    private abstract class AbstractRequestCompletionHandler
+            <R, H extends org.forgerock.opendj.ldap.ResultHandler<? super R>>
             implements org.forgerock.opendj.ldap.ResultHandler<R> {
         final Connection connection;
         final H resultHandler;
@@ -161,6 +160,7 @@
     // Dummy exception used for signalling search success.
     private static final ResourceException SUCCESS = new UncategorizedException(0, null, null);
 
+    private final List<Attribute> additionalLDAPAttributes;
     private final AttributeMapper attributeMapper;
     private final DN baseDN; // TODO: support template variables.
     private final Config config;
@@ -170,13 +170,15 @@
 
     LDAPCollectionResourceProvider(final DN baseDN, final AttributeMapper mapper,
             final ConnectionFactory factory, final NameStrategy nameStrategy,
-            final MVCCStrategy mvccStrategy, final Config config) {
+            final MVCCStrategy mvccStrategy, final Config config,
+            final List<Attribute> additionalLDAPAttributes) {
         this.baseDN = baseDN;
         this.attributeMapper = mapper;
         this.factory = factory;
         this.config = config;
         this.nameStrategy = nameStrategy;
         this.mvccStrategy = mvccStrategy;
+        this.additionalLDAPAttributes = additionalLDAPAttributes;
     }
 
     @Override
@@ -204,10 +206,19 @@
             @Override
             public void handleResult(final List<Attribute> result) {
                 final AddRequest addRequest = Requests.newAddRequest(DN.rootDN());
+                for (final Attribute attribute : additionalLDAPAttributes) {
+                    addRequest.addAttribute(attribute);
+                }
                 for (final Attribute attribute : result) {
                     addRequest.addAttribute(attribute);
                 }
-                nameStrategy.setResourceId(c, getBaseDN(c), request.getNewResourceId(), addRequest);
+                try {
+                    nameStrategy.setResourceId(c, getBaseDN(c), request.getNewResourceId(),
+                            addRequest);
+                } catch (ResourceException e) {
+                    handler.handleError(e);
+                    return;
+                }
                 if (config.readOnUpdatePolicy() == USE_READ_ENTRY_CONTROLS) {
                     final String[] attributes = getLDAPAttributes(c, request.getFieldFilters());
                     addRequest.addControl(PostReadRequestControl.newControl(false, attributes));
@@ -254,24 +265,22 @@
 
                 final String id = nameStrategy.getResourceId(c, entry);
                 final String revision = mvccStrategy.getRevisionFromEntry(c, entry);
-                final ResultHandler<Map<String, Object>> mapHandler =
-                        new ResultHandler<Map<String, Object>>() {
-                            @Override
-                            public void handleError(final ResourceException e) {
-                                pendingResult.compareAndSet(null, e);
-                                pendingResourceCount.decrementAndGet();
-                                completeIfNecessary();
-                            }
+                final ResultHandler<JsonValue> mapHandler = new ResultHandler<JsonValue>() {
+                    @Override
+                    public void handleError(final ResourceException e) {
+                        pendingResult.compareAndSet(null, e);
+                        pendingResourceCount.decrementAndGet();
+                        completeIfNecessary();
+                    }
 
-                            @Override
-                            public void handleResult(final Map<String, Object> result) {
-                                final Resource resource =
-                                        new Resource(id, revision, new JsonValue(result));
-                                handler.handleResource(resource);
-                                pendingResourceCount.decrementAndGet();
-                                completeIfNecessary();
-                            }
-                        };
+                    @Override
+                    public void handleResult(final JsonValue result) {
+                        final Resource resource = new Resource(id, revision, result);
+                        handler.handleResource(resource);
+                        pendingResourceCount.decrementAndGet();
+                        completeIfNecessary();
+                    }
+                };
 
                 pendingResourceCount.incrementAndGet();
                 attributeMapper.toJSON(c, entry, mapHandler);
@@ -406,21 +415,12 @@
             final ResultHandler<Resource> handler) {
         final String actualResourceId = nameStrategy.getResourceId(c, entry);
         final String revision = mvccStrategy.getRevisionFromEntry(c, entry);
-        final ResultHandler<Map<String, Object>> mapHandler =
-                new ResultHandler<Map<String, Object>>() {
-                    @Override
-                    public void handleError(final ResourceException e) {
-                        handler.handleError(e);
-                    }
-
-                    @Override
-                    public void handleResult(final Map<String, Object> result) {
-                        final Resource resource =
-                                new Resource(actualResourceId, revision, new JsonValue(result));
-                        handler.handleResult(resource);
-                    }
-                };
-        attributeMapper.toJSON(c, entry, mapHandler);
+        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));
     }
 
     /**
@@ -454,6 +454,23 @@
         return ResourceException.getException(resourceResultCode, null, error.getMessage(), error);
     }
 
+    private void applyUpdate(final Context c, final ChangeRecord request,
+            final ResultHandler<Resource> handler) {
+        final org.forgerock.opendj.ldap.ResultHandler<Result> resultHandler =
+                postUpdateHandler(c, handler);
+        final ConnectionCompletionHandler<Result> outerHandler =
+                new ConnectionCompletionHandler<Result>(resultHandler) {
+
+                    @Override
+                    public void handleResult(final Connection connection) {
+                        final RequestCompletionHandler<Result> innerHandler =
+                                new RequestCompletionHandler<Result>(connection, resultHandler);
+                        connection.applyChangeAsync(request, null, innerHandler);
+                    }
+                };
+        factory.getConnectionAsync(outerHandler);
+    }
+
     private DN getBaseDN(final Context context) {
         return baseDN;
     }
@@ -675,10 +692,6 @@
         queryFilter.accept(visitor, h);
     }
 
-    private Context wrap(final ServerContext context) {
-        return new Context(config, context);
-    }
-
     private org.forgerock.opendj.ldap.ResultHandler<Result> postUpdateHandler(final Context c,
             final ResultHandler<Resource> handler) {
         // The handler which will be invoked for the LDAP add result.
@@ -694,13 +707,13 @@
                         // FIXME: handle USE_SEARCH policy.
                         Entry entry;
                         try {
-                            PostReadResponseControl postReadControl =
+                            final PostReadResponseControl postReadControl =
                                     result.getControl(PostReadResponseControl.DECODER, config
                                             .decodeOptions());
                             if (postReadControl != null) {
                                 entry = postReadControl.getEntry();
                             } else {
-                                PreReadResponseControl preReadControl =
+                                final PreReadResponseControl preReadControl =
                                         result.getControl(PreReadResponseControl.DECODER, config
                                                 .decodeOptions());
                                 if (preReadControl != null) {
@@ -709,7 +722,7 @@
                                     entry = null;
                                 }
                             }
-                        } catch (DecodeException e) {
+                        } catch (final DecodeException e) {
                             // FIXME: log something?
                             entry = null;
                         }
@@ -726,20 +739,7 @@
         return resultHandler;
     }
 
-    private void applyUpdate(final Context c, final ChangeRecord request,
-            final ResultHandler<Resource> handler) {
-        final org.forgerock.opendj.ldap.ResultHandler<Result> resultHandler =
-                postUpdateHandler(c, handler);
-        final ConnectionCompletionHandler<Result> outerHandler =
-                new ConnectionCompletionHandler<Result>(resultHandler) {
-
-                    @Override
-                    public void handleResult(final Connection connection) {
-                        final RequestCompletionHandler<Result> innerHandler =
-                                new RequestCompletionHandler<Result>(connection, resultHandler);
-                        connection.applyChangeAsync(request, null, innerHandler);
-                    }
-                };
-        factory.getConnectionAsync(outerHandler);
+    private Context wrap(final ServerContext context) {
+        return new Context(config, context);
     }
 }
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPConstantAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPConstantAttributeMapper.java
deleted file mode 100644
index e1b5ade..0000000
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPConstantAttributeMapper.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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 2012-2013 ForgeRock AS.
- */
-package org.forgerock.opendj.rest2ldap;
-
-import static java.util.Collections.singletonList;
-import static org.forgerock.opendj.ldap.Attributes.singletonAttribute;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-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.AttributeDescription;
-import org.forgerock.opendj.ldap.Entry;
-import org.forgerock.opendj.ldap.Filter;
-import org.forgerock.opendj.ldap.LinkedAttribute;
-
-/**
- * An attribute mapper which maps a single LDAP attribute to a fixed value.
- */
-final class LDAPConstantAttributeMapper extends AttributeMapper {
-    private final List<Attribute> attributes;
-
-    LDAPConstantAttributeMapper(final AttributeDescription attributeName,
-            final Object... attributeValues) {
-        if (attributeValues.length == 1) {
-            attributes = singletonList(singletonAttribute(attributeName, attributeValues[0]));
-        } else {
-            attributes =
-                    singletonList((Attribute) new LinkedAttribute(attributeName, attributeValues));
-        }
-    }
-
-    @Override
-    void getLDAPAttributes(final Context c, final JsonPointer jsonAttribute,
-            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) {
-        // This attribute mapper cannot handle the provided filter component.
-        h.handleResult(null);
-    }
-
-    @Override
-    void toJSON(final Context c, final Entry e, final ResultHandler<Map<String, Object>> h) {
-        h.handleResult(Collections.<String, Object> emptyMap());
-    }
-
-    @Override
-    void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) {
-        h.handleResult(attributes);
-    }
-
-}
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/MVCCStrategy.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/MVCCStrategy.java
index 944bce5..5e33383 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/MVCCStrategy.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/MVCCStrategy.java
@@ -35,17 +35,6 @@
     }
 
     /**
-     * Retrieves the revision value (etag) from the provided LDAP entry.
-     *
-     * @param c
-     *            The context.
-     * @param entry
-     *            The LDAP entry.
-     * @return The revision value.
-     */
-    abstract String getRevisionFromEntry(Context c, Entry entry);
-
-    /**
      * Adds the name of any LDAP attribute required by this MVCC strategy to the
      * provided set.
      *
@@ -57,4 +46,15 @@
      */
     abstract void getLDAPAttributes(Context c, Set<String> ldapAttributes);
 
+    /**
+     * Retrieves the revision value (etag) from the provided LDAP entry.
+     *
+     * @param c
+     *            The context.
+     * @param entry
+     *            The LDAP entry.
+     * @return The revision value.
+     */
+    abstract String getRevisionFromEntry(Context c, Entry entry);
+
 }
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java
index f8e9ad6..323f962 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java
@@ -18,6 +18,7 @@
 
 import java.util.Set;
 
+import org.forgerock.json.resource.ResourceException;
 import org.forgerock.opendj.ldap.DN;
 import org.forgerock.opendj.ldap.Entry;
 import org.forgerock.opendj.ldap.requests.SearchRequest;
@@ -89,7 +90,10 @@
      * @param entry
      *            The LDAP entry whose DN and resource ID attributes are to be
      *            set.
+     * @throws ResourceException
+     *             If the resource ID cannot be determined.
      */
-    abstract void setResourceId(Context c, DN baseDN, String resourceId, Entry entry);
+    abstract void setResourceId(Context c, DN baseDN, String resourceId, Entry entry)
+            throws ResourceException;
 
 }
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
new file mode 100644
index 0000000..69d421e
--- /dev/null
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
@@ -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 2012-2013 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static org.forgerock.opendj.rest2ldap.Utils.accumulate;
+import static org.forgerock.opendj.rest2ldap.Utils.toLowerCase;
+import static org.forgerock.opendj.rest2ldap.Utils.transform;
+
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+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.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;
+
+/**
+ * An attribute mapper which maps JSON objects to LDAP attributes.
+ */
+public final class ObjectAttributeMapper extends AttributeMapper {
+
+    private static final class Mapping {
+        private final AttributeMapper mapper;
+        private final String name;
+
+        private Mapping(final String name, final AttributeMapper mapper) {
+            this.name = name;
+            this.mapper = mapper;
+        }
+    }
+
+    private final Map<String, Mapping> mappings = new LinkedHashMap<String, Mapping>();
+
+    ObjectAttributeMapper() {
+        // Nothing to do.
+    }
+
+    /**
+     * Creates a mapping for an attribute contained in the JSON object.
+     *
+     * @param name
+     *            The name of the JSON attribute to be mapped.
+     * @param mapper
+     *            The attribute mapper responsible for mapping the JSON
+     *            attribute to LDAP attribute(s).
+     * @return A reference to this attribute mapper.
+     */
+    public ObjectAttributeMapper attribute(final String name, final AttributeMapper mapper) {
+        mappings.put(toLowerCase(name), new Mapping(name, mapper));
+        return this;
+    }
+
+    @Override
+    void getLDAPAttributes(final Context c, final JsonPointer jsonAttribute,
+            final Set<String> ldapAttributes) {
+        if (jsonAttribute.isEmpty()) {
+            // Request all subordinate mappings.
+            for (final Mapping mapping : mappings.values()) {
+                mapping.mapper.getLDAPAttributes(c, jsonAttribute, ldapAttributes);
+            }
+        } else {
+            // Request single subordinate mapping.
+            final Mapping mapping = getMapping(jsonAttribute);
+            if (mapping != null) {
+                final JsonPointer relativePointer = jsonAttribute.relativePointer();
+                mapping.mapper.getLDAPAttributes(c, 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);
+        if (mapping != null) {
+            final JsonPointer relativePointer = jsonAttribute.relativePointer();
+            mapping.mapper.getLDAPFilter(c, type, relativePointer, 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.
+            h.handleResult(c.getConfig().falseFilter());
+        }
+    }
+
+    boolean isEmpty() {
+        return mappings.isEmpty();
+    }
+
+    @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.
+        final ResultHandler<Map.Entry<String, JsonValue>> handler =
+                accumulate(mappings.size(), transform(
+                        new Function<List<Map.Entry<String, JsonValue>>, JsonValue, Void>() {
+                            @Override
+                            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.
+                                    return null;
+                                } else {
+                                    // 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) {
+                                        result.put(e.getKey(), e.getValue().getObject());
+                                    }
+                                    return new JsonValue(result);
+                                }
+                            }
+                        }, h));
+
+        for (final Mapping mapping : mappings.values()) {
+            mapping.mapper.toJSON(c, e, transform(
+                    new Function<JsonValue, Map.Entry<String, JsonValue>, Void>() {
+                        @Override
+                        public Map.Entry<String, JsonValue> apply(final JsonValue value,
+                                final Void p) {
+                            return new SimpleImmutableEntry<String, JsonValue>(mapping.name, value);
+                        }
+                    }, handler));
+        }
+    }
+
+    @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.
+        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 + "'"));
+                        return;
+                    }
+                }
+            } else {
+                h.handleError(new BadRequestException("JSON object expected"));
+                return;
+            }
+        }
+
+        // Accumulate the results of the subordinate mappings.
+        final ResultHandler<List<Attribute>> handler =
+                accumulate(mappings.size(), transform(
+                        new Function<List<List<Attribute>>, List<Attribute>, Void>() {
+                            @Override
+                            public List<Attribute> apply(final List<List<Attribute>> value,
+                                    final Void p) {
+                                switch (value.size()) {
+                                case 0:
+                                    return Collections.emptyList();
+                                case 1:
+                                    return value.get(0) != null ? value.get(0) : Collections
+                                            .<Attribute> emptyList();
+                                default:
+                                    final List<Attribute> attributes =
+                                            new ArrayList<Attribute>(value.size());
+                                    for (final List<Attribute> a : value) {
+                                        attributes.addAll(a);
+                                    }
+                                    return attributes;
+                                }
+                            }
+                        }, h));
+
+        // 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);
+            }
+        }
+
+        // Invoke mappings for which there were no values provided.
+        for (final Mapping mapping : missingMappings.values()) {
+            mapping.mapper.toLDAP(c, null, handler);
+        }
+    }
+
+    private Mapping getMapping(final JsonPointer jsonAttribute) {
+        return jsonAttribute.isEmpty() ? null : getMapping(jsonAttribute.get(0));
+    }
+
+    private Mapping getMapping(final String jsonAttribute) {
+        return mappings.get(toLowerCase(jsonAttribute));
+    }
+
+}
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
index 843e895..fa82214 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
@@ -21,13 +21,14 @@
 import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.USE_READ_ENTRY_CONTROLS;
 import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull;
 
-import java.util.Arrays;
-import java.util.Collection;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
 
+import org.forgerock.json.resource.BadRequestException;
 import org.forgerock.json.resource.CollectionResourceProvider;
+import org.forgerock.json.resource.ResourceException;
+import org.forgerock.opendj.ldap.Attribute;
 import org.forgerock.opendj.ldap.AttributeDescription;
 import org.forgerock.opendj.ldap.ByteString;
 import org.forgerock.opendj.ldap.ConnectionFactory;
@@ -52,19 +53,45 @@
      * A builder for incrementally constructing LDAP resource collections.
      */
     public static final class Builder {
+        private final List<Attribute> additionalLDAPAttributes = new LinkedList<Attribute>();
         private DN baseDN; // TODO: support template variables.
         private ConnectionFactory factory;
         private final Filter falseFilter = Filter.present("1.1");
-        private final List<AttributeMapper> mappers = new LinkedList<AttributeMapper>();
         private MVCCStrategy mvccStrategy;
         private NameStrategy nameStrategy;
         private ReadOnUpdatePolicy readOnUpdatePolicy = USE_READ_ENTRY_CONTROLS;
+        private final ObjectAttributeMapper rootMapper = new ObjectAttributeMapper();
         private Schema schema = Schema.getDefaultSchema();
         private Filter trueFilter = Filter.objectClassPresent();
 
         Builder() {
             useEtagAttribute();
-            useServerEntryUUIDNaming("uid");
+            useClientDNNaming("uid");
+        }
+
+        public Builder additionalLDAPAttribute(final Attribute attribute) {
+            additionalLDAPAttributes.add(attribute);
+            return this;
+        }
+
+        public Builder additionalLDAPAttribute(final String attribute, final Object... values) {
+            additionalLDAPAttributes.add(new LinkedAttribute(attribute, values));
+            return this;
+        }
+
+        /**
+         * Creates a mapping for the named JSON attribute.
+         *
+         * @param name
+         *            The name of the JSON attribute to be mapped.
+         * @param mapper
+         *            The attribute mapper responsible for mapping the JSON
+         *            attribute to LDAP attribute(s).
+         * @return A reference to this builder.
+         */
+        public Builder attribute(final String name, final AttributeMapper mapper) {
+            rootMapper.attribute(name, mapper);
+            return this;
         }
 
         public Builder baseDN(final DN dn) {
@@ -82,12 +109,12 @@
         public CollectionResourceProvider build() {
             ensureNotNull(factory);
             ensureNotNull(baseDN);
-            if (mappers.isEmpty()) {
+            if (rootMapper.isEmpty()) {
                 throw new IllegalStateException("No mappings provided");
             }
-            return new LDAPCollectionResourceProvider(baseDN, mapOf(mappers), factory,
-                    nameStrategy, mvccStrategy, new Config(trueFilter, falseFilter,
-                            readOnUpdatePolicy, schema));
+            return new LDAPCollectionResourceProvider(baseDN, rootMapper, factory, nameStrategy,
+                    mvccStrategy, new Config(trueFilter, falseFilter, readOnUpdatePolicy, schema),
+                    additionalLDAPAttributes);
         }
 
         public Builder factory(final ConnectionFactory factory) {
@@ -109,18 +136,6 @@
             return this;
         }
 
-        public Builder map(final AttributeMapper... mappers) {
-            ensureNotNull(mappers);
-            this.mappers.addAll(Arrays.asList(mappers));
-            return this;
-        }
-
-        public Builder map(final Collection<AttributeMapper> mappers) {
-            ensureNotNull(mappers);
-            this.mappers.addAll(mappers);
-            return this;
-        }
-
         /**
          * Sets the policy which should be used in order to read an entry before
          * it is deleted, or after it is added or modified.
@@ -303,9 +318,15 @@
 
         @Override
         void setResourceId(final Context c, final DN baseDN, final String resourceId,
-                final Entry entry) {
-            entry.setName(baseDN.child(rdn(resourceId)));
-            entry.addAttribute(new LinkedAttribute(attribute, ByteString.valueOf(resourceId)));
+                final Entry entry) throws ResourceException {
+            if (resourceId != null) {
+                entry.setName(baseDN.child(rdn(resourceId)));
+                entry.addAttribute(new LinkedAttribute(attribute, ByteString.valueOf(resourceId)));
+            } else if (entry.getAttribute(attribute) != null) {
+                entry.setName(baseDN.child(rdn(entry.parseAttribute(attribute).asString())));
+            } else {
+                throw new BadRequestException("Unable to set the resource ID");
+            }
         }
 
         private RDN rdn(final String resourceId) {
@@ -318,58 +339,20 @@
         return new Builder();
     }
 
-    public static SimpleAttributeMapper map(final AttributeDescription attribute) {
-        return map(attribute.toString(), attribute);
+    public static AttributeMapper constant(final Object value) {
+        return new JSONConstantAttributeMapper(value);
     }
 
-    public static SimpleAttributeMapper map(final String attribute) {
-        return map(attribute, attribute);
+    public static ObjectAttributeMapper object() {
+        return new ObjectAttributeMapper();
     }
 
-    public static SimpleAttributeMapper map(final String jsonAttribute,
-            final AttributeDescription ldapAttribute) {
-        return new SimpleAttributeMapper(jsonAttribute, ldapAttribute);
+    public static SimpleAttributeMapper simple(final AttributeDescription attribute) {
+        return new SimpleAttributeMapper(attribute);
     }
 
-    public static SimpleAttributeMapper map(final String jsonAttribute, final String ldapAttribute) {
-        return map(jsonAttribute, AttributeDescription.valueOf(ldapAttribute));
-    }
-
-    public static AttributeMapper mapAllExcept(final String... attributes) {
-        return new DefaultAttributeMapper().excludeAttribute(attributes);
-    }
-
-    public static AttributeMapper mapAllOf(final String... attributes) {
-        return new DefaultAttributeMapper().includeAttribute(attributes);
-    }
-
-    public static AttributeMapper mapComplex(final String jsonAttribute,
-            final AttributeMapper... mappers) {
-        return mapComplex(jsonAttribute, Arrays.asList(mappers));
-    }
-
-    public static AttributeMapper mapComplex(final String jsonAttribute,
-            final Collection<AttributeMapper> mappers) {
-        return new ComplexAttributeMapper(jsonAttribute, mapOf(mappers));
-    }
-
-    public static AttributeMapper mapJSONConstant(final String attribute,
-            final Object attributeValue) {
-        return new JSONConstantAttributeMapper(attribute, attributeValue);
-    }
-
-    public static AttributeMapper mapLDAPConstant(final AttributeDescription attribute,
-            final Object... attributeValues) {
-        return new LDAPConstantAttributeMapper(attribute, attributeValues);
-    }
-
-    public static AttributeMapper mapLDAPConstant(final String attribute,
-            final Object... attributeValues) {
-        return mapLDAPConstant(AttributeDescription.valueOf(attribute), attributeValues);
-    }
-
-    private static AttributeMapper mapOf(final Collection<AttributeMapper> mappers) {
-        return new CompositeAttributeMapper(mappers);
+    public static SimpleAttributeMapper simple(final String attribute) {
+        return simple(AttributeDescription.valueOf(attribute));
     }
 
     private Rest2LDAP() {
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
index a8375e3..9546963 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
@@ -19,23 +19,23 @@
 import static java.util.Collections.emptySet;
 import static java.util.Collections.singleton;
 import static java.util.Collections.singletonList;
-import static java.util.Collections.singletonMap;
 import static org.forgerock.opendj.ldap.Functions.fixedFunction;
 import static org.forgerock.opendj.rest2ldap.Utils.byteStringToJson;
 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.Utils.toLowerCase;
+import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_ONLY;
+import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE;
 
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 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,37 +46,23 @@
 import org.forgerock.opendj.ldap.LinkedAttribute;
 
 /**
- * An attribute mapper which maps a single JSON attribute to a single LDAP
- * attribute.
+ * An attribute mapper which provides a simple mapping from a JSON value to a
+ * single LDAP attribute.
  */
 public final class SimpleAttributeMapper extends AttributeMapper {
-
     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 forceSingleValued = false;
-
-    // private boolean isReadOnly = false;
-    private final String jsonAttributeName;
+    private boolean isIgnoreUpdates = true;
+    private boolean isRequired = false;
+    private boolean isSingleValued = false;
     private final AttributeDescription ldapAttributeName;
-    private final String normalizedJsonAttributeName;
+    private WritabilityPolicy writabilityPolicy = READ_WRITE;
 
-    /**
-     * Creates a new simple attribute mapper which maps a single LDAP attribute
-     * to an entry.
-     *
-     * @param jsonAttributeName
-     *            The name of the simple JSON attribute.
-     * @param ldapAttributeName
-     *            The name of the LDAP attribute.
-     */
-    SimpleAttributeMapper(final String jsonAttributeName,
-            final AttributeDescription ldapAttributeName) {
-        this.jsonAttributeName = jsonAttributeName;
+    SimpleAttributeMapper(final AttributeDescription ldapAttributeName) {
         this.ldapAttributeName = ldapAttributeName;
-        this.normalizedJsonAttributeName = toLowerCase(jsonAttributeName);
     }
 
     /**
@@ -133,70 +119,118 @@
     }
 
     /**
-     * Prevents the LDAP attribute from being updated.
+     * Indicates whether or not an attempt to update the LDAP attribute should
+     * be ignored when the update is incompatible with the writability policy.
+     * The default is {@code true}.
      *
-     * @param readOnly
-     *            {@code true} if the LDAP attribute is read-only.
+     * @param ignore
+     *            {@code true} an attempt to update the LDAP attribute should be
+     *            ignored.
      * @return This attribute mapper.
      */
-    public SimpleAttributeMapper readOnly(final boolean readOnly) {
-        // TODO: enforcement policy: ignore, warn, or reject.
-        // this.isReadOnly = readOnly;
+    public SimpleAttributeMapper ignoreUpdates(final boolean ignore) {
+        this.isIgnoreUpdates = ignore;
+        return this;
+    }
+
+    /**
+     * Indicates that the LDAP attribute is mandatory and must be provided
+     * during create requests. The default is {@code false}.
+     *
+     * @param isRequired
+     *            {@code true} if the LDAP attribute is mandatory and must be
+     *            provided during create requests.
+     * @return This attribute mapper.
+     */
+    public SimpleAttributeMapper required(final boolean isRequired) {
+        this.isRequired = isRequired;
         return this;
     }
 
     /**
      * Forces a multi-valued LDAP attribute to be represented as a single-valued
-     * JSON value, rather than an array of values.
+     * JSON value, rather than an array of values. The default is {@code false}.
      *
-     * @param singleValued
+     * @param isSingleValued
      *            {@code true} if the LDAP attribute should be treated as a
      *            single-valued attribute.
      * @return This attribute mapper.
      */
-    public SimpleAttributeMapper singleValued(final boolean singleValued) {
-        this.forceSingleValued = singleValued;
+    public SimpleAttributeMapper singleValued(final boolean isSingleValued) {
+        this.isSingleValued = isSingleValued;
+        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) {
-        if (jsonAttribute.isEmpty() || matches(jsonAttribute)) {
-            ldapAttributes.add(ldapAttributeName.toString());
-        }
+        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 (matches(jsonAttribute)) {
+        if (jsonAttribute.isEmpty()) {
             h.handleResult(toFilter(c, type, ldapAttributeName.toString(), valueAssertion));
         } else {
-            // This attribute mapper cannot handle the provided filter component.
-            h.handleResult(null);
+            // This attribute mapper does not support partial filtering.
+            h.handleResult(c.getConfig().falseFilter());
         }
     }
 
     @Override
-    void toJSON(final Context c, final Entry e, final ResultHandler<Map<String, Object>> h) {
+    void toJSON(final Context c, final Entry e, final ResultHandler<JsonValue> h) {
         final Function<ByteString, ?, Void> f =
                 decoder == null ? fixedFunction(byteStringToJson(), ldapAttributeName) : decoder;
         final Object value;
-        if (forceSingleValued || ldapAttributeName.getAttributeType().isSingleValue()) {
+        if (isSingleValued || ldapAttributeName.getAttributeType().isSingleValue()) {
             value = e.parseAttribute(ldapAttributeName).as(f, defaultJSONValue);
         } else {
             value = e.parseAttribute(ldapAttributeName).asSetOf(f, defaultJSONValues);
         }
-        h.handleResult(singletonMap(jsonAttributeName, value));
+        h.handleResult(new JsonValue(value));
     }
 
     @Override
     void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) {
-        if (v.isMap()) {
-            final Object value = v.get(jsonAttributeName).getObject();
-            try {
-                final List<Attribute> result;
+        try {
+            final List<Attribute> result;
+            if (v == null || v.isNull()) {
+                if (isRequired()) {
+                    // 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() && isSingleValued()) {
+                // FIXME: improve error message.
+                throw new BadRequestException("expected single value, but got multiple values");
+            } else if (isCreate()) {
+                if (isIgnoreUpdates) {
+                    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) {
                     final Function<Object, ByteString, Void> f =
                             encoder != null ? encoder : fixedFunction(jsonToByteString(),
@@ -209,21 +243,27 @@
                 } else {
                     result = emptyList();
                 }
-                h.handleResult(result);
-            } catch (final Exception e) {
-                // FIXME: improve error message.
-                h.handleError(new BadRequestException("The field " + jsonAttributeName
-                        + " is invalid"));
-                return;
             }
-        } else {
-            h.handleResult(Collections.<Attribute> 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 matches(final JsonPointer jsonAttribute) {
-        return !jsonAttribute.isEmpty()
-                && toLowerCase(jsonAttribute.get(0)).equals(normalizedJsonAttributeName);
+    private boolean isCreate() {
+        return writabilityPolicy != READ_ONLY
+                && ldapAttributeName.getAttributeType().isNoUserModification();
+    }
+
+    private boolean isRequired() {
+        return isRequired && defaultJSONValue == null;
+    }
+
+    private boolean isSingleValued() {
+        return isSingleValued || ldapAttributeName.getAttributeType().isSingleValue();
     }
 
 }
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
index e230bf4..559c92f 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
@@ -103,11 +103,8 @@
                     } else if (syntax.equals(getGeneralizedTimeSyntax())) {
                         return printDateTime(byteStringToGeneralizedTime().apply(value, null)
                                 .toCalendar());
-                    } else if (syntax.isHumanReadable()) {
-                        return byteStringToString().apply(value, null);
                     } else {
-                        // Base 64 encoded binary.
-                        return value.toBase64String();
+                        return byteStringToString().apply(value, null);
                     }
                 }
             };
@@ -121,11 +118,8 @@
                         if (syntax.equals(getGeneralizedTimeSyntax())) {
                             return ByteString.valueOf(GeneralizedTime.valueOf(parseDateTime(value
                                     .toString())));
-                        } else if (syntax.isHumanReadable()) {
-                            return ByteString.valueOf(value);
                         } else {
-                            // Base 64 encoded binary.
-                            return ByteString.valueOfBase64(value.toString());
+                            return ByteString.valueOf(value);
                         }
                     } else {
                         throw new IllegalArgumentException("Unrecognized type of JSON value: "
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/WritabilityPolicy.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/WritabilityPolicy.java
new file mode 100644
index 0000000..2b062bd
--- /dev/null
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/WritabilityPolicy.java
@@ -0,0 +1,41 @@
+/*
+ * 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;
+
+/**
+ * The writability policy determines whether or not an attribute supports
+ * updates.
+ */
+public enum WritabilityPolicy {
+    /**
+     * The attribute may be provided when creating a new resource, but cannot be
+     * modified afterwards.
+     */
+    CREATE_ONLY,
+
+    /**
+     * The attribute cannot be provided when creating a new resource, nor
+     * modified afterwards.
+     */
+    READ_ONLY,
+
+    /**
+     * The attribute may be provided when creating a new resource, and modified
+     * afterwards.
+     */
+    READ_WRITE;
+}
diff --git a/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Example.java b/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Example.java
index 8a042c8..070a2f5 100644
--- a/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Example.java
+++ b/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Example.java
@@ -18,7 +18,12 @@
 
 import static org.forgerock.json.resource.Resources.newInternalConnectionFactory;
 import static org.forgerock.opendj.ldap.Connections.newAuthenticatedConnectionFactory;
-import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.builder;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.constant;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.object;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.simple;
+import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.CREATE_ONLY;
+import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_ONLY;
 
 import java.util.Arrays;
 import java.util.logging.Logger;
@@ -27,7 +32,6 @@
 import org.forgerock.json.resource.Router;
 import org.forgerock.json.resource.servlet.HttpServlet;
 import org.forgerock.opendj.ldap.ConnectionFactory;
-import org.forgerock.opendj.ldap.Functions;
 import org.forgerock.opendj.ldap.LDAPConnectionFactory;
 import org.forgerock.opendj.ldap.requests.Requests;
 import org.glassfish.grizzly.http.server.HttpServer;
@@ -62,26 +66,29 @@
 
         // Create user resource.
         CollectionResourceProvider users =
-                builder().factory(ldapFactory).baseDN("ou=people,dc=example,dc=com").map(
-                        mapJSONConstant("schemas", Arrays.asList("urn:scim:schemas:core:1.0")),
-                        map("id", "entryUUID").singleValued(true),
-                        map("externalId", "uid").singleValued(true),
-                        map("userName", "mail").singleValued(true),
-                        map("displayName", "cn").singleValued(true),
-                        mapComplex("name", map("givenName", "givenName").singleValued(true), map(
-                                "familyName", "sn").singleValued(true)),
-                        mapComplex("contactInformation", map("telephoneNumber").decoder(
-                                Functions.byteStringToString()).encoder(
-                                Functions.objectToByteString()).singleValued(true), map(
-                                "emailAddress", "mail").singleValued(true)),
-                        mapLDAPConstant("objectClass", "top", "person", "organizationalPerson", "inetOrgPerson"))
-                        .build();
+                builder().factory(ldapFactory).baseDN("ou=people,dc=example,dc=com")
+                    .attribute("schemas", constant(Arrays.asList("urn:scim:schemas:core:1.0")))
+                    .attribute("id", simple("uid").singleValued(true).required(true).writability(CREATE_ONLY))
+                    .attribute("rev", simple("etag").singleValued(true).writability(READ_ONLY))
+                    .attribute("userName", simple("mail").singleValued(true).writability(READ_ONLY))
+                    .attribute("displayName", simple("cn").singleValued(true).required(true))
+                    .attribute("name", object()
+                            .attribute("givenName", simple("givenName").singleValued(true))
+                            .attribute("familyName", simple("sn").singleValued(true).required(true)))
+                    .attribute("contactInformation", object()
+                            .attribute("telephoneNumber", simple("telephoneNumber").singleValued(true))
+                            .attribute("emailAddress", simple("mail").singleValued(true)))
+                    .additionalLDAPAttribute("objectClass", "top", "person", "organizationalPerson", "inetOrgPerson")
+                    .build();
         router.addRoute("/users", users);
 
         // Create group resource.
         CollectionResourceProvider groups =
-                builder().factory(ldapFactory).baseDN("ou=groups,dc=example,dc=com").map(
-                        mapAllOf("cn", "ou", "description", "uniquemember")).build();
+                builder().factory(ldapFactory).baseDN("ou=groups,dc=example,dc=com")
+                    .attribute("cn", simple("cn").singleValued(true))
+                    .attribute("description", simple("description"))
+                    .attribute("member", simple("uniquemember"))
+                    .build();
         router.addRoute("/groups", groups);
 
         final org.forgerock.json.resource.ConnectionFactory resourceFactory =

--
Gitblit v1.10.0