From 65f47d9c24da91fdeac1eb6f012f44cc056ac4e4 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Thu, 25 Aug 2016 15:15:46 +0000
Subject: [PATCH] OPENDJ-3160 Support DN templates in reference property base DNs

---
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java   |    5 +
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RoutingContext.java          |   16 +++++
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java    |    3 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DnTemplate.java              |   13 ++--
 opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/DnTemplateTest.java          |    3 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java         |   68 ++++++++++++++--------
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java |   17 +++--
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java               |   23 ++++---
 8 files changed, 95 insertions(+), 53 deletions(-)

diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DnTemplate.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DnTemplate.java
index b8e7ea2..83d561b 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DnTemplate.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DnTemplate.java
@@ -44,10 +44,11 @@
  * </table>
  */
 final class DnTemplate {
-    private static final Pattern TEMPLATE_KEY_RE = Pattern.compile("\\{([^}]+)\\}");
+    private static final Pattern TEMPLATE_VARIABLE_RE = Pattern.compile("\\{([^}]+)\\}");
     private final String template;
     private final String formatString;
     private final List<String> variables;
+    /** A value of -1 means that this DN template is absolute. */
     private final int relativeOffset;
 
     /**
@@ -65,8 +66,7 @@
 
     /**
      * Compiles a DN template which will resolve LDAP entries relative to the root DSE by default, but MAY include
-     * relative RDNs indicating that that the DN template will be resolved against current routing state
-     * instead.
+     * relative RDNs indicating that the DN template will be resolved against current routing state instead.
      *
      * @param template
      *         The string representation of the DN template.
@@ -100,7 +100,7 @@
         }
 
         final List<String> templateVariables = new ArrayList<>();
-        final Matcher matcher = TEMPLATE_KEY_RE.matcher(trimmedTemplate);
+        final Matcher matcher = TEMPLATE_VARIABLE_RE.matcher(trimmedTemplate);
         final StringBuffer buffer = new StringBuffer(trimmedTemplate.length());
         while (matcher.find()) {
             matcher.appendReplacement(buffer, "%s");
@@ -121,7 +121,8 @@
         // First determine the base DN based on the context DN and the relative offset.
         DN baseDn = null;
         if (relativeOffset >= 0 && context.containsContext(RoutingContext.class)) {
-            baseDn = context.asContext(RoutingContext.class).getDn().parent(relativeOffset);
+            final RoutingContext routingContext = context.asContext(RoutingContext.class);
+            baseDn = routingContext.getDn().parent(routingContext.isCollection() ? relativeOffset - 1 : relativeOffset);
         }
         if (baseDn == null) {
             baseDn = DN.rootDN();
@@ -152,7 +153,7 @@
                 return value;
             }
             if (!uriRouterContext.getParent().containsContext(UriRouterContext.class)) {
-                throw new IllegalStateException("DN template parameter " + parameter + " cannot be resolved");
+                throw new IllegalStateException("DN template parameter \"" + parameter + "\" cannot be resolved");
             }
             uriRouterContext = uriRouterContext.getParent().asContext(UriRouterContext.class);
         }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java
index f20052b..a1496b3 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java
@@ -72,18 +72,19 @@
      */
     private static final int SEARCH_MAX_CANDIDATES = 1000;
 
-    private final DN baseDn;
+    private final DnTemplate baseDnTemplate;
     private final Schema schema;
     private Filter filter;
     private final PropertyMapper mapper;
     private final AttributeDescription primaryKey;
     private SearchScope scope = SearchScope.WHOLE_SUBTREE;
 
-    ReferencePropertyMapper(final Schema schema, final AttributeDescription ldapAttributeName, final DN baseDn,
-                            final AttributeDescription primaryKey, final PropertyMapper mapper) {
+    ReferencePropertyMapper(final Schema schema, final AttributeDescription ldapAttributeName,
+                            final String baseDnTemplate, final AttributeDescription primaryKey,
+                            final PropertyMapper mapper) {
         super(ldapAttributeName);
         this.schema = schema;
-        this.baseDn = baseDn;
+        this.baseDnTemplate = DnTemplate.compile(baseDnTemplate);
         this.primaryKey = primaryKey;
         this.mapper = mapper;
     }
@@ -144,7 +145,7 @@
                     @Override
                     public Promise<Filter, ResourceException> apply(final Filter result) {
                         // Search for all referenced entries and construct a filter.
-                        final SearchRequest request = createSearchRequest(result);
+                        final SearchRequest request = createSearchRequest(context, result);
                         final List<Filter> subFilters = new LinkedList<>();
 
                         return connectionFrom(context).searchAsync(request, new SearchResultHandler() {
@@ -224,7 +225,7 @@
                           // Now search for the referenced entry in to get its DN.
                           final ByteString primaryKeyValue = primaryKeyAttribute.firstValue();
                           final Filter filter = Filter.equality(primaryKey.toString(), primaryKeyValue);
-                          final SearchRequest search = createSearchRequest(filter);
+                          final SearchRequest search = createSearchRequest(context, filter);
                           connectionFrom(context).searchSingleEntryAsync(search)
                                     .thenOnResult(new ResultHandler<SearchResultEntry>() {
                                         @Override
@@ -325,9 +326,9 @@
         }
     }
 
-    private SearchRequest createSearchRequest(final Filter result) {
+    private SearchRequest createSearchRequest(final Context context, final Filter result) {
         final Filter searchFilter = filter != null ? Filter.and(filter, result) : result;
-        return newSearchRequest(baseDn, scope, searchFilter, "1.1");
+        return newSearchRequest(baseDnTemplate.format(context), scope, searchFilter, "1.1");
     }
 
     private Promise<JsonValue, ResourceException> readEntry(
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java
index 1b7b2e5..0ed6cba 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java
@@ -54,7 +54,6 @@
 import org.forgerock.opendj.ldap.AuthorizationException;
 import org.forgerock.opendj.ldap.ConnectionException;
 import org.forgerock.opendj.ldap.ConstraintViolationException;
-import org.forgerock.opendj.ldap.DN;
 import org.forgerock.opendj.ldap.DecodeOptions;
 import org.forgerock.opendj.ldap.EntryNotFoundException;
 import org.forgerock.opendj.ldap.LdapException;
@@ -244,18 +243,21 @@
      *
      * @param attribute
      *         The DN valued LDAP attribute to be mapped.
-     * @param baseDN
-     *         The search base DN for performing reverse lookups.
+     * @param baseDnTemplate
+     *         The DN template which will be used as the search base when performing reverse lookups. The DN template
+     *         may include template parameters and also parent RDNs using ".." notation. For example, the DN template
+     *         "ou=groups,..,.." specifies that the search base DN should be computed by appending the RDN
+     *         "ou=groups" to the grand-parent of the current resource's LDAP entry.
      * @param primaryKey
      *         The search primary key LDAP attribute to use for performing reverse lookups.
      * @param mapper
      *         An property mapper which will be used to map LDAP attributes in the referenced entry.
      * @return The property mapper.
      */
-    public static ReferencePropertyMapper reference(final AttributeDescription attribute, final DN baseDN,
+    public static ReferencePropertyMapper reference(final AttributeDescription attribute, final String baseDnTemplate,
                                                     final AttributeDescription primaryKey,
                                                     final PropertyMapper mapper) {
-        return new ReferencePropertyMapper(Schema.getDefaultSchema(), attribute, baseDN, primaryKey, mapper);
+        return new ReferencePropertyMapper(Schema.getDefaultSchema(), attribute, baseDnTemplate, primaryKey, mapper);
     }
 
     /**
@@ -263,18 +265,21 @@
      *
      * @param attribute
      *         The DN valued LDAP attribute to be mapped.
-     * @param baseDN
-     *         The search base DN for performing reverse lookups.
+     * @param baseDnTemplate
+     *         The DN template which will be used as the search base when performing reverse lookups. The DN template
+     *         may include template parameters and also parent RDNs using ".." notation. For example, the DN template
+     *         "ou=groups,..,.." specifies that the search base DN should be computed by appending the RDN
+     *         "ou=groups" to the grand-parent of the current resource's LDAP entry.
      * @param primaryKey
      *         The search primary key LDAP attribute to use for performing reverse lookups.
      * @param mapper
      *         An property mapper which will be used to map LDAP attributes in the referenced entry.
      * @return The property mapper.
      */
-    public static ReferencePropertyMapper reference(final String attribute, final String baseDN,
+    public static ReferencePropertyMapper reference(final String attribute, final String baseDnTemplate,
                                                     final String primaryKey, final PropertyMapper mapper) {
         return reference(AttributeDescription.valueOf(attribute),
-                         DN.valueOf(baseDN),
+                         baseDnTemplate,
                          AttributeDescription.valueOf(primaryKey),
                          mapper);
     }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RoutingContext.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RoutingContext.java
index fb2a3ae..9e4f41b 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RoutingContext.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RoutingContext.java
@@ -24,13 +24,23 @@
  * A {@link Context} which communicates the current Rest2Ldap routing state to downstream handlers.
  */
 final class RoutingContext extends AbstractContext {
+    static RoutingContext newCollectionRoutingContext(Context parent, DN collectionDn, Resource resource) {
+        return new RoutingContext(parent, collectionDn, resource, true);
+    }
+
+    static RoutingContext newRoutingContext(Context parent, DN resourceDn, Resource resource) {
+        return new RoutingContext(parent, resourceDn, resource, false);
+    }
+
     private final DN dn;
     private final Resource resource;
+    private final boolean isCollection;
 
-    RoutingContext(final Context parent, final DN dn, final Resource resource) {
+    private RoutingContext(Context parent, DN dn, Resource resource, boolean isCollection) {
         super(parent, "routing context");
         this.dn = dn;
         this.resource = resource;
+        this.isCollection = isCollection;
     }
 
     DN getDn() {
@@ -40,4 +50,8 @@
     Resource getType() {
         return resource;
     }
+
+    boolean isCollection() {
+        return isCollection;
+    }
 }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
index 4dad784..3cfe97a 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
@@ -25,6 +25,7 @@
 import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
 import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException;
 import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
+import static org.forgerock.opendj.rest2ldap.RoutingContext.newRoutingContext;
 import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
 import static org.forgerock.util.promise.Promises.newResultPromise;
 
@@ -234,7 +235,7 @@
         final SearchRequest searchRequest = namingStrategy.createSearchRequest(dnFrom(context), idFrom(context));
         if (searchRequest.getScope().equals(BASE_OBJECT) && !resource.hasSubTypesWithSubResources()) {
             // There's no point in doing a search because we already know the DN and sub-resources.
-            return newResultPromise(new RoutingContext(context, searchRequest.getName(), resource));
+            return newResultPromise(newRoutingContext(context, searchRequest.getName(), resource));
         }
         searchRequest.addAttribute("objectClass");
         return conn.searchSingleEntryAsync(searchRequest)
@@ -243,7 +244,7 @@
                              public Promise<RoutingContext, ResourceException> apply(SearchResultEntry entry)
                                      throws ResourceException {
                                  final Resource subType = resource.resolveSubTypeFromObjectClasses(entry);
-                                 return newResultPromise(new RoutingContext(context, entry.getName(), subType));
+                                 return newResultPromise(newRoutingContext(context, entry.getName(), subType));
                              }
                          }, new AsyncFunction<LdapException, RoutingContext, ResourceException>() {
                              @Override
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java
index 5e6f81c..7cec13f 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java
@@ -34,6 +34,8 @@
 import static org.forgerock.opendj.ldap.SearchScope.SINGLE_LEVEL;
 import static org.forgerock.opendj.ldap.requests.Requests.*;
 import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS;
+import static org.forgerock.opendj.rest2ldap.RoutingContext.newCollectionRoutingContext;
+import static org.forgerock.opendj.rest2ldap.RoutingContext.newRoutingContext;
 import static org.forgerock.opendj.rest2ldap.Utils.connectionFrom;
 import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
 import static org.forgerock.opendj.rest2ldap.Utils.newNotSupportedException;
@@ -260,10 +262,15 @@
             return e.asPromise();
         }
 
+        // Temporary routing context which will be used for encoding LDAP attributes. Note that the DN represents the
+        // DN of the collection, not the resource being created. The DN of the resource can only be determined once
+        // the LDAP attributes have been encoded.
+        final RoutingContext parentDnAndType = newCollectionRoutingContext(context, baseDn, subType);
+
         // Now build the LDAP representation and add it.
         final Connection connection = connectionFrom(context);
         return subType.getPropertyMapper()
-                      .create(context, subType, ROOT, request.getContent())
+                      .create(parentDnAndType, subType, ROOT, request.getContent())
                       .thenAsync(new AsyncFunction<List<Attribute>, ResourceResponse, ResourceException>() {
                           @Override
                           public Promise<ResourceResponse, ResourceException> apply(final List<Attribute> attributes) {
@@ -284,9 +291,12 @@
                                           getLdapAttributesForKnownType(request.getFields(), subType);
                                   addRequest.addControl(PostReadRequestControl.newControl(false, ldapAttributes));
                               }
+                              // Use a routing context which refers to the created entry when computing the response.
+                              final RoutingContext dnAndType =
+                                      newRoutingContext(context, addRequest.getName(), subType);
                               return connection.addAsync(addRequest)
                                                .thenCatchAsync(lazilyAddGlueEntry(connection, addRequest))
-                                               .thenAsync(encodeUpdateResourceResponse(context, subType),
+                                               .thenAsync(encodeUpdateResourceResponse(dnAndType, subType),
                                                           adaptLdapException(ResourceResponse.class));
                           }
                       });
@@ -346,7 +356,7 @@
                         return connection.applyChangeAsync(deleteRequest)
                                          .thenCatchAsync(deleteSubtreeWithoutUsingSubtreeDeleteControl(connection,
                                                                                                        deleteRequest))
-                                         .thenAsync(encodeUpdateResourceResponse(context, dnAndType.getType()),
+                                         .thenAsync(encodeUpdateResourceResponse(dnAndType, dnAndType.getType()),
                                                     adaptLdapException(ResourceResponse.class));
                     }
                 });
@@ -433,7 +443,7 @@
                         final Resource subType = dnAndType.getType();
                         final PropertyMapper propertyMapper = subType.getPropertyMapper();
                         for (final PatchOperation operation : request.getPatchOperations()) {
-                            promises.add(propertyMapper.patch(context, subType, ROOT, operation));
+                            promises.add(propertyMapper.patch(dnAndType, subType, ROOT, operation));
                         }
                         return when(promises);
                     }
@@ -457,7 +467,7 @@
                         if (modifyRequest.getModifications().isEmpty()) {
                             // This patch is a no-op so just read the entry and check its version.
                             return connection.readEntryAsync(dnAndType.getDn(), attributes)
-                                             .thenAsync(encodeEmptyPatchResourceResponse(context, subType, request),
+                                             .thenAsync(encodeEmptyPatchResourceResponse(dnAndType, subType, request),
                                                         adaptLdapException(ResourceResponse.class));
                         } else {
                             // Add controls and perform the modify request.
@@ -469,7 +479,7 @@
                             }
                             addAssertionControl(modifyRequest, request.getRevision());
                             return connection.applyChangeAsync(modifyRequest)
-                                             .thenAsync(encodeUpdateResourceResponse(context, subType),
+                                             .thenAsync(encodeUpdateResourceResponse(dnAndType, subType),
                                                         adaptLdapException(ResourceResponse.class));
                         }
                     }
@@ -503,6 +513,9 @@
         if (queryFilter == null) {
             return new BadRequestException(ERR_QUERY_BY_ID_OR_EXPRESSION_NOT_SUPPORTED.get().toString()).asPromise();
         }
+
+        // Temporary routing context which will be used for encoding the LDAP filter.
+        final RoutingContext parentDnAndType = newCollectionRoutingContext(context, baseDn, resource);
         final PropertyMapper propertyMapper = resource.getPropertyMapper();
         final QueryFilterVisitor<Promise<Filter, ResourceException>, Void, JsonPointer> visitor =
                 new QueryFilterVisitor<Promise<Filter, ResourceException>, Void, JsonPointer>() {
@@ -549,14 +562,14 @@
                     public Promise<Filter, ResourceException> visitContainsFilter(
                             final Void unused, final JsonPointer field, final Object valueAssertion) {
                         return propertyMapper.getLdapFilter(
-                                context, resource, ROOT, field, CONTAINS, null, valueAssertion);
+                                parentDnAndType, resource, ROOT, field, CONTAINS, null, valueAssertion);
                     }
 
                     @Override
                     public Promise<Filter, ResourceException> visitEqualsFilter(
                             final Void unused, final JsonPointer field, final Object valueAssertion) {
                         return propertyMapper.getLdapFilter(
-                                context, resource, ROOT, field, EQUAL_TO, null, valueAssertion);
+                                parentDnAndType, resource, ROOT, field, EQUAL_TO, null, valueAssertion);
                     }
 
                     @Override
@@ -565,35 +578,35 @@
                                                                                        final String operator,
                                                                                        final Object valueAssertion) {
                         return propertyMapper.getLdapFilter(
-                                context, resource, ROOT, field, EXTENDED, operator, valueAssertion);
+                                parentDnAndType, resource, ROOT, field, EXTENDED, operator, valueAssertion);
                     }
 
                     @Override
                     public Promise<Filter, ResourceException> visitGreaterThanFilter(
                             final Void unused, final JsonPointer field, final Object valueAssertion) {
                         return propertyMapper.getLdapFilter(
-                                context, resource, ROOT, field, GREATER_THAN, null, valueAssertion);
+                                parentDnAndType, resource, ROOT, field, GREATER_THAN, null, valueAssertion);
                     }
 
                     @Override
                     public Promise<Filter, ResourceException> visitGreaterThanOrEqualToFilter(
                             final Void unused, final JsonPointer field, final Object valueAssertion) {
                         return propertyMapper.getLdapFilter(
-                                context, resource, ROOT, field, GREATER_THAN_OR_EQUAL_TO, null, valueAssertion);
+                                parentDnAndType, resource, ROOT, field, GREATER_THAN_OR_EQUAL_TO, null, valueAssertion);
                     }
 
                     @Override
                     public Promise<Filter, ResourceException> visitLessThanFilter(
                             final Void unused, final JsonPointer field, final Object valueAssertion) {
                         return propertyMapper.getLdapFilter(
-                                context, resource, ROOT, field, LESS_THAN, null, valueAssertion);
+                                parentDnAndType, resource, ROOT, field, LESS_THAN, null, valueAssertion);
                     }
 
                     @Override
                     public Promise<Filter, ResourceException> visitLessThanOrEqualToFilter(
                             final Void unused, final JsonPointer field, final Object valueAssertion) {
                         return propertyMapper.getLdapFilter(
-                                context, resource, ROOT, field, LESS_THAN_OR_EQUAL_TO, null, valueAssertion);
+                                parentDnAndType, resource, ROOT, field, LESS_THAN_OR_EQUAL_TO, null, valueAssertion);
                     }
 
                     @Override
@@ -649,14 +662,15 @@
                     @Override
                     public Promise<Filter, ResourceException> visitPresentFilter(
                             final Void unused, final JsonPointer field) {
-                        return propertyMapper.getLdapFilter(context, resource, ROOT, field, PRESENT, null, null);
+                        return propertyMapper.getLdapFilter(
+                                parentDnAndType, resource, ROOT, field, PRESENT, null, null);
                     }
 
                     @Override
                     public Promise<Filter, ResourceException> visitStartsWithFilter(
                             final Void unused, final JsonPointer field, final Object valueAssertion) {
                         return propertyMapper.getLdapFilter(
-                                context, resource, ROOT, field, STARTS_WITH, null, valueAssertion);
+                                parentDnAndType, resource, ROOT, field, STARTS_WITH, null, valueAssertion);
                     }
                 };
         // Note that the returned LDAP filter may be null if it could not be mapped by any property mappers.
@@ -741,8 +755,9 @@
                         final String id = namingStrategy.decodeResourceId(entry);
                         final String revision = getRevisionFromEntry(entry);
                         final Resource subType = resource.resolveSubTypeFromObjectClasses(entry);
+                        final RoutingContext dnAndType = newRoutingContext(context, entry.getName(), subType);
                         final PropertyMapper propertyMapper = subType.getPropertyMapper();
-                        propertyMapper.read(context, subType, ROOT, entry)
+                        propertyMapper.read(dnAndType, subType, ROOT, entry)
                                       .thenOnResult(new ResultHandler<JsonValue>() {
                                           @Override
                                           public void handleResult(final JsonValue result) {
@@ -845,7 +860,8 @@
                              @Override
                              public Promise<ResourceResponse, ResourceException> apply(SearchResultEntry entry) {
                                  final Resource subType = resource.resolveSubTypeFromObjectClasses(entry);
-                                 return encodeResourceResponse(context, subType, entry);
+                                 final RoutingContext dnAndType = newRoutingContext(context, entry.getName(), subType);
+                                 return encodeResourceResponse(dnAndType, subType, entry);
                              }
                          });
     }
@@ -854,7 +870,7 @@
             final Context context, final String resourceId, final UpdateRequest request) {
         final Connection connection = connectionFrom(context);
         final AtomicReference<Entry> entryHolder = new AtomicReference<>();
-        final AtomicReference<Resource> subTypeHolder = new AtomicReference<>();
+        final AtomicReference<RoutingContext> dnAndTypeHolder = new AtomicReference<>();
         return connection
                 .searchSingleEntryAsync(searchRequestForUnknownType(resourceId, Collections.<JsonPointer>emptyList()))
                 .thenCatchAsync(adaptLdapException(SearchResultEntry.class))
@@ -869,18 +885,20 @@
 
                         // Determine the type of resource and set of changes that need to be performed.
                         final Resource subType = resource.resolveSubTypeFromObjectClasses(entry);
-                        subTypeHolder.set(subType);
+                        final RoutingContext dnAndType = newRoutingContext(context, entry.getName(), subType);
+                        dnAndTypeHolder.set(dnAndType);
                         final PropertyMapper propertyMapper = subType.getPropertyMapper();
-                        return propertyMapper.update(context, subType , ROOT, entry, request.getContent());
+                        return propertyMapper.update(dnAndType, subType , ROOT, entry, request.getContent());
                     }
                 }).thenAsync(new AsyncFunction<List<Modification>, ResourceResponse, ResourceException>() {
                     @Override
                     public Promise<ResourceResponse, ResourceException> apply(List<Modification> modifications)
                             throws ResourceException {
-                        final Resource subType = subTypeHolder.get();
+                        final RoutingContext dnAndType = dnAndTypeHolder.get();
+                        final Resource subType = dnAndType.getType();
                         if (modifications.isEmpty()) {
                             // No changes to be performed so just return the entry that we read.
-                            return encodeResourceResponse(context, subType, entryHolder.get());
+                            return encodeResourceResponse(dnAndType, subType, entryHolder.get());
                         }
                         // Perform the modify operation.
                         final ModifyRequest modifyRequest = newModifyRequest(entryHolder.get().getName());
@@ -894,7 +912,7 @@
                         addAssertionControl(modifyRequest, request.getRevision());
                         modifyRequest.getModifications().addAll(modifications);
                         return connection.applyChangeAsync(modifyRequest)
-                                         .thenAsync(encodeUpdateResourceResponse(context, subType),
+                                         .thenAsync(encodeUpdateResourceResponse(dnAndType, subType),
                                                     adaptLdapException(ResourceResponse.class));
                     }
                 });
@@ -928,7 +946,7 @@
         final SearchRequest searchRequest = namingStrategy.createSearchRequest(baseDn, resourceId);
         if (searchRequest.getScope().equals(BASE_OBJECT) && !resource.hasSubTypes()) {
             // There's no point in doing a search because we already know the DN and sub-resources.
-            return newResultPromise(new RoutingContext(context, searchRequest.getName(), resource));
+            return newResultPromise(newRoutingContext(context, searchRequest.getName(), resource));
         }
         if (etagAttribute != null && revision != null) {
             searchRequest.addAttribute(etagAttribute.toString());
@@ -943,7 +961,7 @@
                                  // Fail-fast if there is a version mismatch.
                                  ensureMvccVersionMatches(entry, revision);
                                  final Resource subType = resource.resolveSubTypeFromObjectClasses(entry);
-                                 return newResultPromise(new RoutingContext(context, entry.getName(), subType));
+                                 return newResultPromise(newRoutingContext(context, entry.getName(), subType));
                              }
                          }, adaptLdapException(RoutingContext.class));
     }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
index 2e4076e..1fe6eb4 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
@@ -23,6 +23,7 @@
 import static org.forgerock.opendj.ldap.SearchScope.BASE_OBJECT;
 import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
 import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNSUPPORTED_REQUEST_AGAINST_SINGLETON;
+import static org.forgerock.opendj.rest2ldap.RoutingContext.newRoutingContext;
 import static org.forgerock.util.promise.Promises.newResultPromise;
 
 import org.forgerock.json.resource.ActionRequest;
@@ -135,7 +136,7 @@
     }
 
     private Promise<RoutingContext, ResourceException> route(final Context context) {
-        return newResultPromise(new RoutingContext(context, dnFrom(context), resource));
+        return newResultPromise(newRoutingContext(context, dnFrom(context), resource));
     }
 
     private SubResourceImpl singleton(final Context context) {
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/DnTemplateTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/DnTemplateTest.java
index 7f7b3cf..cc847dc 100644
--- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/DnTemplateTest.java
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/DnTemplateTest.java
@@ -18,6 +18,7 @@
 import static java.util.Collections.singletonMap;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.forgerock.opendj.rest2ldap.Rest2Ldap.rest2Ldap;
+import static org.forgerock.opendj.rest2ldap.RoutingContext.newRoutingContext;
 import static org.forgerock.util.Options.defaultOptions;
 
 import org.forgerock.http.routing.UriRouterContext;
@@ -36,7 +37,7 @@
         Context ctx = new RootContext();
         ctx = new Rest2LdapContext(ctx, rest2Ldap(defaultOptions()));
         ctx = new UriRouterContext(ctx, "", "", singletonMap("subdomain", "www"));
-        ctx = new RoutingContext(ctx, DN.valueOf("dc=example,dc=com"), null);
+        ctx = newRoutingContext(ctx, DN.valueOf("dc=example,dc=com"), null);
         ctx = new UriRouterContext(ctx, "", "", singletonMap("tenant", "acme"));
         context = ctx;
     }

--
Gitblit v1.10.0