From 507e00fb190713b1654579123d284bcd3d750abe Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Wed, 10 Apr 2013 10:31:19 +0000
Subject: [PATCH] Partial fix for OPENDJ-693: Implement modify/update support

---
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java |  276 +++++++++++++++++++++---------------------------------
 1 files changed, 107 insertions(+), 169 deletions(-)

diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
index 51b3a09..3ac297f 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
@@ -15,16 +15,15 @@
  */
 package org.forgerock.opendj.rest2ldap;
 
-import static java.util.Collections.singletonList;
 import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
+import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
 import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
 import static org.forgerock.opendj.rest2ldap.Utils.accumulate;
 import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull;
+import static org.forgerock.opendj.rest2ldap.Utils.i18n;
 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.LinkedList;
 import java.util.List;
@@ -39,6 +38,7 @@
 import org.forgerock.json.resource.ResultHandler;
 import org.forgerock.opendj.ldap.Attribute;
 import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.Attributes;
 import org.forgerock.opendj.ldap.ByteString;
 import org.forgerock.opendj.ldap.DN;
 import org.forgerock.opendj.ldap.Entry;
@@ -47,11 +47,11 @@
 import org.forgerock.opendj.ldap.Filter;
 import org.forgerock.opendj.ldap.Function;
 import org.forgerock.opendj.ldap.LinkedAttribute;
+import org.forgerock.opendj.ldap.Modification;
 import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
 import org.forgerock.opendj.ldap.ResultCode;
 import org.forgerock.opendj.ldap.SearchResultHandler;
 import org.forgerock.opendj.ldap.SearchScope;
-import org.forgerock.opendj.ldap.requests.Requests;
 import org.forgerock.opendj.ldap.requests.SearchRequest;
 import org.forgerock.opendj.ldap.responses.Result;
 import org.forgerock.opendj.ldap.responses.SearchResultEntry;
@@ -61,7 +61,8 @@
  * An attribute mapper which provides a mapping from a JSON value to a single DN
  * valued LDAP attribute.
  */
-public final class ReferenceAttributeMapper extends AttributeMapper {
+public final class ReferenceAttributeMapper extends
+        AbstractLDAPAttributeMapper<ReferenceAttributeMapper> {
     /**
      * The maximum number of candidate references to allow in search filters.
      */
@@ -69,45 +70,19 @@
 
     private final DN baseDN;
     private Filter filter = null;
-    private boolean isRequired = false;
-    private boolean isSingleValued = false;
-    private final AttributeDescription ldapAttributeName;
     private final AttributeMapper mapper;
     private final AttributeDescription primaryKey;
     private SearchScope scope = SearchScope.WHOLE_SUBTREE;
-    private WritabilityPolicy writabilityPolicy = READ_WRITE;
 
     ReferenceAttributeMapper(final AttributeDescription ldapAttributeName, final DN baseDN,
             final AttributeDescription primaryKey, final AttributeMapper mapper) {
-        this.ldapAttributeName = ldapAttributeName;
+        super(ldapAttributeName);
         this.baseDN = baseDN;
         this.primaryKey = primaryKey;
         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;
-    }
-
-    /**
      * Sets the filter which should be used when searching for referenced LDAP
      * entries. The default is {@code (objectClass=*)}.
      *
@@ -148,30 +123,12 @@
         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) {
+    void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath,
+            final FilterType type, final String operator, final Object valueAssertion,
+            final ResultHandler<Filter> h) {
         // Construct a filter which can be used to find referenced resources.
-        mapper.getLDAPFilter(c, type, jsonAttribute, operator, valueAssertion,
+        mapper.getLDAPFilter(c, path, subPath, type, operator, valueAssertion,
                 new ResultHandler<Filter>() {
                     @Override
                     public void handleError(final ResourceException error) {
@@ -224,86 +181,27 @@
     }
 
     @Override
-    void toJSON(final Context c, final Entry e, final ResultHandler<JsonValue> h) {
-        final Attribute attribute = e.getAttribute(ldapAttributeName);
-        if (attribute == null || attribute.isEmpty()) {
-            h.handleResult(null);
-        } else if (attributeIsSingleValued()) {
-            try {
-                final DN dn = attribute.parse().usingSchema(c.getConfig().schema()).asDN();
-                readEntry(c, dn, h);
-            } catch (final Exception ex) {
-                // The LDAP attribute could not be decoded.
-                h.handleError(asResourceException(ex));
-            }
-        } else {
-            try {
-                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);
-                }
-            } catch (final Exception ex) {
-                // The LDAP attribute could not be decoded.
-                h.handleError(asResourceException(ex));
-            }
+    void getNewLDAPAttributes(final Context c, final JsonPointer path,
+            final List<Object> newValues, final ResultHandler<Attribute> h) {
+        // No need to do anything if there are no values.
+        if (newValues.isEmpty()) {
+            h.handleResult(Attributes.emptyAttribute(ldapAttributeName));
+            return;
         }
-    }
 
-    @Override
-    void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) {
-        try {
-            if (v == null || v.isNull()) {
-                if (attributeIsRequired()) {
-                    // FIXME: improve error message.
-                    throw new BadRequestException("no value provided");
-                } else {
-                    h.handleResult(Collections.<Attribute> emptyList());
-                }
-            } else if (v.isList() && attributeIsSingleValued()) {
-                // FIXME: improve error message.
-                throw new BadRequestException("expected single value, but got multiple values");
-            } else if (!writabilityPolicy.canCreate(ldapAttributeName)) {
-                if (writabilityPolicy.discardWrites()) {
-                    h.handleResult(Collections.<Attribute> emptyList());
-                } else {
-                    // FIXME: improve error message.
-                    throw new BadRequestException("attempted to create a read-only value");
-                }
-            } else {
-                /*
-                 * For each value use the subordinate mapper to obtain the LDAP
-                 * primary key, the perform a search for each one to find the
-                 * corresponding entries.
-                 */
-                final JsonValue valueList =
-                        v.isList() ? v : new JsonValue(singletonList(v.getObject()));
-                final Attribute reference = new LinkedAttribute(ldapAttributeName);
-                final AtomicInteger pendingSearches = new AtomicInteger(valueList.size());
-                final AtomicReference<ResourceException> exception =
-                        new AtomicReference<ResourceException>();
+        /*
+         * For each value use the subordinate mapper to obtain the LDAP primary
+         * key, the perform a search for each one to find the corresponding
+         * entries.
+         */
+        final Attribute newLDAPAttribute = new LinkedAttribute(ldapAttributeName);
+        final AtomicInteger pendingSearches = new AtomicInteger(newValues.size());
+        final AtomicReference<ResourceException> exception =
+                new AtomicReference<ResourceException>();
 
-                for (final JsonValue value : valueList) {
-                    mapper.toLDAP(c, value, new ResultHandler<List<Attribute>>() {
+        for (final Object value : newValues) {
+            mapper.toLDAP(c, path, null /* force create */, new JsonValue(value),
+                    new ResultHandler<List<Modification>>() {
 
                         @Override
                         public void handleError(final ResourceException error) {
@@ -311,32 +209,29 @@
                         }
 
                         @Override
-                        public void handleResult(final List<Attribute> result) {
+                        public void handleResult(final List<Modification> result) {
                             Attribute primaryKeyAttribute = null;
-                            for (final Attribute attribute : result) {
-                                if (attribute.getAttributeDescription().equals(primaryKey)) {
-                                    primaryKeyAttribute = attribute;
+                            for (final Modification modification : result) {
+                                if (modification.getAttribute().getAttributeDescription().equals(
+                                        primaryKey)) {
+                                    primaryKeyAttribute = modification.getAttribute();
                                     break;
                                 }
                             }
-                            if (primaryKeyAttribute == null) {
-                                // FIXME: improve error message.
-                                h.handleError(new BadRequestException(
-                                        "reference primary key attribute is missing"));
-                                return;
-                            }
 
-                            if (primaryKeyAttribute.isEmpty()) {
-                                // FIXME: improve error message.
+                            if (primaryKeyAttribute == null || primaryKeyAttribute.isEmpty()) {
                                 h.handleError(new BadRequestException(
-                                        "reference primary key attribute is empty"));
+                                        i18n("The request cannot be processed because the reference "
+                                                + "field '%s' contains a value which does not contain "
+                                                + "a primary key", path)));
                                 return;
                             }
 
                             if (primaryKeyAttribute.size() > 1) {
-                                // FIXME: improve error message.
                                 h.handleError(new BadRequestException(
-                                        "reference primary key attribute contains multiple values"));
+                                        i18n("The request cannot be processed because the reference "
+                                                + "field '%s' contains a value which contains multiple "
+                                                + "primary keys", path)));
                                 return;
                             }
 
@@ -356,7 +251,7 @@
                                                     ResourceException re;
                                                     try {
                                                         throw error;
-                                                    } catch (EntryNotFoundException e) {
+                                                    } catch (final EntryNotFoundException e) {
                                                         // FIXME: improve error message.
                                                         re =
                                                                 new BadRequestException(
@@ -364,7 +259,7 @@
                                                                                 + primaryKeyValue
                                                                                         .toString()
                                                                                 + "' does not exist");
-                                                    } catch (MultipleEntriesFoundException e) {
+                                                    } catch (final MultipleEntriesFoundException e) {
                                                         // FIXME: improve error message.
                                                         re =
                                                                 new BadRequestException(
@@ -372,7 +267,7 @@
                                                                                 + primaryKeyValue
                                                                                         .toString()
                                                                                 + "' is ambiguous");
-                                                    } catch (ErrorResultException e) {
+                                                    } catch (final ErrorResultException e) {
                                                         re = asResourceException(e);
                                                     }
                                                     exception.compareAndSet(null, re);
@@ -382,8 +277,8 @@
                                                 @Override
                                                 public void handleResult(
                                                         final SearchResultEntry result) {
-                                                    synchronized (reference) {
-                                                        reference.add(result.getName());
+                                                    synchronized (newLDAPAttribute) {
+                                                        newLDAPAttribute.add(result.getName());
                                                     }
                                                     completeIfNecessary();
                                                 }
@@ -393,40 +288,80 @@
                         private void completeIfNecessary() {
                             if (pendingSearches.decrementAndGet() == 0) {
                                 if (exception.get() == null) {
-                                    h.handleResult(singletonList(reference));
+                                    h.handleResult(newLDAPAttribute);
                                 } else {
                                     h.handleError(exception.get());
                                 }
                             }
                         }
                     });
-                }
-            }
-        } catch (final ResourceException e) {
-            h.handleError(e);
-        } catch (final Exception e) {
-            // FIXME: improve error message.
-            h.handleError(new BadRequestException(e.getMessage()));
         }
     }
 
-    private boolean attributeIsRequired() {
-        return isRequired;
+    @Override
+    ReferenceAttributeMapper getThis() {
+        return this;
     }
 
-    private boolean attributeIsSingleValued() {
-        return isSingleValued || ldapAttributeName.getAttributeType().isSingleValue();
+    @Override
+    void toJSON(final Context c, final JsonPointer path, final Entry e,
+            final ResultHandler<JsonValue> h) {
+        final Attribute attribute = e.getAttribute(ldapAttributeName);
+        if (attribute == null || attribute.isEmpty()) {
+            h.handleResult(null);
+        } else if (attributeIsSingleValued()) {
+            try {
+                final DN dn = attribute.parse().usingSchema(c.getConfig().schema()).asDN();
+                readEntry(c, path, dn, h);
+            } catch (final Exception ex) {
+                // The LDAP attribute could not be decoded.
+                h.handleError(asResourceException(ex));
+            }
+        } else {
+            try {
+                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, path, dn, handler);
+                }
+            } catch (final Exception ex) {
+                // The LDAP attribute could not be decoded.
+                h.handleError(asResourceException(ex));
+            }
+        }
     }
 
     private SearchRequest createSearchRequest(final Filter result) {
         final Filter searchFilter = filter != null ? Filter.and(filter, result) : result;
-        final SearchRequest request = Requests.newSearchRequest(baseDN, scope, searchFilter, "1.1");
-        return request;
+        return newSearchRequest(baseDN, scope, searchFilter, "1.1");
     }
 
-    private void readEntry(final Context c, final DN dn, final ResultHandler<JsonValue> handler) {
+    private void readEntry(final Context c, final JsonPointer path, final DN dn,
+            final ResultHandler<JsonValue> handler) {
         final Set<String> requestedLDAPAttributes = new LinkedHashSet<String>();
-        mapper.getLDAPAttributes(c, new JsonPointer(), requestedLDAPAttributes);
+        mapper.getLDAPAttributes(c, path, new JsonPointer(), requestedLDAPAttributes);
         c.getConnection().readEntryAsync(dn, requestedLDAPAttributes,
                 new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() {
 
@@ -435,14 +370,17 @@
                         if (!(error instanceof EntryNotFoundException)) {
                             handler.handleError(asResourceException(error));
                         } else {
-                            // The referenced entry does not exist so ignore it since it cannot be mapped.
+                            /*
+                             * The referenced entry does not exist so ignore it
+                             * since it cannot be mapped.
+                             */
                             handler.handleResult(null);
                         }
                     }
 
                     @Override
                     public void handleResult(final SearchResultEntry result) {
-                        mapper.toJSON(c, result, handler);
+                        mapper.toJSON(c, path, result, handler);
                     }
                 });
     }

--
Gitblit v1.10.0