From ae048917cee151bc66adbfca71aff3f0c630dc97 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Tue, 05 Mar 2013 16:05:52 +0000
Subject: [PATCH] Partial fix for OPENDJ-699: Implement DN reference mapping

---
 opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java          |    7 
 opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java                        |   39 ++-
 opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java                      |  109 ++++++---
 opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java                          |   79 +++++-
 opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java |  194 ++++++-----------
 opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java       |  177 ++++++++++++++++
 opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java          |   10 
 7 files changed, 415 insertions(+), 200 deletions(-)

diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java
index 6e75dde..2a65484 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java
+++ b/opendj-sdk/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");
+        }
+    }
+
 }
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
index fd6742e..05e7764 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
+++ b/opendj-sdk/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
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
index 818bd11..0251974 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
+++ b/opendj-sdk/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());
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
new file mode 100644
index 0000000..02b6be7
--- /dev/null
+++ b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
@@ -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);
+                    }
+                });
+    }
+
+}
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
index 97826f5..75ccee5 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
+++ b/opendj-sdk/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);
     }
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
index 2597572..c014547 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
+++ b/opendj-sdk/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
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
index 9c7ef7a..ce51dcc 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
+++ b/opendj-sdk/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) {

--
Gitblit v1.10.0