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

Matthew Swift
14.44.2013 a2bc68638f55ae0ad7b9e3a04c7a3c02d01384f8
Partial fix for OPENDJ-758 : Implement configurable update policy for simple and default mappers

In addition, simplify attribute mapping APIs.
4 files deleted
2 files added
10 files modified
1444 ■■■■■ changed files
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java 49 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ComplexAttributeMapper.java 114 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/CompositeAttributeMapper.java 189 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java 28 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DefaultAttributeMapper.java 172 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java 84 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java 118 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPConstantAttributeMapper.java 74 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/MVCCStrategy.java 22 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java 6 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java 217 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java 121 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java 156 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java 10 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/WritabilityPolicy.java 41 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Example.java 43 ●●●●● patch | view | raw | blame | history
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.
     */
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ComplexAttributeMapper.java
File was deleted
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/CompositeAttributeMapper.java
File was deleted
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
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DefaultAttributeMapper.java
File was deleted
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,
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);
    }
}
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPConstantAttributeMapper.java
File was deleted
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);
}
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;
}
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
New file
@@ -0,0 +1,217 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 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));
    }
}
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() {
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();
    }
}
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: "
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/WritabilityPolicy.java
New file
@@ -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;
}
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 =