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

Matthew Swift
05.05.2013 b8f496a9076253f7dc672d361b7bd65d4a110a29
Partial fix for OPENDJ-699: Implement DN reference mapping

* add ReferenceAttributeMapper class which includes read support and no filtering.
1 files added
6 files modified
615 ■■■■■ changed files
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java 39 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java 194 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java 10 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java 177 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java 109 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java 7 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java 79 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java
@@ -15,13 +15,18 @@
 */
package org.forgerock.opendj.rest2ldap;
import java.io.Closeable;
import java.util.concurrent.atomic.AtomicReference;
import org.forgerock.json.resource.ServerContext;
import org.forgerock.opendj.ldap.Connection;
/**
 * Common context information passed to containers and mappers.
 */
final class Context {
final class Context implements Closeable {
    private final Config config;
    private final AtomicReference<Connection> connection = new AtomicReference<Connection>();
    private final ServerContext context;
    Context(final Config config, final ServerContext context) {
@@ -30,20 +35,32 @@
    }
    /**
     * Returns the common configuration options.
     *
     * @return The common configuration options.
     * {@inheritDoc}
     */
    public Config getConfig() {
    @Override
    public void close() {
        final Connection c = connection.getAndSet(null);
        if (c != null) {
            c.close();
        }
    }
    Config getConfig() {
        return config;
    }
    /**
     * Returns the commons REST server context passed in with the REST request.
     *
     * @return The commons REST server context passed in with the REST request.
     */
    public ServerContext getServerContext() {
    Connection getConnection() {
        return connection.get();
    }
    ServerContext getServerContext() {
        return context;
    }
    void setConnection(final Connection connection) {
        if (!this.connection.compareAndSet(null, connection)) {
            throw new IllegalStateException("LDAP connection obtained multiple times");
        }
    }
}
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
@@ -19,6 +19,7 @@
import static org.forgerock.opendj.ldap.Filter.alwaysTrue;
import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS;
import static org.forgerock.opendj.rest2ldap.Utils.accumulate;
import static org.forgerock.opendj.rest2ldap.Utils.adapt;
import static org.forgerock.opendj.rest2ldap.Utils.toFilter;
import static org.forgerock.opendj.rest2ldap.Utils.transform;
@@ -52,25 +53,17 @@
import org.forgerock.json.resource.ServerContext;
import org.forgerock.json.resource.UncategorizedException;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.opendj.ldap.AssertionFailureException;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.AuthenticationException;
import org.forgerock.opendj.ldap.AuthorizationException;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.ConnectionException;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.DecodeException;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.EntryNotFoundException;
import org.forgerock.opendj.ldap.ErrorResultException;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Function;
import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.TimeoutResultException;
import org.forgerock.opendj.ldap.controls.PostReadRequestControl;
import org.forgerock.opendj.ldap.controls.PostReadResponseControl;
import org.forgerock.opendj.ldap.controls.PreReadResponseControl;
@@ -87,38 +80,51 @@
 * resource collection to LDAP entries beneath a base DN.
 */
final class LDAPCollectionResourceProvider implements CollectionResourceProvider {
    private <V> ResultHandler<V> wrap(final Context c, final ResultHandler<V> handler) {
        return new ResultHandler<V>() {
            @Override
            public void handleError(ResourceException error) {
                c.close();
                handler.handleError(error);
            }
    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;
            @Override
            public void handleResult(V result) {
                c.close();
                handler.handleResult(result);
            }
        };
    }
        AbstractRequestCompletionHandler(final Connection connection, final H resultHandler) {
            this.connection = connection;
            this.resultHandler = resultHandler;
        }
    private QueryResultHandler wrap(final Context c, final QueryResultHandler handler) {
        return new QueryResultHandler() {
            @Override
            public void handleError(ResourceException error) {
                c.close();
                handler.handleError(error);
            }
        @Override
        public final void handleErrorResult(final ErrorResultException error) {
            connection.close();
            resultHandler.handleErrorResult(error);
        }
            @Override
            public boolean handleResource(Resource resource) {
                return handler.handleResource(resource);
            }
        @Override
        public final void handleResult(final R result) {
            connection.close();
            resultHandler.handleResult(result);
        }
            @Override
            public void handleResult(QueryResult result) {
                c.close();
                handler.handleResult(result);
            }
        };
    }
    private abstract class ConnectionCompletionHandler<R> implements
            org.forgerock.opendj.ldap.ResultHandler<Connection> {
        private final org.forgerock.opendj.ldap.ResultHandler<? super R> resultHandler;
        private final Context c;
        ConnectionCompletionHandler(
        ConnectionCompletionHandler(final Context c,
                final org.forgerock.opendj.ldap.ResultHandler<? super R> resultHandler) {
            this.c = c;
            this.resultHandler = resultHandler;
        }
@@ -128,36 +134,12 @@
        }
        @Override
        public abstract void handleResult(Connection connection);
    }
    private final class RequestCompletionHandler<R> extends
            AbstractRequestCompletionHandler<R, org.forgerock.opendj.ldap.ResultHandler<? super R>> {
        RequestCompletionHandler(final Connection connection,
                final org.forgerock.opendj.ldap.ResultHandler<? super R> resultHandler) {
            super(connection, resultHandler);
        }
    }
    private final class SearchRequestCompletionHandler extends
            AbstractRequestCompletionHandler<Result, SearchResultHandler> implements
            SearchResultHandler {
        SearchRequestCompletionHandler(final Connection connection,
                final SearchResultHandler resultHandler) {
            super(connection, resultHandler);
        public final void handleResult(Connection connection) {
            c.setConnection(connection);
            chain();
        }
        @Override
        public final boolean handleEntry(final SearchResultEntry entry) {
            return resultHandler.handleEntry(entry);
        }
        @Override
        public final boolean handleReference(final SearchResultReference reference) {
            return resultHandler.handleReference(reference);
        }
        abstract void chain();
    }
@@ -201,10 +183,12 @@
    public void createInstance(final ServerContext context, final CreateRequest request,
            final ResultHandler<Resource> handler) {
        final Context c = wrap(context);
        final ResultHandler<Resource> h = wrap(c, handler);
        attributeMapper.toLDAP(c, request.getContent(), new ResultHandler<List<Attribute>>() {
            @Override
            public void handleError(final ResourceException error) {
                handler.handleError(error);
                h.handleError(error);
            }
            @Override
@@ -220,14 +204,14 @@
                    nameStrategy.setResourceId(c, getBaseDN(c), request.getNewResourceId(),
                            addRequest);
                } catch (ResourceException e) {
                    handler.handleError(e);
                    h.handleError(e);
                    return;
                }
                if (config.readOnUpdatePolicy() == CONTROLS) {
                    final String[] attributes = getLDAPAttributes(c, request.getFieldFilters());
                    addRequest.addControl(PostReadRequestControl.newControl(false, attributes));
                }
                applyUpdate(c, addRequest, handler);
                applyUpdate(c, addRequest, h);
            }
        });
    }
@@ -248,6 +232,7 @@
    public void queryCollection(final ServerContext context, final QueryRequest request,
            final QueryResultHandler handler) {
        final Context c = wrap(context);
        final QueryResultHandler h = wrap(c, handler);
        // The handler which will be invoked for each LDAP search result.
        final SearchResultHandler searchHandler = new SearchResultHandler() {
@@ -280,7 +265,7 @@
                    @Override
                    public void handleResult(final JsonValue result) {
                        final Resource resource = new Resource(id, revision, result);
                        handler.handleResource(resource);
                        h.handleResource(resource);
                        pendingResourceCount.decrementAndGet();
                        completeIfNecessary();
                    }
@@ -293,7 +278,7 @@
            @Override
            public void handleErrorResult(final ErrorResultException error) {
                pendingResult.compareAndSet(null, adaptErrorResult(error));
                pendingResult.compareAndSet(null, adapt(error));
                completeIfNecessary();
            }
@@ -319,9 +304,9 @@
                    final ResourceException result = pendingResult.get();
                    if (result != null && resultSent.compareAndSet(false, true)) {
                        if (result == SUCCESS) {
                            handler.handleResult(new QueryResult());
                            h.handleResult(new QueryResult());
                        } else {
                            handler.handleError(result);
                            h.handleError(result);
                        }
                    }
                }
@@ -332,23 +317,20 @@
        final ResultHandler<Filter> filterHandler = new ResultHandler<Filter>() {
            @Override
            public void handleError(final ResourceException error) {
                handler.handleError(error);
                h.handleError(error);
            }
            @Override
            public void handleResult(final Filter ldapFilter) {
                // Avoid performing a search if the filter could not be mapped or if it will never match.
                if (ldapFilter == null || ldapFilter == alwaysFalse()) {
                    handler.handleResult(new QueryResult());
                    h.handleResult(new QueryResult());
                } else {
                    final ConnectionCompletionHandler<Result> outerHandler =
                            new ConnectionCompletionHandler<Result>(searchHandler) {
                            new ConnectionCompletionHandler<Result>(c, searchHandler) {
                                @Override
                                public void handleResult(final Connection connection) {
                                    final SearchRequestCompletionHandler innerHandler =
                                            new SearchRequestCompletionHandler(connection,
                                                    searchHandler);
                                void chain() {
                                    final String[] attributes =
                                            getLDAPAttributes(c, request.getFieldFilters());
                                    final SearchRequest request =
@@ -357,7 +339,7 @@
                                                            .alwaysTrue() ? Filter
                                                            .objectClassPresent() : ldapFilter,
                                                    attributes);
                                    connection.searchAsync(request, null, innerHandler);
                                    c.getConnection().searchAsync(request, null, searchHandler);
                                }
                            };
@@ -374,36 +356,33 @@
    public void readInstance(final ServerContext context, final String resourceId,
            final ReadRequest request, final ResultHandler<Resource> handler) {
        final Context c = wrap(context);
        final ResultHandler<Resource> h = wrap(c, handler);
        // The handler which will be invoked for the LDAP search result.
        final org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry> searchHandler =
                new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() {
                    @Override
                    public void handleErrorResult(final ErrorResultException error) {
                        handler.handleError(adaptErrorResult(error));
                        h.handleError(adapt(error));
                    }
                    @Override
                    public void handleResult(final SearchResultEntry entry) {
                        adaptEntry(c, entry, handler);
                        adaptEntry(c, entry, h);
                    }
                };
        // The handler which will be invoked
        final ConnectionCompletionHandler<SearchResultEntry> outerHandler =
                new ConnectionCompletionHandler<SearchResultEntry>(searchHandler) {
                new ConnectionCompletionHandler<SearchResultEntry>(c, searchHandler) {
                    @Override
                    public void handleResult(final Connection connection) {
                        final RequestCompletionHandler<SearchResultEntry> innerHandler =
                                new RequestCompletionHandler<SearchResultEntry>(connection,
                                        searchHandler);
                    void chain() {
                        final String[] attributes = getLDAPAttributes(c, request.getFieldFilters());
                        final SearchRequest request =
                                nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId)
                                        .addAttribute(attributes);
                        connection.searchSingleEntryAsync(request, innerHandler);
                        c.getConnection().searchSingleEntryAsync(request, searchHandler);
                    }
                };
@@ -429,56 +408,15 @@
        }, handler));
    }
    /**
     * Adapts an LDAP result code to a resource exception.
     *
     * @param error
     *            The LDAP error that should be adapted.
     * @return The equivalent resource exception.
     */
    private ResourceException adaptErrorResult(final ErrorResultException error) {
        int resourceResultCode;
        try {
            throw error;
        } catch (final AssertionFailureException e) {
            resourceResultCode = ResourceException.VERSION_MISMATCH;
        } catch (final AuthenticationException e) {
            resourceResultCode = 401;
        } catch (final AuthorizationException e) {
            resourceResultCode = ResourceException.FORBIDDEN;
        } catch (final ConnectionException e) {
            resourceResultCode = ResourceException.UNAVAILABLE;
        } catch (final EntryNotFoundException e) {
            resourceResultCode = ResourceException.NOT_FOUND;
        } catch (final MultipleEntriesFoundException e) {
            resourceResultCode = ResourceException.INTERNAL_ERROR;
        } catch (final TimeoutResultException e) {
            resourceResultCode = 408;
        } catch (final ErrorResultException e) {
            final ResultCode rc = e.getResult().getResultCode();
            if (rc.equals(ResultCode.ADMIN_LIMIT_EXCEEDED)) {
                resourceResultCode = 413; // Request Entity Too Large
            } else if (rc.equals(ResultCode.SIZE_LIMIT_EXCEEDED)) {
                resourceResultCode = 413; // Request Entity Too Large
            } else {
                resourceResultCode = ResourceException.INTERNAL_ERROR;
            }
        }
        return ResourceException.getException(resourceResultCode, 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) {
                new ConnectionCompletionHandler<Result>(c, resultHandler) {
                    @Override
                    public void handleResult(final Connection connection) {
                        final RequestCompletionHandler<Result> innerHandler =
                                new RequestCompletionHandler<Result>(connection, resultHandler);
                        connection.applyChangeAsync(request, null, innerHandler);
                    void chain() {
                        c.getConnection().applyChangeAsync(request, null, resultHandler);
                    }
                };
        factory.getConnectionAsync(outerHandler);
@@ -536,7 +474,7 @@
                                                final Iterator<Filter> i = value.iterator();
                                                while (i.hasNext()) {
                                                    final Filter f = i.next();
                                                    if (f == null || f == alwaysFalse()) {
                                                    if (f == alwaysFalse()) {
                                                        return alwaysFalse();
                                                    } else if (f == alwaysTrue()) {
                                                        i.remove();
@@ -653,7 +591,7 @@
                                                final Iterator<Filter> i = value.iterator();
                                                while (i.hasNext()) {
                                                    final Filter f = i.next();
                                                    if (f == null || f == alwaysFalse()) {
                                                    if (f == alwaysFalse()) {
                                                        i.remove();
                                                    } else if (f == alwaysTrue()) {
                                                        return alwaysTrue();
@@ -702,7 +640,7 @@
                new org.forgerock.opendj.ldap.ResultHandler<Result>() {
                    @Override
                    public void handleErrorResult(final ErrorResultException error) {
                        handler.handleError(adaptErrorResult(error));
                        handler.handleError(adapt(error));
                    }
                    @Override
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
@@ -106,10 +106,6 @@
        }
    }
    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
@@ -143,7 +139,8 @@
                        @Override
                        public Map.Entry<String, JsonValue> apply(final JsonValue value,
                                final Void p) {
                            return new SimpleImmutableEntry<String, JsonValue>(mapping.name, value);
                            return value != null ? new SimpleImmutableEntry<String, JsonValue>(
                                    mapping.name, value) : null;
                        }
                    }, handler));
        }
@@ -179,8 +176,7 @@
                                case 0:
                                    return Collections.emptyList();
                                case 1:
                                    return value.get(0) != null ? value.get(0) : Collections
                                            .<Attribute> emptyList();
                                    return value.get(0);
                                default:
                                    final List<Attribute> attributes =
                                            new ArrayList<Attribute>(value.size());
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
New file
@@ -0,0 +1,177 @@
/*
 * 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.ldap.Filter.alwaysFalse;
import static org.forgerock.opendj.rest2ldap.Utils.accumulate;
import static org.forgerock.opendj.rest2ldap.Utils.adapt;
import static org.forgerock.opendj.rest2ldap.Utils.transform;
import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
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.DN;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.ErrorResultException;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Function;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
/**
 * An attribute mapper which provides a mapping from a JSON value to a single DN
 * valued LDAP attribute.
 */
public final class ReferenceAttributeMapper extends AttributeMapper {
    private boolean isRequired = false;
    private boolean isSingleValued = false;
    private final AttributeDescription ldapAttributeName;
    private final AttributeMapper mapper;
    private WritabilityPolicy writabilityPolicy = READ_WRITE;
    ReferenceAttributeMapper(final AttributeDescription ldapAttributeName,
            final AttributeMapper mapper) {
        this.ldapAttributeName = ldapAttributeName;
        this.mapper = mapper;
    }
    /**
     * Indicates that the LDAP attribute is mandatory and must be provided
     * during create requests.
     *
     * @return This attribute mapper.
     */
    public ReferenceAttributeMapper isRequired() {
        this.isRequired = true;
        return this;
    }
    /**
     * Indicates that multi-valued LDAP attribute should be represented as a
     * single-valued JSON value, rather than an array of values.
     *
     * @return This attribute mapper.
     */
    public ReferenceAttributeMapper isSingleValued() {
        this.isSingleValued = true;
        return this;
    }
    /**
     * Indicates whether or not the LDAP attribute supports updates. The default
     * is {@link WritabilityPolicy#READ_WRITE}.
     *
     * @param policy
     *            The writability policy.
     * @return This attribute mapper.
     */
    public ReferenceAttributeMapper writability(final WritabilityPolicy policy) {
        this.writabilityPolicy = policy;
        return this;
    }
    @Override
    void getLDAPAttributes(final Context c, final JsonPointer jsonAttribute,
            final Set<String> ldapAttributes) {
        ldapAttributes.add(ldapAttributeName.toString());
    }
    @Override
    void getLDAPFilter(final Context c, final FilterType type, final JsonPointer jsonAttribute,
            final String operator, final Object valueAssertion, final ResultHandler<Filter> h) {
        // TODO: only presence and equality matching will be supported. Equality matching will
        // only work for the primary key (whatever that is) by performing a reverse look up to
        // convert the primary key to a DN.
        h.handleResult(alwaysFalse());
    }
    @Override
    void toJSON(final Context c, final Entry e, final ResultHandler<JsonValue> h) {
        final Attribute attribute = e.getAttribute(ldapAttributeName);
        if (attribute == null) {
            h.handleResult(null);
        } else if (attributeIsSingleValued()) {
            final DN dn = attribute.parse().usingSchema(c.getConfig().schema()).asDN();
            readEntry(c, dn, h);
        } else {
            final Set<DN> dns = attribute.parse().usingSchema(c.getConfig().schema()).asSetOfDN();
            final ResultHandler<JsonValue> handler =
                    accumulate(dns.size(), transform(
                            new Function<List<JsonValue>, JsonValue, Void>() {
                                @Override
                                public JsonValue apply(final List<JsonValue> value, final Void p) {
                                    if (value.isEmpty()) {
                                        // No values, so omit the entire JSON object from the resource.
                                        return null;
                                    } else {
                                        // Combine values into a single JSON array.
                                        final List<Object> result =
                                                new ArrayList<Object>(value.size());
                                        for (final JsonValue e : value) {
                                            result.add(e.getObject());
                                        }
                                        return new JsonValue(result);
                                    }
                                }
                            }, h));
            for (final DN dn : dns) {
                readEntry(c, dn, handler);
            }
        }
    }
    @Override
    void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) {
        // TODO:
        h.handleResult(Collections.<Attribute> emptyList());
    }
    private boolean attributeIsRequired() {
        return isRequired;
    }
    private boolean attributeIsSingleValued() {
        return isSingleValued || ldapAttributeName.getAttributeType().isSingleValue();
    }
    private void readEntry(final Context c, final DN dn, final ResultHandler<JsonValue> handler) {
        final Set<String> requestedLDAPAttributes = new LinkedHashSet<String>();
        mapper.getLDAPAttributes(c, new JsonPointer(), requestedLDAPAttributes);
        c.getConnection().readEntryAsync(dn, requestedLDAPAttributes,
                new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() {
                    @Override
                    public void handleErrorResult(final ErrorResultException error) {
                        handler.handleError(adapt(error));
                    }
                    @Override
                    public void handleResult(final SearchResultEntry result) {
                        mapper.toJSON(c, result, handler);
                    }
                });
    }
}
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
@@ -112,7 +112,7 @@
         * configuration. See
         * {@link Rest2LDAP#configureConnectionFactory(JsonValue)} for a
         * detailed specification of the JSON configuration.
         *
         *
         * @param configuration
         *            The JSON configuration.
         * @return A reference to this builder.
@@ -129,16 +129,16 @@
         * configuration. The caller is still required to set the connection
         * factory. The configuration should look like this, excluding the
         * C-like comments:
         *
         *
         * <pre>
         * {
         *     // The base DN beneath which LDAP entries are to be found.
         *     "baseDN" : "ou=people,dc=example,dc=com",
         *
         *
         *     // The mechanism which should be used for read resources during updates, must be
         *     // one of "disabled", "controls", or "search".
         *     "readOnUpdatePolicy" : "controls",
         *
         *
         *     // Additional LDAP attributes which should be included with entries during add (create) operations.
         *     "additionalLDAPAttributes" : [
         *         {
@@ -149,28 +149,28 @@
         *             ]
         *         }
         *     ],
         *
         *
         *     // The strategy which should be used for deriving LDAP entry names from JSON resources.
         *     "namingStrategy" : {
         *         // Option 1) the RDN and resource ID are both derived from a single user attribute in the entry.
         *         "strategy" : "clientDNNaming",
         *         "dnAttribute" : "uid"
         *
         *
         *         // Option 2) the RDN and resource ID are derived from separate user attributes in the entry.
         *         "strategy" : "clientNaming",
         *         "dnAttribute" : "uid",
         *         "idAttribute" : "mail"
         *
         *
         *         // Option 3) the RDN and is derived from a user attribute and the resource ID from an operational
         *         //           attribute in the entry.
         *         "strategy" : "serverNaming",
         *         "dnAttribute" : "uid",
         *         "idAttribute" : "entryUUID"
         *     },
         *
         *
         *     // The attribute which will be used for performing MVCC.
         *     "etagAttribute" : "etag",
         *
         *
         *     // The JSON to LDAP attribute mappings.
         *     "attributes" : {
         *         "schemas"     : { "constant" : [ "urn:scim:schemas:core:1.0" ] },
@@ -182,11 +182,18 @@
         *             "givenName"  : { "simple"   : { "ldapAttribute" : "givenName", "isSingleValued" : true } },
         *             "familyName" : { "simple"   : { "ldapAttribute" : "sn", "isSingleValued" : true, "isRequired" : true } },
         *         },
         *         "manager"     : { "reference" : {
         *             "ldapAttribute" : "manager",
         *             "mapping"       : { "object" : {
         *                 "id"          : { "simple"   : { "ldapAttribute" : "uid", "isSingleValued" : true } },
         *                 "displayName" : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true } }
         *             } }
         *         },
         *         ...
         *     }
         * }
         * </pre>
         *
         *
         * @param configuration
         *            The JSON configuration.
         * @return A reference to this builder.
@@ -248,7 +255,7 @@
        /**
         * Sets the policy which should be used in order to read an entry before
         * it is deleted, or after it is added or modified.
         *
         *
         * @param policy
         *            The policy which should be used in order to read an entry
         *            before it is deleted, or after it is added or modified.
@@ -262,7 +269,7 @@
        /**
         * Sets the schema which should be used when attribute types and
         * controls.
         *
         *
         * @param schema
         *            The schema which should be used when attribute types and
         *            controls.
@@ -354,25 +361,22 @@
                if (config.get("isSingleValued").defaultTo(false).asBoolean()) {
                    s.isSingleValued();
                }
                if (config.isDefined("writability")) {
                    final String writability = config.get("writability").asString();
                    if (writability.equalsIgnoreCase("readOnly")) {
                        s.writability(WritabilityPolicy.READ_ONLY);
                    } else if (writability.equalsIgnoreCase("readOnlyDiscardWrites")) {
                        s.writability(WritabilityPolicy.READ_ONLY_DISCARD_WRITES);
                    } else if (writability.equalsIgnoreCase("createOnly")) {
                        s.writability(WritabilityPolicy.CREATE_ONLY);
                    } else if (writability.equalsIgnoreCase("createOnlyDiscardWrites")) {
                        s.writability(WritabilityPolicy.CREATE_ONLY_DISCARD_WRITES);
                    } else if (writability.equalsIgnoreCase("readWrite")) {
                        s.writability(WritabilityPolicy.READ_WRITE);
                    } else {
                        throw new JsonValueException(mapper,
                                "Illegal writability: must be one of readOnly, readOnlyDiscardWrites, "
                                        + "createOnly, createOnlyDiscardWrites, or readWrite");
                    }
                }
                s.writability(parseWritability(mapper, config));
                return s;
            } else if (mapper.isDefined("reference")) {
                final JsonValue config = mapper.get("reference");
                final AttributeDescription ldapAttribute =
                        ad(config.get("ldapAttribute").required().asString());
                final AttributeMapper m = configureMapper(config.get("mapper").required());
                final ReferenceAttributeMapper r = reference(ldapAttribute, m);
                if (config.get("isRequired").defaultTo(false).asBoolean()) {
                    r.isRequired();
                }
                if (config.get("isSingleValued").defaultTo(false).asBoolean()) {
                    r.isSingleValued();
                }
                r.writability(parseWritability(mapper, config));
                return r;
            } else if (mapper.isDefined("object")) {
                return configureObjectMapper(mapper.get("object"));
            } else {
@@ -388,6 +392,29 @@
            }
            return object;
        }
        private WritabilityPolicy parseWritability(final JsonValue mapper, final JsonValue config) {
            if (config.isDefined("writability")) {
                final String writability = config.get("writability").asString();
                if (writability.equalsIgnoreCase("readOnly")) {
                    return WritabilityPolicy.READ_ONLY;
                } else if (writability.equalsIgnoreCase("readOnlyDiscardWrites")) {
                    return WritabilityPolicy.READ_ONLY_DISCARD_WRITES;
                } else if (writability.equalsIgnoreCase("createOnly")) {
                    return WritabilityPolicy.CREATE_ONLY;
                } else if (writability.equalsIgnoreCase("createOnlyDiscardWrites")) {
                    return WritabilityPolicy.CREATE_ONLY_DISCARD_WRITES;
                } else if (writability.equalsIgnoreCase("readWrite")) {
                    return WritabilityPolicy.READ_WRITE;
                } else {
                    throw new JsonValueException(mapper,
                            "Illegal writability: must be one of readOnly, readOnlyDiscardWrites, "
                                    + "createOnly, createOnlyDiscardWrites, or readWrite");
                }
            } else {
                return WritabilityPolicy.READ_WRITE;
            }
        }
    }
    private static final class AttributeMVCCStrategy extends MVCCStrategy {
@@ -507,7 +534,7 @@
    /**
     * Creates a new connection factory using the provided JSON configuration.
     * The configuration should look like this, excluding the C-like comments:
     *
     *
     * <pre>
     * {
     *     // The primary data center, must contain at least one LDAP server.
@@ -521,7 +548,7 @@
     *             "port"     : 389
     *         },
     *     ],
     *
     *
     *     // The optional secondary (fail-over) data center.
     *     "secondaryLDAPServers" : [
     *         {
@@ -533,18 +560,18 @@
     *             "port"     : 389
     *         },
     *     ],
     *
     *
     *     // Connection pool configuration.
     *     "connectionPoolSize"       : 10,
     *     "heartBeatIntervalSeconds" : 30,
     *
     *
     *     // SSL/TLS configuration (optional and TBD).
     *     "useSSL" : {
     *         // Elect to use StartTLS instead of SSL.
     *         "useStartTLS" : true,
     *         ...
     *     },
     *
     *
     *     // Authentication configuration (optional and TBD).
     *     "authentication" : {
     *         "bindDN"   : "cn=directory manager",
@@ -552,7 +579,7 @@
     *     },
     * }
     * </pre>
     *
     *
     * @param configuration
     *            The JSON configuration.
     * @return A new connection factory using the provided JSON configuration.
@@ -616,6 +643,16 @@
        return new ObjectAttributeMapper();
    }
    public static ReferenceAttributeMapper reference(final AttributeDescription attribute,
            final AttributeMapper mapper) {
        return new ReferenceAttributeMapper(attribute, mapper);
    }
    public static ReferenceAttributeMapper reference(final String attribute,
            final AttributeMapper mapper) {
        return reference(AttributeDescription.valueOf(attribute), mapper);
    }
    public static SimpleAttributeMapper simple(final AttributeDescription attribute) {
        return new SimpleAttributeMapper(attribute);
    }
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
@@ -193,12 +193,13 @@
        final Function<ByteString, ?, Void> f =
                decoder == null ? fixedFunction(byteStringToJson(), ldapAttributeName) : decoder;
        final Object value;
        if (isSingleValued || ldapAttributeName.getAttributeType().isSingleValue()) {
        if (attributeIsSingleValued()) {
            value = e.parseAttribute(ldapAttributeName).as(f, defaultJSONValue);
        } else {
            value = e.parseAttribute(ldapAttributeName).asSetOf(f, defaultJSONValues);
            final Set<Object> s = e.parseAttribute(ldapAttributeName).asSetOf(f, defaultJSONValues);
            value = s.isEmpty() ? null : s;
        }
        h.handleResult(new JsonValue(value));
        h.handleResult(value != null ? new JsonValue(value) : null);
    }
    @Override
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
@@ -36,13 +36,22 @@
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResultHandler;
import org.forgerock.opendj.ldap.AssertionFailureException;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.AttributeDescription;
import org.forgerock.opendj.ldap.AuthenticationException;
import org.forgerock.opendj.ldap.AuthorizationException;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ConnectionException;
import org.forgerock.opendj.ldap.EntryNotFoundException;
import org.forgerock.opendj.ldap.ErrorResultException;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Function;
import org.forgerock.opendj.ldap.GeneralizedTime;
import org.forgerock.opendj.ldap.LinkedAttribute;
import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.TimeoutResultException;
import org.forgerock.opendj.ldap.schema.Syntax;
/**
@@ -82,8 +91,10 @@
         */
        @Override
        public void handleResult(final V result) {
            synchronized (results) {
                results.add(result);
            if (result != null) {
                synchronized (results) {
                    results.add(result);
                }
            }
            if (latch.decrementAndGet() == 0) {
                handler.handleResult(results);
@@ -92,22 +103,22 @@
    }
    private static final Function<ByteString, String, Void> BYTESTRING_TO_BASE64 =
            new Function<ByteString, String, Void>() {
                @Override
                public String apply(ByteString value, Void p) {
                    return value.toBase64String();
                }
            };
    private static final Function<Object, ByteString, Void> BASE64_TO_BYTESTRING =
            new Function<Object, ByteString, Void>() {
                @Override
                public ByteString apply(Object value, Void p) {
                public ByteString apply(final Object value, final Void p) {
                    return ByteString.valueOfBase64(String.valueOf(value));
                }
            };
    private static final Function<ByteString, String, Void> BYTESTRING_TO_BASE64 =
            new Function<ByteString, String, Void>() {
                @Override
                public String apply(final ByteString value, final Void p) {
                    return value.toBase64String();
                }
            };
    private static final Function<ByteString, Object, AttributeDescription> BYTESTRING_TO_JSON =
            new Function<ByteString, Object, AttributeDescription>() {
                @Override
@@ -149,6 +160,44 @@
        return new AccumulatingResultHandler<V>(size, handler);
    }
    /**
     * Adapts an LDAP result code to a resource exception.
     *
     * @param error
     *            The LDAP error that should be adapted.
     * @return The equivalent resource exception.
     */
    static ResourceException adapt(final ErrorResultException error) {
        int resourceResultCode;
        try {
            throw error;
        } catch (final AssertionFailureException e) {
            resourceResultCode = ResourceException.VERSION_MISMATCH;
        } catch (final AuthenticationException e) {
            resourceResultCode = 401;
        } catch (final AuthorizationException e) {
            resourceResultCode = ResourceException.FORBIDDEN;
        } catch (final ConnectionException e) {
            resourceResultCode = ResourceException.UNAVAILABLE;
        } catch (final EntryNotFoundException e) {
            resourceResultCode = ResourceException.NOT_FOUND;
        } catch (final MultipleEntriesFoundException e) {
            resourceResultCode = ResourceException.INTERNAL_ERROR;
        } catch (final TimeoutResultException e) {
            resourceResultCode = 408;
        } catch (final ErrorResultException e) {
            final ResultCode rc = e.getResult().getResultCode();
            if (rc.equals(ResultCode.ADMIN_LIMIT_EXCEEDED)) {
                resourceResultCode = 413; // Request Entity Too Large
            } else if (rc.equals(ResultCode.SIZE_LIMIT_EXCEEDED)) {
                resourceResultCode = 413; // Request Entity Too Large
            } else {
                resourceResultCode = ResourceException.INTERNAL_ERROR;
            }
        }
        return ResourceException.getException(resourceResultCode, error.getMessage(), error);
    }
    static Object attributeToJson(final Attribute a) {
        final Function<ByteString, Object, Void> f =
                fixedFunction(BYTESTRING_TO_JSON, a.getAttributeDescription());
@@ -157,16 +206,16 @@
        return isSingleValued ? a.parse().as(f) : asList(a.parse().asSetOf(f));
    }
    static Function<ByteString, Object, AttributeDescription> byteStringToJson() {
        return BYTESTRING_TO_JSON;
    static Function<Object, ByteString, Void> base64ToByteString() {
        return BASE64_TO_BYTESTRING;
    }
    static Function<ByteString, String, Void> byteStringToBase64() {
        return BYTESTRING_TO_BASE64;
    }
    static Function<Object, ByteString, Void> base64ToByteString() {
        return BASE64_TO_BYTESTRING;
    static Function<ByteString, Object, AttributeDescription> byteStringToJson() {
        return BYTESTRING_TO_JSON;
    }
    static <T> T ensureNotNull(final T object) {