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

Matthew Swift
15.42.2016 a08c81f677247ec9eb7721a86250c663065e9930
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/Rest2ldapEndpointConfiguration.xml
@@ -39,24 +39,30 @@
      </adm:defined>
    </adm:default-behavior>
  </adm:property-override>
  <adm:property name="config-url" mandatory="true">
  <adm:property name="config-directory" mandatory="true">
    <adm:synopsis>
      URL of the REST2LDAP configuration file.
      The directory containing the Rest2Ldap configuration file(s) for this specific endpoint.
    </adm:synopsis>
    <adm:description>
      The directory must be readable by the server and may contain multiple configuration files, one for each
      supported version of the REST endpoint. If a relative path is used then it will be resolved against the server's
      instance directory.
    </adm:description>
    <adm:syntax>
      <adm:string>
        <adm:pattern>
          <adm:regex>.*</adm:regex>
          <adm:usage>URL</adm:usage>
          <adm:usage>DIRECTORY</adm:usage>
          <adm:synopsis>
            A URL to an existing file that is readable by the server.
            A directory that is readable by the server.
          </adm:synopsis>
        </adm:pattern>
      </adm:string>
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-config-url</ldap:name>
        <ldap:name>ds-cfg-config-directory</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
</adm:managed-object>
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/config.json
File was renamed from opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
@@ -302,247 +302,5 @@
                "authzIdTemplate": "dn:uid={uid},ou=People,dc=example,dc=com"
            }
        }
    },
    // The REST APIs and their LDAP attribute mappings.
    "mappings": {
        "/users": {
            "baseDN": "ou=people,dc=example,dc=com",
            "readOnUpdatePolicy": "controls",
            "useSubtreeDelete": false,
            "usePermissiveModify": true,
            "etagAttribute": "etag",
            "namingStrategy": {
                "strategy": "clientDNNaming",
                "dnAttribute": "uid"
            },
            "additionalLDAPAttributes": [{
                "type": "objectClass",
                "values": [
                    "top",
                    "person",
                    "organizationalPerson",
                    "inetOrgPerson"
                ]
            }],
            "attributes": {
                "schemas": {
                    "constant": ["urn:scim:schemas:core:1.0"]
                },
                "_id": {
                    "simple": {
                        "ldapAttribute": "uid",
                        "isSingleValued": true,
                        "isRequired": true,
                        "writability": "createOnly"
                    }
                },
                "_rev": {
                    "simple": {
                        "ldapAttribute": "etag",
                        "isSingleValued": true,
                        "writability": "readOnly"
                    }
                },
                "userName": {
                    "simple": {
                        "ldapAttribute": "mail",
                        "isSingleValued": true,
                        "writability": "readOnly"
                    }
                },
                "displayName": {
                    "simple": {
                        "ldapAttribute": "cn",
                        "isSingleValued": true,
                        "isRequired": true
                    }
                },
                "name": {
                    "object": {
                        "givenName": {
                            "simple": {
                                "ldapAttribute": "givenName",
                                "isSingleValued": true
                            }
                        },
                        "familyName": {
                            "simple": {
                                "ldapAttribute": "sn",
                                "isSingleValued": true,
                                "isRequired": true
                            }
                        }
                    }
                },
                "manager": {
                    "reference": {
                        "ldapAttribute": "manager",
                        "baseDN": "ou=people,dc=example,dc=com",
                        "primaryKey": "uid",
                        "mapper": {
                            "object": {
                                "_id": {
                                    "simple": {
                                        "ldapAttribute": "uid",
                                        "isSingleValued": true,
                                        "isRequired": true
                                    }
                                },
                                "displayName": {
                                    "simple": {
                                        "ldapAttribute": "cn",
                                        "isSingleValued": true,
                                        "writability": "readOnlyDiscardWrites"
                                    }
                                }
                            }
                        }
                    }
                },
                "groups": {
                    "reference": {
                        "ldapAttribute": "isMemberOf",
                        "baseDN": "ou=groups,dc=example,dc=com",
                        "writability": "readOnly",
                        "primaryKey": "cn",
                        "mapper": {
                            "object": {
                                "_id": {
                                    "simple": {
                                        "ldapAttribute": "cn",
                                        "isSingleValued": true
                                    }
                                }
                            }
                        }
                    }
                },
                "contactInformation": {
                    "object": {
                        "telephoneNumber": {
                            "simple": {
                                "ldapAttribute": "telephoneNumber",
                                "isSingleValued": true
                            }
                        },
                        "emailAddress": {
                            "simple": {
                                "ldapAttribute": "mail",
                                "isSingleValued": true
                            }
                        }
                    }
                },
                "meta": {
                    "object": {
                        "created": {
                            "simple": {
                                "ldapAttribute": "createTimestamp",
                                "isSingleValued": true,
                                "writability": "readOnly"
                            }
                        },
                        "lastModified": {
                            "simple": {
                                "ldapAttribute": "modifyTimestamp",
                                "isSingleValued": true,
                                "writability": "readOnly"
                            }
                        }
                    }
                }
            }
        },
        "/groups": {
            "baseDN": "ou=groups,dc=example,dc=com",
            "readOnUpdatePolicy": "controls",
            "useSubtreeDelete": false,
            "usePermissiveModify": true,
            "etagAttribute": "etag",
            "namingStrategy": {
                "strategy": "clientDNNaming",
                "dnAttribute": "cn"
            },
            "additionalLDAPAttributes": [{
                "type": "objectClass",
                "values": [
                    "top",
                    "groupOfUniqueNames"
                ]
            }],
            "attributes": {
                "schemas": {
                    "constant": ["urn:scim:schemas:core:1.0"]
                },
                "_id": {
                    "simple": {
                        "ldapAttribute": "cn",
                        "isSingleValued": true,
                        "isRequired": true,
                        "writability": "createOnly"
                    }
                },
                "_rev": {
                    "simple": {
                        "ldapAttribute": "etag",
                        "isSingleValued": true,
                        "writability": "readOnly"
                    }
                },
                "displayName": {
                    "simple": {
                        "ldapAttribute": "cn",
                        "isSingleValued": true,
                        "isRequired": true,
                        "writability": "readOnly"
                    }
                },
                "members": {
                    "reference": {
                        "ldapAttribute": "uniqueMember",
                        "baseDN": "dc=example,dc=com",
                        "primaryKey": "uid",
                        "mapper": {
                            "object": {
                                "_id": {
                                    "simple": {
                                        "ldapAttribute": "uid",
                                        "isSingleValued": true,
                                        "isRequired": true
                                    }
                                },
                                "displayName": {
                                    "simple": {
                                        "ldapAttribute": "cn",
                                        "isSingleValued": true,
                                        "writability": "readOnlyDiscardWrites"
                                    }
                                }
                            }
                        }
                    }
                },
                "meta": {
                    "object": {
                        "created": {
                            "simple": {
                                "ldapAttribute": "createTimestamp",
                                "isSingleValued": true,
                                "writability": "readOnly"
                            }
                        },
                        "lastModified": {
                            "simple": {
                                "ldapAttribute": "modifyTimestamp",
                                "isSingleValued": true,
                                "writability": "readOnly"
                            }
                        }
                    }
                }
            }
        }
    }
}
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/users-and-groups-v1.json
New file
@@ -0,0 +1,234 @@
{
    // This file defines an example Rest2Ldap API mapping exposing a multi-tenant deployment exposing users,
    // POSIX users, and groups, as follows:
    //
    // /api/{tenant}/users/{uid} - users for a given tenant, e.g. "/api/example/users/bjensen"
    // /api/{tenant}/groups/{cn} - groups for a given tenant, e.g. "/api/example/groups/administrators"
    //
    "version": "1.0",
    // This section defines all of the resources, their inheritance, and relationships.
    "resourceTypes": {
        // This resource represents the entry point into the user/group API. It only defines sub-resources and
        // does not have any properties itself. The URL and DN templates include a template variable allowing
        // this API to support multi-tenancy. Multiple template variables are permitted.
        "users-and-groups-v1": {
            "subResources": {
                "{tenant}/users": {
                    "type": "collection",
                    "dnTemplate": "ou=people,dc={tenant},dc=com",
                    "resource": "frapi:opendj:rest2ldap:user:1.0",
                    "namingStrategy": {
                        "type": "clientDnNaming",
                        "dnAttribute": "uid"
                    }
                },
                "{tenant}/groups": {
                    "type": "collection",
                    "dnTemplate": "ou=groups,dc={tenant},dc=com",
                    "resource": "frapi:opendj:rest2ldap:group:1.0",
                    "namingStrategy": {
                        "type": "clientDNNaming",
                        "dnAttribute": "cn"
                    }
                }
            }
        },
        // This resource will act as the common parent of all resources that have a JSON representation.
        "frapi:opendj:rest2ldap:object:1.0": {
            "isAbstract": true,
            "objectClasses": [ "top" ],
            // This property will store type information in a resource's JSON representation. It is the
            // equivalent of the "objectClass" attribute, except that it is single valued and will contain
            // the resource name, e.g. "frapi:opendj:rest2ldap:user:1.0" or "frapi:opendj:rest2ldap:group:1.0".
            "resourceTypeProperty": "_schema",
            "properties": {
                // Resource type property mappers store the resource's type and don't have any configuration.
                "_schema": {
                    "type": "resourceType"
                },
                "_rev": {
                    "type": "simple",
                    "ldapAttribute": "etag",
                    "writability": "readOnly"
                },
                "_meta": {
                    "type": "object",
                    "properties": {
                        "created": {
                            "type": "simple",
                            "ldapAttribute": "createTimestamp",
                            "writability": "readOnly"
                        },
                        "lastModified": {
                            "type": "simple",
                            "ldapAttribute": "modifyTimestamp",
                            "writability": "readOnly"
                        }
                    }
                }
            }
        },
        // A "user" resource includes property mapping for the inetOrgPerson LDAP object class and is identified by
        // the "uid" LDAP attribute. Users have a single sub-type representing users with POSIX account information.
        "frapi:opendj:rest2ldap:user:1.0": {
            "superType": "frapi:opendj:rest2ldap:object:1.0",
            "objectClasses": [ "person", "organizationalPerson", "inetOrgPerson" ],
            "supportedActions": [ "passwordModify" ],
            "properties": {
                "_id": {
                    "type": "simple",
                    "ldapAttribute": "uid",
                    "isRequired": true,
                    "writability": "createOnly"
                },
                "userName": {
                    "type": "simple",
                    "ldapAttribute": "mail"
                },
                "displayName": {
                    "type": "simple",
                    "ldapAttribute": "cn",
                    "isMultiValued": true,
                    "isRequired": true
                },
                "name": {
                    "type": "object",
                    "properties": {
                        "givenName": {
                            "type": "simple"
                        },
                        "familyName": {
                            "type": "simple",
                            "ldapAttribute": "sn",
                            "isRequired": true
                        }
                    }
                },
                "description": {
                    "type": "simple"
                },
                "manager": {
                    "type": "reference",
                    "ldapAttribute": "manager",
                    "baseDn": "ou=people,dc=example,dc=com",
                    "primaryKey": "uid",
                    "mapper": {
                        "type": "object",
                        "properties": {
                            "_id": {
                                "type": "simple",
                                "ldapAttribute": "uid",
                                "isRequired": true
                            },
                            "displayName": {
                                "type": "simple",
                                "ldapAttribute": "cn",
                                "writability": "readOnlyDiscardWrites"
                            }
                        }
                    }
                },
                "groups": {
                    "type": "reference",
                    "ldapAttribute": "isMemberOf",
                    "baseDn": "ou=groups,dc=example,dc=com",
                    "isMultiValued": true,
                    "writability": "readOnly",
                    "primaryKey": "cn",
                    "mapper": {
                        "type": "object",
                        "properties": {
                            "_id": {
                                "type": "simple",
                                "ldapAttribute": "cn"
                            }
                        }
                    }
                },
                "contactInformation": {
                    "type": "object",
                    "properties": {
                        "telephoneNumber": {
                            "type": "simple"
                        },
                        "emailAddress": {
                            "type": "simple",
                            "ldapAttribute": "mail"
                        }
                    }
                }
            }
        },
        // A user with POSIX account information.
        "frapi:opendj:rest2ldap:posixUser:1.0": {
            "superType": "frapi:opendj:rest2ldap:user:1.0",
            "objectClasses": [ "posixAccount" ],
            "properties": {
                "uidNumber": {
                    "type": "simple",
                    "isRequired": true
                },
                "gidNumber": {
                    "type": "simple",
                    "isRequired": true
                },
                "homeDirectory": {
                    "type": "simple",
                    "isRequired": true
                },
                "loginShell": {
                    "type": "simple"
                },
                "gecos": {
                    "type": "simple"
                }
            }
        },
        // A "group" resource includes property mapping for the inetOrgPerson LDAP object class and is identified by
        // the "uid" LDAP attribute. Users have a single sub-type representing users with POSIX account information.
        "frapi:opendj:rest2ldap:group:1.0": {
            "superType": "frapi:opendj:rest2ldap:object:1.0",
            "objectClasses": [ "groupOfUniqueNames" ],
            "properties": {
                "_id": {
                    "type": "simple",
                    "ldapAttribute": "cn",
                    "isRequired": true,
                    "writability": "createOnly"
                },
                "displayName": {
                    "type": "simple",
                    "ldapAttribute": "cn",
                    "isRequired": true,
                    "writability": "readOnly"
                },
                "description": {
                    "type": "simple"
                },
                "members": {
                    "type": "reference",
                    "ldapAttribute": "uniqueMember",
                    "baseDn": "dc=example,dc=com",
                    "primaryKey": "uid",
                    "isMultiValued": true,
                    "mapper": {
                        "type": "object",
                        "properties": {
                            "_id": {
                                "type": "simple",
                                "ldapAttribute": "uid",
                                "isRequired": true
                            },
                            "displayName": {
                                "type": "simple",
                                "ldapAttribute": "cn",
                                "writability": "readOnlyDiscardWrites"
                            }
                        }
                    }
                }
            }
        }
    }
}
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/rest2ldap.json
New file
@@ -0,0 +1,8 @@
{
    // Options controlling how Rest2Ldap interacts with LDAP servers.
    "useMvcc": true,
    "mvccAttribute": "etag",
    "readOnUpdatePolicy": "controls",
    "useSubtreeDelete": true,
    "usePermissiveModify": true
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLdapPropertyMapper.java
@@ -24,6 +24,8 @@
import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
import static org.forgerock.opendj.rest2ldap.Utils.newNotSupportedException;
import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE;
import static org.forgerock.util.promise.Promises.newExceptionPromise;
import static org.forgerock.util.promise.Promises.newResultPromise;
import java.util.ArrayList;
import java.util.Collections;
@@ -54,7 +56,7 @@
    List<Object> defaultJsonValues = emptyList();
    final AttributeDescription ldapAttributeName;
    private boolean isRequired;
    private boolean isSingleValued;
    private boolean isMultiValued;
    private WritabilityPolicy writabilityPolicy = READ_WRITE;
    AbstractLdapPropertyMapper(final AttributeDescription ldapAttributeName) {
@@ -62,30 +64,29 @@
    }
    /**
     * Indicates that the LDAP attribute is mandatory and must be provided
     * during create requests.
     * Indicates that the LDAP attribute is mandatory and must be provided during create requests.
     *
     * @param isRequired {@code true} if this property is required.
     * @return This property mapper.
     */
    public final T isRequired() {
        this.isRequired = true;
    public final T isRequired(final boolean isRequired) {
        this.isRequired = isRequired;
        return getThis();
    }
    /**
     * Indicates that multi-valued LDAP attribute should be represented as a
     * single-valued JSON value, rather than an array of values.
     * Indicates that the LDAP attribute is multi-valued and should be represented in JSON using an array of values.
     *
     * @param isMultiValued {@code true} if this property is multi-valued.
     * @return This property mapper.
     */
    public final T isSingleValued() {
        this.isSingleValued = true;
    public final T isMultiValued(final boolean isMultiValued) {
        this.isMultiValued = isMultiValued;
        return getThis();
    }
    /**
     * Indicates whether the LDAP attribute supports updates.
     * The default is {@link WritabilityPolicy#READ_WRITE}.
     * Indicates whether the LDAP attribute supports updates. The default is {@link WritabilityPolicy#READ_WRITE}.
     *
     * @param policy
     *            The writability policy.
@@ -97,13 +98,14 @@
    }
    boolean attributeIsSingleValued() {
        return isSingleValued || ldapAttributeName.getAttributeType().isSingleValue();
        return !isMultiValued || ldapAttributeName.getAttributeType().isSingleValue();
    }
    @Override
    Promise<List<Attribute>, ResourceException> create(
            final Connection connection, final JsonPointer path, final JsonValue v) {
        return getNewLdapAttributes(connection, path, v).then(
    Promise<List<Attribute>, ResourceException> create(final Connection connection,
                                                       final Resource resource, final JsonPointer path,
                                                       final JsonValue v) {
        return getNewLdapAttributes(connection, resource, path, v).then(
            new Function<Attribute, List<Attribute>, ResourceException>() {
                @Override
                public List<Attribute> apply(Attribute newLDAPAttribute) throws ResourceException {
@@ -114,38 +116,33 @@
                        return Collections.emptyList();
                    } else if (newLDAPAttribute.isEmpty()) {
                        if (isRequired) {
                            throw newBadRequestException(ERR_REMOVE_REQUIRED_FIELD.get("create", path));
                            throw newBadRequestException(ERR_MISSING_REQUIRED_FIELD.get(path));
                        }
                        return Collections.emptyList();
                    }
                    return singletonList(newLDAPAttribute);
                }
            });
    }
    @Override
    void getLdapAttributes(final Connection connection, final JsonPointer path,
                           final JsonPointer subPath, final Set<String> ldapAttributes) {
    void getLdapAttributes(final JsonPointer path, final JsonPointer subPath, final Set<String> ldapAttributes) {
        ldapAttributes.add(ldapAttributeName.toString());
    }
    abstract Promise<Attribute, ResourceException> getNewLdapAttributes(Connection connection, JsonPointer path,
                                                                        List<Object> newValues);
    abstract Promise<Attribute, ResourceException> getNewLdapAttributes(Connection connection, Resource resource,
                                                                        JsonPointer path, List<Object> newValues);
    abstract T getThis();
    @Override
    Promise<List<Modification>, ResourceException> patch(
                final Connection connection, final JsonPointer path, final PatchOperation operation) {
    Promise<List<Modification>, ResourceException> patch(final Connection connection, final Resource resource,
                                                         final JsonPointer path, final PatchOperation operation) {
        try {
            final JsonPointer field = operation.getField();
            final JsonValue v = operation.getValue();
            /*
             * Reject any attempts to patch this field if it is read-only, even
             * if it is configured to discard writes.
             */
            // Reject any attempts to patch this field if it is read-only, even if it is configured to discard writes.
            if (!writabilityPolicy.canWrite(ldapAttributeName)) {
                throw newBadRequestException(ERR_MODIFY_READ_ONLY_FIELD.get("patch", path));
            }
@@ -211,8 +208,7 @@
                 * LDAP attribute is multi-valued, or the attribute already
                 * contains a value.
                 */
                modType =
                        attributeIsSingleValued() ? ModificationType.REPLACE : ModificationType.ADD;
                modType = attributeIsSingleValued() ? ModificationType.REPLACE : ModificationType.ADD;
                if (newValues.isEmpty()) {
                    throw newBadRequestException(ERR_PATCH_ADD_NO_VALUE_FOR_FIELD.get(path.child(field.get(0))));
                }
@@ -233,11 +229,11 @@
                    return Promises.<List<Modification>, ResourceException> newExceptionPromise(
                            newBadRequestException(ERR_REMOVE_REQUIRED_FIELD.get("update", path)));
                } else {
                    return Promises.newResultPromise(
                    return newResultPromise(
                        singletonList(new Modification(modType, emptyAttribute(ldapAttributeName))));
                }
            } else {
                return getNewLdapAttributes(connection, path, newValues)
                return getNewLdapAttributes(connection, resource, path, newValues)
                        .then(new Function<Attribute, List<Modification>, ResourceException>() {
                            @Override
                            public List<Modification> apply(final Attribute value) {
@@ -246,16 +242,16 @@
                        });
            }
        } catch (final RuntimeException e) {
            return Promises.newExceptionPromise(asResourceException(e));
            return asResourceException(e).asPromise();
        } catch (final ResourceException e) {
            return Promises.newExceptionPromise(e);
            return newExceptionPromise(e);
        }
    }
    @Override
    Promise<List<Modification>, ResourceException> update(final Connection connection, final JsonPointer path,
            final Entry e, final JsonValue v) {
        return getNewLdapAttributes(connection, path, v).then(
    Promise<List<Modification>, ResourceException> update(final Connection connection, final Resource resource,
                                                          final JsonPointer path, final Entry e, final JsonValue v) {
        return getNewLdapAttributes(connection, resource, path, v).then(
            new Function<Attribute, List<Modification>, ResourceException>() {
                @Override
                public List<Modification> apply(final Attribute newLDAPAttribute) throws ResourceException {
@@ -338,19 +334,20 @@
    }
    private Promise<Attribute, ResourceException> getNewLdapAttributes(final Connection connection,
                                                                       final JsonPointer path, final JsonValue v) {
                                                                       final Resource resource, final JsonPointer path,
                                                                       final JsonValue v) {
        try {
            // Ensure that the value is of the correct type.
            checkSchema(path, v);
            final List<Object> newValues = asList(v, defaultJsonValues);
            if (newValues.isEmpty()) {
                // Skip sub-class implementation if there are no values.
                return Promises.newResultPromise(emptyAttribute(ldapAttributeName));
                return newResultPromise(emptyAttribute(ldapAttributeName));
            } else {
                return getNewLdapAttributes(connection, path, newValues);
                return getNewLdapAttributes(connection, resource, path, newValues);
            }
        } catch (final Exception ex) {
            return Promises.newExceptionPromise(asResourceException(ex));
        } catch (final Exception e) {
            return asResourceException(e).asPromise();
        }
    }
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractRequestHandler.java
New file
@@ -0,0 +1,84 @@
/*
 * 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 2016 ForgeRock AS.
 *
 */
package org.forgerock.opendj.rest2ldap;
import org.forgerock.json.resource.ActionRequest;
import org.forgerock.json.resource.ActionResponse;
import org.forgerock.json.resource.CreateRequest;
import org.forgerock.json.resource.DeleteRequest;
import org.forgerock.json.resource.PatchRequest;
import org.forgerock.json.resource.QueryRequest;
import org.forgerock.json.resource.QueryResourceHandler;
import org.forgerock.json.resource.QueryResponse;
import org.forgerock.json.resource.ReadRequest;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.services.context.Context;
import org.forgerock.util.promise.Promise;
/**
 * An abstract base class from which request handlers may be easily implemented. The default implementation of each
 * method is to return the {@link ResourceException} passed in during construction.
 */
abstract class AbstractRequestHandler implements RequestHandler {
    private final ResourceException defaultErrorResponse;
    AbstractRequestHandler(final ResourceException defaultErrorResponse) {
        this.defaultErrorResponse = defaultErrorResponse;
    }
    @Override
    public Promise<ActionResponse, ResourceException> handleAction(final Context context, final ActionRequest request) {
        return defaultErrorResponse.asPromise();
    }
    @Override
    public Promise<ResourceResponse, ResourceException> handleCreate(final Context context,
                                                                     final CreateRequest request) {
        return defaultErrorResponse.asPromise();
    }
    @Override
    public Promise<ResourceResponse, ResourceException> handleDelete(final Context context,
                                                                     final DeleteRequest request) {
        return defaultErrorResponse.asPromise();
    }
    @Override
    public Promise<ResourceResponse, ResourceException> handlePatch(final Context context, final PatchRequest request) {
        return defaultErrorResponse.asPromise();
    }
    @Override
    public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request,
                                                                 final QueryResourceHandler handler) {
        return defaultErrorResponse.asPromise();
    }
    @Override
    public Promise<ResourceResponse, ResourceException> handleRead(final Context context, final ReadRequest request) {
        return defaultErrorResponse.asPromise();
    }
    @Override
    public Promise<ResourceResponse, ResourceException> handleUpdate(final Context context,
                                                                     final UpdateRequest request) {
        return defaultErrorResponse.asPromise();
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Action.java
New file
@@ -0,0 +1,44 @@
/*
 * 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 2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
/**
 * Represents an {@link org.forgerock.json.resource.ActionRequest action} that may be performed against a resource.
 * Resources will only accept actions which have been {@link Resource#supportedAction(Action) registered} as being
 * supported.
 */
public enum Action {
    // Notes:
    //
    // - actions are likely to become an extension point in future versions of Rest2Ldap, in which case this enum
    //   will need to be converted into a regular class or interface,
    //
    // - the actions are named so that they can be parsed easily from JSON.
    /** An action that allows users to change or reset their password. */
    PASSWORDMODIFY("passwordModify");
    private final String actionId;
    Action(final String actionId) {
        this.actionId = actionId;
    }
    @Override
    public String toString() {
        return actionId;
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
File was deleted
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JsonConstantPropertyMapper.java
@@ -22,6 +22,7 @@
import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
import static org.forgerock.opendj.rest2ldap.Utils.toFilter;
import static org.forgerock.opendj.rest2ldap.Utils.toLowerCase;
import static org.forgerock.util.promise.Promises.newResultPromise;
import java.util.Collections;
import java.util.List;
@@ -37,7 +38,6 @@
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Modification;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.Promises;
/**
 * An property mapper which maps a single JSON attribute to a fixed value.
@@ -55,26 +55,26 @@
    }
    @Override
    Promise<List<Attribute>, ResourceException> create(final Connection connection, final JsonPointer path,
            final JsonValue v) {
    Promise<List<Attribute>, ResourceException> create(final Connection connection,
                                                       final Resource resource, final JsonPointer path,
                                                       final JsonValue v) {
        if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) {
            return Promises.<List<Attribute>, ResourceException> newExceptionPromise(
                    newBadRequestException(ERR_CREATION_READ_ONLY_FIELD.get(path)));
            return newBadRequestException(ERR_CREATION_READ_ONLY_FIELD.get(path)).asPromise();
        } else {
            return Promises.newResultPromise(Collections.<Attribute> emptyList());
            return newResultPromise(Collections.<Attribute> emptyList());
        }
    }
    @Override
    void getLdapAttributes(final Connection connection, final JsonPointer path, final JsonPointer subPath,
                           final Set<String> ldapAttributes) {
    void getLdapAttributes(final JsonPointer path, final JsonPointer subPath, final Set<String> ldapAttributes) {
        // Nothing to do.
    }
    @Override
    Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final JsonPointer path,
                                                     final JsonPointer subPath, final FilterType type,
                                                     final String operator, final Object valueAssertion) {
    Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final Resource resource,
                                                     final JsonPointer path, final JsonPointer subPath,
                                                     final FilterType type, final String operator,
                                                     final Object valueAssertion) {
        final Filter filter;
        final JsonValue subValue = value.get(subPath);
        if (subValue == null) {
@@ -107,29 +107,28 @@
            // This property mapper is a candidate but it does not match.
            filter = alwaysFalse();
        }
        return Promises.newResultPromise(filter);
        return newResultPromise(filter);
    }
    @Override
    Promise<List<Modification>, ResourceException> patch(final Connection connection, final JsonPointer path,
            final PatchOperation operation) {
        return Promises.<List<Modification>, ResourceException> newExceptionPromise(
                newBadRequestException(ERR_PATCH_READ_ONLY_FIELD.get(path)));
    Promise<List<Modification>, ResourceException> patch(final Connection connection, final Resource resource,
                                                         final JsonPointer path, final PatchOperation operation) {
        return newBadRequestException(ERR_PATCH_READ_ONLY_FIELD.get(path)).asPromise();
    }
    @Override
    Promise<JsonValue, ResourceException> read(final Connection connection, final JsonPointer path, final Entry e) {
        return Promises.newResultPromise(value.copy());
    Promise<JsonValue, ResourceException> read(final Connection connection, final Resource resource,
                                               final JsonPointer path, final Entry e) {
        return newResultPromise(value.copy());
    }
    @Override
    Promise<List<Modification>, ResourceException> update(
            final Connection connection, final JsonPointer path, final Entry e, final JsonValue v) {
    Promise<List<Modification>, ResourceException> update(final Connection connection, final Resource resource,
                                                          final JsonPointer path, final Entry e, final JsonValue v) {
        if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) {
            return Promises.<List<Modification>, ResourceException> newExceptionPromise(
                    newBadRequestException(ERR_MODIFY_READ_ONLY_FIELD.get("update", path)));
            return newBadRequestException(ERR_MODIFY_READ_ONLY_FIELD.get("update", path)).asPromise();
        } else {
            return Promises.newResultPromise(Collections.<Modification> emptyList());
            return newResultPromise(Collections.<Modification>emptyList());
        }
    }
@@ -149,5 +148,4 @@
            return alwaysFalse(); // Not supported.
        }
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NamingStrategy.java
@@ -13,88 +13,57 @@
 *
 * Copyright 2013-2016 ForgeRock AS.
 */
package org.forgerock.opendj.rest2ldap;
import java.util.Set;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.requests.SearchRequest;
/**
 * A naming strategy is responsible for naming REST resources and LDAP entries.
 * A naming strategy is responsible for naming JSON resources and LDAP entries.
 */
abstract class NamingStrategy {
    /*
     * This interface is an abstract class so that methods can be made package
     * private until API is finalized.
     */
    NamingStrategy() {
        // Nothing to do.
    }
interface NamingStrategy {
    /**
     * Returns a search request which can be used to obtain the specified REST
     * resource.
     * Returns a search request which can be used to obtain the specified JSON resource.
     *
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param baseDN
     *            The search base DN.
     * @param baseDn
     *         The search base DN.
     * @param resourceId
     *            The resource ID.
     * @return A search request which can be used to obtain the specified REST
     *         resource.
     *         The resource ID.
     * @return A search request which can be used to obtain the specified JSON resource.
     */
    abstract SearchRequest createSearchRequest(Connection connection, DN baseDN, String resourceId);
    SearchRequest createSearchRequest(DN baseDn, String resourceId);
    /**
     * Adds the name of any LDAP attribute required by this naming strategy to the
     * provided set.
     * Returns the name of the LDAP attribute from which this naming strategy computes the JSON resource ID.
     *
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param ldapAttributes
     *            The set into which any required LDAP attribute name should be
     *            put.
     * @return The name of the LDAP attribute from which this naming strategy computes the JSON resource ID.
     */
    abstract void getLdapAttributes(Connection connection, Set<String> ldapAttributes);
    String getResourceIdLdapAttribute();
    /**
     * Retrieves the resource ID from the provided LDAP entry. Implementations
     * may use the entry DN as well as any attributes in order to determine the
     * resource ID.
     * Decodes the JSON resource ID from the provided LDAP entry. Implementations may use the entry DN as well as any
     * attributes in order to determine the resource ID.
     *
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param entry
     *            The LDAP entry from which the resource ID should be obtained.
     * @return The resource ID.
     *         The LDAP entry from which the resource ID should be obtained.
     * @return The resource ID or {@code null} if the resource ID will be obtained from the resource's "_id" field.
     */
    abstract String getResourceId(Connection connection, Entry entry);
    String decodeResourceId(Entry entry);
    /**
     * Sets the resource ID in the provided LDAP entry. Implementations are
     * responsible for setting the entry DN as well as any attributes associated
     * with the resource ID.
     * Encodes the JSON resource ID in the provided LDAP entry. Implementations are responsible for setting the entry
     * DN as well as any attributes associated with the resource ID.
     *
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param baseDN
     *            The baseDN to use when constructing the entry's DN.
     * @param baseDn
     *         The base DN to use when constructing the entry's DN.
     * @param resourceId
     *            The resource ID.
     *         The resource ID.
     * @param entry
     *            The LDAP entry whose DN and resource ID attributes are to be
     *            set.
     *         The LDAP entry whose DN and resource ID attributes are to be set.
     * @throws ResourceException
     *             If the resource ID cannot be determined.
     *         If the resource ID cannot be determined.
     */
    abstract void setResourceId(Connection connection, DN baseDN, String resourceId, Entry entry)
            throws ResourceException;
    void encodeResourceId(DN baseDn, String resourceId, Entry entry) throws ResourceException;
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectPropertyMapper.java
@@ -15,20 +15,25 @@
 */
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.simple;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
import static org.forgerock.json.resource.PatchOperation.operation;
import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException;
import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
import static org.forgerock.opendj.rest2ldap.Utils.toLowerCase;
import static org.forgerock.util.Utils.joinAsString;
import static org.forgerock.util.promise.Promises.newResultPromise;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
@@ -40,12 +45,12 @@
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Modification;
import org.forgerock.util.Function;
import org.forgerock.util.Pair;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.Promises;
/** An property mapper which maps JSON objects to LDAP attributes. */
public final class ObjectPropertyMapper extends PropertyMapper {
    private static final class Mapping {
        private final PropertyMapper mapper;
        private final String name;
@@ -63,39 +68,80 @@
    private final Map<String, Mapping> mappings = new LinkedHashMap<>();
    private boolean includeAllUserAttributesByDefault = false;
    private final Set<String> excludedDefaultUserAttributes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
    ObjectPropertyMapper() {
        // Nothing to do.
    }
    /**
     * Creates a mapping for an attribute contained in the JSON object.
     * Creates an explicit mapping for a property contained in the JSON object. When user attributes are
     * {@link #includeAllUserAttributesByDefault included} by default, be careful to {@link
     * #excludedDefaultUserAttributes exclude} any attributes which have explicit mappings defined using this method,
     * otherwise they will be duplicated in the JSON representation.
     *
     * @param name
     *            The name of the JSON attribute to be mapped.
     *            The name of the JSON property to be mapped.
     * @param mapper
     *            The property mapper responsible for mapping the JSON
     *            attribute to LDAP attribute(s).
     *            The property mapper responsible for mapping the JSON attribute to LDAP attribute(s).
     * @return A reference to this property mapper.
     */
    public ObjectPropertyMapper attribute(final String name, final PropertyMapper mapper) {
    public ObjectPropertyMapper property(final String name, final PropertyMapper mapper) {
        mappings.put(toLowerCase(name), new Mapping(name, mapper));
        return this;
    }
    /**
     * Specifies whether all LDAP user attributes should be mapped by default using the default schema based mapping
     * rules. Individual attributes can be excluded using {@link #excludedDefaultUserAttributes} in order to prevent
     * attributes with explicit mappings being mapped twice.
     *
     * @param include {@code true} if all LDAP user attributes be mapped by default.
     * @return A reference to this property mapper.
     */
    public ObjectPropertyMapper includeAllUserAttributesByDefault(final boolean include) {
        this.includeAllUserAttributesByDefault = include;
        return this;
    }
    /**
     * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when
     * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be
     * excluded in order to prevent duplication.
     *
     * @param attributeNames The list of attributes to be excluded.
     * @return A reference to this property mapper.
     */
    public ObjectPropertyMapper excludedDefaultUserAttributes(final String... attributeNames) {
        return excludedDefaultUserAttributes(Arrays.asList(attributeNames));
    }
    /**
     * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when
     * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be
     * excluded in order to prevent duplication.
     *
     * @param attributeNames The list of attributes to be excluded.
     * @return A reference to this property mapper.
     */
    public ObjectPropertyMapper excludedDefaultUserAttributes(final Collection<String> attributeNames) {
        excludedDefaultUserAttributes.addAll(attributeNames);
        return this;
    }
    @Override
    public String toString() {
        return "object(" + mappings.values() + ")";
        return "object(" + joinAsString(", ", mappings.values()) + ")";
    }
    @Override
    Promise<List<Attribute>, ResourceException> create(final Connection connection, final JsonPointer path,
            final JsonValue v) {
    Promise<List<Attribute>, ResourceException> create(final Connection connection,
                                                       final Resource resource, final JsonPointer path,
                                                       final JsonValue v) {
        try {
            /*
             * First check that the JSON value is an object and that the fields
             * it contains are known by this mapper.
             */
            final Map<String, Mapping> missingMappings = checkMapping(path, v);
            // First check that the JSON value is an object and that the fields it contains are known by this mapper.
            final Map<String, Mapping> missingMappings = validateJsonValue(path, v);
            // Accumulate the results of the subordinate mappings.
            final List<Promise<List<Attribute>, ResourceException>> promises = new ArrayList<>();
@@ -105,61 +151,70 @@
                for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
                    final Mapping mapping = getMapping(me.getKey());
                    final JsonValue subValue = new JsonValue(me.getValue());
                    promises.add(mapping.mapper.create(connection, path.child(me.getKey()), subValue));
                    promises.add(mapping.mapper.create(connection, resource, path.child(me.getKey()),
                                                       subValue));
                }
            }
            // Invoke mappings for which there were no values provided.
            for (final Mapping mapping : missingMappings.values()) {
                promises.add(mapping.mapper.create(connection, path.child(mapping.name), null));
                promises.add(mapping.mapper.create(connection, resource, path.child(mapping.name), null));
            }
            return Promises.when(promises)
                           .then(this.<Attribute> accumulateResults());
        } catch (final Exception e) {
            return Promises.newExceptionPromise(asResourceException(e));
            return asResourceException(e).asPromise();
        }
    }
    @Override
    void getLdapAttributes(final Connection connection, final JsonPointer path, final JsonPointer subPath,
                           final Set<String> ldapAttributes) {
    void getLdapAttributes(final JsonPointer path, final JsonPointer subPath, final Set<String> ldapAttributes) {
        if (subPath.isEmpty()) {
            // Request all subordinate mappings.
            if (includeAllUserAttributesByDefault) {
                ldapAttributes.add("*");
                // Continue because there may be explicit mappings for operational attributes.
            }
            for (final Mapping mapping : mappings.values()) {
                mapping.mapper.getLdapAttributes(connection, path.child(mapping.name), subPath, ldapAttributes);
                mapping.mapper.getLdapAttributes(path.child(mapping.name), subPath, ldapAttributes);
            }
        } else {
            // Request single subordinate mapping.
            final Mapping mapping = getMapping(subPath);
            final Mapping mapping = getMappingOrNull(subPath);
            if (mapping != null) {
                mapping.mapper.getLdapAttributes(
                        connection, path.child(subPath.get(0)), subPath.relativePointer(), ldapAttributes);
                mapping.mapper.getLdapAttributes(path.child(subPath.get(0)), subPath.relativePointer(), ldapAttributes);
            }
        }
    }
    @Override
    Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final JsonPointer path,
                                                     final JsonPointer subPath, final FilterType type,
                                                     final String operator, final Object valueAssertion) {
        final Mapping mapping = getMapping(subPath);
    Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final Resource resource,
                                                     final JsonPointer path, final JsonPointer subPath,
                                                     final FilterType type, final String operator,
                                                     final Object valueAssertion) {
        final Mapping mapping = getMappingOrNull(subPath);
        if (mapping != null) {
            return mapping.mapper.getLdapFilter(connection, path.child(subPath.get(0)),
                                                subPath.relativePointer(), type, operator, valueAssertion);
            return mapping.mapper.getLdapFilter(connection,
                                                resource,
                                                path.child(subPath.get(0)),
                                                subPath.relativePointer(),
                                                type,
                                                operator,
                                                valueAssertion);
        } else {
            /*
             * Either the filter targeted the entire object (i.e. it was "/"),
             * or it targeted an unrecognized attribute within the object.
             * Either way, the filter will never match.
             */
            return Promises.newResultPromise(alwaysFalse());
            return newResultPromise(alwaysFalse());
        }
    }
    @Override
    Promise<List<Modification>, ResourceException> patch(final Connection connection, final JsonPointer path,
            final PatchOperation operation) {
    Promise<List<Modification>, ResourceException> patch(final Connection connection, final Resource resource,
                                                         final JsonPointer path, final PatchOperation operation) {
        try {
            final JsonPointer field = operation.getField();
            final JsonValue v = operation.getValue();
@@ -170,7 +225,7 @@
                 * by allowing the JSON value to be a partial object and
                 * add/remove/replace only the provided values.
                 */
                checkMapping(path, v);
                validateJsonValue(path, v);
                // Accumulate the results of the subordinate mappings.
                final List<Promise<List<Modification>, ResourceException>> promises = new ArrayList<>();
@@ -182,7 +237,7 @@
                        final JsonValue subValue = new JsonValue(me.getValue());
                        final PatchOperation subOperation =
                                operation(operation.getOperation(), field /* empty */, subValue);
                        promises.add(mapping.mapper.patch(connection, path.child(me.getKey()), subOperation));
                        promises.add(mapping.mapper.patch(connection, resource, path.child(me.getKey()), subOperation));
                    }
                }
@@ -195,71 +250,93 @@
                 * appropriate mapper.
                 */
                final String fieldName = field.get(0);
                final Mapping mapping = getMapping(fieldName);
                final Mapping mapping = getMappingOrNull(fieldName);
                if (mapping == null) {
                    throw newBadRequestException(ERR_UNRECOGNIZED_FIELD.get(path.child(fieldName)));
                }
                final PatchOperation subOperation =
                        operation(operation.getOperation(), field.relativePointer(), v);
                return mapping.mapper.patch(connection, path.child(fieldName), subOperation);
                return mapping.mapper.patch(connection, resource, path.child(fieldName), subOperation);
            }
        } catch (final Exception ex) {
            return Promises.newExceptionPromise(asResourceException(ex));
        } catch (final Exception e) {
            return asResourceException(e).asPromise();
        }
    }
    @Override
    Promise<JsonValue, ResourceException> read(final Connection connection, final JsonPointer path, final Entry e) {
    Promise<JsonValue, ResourceException> read(final Connection connection, final Resource resource,
                                               final JsonPointer path, final Entry e) {
        /*
         * Use an accumulator which will aggregate the results from the
         * subordinate mappers into a single list. On completion, the
         * accumulator combines the results into a single JSON map object.
         */
        final List<Promise<Map.Entry<String, JsonValue>, ResourceException>> promises =
        final List<Promise<Pair<String, JsonValue>, ResourceException>> promises =
                new ArrayList<>(mappings.size());
        for (final Mapping mapping : mappings.values()) {
            promises.add(mapping.mapper.read(connection, path.child(mapping.name), e)
                    .then(new Function<JsonValue, Map.Entry<String, JsonValue>, ResourceException>() {
                        @Override
                        public Map.Entry<String, JsonValue> apply(final JsonValue value) {
                            return value != null ? new SimpleImmutableEntry<String, JsonValue>(mapping.name, value)
                                                 : null;
                        }
                    }));
            promises.add(mapping.mapper.read(connection, resource, path.child(mapping.name), e)
                                       .then(toProperty(mapping.name)));
        }
        if (includeAllUserAttributesByDefault) {
            // Map all user attributes using a default simple mapping. It would be nice if we could automatically
            // detect which attributes have been mapped already using explicit mappings, but it would require us to
            // track which attributes have been accessed in the entry. Instead, we'll rely on the user to exclude
            // attributes which have explicit mappings.
            for (final Attribute attribute : e.getAllAttributes()) {
                // Don't include operational attributes. They must have explicit mappings.
                if (attribute.getAttributeDescription().getAttributeType().isOperational()) {
                    continue;
                }
                // Filter out excluded attributes.
                final String attributeName = attribute.getAttributeDescriptionAsString();
                if (!excludedDefaultUserAttributes.isEmpty() && excludedDefaultUserAttributes.contains(attributeName)) {
                    continue;
                }
                // This attribute needs to be mapped.
                final SimplePropertyMapper mapper = simple(attribute.getAttributeDescription());
                promises.add(mapper.read(connection, resource, path.child(attributeName), e)
                                   .then(toProperty(attributeName)));
            }
        }
        return Promises.when(promises)
                .then(new Function<List<Map.Entry<String, JsonValue>>, JsonValue, ResourceException>() {
                    @Override
                    public JsonValue apply(final List<Map.Entry<String, JsonValue>> value) {
                        if (value.isEmpty()) {
                            /*
                             * No subordinate attributes, so omit the entire
                             * JSON object from the resource.
                             */
                            return null;
                        } else {
                            // Combine the sub-attributes into a single JSON object.
                            final Map<String, Object> result = new LinkedHashMap<>(value.size());
                            for (final Map.Entry<String, JsonValue> e : value) {
                                if (e != null) {
                                    result.put(e.getKey(), e.getValue().getObject());
                                }
                            }
                            return new JsonValue(result);
                        }
                    }
                });
                       .then(new Function<List<Pair<String, JsonValue>>, JsonValue, ResourceException>() {
                           @Override
                           public JsonValue apply(final List<Pair<String, JsonValue>> value) {
                               if (value.isEmpty()) {
                                   // No subordinate attributes, so omit the entire JSON object from the resource.
                                   return null;
                               } else {
                                   // Combine the sub-attributes into a single JSON object.
                                   final Map<String, Object> result = new LinkedHashMap<>(value.size());
                                   for (final Pair<String, JsonValue> e : value) {
                                       if (e != null) {
                                           result.put(e.getFirst(), e.getSecond().getObject());
                                       }
                                   }
                                   return new JsonValue(result);
                               }
                           }
                       });
    }
    private Function<JsonValue, Pair<String, JsonValue>, ResourceException> toProperty(final String name) {
        return new Function<JsonValue, Pair<String, JsonValue>, ResourceException>() {
            @Override
            public Pair<String, JsonValue> apply(final JsonValue value) {
                return value != null ? Pair.of(name, value) : null;
            }
        };
    }
    @Override
    Promise<List<Modification>, ResourceException> update(
            final Connection connection, final JsonPointer path, final Entry e, final JsonValue v) {
    Promise<List<Modification>, ResourceException> update(final Connection connection, final Resource resource,
                                                          final JsonPointer path, final Entry e, final JsonValue v) {
        try {
            // First check that the JSON value is an object and that the fields
            // it contains are known by this mapper.
            final Map<String, Mapping> missingMappings = checkMapping(path, v);
            // First check that the JSON value is an object and that the fields it contains are known by this mapper.
            final Map<String, Mapping> missingMappings = validateJsonValue(path, v);
            // Accumulate the results of the subordinate mappings.
            final List<Promise<List<Modification>, ResourceException>> promises = new ArrayList<>();
@@ -269,19 +346,19 @@
                for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
                    final Mapping mapping = getMapping(me.getKey());
                    final JsonValue subValue = new JsonValue(me.getValue());
                    promises.add(mapping.mapper.update(connection, path.child(me.getKey()), e, subValue));
                    promises.add(mapping.mapper.update(connection, resource, path.child(me.getKey()), e, subValue));
                }
            }
            // Invoke mappings for which there were no values provided.
            for (final Mapping mapping : missingMappings.values()) {
                promises.add(mapping.mapper.update(connection, path.child(mapping.name), e, null));
                promises.add(mapping.mapper.update(connection, resource, path.child(mapping.name), e, null));
            }
            return Promises.when(promises)
                           .then(this.<Modification> accumulateResults());
        } catch (final Exception ex) {
            return Promises.newExceptionPromise(asResourceException(ex));
            return asResourceException(ex).asPromise();
        }
    }
@@ -306,13 +383,13 @@
    }
    /** Fail immediately if the JSON value has the wrong type or contains unknown attributes. */
    private Map<String, Mapping> checkMapping(final JsonPointer path, final JsonValue v)
            throws ResourceException {
    private Map<String, Mapping> validateJsonValue(final JsonPointer path, final JsonValue v) throws ResourceException {
        final Map<String, Mapping> missingMappings = new LinkedHashMap<>(mappings);
        if (v != null && !v.isNull()) {
            if (v.isMap()) {
                for (final String attribute : v.asMap().keySet()) {
                    if (missingMappings.remove(toLowerCase(attribute)) == null) {
                    if (missingMappings.remove(toLowerCase(attribute)) == null
                            && !isIncludedDefaultUserAttribute(attribute)) {
                        throw newBadRequestException(ERR_UNRECOGNIZED_FIELD.get(path.child(attribute)));
                    }
                }
@@ -323,12 +400,31 @@
        return missingMappings;
    }
    private Mapping getMapping(final JsonPointer jsonAttribute) {
        return jsonAttribute.isEmpty() ? null : getMapping(jsonAttribute.get(0));
    private Mapping getMappingOrNull(final JsonPointer jsonAttribute) {
        return jsonAttribute.isEmpty() ? null : getMappingOrNull(jsonAttribute.get(0));
    }
    private Mapping getMappingOrNull(final String jsonAttribute) {
        final Mapping mapping = mappings.get(toLowerCase(jsonAttribute));
        if (mapping != null) {
            return mapping;
        }
        if (isIncludedDefaultUserAttribute(jsonAttribute)) {
            return new Mapping(jsonAttribute, simple(jsonAttribute));
        }
        return null;
    }
    private Mapping getMapping(final String jsonAttribute) {
        return mappings.get(toLowerCase(jsonAttribute));
        final Mapping mappingOrNull = getMappingOrNull(jsonAttribute);
        if (mappingOrNull != null) {
            return mappingOrNull;
        }
        throw new IllegalStateException("Unexpected null mapping for jsonAttribute: " + jsonAttribute);
    }
    private boolean isIncludedDefaultUserAttribute(final String attributeName) {
        return includeAllUserAttributesByDefault
                && (excludedDefaultUserAttributes.isEmpty() || !excludedDefaultUserAttributes.contains(attributeName));
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/PropertyMapper.java
@@ -53,6 +53,7 @@
     *
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param resource The exact type of resource being created.
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            property mapper. This may be used when constructing error
@@ -63,7 +64,8 @@
     *            in the resource.
     * @return A {@link Promise} containing the result of the operation.
     */
    abstract Promise<List<Attribute>, ResourceException> create(Connection connection, JsonPointer path, JsonValue v);
    abstract Promise<List<Attribute>, ResourceException> create(Connection connection, Resource resource,
                                                                JsonPointer path, JsonValue v);
    /**
     * Adds the names of the LDAP attributes required by this property mapper
@@ -72,8 +74,6 @@
     * Implementations should only add the names of attributes found in the LDAP
     * entry directly associated with the resource.
     *
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            property mapper. This may be used when constructing error
@@ -83,11 +83,9 @@
     *            root if all attributes associated with this mapper have been
     *            targeted.
     * @param ldapAttributes
     *            The set into which the required LDAP attribute names should be
     *            put.
 *            The set into which the required LDAP attribute names should be
     */
    abstract void getLdapAttributes(Connection connection, JsonPointer path, JsonPointer subPath,
                                    Set<String> ldapAttributes);
    abstract void getLdapAttributes(JsonPointer path, JsonPointer subPath, Set<String> ldapAttributes);
    /**
     * Transforms the provided REST comparison filter parameters to an LDAP
@@ -100,6 +98,7 @@
     *
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param resource The type of resource being queried.
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            property mapper. This may be used when constructing error
@@ -119,9 +118,9 @@
     *            {@link FilterType#PRESENT}.
     * @return A {@link Promise} containing the result of the operation.
     */
    abstract Promise<Filter, ResourceException> getLdapFilter(Connection connection, JsonPointer path,
                                                              JsonPointer subPath, FilterType type, String operator,
                                                              Object valueAssertion);
    abstract Promise<Filter, ResourceException> getLdapFilter(Connection connection, Resource resource,
                                                              JsonPointer path, JsonPointer subPath, FilterType type,
                                                              String operator, Object valueAssertion);
    /**
     * Maps a JSON patch operation to one or more LDAP modifications, returning
@@ -130,6 +129,7 @@
     *
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param resource The exact type of resource being patched.
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            property mapper. This may be used when constructing error
@@ -141,8 +141,8 @@
     *            with this mapper have been targeted.
     * @return A {@link Promise} containing the result of the operation.
     */
    abstract Promise<List<Modification>, ResourceException> patch(
            Connection connection, JsonPointer path, PatchOperation operation);
    abstract Promise<List<Modification>, ResourceException> patch(Connection connection, Resource resource,
                                                                  JsonPointer path, PatchOperation operation);
    /**
     * Maps one or more LDAP attributes to their JSON representation, returning
@@ -161,6 +161,7 @@
     *
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param resource The exact type of resource being read.
     * @param path
     *            The pointer from the root of the JSON resource to this
     *            property mapper. This may be used when constructing error
@@ -169,7 +170,8 @@
     *            The LDAP entry to be converted to JSON.
     * @return A {@link Promise} containing the result of the operation.
     */
    abstract Promise<JsonValue, ResourceException> read(Connection connection, JsonPointer path, Entry e);
    abstract Promise<JsonValue, ResourceException> read(Connection connection, Resource resource,
                                                        JsonPointer path, Entry e);
    /**
     * Maps a JSON value to one or more LDAP modifications, returning a promise
@@ -184,14 +186,15 @@
     *
     * @param connection
     *            The LDAP connection to use to perform the operation.
     * @param resource The exact type of resource being updated.
     * @param v
     *            The JSON value to be converted to LDAP attributes, which may
     *            be {@code null} indicating that the JSON value was not present
     *            in the resource.
     * @return A {@link Promise} containing the result of the operation.
     */
    abstract Promise<List<Modification>, ResourceException> update(Connection connection, JsonPointer path, Entry e,
            JsonValue v);
    abstract Promise<List<Modification>, ResourceException> update(Connection connection, Resource resource,
                                                                   JsonPointer path, Entry e, JsonValue v);
    // TODO: methods for obtaining schema information (e.g. name, description, type information).
    // TODO: methods for creating sort controls.
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java
New file
@@ -0,0 +1,54 @@
/*
 * 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 2016 ForgeRock AS.
 *
 */
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_READ_ONLY_ENDPOINT;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.QueryRequest;
import org.forgerock.json.resource.QueryResourceHandler;
import org.forgerock.json.resource.QueryResponse;
import org.forgerock.json.resource.ReadRequest;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.services.context.Context;
import org.forgerock.util.promise.Promise;
/**
 * Provides a read-only view of an underlying request handler.
 */
final class ReadOnlyRequestHandler extends AbstractRequestHandler {
    private final RequestHandler delegate;
    ReadOnlyRequestHandler(final RequestHandler delegate) {
        super(new BadRequestException(ERR_READ_ONLY_ENDPOINT.get().toString()));
        this.delegate = delegate;
    }
    @Override
    public Promise<QueryResponse, ResourceException> handleQuery(
            final Context context, final QueryRequest request, final QueryResourceHandler handler) {
        return delegate.handleQuery(context, request, handler);
    }
    @Override
    public Promise<ResourceResponse, ResourceException> handleRead(
            final Context context, final ReadRequest request) {
        return delegate.handleRead(context, request);
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java
@@ -15,12 +15,14 @@
 */
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.opendj.ldap.ResultCode.ADMIN_LIMIT_EXCEEDED;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
import static org.forgerock.opendj.ldap.LdapException.newLdapException;
import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException;
import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
import static org.forgerock.util.Reject.checkNotNull;
import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
import static org.forgerock.util.promise.Promises.newResultPromise;
import java.util.ArrayList;
import java.util.LinkedHashSet;
@@ -44,7 +46,6 @@
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.LinkedAttribute;
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.SearchRequest;
@@ -133,10 +134,11 @@
    }
    @Override
    Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final JsonPointer path,
                                                     final JsonPointer subPath, final FilterType type,
                                                     final String operator, final Object valueAssertion) {
        return mapper.getLdapFilter(connection, path, subPath, type, operator, valueAssertion)
    Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final Resource resource,
                                                     final JsonPointer path, final JsonPointer subPath,
                                                     final FilterType type, final String operator,
                                                     final Object valueAssertion) {
        return mapper.getLdapFilter(connection, resource, path, subPath, type, operator, valueAssertion)
                .thenAsync(new AsyncFunction<Filter, Filter, ResourceException>() {
                    @Override
                    public Promise<Filter, ResourceException> apply(final Filter result) {
@@ -164,8 +166,7 @@
                            @Override
                            public Filter apply(Result result) throws ResourceException {
                                if (subFilters.size() >= SEARCH_MAX_CANDIDATES) {
                                    throw asResourceException(
                                            newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED));
                                    throw asResourceException(newLdapException(ADMIN_LIMIT_EXCEEDED));
                                } else if (subFilters.size() == 1) {
                                    return subFilters.get(0);
                                } else {
@@ -183,8 +184,8 @@
    }
    @Override
    Promise<Attribute, ResourceException> getNewLdapAttributes(final Connection connection, final JsonPointer path,
                                                               final List<Object> newValues) {
    Promise<Attribute, ResourceException> getNewLdapAttributes(final Connection connection, final Resource resource,
                                                               final JsonPointer path, final List<Object> newValues) {
        /*
         * 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.
@@ -195,70 +196,75 @@
        final PromiseImpl<Attribute, ResourceException> promise = PromiseImpl.create();
        for (final Object value : newValues) {
            mapper.create(connection, path, new JsonValue(value)).thenOnResult(new ResultHandler<List<Attribute>>() {
                @Override
                public void handleResult(List<Attribute> result) {
                    Attribute primaryKeyAttribute = null;
                    for (final Attribute attribute : result) {
                        if (attribute.getAttributeDescription().equals(primaryKey)) {
                            primaryKeyAttribute = attribute;
                            break;
                        }
                    }
            mapper.create(connection, resource, path, new JsonValue(value))
                  .thenOnResult(new ResultHandler<List<Attribute>>() {
                      @Override
                      public void handleResult(List<Attribute> result) {
                          Attribute primaryKeyAttribute = null;
                          for (final Attribute attribute : result) {
                              if (attribute.getAttributeDescription().equals(primaryKey)) {
                                  primaryKeyAttribute = attribute;
                                  break;
                              }
                          }
                    if (primaryKeyAttribute == null || primaryKeyAttribute.isEmpty()) {
                        promise.handleException(newBadRequestException(ERR_REFERENCE_FIELD_NO_PRIMARY_KEY.get(path)));
                    }
                          if (primaryKeyAttribute == null || primaryKeyAttribute.isEmpty()) {
                              promise.handleException(newBadRequestException(
                                      ERR_REFERENCE_FIELD_NO_PRIMARY_KEY.get(path)));
                              return;
                          }
                    if (primaryKeyAttribute.size() > 1) {
                        promise.handleException(
                                newBadRequestException(ERR_REFERENCE_FIELD_MULTIPLE_PRIMARY_KEYS.get(path)));
                    }
                          if (primaryKeyAttribute.size() > 1) {
                              promise.handleException(
                                      newBadRequestException(ERR_REFERENCE_FIELD_MULTIPLE_PRIMARY_KEYS.get(path)));
                              return;
                          }
                    // 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);
                    connection.searchSingleEntryAsync(search)
                              .thenOnResult(new ResultHandler<SearchResultEntry>() {
                                  @Override
                                  public void handleResult(final SearchResultEntry result) {
                                      synchronized (newLDAPAttribute) {
                                          newLDAPAttribute.add(result.getName());
                                      }
                                      completeIfNecessary();
                                  }
                              }).thenOnException(new ExceptionHandler<LdapException>() {
                                  @Override
                                  public void handleException(final LdapException error) {
                                      ResourceException re;
                                      try {
                                          throw error;
                                      } catch (final EntryNotFoundException e) {
                                          re = newBadRequestException(ERR_REFERENCE_FIELD_DOES_NOT_EXIST.get(
                                                  primaryKeyValue.toString(), path));
                                      } catch (final MultipleEntriesFoundException e) {
                                          re = newBadRequestException(
                                                  ERR_REFERENCE_FIELD_AMBIGUOUS.get(primaryKeyValue.toString(), path));
                                      } catch (final LdapException e) {
                                          re = asResourceException(e);
                                      }
                                      exception.compareAndSet(null, re);
                                      completeIfNecessary();
                                  }
                              });
                }
                          // 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);
                          connection.searchSingleEntryAsync(search)
                                    .thenOnResult(new ResultHandler<SearchResultEntry>() {
                                        @Override
                                        public void handleResult(final SearchResultEntry result) {
                                            synchronized (newLDAPAttribute) {
                                                newLDAPAttribute.add(result.getName());
                                            }
                                            completeIfNecessary();
                                        }
                                    })
                                    .thenOnException(new ExceptionHandler<LdapException>() {
                                        @Override
                                        public void handleException(final LdapException error) {
                                            ResourceException re;
                                            try {
                                                throw error;
                                            } catch (final EntryNotFoundException e) {
                                                re = newBadRequestException(
                                                        ERR_REFERENCE_FIELD_DOES_NOT_EXIST.get(primaryKeyValue, path));
                                            } catch (final MultipleEntriesFoundException e) {
                                                re = newBadRequestException(
                                                        ERR_REFERENCE_FIELD_AMBIGUOUS.get(primaryKeyValue, path));
                                            } catch (final LdapException e) {
                                                re = asResourceException(e);
                                            }
                                            exception.compareAndSet(null, re);
                                            completeIfNecessary();
                                        }
                                    });
                      }
                private void completeIfNecessary() {
                    if (pendingSearches.decrementAndGet() == 0) {
                        if (exception.get() != null) {
                            promise.handleException(exception.get());
                        } else {
                            promise.handleResult(newLDAPAttribute);
                        }
                    }
                }
            });
                      private void completeIfNecessary() {
                          if (pendingSearches.decrementAndGet() == 0) {
                              if (exception.get() != null) {
                                  promise.handleException(exception.get());
                              } else {
                                  promise.handleResult(newLDAPAttribute);
                              }
                          }
                      }
                  });
        }
        return promise;
    }
@@ -268,28 +274,30 @@
        return this;
    }
    @SuppressWarnings("fallthrough")
    @Override
    Promise<JsonValue, ResourceException> read(final Connection connection, final JsonPointer path, final Entry e) {
        final Attribute attribute = e.getAttribute(ldapAttributeName);
        if (attribute == null || attribute.isEmpty()) {
            return Promises.newResultPromise(null);
        } else if (attributeIsSingleValued()) {
            try {
                final DN dn = attribute.parse().usingSchema(schema).asDN();
                return readEntry(connection, path, dn);
            } catch (final Exception ex) {
                // The LDAP attribute could not be decoded.
                return Promises.newExceptionPromise(asResourceException(ex));
    Promise<JsonValue, ResourceException> read(final Connection connection, final Resource resource,
                                               final JsonPointer path, final Entry e) {
        final Set<DN> dns = e.parseAttribute(ldapAttributeName).usingSchema(schema).asSetOfDN();
        switch (dns.size()) {
        case 0:
            return newResultPromise(null);
        case 1:
            if (attributeIsSingleValued()) {
                try {
                    return readEntry(connection, resource, path, dns.iterator().next());
                } catch (final Exception ex) {
                    // The LDAP attribute could not be decoded.
                    return Promises.newExceptionPromise(asResourceException(ex));
                }
            }
        } else {
            // Fall-though: unexpectedly got multiple values. It's probably best to just return them.
        default:
            try {
                final Set<DN> dns = attribute.parse().usingSchema(schema).asSetOfDN();
                final List<Promise<JsonValue, ResourceException>> promises = new ArrayList<>(dns.size());
                for (final DN dn : dns) {
                    promises.add(readEntry(connection, path, dn));
                    promises.add(readEntry(connection, resource, path, dn));
                }
                return Promises.when(promises)
                               .then(new Function<List<JsonValue>, JsonValue, ResourceException>() {
                                   @Override
@@ -322,9 +330,9 @@
    }
    private Promise<JsonValue, ResourceException> readEntry(
            final Connection connection, final JsonPointer path, final DN dn) {
            final Connection connection, final Resource resource, final JsonPointer path, final DN dn) {
        final Set<String> requestedLDAPAttributes = new LinkedHashSet<>();
        mapper.getLdapAttributes(connection, path, new JsonPointer(), requestedLDAPAttributes);
        mapper.getLdapAttributes(path, new JsonPointer(), requestedLDAPAttributes);
        final Filter searchFilter = filter != null ? filter : Filter.alwaysTrue();
        final String[] attributes = requestedLDAPAttributes.toArray(new String[requestedLDAPAttributes.size()]);
@@ -335,7 +343,7 @@
                .thenAsync(new AsyncFunction<SearchResultEntry, JsonValue, ResourceException>() {
                    @Override
                    public Promise<JsonValue, ResourceException> apply(final SearchResultEntry result) {
                        return mapper.read(connection, path, result);
                        return mapper.read(connection, resource, path, result);
                    }
                }, new AsyncFunction<LdapException, JsonValue, ResourceException>() {
                    @Override
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java
New file
@@ -0,0 +1,447 @@
/*
 * 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 2016 ForgeRock AS.
 *
 */
package org.forgerock.opendj.rest2ldap;
import static java.util.Arrays.asList;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_ABSTRACT_TYPE_IN_CREATE;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_MISSING_TYPE_PROPERTY_IN_CREATE;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_RESOURCE_SUPER_TYPE;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_TYPE_IN_CREATE;
import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
import static org.forgerock.util.Utils.joinAsString;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.Router;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.LinkedAttribute;
/**
 * Defines the characteristics of a resource, including its properties, inheritance, and sub-resources.
 */
public final class Resource {
    /** The resource ID. */
    private final String id;
    /** {@code true} if only sub-types of this resource can be created. */
    private boolean isAbstract;
    /** The ID of the super-type of this resource, may be {@code null}. */
    private String superTypeId;
    /** The LDAP object classes associated with this resource. */
    private final Attribute objectClasses = new LinkedAttribute("objectClass");
    /** The possibly empty set of sub-resources. */
    private final Set<SubResource> subResources = new LinkedHashSet<>();
    /** The set of property mappers associated with this resource, excluding inherited properties. */
    private final Map<String, PropertyMapper> declaredProperties = new LinkedHashMap<>();
    /** The set of property mappers associated with this resource, including inherited properties. */
    private final Map<String, PropertyMapper> allProperties = new LinkedHashMap<>();
    /**
     * A JSON pointer to the primitive JSON property that will be used to convey type information. May be {@code
     * null} if the type property is defined in a super type or if this resource does not have any sub-types.
     */
    private JsonPointer resourceTypeProperty;
    /** Set to {@code true} once this Resource has been built. */
    private boolean isBuilt = false;
    /** The resolved super-type. */
    private Resource superType;
    /** The resolved sub-resources (only immediate children). */
    private final Set<Resource> subTypes = new LinkedHashSet<>();
    /** The property mapper which will map all properties for this resource including inherited properties. */
    private final ObjectPropertyMapper propertyMapper = new ObjectPropertyMapper();
    /** Routes requests to sub-resources. */
    private final Router subResourceRouter = new Router();
    private volatile Boolean hasSubTypesWithSubResources = null;
    /** The set of actions supported by this resource and its sub-types. */
    private final Set<Action> supportedActions = new HashSet<>();
    Resource(final String id) {
        this.id = id;
    }
    /**
     * Returns the resource ID of this resource.
     *
     * @return The resource ID of this resource.
     */
    @Override
    public String toString() {
        return id;
    }
    /**
     * Returns {@code true} if the provided parameter is a {@code Resource} having the same resource ID as this
     * resource.
     *
     * @param o
     *         The object to compare.
     * @return {@code true} if the provided parameter is a {@code Resource} having the same resource ID as this
     * resource.
     */
    @Override
    public boolean equals(final Object o) {
        return this == o || (o instanceof Resource && id.equals(((Resource) o).id));
    }
    @Override
    public int hashCode() {
        return id.hashCode();
    }
    /**
     * Specifies the resource ID of the resource which is a super-type of this resource. This resource will inherit
     * the properties and sub-resources of the super-type, and may optionally override them.
     *
     * @param resourceId
     *         The resource ID of the resource which is a super-type of this resource, or {@code null} if there is no
     *         super-type.
     * @return A reference to this object.
     */
    public Resource superType(final String resourceId) {
        this.superTypeId = resourceId;
        return this;
    }
    /**
     * Specifies whether this resource is an abstract type and therefore cannot be created. Only non-abstract
     * sub-types can be created.
     *
     * @param isAbstract
     *         {@code true} if this resource is abstract.
     * @return A reference to this object.
     */
    public Resource isAbstract(final boolean isAbstract) {
        this.isAbstract = isAbstract;
        return this;
    }
    /**
     * Specifies a mapping for a property contained in this JSON resource. Properties are inherited and sub-types may
     * override them. Properties are optional: a resource that does not have any properties cannot be created, read,
     * or modified, and may only be used for accessing sub-resources. These resources usually represent API
     * "endpoints".
     *
     * @param name
     *         The name of the JSON property to be mapped.
     * @param mapper
     *         The property mapper responsible for mapping the JSON property to LDAP attribute(s).
     * @return A reference to this object.
     */
    public Resource property(final String name, final PropertyMapper mapper) {
        declaredProperties.put(name, mapper);
        return this;
    }
    /**
     * Specifies whether all LDAP user attributes should be mapped by default using the default schema based mapping
     * rules. Individual attributes can be excluded using {@link #excludedDefaultUserAttributes} in order to prevent
     * attributes with explicit mappings being mapped twice.
     *
     * @param include {@code true} if all LDAP user attributes be mapped by default.
     * @return A reference to this object.
     */
    public Resource includeAllUserAttributesByDefault(final boolean include) {
        propertyMapper.includeAllUserAttributesByDefault(include);
        return this;
    }
    /**
     * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when
     * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be
     * excluded in order to prevent duplication.
     *
     * @param attributeNames The list of attributes to be excluded.
     * @return A reference to this object.
     */
    public Resource excludedDefaultUserAttributes(final String... attributeNames) {
        return excludedDefaultUserAttributes(Arrays.asList(attributeNames));
    }
    /**
     * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when
     * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be
     * excluded in order to prevent duplication.
     *
     * @param attributeNames The list of attributes to be excluded.
     * @return A reference to this object.
     */
    public Resource excludedDefaultUserAttributes(final Collection<String> attributeNames) {
        propertyMapper.excludedDefaultUserAttributes(attributeNames);
        return this;
    }
    /**
     * Specifies the name of the JSON property which contains the resource's type, whose value is the
     * resource ID. The resource type property is inherited by sub-types and must be available to any resources
     * referenced from {@link SubResource sub-resources}.
     *
     * @param resourceTypeProperty
     *         The name of the JSON property which contains the resource's type, or {@code null} if this resource does
     *         not have a resource type property or if it should be inherited from a super-type.
     * @return A reference to this object.
     */
    public Resource resourceTypeProperty(final JsonPointer resourceTypeProperty) {
        this.resourceTypeProperty = resourceTypeProperty;
        return this;
    }
    /**
     * Specifies an LDAP object class which is to be associated with this resource. Multiple object classes may be
     * specified. The object classes are used for determining the type of resource being accessed during all requests
     * other than create. Object classes are inherited by sub-types and must be defined for any resources that are
     * non-abstract and which can be created.
     *
     * @param objectClass
     *         An LDAP object class associated with this resource's LDAP representation.
     * @return A reference to this object.
     */
    public Resource objectClass(final String objectClass) {
        this.objectClasses.add(objectClass);
        return this;
    }
    /**
     * Specifies LDAP object classes which are to be associated with this resource. Multiple object classes may be
     * specified. The object classes are used for determining the type of resource being accessed during all requests
     * other than create. Object classes are inherited by sub-types and must be defined for any resources that are
     * non-abstract and which can be created.
     *
     * @param objectClasses
     *         The LDAP object classes associated with this resource's LDAP representation.
     * @return A reference to this object.
     */
    public Resource objectClasses(final String... objectClasses) {
        this.objectClasses.add((Object[]) objectClasses);
        return this;
    }
    /**
     * Registers an action which should be supported by this resource. By default, no actions are supported.
     *
     * @param action
     *         The action supported by this resource.
     * @return A reference to this object.
     */
    public Resource supportedAction(final Action action) {
        this.supportedActions.add(action);
        return this;
    }
    /**
     * Registers zero or more actions which should be supported by this resource. By default, no actions are supported.
     *
     * @param actions
     *         The actions supported by this resource.
     * @return A reference to this object.
     */
    public Resource supportedActions(final Action... actions) {
        this.supportedActions.addAll(Arrays.asList(actions));
        return this;
    }
    /**
     * Specifies a parent-child relationship with another resource. Sub-resources are inherited by sub-types and may
     * be overridden.
     *
     * @param subResource
     *         The sub-resource definition.
     * @return A reference to this object.
     */
    public Resource subResource(final SubResource subResource) {
        this.subResources.add(subResource);
        return this;
    }
    /**
     * Specifies a parent-child relationship with zero or more resources. Sub-resources are inherited by sub-types and
     * may be overridden.
     *
     * @param subResources
     *         The sub-resource definitions.
     * @return A reference to this object.
     */
    public Resource subResources(final SubResource... subResources) {
        this.subResources.addAll(asList(subResources));
        return this;
    }
    boolean hasSupportedAction(final Action action) {
        return supportedActions.contains(action);
    }
    boolean hasSubTypes() {
        return !subTypes.isEmpty();
    }
    boolean mayHaveSubResources() {
        return !subResources.isEmpty() || hasSubTypesWithSubResources();
    }
    boolean hasSubTypesWithSubResources() {
        if (hasSubTypesWithSubResources == null) {
            for (final Resource subType : subTypes) {
                if (!subType.subResources.isEmpty() || subType.hasSubTypesWithSubResources()) {
                    hasSubTypesWithSubResources = true;
                    return true;
                }
            }
            hasSubTypesWithSubResources = false;
        }
        return hasSubTypesWithSubResources;
    }
    Set<Resource> getSubTypes() {
        return subTypes;
    }
    Resource resolveSubTypeFromJson(final JsonValue content) throws ResourceException {
        if (!hasSubTypes()) {
            // The resource type is implied because this resource does not have sub-types. In particular, resources
            // are not required to have type information if they don't have sub-types.
            return this;
        }
        final JsonValue jsonType = content.get(resourceTypeProperty);
        if (jsonType == null || !jsonType.isString()) {
            throw newBadRequestException(ERR_MISSING_TYPE_PROPERTY_IN_CREATE.get(resourceTypeProperty));
        }
        final String type = jsonType.asString();
        final Resource subType = resolveSubTypeFromString(type);
        if (subType == null) {
            throw newBadRequestException(ERR_UNRECOGNIZED_TYPE_IN_CREATE.get(type, getAllowedResourceTypes()));
        }
        if (subType.isAbstract) {
            throw newBadRequestException(ERR_ABSTRACT_TYPE_IN_CREATE.get(type, getAllowedResourceTypes()));
        }
        return subType;
    }
    private String getAllowedResourceTypes() {
        final List<String> allowedTypes = new ArrayList<>();
        getAllowedResourceTypes(allowedTypes);
        return joinAsString(", ", allowedTypes);
    }
    private void getAllowedResourceTypes(final List<String> allowedTypes) {
        if (!isAbstract) {
            allowedTypes.add(id);
        }
        for (final Resource subType : subTypes) {
            subType.getAllowedResourceTypes(allowedTypes);
        }
    }
    Resource resolveSubTypeFromString(final String type) {
        if (id.equalsIgnoreCase(type)) {
            return this;
        }
        for (final Resource subType : subTypes) {
            final Resource resolvedSubType = subType.resolveSubTypeFromString(type);
            if (resolvedSubType != null) {
                return resolvedSubType;
            }
        }
        return null;
    }
    Resource resolveSubTypeFromObjectClasses(final Entry entry) {
        if (!hasSubTypes()) {
            // This resource does not have sub-types.
            return this;
        }
        final Attribute objectClassesFromEntry = entry.getAttribute("objectClass");
        final Resource subType = resolveSubTypeFromObjectClasses(objectClassesFromEntry);
        if (subType == null) {
            // Best effort.
            return this;
        }
        return subType;
    }
    private Resource resolveSubTypeFromObjectClasses(final Attribute objectClassesFromEntry) {
        if (!objectClassesFromEntry.containsAll(objectClasses)) {
            return null;
        }
        // This resource is a potential match, but sub-types may be better.
        for (final Resource subType : subTypes) {
            final Resource resolvedSubType = subType.resolveSubTypeFromObjectClasses(objectClassesFromEntry);
            if (resolvedSubType != null) {
                return resolvedSubType;
            }
        }
        return this;
    }
    Attribute getObjectClassAttribute() {
        return objectClasses;
    }
    RequestHandler getSubResourceRouter() {
        return subResourceRouter;
    }
    String getResourceId() {
        return id;
    }
    void build(final Rest2Ldap rest2Ldap) {
        // Prevent re-entrant calls.
        if (isBuilt) {
            return;
        }
        isBuilt = true;
        if (superTypeId != null) {
            superType = rest2Ldap.getResource(superTypeId);
            if (superType == null) {
                throw new LocalizedIllegalArgumentException(ERR_UNRECOGNIZED_RESOURCE_SUPER_TYPE.get(id, superTypeId));
            }
            // Inherit content from super-type.
            superType.build(rest2Ldap);
            superType.subTypes.add(this);
            if (resourceTypeProperty == null) {
                resourceTypeProperty = superType.resourceTypeProperty;
            }
            objectClasses.addAll(superType.objectClasses);
            subResourceRouter.addAllRoutes(superType.subResourceRouter);
            allProperties.putAll(superType.allProperties);
        }
        allProperties.putAll(declaredProperties);
        for (final Map.Entry<String, PropertyMapper> property : allProperties.entrySet()) {
            propertyMapper.property(property.getKey(), property.getValue());
        }
        for (final SubResource subResource : subResources) {
            subResource.build(rest2Ldap, id);
            subResource.addRoutes(subResourceRouter);
        }
    }
    PropertyMapper getPropertyMapper() {
        return propertyMapper;
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ResourceTypePropertyMapper.java
New file
@@ -0,0 +1,123 @@
/*
 * 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 2016 ForgeRock AS.
 *
 */
package org.forgerock.opendj.rest2ldap;
import static java.util.Collections.singletonList;
import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
import static org.forgerock.opendj.ldap.Filter.alwaysTrue;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_ILLEGAL_FILTER_ASSERTION_VALUE;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_MODIFY_READ_ONLY_FIELD;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_PATCH_READ_ONLY_FIELD;
import static org.forgerock.opendj.rest2ldap.Utils.isNullOrEmpty;
import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
import static org.forgerock.util.promise.Promises.newResultPromise;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.PatchOperation;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Modification;
import org.forgerock.util.promise.Promise;
/**
 * A property mapper which maps a single JSON property containing the resource type to the resource's object classes.
 */
final class ResourceTypePropertyMapper extends PropertyMapper {
    static final ResourceTypePropertyMapper INSTANCE = new ResourceTypePropertyMapper();
    private ResourceTypePropertyMapper() { }
    @Override
    public String toString() {
        return "type()";
    }
    @Override
    Promise<List<Attribute>, ResourceException> create(final Connection connection,
                                                       final Resource resource, final JsonPointer path,
                                                       final JsonValue v) {
        return newResultPromise(singletonList(resource.getObjectClassAttribute()));
    }
    @Override
    void getLdapAttributes(final JsonPointer path, final JsonPointer subPath, final Set<String> ldapAttributes) {
        ldapAttributes.add("objectClass");
    }
    @Override
    Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final Resource resource,
                                                     final JsonPointer path, final JsonPointer subPath,
                                                     final FilterType type, final String operator,
                                                     final Object valueAssertion) {
        if (subPath.isEmpty()) {
            switch (type) {
            case PRESENT:
                return newResultPromise(alwaysTrue());
            case EQUAL_TO:
                if (valueAssertion instanceof String) {
                    final Resource subType = resource.resolveSubTypeFromString((String) valueAssertion);
                    if (subType == null) {
                        return newResultPromise(alwaysFalse());
                    }
                    final List<Filter> subFilters = new ArrayList<>();
                    for (final ByteString objectClass : subType.getObjectClassAttribute()) {
                        subFilters.add(Filter.equality("objectClass", objectClass));
                    }
                    return newResultPromise(Filter.and(subFilters));
                }
                return newBadRequestException(ERR_ILLEGAL_FILTER_ASSERTION_VALUE.get(valueAssertion, path)).asPromise();
            default:
                return newResultPromise(alwaysFalse()); // Not supported.
            }
        } else {
            // This property mapper does not support partial filtering.
            return newResultPromise(alwaysFalse());
        }
    }
    @Override
    Promise<List<Modification>, ResourceException> patch(final Connection connection, final Resource resource,
                                                         final JsonPointer path, final PatchOperation operation) {
        return newBadRequestException(ERR_PATCH_READ_ONLY_FIELD.get(path)).asPromise();
    }
    @Override
    Promise<JsonValue, ResourceException> read(final Connection connection, final Resource resource,
                                               final JsonPointer path, final Entry e) {
        return newResultPromise(new JsonValue(resource.getResourceId()));
    }
    @Override
    Promise<List<Modification>, ResourceException> update(final Connection connection, final Resource resource,
                                                          final JsonPointer path, final Entry e, final JsonValue v) {
        if (!isNullOrEmpty(v) && !v.getObject().equals(resource.getResourceId())) {
            return newBadRequestException(ERR_MODIFY_READ_ONLY_FIELD.get("update", path)).asPromise();
        } else {
            return newResultPromise(Collections.<Modification>emptyList());
        }
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java
@@ -11,845 +11,206 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2013-2016 ForgeRock AS.
 * Copyright 2016 ForgeRock AS.
 *
 */
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
import static java.util.Arrays.asList;
import static org.forgerock.json.resource.ResourceException.newResourceException;
import static org.forgerock.opendj.ldap.Connections.newCachedConnectionPool;
import static org.forgerock.opendj.ldap.Connections.newFailoverLoadBalancer;
import static org.forgerock.opendj.ldap.Connections.newRoundRobinLoadBalancer;
import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
import static org.forgerock.opendj.ldap.Connections.LOAD_BALANCER_MONITORING_INTERVAL;
import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
import static org.forgerock.opendj.ldap.schema.CoreSchema.getEntryUUIDAttributeType;
import static org.forgerock.opendj.ldap.ResultCode.ADMIN_LIMIT_EXCEEDED;
import static org.forgerock.opendj.ldap.ResultCode.ENTRY_ALREADY_EXISTS;
import static org.forgerock.opendj.ldap.ResultCode.SIZE_LIMIT_EXCEEDED;
import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS;
import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
import static org.forgerock.opendj.rest2ldap.Utils.newLocalizedIllegalArgumentException;
import static org.forgerock.opendj.rest2ldap.Utils.newJsonValueException;
import static org.forgerock.util.Reject.checkNotNull;
import static org.forgerock.util.time.Duration.*;
import static org.forgerock.opendj.ldap.KeyManagers.useSingleCertificate;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509KeyManager;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.CollectionResourceProvider;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.ForbiddenException;
import org.forgerock.json.resource.InternalServerErrorException;
import org.forgerock.json.resource.NotFoundException;
import org.forgerock.json.resource.PermanentException;
import org.forgerock.json.resource.PreconditionFailedException;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.RetryableException;
import org.forgerock.json.resource.Router;
import org.forgerock.json.resource.ServiceUnavailableException;
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.Connection;
import org.forgerock.opendj.ldap.ConnectionException;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.ConstraintViolationException;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.DecodeOptions;
import org.forgerock.opendj.ldap.EntryNotFoundException;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.LDAPConnectionFactory;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.LinkedAttribute;
import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
import org.forgerock.opendj.ldap.RDN;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SSLContextBuilder;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.TimeoutResultException;
import org.forgerock.opendj.ldap.requests.BindRequest;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.schema.AttributeType;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.util.Option;
import org.forgerock.util.Options;
import org.forgerock.util.Reject;
import org.forgerock.util.time.Duration;
/** Provides core factory methods and builders for constructing LDAP resource collections. */
/**
 * Provides methods for constructing Rest2Ldap protocol gateways. Applications construct a new Rest2Ldap
 * instance by calling {@link #rest2Ldap} passing in a list of {@link Resource resources} which together define
 * the data model being exposed by the gateway. Call {@link #newRequestHandlerFor(String)} in order to obtain
 * a request handler for a specific resource. The methods in this class can be categorized as follows:
 * <p/>
 * Creating Rest2Ldap gateways:
 * <ul>
 * <li>{@link #rest2Ldap} - creates a gateway for a given set of resources</li>
 * <li>{@link #newRequestHandlerFor} - obtains a request handler for the specified endpoint resource.</li>
 * </ul>
 * <p/>
 * Defining resource types, e.g. users, groups, devices, etc:
 * <ul>
 * <li>{@link #resource} - creates a resource having a fluent API for defining additional characteristics
 * such as the resource's inheritance, sub-resources, and properties</li>
 * </ul>
 * <p/>
 * Defining a resource's sub-resources. A sub-resource is a resource which is subordinate to another resource. Or, to
 * put it another way, sub-resources define parent child relationships where the life-cycle of a child resource is
 * constrained by the life-cycle of the parent: deleting the parent implies that all children are deleted as well. An
 * example of a sub-resource is a subscriber having one or more devices:
 * <ul>
 * <li>{@link #collectionOf} - creates a one-to-many relationship. Collections support creation, deletion,
 * and querying of child resources</li>
 * <li>{@link #singletonOf} - creates a one-to-one relationship. Singletons cannot be created or destroyed,
 * although they may be modified if they have properties which are modifiable. Singletons are usually only used as
 * top-level entry points into REST APIs.
 * </li>
 * </ul>
 * <p/>
 * Defining a resource's properties:
 * <ul>
 * <li>{@link #resourceType} - defines a property whose JSON value will be the name of the resource, e.g. "user"</li>
 * <li>{@link #simple} - defines a property which maps a JSON value to a single LDAP attribute</li>
 * <li>{@link #object} - defines a property which is a JSON object having zero or more nested properties</li>
 * <li>{@link #reference} - defines a property whose JSON value is a reference to another resource. Use these for
 * mapping LDAP attributes which contain the DN of another LDAP entry exposed by Rest2Ldap. For example, a user's
 * "manager" attribute or the members of a group.</li>
 * </ul>
 */
public final class Rest2Ldap {
    /** Indicates whether LDAP client connections should use SSL or StartTLS. */
    private enum ConnectionSecurity {
        NONE, SSL, STARTTLS
    }
    /**
     * Specifies the mechanism which should be used for trusting certificates
     * presented by the LDAP server.
     * Specifies the LDAP decoding options which should be used when decoding LDAP DNs, attribute types, and controls.
     * By default Rest2Ldap will use a set of options of will always use the default schema.
     */
    enum TrustManagerType {
        TRUSTALL, JVM, FILE
    }
    public static final Option<DecodeOptions> DECODE_OPTIONS = Option.withDefault(new DecodeOptions());
    /**
     * Specifies the mechanism which manage which X509 certificate-based key pairs should be used to authenticate the
     * local side of a secure socket.
     * Specifies whether Rest2Ldap should support multi-version concurrency control (MVCC) through the use of an MVCC
     * LDAP {@link #MVCC_ATTRIBUTE attribute} such as "etag". By default Rest2Ldap will use MVCC.
     */
    enum KeyManagerType {
        JVM, KEYSTORE, PKCS11
    }
    /** A builder for incrementally constructing LDAP resource collections. */
    public static final class Builder {
        private final List<Attribute> additionalLDAPAttributes = new LinkedList<>();
        private DN baseDN; // TODO: support template variables.
        private AttributeDescription etagAttribute;
        private NamingStrategy namingStrategy;
        private ReadOnUpdatePolicy readOnUpdatePolicy = CONTROLS;
        private PropertyMapper rootMapper;
        private Schema schema = Schema.getDefaultSchema();
        private boolean usePermissiveModify;
        private boolean useSubtreeDelete;
        private Builder() {
            useEtagAttribute();
            useClientDNNaming("uid");
        }
        /**
         * Specifies an additional LDAP attribute which should be included with
         * new LDAP entries when they are created. Use this method to specify
         * the LDAP objectClass attribute.
         *
         * @param attribute
         *            The additional LDAP attribute to be included with new LDAP
         *            entries.
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder additionalLDAPAttribute(final Attribute attribute) {
            additionalLDAPAttributes.add(attribute);
            return this;
        }
        /**
         * Specifies an additional LDAP attribute which should be included with
         * new LDAP entries when they are created. Use this method to specify
         * the LDAP objectClass attribute.
         *
         * @param attribute
         *            The name of the additional LDAP attribute to be included
         *            with new LDAP entries.
         * @param values
         *            The value(s) of the additional LDAP attribute.
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder additionalLDAPAttribute(final String attribute, final Object... values) {
            return additionalLDAPAttribute(new LinkedAttribute(ad(attribute), values));
        }
        /**
         * Sets the base DN beneath which LDAP entries (resources) are to be found.
         *
         * @param dn
         *            The base DN.
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder baseDN(final DN dn) {
            Reject.ifNull(dn);
            this.baseDN = dn;
            return this;
        }
        /**
         * Sets the base DN beneath which LDAP entries (resources) are to be found.
         *
         * @param dn
         *            The base DN.
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder baseDN(final String dn) {
            return baseDN(DN.valueOf(dn, schema));
        }
        /**
         * Creates a new LDAP resource collection configured using this builder.
         *
         * @return The new LDAP resource collection.
         */
        public CollectionResourceProvider build() {
            Reject.ifNull(baseDN);
            if (rootMapper == null) {
                throw new IllegalStateException(ERR_CONFIG_NO_MAPPINGS_PROVIDED.get().toString());
            }
            return new SubResourceImpl(baseDN, rootMapper, namingStrategy, etagAttribute,
                                       new Config(readOnUpdatePolicy, useSubtreeDelete, usePermissiveModify, schema),
                                       additionalLDAPAttributes);
        }
        /**
         * Configures the JSON to LDAP mapping using the provided JSON
         * configuration. The caller is still required to set the connection
         * factory. See the sample configuration file for a detailed description
         * of its content.
         *
         * @param configuration
         *            The JSON configuration.
         * @return A reference to this LDAP resource collection builder.
         * @throws IllegalArgumentException
         *             If the configuration is invalid.
         */
        public Builder configureMapping(final JsonValue configuration) {
            baseDN(configuration.get("baseDN").required().asString());
            final JsonValue readOnUpdatePolicy = configuration.get("readOnUpdatePolicy");
            if (!readOnUpdatePolicy.isNull()) {
                readOnUpdatePolicy(readOnUpdatePolicy.asEnum(ReadOnUpdatePolicy.class));
            }
            for (final JsonValue v : configuration.get("additionalLDAPAttributes")) {
                final String type = v.get("type").required().asString();
                final List<Object> values = v.get("values").required().asList();
                additionalLDAPAttribute(new LinkedAttribute(type, values));
            }
            final JsonValue namingStrategy = configuration.get("namingStrategy");
            if (!namingStrategy.isNull()) {
                final String name = namingStrategy.get("strategy").required().asString();
                if (name.equalsIgnoreCase("clientDNNaming")) {
                    useClientDNNaming(namingStrategy.get("dnAttribute").required().asString());
                } else if (name.equalsIgnoreCase("clientNaming")) {
                    useClientNaming(namingStrategy.get("dnAttribute").required().asString(),
                            namingStrategy.get("idAttribute").required().asString());
                } else if (name.equalsIgnoreCase("serverNaming")) {
                    useServerNaming(namingStrategy.get("dnAttribute").required().asString(),
                            namingStrategy.get("idAttribute").required().asString());
                } else {
                    throw newLocalizedIllegalArgumentException(ERR_CONFIG_UNKNOWN_NAMING_CONFIGURATION.get(
                            namingStrategy.asString(), "clientDNNaming, clientNaming or serverNaming"));
                }
            }
            final JsonValue etagAttribute = configuration.get("etagAttribute");
            if (!etagAttribute.isNull()) {
                useEtagAttribute(etagAttribute.asString());
            }
            /*
             * Default to false, even though it is supported by OpenDJ, because
             * it requires additional permissions.
             */
            if (configuration.get("useSubtreeDelete").defaultTo(false).asBoolean()) {
                useSubtreeDelete();
            }
            /*
             * Default to true because it is supported by OpenDJ and does not
             * require additional permissions.
             */
            if (configuration.get("usePermissiveModify").defaultTo(true).asBoolean()) {
                usePermissiveModify();
            }
            mapper(configureObjectMapper(configuration.get("attributes").required()));
            return this;
        }
        /**
         * Sets the property mapper which should be used for mapping JSON
         * resources to and from LDAP entries.
         *
         * @param mapper
         *            The property mapper.
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder mapper(final PropertyMapper mapper) {
            this.rootMapper = mapper;
            return this;
        }
        /**
         * Sets the policy which should be used in order to read an entry before
         * it is deleted, or after it is added or modified. The default read on
         * update policy is to use {@link ReadOnUpdatePolicy#CONTROLS controls}.
         *
         * @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.
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder readOnUpdatePolicy(final ReadOnUpdatePolicy policy) {
            this.readOnUpdatePolicy = checkNotNull(policy);
            return this;
        }
        /**
         * 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.
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder schema(final Schema schema) {
            this.schema = checkNotNull(schema);
            return this;
        }
        /**
         * Indicates that the JSON resource ID must be provided by the user, and
         * will be used for naming the associated LDAP entry. More specifically,
         * LDAP entry names will be derived by appending a single RDN to the
         * {@link #baseDN(String) base DN} composed of the specified attribute
         * type and LDAP value taken from the LDAP entry once attribute mapping
         * has been performed.
         * <p>
         * Note that this naming policy requires that the user provides the
         * resource name when creating new resources, which means it must be
         * included in the resource content when not specified explicitly in the
         * create request.
         *
         * @param attribute
         *            The LDAP attribute which will be used for naming.
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder useClientDNNaming(final AttributeType attribute) {
            this.namingStrategy = new DNNamingStrategy(attribute);
            return this;
        }
        /**
         * Indicates that the JSON resource ID must be provided by the user, and
         * will be used for naming the associated LDAP entry. More specifically,
         * LDAP entry names will be derived by appending a single RDN to the
         * {@link #baseDN(String) base DN} composed of the specified attribute
         * type and LDAP value taken from the LDAP entry once attribute mapping
         * has been performed.
         * <p>
         * Note that this naming policy requires that the user provides the
         * resource name when creating new resources, which means it must be
         * included in the resource content when not specified explicitly in the
         * create request.
         *
         * @param attribute
         *            The LDAP attribute which will be used for naming.
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder useClientDNNaming(final String attribute) {
            return useClientDNNaming(at(attribute));
        }
        /**
         * Indicates that the JSON resource ID must be provided by the user, but
         * will not be used for naming the associated LDAP entry. Instead the
         * JSON resource ID will be taken from the {@code idAttribute} in the
         * LDAP entry, and the LDAP entry name will be derived by appending a
         * single RDN to the {@link #baseDN(String) base DN} composed of the
         * {@code dnAttribute} taken from the LDAP entry once attribute mapping
         * has been performed.
         * <p>
         * Note that this naming policy requires that the user provides the
         * resource name when creating new resources, which means it must be
         * included in the resource content when not specified explicitly in the
         * create request.
         *
         * @param dnAttribute
         *            The attribute which will be used for naming LDAP entries.
         * @param idAttribute
         *            The attribute which will be used for JSON resource IDs.
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder useClientNaming(final AttributeType dnAttribute,
                final AttributeDescription idAttribute) {
            this.namingStrategy = new AttributeNamingStrategy(dnAttribute, idAttribute, false);
            return this;
        }
        /**
         * Indicates that the JSON resource ID must be provided by the user, but
         * will not be used for naming the associated LDAP entry. Instead the
         * JSON resource ID will be taken from the {@code idAttribute} in the
         * LDAP entry, and the LDAP entry name will be derived by appending a
         * single RDN to the {@link #baseDN(String) base DN} composed of the
         * {@code dnAttribute} taken from the LDAP entry once attribute mapping
         * has been performed.
         * <p>
         * Note that this naming policy requires that the user provides the
         * resource name when creating new resources, which means it must be
         * included in the resource content when not specified explicitly in the
         * create request.
         *
         * @param dnAttribute
         *            The attribute which will be used for naming LDAP entries.
         * @param idAttribute
         *            The attribute which will be used for JSON resource IDs.
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder useClientNaming(final String dnAttribute, final String idAttribute) {
            return useClientNaming(at(dnAttribute), ad(idAttribute));
        }
        /**
         * Indicates that the "etag" LDAP attribute should be used for resource
         * versioning. This is the default behavior.
         *
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder useEtagAttribute() {
            return useEtagAttribute("etag");
        }
        /**
         * Indicates that the provided LDAP attribute should be used for
         * resource versioning. The "etag" attribute will be used by default.
         *
         * @param attribute
         *            The name of the attribute to use for versioning, or
         *            {@code null} if resource versioning will not supported.
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder useEtagAttribute(final AttributeDescription attribute) {
            this.etagAttribute = attribute;
            return this;
        }
        /**
         * Indicates that the provided LDAP attribute should be used for
         * resource versioning. The "etag" attribute will be used by default.
         *
         * @param attribute
         *            The name of the attribute to use for versioning, or
         *            {@code null} if resource versioning will not supported.
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder useEtagAttribute(final String attribute) {
            return useEtagAttribute(attribute != null ? ad(attribute) : null);
        }
        /**
         * Indicates that all LDAP modify operations should be performed using
         * the LDAP permissive modify control. The default behavior is to not
         * use the permissive modify control. Use of the control is strongly
         * recommended.
         *
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder usePermissiveModify() {
            this.usePermissiveModify = true;
            return this;
        }
        /**
         * Indicates that the JSON resource ID will be derived from the server
         * provided "entryUUID" LDAP attribute. The LDAP entry name will be
         * derived by appending a single RDN to the {@link #baseDN(String) base
         * DN} composed of the {@code dnAttribute} taken from the LDAP entry
         * once attribute mapping has been performed.
         * <p>
         * Note that this naming policy requires that the server provides the
         * resource name when creating new resources, which means it must not be
         * specified in the create request, nor included in the resource
         * content.
         *
         * @param dnAttribute
         *            The attribute which will be used for naming LDAP entries.
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder useServerEntryUUIDNaming(final AttributeType dnAttribute) {
            return useServerNaming(dnAttribute, AttributeDescription
                    .create(getEntryUUIDAttributeType()));
        }
        /**
         * Indicates that the JSON resource ID will be derived from the server
         * provided "entryUUID" LDAP attribute. The LDAP entry name will be
         * derived by appending a single RDN to the {@link #baseDN(String) base
         * DN} composed of the {@code dnAttribute} taken from the LDAP entry
         * once attribute mapping has been performed.
         * <p>
         * Note that this naming policy requires that the server provides the
         * resource name when creating new resources, which means it must not be
         * specified in the create request, nor included in the resource
         * content.
         *
         * @param dnAttribute
         *            The attribute which will be used for naming LDAP entries.
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder useServerEntryUUIDNaming(final String dnAttribute) {
            return useServerEntryUUIDNaming(at(dnAttribute));
        }
        /**
         * Indicates that the JSON resource ID must not be provided by the user,
         * and will not be used for naming the associated LDAP entry. Instead
         * the JSON resource ID will be taken from the {@code idAttribute} in
         * the LDAP entry, and the LDAP entry name will be derived by appending
         * a single RDN to the {@link #baseDN(String) base DN} composed of the
         * {@code dnAttribute} taken from the LDAP entry once attribute mapping
         * has been performed.
         * <p>
         * Note that this naming policy requires that the server provides the
         * resource name when creating new resources, which means it must not be
         * specified in the create request, nor included in the resource
         * content.
         *
         * @param dnAttribute
         *            The attribute which will be used for naming LDAP entries.
         * @param idAttribute
         *            The attribute which will be used for JSON resource IDs.
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder useServerNaming(final AttributeType dnAttribute,
                final AttributeDescription idAttribute) {
            this.namingStrategy = new AttributeNamingStrategy(dnAttribute, idAttribute, true);
            return this;
        }
        /**
         * Indicates that the JSON resource ID must not be provided by the user,
         * and will not be used for naming the associated LDAP entry. Instead
         * the JSON resource ID will be taken from the {@code idAttribute} in
         * the LDAP entry, and the LDAP entry name will be derived by appending
         * a single RDN to the {@link #baseDN(String) base DN} composed of the
         * {@code dnAttribute} taken from the LDAP entry once attribute mapping
         * has been performed.
         * <p>
         * Note that this naming policy requires that the server provides the
         * resource name when creating new resources, which means it must not be
         * specified in the create request, nor included in the resource
         * content.
         *
         * @param dnAttribute
         *            The attribute which will be used for naming LDAP entries.
         * @param idAttribute
         *            The attribute which will be used for JSON resource IDs.
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder useServerNaming(final String dnAttribute, final String idAttribute) {
            return useServerNaming(at(dnAttribute), ad(idAttribute));
        }
        /**
         * Indicates that all LDAP delete operations should be performed using
         * the LDAP subtree delete control. The default behavior is to not use
         * the subtree delete control.
         *
         * @return A reference to this LDAP resource collection builder.
         */
        public Builder useSubtreeDelete() {
            this.useSubtreeDelete = true;
            return this;
        }
        private AttributeDescription ad(final String attribute) {
            return AttributeDescription.valueOf(attribute, schema);
        }
        private AttributeType at(final String attribute) {
            return schema.getAttributeType(attribute);
        }
        private PropertyMapper configureMapper(final JsonValue mapper) {
            if (mapper.isDefined("constant")) {
                return constant(mapper.get("constant").getObject());
            } else if (mapper.isDefined("simple")) {
                final JsonValue config = mapper.get("simple");
                final SimplePropertyMapper s =
                        simple(ad(config.get("ldapAttribute").required().asString()));
                if (config.isDefined("defaultJSONValue")) {
                    s.defaultJsonValue(config.get("defaultJSONValue").getObject());
                }
                if (config.get("isBinary").defaultTo(false).asBoolean()) {
                    s.isBinary();
                }
                if (config.get("isRequired").defaultTo(false).asBoolean()) {
                    s.isRequired();
                }
                if (config.get("isSingleValued").defaultTo(false).asBoolean()) {
                    s.isSingleValued();
                }
                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 DN baseDN = DN.valueOf(config.get("baseDN").required().asString(), schema);
                final AttributeDescription primaryKey =
                        ad(config.get("primaryKey").required().asString());
                final PropertyMapper m = configureMapper(config.get("mapper").required());
                final ReferencePropertyMapper r = reference(ldapAttribute, baseDN, primaryKey, m);
                if (config.get("isRequired").defaultTo(false).asBoolean()) {
                    r.isRequired();
                }
                if (config.get("isSingleValued").defaultTo(false).asBoolean()) {
                    r.isSingleValued();
                }
                if (config.isDefined("searchFilter")) {
                    r.searchFilter(config.get("searchFilter").asString());
                }
                r.writability(parseWritability(mapper, config));
                return r;
            } else if (mapper.isDefined("object")) {
                return configureObjectMapper(mapper.get("object"));
            } else {
                throw newJsonValueException(mapper, ERR_CONFIG_NO_MAPPING_IN_CONFIGURATION.get(
                        "constant, simple, reference or object"));
            }
        }
        private ObjectPropertyMapper configureObjectMapper(final JsonValue mapper) {
            final ObjectPropertyMapper object = object();
            for (final String attribute : mapper.keys()) {
                object.attribute(attribute, configureMapper(mapper.get(attribute)));
            }
            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 newJsonValueException(mapper, ERR_CONFIG_UNKNOWN_WRITABILITY.get(writability,
                                "readOnly, readOnlyDiscardWrites, createOnly, createOnlyDiscardWrites, or readWrite"));
                }
            } else {
                return WritabilityPolicy.READ_WRITE;
            }
        }
    }
    private static final class AttributeNamingStrategy extends NamingStrategy {
        private final AttributeDescription dnAttribute;
        private final AttributeDescription idAttribute;
        private final boolean isServerProvided;
        private AttributeNamingStrategy(final AttributeType dnAttribute,
                                        final AttributeDescription idAttribute, final boolean isServerProvided) {
            this.dnAttribute = AttributeDescription.create(dnAttribute);
            if (this.dnAttribute.equals(idAttribute)) {
                throw newLocalizedIllegalArgumentException(ERR_CONFIG_NAMING_STRATEGY_DN_AND_ID_NOT_DIFFERENT.get());
            }
            this.idAttribute = checkNotNull(idAttribute);
            this.isServerProvided = isServerProvided;
        }
        @Override
        SearchRequest createSearchRequest(final Connection connection, final DN baseDN, final String resourceId) {
            return newSearchRequest(baseDN, SearchScope.SINGLE_LEVEL, Filter.equality(idAttribute
                    .toString(), resourceId));
        }
        @Override
        void getLdapAttributes(final Connection connection, final Set<String> ldapAttributes) {
            ldapAttributes.add(idAttribute.toString());
        }
        @Override
        String getResourceId(final Connection connection, final Entry entry) {
            return entry.parseAttribute(idAttribute).asString();
        }
        @Override
        void setResourceId(final Connection connection, final DN baseDN, final String resourceId,
                final Entry entry) throws ResourceException {
            if (isServerProvided) {
                if (resourceId != null) {
                    throw newBadRequestException(ERR_CLIENT_PROVIDER_RESOURCE_ID_MISSING.get());
                }
            } else {
                entry.addAttribute(new LinkedAttribute(idAttribute, ByteString.valueOfUtf8(resourceId)));
            }
            final String rdnValue = entry.parseAttribute(dnAttribute).asString();
            final RDN rdn = new RDN(dnAttribute.getAttributeType(), rdnValue);
            entry.setName(baseDN.child(rdn));
        }
    }
    private static final class DNNamingStrategy extends NamingStrategy {
        private final AttributeDescription attribute;
        private DNNamingStrategy(final AttributeType attribute) {
            this.attribute = AttributeDescription.create(attribute);
        }
        @Override
        SearchRequest createSearchRequest(final Connection connection, final DN baseDN, final String resourceId) {
            return newSearchRequest(baseDN.child(rdn(resourceId)), SearchScope.BASE_OBJECT, Filter
                    .objectClassPresent());
        }
        @Override
        void getLdapAttributes(final Connection connection, final Set<String> ldapAttributes) {
            ldapAttributes.add(attribute.toString());
        }
        @Override
        String getResourceId(final Connection connection, final Entry entry) {
            return entry.parseAttribute(attribute).asString();
        }
        @Override
        void setResourceId(final Connection connection, final DN baseDN, final String resourceId,
                final Entry entry) throws ResourceException {
            if (resourceId != null) {
                entry.setName(baseDN.child(rdn(resourceId)));
                entry.addAttribute(new LinkedAttribute(attribute, ByteString.valueOfUtf8(resourceId)));
            } else if (entry.getAttribute(attribute) != null) {
                entry.setName(baseDN.child(rdn(entry.parseAttribute(attribute).asString())));
            } else {
                throw newBadRequestException(ERR_CLIENT_PROVIDER_RESOURCE_ID_MISSING.get());
            }
        }
        private RDN rdn(final String resourceId) {
            return new RDN(attribute.getAttributeType(), resourceId);
        }
    }
    public static final Option<Boolean> USE_MVCC = Option.withDefault(true);
    /**
     * Specifies the name of the LDAP attribute which should be used for multi-version concurrency control (MVCC) if
     * {@link #USE_MVCC enabled}. By default Rest2Ldap will use the "etag" operational attribute.
     */
    public static final Option<String> MVCC_ATTRIBUTE = Option.withDefault("etag");
    /**
     * Specifies the policy which should be used in order to read an entry before it is deleted, or after it is added or
     * modified. By default Rest2Ldap will use the {@link ReadOnUpdatePolicy#CONTROLS controls} read on update policy.
     */
    public static final Option<ReadOnUpdatePolicy> READ_ON_UPDATE_POLICY = Option.withDefault(CONTROLS);
    /**
     * Specifies whether Rest2Ldap should perform LDAP modify operations using the LDAP permissive modify
     * control. By default Rest2Ldap will use the permissive modify control and use of the control is strongly
     * recommended.
     */
    public static final Option<Boolean> USE_PERMISSIVE_MODIFY = Option.withDefault(true);
    /**
     * Specifies whether Rest2Ldap should perform LDAP delete operations using the LDAP subtree delete control. By
     * default Rest2Ldap will use the subtree delete control and use of the control is strongly recommended.
     */
    public static final Option<Boolean> USE_SUBTREE_DELETE = Option.withDefault(true);
    /**
     * Adapts a {@code Throwable} to a {@code ResourceException}. If the
     * {@code Throwable} is an LDAP {@link LdapException} then an
     * appropriate {@code ResourceException} is returned, otherwise an
     * {@code InternalServerErrorException} is returned.
     * Creates a new {@link Rest2Ldap} instance using the provided options and {@link Resource resources}.
     * Applications should call {@link #newRequestHandlerFor(String)} to obtain a request handler for a specific
     * resource.
     * <p>
     * The supported options are defined in this class.
     *
     * @param t
     *            The {@code Throwable} to be converted.
     * @return The equivalent resource exception.
     * @param options The configuration options for interactions with the backend LDAP server. The set of available
     *                options are provided in this class.
     * @param resources The list of resources.
     * @return A new Rest2Ldap instance from which REST request handlers can be obtained.
     */
    public static ResourceException asResourceException(final Throwable t) {
        int resourceResultCode;
        try {
            throw t;
        } catch (final ResourceException e) {
            return e;
        } catch (final AssertionFailureException e) {
            resourceResultCode = ResourceException.VERSION_MISMATCH;
        } catch (final ConstraintViolationException e) {
            final ResultCode rc = e.getResult().getResultCode();
            if (rc.equals(ResultCode.ENTRY_ALREADY_EXISTS)) {
                resourceResultCode = ResourceException.VERSION_MISMATCH; // Consistent with MVCC.
            } else {
                // Schema violation, etc.
                resourceResultCode = ResourceException.BAD_REQUEST;
            }
        } 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 LdapException 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;
            }
        } catch (final Throwable tmp) {
            resourceResultCode = ResourceException.INTERNAL_ERROR;
        }
        return newResourceException(resourceResultCode, t.getMessage(), t);
    public static Rest2Ldap rest2Ldap(final Options options, final Collection<Resource> resources) {
        return new Rest2Ldap(options, resources);
    }
    /**
     * Returns a builder for incrementally constructing LDAP resource
     * collections.
     * Creates a new {@link Rest2Ldap} instance using the provided options and {@link Resource resources}.
     * Applications should call {@link #newRequestHandlerFor(String)} to obtain a request handler for a specific
     * resource.
     * <p>
     * The supported options are defined in this class.
     *
     * @return An LDAP resource collection builder.
     * @param options The configuration options for interactions with the backend LDAP server. The set of available
     *                options are provided in this class.
     * @param resources The list of resources.
     * @return A new Rest2Ldap instance from which REST request handlers can be obtained.
     */
    public static Builder builder() {
        return new Builder();
    public static Rest2Ldap rest2Ldap(final Options options, final Resource... resources) {
        return rest2Ldap(options, Arrays.asList(resources));
    }
    /**
     * Creates a new connection factory using the named configuration in the
     * provided JSON list of factory configurations. See the sample
     * configuration file for a detailed description of its content.
     * Creates a new {@link Resource resource} definition with the provided resource ID.
     *
     * @param configuration
     *            The JSON configuration.
     * @param name
     *            The name of the connection factory configuration to be parsed.
     * @param trustManager
     *            The trust manager to use for secure connection. Can be {@code null}
     *            to use the default JVM trust manager.
     * @param keyManager
     *            The key manager to use for secure connection. Can be {@code null}
     *            to use the default JVM key manager.
     * @param providerClassLoader
     *            The {@link ClassLoader} used to fetch the
     *            {@link org.forgerock.opendj.ldap.spi.TransportProvider}.
     *            This can be useful in OSGI environments.
     * @return A new connection factory using the provided JSON configuration.
     * @throws IllegalArgumentException
     *             If the configuration is invalid.
     * @param resourceId
     *         The resource ID.
     * @return A new resource definition with the provided resource ID.
     */
    public static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
                                                               final String name,
                                                               final TrustManager trustManager,
                                                               final X509KeyManager keyManager,
                                                               final ClassLoader providerClassLoader) {
        final JsonValue normalizedConfiguration =
                normalizeConnectionFactory(configuration, name, 0);
        return configureConnectionFactory(normalizedConfiguration, trustManager, keyManager, providerClassLoader);
    public static Resource resource(final String resourceId) {
        return new Resource(resourceId);
    }
    /**
     * Creates a new connection factory using the named configuration in the
     * provided JSON list of factory configurations. See the sample
     * configuration file for a detailed description of its content.
     * Creates a new {@link SubResourceCollection collection} sub-resource definition whose members will be resources
     * having the provided resource ID or its sub-types.
     *
     * @param configuration
     *            The JSON configuration.
     * @param name
     *            The name of the connection factory configuration to be parsed.
     * @param trustManager
     *            The trust manager to use for secure connection. Can be {@code null}
     *            to use the default JVM trust manager.
     * @param keyManager
     *            The key manager to use for secure connection. Can be {@code null}
     *            to use the default JVM key manager.
     * @return A new connection factory using the provided JSON configuration.
     * @throws IllegalArgumentException
     *             If the configuration is invalid.
     * @param resourceId
     *         The type of resource contained in the sub-resource collection.
     * @return A new sub-resource definition with the provided resource ID.
     */
    public static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
            final String name, final TrustManager trustManager, final X509KeyManager keyManager) {
        return configureConnectionFactory(configuration, name, trustManager, keyManager, null);
    public static SubResourceCollection collectionOf(final String resourceId) {
        return new SubResourceCollection(resourceId);
    }
    /**
     * Returns an property mapper which maps a single JSON attribute to a JSON
     * constant.
     * Creates a new {@link SubResourceSingleton singleton} sub-resource definition which will reference a single
     * resource having the specified resource ID.
     *
     * @param resourceId
     *         The type of resource referenced by the sub-resource singleton.
     * @return A new sub-resource definition with the provided resource ID.
     */
    public static SubResourceSingleton singletonOf(final String resourceId) {
        return new SubResourceSingleton(resourceId);
    }
    /**
     * Returns a property mapper which maps a JSON property containing the resource type to its associated LDAP
     * object classes.
     *
     * @return The property mapper.
     */
    public static PropertyMapper resourceType() {
        return ResourceTypePropertyMapper.INSTANCE;
    }
    /**
     * Returns a property mapper which maps a single JSON attribute to a JSON constant.
     *
     * @param value
     *            The constant JSON value (a Boolean, Number, String, Map, or
     *            List).
     *         The constant JSON value (a Boolean, Number, String, Map, or List).
     * @return The property mapper.
     */
    public static PropertyMapper constant(final Object value) {
@@ -857,7 +218,7 @@
    }
    /**
     * Returns an property mapper which maps JSON objects to LDAP attributes.
     * Returns a property mapper which maps JSON objects to LDAP attributes.
     *
     * @return The property mapper.
     */
@@ -866,55 +227,50 @@
    }
    /**
     * Returns an property mapper which provides a mapping from a JSON value to
     * a single DN valued LDAP attribute.
     * Returns a property mapper which provides a mapping from a JSON value to a single DN valued LDAP attribute.
     *
     * @param attribute
     *            The DN valued LDAP attribute to be mapped.
     *         The DN valued LDAP attribute to be mapped.
     * @param baseDN
     *            The search base DN for performing reverse lookups.
     *         The search base DN for performing reverse lookups.
     * @param primaryKey
     *            The search primary key LDAP attribute to use for performing
     *            reverse lookups.
     *         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.
     *         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, final AttributeDescription primaryKey,
    public static ReferencePropertyMapper reference(final AttributeDescription attribute, final DN baseDN,
                                                    final AttributeDescription primaryKey,
                                                    final PropertyMapper mapper) {
        return new ReferencePropertyMapper(Schema.getDefaultSchema(), attribute, baseDN, primaryKey, mapper);
    }
    /**
     * Returns an property mapper which provides a mapping from a JSON value to
     * a single DN valued LDAP attribute.
     * Returns a property mapper which provides a mapping from a JSON value to a single DN valued LDAP attribute.
     *
     * @param attribute
     *            The DN valued LDAP attribute to be mapped.
     *         The DN valued LDAP attribute to be mapped.
     * @param baseDN
     *            The search base DN for performing reverse lookups.
     *         The search base DN for performing reverse lookups.
     * @param primaryKey
     *            The search primary key LDAP attribute to use for performing
     *            reverse lookups.
     *         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.
     *         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,
                                                    final String primaryKey, final PropertyMapper mapper) {
        return reference(AttributeDescription.valueOf(attribute), DN.valueOf(baseDN),
                AttributeDescription.valueOf(primaryKey), mapper);
        return reference(AttributeDescription.valueOf(attribute),
                         DN.valueOf(baseDN),
                         AttributeDescription.valueOf(primaryKey),
                         mapper);
    }
    /**
     * Returns an property mapper which provides a simple mapping from a JSON
     * value to a single LDAP attribute.
     * Returns a property mapper which provides a simple mapping from a JSON value to a single LDAP attribute.
     *
     * @param attribute
     *            The LDAP attribute to be mapped.
     *         The LDAP attribute to be mapped.
     * @return The property mapper.
     */
    public static SimplePropertyMapper simple(final AttributeDescription attribute) {
@@ -922,142 +278,95 @@
    }
    /**
     * Returns an property mapper which provides a simple mapping from a JSON
     * value to a single LDAP attribute.
     * Returns a property mapper which provides a simple mapping from a JSON value to a single LDAP attribute.
     *
     * @param attribute
     *            The LDAP attribute to be mapped.
     *         The LDAP attribute to be mapped.
     * @return The property mapper.
     */
    public static SimplePropertyMapper simple(final String attribute) {
        return simple(AttributeDescription.valueOf(attribute));
    }
    private static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
                                                                final TrustManager trustManager,
                                                                final X509KeyManager keyManager,
                                                                final ClassLoader providerClassLoader) {
        final long heartBeatIntervalSeconds = configuration.get("heartBeatIntervalSeconds").defaultTo(30L).asLong();
        final Duration heartBeatInterval = duration(Math.max(heartBeatIntervalSeconds, 1L), TimeUnit.SECONDS);
        final long heartBeatTimeoutMillis = configuration.get("heartBeatTimeoutMilliSeconds").defaultTo(500L).asLong();
        final Duration heartBeatTimeout = duration(Math.max(heartBeatTimeoutMillis, 100L), TimeUnit.MILLISECONDS);
        final Options options = Options.defaultOptions()
                                       .set(TRANSPORT_PROVIDER_CLASS_LOADER, providerClassLoader)
                                       .set(HEARTBEAT_ENABLED, true)
                                       .set(HEARTBEAT_INTERVAL, heartBeatInterval)
                                       .set(HEARTBEAT_TIMEOUT, heartBeatTimeout)
                                       .set(LOAD_BALANCER_MONITORING_INTERVAL, heartBeatInterval);
        // Parse pool parameters,
        final int connectionPoolSize =
                Math.max(configuration.get("connectionPoolSize").defaultTo(10).asInteger(), 1);
        // Parse authentication parameters.
        if (configuration.isDefined("authentication")) {
            final JsonValue authn = configuration.get("authentication");
            if (authn.isDefined("simple")) {
                final JsonValue simple = authn.get("simple");
                final BindRequest bindRequest =
                        Requests.newSimpleBindRequest(simple.get("bindDN").required().asString(),
                                simple.get("bindPassword").required().asString().toCharArray());
                options.set(AUTHN_BIND_REQUEST, bindRequest);
    /**
     * Adapts a {@code Throwable} to a {@code ResourceException}. If the {@code Throwable} is an LDAP
     * {@link LdapException} then an appropriate {@code ResourceException} is returned, otherwise an {@code
     * InternalServerErrorException} is returned.
     * @param t
     *         The {@code Throwable} to be converted.
     * @return The equivalent resource exception.
     */
    public static ResourceException asResourceException(final Throwable t) {
        try {
            throw t;
        } catch (final ResourceException e) {
            return e;
        } catch (final AssertionFailureException e) {
            return new PreconditionFailedException(e);
        } catch (final ConstraintViolationException e) {
            final ResultCode rc = e.getResult().getResultCode();
            if (rc.equals(ENTRY_ALREADY_EXISTS)) {
                return new PreconditionFailedException(e); // Consistent with MVCC.
            } else {
                throw newLocalizedIllegalArgumentException(ERR_CONFIG_INVALID_AUTHENTICATION.get());
                return new BadRequestException(e); // Schema violation, etc.
            }
        }
        // Parse SSL/StartTLS parameters.
        final ConnectionSecurity connectionSecurity =
                configuration.get("connectionSecurity").defaultTo(ConnectionSecurity.NONE).asEnum(
                        ConnectionSecurity.class);
        if (connectionSecurity != ConnectionSecurity.NONE) {
            try {
                // Configure SSL.
                final SSLContextBuilder builder = new SSLContextBuilder();
                builder.setTrustManager(trustManager);
                final String sslCertAlias = configuration.get("sslCertAlias").asString();
                builder.setKeyManager(sslCertAlias != null
                        ? useSingleCertificate(sslCertAlias, keyManager)
                        : keyManager);
                options.set(SSL_CONTEXT, builder.getSSLContext());
                options.set(SSL_USE_STARTTLS, connectionSecurity == ConnectionSecurity.STARTTLS);
            } catch (GeneralSecurityException e) {
                // Rethrow as unchecked exception.
                throw new IllegalArgumentException(e);
            }
        }
        // Parse primary data center.
        final JsonValue primaryLDAPServers = configuration.get("primaryLDAPServers");
        if (!primaryLDAPServers.isList() || primaryLDAPServers.size() == 0) {
            throw new IllegalArgumentException("No primaryLDAPServers");
        }
        final ConnectionFactory primary = parseLDAPServers(primaryLDAPServers, connectionPoolSize, options);
        // Parse secondary data center(s).
        final JsonValue secondaryLDAPServers = configuration.get("secondaryLDAPServers");
        ConnectionFactory secondary = null;
        if (secondaryLDAPServers.isList()) {
            if (secondaryLDAPServers.size() > 0) {
                secondary = parseLDAPServers(secondaryLDAPServers, connectionPoolSize, options);
            }
        } else if (!secondaryLDAPServers.isNull()) {
            throw newLocalizedIllegalArgumentException(ERR_CONFIG_INVALID_SECONDARY_LDAP_SERVER.get());
        }
        // Create fail-over.
        if (secondary != null) {
            return newFailoverLoadBalancer(asList(primary, secondary), options);
        } else {
            return primary;
        }
    }
    private static JsonValue normalizeConnectionFactory(final JsonValue configuration,
            final String name, final int depth) {
        // Protect against infinite recursion in the configuration.
        if (depth > 100) {
            throw newLocalizedIllegalArgumentException(ERR_CONFIG_SERVER_CIRCULAR_DEPENDENCIES.get(name));
        }
        final JsonValue current = configuration.get(name).required();
        if (current.isDefined("inheritFrom")) {
            // Inherit missing fields from inherited configuration.
            final JsonValue parent =
                    normalizeConnectionFactory(configuration,
                            current.get("inheritFrom").asString(), depth + 1);
            final Map<String, Object> normalized = new LinkedHashMap<>(parent.asMap());
            normalized.putAll(current.asMap());
            normalized.remove("inheritFrom");
            return new JsonValue(normalized);
        } else {
            // No normalization required.
            return current;
        }
    }
    private static ConnectionFactory parseLDAPServers(JsonValue config, int poolSize, Options options) {
        final List<ConnectionFactory> servers = new ArrayList<>(config.size());
        for (final JsonValue server : config) {
            final String host = server.get("hostname").required().asString();
            final int port = server.get("port").required().asInteger();
            final ConnectionFactory factory = new LDAPConnectionFactory(host, port, options);
            if (poolSize > 1) {
                servers.add(newCachedConnectionPool(factory, 0, poolSize, 60L, TimeUnit.SECONDS));
        } catch (final AuthenticationException e) {
            return new PermanentException(401, null, e); // Unauthorized
        } catch (final AuthorizationException e) {
            return new ForbiddenException(e);
        } catch (final ConnectionException e) {
            return new ServiceUnavailableException(e);
        } catch (final EntryNotFoundException e) {
            return new NotFoundException(e);
        } catch (final MultipleEntriesFoundException e) {
            return new InternalServerErrorException(e);
        } catch (final TimeoutResultException e) {
            return new RetryableException(408, null, e); // Request Timeout
        } catch (final LdapException e) {
            final ResultCode rc = e.getResult().getResultCode();
            if (rc.equals(ADMIN_LIMIT_EXCEEDED) || rc.equals(SIZE_LIMIT_EXCEEDED)) {
                return new PermanentException(413, null, e); // Payload Too Large (Request Entity Too Large)
            } else {
                servers.add(factory);
                return new InternalServerErrorException(e);
            }
        }
        if (servers.size() > 1) {
            return newRoundRobinLoadBalancer(servers, options);
        } else {
            return servers.get(0);
        } catch (final Throwable tmp) {
            return new InternalServerErrorException(t);
        }
    }
    private Rest2Ldap() {
        // Prevent instantiation.
    private final Map<String, Resource> resources = new LinkedHashMap<>();
    private final Options options;
    private Rest2Ldap(final Options options, final Collection<Resource> resources) {
        this.options = options;
        for (final Resource resource : resources) {
            this.resources.put(resource.getResourceId(), resource);
        }
        // Now build the model.
        for (final Resource resource : resources) {
            resource.build(this);
        }
    }
    /**
     * Returns a {@link RequestHandler} which will handle requests to the named resource and any of its sub-resources.
     *
     * @param resourceId
     *         The resource ID.
     * @return A {@link RequestHandler} which will handle requests to the named resource.
     */
    public RequestHandler newRequestHandlerFor(final String resourceId) {
        Reject.ifTrue(!resources.containsKey(resourceId), "unrecognized resource '" + resourceId + "'");
        final SubResourceSingleton root = singletonOf(resourceId);
        root.build(this, null);
        return root.addRoutes(new Router());
    }
    Options getOptions() {
        return options;
    }
    Resource getResource(final String resourceId) {
        return resources.get(resourceId);
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapHttpApplication.java
@@ -16,39 +16,37 @@
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
import static org.forgerock.http.handler.HttpClientHandler.*;
import static org.forgerock.opendj.ldap.KeyManagers.*;
import static org.forgerock.opendj.ldap.TrustManagers.checkUsingTrustStore;
import static org.forgerock.opendj.ldap.TrustManagers.trustAll;
import static org.forgerock.http.util.Json.readJsonLenient;
import static org.forgerock.http.handler.HttpClientHandler.OPTION_KEY_MANAGERS;
import static org.forgerock.http.handler.HttpClientHandler.OPTION_TRUST_MANAGERS;
import static org.forgerock.json.JsonValueFunctions.duration;
import static org.forgerock.json.JsonValueFunctions.enumConstant;
import static org.forgerock.json.JsonValueFunctions.setOf;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.configureConnectionFactory;
import static org.forgerock.opendj.rest2ldap.Utils.newLocalizedIllegalArgumentException;
import static org.forgerock.json.resource.http.CrestHttp.newHttpHandler;
import static org.forgerock.opendj.ldap.KeyManagers.useSingleCertificate;
import static org.forgerock.opendj.rest2ldap.Rest2LdapJsonConfigurator.*;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
import static org.forgerock.opendj.rest2ldap.Utils.newJsonValueException;
import static org.forgerock.opendj.rest2ldap.authz.AuthenticationStrategies.*;
import static org.forgerock.opendj.rest2ldap.authz.AuthenticationStrategies.newSASLPlainStrategy;
import static org.forgerock.opendj.rest2ldap.authz.AuthenticationStrategies.newSearchThenBindStrategy;
import static org.forgerock.opendj.rest2ldap.authz.AuthenticationStrategies.newSimpleBindStrategy;
import static org.forgerock.opendj.rest2ldap.authz.Authorizations.*;
import static org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.*;
import static org.forgerock.opendj.rest2ldap.authz.CredentialExtractors.*;
import static org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.newConditionalFilter;
import static org.forgerock.opendj.rest2ldap.authz.CredentialExtractors.httpBasicExtractor;
import static org.forgerock.opendj.rest2ldap.authz.CredentialExtractors.newCustomHeaderExtractor;
import static org.forgerock.util.Reject.checkNotNull;
import static org.forgerock.util.Utils.closeSilently;
import static org.forgerock.util.Utils.joinAsString;
import static org.forgerock.opendj.rest2ldap.Utils.readPasswordFromFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@@ -56,11 +54,6 @@
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509KeyManager;
import org.forgerock.openig.oauth2.AccessTokenInfo;
import org.forgerock.openig.oauth2.AccessTokenException;
import org.forgerock.openig.oauth2.AccessTokenResolver;
import org.forgerock.openig.oauth2.resolver.CachingAccessTokenResolver;
import org.forgerock.openig.oauth2.resolver.OpenAmAccessTokenResolver;
import org.forgerock.http.Filter;
import org.forgerock.http.Handler;
import org.forgerock.http.HttpApplication;
@@ -71,20 +64,22 @@
import org.forgerock.http.io.Buffer;
import org.forgerock.http.protocol.Headers;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.Router;
import org.forgerock.json.resource.http.CrestHttp;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.rest2ldap.Rest2LDAP.KeyManagerType;
import org.forgerock.opendj.rest2ldap.Rest2LDAP.TrustManagerType;
import org.forgerock.opendj.rest2ldap.authz.AuthenticationStrategy;
import org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.ConditionalFilter;
import org.forgerock.openig.oauth2.AccessTokenException;
import org.forgerock.openig.oauth2.AccessTokenInfo;
import org.forgerock.openig.oauth2.AccessTokenResolver;
import org.forgerock.openig.oauth2.resolver.CachingAccessTokenResolver;
import org.forgerock.openig.oauth2.resolver.OpenAmAccessTokenResolver;
import org.forgerock.services.context.SecurityContext;
import org.forgerock.util.Factory;
import org.forgerock.util.Function;
@@ -116,8 +111,8 @@
    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
    /** URL to the JSON configuration file. */
    protected final URL configurationUrl;
    /** The name of the JSON configuration directory in which config.json and rest2ldap/rest2ldap.json are located. */
    protected final File configDirectory;
    /** Schema used to perform DN validations. */
    protected final Schema schema;
@@ -166,37 +161,47 @@
    }
    /**
     * Default constructor called by the HTTP Framework which will use the default configuration file location.
     * Default constructor called by the HTTP Framework which will use the default configuration directory.
     */
    public Rest2LdapHttpApplication() {
        this.configurationUrl = getClass().getResource("/opendj-rest2ldap-config.json");
        try {
            // The null check is required for unit test mocks because the resource does not exist.
            final URL configUrl = getClass().getResource("/config.json");
            this.configDirectory = configUrl != null ? new File(configUrl.toURI()).getParentFile() : null;
        } catch (final URISyntaxException e) {
            throw new IllegalStateException(e);
        }
        this.schema = Schema.getDefaultSchema();
    }
    /**
     * Creates a new Rest2LDAP HTTP application using the provided configuration URL.
     * Creates a new Rest2LDAP HTTP application using the provided configuration directory.
     *
     * @param configurationURL
     *            The URL to the JSON configuration file
     * @param configDirectory
     *         The name of the JSON configuration directory in which config.json and rest2ldap/rest2ldap.json are
     *         located.
     * @param schema
     *            The {@link Schema} used to perform DN validations
     *         The {@link Schema} used to perform DN validations
     */
    public Rest2LdapHttpApplication(final URL configurationURL, final Schema schema) {
        this.configurationUrl = checkNotNull(configurationURL, "configurationURL cannot be null");
    public Rest2LdapHttpApplication(final File configDirectory, final Schema schema) {
        this.configDirectory = checkNotNull(configDirectory, "configDirectory cannot be null");
        this.schema = checkNotNull(schema, "schema cannot be null");
    }
    @Override
    public final Handler start() throws HttpApplicationException {
        try {
            final JsonValue configuration = readJson(configurationUrl);
            logger.info(INFO_REST2LDAP_STARTING.get(configDirectory));
            executorService = Executors.newSingleThreadScheduledExecutor();
            configureSecurity(configuration.get("security"));
            configureConnectionFactories(configuration.get("ldapConnectionFactories"));
            return Handlers.chainOf(
                    CrestHttp.newHttpHandler(configureRest2Ldap(configuration)),
                    new ErrorLoggerFilter(),
                    buildAuthorizationFilter(configuration.get("authorization").required()));
            final JsonValue config = readJson(new File(configDirectory, "config.json"));
            configureSecurity(config.get("security"));
            configureConnectionFactories(config.get("ldapConnectionFactories"));
            final Filter authorizationFilter = buildAuthorizationFilter(config.get("authorization").required());
            return Handlers.chainOf(newHttpHandler(configureRest2Ldap(configDirectory)),
                                    new ErrorLoggerFilter(),
                                    authorizationFilter);
        } catch (final Exception e) {
            final LocalizableMessage errorMsg = ERR_FAIL_PARSE_CONFIGURATION.get(e.getLocalizedMessage());
            logger.error(errorMsg, e);
@@ -205,86 +210,16 @@
        }
    }
    private static JsonValue readJson(final URL resource) throws IOException {
        try (InputStream in = resource.openStream()) {
            return new JsonValue(readJsonLenient(in));
        }
    }
    private static RequestHandler configureRest2Ldap(final JsonValue configuration) {
        final JsonValue mappings = configuration.get("mappings").required();
        final Router router = new Router();
        for (final String mappingUrl : mappings.keys()) {
            final JsonValue mapping = mappings.get(mappingUrl);
            router.addRoute(Router.uriTemplate(mappingUrl), Rest2Ldap.builder().configureMapping(mapping).build());
        }
        return router;
    private static RequestHandler configureRest2Ldap(final File configDirectory) throws IOException {
        final File rest2LdapConfigDirectory = new File(configDirectory, "rest2ldap");
        final Options options = configureOptions(readJson(new File(rest2LdapConfigDirectory, "rest2ldap.json")));
        final File endpointsDirectory = new File(rest2LdapConfigDirectory, "endpoints");
        return configureEndpoints(endpointsDirectory, options);
    }
    private void configureSecurity(final JsonValue configuration) {
        try {
            trustManager = configureTrustManager(configuration, TrustManagerType.JVM);
        } catch (GeneralSecurityException | IOException e) {
            throw new IllegalArgumentException(ERR_CONFIG_INVALID_TRUST_MANAGER
                    .get(configuration.getPointer(), e.getLocalizedMessage()).toString(), e);
        }
        try {
            keyManager = configureKeyManager(configuration, KeyManagerType.JVM);
        } catch (GeneralSecurityException | IOException e) {
            throw new IllegalArgumentException(ERR_CONFIG_INVALID_KEY_MANAGER
                    .get(configuration.getPointer(), e.getLocalizedMessage()).toString(), e);
        }
    }
    private TrustManager configureTrustManager(JsonValue config, TrustManagerType defaultIfMissing)
            throws GeneralSecurityException, IOException {
        // Parse trust store configuration.
        final TrustManagerType trustManagerType =
                config.get("trustManager").defaultTo(defaultIfMissing).as(enumConstant(TrustManagerType.class));
        switch (trustManagerType) {
        case TRUSTALL:
            return trustAll();
        case JVM:
            return null;
        case FILE:
            final String fileName = config.get("fileBasedTrustManagerFile").required().asString();
            final String passwordFile = config.get("fileBasedTrustManagerPasswordFile").asString();
            final String password = passwordFile != null
                    ? readPasswordFromFile(passwordFile)
                    : config.get("fileBasedTrustManagerPassword").asString();
            final String type = config.get("fileBasedTrustManagerType").asString();
            return checkUsingTrustStore(fileName, password != null ? password.toCharArray() : null, type);
        default:
            throw new IllegalArgumentException("Unsupported trust-manager type: " + trustManagerType);
        }
    }
    private X509KeyManager configureKeyManager(JsonValue config, KeyManagerType defaultIfMissing)
            throws GeneralSecurityException, IOException {
        // Parse trust store configuration.
        final KeyManagerType keyManagerType = config.get("keyManager").defaultTo(defaultIfMissing)
                .as(enumConstant(KeyManagerType.class));
        switch (keyManagerType) {
        case JVM:
            return useJvmDefaultKeyStore();
        case KEYSTORE:
            final String fileName = config.get("keyStoreFile").required().asString();
            final String passwordFile = config.get("keyStorePasswordFile").asString();
            final String password = passwordFile != null
                    ? readPasswordFromFile(passwordFile)
                    : config.get("keyStorePassword").asString();
            final String format = config.get("keyStoreFormat").asString();
            final String provider = config.get("keyStoreProvider").asString();
            return useKeyStoreFile(fileName, password != null ? password.toCharArray() : null, format, provider);
        case PKCS11:
            final String pkcs11PasswordFile = config.get("pkcs11PasswordFile").asString();
            return usePKCS11Token(pkcs11PasswordFile != null
                    ? readPasswordFromFile(pkcs11PasswordFile).toCharArray()
                    : null);
        default:
            throw new IllegalArgumentException("Unsupported key-manager type: " + keyManagerType);
        }
        trustManager = configureTrustManager(configuration);
        keyManager = configureKeyManager(configuration);
    }
    private void configureConnectionFactories(final JsonValue config) {
@@ -505,8 +440,8 @@
        case SASL_PLAIN:
            return buildSaslBindStrategy(config);
        default:
            throw newLocalizedIllegalArgumentException(ERR_CONFIG_UNSUPPORTED_BIND_STRATEGY.get(
                    strategy, BindStrategy.listValues()));
            throw new LocalizedIllegalArgumentException(
                    ERR_CONFIG_UNSUPPORTED_BIND_STRATEGY.get(strategy, BindStrategy.listValues()));
        }
    }
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfigurator.java
New file
@@ -0,0 +1,658 @@
/*
 * 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 2016 ForgeRock AS.
 *
 */
package org.forgerock.opendj.rest2ldap;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static org.forgerock.http.routing.RoutingMode.STARTS_WITH;
import static org.forgerock.http.routing.Version.version;
import static org.forgerock.http.util.Json.readJsonLenient;
import static org.forgerock.json.JsonValueFunctions.enumConstant;
import static org.forgerock.json.JsonValueFunctions.pointer;
import static org.forgerock.json.JsonValueFunctions.setOf;
import static org.forgerock.json.resource.RouteMatchers.requestUriMatcher;
import static org.forgerock.opendj.ldap.Connections.LOAD_BALANCER_MONITORING_INTERVAL;
import static org.forgerock.opendj.ldap.Connections.newCachedConnectionPool;
import static org.forgerock.opendj.ldap.Connections.newFailoverLoadBalancer;
import static org.forgerock.opendj.ldap.Connections.newRoundRobinLoadBalancer;
import static org.forgerock.opendj.ldap.KeyManagers.useJvmDefaultKeyStore;
import static org.forgerock.opendj.ldap.KeyManagers.useKeyStoreFile;
import static org.forgerock.opendj.ldap.KeyManagers.usePKCS11Token;
import static org.forgerock.opendj.ldap.KeyManagers.useSingleCertificate;
import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
import static org.forgerock.opendj.ldap.TrustManagers.checkUsingTrustStore;
import static org.forgerock.opendj.ldap.TrustManagers.trustAll;
import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.*;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
import static org.forgerock.opendj.rest2ldap.Utils.newJsonValueException;
import static org.forgerock.util.time.Duration.duration;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509KeyManager;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.Router;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.LDAPConnectionFactory;
import org.forgerock.opendj.ldap.SSLContextBuilder;
import org.forgerock.opendj.ldap.requests.BindRequest;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.util.Options;
import org.forgerock.util.time.Duration;
/** Provides core factory methods and builders for constructing Rest2Ldap endpoints from JSON configuration. */
public final class Rest2LdapJsonConfigurator {
    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
    /**
     * Parses Rest2Ldap configuration options. The JSON configuration must have the following format:
     * <p>
     * <pre>
     * {
     *      "readOnUpdatePolicy": "controls",
     *      "useSubtreeDelete": true,
     *      "usePermissiveModify": true,
     *      "useMvcc": true
     *      "mvccAttribute": "etag"
     * }
     * </pre>
     * <p>
     * See the sample configuration file for a detailed description of its content.
     *
     * @param config
     *         The JSON configuration.
     * @return The parsed Rest2Ldap configuration options.
     * @throws IllegalArgumentException
     *         If the configuration is invalid.
     */
    public static Options configureOptions(final JsonValue config) {
        final Options options = Options.defaultOptions();
        options.set(READ_ON_UPDATE_POLICY,
                    config.get("readOnUpdatePolicy").defaultTo(CONTROLS).as(enumConstant(ReadOnUpdatePolicy.class)));
        // Default to false, even though it is supported by OpenDJ, because it requires additional permissions.
        options.set(USE_SUBTREE_DELETE, config.get("useSubtreeDelete").defaultTo(false).asBoolean());
        // Default to true because it is supported by OpenDJ and does not require additional permissions.
        options.set(USE_PERMISSIVE_MODIFY, config.get("usePermissiveModify").defaultTo(false).asBoolean());
        options.set(USE_MVCC, config.get("useMvcc").defaultTo(true).asBoolean());
        options.set(MVCC_ATTRIBUTE, config.get("mvccAttribute").defaultTo("etag").asString());
        return options;
    }
    /**
     * Parses a list of Rest2Ldap resource definitions. The JSON configuration must have the following format:
     * <p>
     * <pre>
     * "top": {
     *     "isAbstract": true,
     *     "properties": {
     *         "_rev": {
     *             "type": "simple"
     *             "ldapAttribute": "etag",
     *             "writability": "readOnly"
     *         },
     *         ...
     *     },
     *     ...
     * },
     * ...
     * </pre>
     * <p>
     * See the sample configuration file for a detailed description of its content.
     *
     * @param config
     *         The JSON configuration.
     * @return The parsed list of Rest2Ldap resource definitions.
     * @throws IllegalArgumentException
     *         If the configuration is invalid.
     */
    public static List<Resource> configureResources(final JsonValue config) {
        final JsonValue resourcesConfig = config.required().expect(Map.class);
        final List<Resource> resources = new LinkedList<>();
        for (final String resourceId : resourcesConfig.keys()) {
            resources.add(configureResource(resourceId, resourcesConfig.get(resourceId)));
        }
        return resources;
    }
    /**
     * Creates a new CREST {@link Router} using the provided endpoints configuration directory and Rest2Ldap options.
     * The Rest2Ldap configuration typically has the following structure on disk:
     * <ul>
     * <li> config.json - contains the configuration for the LDAP connection factories and authorization
     * <li> rest2ldap/rest2ldap.json - defines Rest2Ldap configuration options
     * <li> rest2ldap/endpoints/{api} - a directory containing the endpoint's resource definitions for endpoint {api}
     * <li> rest2ldap/endpoints/{api}/{resource-id}.json - the resource definitions for a specific version of API {api}.
     * The name of the file, {resource-id}, determines which resource type definition in the mapping file will be
     * used as the root resource.
     * </ul>
     *
     * @param endpointsDirectory The directory representing the Rest2Ldap "endpoints" directory.
     * @param options The Rest2Ldap configuration options.
     * @return A new CREST {@link Router} configured using the provided options and endpoints.
     * @throws IOException If the endpoints configuration cannot be read.
     * @throws IllegalArgumentException
     *         If the configuration is invalid.
     */
    public static Router configureEndpoints(final File endpointsDirectory, final Options options) throws IOException {
        final Router pathRouter = new Router();
        final File[] endpoints = endpointsDirectory.listFiles(new FileFilter() {
            @Override
            public boolean accept(final File pathname) {
                return pathname.isDirectory() && pathname.canRead();
            }
        });
        if (endpoints == null) {
            throw new LocalizedIllegalArgumentException(ERR_INVALID_ENDPOINTS_DIRECTORY.get(endpointsDirectory));
        }
        for (final File endpoint : endpoints) {
            final Router router = configureEndpoint(endpoint, options);
            pathRouter.addRoute(requestUriMatcher(STARTS_WITH, endpoint.getName()), router);
        }
        return pathRouter;
    }
    /**
     * Creates a new CREST {@link Router} representing a single endpoint whose configuration is defined in the
     * provided {@code endpointDirectory} parameter. The directory should contain a separate file for each supported
     * version of the REST endpoint. The name of the file, excluding the suffix, identifies the resource definition
     * which acts as the entry point into the endpoint.
     *
     * @param endpointDirectory The directory containing the endpoint's resource definitions, e.g.
     *                          rest2ldap/routes/api would contain definitions for the "api" endpoint.
     * @param options The Rest2Ldap configuration options.
     * @return A new CREST {@link Router} configured using the provided options and endpoint mappings.
     * @throws IOException If the endpoint configuration cannot be read.
     * @throws IllegalArgumentException
     *         If the configuration is invalid.
     */
    public static Router configureEndpoint(final File endpointDirectory, final Options options) throws IOException {
        final Router versionRouter = new Router();
        final File[] endpointVersions = endpointDirectory.listFiles(new FileFilter() {
            @Override
            public boolean accept(final File pathname) {
                return pathname.isFile() && pathname.canRead() && pathname.getName().endsWith(".json");
            }
        });
        if (endpointVersions == null) {
            throw new LocalizedIllegalArgumentException(ERR_INVALID_ENDPOINT_DIRECTORY.get(endpointDirectory));
        }
        for (final File endpointVersion : endpointVersions) {
            final JsonValue mappingConfig = readJson(endpointVersion);
            final String version = mappingConfig.get("version").defaultTo("*").asString();
            final List<Resource> resourceTypes = configureResources(mappingConfig.get("resourceTypes"));
            final Rest2Ldap rest2Ldap = rest2Ldap(options, resourceTypes);
            final String endpointVersionFileName = endpointVersion.getName();
            final int endIndex = endpointVersionFileName.lastIndexOf('.');
            final String rootResourceType = endpointVersionFileName.substring(0, endIndex);
            final RequestHandler handler = rest2Ldap.newRequestHandlerFor(rootResourceType);
            if (version.equals("*")) {
                versionRouter.setDefaultRoute(handler);
            } else {
                versionRouter.addRoute(version(version), handler);
            }
            logger.debug(INFO_REST2LDAP_CREATING_ENDPOINT.get(endpointDirectory.getName(), version));
        }
        return versionRouter;
    }
    static JsonValue readJson(final File resource) throws IOException {
        try (InputStream in = new FileInputStream(resource)) {
            return new JsonValue(readJsonLenient(in));
        }
    }
    private static Resource configureResource(final String resourceId, final JsonValue config) {
        final Resource resource = resource(resourceId)
                .isAbstract(config.get("isAbstract").defaultTo(false).asBoolean())
                .superType(config.get("superType").asString())
                .objectClasses(config.get("objectClasses")
                                     .defaultTo(emptyList()).asList(String.class).toArray(new String[0]))
                .supportedActions(config.get("supportedActions")
                                        .defaultTo(emptyList())
                                        .as(setOf(enumConstant(Action.class))).toArray(new Action[0]))
                .resourceTypeProperty(config.get("resourceTypeProperty").as(pointer()))
                .includeAllUserAttributesByDefault(config.get("includeAllUserAttributesByDefault")
                                                         .defaultTo(false).asBoolean())
                .excludedDefaultUserAttributes(config.get("excludedDefaultUserAttributes")
                                                     .defaultTo(Collections.emptyList()).asList(String.class));
        final JsonValue properties = config.get("properties").expect(Map.class);
        for (final String property : properties.keys()) {
            resource.property(property, configurePropertyMapper(properties.get(property), property));
        }
        final JsonValue subResources = config.get("subResources").expect(Map.class);
        for (final String urlTemplate : subResources.keys()) {
            resource.subResource(configureSubResource(urlTemplate, subResources.get(urlTemplate)));
        }
        return resource;
    }
    private enum NamingStrategyType { CLIENTDNNAMING, CLIENTNAMING, SERVERNAMING }
    private enum SubResourceType { COLLECTION, SINGLETON }
    private static SubResource configureSubResource(final String urlTemplate, final JsonValue config) {
        final String dnTemplate = config.get("dnTemplate").defaultTo("").asString();
        final Boolean isReadOnly = config.get("isReadOnly").defaultTo(false).asBoolean();
        final String resourceId = config.get("resource").required().asString();
        if (config.get("type").required().as(enumConstant(SubResourceType.class)) == SubResourceType.COLLECTION) {
            final String[] glueObjectClasses = config.get("glueObjectClasses")
                                                     .defaultTo(emptyList())
                                                     .asList(String.class)
                                                     .toArray(new String[0]);
            final SubResourceCollection collection = collectionOf(resourceId).urlTemplate(urlTemplate)
                                                                             .dnTemplate(dnTemplate)
                                                                             .isReadOnly(isReadOnly)
                                                                             .glueObjectClasses(glueObjectClasses);
            final JsonValue namingStrategy = config.get("namingStrategy").required();
            switch (namingStrategy.get("type").required().as(enumConstant(NamingStrategyType.class))) {
            case CLIENTDNNAMING:
                collection.useClientDnNaming(namingStrategy.get("dnAttribute").required().asString());
                break;
            case CLIENTNAMING:
                collection.useClientNaming(namingStrategy.get("dnAttribute").required().asString(),
                                           namingStrategy.get("idAttribute").required().asString());
                break;
            case SERVERNAMING:
                collection.useServerNaming(namingStrategy.get("dnAttribute").required().asString(),
                                           namingStrategy.get("idAttribute").required().asString());
                break;
            }
            return collection;
        } else {
            return singletonOf(resourceId).urlTemplate(urlTemplate).dnTemplate(dnTemplate).isReadOnly(isReadOnly);
        }
    }
    private static PropertyMapper configurePropertyMapper(final JsonValue mapper, final String defaultLdapAttribute) {
        switch (mapper.get("type").required().asString()) {
        case "resourceType":
            return resourceType();
        case "constant":
            return constant(mapper.get("value").getObject());
        case "simple":
            return simple(mapper.get("ldapAttribute").defaultTo(defaultLdapAttribute).required().asString())
                    .defaultJsonValue(mapper.get("defaultJsonValue").getObject())
                    .isBinary(mapper.get("isBinary").defaultTo(false).asBoolean())
                    .isRequired(mapper.get("isRequired").defaultTo(false).asBoolean())
                    .isMultiValued(mapper.get("isMultiValued").defaultTo(false).asBoolean())
                    .writability(parseWritability(mapper));
        case "reference":
            final String ldapAttribute = mapper.get("ldapAttribute")
                                               .defaultTo(defaultLdapAttribute).required().asString();
            final String baseDN = mapper.get("baseDn").required().asString();
            final String primaryKey = mapper.get("primaryKey").required().asString();
            final PropertyMapper m = configurePropertyMapper(mapper.get("mapper").required(), primaryKey);
            return reference(ldapAttribute, baseDN, primaryKey, m)
                    .isRequired(mapper.get("isRequired").defaultTo(false).asBoolean())
                    .isMultiValued(mapper.get("isMultiValued").defaultTo(false).asBoolean())
                    .searchFilter(mapper.get("searchFilter").defaultTo("(objectClass=*)").asString())
                    .writability(parseWritability(mapper));
        case "object":
            final JsonValue properties = mapper.get("properties");
            final ObjectPropertyMapper object = object();
            for (final String attribute : properties.keys()) {
                object.property(attribute, configurePropertyMapper(properties.get(attribute), attribute));
            }
            return object;
        default:
            throw newJsonValueException(mapper, ERR_CONFIG_NO_MAPPING_IN_CONFIGURATION.get(
                    "constant, simple, reference, object"));
        }
    }
    private static WritabilityPolicy parseWritability(final JsonValue mapper) {
        if (mapper.isDefined("writability")) {
            final String writability = mapper.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 newJsonValueException(mapper, ERR_CONFIG_UNKNOWN_WRITABILITY.get(writability,
                            "readOnly, readOnlyDiscardWrites, createOnly, createOnlyDiscardWrites, readWrite"));
            }
        } else {
            return WritabilityPolicy.READ_WRITE;
        }
    }
    /** Indicates whether LDAP client connections should use SSL or StartTLS. */
    private enum ConnectionSecurity { NONE, SSL, STARTTLS }
    /** Specifies the mechanism which will be used for trusting certificates presented by the LDAP server. */
    private enum TrustManagerType { TRUSTALL, JVM, FILE }
    /** Specifies the type of key-store to use when performing SSL client authentication. */
    private enum KeyManagerType { JVM, KEYSTORE, PKCS11 }
    /**
     * Configures a {@link X509KeyManager} using the provided JSON configuration.
     *
     * @param configuration
     *         The JSON object containing the key manager configuration.
     * @return The configured key manager.
     */
    public static X509KeyManager configureKeyManager(final JsonValue configuration) {
        try {
            return configureKeyManager(configuration, KeyManagerType.JVM);
        } catch (GeneralSecurityException | IOException e) {
            throw new IllegalArgumentException(ERR_CONFIG_INVALID_KEY_MANAGER.get(
                    configuration.getPointer(), e.getLocalizedMessage()).toString(), e);
        }
    }
    private static X509KeyManager configureKeyManager(JsonValue config, KeyManagerType defaultIfMissing)
            throws GeneralSecurityException, IOException {
        final KeyManagerType keyManagerType = config.get("keyManager")
                                                    .defaultTo(defaultIfMissing)
                                                    .as(enumConstant(KeyManagerType.class));
        switch (keyManagerType) {
        case JVM:
            return useJvmDefaultKeyStore();
        case KEYSTORE:
            final String fileName = config.get("keyStoreFile").required().asString();
            final String passwordFile = config.get("keyStorePasswordFile").asString();
            final String password = passwordFile != null
                    ? readPasswordFromFile(passwordFile)
                    : config.get("keyStorePassword").asString();
            final String format = config.get("keyStoreFormat").asString();
            final String provider = config.get("keyStoreProvider").asString();
            return useKeyStoreFile(fileName, password != null ? password.toCharArray() : null, format, provider);
        case PKCS11:
            final String pkcs11PasswordFile = config.get("pkcs11PasswordFile").asString();
            return usePKCS11Token(pkcs11PasswordFile != null
                                          ? readPasswordFromFile(pkcs11PasswordFile).toCharArray()
                                          : null);
        default:
            throw new IllegalArgumentException("Unsupported key-manager type: " + keyManagerType);
        }
    }
    private static String readPasswordFromFile(String fileName) throws IOException {
        try (final BufferedReader reader = new BufferedReader(new FileReader(new File(fileName)))) {
            return reader.readLine();
        }
    }
    /**
     * Configures a {@link TrustManager} using the provided JSON configuration.
     *
     * @param configuration
     *         The JSON object containing the trust manager configuration.
     * @return The configured trust manager.
     */
    public static TrustManager configureTrustManager(final JsonValue configuration) {
        try {
            return configureTrustManager(configuration, TrustManagerType.JVM);
        } catch (GeneralSecurityException | IOException e) {
            throw new IllegalArgumentException(ERR_CONFIG_INVALID_TRUST_MANAGER.get(
                    configuration.getPointer(), e.getLocalizedMessage()).toString(), e);
        }
    }
    private static TrustManager configureTrustManager(JsonValue config, TrustManagerType defaultIfMissing)
            throws GeneralSecurityException, IOException {
        final TrustManagerType trustManagerType = config.get("trustManager")
                                                        .defaultTo(defaultIfMissing)
                                                        .as(enumConstant(TrustManagerType.class));
        switch (trustManagerType) {
        case TRUSTALL:
            return trustAll();
        case JVM:
            return null;
        case FILE:
            final String fileName = config.get("fileBasedTrustManagerFile").required().asString();
            final String passwordFile = config.get("fileBasedTrustManagerPasswordFile").asString();
            final String password = passwordFile != null
                    ? readPasswordFromFile(passwordFile) : config.get("fileBasedTrustManagerPassword").asString();
            final String type = config.get("fileBasedTrustManagerType").asString();
            return checkUsingTrustStore(fileName, password != null ? password.toCharArray() : null, type);
        default:
            throw new IllegalArgumentException("Unsupported trust-manager type: " + trustManagerType);
        }
    }
    /**
     * Creates a new connection factory using the named configuration in the provided JSON list of factory
     * configurations. See the sample configuration file for a detailed description of its content.
     *
     * @param configuration
     *         The JSON configuration.
     * @param name
     *         The name of the connection factory configuration to be parsed.
     * @param trustManager
     *            The trust manager to use for secure connection. Can be {@code null}
     *            to use the default JVM trust manager.
     * @param keyManager
     *            The key manager to use for secure connection. Can be {@code null}
     *            to use the default JVM key manager.
     * @param providerClassLoader
     *         The {@link ClassLoader} used to fetch the {@link org.forgerock.opendj.ldap.spi.TransportProvider}. This
     *         can be useful in OSGI environments.
     * @return A new connection factory using the provided JSON configuration.
     * @throws IllegalArgumentException
     *         If the configuration is invalid.
     */
    public static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
                                                               final String name,
                                                               final TrustManager trustManager,
                                                               final X509KeyManager keyManager,
                                                               final ClassLoader providerClassLoader) {
        final JsonValue normalizedConfiguration = normalizeConnectionFactory(configuration, name, 0);
        return configureConnectionFactory(normalizedConfiguration, trustManager, keyManager, providerClassLoader);
    }
    /**
     * Creates a new connection factory using the named configuration in the provided JSON list of factory
     * configurations. See the sample configuration file for a detailed description of its content.
     *
     * @param configuration
     *         The JSON configuration.
     * @param name
     *         The name of the connection factory configuration to be parsed.
     * @param trustManager
     *            The trust manager to use for secure connection. Can be {@code null}
     *            to use the default JVM trust manager.
     * @param keyManager
     *            The key manager to use for secure connection. Can be {@code null}
     *            to use the default JVM key manager.
     * @return A new connection factory using the provided JSON configuration.
     * @throws IllegalArgumentException
     *         If the configuration is invalid.
     */
    public static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
                                                               final String name,
                                                               final TrustManager trustManager,
                                                               final X509KeyManager keyManager) {
        return configureConnectionFactory(configuration, name, trustManager, keyManager, null);
    }
    private static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
                                                                final TrustManager trustManager,
                                                                final X509KeyManager keyManager,
                                                                final ClassLoader providerClassLoader) {
        final long heartBeatIntervalSeconds = configuration.get("heartBeatIntervalSeconds").defaultTo(30L).asLong();
        final Duration heartBeatInterval = duration(Math.max(heartBeatIntervalSeconds, 1L), TimeUnit.SECONDS);
        final long heartBeatTimeoutMillis = configuration.get("heartBeatTimeoutMilliSeconds").defaultTo(500L).asLong();
        final Duration heartBeatTimeout = duration(Math.max(heartBeatTimeoutMillis, 100L), TimeUnit.MILLISECONDS);
        final Options options = Options.defaultOptions()
                                       .set(TRANSPORT_PROVIDER_CLASS_LOADER, providerClassLoader)
                                       .set(HEARTBEAT_ENABLED, true)
                                       .set(HEARTBEAT_INTERVAL, heartBeatInterval)
                                       .set(HEARTBEAT_TIMEOUT, heartBeatTimeout)
                                       .set(LOAD_BALANCER_MONITORING_INTERVAL, heartBeatInterval);
        // Parse pool parameters,
        final int connectionPoolSize =
                Math.max(configuration.get("connectionPoolSize").defaultTo(10).asInteger(), 1);
        // Parse authentication parameters.
        if (configuration.isDefined("authentication")) {
            final JsonValue authn = configuration.get("authentication");
            if (authn.isDefined("simple")) {
                final JsonValue simple = authn.get("simple");
                final BindRequest bindRequest =
                        Requests.newSimpleBindRequest(simple.get("bindDN").required().asString(),
                                                      simple.get("bindPassword").required().asString().toCharArray());
                options.set(AUTHN_BIND_REQUEST, bindRequest);
            } else {
                throw new LocalizedIllegalArgumentException(ERR_CONFIG_INVALID_AUTHENTICATION.get());
            }
        }
        // Parse SSL/StartTLS parameters.
        final ConnectionSecurity connectionSecurity = configuration.get("connectionSecurity")
                                                                   .defaultTo(ConnectionSecurity.NONE)
                                                                   .as(enumConstant(ConnectionSecurity.class));
        if (connectionSecurity != ConnectionSecurity.NONE) {
            try {
                // Configure SSL.
                final SSLContextBuilder builder = new SSLContextBuilder();
                builder.setTrustManager(trustManager);
                final String sslCertAlias = configuration.get("sslCertAlias").asString();
                builder.setKeyManager(sslCertAlias != null
                                              ? useSingleCertificate(sslCertAlias, keyManager)
                                              : keyManager);
                options.set(SSL_CONTEXT, builder.getSSLContext());
                options.set(SSL_USE_STARTTLS, connectionSecurity == ConnectionSecurity.STARTTLS);
            } catch (GeneralSecurityException e) {
                // Rethrow as unchecked exception.
                throw new IllegalArgumentException(e);
            }
        }
        // Parse primary data center.
        final JsonValue primaryLdapServers = configuration.get("primaryLDAPServers");
        if (!primaryLdapServers.isList() || primaryLdapServers.size() == 0) {
            throw new IllegalArgumentException("No primaryLDAPServers");
        }
        final ConnectionFactory primary = parseLdapServers(primaryLdapServers, connectionPoolSize, options);
        // Parse secondary data center(s).
        final JsonValue secondaryLdapServers = configuration.get("secondaryLDAPServers");
        ConnectionFactory secondary = null;
        if (secondaryLdapServers.isList()) {
            if (secondaryLdapServers.size() > 0) {
                secondary = parseLdapServers(secondaryLdapServers, connectionPoolSize, options);
            }
        } else if (!secondaryLdapServers.isNull()) {
            throw new LocalizedIllegalArgumentException(ERR_CONFIG_INVALID_SECONDARY_LDAP_SERVER.get());
        }
        // Create fail-over.
        if (secondary != null) {
            return newFailoverLoadBalancer(asList(primary, secondary), options);
        } else {
            return primary;
        }
    }
    private static JsonValue normalizeConnectionFactory(final JsonValue configuration,
                                                        final String name, final int depth) {
        // Protect against infinite recursion in the configuration.
        if (depth > 100) {
            throw new LocalizedIllegalArgumentException(ERR_CONFIG_SERVER_CIRCULAR_DEPENDENCIES.get(name));
        }
        final JsonValue current = configuration.get(name).required();
        if (current.isDefined("inheritFrom")) {
            // Inherit missing fields from inherited configuration.
            final JsonValue parent =
                    normalizeConnectionFactory(configuration,
                                               current.get("inheritFrom").asString(), depth + 1);
            final Map<String, Object> normalized = new LinkedHashMap<>(parent.asMap());
            normalized.putAll(current.asMap());
            normalized.remove("inheritFrom");
            return new JsonValue(normalized);
        } else {
            // No normalization required.
            return current;
        }
    }
    private static ConnectionFactory parseLdapServers(JsonValue config, int poolSize, Options options) {
        final List<ConnectionFactory> servers = new ArrayList<>(config.size());
        for (final JsonValue server : config) {
            final String host = server.get("hostname").required().asString();
            final int port = server.get("port").required().asInteger();
            final ConnectionFactory factory = new LDAPConnectionFactory(host, port, options);
            if (poolSize > 1) {
                servers.add(newCachedConnectionPool(factory, 0, poolSize, 60L, TimeUnit.SECONDS));
            } else {
                servers.add(factory);
            }
        }
        if (servers.size() > 1) {
            return newRoundRobinLoadBalancer(servers, options);
        } else {
            return servers.get(0);
        }
    }
    private Rest2LdapJsonConfigurator() {
        // Prevent instantiation.
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RoutingContext.java
New file
@@ -0,0 +1,43 @@
/*
 * 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 2016 ForgeRock AS.
 *
 */
package org.forgerock.opendj.rest2ldap;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.services.context.AbstractContext;
import org.forgerock.services.context.Context;
/**
 * A {@link Context} which communicates the current Rest2Ldap routing state to downstream handlers.
 */
final class RoutingContext extends AbstractContext {
    private final DN dn;
    private final Resource resource;
    RoutingContext(final Context parent, final DN dn, final Resource resource) {
        super(parent, "routing context");
        this.dn = dn;
        this.resource = resource;
    }
    DN getDn() {
        return dn;
    }
    Resource getType() {
        return resource;
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimplePropertyMapper.java
@@ -18,6 +18,7 @@
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
@@ -37,15 +38,11 @@
import static java.util.Collections.*;
import static org.forgerock.opendj.ldap.Filter.*;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.*;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException;
import static org.forgerock.opendj.rest2ldap.Utils.*;
import static org.forgerock.util.promise.Promises.newExceptionPromise;
import static org.forgerock.util.promise.Promises.newResultPromise;
/**
 * An property mapper which provides a simple mapping from a JSON value to a
 * single LDAP attribute.
 */
/** An property mapper which provides a simple mapping from a JSON value to a single LDAP attribute. */
public final class SimplePropertyMapper extends AbstractLdapPropertyMapper<SimplePropertyMapper> {
    private Function<ByteString, ?, NeverThrowsException> decoder;
    private Function<Object, ByteString, NeverThrowsException> encoder;
@@ -68,8 +65,7 @@
    }
    /**
     * Sets the default JSON value which should be substituted when the LDAP
     * attribute is not found in the LDAP entry.
     * Sets the default JSON value which should be substituted when the LDAP attribute is not found in the LDAP entry.
     *
     * @param defaultValue
     *            The default JSON value.
@@ -81,6 +77,18 @@
    }
    /**
     * Sets the default JSON values which should be substituted when the LDAP attribute is not found in the LDAP entry.
     *
     * @param defaultValues
     *            The default JSON values.
     * @return This property mapper.
     */
    public SimplePropertyMapper defaultJsonValues(final Collection<?> defaultValues) {
        this.defaultJsonValues = defaultValues != null ? new ArrayList<>(defaultValues) : emptyList();
        return this;
    }
    /**
     * Sets the encoder which will be used for converting JSON values to LDAP
     * attribute values.
     *
@@ -95,18 +103,27 @@
    /**
     * Indicates that JSON values are base 64 encodings of binary data. Calling
     * this method is equivalent to the following:
     * this method with the value {@code true} is equivalent to the following:
     *
     * <pre>
     * mapper.decoder(...); // function that converts binary data to base 64
     * mapper.encoder(...); // function that converts base 64 to binary data
     * </pre>
     *
     * Passing in a value of {@code false} resets the encoding and decoding
     * functions to the default.
     *
     * @param isBinary {@code true} if this property is binary.
     * @return This property mapper.
     */
    public SimplePropertyMapper isBinary() {
        decoder = byteStringToBase64();
        encoder = base64ToByteString();
    public SimplePropertyMapper isBinary(final boolean isBinary) {
        if (isBinary) {
            decoder = byteStringToBase64();
            encoder = base64ToByteString();
        } else {
            decoder = null;
            encoder = null;
        }
        return this;
    }
@@ -116,18 +133,18 @@
    }
    @Override
    Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final JsonPointer path,
                                                     final JsonPointer subPath, final FilterType type,
                                                     final String operator, final Object valueAssertion) {
    Promise<Filter, ResourceException> getLdapFilter(final Connection connection, final Resource resource,
                                                     final JsonPointer path, final JsonPointer subPath,
                                                     final FilterType type, final String operator,
                                                     final Object valueAssertion) {
        if (subPath.isEmpty()) {
            try {
                final ByteString va =
                        valueAssertion != null ? encoder().apply(valueAssertion) : null;
                final ByteString va = valueAssertion != null ? encoder().apply(valueAssertion) : null;
                return newResultPromise(toFilter(type, ldapAttributeName.toString(), va));
            } catch (final Exception e) {
                // Invalid assertion value - bad request.
                return newExceptionPromise((ResourceException) newBadRequestException(
                        ERR_ILLEGAL_FILTER_ASSERTION_VALUE.get(String.valueOf(valueAssertion), path), e));
                return newBadRequestException(
                        ERR_ILLEGAL_FILTER_ASSERTION_VALUE.get(String.valueOf(valueAssertion), path), e).asPromise();
            }
        } else {
            // This property mapper does not support partial filtering.
@@ -136,13 +153,12 @@
    }
    @Override
    Promise<Attribute, ResourceException> getNewLdapAttributes(
            final Connection connection, final JsonPointer path, final List<Object> newValues) {
    Promise<Attribute, ResourceException> getNewLdapAttributes(final Connection connection, final Resource resource,
                                                               final JsonPointer path, final List<Object> newValues) {
        try {
            return newResultPromise(jsonToAttribute(newValues, ldapAttributeName, encoder()));
        } catch (final Exception ex) {
            return newExceptionPromise((ResourceException) newBadRequestException(
                    ERR_ENCODING_VALUES_FOR_FIELD.get(path, ex.getMessage())));
            return newBadRequestException(ERR_ENCODING_VALUES_FOR_FIELD.get(path, ex.getMessage())).asPromise();
        }
    }
@@ -151,21 +167,26 @@
        return this;
    }
    @SuppressWarnings("fallthrough")
    @Override
    Promise<JsonValue, ResourceException> read(final Connection connection, final JsonPointer path, final Entry e) {
    Promise<JsonValue, ResourceException> read(final Connection connection, final Resource resource,
                                               final JsonPointer path, final Entry e) {
        try {
            final Object value;
            if (attributeIsSingleValued()) {
                value = e.parseAttribute(ldapAttributeName)
                         .as(decoder(), defaultJsonValues.isEmpty() ? null : defaultJsonValues.get(0));
            } else {
                final Set<Object> s = e.parseAttribute(ldapAttributeName).asSetOf(decoder(), defaultJsonValues);
                value = s.isEmpty() ? null : new ArrayList<>(s);
            final Set<Object> s = e.parseAttribute(ldapAttributeName).asSetOf(decoder(), defaultJsonValues);
            switch (s.size()) {
            case 0:
                return newResultPromise(null);
            case 1:
                if (attributeIsSingleValued()) {
                    return newResultPromise(new JsonValue(s.iterator().next()));
                }
                // Fall-though: unexpectedly got multiple values. It's probably best to just return them.
            default:
                return newResultPromise(new JsonValue(new ArrayList<>(s)));
            }
            return newResultPromise(value != null ? new JsonValue(value) : null);
        } catch (final Exception ex) {
            // The LDAP attribute could not be decoded.
            return newExceptionPromise(asResourceException(ex));
            return asResourceException(ex).asPromise();
        }
    }
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java
New file
@@ -0,0 +1,151 @@
/*
 * 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 2016 ForgeRock AS.
 *
 */
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.DECODE_OPTIONS;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_SUB_RESOURCE_TYPE;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.forgerock.http.routing.UriRouterContext;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.NotFoundException;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.Router;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.services.context.Context;
import org.forgerock.util.Function;
/**
 * Defines a parent-child relationship between a parent resource and one or more child resource(s). Removal of the
 * parent resource implies that the children (the sub-resources) are also removed. There are two types of
 * sub-resource:
 * <ul>
 * <li>{@link SubResourceSingleton} represents a one-to-one relationship supporting read, update, patch, and action
 *     requests</li>
 * <li>{@link SubResourceCollection} represents a one-to-many relationship supporting all requests.</li>
 * </ul>
 */
public abstract class SubResource {
    private static final Pattern TEMPLATE_KEY_RE = Pattern.compile("\\{([^}]+)\\}");
    private final String resourceId;
    private final List<String> dnTemplateVariables = new ArrayList<>();
    private String dnTemplateFormatString;
    String urlTemplate = "";
    String dnTemplate = "";
    boolean isReadOnly = false;
    Rest2Ldap rest2Ldap;
    Resource resource;
    SubResource(final String resourceId) {
        this.resourceId = resourceId;
    }
    @Override
    public final boolean equals(final Object o) {
        return this == o || (o instanceof SubResource && urlTemplate.equals(((SubResource) o).urlTemplate));
    }
    @Override
    public final int hashCode() {
        return urlTemplate.hashCode();
    }
    @Override
    public final String toString() {
        return urlTemplate;
    }
    final Resource getResource() {
        return resource;
    }
    final void build(final Rest2Ldap rest2Ldap, final String parent) {
        this.rest2Ldap = rest2Ldap;
        this.resource = rest2Ldap.getResource(resourceId);
        if (resource == null) {
            throw new LocalizedIllegalArgumentException(ERR_UNRECOGNIZED_SUB_RESOURCE_TYPE.get(parent, resourceId));
        }
        this.dnTemplateFormatString = formatTemplate(dnTemplate, dnTemplateVariables);
    }
    // Parse the template keys and replace them with %s for formatting.
    private String formatTemplate(final String template, final List<String> templateVariables) {
        final Matcher matcher = TEMPLATE_KEY_RE.matcher(template);
        final StringBuffer buffer = new StringBuffer(template.length());
        while (matcher.find()) {
            matcher.appendReplacement(buffer, "%s");
            templateVariables.add(matcher.group(1));
        }
        matcher.appendTail(buffer);
        return buffer.toString();
    }
    abstract Router addRoutes(Router router);
    /** A 404 indicates that this instance is not also a collection, so return a more helpful message. */
    static <T> Function<ResourceException, T, ResourceException> convert404To400(final LocalizableMessage msg) {
        return new Function<ResourceException, T, ResourceException>() {
            @Override
            public T apply(final ResourceException e) throws ResourceException {
                if (e instanceof NotFoundException) {
                    throw new BadRequestException(msg.toString());
                }
                throw e;
            }
        };
    }
    final RequestHandler readOnly(final RequestHandler handler) {
        return isReadOnly ? new ReadOnlyRequestHandler(handler) : handler;
    }
    final DN dnFrom(final Context context) {
        final DN baseDn = context.containsContext(RoutingContext.class)
                ? context.asContext(RoutingContext.class).getDn() : DN.rootDN();
        final Schema schema = rest2Ldap.getOptions().get(DECODE_OPTIONS).getSchemaResolver().resolveSchema(dnTemplate);
        if (dnTemplateVariables.isEmpty()) {
            final DN relativeDn = DN.valueOf(dnTemplate, schema);
            return baseDn.child(relativeDn);
        } else {
            final UriRouterContext uriRouterContext = context.asContext(UriRouterContext.class);
            final Map<String, String> uriTemplateVariables = uriRouterContext.getUriTemplateVariables();
            final String[] values = new String[dnTemplateVariables.size()];
            for (int i = 0; i < values.length; i++) {
                final String key = dnTemplateVariables.get(i);
                values[i] = uriTemplateVariables.get(key);
            }
            final DN relativeDn = DN.format(dnTemplateFormatString, schema, (Object[]) values);
            return baseDn.child(relativeDn);
        }
    }
    final RequestHandler subResourceRouterFrom(final RoutingContext context) {
        return context.getType().getSubResourceRouter();
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
New file
@@ -0,0 +1,530 @@
/*
 * 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 2016 ForgeRock AS.
 *
 */
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.http.routing.RoutingMode.EQUALS;
import static org.forgerock.http.routing.RoutingMode.STARTS_WITH;
import static org.forgerock.json.resource.RouteMatchers.requestUriMatcher;
import static org.forgerock.opendj.ldap.Filter.objectClassPresent;
import static org.forgerock.opendj.ldap.SearchScope.BASE_OBJECT;
import static org.forgerock.opendj.ldap.SearchScope.SINGLE_LEVEL;
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.Utils.newBadRequestException;
import static org.forgerock.util.promise.Promises.newResultPromise;
import org.forgerock.http.routing.UriRouterContext;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.forgerock.json.resource.ActionRequest;
import org.forgerock.json.resource.ActionResponse;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.CreateRequest;
import org.forgerock.json.resource.DeleteRequest;
import org.forgerock.json.resource.NotSupportedException;
import org.forgerock.json.resource.PatchRequest;
import org.forgerock.json.resource.QueryRequest;
import org.forgerock.json.resource.QueryResourceHandler;
import org.forgerock.json.resource.QueryResponse;
import org.forgerock.json.resource.ReadRequest;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.Router;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.AttributeDescription;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.LinkedAttribute;
import org.forgerock.opendj.ldap.RDN;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
import org.forgerock.services.context.Context;
import org.forgerock.util.AsyncFunction;
import org.forgerock.util.Function;
import org.forgerock.util.promise.Promise;
/**
 * Defines a one-to-many relationship between a parent resource and its children. Removal of the parent resource
 * implies that the children (the sub-resources) are also removed. Collections support all request types.
 */
public final class SubResourceCollection extends SubResource {
    /** The LDAP object classes associated with the glue entries forming the DN template. */
    private final Attribute glueObjectClasses = new LinkedAttribute("objectClass");
    private NamingStrategy namingStrategy;
    SubResourceCollection(final String resourceId) {
        super(resourceId);
        useClientDnNaming("uid");
    }
    /**
     * Indicates that the JSON resource ID must be provided by the user, and will be used for naming the associated LDAP
     * entry. More specifically, LDAP entry names will be derived by appending a single RDN to the collection's base DN
     * composed of the specified attribute type and LDAP value taken from the LDAP entry once attribute mapping has been
     * performed.
     * <p>
     * Note that this naming policy requires that the user provides the resource name when creating new resources, which
     * means it must be included in the resource content when not specified explicitly in the create request.
     *
     * @param dnAttribute
     *         The LDAP attribute which will be used for naming.
     * @return A reference to this object.
     */
    public SubResourceCollection useClientDnNaming(final String dnAttribute) {
        this.namingStrategy = new DnNamingStrategy(dnAttribute);
        return this;
    }
    /**
     * Indicates that the JSON resource ID must be provided by the user, but will not be used for naming the
     * associated LDAP entry. Instead the JSON resource ID will be taken from the {@code idAttribute} in the LDAP
     * entry, and the LDAP entry name will be derived by appending a single RDN to the collection's base DN composed
     * of the {@code dnAttribute} taken from the LDAP entry once attribute mapping has been performed.
     * <p>
     * Note that this naming policy requires that the user provides the resource name when creating new resources, which
     * means it must be included in the resource content when not specified explicitly in the create request.
     *
     * @param dnAttribute
     *         The attribute which will be used for naming LDAP entries.
     * @param idAttribute
     *         The attribute which will be used for JSON resource IDs.
     * @return A reference to this object.
     */
    public SubResourceCollection useClientNaming(final String dnAttribute, final String idAttribute) {
        this.namingStrategy = new AttributeNamingStrategy(dnAttribute, idAttribute, false);
        return this;
    }
    /**
     * Indicates that the JSON resource ID will be derived from the server provided "entryUUID" LDAP attribute. The
     * LDAP entry name will be derived by appending a single RDN to the collection's base DN composed of the {@code
     * dnAttribute} taken from the LDAP entry once attribute mapping has been performed.
     * <p>
     * Note that this naming policy requires that the server provides the resource name when creating new resources,
     * which means it must not be specified in the create request, nor included in the resource content.
     *
     * @param dnAttribute
     *         The attribute which will be used for naming LDAP entries.
     * @return A reference to this object.
     */
    public SubResourceCollection useServerEntryUuidNaming(final String dnAttribute) {
        return useServerNaming(dnAttribute, "entryUUID");
    }
    /**
     * Indicates that the JSON resource ID must not be provided by the user, and will not be used for naming the
     * associated LDAP entry. Instead the JSON resource ID will be taken from the {@code idAttribute} in the LDAP
     * entry, and the LDAP entry name will be derived by appending a single RDN to the collection's base DN composed
     * of the {@code dnAttribute} taken from the LDAP entry once attribute mapping has been performed.
     * <p>
     * Note that this naming policy requires that the server provides the resource name when creating new resources,
     * which means it must not be specified in the create request, nor included in the resource content.
     *
     * @param dnAttribute
     *         The attribute which will be used for naming LDAP entries.
     * @param idAttribute
     *         The attribute which will be used for JSON resource IDs.
     * @return A reference to this object.
     */
    public SubResourceCollection useServerNaming(final String dnAttribute, final String idAttribute) {
        this.namingStrategy = new AttributeNamingStrategy(dnAttribute, idAttribute, true);
        return this;
    }
    /**
     * Sets the relative URL template beneath which the sub-resources will be located. The template may be empty
     * indicating that the sub-resources will be located directly beneath the parent resource. Any URL template
     * variables will be substituted into the {@link #dnTemplate(String) DN template}.
     *
     * @param urlTemplate
     *         The relative URL template.
     * @return A reference to this object.
     */
    public SubResourceCollection urlTemplate(final String urlTemplate) {
        this.urlTemplate = urlTemplate;
        return this;
    }
    /**
     * Sets the relative DN template beneath which the sub-resource LDAP entries will be located. The template may be
     * empty indicating that the LDAP entries will be located directly beneath the parent LDAP entry. Any DN template
     * variables will be substituted using values extracted from the {@link #urlTemplate(String) URL template}.
     *
     * @param dnTemplate
     *         The relative DN template.
     * @return A reference to this object.
     */
    public SubResourceCollection dnTemplate(final String dnTemplate) {
        this.dnTemplate = dnTemplate;
        return this;
    }
    /**
     * Specifies an LDAP object class which is to be associated with any intermediate "glue" entries forming the DN
     * template. Multiple object classes may be specified.
     *
     * @param objectClass
     *         An LDAP object class which is to be associated with any intermediate "glue" entries forming the DN
     *         template.
     * @return A reference to this object.
     */
    public SubResourceCollection glueObjectClass(final String objectClass) {
        this.glueObjectClasses.add(objectClass);
        return this;
    }
    /**
     * Specifies one or more LDAP object classes which is to be associated with any intermediate "glue" entries
     * forming the DN template. Multiple object classes may be specified.
     *
     * @param objectClasses
     *         The LDAP object classes which is to be associated with any intermediate "glue" entries forming the DN
     *         template.
     * @return A reference to this object.
     */
    public SubResourceCollection glueObjectClasses(final String... objectClasses) {
        this.glueObjectClasses.add((Object[]) objectClasses);
        return this;
    }
    /**
     * Indicates whether this sub-resource collection only supports read and query operations.
     *
     * @param readOnly
     *         {@code true} if this sub-resource collection is read-only.
     * @return A reference to this object.
     */
    public SubResourceCollection isReadOnly(final boolean readOnly) {
        isReadOnly = readOnly;
        return this;
    }
    @Override
    Router addRoutes(final Router router) {
        router.addRoute(requestUriMatcher(EQUALS, urlTemplate), readOnly(new CollectionHandler()));
        router.addRoute(requestUriMatcher(EQUALS, urlTemplate + "/{id}"), readOnly(new InstanceHandler()));
        router.addRoute(requestUriMatcher(STARTS_WITH, urlTemplate + "/{id}"), readOnly(new SubResourceHandler()));
        return router;
    }
    private Promise<RoutingContext, ResourceException> route(final Context context) {
        final Connection conn = context.asContext(AuthenticatedConnectionContext.class).getConnection();
        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));
        }
        searchRequest.addAttribute("objectClass");
        return conn.searchSingleEntryAsync(searchRequest)
                         .thenAsync(new AsyncFunction<SearchResultEntry, RoutingContext, ResourceException>() {
                             @Override
                             public Promise<RoutingContext, ResourceException> apply(SearchResultEntry entry)
                                     throws ResourceException {
                                 final Resource subType = resource.resolveSubTypeFromObjectClasses(entry);
                                 return newResultPromise(new RoutingContext(context, entry.getName(), subType));
                             }
                         }, new AsyncFunction<LdapException, RoutingContext, ResourceException>() {
                             @Override
                             public Promise<RoutingContext, ResourceException> apply(LdapException e)
                                     throws ResourceException {
                                 return asResourceException(e).asPromise();
                             }
                         });
    }
    private SubResourceImpl collection(final Context context) {
        return new SubResourceImpl(rest2Ldap,
                                   dnFrom(context),
                                   dnTemplate.isEmpty() ? null : glueObjectClasses,
                                   namingStrategy,
                                   resource);
    }
    private String idFrom(final Context context) {
        return context.asContext(UriRouterContext.class).getUriTemplateVariables().get("id");
    }
    private static final class AttributeNamingStrategy implements NamingStrategy {
        private final AttributeDescription dnAttribute;
        private final AttributeDescription idAttribute;
        private final boolean isServerProvided;
        private AttributeNamingStrategy(final String dnAttribute, final String idAttribute,
                                        final boolean isServerProvided) {
            this.dnAttribute = AttributeDescription.valueOf(dnAttribute);
            this.idAttribute = AttributeDescription.valueOf(idAttribute);
            if (this.dnAttribute.equals(this.idAttribute)) {
                throw new LocalizedIllegalArgumentException(ERR_CONFIG_NAMING_STRATEGY_DN_AND_ID_NOT_DIFFERENT.get());
            }
            this.isServerProvided = isServerProvided;
        }
        @Override
        public SearchRequest createSearchRequest(final DN baseDn, final String resourceId) {
            return newSearchRequest(baseDn, SINGLE_LEVEL, Filter.equality(idAttribute.toString(), resourceId));
        }
        @Override
        public String getResourceIdLdapAttribute() {
            return idAttribute.toString();
        }
        @Override
        public String decodeResourceId(final Entry entry) {
            return entry.parseAttribute(idAttribute).asString();
        }
        @Override
        public void encodeResourceId(final DN baseDn, final String resourceId, final Entry entry)
                throws ResourceException {
            if (isServerProvided) {
                if (resourceId != null) {
                    throw newBadRequestException(ERR_SERVER_PROVIDED_RESOURCE_ID_UNEXPECTED.get());
                }
            } else {
                entry.addAttribute(new LinkedAttribute(idAttribute, ByteString.valueOfUtf8(resourceId)));
            }
            final String rdnValue = entry.parseAttribute(dnAttribute).asString();
            final RDN rdn = new RDN(dnAttribute.getAttributeType(), rdnValue);
            entry.setName(baseDn.child(rdn));
        }
    }
    private static final class DnNamingStrategy implements NamingStrategy {
        private final AttributeDescription attribute;
        private DnNamingStrategy(final String attribute) {
            this.attribute = AttributeDescription.valueOf(attribute);
        }
        @Override
        public SearchRequest createSearchRequest(final DN baseDn, final String resourceId) {
            return newSearchRequest(baseDn.child(rdn(resourceId)), BASE_OBJECT, objectClassPresent());
        }
        @Override
        public String getResourceIdLdapAttribute() {
            return attribute.toString();
        }
        @Override
        public String decodeResourceId(final Entry entry) {
            return entry.parseAttribute(attribute).asString();
        }
        @Override
        public void encodeResourceId(final DN baseDn, final String resourceId, final Entry entry)
                throws ResourceException {
            if (resourceId != null) {
                entry.setName(baseDn.child(rdn(resourceId)));
                entry.addAttribute(new LinkedAttribute(attribute, ByteString.valueOfUtf8(resourceId)));
            } else if (entry.getAttribute(attribute) != null) {
                entry.setName(baseDn.child(rdn(entry.parseAttribute(attribute).asString())));
            } else {
                throw newBadRequestException(ERR_CLIENT_PROVIDED_RESOURCE_ID_MISSING.get());
            }
        }
        private RDN rdn(final String resourceId) {
            return new RDN(attribute.getAttributeType(), resourceId);
        }
    }
    /**
     * Responsible for routing collection requests (CQ) to this collection. More specifically, given the
     * URL template /collection/{id} then this handler processes requests against /collection.
     */
    private final class CollectionHandler extends AbstractRequestHandler {
        private CollectionHandler() {
            super(new BadRequestException(ERR_UNSUPPORTED_REQUEST_AGAINST_COLLECTION.get().toString()));
        }
        @Override
        public Promise<ActionResponse, ResourceException> handleAction(final Context context,
                                                                       final ActionRequest request) {
            return new NotSupportedException(ERR_COLLECTION_ACTIONS_NOT_SUPPORTED.get().toString()).asPromise();
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handleCreate(final Context context,
                                                                         final CreateRequest request) {
            return collection(context).create(context, request);
        }
        @Override
        public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request,
                                                                     final QueryResourceHandler handler) {
            return collection(context).query(context, request, handler);
        }
    }
    /**
     * Responsible for processing instance requests (RUDPA) against this collection and collection requests (CQ) to
     * any collections sharing the same base URL as an instance within this collection. More specifically, given the
     * URL template /collection/{parent}/{child} then this handler processes requests against {parent} since it is
     * both an instance within /collection and also a collection of {child}.
     */
    private final class InstanceHandler implements RequestHandler {
        @Override
        public Promise<ActionResponse, ResourceException> handleAction(final Context context,
                                                                       final ActionRequest request) {
            return collection(context).action(context, idFrom(context), request);
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handleCreate(final Context context,
                                                                         final CreateRequest request) {
            return route(context)
                    .thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
                        @Override
                        public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) {
                            return subResourceRouterFrom(context).handleCreate(context, request);
                        }
                    }).thenCatch(this.<ResourceResponse>convert404To400());
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handleDelete(final Context context,
                                                                         final DeleteRequest request) {
            return collection(context).delete(context, idFrom(context), request);
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handlePatch(final Context context,
                                                                        final PatchRequest request) {
            return collection(context).patch(context, idFrom(context), request);
        }
        @Override
        public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request,
                                                                     final QueryResourceHandler handler) {
            return route(context)
                    .thenAsync(new AsyncFunction<RoutingContext, QueryResponse, ResourceException>() {
                        @Override
                        public Promise<QueryResponse, ResourceException> apply(final RoutingContext context) {
                            return subResourceRouterFrom(context).handleQuery(context, request, handler);
                        }
                    }).thenCatch(this.<QueryResponse>convert404To400());
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handleRead(final Context context,
                                                                       final ReadRequest request) {
            return collection(context).read(context, idFrom(context), request);
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handleUpdate(final Context context,
                                                                         final UpdateRequest request) {
            return collection(context).update(context, idFrom(context), request);
        }
        private <T> Function<ResourceException, T, ResourceException> convert404To400() {
            return SubResource.convert404To400(ERR_UNSUPPORTED_REQUEST_AGAINST_INSTANCE.get());
        }
    }
    /**
     * Responsible for routing requests to sub-resources of instances within this collection. More specifically, given
     * the URL template /collection/{id} then this handler processes all requests beneath /collection/{id}.
     */
    private final class SubResourceHandler implements RequestHandler {
        @Override
        public Promise<ActionResponse, ResourceException> handleAction(final Context context,
                                                                       final ActionRequest request) {
            return route(context).thenAsync(new AsyncFunction<RoutingContext, ActionResponse, ResourceException>() {
                @Override
                public Promise<ActionResponse, ResourceException> apply(final RoutingContext context) {
                    return subResourceRouterFrom(context).handleAction(context, request);
                }
            });
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handleCreate(final Context context,
                                                                         final CreateRequest request) {
            return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
                @Override
                public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) {
                    return subResourceRouterFrom(context).handleCreate(context, request);
                }
            });
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handleDelete(final Context context,
                                                                         final DeleteRequest request) {
            return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
                @Override
                public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) {
                    return subResourceRouterFrom(context).handleDelete(context, request);
                }
            });
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handlePatch(final Context context,
                                                                        final PatchRequest request) {
            return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
                @Override
                public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) {
                    return subResourceRouterFrom(context).handlePatch(context, request);
                }
            });
        }
        @Override
        public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request,
                                                                     final QueryResourceHandler handler) {
            return route(context).thenAsync(new AsyncFunction<RoutingContext, QueryResponse, ResourceException>() {
                @Override
                public Promise<QueryResponse, ResourceException> apply(final RoutingContext context) {
                    return subResourceRouterFrom(context).handleQuery(context, request, handler);
                }
            });
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handleRead(final Context context,
                                                                       final ReadRequest request) {
            return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
                @Override
                public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) {
                    return subResourceRouterFrom(context).handleRead(context, request);
                }
            });
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handleUpdate(final Context context,
                                                                         final UpdateRequest request) {
            return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
                @Override
                public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) {
                    return subResourceRouterFrom(context).handleUpdate(context, request);
                }
            });
        }
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java
@@ -16,20 +16,30 @@
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.i18n.LocalizableMessage.raw;
import static org.forgerock.json.resource.Responses.newResourceResponse;
import static org.forgerock.opendj.ldap.ResultCode.Enum.NOT_ALLOWED_ON_NONLEAF;
import static org.forgerock.opendj.ldap.SearchScope.BASE_OBJECT;
import static org.forgerock.opendj.ldap.responses.Responses.newResult;
import static org.forgerock.opendj.ldap.spi.LdapPromises.newSuccessfulLdapPromise;
import static org.forgerock.opendj.rest2ldap.FilterType.*;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.*;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
import static java.util.Arrays.asList;
import static org.forgerock.json.resource.ResourceException.FORBIDDEN;
import static org.forgerock.json.resource.ResourceException.newResourceException;
import static org.forgerock.json.resource.Responses.newActionResponse;
import static org.forgerock.json.resource.Responses.newQueryResponse;
import static org.forgerock.json.resource.Responses.newResourceResponse;
import static org.forgerock.opendj.ldap.ByteString.valueOfBytes;
import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
import static org.forgerock.opendj.ldap.Filter.alwaysTrue;
import static org.forgerock.opendj.ldap.requests.Requests.newAddRequest;
import static org.forgerock.opendj.ldap.requests.Requests.newDeleteRequest;
import static org.forgerock.opendj.ldap.requests.Requests.newModifyRequest;
import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
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.Rest2Ldap.asResourceException;
import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
import static org.forgerock.opendj.rest2ldap.Utils.newNotSupportedException;
import static org.forgerock.opendj.rest2ldap.Utils.toFilter;
import static org.forgerock.util.Utils.asEnum;
import static org.forgerock.util.promise.Promises.newResultPromise;
import static org.forgerock.util.promise.Promises.when;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@@ -40,6 +50,7 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
@@ -48,7 +59,7 @@
import org.forgerock.json.JsonValueException;
import org.forgerock.json.resource.ActionRequest;
import org.forgerock.json.resource.ActionResponse;
import org.forgerock.json.resource.CollectionResourceProvider;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.CreateRequest;
import org.forgerock.json.resource.DeleteRequest;
import org.forgerock.json.resource.NotSupportedException;
@@ -61,7 +72,6 @@
import org.forgerock.json.resource.ReadRequest;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.Responses;
import org.forgerock.json.resource.UncategorizedException;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.opendj.ldap.Attribute;
@@ -72,9 +82,12 @@
import org.forgerock.opendj.ldap.DecodeException;
import org.forgerock.opendj.ldap.DecodeOptions;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.EntryNotFoundException;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.LdapPromise;
import org.forgerock.opendj.ldap.Modification;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.controls.AssertionRequestControl;
@@ -88,7 +101,6 @@
import org.forgerock.opendj.ldap.requests.AddRequest;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.PasswordModifyExtendedRequest;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.responses.PasswordModifyExtendedResult;
import org.forgerock.opendj.ldap.responses.Result;
@@ -104,72 +116,69 @@
import org.forgerock.util.promise.ExceptionHandler;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.PromiseImpl;
import org.forgerock.util.promise.Promises;
import org.forgerock.util.promise.ResultHandler;
import org.forgerock.util.query.QueryFilter;
import org.forgerock.util.query.QueryFilterVisitor;
/**
 * A {@code CollectionResourceProvider} implementation which maps a JSON
 * resource collection to LDAP entries beneath a base DN.
 */
final class SubResourceImpl implements CollectionResourceProvider {
/** Implements the core CREST operations supported by singleton and collection sub-resources. */
final class SubResourceImpl {
    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
    /** Dummy exception used for signalling search success. */
    private static final ResourceException SUCCESS = new UncategorizedException(0, null, null);
    /** Empty decode options required for decoding response controls. */
    private static final DecodeOptions DECODE_OPTIONS = new DecodeOptions();
    private static final JsonPointer ROOT = new JsonPointer();
    private final List<Attribute> additionalLDAPAttributes;
    private final PropertyMapper propertyMapper;
    private final DN baseDn; // TODO: support template variables.
    private final Config config;
    private final DN baseDn;
    private final AttributeDescription etagAttribute;
    private final NamingStrategy namingStrategy;
    private final DecodeOptions decodeOptions;
    private final ReadOnUpdatePolicy readOnUpdatePolicy;
    private final boolean useSubtreeDelete;
    private final boolean usePermissiveModify;
    private final Resource resource;
    private final Attribute glueObjectClasses;
    SubResourceImpl(final DN baseDn, final PropertyMapper mapper,
                    final NamingStrategy namingStrategy, final AttributeDescription etagAttribute,
                    final Config config, final List<Attribute> additionalLDAPAttributes) {
    SubResourceImpl(final Rest2Ldap rest2Ldap, final DN baseDn, final Attribute glueObjectClasses,
                    final NamingStrategy namingStrategy, final Resource resource) {
        this.readOnUpdatePolicy = rest2Ldap.getOptions().get(READ_ON_UPDATE_POLICY);
        this.useSubtreeDelete = rest2Ldap.getOptions().get(USE_SUBTREE_DELETE);
        this.usePermissiveModify = rest2Ldap.getOptions().get(USE_PERMISSIVE_MODIFY);
        this.etagAttribute = rest2Ldap.getOptions().get(USE_MVCC)
                ? AttributeDescription.valueOf(rest2Ldap.getOptions().get(MVCC_ATTRIBUTE)) : null;
        this.decodeOptions = rest2Ldap.getOptions().get(DECODE_OPTIONS);
        this.baseDn = baseDn;
        this.propertyMapper = mapper;
        this.config = config;
        this.glueObjectClasses = glueObjectClasses;
        this.namingStrategy = namingStrategy;
        this.etagAttribute = etagAttribute;
        this.additionalLDAPAttributes = additionalLDAPAttributes;
        this.resource = resource;
    }
    @Override
    public Promise<ActionResponse, ResourceException> actionCollection(
            final Context context, final ActionRequest request) {
        return Promises.<ActionResponse, ResourceException> newExceptionPromise(
                                                            newNotSupportedException(ERR_NOT_YET_IMPLEMENTED.get()));
    }
    @Override
    public Promise<ActionResponse, ResourceException> actionInstance(
    Promise<ActionResponse, ResourceException> action(
            final Context context, final String resourceId, final ActionRequest request) {
        String actionId = request.getAction();
        if (actionId.equals("passwordModify")) {
            return passwordModify(context, resourceId, request);
        try {
            final Action action = asEnum(request.getAction(), Action.class);
            if (resource.hasSupportedAction(action)) {
                switch (action) {
                case PASSWORDMODIFY:
                    return passwordModify(context, resourceId, request);
                }
            }
        } catch (final IllegalArgumentException ignored) {
            // fall-through
        }
        return Promises.<ActionResponse, ResourceException> newExceptionPromise(
                newNotSupportedException(ERR_ACTION_NOT_SUPPORTED.get(actionId)));
        return newNotSupportedException(ERR_ACTION_NOT_SUPPORTED.get(request.getAction())).asPromise();
    }
    private Promise<ActionResponse, ResourceException> passwordModify(
            final Context context, final String resourceId, final ActionRequest request) {
        if (!context.containsContext(ClientContext.class)
                || !context.asContext(ClientContext.class).isSecure()) {
            return Promises.newExceptionPromise(ResourceException.newResourceException(
                    ResourceException.FORBIDDEN, ERR_PASSWORD_MODIFY_SECURE_CONNECTION.get().toString()));
            return newResourceException(FORBIDDEN, ERR_PASSWORD_MODIFY_SECURE_CONNECTION.get().toString()).asPromise();
        }
        if (!context.containsContext(SecurityContext.class)
                || context.asContext(SecurityContext.class).getAuthenticationId() == null) {
            return Promises.newExceptionPromise(ResourceException.newResourceException(
                    ResourceException.FORBIDDEN, ERR_PASSWORD_MODIFY_USER_AUTHENTICATED.get().toString()));
            return newResourceException(FORBIDDEN, ERR_PASSWORD_MODIFY_USER_AUTHENTICATED.get().toString()).asPromise();
        }
        final JsonValue jsonContent = request.getContent();
@@ -182,231 +191,307 @@
            final LocalizableMessage msg = ERR_PASSWORD_MODIFY_REQUEST_IS_INVALID.get();
            final ResourceException ex = newBadRequestException(msg, e);
            logger.error(msg, e);
            return Promises.newExceptionPromise(ex);
            return ex.asPromise();
        }
        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
        List<JsonPointer> attrs = Collections.emptyList();
        return connection.searchSingleEntryAsync(searchRequest(connection, resourceId, attrs))
                         .thenAsync(new AsyncFunction<SearchResultEntry, ActionResponse, ResourceException>() {
                             @Override
                             public Promise<ActionResponse, ResourceException> apply(
                                       final SearchResultEntry entry) {
                                 PasswordModifyExtendedRequest pwdModifyRequest =
                                         Requests.newPasswordModifyExtendedRequest();
                                 pwdModifyRequest.setUserIdentity("dn: " + entry.getName());
                                 pwdModifyRequest.setOldPassword(asBytes(oldPassword));
                                 pwdModifyRequest.setNewPassword(asBytes(newPassword));
                                 return connection.extendedRequestAsync(pwdModifyRequest)
                                     .thenAsync(new AsyncFunction<PasswordModifyExtendedResult,
                                             ActionResponse, ResourceException>() {
                                         @Override
                                         public Promise<ActionResponse, ResourceException> apply(
                                                 PasswordModifyExtendedResult value) throws ResourceException {
                                             JsonValue result = new JsonValue(new LinkedHashMap<>());
                                             byte[] generatedPwd = value.getGeneratedPassword();
                                             if (generatedPwd != null) {
                                                 result = result.put("generatedPassword",
                                                         ByteString.valueOfBytes(generatedPwd).toString());
                                             }
                                             return Responses.newActionResponse(result).asPromise();
                                         }
                                     }, Exceptions.<ActionResponse>toResourceException());
                             }
                         }, Exceptions.<ActionResponse>toResourceException());
        final Connection connection = connectionFrom(context);
        return resolveResourceDnAndType(context, connection, resourceId, null)
                .thenAsync(new AsyncFunction<RoutingContext, PasswordModifyExtendedResult, ResourceException>() {
                    @Override
                    public Promise<PasswordModifyExtendedResult, ResourceException> apply(RoutingContext dnAndType) {
                        final PasswordModifyExtendedRequest pwdModifyRequest = newPasswordModifyExtendedRequest()
                                .setUserIdentity("dn: " + dnAndType.getDn())
                                .setOldPassword(asBytes(oldPassword))
                                .setNewPassword(asBytes(newPassword));
                        return connection.extendedRequestAsync(pwdModifyRequest)
                                         .thenCatchAsync(adaptLdapException(PasswordModifyExtendedResult.class));
                    }
                }).thenAsync(new AsyncFunction<PasswordModifyExtendedResult, ActionResponse, ResourceException>() {
                    @Override
                    public Promise<ActionResponse, ResourceException> apply(PasswordModifyExtendedResult r) {
                        final JsonValue result = new JsonValue(new LinkedHashMap<>());
                        final byte[] generatedPwd = r.getGeneratedPassword();
                        if (generatedPwd != null) {
                            result.put("generatedPassword", valueOfBytes(generatedPwd).toString());
                        }
                        return newActionResponse(result).asPromise();
                    }
                });
    }
    private byte[] asBytes(final String s) {
        return s != null ? s.getBytes(StandardCharsets.UTF_8) : null;
    }
    @Override
    public Promise<ResourceResponse, ResourceException> createInstance(final Context context,
            final CreateRequest request) {
        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
        // Calculate entry content.
        return propertyMapper
                .create(connection, new JsonPointer(), request.getContent())
                .thenAsync(new AsyncFunction<List<Attribute>, ResourceResponse, ResourceException>() {
                    @Override
                    public Promise<ResourceResponse, ResourceException> apply(final List<Attribute> attributes) {
                        // Perform add operation.
                        final AddRequest addRequest = newAddRequest(DN.rootDN());
                        for (final Attribute attribute : additionalLDAPAttributes) {
                            addRequest.addAttribute(attribute);
                        }
                        for (final Attribute attribute : attributes) {
                            addRequest.addAttribute(attribute);
                        }
                        try {
                            namingStrategy.setResourceId(connection, getBaseDn(),
                                                         request.getNewResourceId(),
                                                         addRequest);
                        } catch (final ResourceException e) {
                            logger.error(raw(e.getLocalizedMessage()), e);
                            return Promises.newExceptionPromise(e);
                        }
                        if (config.readOnUpdatePolicy() == CONTROLS) {
                            addRequest.addControl(PostReadRequestControl.newControl(
                                    false, getLdapAttributes(connection, request.getFields())));
                        }
                        return connection.applyChangeAsync(addRequest)
                                         .thenAsync(
                                                 postUpdateResultAsyncFunction(connection),
                                                 Exceptions.<ResourceResponse>toResourceException());
                    }
                });
    }
    @Override
    public Promise<ResourceResponse, ResourceException> deleteInstance(
            final Context context, final String resourceId, final DeleteRequest request) {
        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
        return doUpdateFunction(connection, resourceId, request.getRevision())
                .thenAsync(new AsyncFunction<DN, ResourceResponse, ResourceException>() {
                    @Override
                    public Promise<ResourceResponse, ResourceException> apply(DN dn) throws ResourceException {
                        try {
                            final ChangeRecord deleteRequest = newDeleteRequest(dn);
                            if (config.readOnUpdatePolicy() == CONTROLS) {
                                final String[] attributes = getLdapAttributes(connection, request.getFields());
                                deleteRequest.addControl(PreReadRequestControl.newControl(false, attributes));
                            }
                            if (config.useSubtreeDelete()) {
                                deleteRequest.addControl(SubtreeDeleteRequestControl.newControl(true));
                            }
                            addAssertionControl(deleteRequest, request.getRevision());
                            return connection.applyChangeAsync(deleteRequest)
                                             .thenAsync(
                                                     postUpdateResultAsyncFunction(connection),
                                                     Exceptions.<ResourceResponse>toResourceException());
                        } catch (final Exception e) {
                            return Promises.newExceptionPromise(asResourceException(e));
                        }
                    }
                });
    }
    @Override
    public Promise<ResourceResponse, ResourceException> patchInstance(
            final Context context, final String resourceId, final PatchRequest request) {
        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
        if (request.getPatchOperations().isEmpty()) {
            return emptyPatchInstance(connection, resourceId, request);
    Promise<ResourceResponse, ResourceException> create(final Context context, final CreateRequest  request) {
        // First determine the type of resource being created.
        final Resource subType;
        try {
            subType = resource.resolveSubTypeFromJson(request.getContent());
        } catch (final ResourceException e) {
            return e.asPromise();
        }
        return doUpdateFunction(connection, resourceId, request.getRevision())
                .thenAsync(new AsyncFunction<DN, ResourceResponse, ResourceException>() {
        // Now build the LDAP representation and add it.
        final Connection connection = connectionFrom(context);
        return subType.getPropertyMapper()
                      .create(connection, subType, ROOT, request.getContent())
                      .thenAsync(new AsyncFunction<List<Attribute>, ResourceResponse, ResourceException>() {
                          @Override
                          public Promise<ResourceResponse, ResourceException> apply(final List<Attribute> attributes) {
                              // Perform add operation.
                              final AddRequest addRequest = newAddRequest(DN.rootDN());
                              addRequest.addAttribute(subType.getObjectClassAttribute());
                              for (final Attribute attribute : attributes) {
                                  addRequest.addAttribute(attribute);
                              }
                              try {
                                  namingStrategy.encodeResourceId(baseDn, request.getNewResourceId(), addRequest);
                              } catch (final ResourceException e) {
                                  logger.error(raw(e.getLocalizedMessage()), e);
                                  return e.asPromise();
                              }
                              if (readOnUpdatePolicy == CONTROLS) {
                                  final Set<String> ldapAttributes =
                                          getLdapAttributesForKnownType(request.getFields(), subType);
                                  addRequest.addControl(PostReadRequestControl.newControl(false, ldapAttributes));
                              }
                              return connection.addAsync(addRequest)
                                               .thenCatchAsync(lazilyAddGlueEntry(connection, addRequest))
                                               .thenAsync(encodeUpdateResourceResponse(connection, subType),
                                                          adaptLdapException(ResourceResponse.class));
                          }
                      });
    }
    /**
     * A resource and sub-resource may be separated by a "glue" entry in LDAP. This method detects when a glue entry
     * is missing, creates it, and then retries the original add operation. As a concrete example, consider the
     * backend configuration entry "ds-cfg-backend-id=userRoot,cn=backends,cn=config". Since its indexes are located
     * beneath "cn=Indexes,ds-cfg-backend-id=userRoot,cn=backends,cn=config" we need to add "cn=Indexes" before
     * adding an index entry.
     */
    private AsyncFunction<LdapException, Result, LdapException> lazilyAddGlueEntry(final Connection connection,
                                                                                   final AddRequest addRequest) {
        return new AsyncFunction<LdapException, Result, LdapException>() {
            @Override
            public Promise<Result, LdapException> apply(final LdapException e) throws LdapException {
                if (glueObjectClasses != null && e instanceof EntryNotFoundException) {
                    // The parent glue entry may be missing - lazily create it.
                    final AddRequest glueAddRequest = newAddRequest(baseDn);
                    glueAddRequest.addAttribute(glueObjectClasses);
                    glueAddRequest.addAttribute(baseDn.rdn().getFirstAVA().toAttribute());
                    return connection.addAsync(glueAddRequest)
                                     .thenAsync(new AsyncFunction<Result, Result, LdapException>() {
                                         @Override
                                         public Promise<Result, LdapException> apply(final Result value) {
                                             return connection.addAsync(addRequest);
                                         }
                                     });
                }
                // Something else happened, so rethrow.
                throw e;
            }
        };
    }
    private Connection connectionFrom(final Context context) {
        return context.asContext(AuthenticatedConnectionContext.class).getConnection();
    }
    Promise<ResourceResponse, ResourceException> delete(
            final Context context, final String resourceId, final DeleteRequest request) {
        final Connection connection = connectionFrom(context);
        return resolveResourceDnAndType(context, connection, resourceId, request.getRevision())
                .thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
                    @Override
                    public Promise<ResourceResponse, ResourceException> apply(final DN dn) throws ResourceException {
                        // Convert the patch operations to LDAP modifications.
                        List<Promise<List<Modification>, ResourceException>> promises =
                                new ArrayList<>(request.getPatchOperations().size());
                        for (final PatchOperation operation : request.getPatchOperations()) {
                            promises.add(propertyMapper.patch(connection, new JsonPointer(), operation));
                    public Promise<ResourceResponse, ResourceException> apply(final RoutingContext dnAndType)
                            throws ResourceException {
                        final ChangeRecord deleteRequest = newDeleteRequest(dnAndType.getDn());
                        if (readOnUpdatePolicy == CONTROLS) {
                            final Set<String> attributes =
                                    getLdapAttributesForKnownType(request.getFields(), dnAndType.getType());
                            deleteRequest.addControl(PreReadRequestControl.newControl(false, attributes));
                        }
                        return Promises.when(promises).thenAsync(
                                new AsyncFunction<List<List<Modification>>, ResourceResponse, ResourceException>() {
                                    @Override
                                    public Promise<ResourceResponse, ResourceException> apply(
                                            final List<List<Modification>> result) {
                                        // The patch operations have been converted successfully.
                                        try {
                                            final ModifyRequest modifyRequest = newModifyRequest(dn);
                                            // Add the modifications.
                                            for (final List<Modification> modifications : result) {
                                                if (modifications != null) {
                                                    modifyRequest.getModifications().addAll(modifications);
                                                }
                                            }
                                            final List<String> attributes =
                                                    asList(getLdapAttributes(connection, request.getFields()));
                                            if (modifyRequest.getModifications().isEmpty()) {
                                                // This patch is a no-op so just read the entry and check its version.
                                                return
                                                   connection
                                                     .readEntryAsync(dn, attributes)
                                                     .thenAsync(postEmptyPatchAsyncFunction(connection, request),
                                                                Exceptions.<ResourceResponse>toResourceException());
                                            } else {
                                                // Add controls and perform the modify request.
                                                if (config.readOnUpdatePolicy() == CONTROLS) {
                                                    modifyRequest.addControl(
                                                            PostReadRequestControl.newControl(false, attributes));
                                                }
                                                if (config.usePermissiveModify()) {
                                                    modifyRequest.addControl(
                                                            PermissiveModifyRequestControl.newControl(true));
                                                }
                                                addAssertionControl(modifyRequest, request.getRevision());
                                                return connection
                                                        .applyChangeAsync(modifyRequest)
                                                        .thenAsync(
                                                                postUpdateResultAsyncFunction(connection),
                                                                Exceptions.<ResourceResponse>toResourceException());
                                            }
                                        } catch (final Exception e) {
                                            return Promises.newExceptionPromise(asResourceException(e));
                                        }
                                    }
                                });
                        if (resource.mayHaveSubResources() && useSubtreeDelete) {
                            // Non-critical so that we can detect failure and retry without the control. Some backends,
                            // such as cn=config, do not support the subtree delete control.
                            deleteRequest.addControl(SubtreeDeleteRequestControl.newControl(false));
                        }
                        addAssertionControl(deleteRequest, request.getRevision());
                        return connection.applyChangeAsync(deleteRequest)
                                         .thenCatchAsync(deleteSubtreeWithoutUsingSubtreeDeleteControl(connection,
                                                                                                       deleteRequest))
                                         .thenAsync(encodeUpdateResourceResponse(connection, dnAndType.getType()),
                                                    adaptLdapException(ResourceResponse.class));
                    }
                });
    }
    /** Just read the entry and check its version. */
    private Promise<ResourceResponse, ResourceException> emptyPatchInstance(final Connection connection,
            final String resourceId, final PatchRequest request) {
        final SearchRequest searchRequest = searchRequest(connection, resourceId, request.getFields());
        return connection
                .searchSingleEntryAsync(searchRequest)
                .thenAsync(postEmptyPatchAsyncFunction(connection, request),
                           Exceptions.<ResourceResponse>toResourceException());
    /**
     * Detects whether a delete request failed because the targeted entry has children and the subtree delete control
     * could not be applied (e.g. due to ACIs or lack of support in the backend). On failure, fall-back to a search
     * and then a recursive bottom up delete of all subordinate entries, before finally retrying the original delete
     * request.
     */
    private AsyncFunction<LdapException, Result, LdapException> deleteSubtreeWithoutUsingSubtreeDeleteControl(
            final Connection connection, final ChangeRecord deleteRequest) {
        return new AsyncFunction<LdapException, Result, LdapException>() {
            @Override
            public Promise<Result, LdapException> apply(final LdapException e) throws LdapException {
                if (e.getResult().getResultCode().asEnum() != NOT_ALLOWED_ON_NONLEAF
                        || !resource.mayHaveSubResources()) {
                    throw e;
                }
                // Perform a subtree search and then delete entries one by one.
                final SearchRequest subordinates = newSearchRequest(deleteRequest.getName(),
                                                                    SearchScope.SUBORDINATES,
                                                                    Filter.objectClassPresent(),
                                                                    "1.1");
                // This list does not need synchronization because search result notification is synchronized.
                final List<DN> subordinateEntries = new ArrayList<>();
                return connection.searchAsync(subordinates, new SearchResultHandler() {
                    @Override
                    public boolean handleEntry(final SearchResultEntry entry) {
                        subordinateEntries.add(entry.getName());
                        return true;
                    }
                    @Override
                    public boolean handleReference(final SearchResultReference reference) {
                        return false;
                    }
                }).thenAsync(new AsyncFunction<Result, Result, LdapException>() {
                    @Override
                    public Promise<Result, LdapException> apply(final Result result) {
                        // Sort the entries in hierarchical order and then delete them in reverse, thus
                        // always deleting children before parents.
                        Collections.sort(subordinateEntries);
                        LdapPromise<Result> promise = newSuccessfulLdapPromise(newResult(ResultCode.SUCCESS));
                        for (int i = subordinateEntries.size() - 1; i >= 0; i--) {
                            final ChangeRecord subordinateDelete = newDeleteRequest(subordinateEntries.get(i));
                            promise = promise.thenAsync(new AsyncFunction<Result, Result, LdapException>() {
                                @Override
                                public Promise<Result, LdapException> apply(final Result result) {
                                    return connection.applyChangeAsync(subordinateDelete);
                                }
                            });
                        }
                        // And finally retry the original delete request.
                        return promise.thenAsync(new AsyncFunction<Result, Result, LdapException>() {
                            @Override
                            public Promise<Result, LdapException> apply(final Result result) {
                                return connection.applyChangeAsync(deleteRequest);
                            }
                        });
                    }
                });
            }
        };
    }
    private AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException> postEmptyPatchAsyncFunction(
            final Connection connection, final PatchRequest request) {
        return new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
    Promise<ResourceResponse, ResourceException> patch(
            final Context context, final String resourceId, final PatchRequest request) {
        final Connection connection = connectionFrom(context);
        final AtomicReference<RoutingContext> dnAndTypeHolder = new AtomicReference<>();
        return resolveResourceDnAndType(context, connection, resourceId, request.getRevision())
                .thenAsync(new AsyncFunction<RoutingContext, List<List<Modification>>, ResourceException>() {
                    @Override
                    public Promise<List<List<Modification>>, ResourceException> apply(final RoutingContext dnAndType)
                            throws ResourceException {
                        dnAndTypeHolder.set(dnAndType);
                        // Convert the patch operations to LDAP modifications.
                        final List<Promise<List<Modification>, ResourceException>> promises =
                                new ArrayList<>(request.getPatchOperations().size());
                        final Resource subType = dnAndType.getType();
                        final PropertyMapper propertyMapper = subType.getPropertyMapper();
                        for (final PatchOperation operation : request.getPatchOperations()) {
                            promises.add(propertyMapper.patch(connection, subType, ROOT, operation));
                        }
                        return when(promises);
                    }
                }).thenAsync(new AsyncFunction<List<List<Modification>>, ResourceResponse, ResourceException>() {
                    @Override
                    public Promise<ResourceResponse, ResourceException> apply(final List<List<Modification>> result)
                            throws ResourceException {
                        // The patch operations have been converted successfully.
                        final RoutingContext dnAndType = dnAndTypeHolder.get();
                        final ModifyRequest modifyRequest = newModifyRequest(dnAndType.getDn());
                        // Add the modifications.
                        for (final List<Modification> modifications : result) {
                            if (modifications != null) {
                                modifyRequest.getModifications().addAll(modifications);
                            }
                        }
                        final Resource subType = dnAndType.getType();
                        final Set<String> attributes = getLdapAttributesForKnownType(request.getFields(), subType);
                        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(connection, subType, request),
                                                        adaptLdapException(ResourceResponse.class));
                        } else {
                            // Add controls and perform the modify request.
                            if (readOnUpdatePolicy == CONTROLS) {
                                modifyRequest.addControl(PostReadRequestControl.newControl(false, attributes));
                            }
                            if (usePermissiveModify) {
                                modifyRequest.addControl(PermissiveModifyRequestControl.newControl(true));
                            }
                            addAssertionControl(modifyRequest, request.getRevision());
                            return connection.applyChangeAsync(modifyRequest)
                                             .thenAsync(encodeUpdateResourceResponse(connection, subType),
                                                        adaptLdapException(ResourceResponse.class));
                        }
                    }
                });
    }
    private AsyncFunction<Entry, ResourceResponse, ResourceException> encodeEmptyPatchResourceResponse(
            final Connection connection, final Resource resource, final PatchRequest request) {
        return new AsyncFunction<Entry, ResourceResponse, ResourceException>() {
            @Override
            public Promise<ResourceResponse, ResourceException> apply(SearchResultEntry entry)
                    throws ResourceException {
            public Promise<ResourceResponse, ResourceException> apply(Entry entry) throws ResourceException {
                try {
                    // Fail if there is a version mismatch.
                    ensureMvccVersionMatches(entry, request.getRevision());
                    return adaptEntry(connection, entry);
                    return encodeResourceResponse(connection, resource, entry);
                } catch (final Exception e) {
                    return Promises.newExceptionPromise(asResourceException(e));
                    return asResourceException(e).asPromise();
                }
            }
        };
    }
    @Override
    public Promise<QueryResponse, ResourceException> queryCollection(
    Promise<QueryResponse, ResourceException> query(
            final Context context, final QueryRequest request, final QueryResourceHandler resourceHandler) {
        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
        // Calculate the filter (this may require the connection).
        final Connection connection = connectionFrom(context);
        return getLdapFilter(connection, request.getQueryFilter())
                .thenAsync(runQuery(request, resourceHandler, connection));
    }
    private Promise<Filter, ResourceException> getLdapFilter(final Connection connection,
                                                             final QueryFilter<JsonPointer> queryFilter) {
    // FIXME: supporting assertions against sub-type properties.
    private Promise<Filter, ResourceException> getLdapFilter(
            final Connection connection, final QueryFilter<JsonPointer> queryFilter) {
        if (queryFilter == null) {
            return new BadRequestException(ERR_QUERY_BY_ID_OR_EXPRESSION_NOT_SUPPORTED.get().toString()).asPromise();
        }
        final PropertyMapper propertyMapper = resource.getPropertyMapper();
        final QueryFilterVisitor<Promise<Filter, ResourceException>, Void, JsonPointer> visitor =
                new QueryFilterVisitor<Promise<Filter, ResourceException>, Void, JsonPointer>() {
                    @Override
                    public Promise<Filter, ResourceException> visitAndFilter(final Void unused,
                            final List<QueryFilter<JsonPointer>> subFilters) {
                    public Promise<Filter, ResourceException> visitAndFilter(
                            final Void unused, final List<QueryFilter<JsonPointer>> subFilters) {
                        final List<Promise<Filter, ResourceException>> promises = new ArrayList<>(subFilters.size());
                        for (final QueryFilter<JsonPointer> subFilter : subFilters) {
                            promises.add(subFilter.accept(this, unused));
                        }
                        return Promises.when(promises).then(new Function<List<Filter>, Filter, ResourceException>() {
                        return when(promises).then(new Function<List<Filter>, Filter, ResourceException>() {
                            @Override
                            public Filter apply(final List<Filter> value) {
                                // Check for unmapped filter components and optimize.
@@ -434,56 +519,58 @@
                    @Override
                    public Promise<Filter, ResourceException> visitBooleanLiteralFilter(
                            final Void unused, final boolean value) {
                        return Promises.newResultPromise(toFilter(value));
                        return newResultPromise(toFilter(value));
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitContainsFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return propertyMapper.getLdapFilter(
                                connection, new JsonPointer(), field, FilterType.CONTAINS, null, valueAssertion);
                                connection, resource, ROOT, field, CONTAINS, null, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitEqualsFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return propertyMapper.getLdapFilter(
                                connection, new JsonPointer(), field, FilterType.EQUAL_TO, null, valueAssertion);
                                connection, resource, ROOT, field, EQUAL_TO, null, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitExtendedMatchFilter(final Void unused,
                            final JsonPointer field, final String operator, final Object valueAssertion) {
                                                                                       final JsonPointer field,
                                                                                       final String operator,
                                                                                       final Object valueAssertion) {
                        return propertyMapper.getLdapFilter(
                                connection, new JsonPointer(), field, FilterType.EXTENDED, operator, valueAssertion);
                                connection, resource, ROOT, field, EXTENDED, operator, valueAssertion);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitGreaterThanFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return propertyMapper.getLdapFilter(
                                connection, new JsonPointer(), field, FilterType.GREATER_THAN, null, valueAssertion);
                                connection, 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(connection, new JsonPointer(), field,
                                                            FilterType.GREATER_THAN_OR_EQUAL_TO, null, valueAssertion);
                        return propertyMapper.getLdapFilter(
                                connection, 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(
                                connection, new JsonPointer(), field, FilterType.LESS_THAN, null, valueAssertion);
                                connection, 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(connection, new JsonPointer(), field,
                                                            FilterType.LESS_THAN_OR_EQUAL_TO, null, valueAssertion);
                        return propertyMapper.getLdapFilter(
                                connection, resource, ROOT, field, LESS_THAN_OR_EQUAL_TO, null, valueAssertion);
                    }
                    @Override
@@ -504,14 +591,14 @@
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitOrFilter(final Void unused,
                            final List<QueryFilter<JsonPointer>> subFilters) {
                    public Promise<Filter, ResourceException> visitOrFilter(
                            final Void unused, final List<QueryFilter<JsonPointer>> subFilters) {
                        final List<Promise<Filter, ResourceException>> promises = new ArrayList<>(subFilters.size());
                        for (final QueryFilter<JsonPointer> subFilter : subFilters) {
                            promises.add(subFilter.accept(this, unused));
                        }
                        return Promises.when(promises).then(new Function<List<Filter>, Filter, ResourceException>() {
                        return when(promises).then(new Function<List<Filter>, Filter, ResourceException>() {
                            @Override
                            public Filter apply(final List<Filter> value) {
                                // Check for unmapped filter components and optimize.
@@ -539,30 +626,25 @@
                    @Override
                    public Promise<Filter, ResourceException> visitPresentFilter(
                            final Void unused, final JsonPointer field) {
                        return propertyMapper.getLdapFilter(
                                connection, new JsonPointer(), field, FilterType.PRESENT, null, null);
                        return propertyMapper.getLdapFilter(connection, resource, ROOT, field, PRESENT, null, null);
                    }
                    @Override
                    public Promise<Filter, ResourceException> visitStartsWithFilter(
                            final Void unused, final JsonPointer field, final Object valueAssertion) {
                        return propertyMapper.getLdapFilter(
                                connection, new JsonPointer(), field, FilterType.STARTS_WITH, null, valueAssertion);
                                connection, 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.
        return queryFilter.accept(visitor, null);
    }
    private AsyncFunction<Filter, QueryResponse, ResourceException> runQuery(final QueryRequest request,
            final QueryResourceHandler resourceHandler, final Connection connection) {
    private AsyncFunction<Filter, QueryResponse, ResourceException> runQuery(
            final QueryRequest request, final QueryResourceHandler resourceHandler, final Connection connection) {
        return new AsyncFunction<Filter, QueryResponse, ResourceException>() {
            /**
             * The following fields are guarded by sequenceLock. In addition,
             * the sequenceLock ensures that we send one JSON resource at a time
             * back to the client.
             */
            // The following fields are guarded by sequenceLock. In addition, the sequenceLock ensures that
            // we send one JSON resource at a time back to the client.
            private final Object sequenceLock = new Object();
            private String cookie;
            private ResourceException pendingResult;
@@ -574,18 +656,17 @@
            public Promise<QueryResponse, ResourceException> apply(final Filter ldapFilter) {
                if (ldapFilter == null || ldapFilter == alwaysFalse()) {
                    // Avoid performing a search if the filter could not be mapped or if it will never match.
                    return Promises.newResultPromise(Responses.newQueryResponse());
                    return newQueryResponse().asPromise();
                }
                final PromiseImpl<QueryResponse, ResourceException> promise = PromiseImpl.create();
                // Perform the search.
                final String[] attributes = getLdapAttributes(connection, request.getFields());
                final String[] attributes = getLdapAttributesForUnknownType(request.getFields()).toArray(new String[0]);
                final Filter searchFilter = ldapFilter == Filter.alwaysTrue() ? Filter.objectClassPresent()
                                                                              : ldapFilter;
                final SearchRequest searchRequest = newSearchRequest(
                        getBaseDn(), SearchScope.SINGLE_LEVEL, searchFilter, attributes);
                        : ldapFilter;
                final SearchRequest searchRequest = newSearchRequest(baseDn, SINGLE_LEVEL, searchFilter, attributes);
                // Add the page results control. We can support the page offset by
                // reading the next offset pages, or offset x page size resources.
                // Add the page results control. We can support the page offset by reading the next offset pages, or
                // offset x page size resources.
                final int pageResultStartIndex;
                final int pageSize = request.getPageSize();
                if (request.getPageSize() > 0) {
@@ -634,37 +715,39 @@
                         * The best solution is probably to process the primary search results in batches using
                         * the paged results control.
                         */
                        final String id = namingStrategy.getResourceId(connection, entry);
                        final String id = namingStrategy.decodeResourceId(entry);
                        final String revision = getRevisionFromEntry(entry);
                        propertyMapper.read(connection, new JsonPointer(), entry)
                        final Resource subType = resource.resolveSubTypeFromObjectClasses(entry);
                        final PropertyMapper propertyMapper = subType.getPropertyMapper();
                        propertyMapper.read(connection, subType, ROOT, entry)
                                      .thenOnResult(new ResultHandler<JsonValue>() {
                                           @Override
                                           public void handleResult(final JsonValue result) {
                                               synchronized (sequenceLock) {
                                                   pendingResourceCount--;
                                                   if (!resultSent) {
                                                       resourceHandler.handleResource(
                                                               Responses.newResourceResponse(id, revision, result));
                                                   }
                                                   completeIfNecessary(promise);
                                               }
                                           }
                                       }).thenOnException(new ExceptionHandler<ResourceException>() {
                                           @Override
                                           public void handleException(ResourceException exception) {
                                               synchronized (sequenceLock) {
                                                   pendingResourceCount--;
                                                   completeIfNecessary(exception, promise);
                                               }
                                           }
                                       });
                                          @Override
                                          public void handleResult(final JsonValue result) {
                                              synchronized (sequenceLock) {
                                                  pendingResourceCount--;
                                                  if (!resultSent) {
                                                      resourceHandler.handleResource(
                                                              newResourceResponse(id, revision, result));
                                                  }
                                                  completeIfNecessary(promise);
                                              }
                                          }
                                      })
                                      .thenOnException(new ExceptionHandler<ResourceException>() {
                                          @Override
                                          public void handleException(ResourceException exception) {
                                              synchronized (sequenceLock) {
                                                  pendingResourceCount--;
                                                  completeIfNecessary(exception, promise);
                                              }
                                          }
                                      });
                        return true;
                    }
                    @Override
                    public boolean handleReference(final SearchResultReference reference) {
                        // TODO: should this be classed as an error since
                        // rest2ldap assumes entries are all colocated?
                        // TODO: should this be classed as an error since rest2ldap assumes entries are all colocated?
                        return true;
                    }
@@ -675,7 +758,7 @@
                            if (request.getPageSize() > 0) {
                                try {
                                    final SimplePagedResultsControl control =
                                            result.getControl(SimplePagedResultsControl.DECODER, DECODE_OPTIONS);
                                            result.getControl(SimplePagedResultsControl.DECODER, decodeOptions);
                                    if (control != null && !control.getCookie().isEmpty()) {
                                        cookie = control.getCookie().toBase64String();
                                    }
@@ -688,9 +771,14 @@
                    }
                }).thenOnException(new ExceptionHandler<LdapException>() {
                    @Override
                    public void handleException(LdapException exception) {
                    public void handleException(final LdapException e) {
                        synchronized (sequenceLock) {
                            completeIfNecessary(asResourceException(exception), promise);
                            if (glueObjectClasses != null && e instanceof EntryNotFoundException) {
                                // Glue entry does not exist, so treat this as an empty result set.
                                completeIfNecessary(SUCCESS, promise);
                            } else {
                                completeIfNecessary(asResourceException(e), promise);
                            }
                        }
                    }
                });
@@ -715,7 +803,7 @@
            private void completeIfNecessary(final PromiseImpl<QueryResponse, ResourceException> handler) {
                if (pendingResourceCount == 0 && pendingResult != null && !resultSent) {
                    if (pendingResult == SUCCESS) {
                        handler.handleResult(Responses.newQueryResponse(cookie));
                        handler.handleResult(newQueryResponse(cookie));
                    } else {
                        handler.handleException(pendingResult);
                    }
@@ -725,93 +813,81 @@
        };
    }
    @Override
    public Promise<ResourceResponse, ResourceException> readInstance(
    Promise<ResourceResponse, ResourceException> read(
            final Context context, final String resourceId, final ReadRequest request) {
        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
        // Do the search.
        SearchRequest searchRequest = searchRequest(connection, resourceId, request.getFields());
        return connection
                .searchSingleEntryAsync(searchRequest)
                .thenAsync(new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
                    @Override
                    public Promise<ResourceResponse, ResourceException> apply(SearchResultEntry entry)
                            throws ResourceException {
                        return adaptEntry(connection, entry);
                    }
                }, Exceptions.<ResourceResponse>toResourceException());
        final Connection connection = connectionFrom(context);
        return connection.searchSingleEntryAsync(searchRequestForUnknownType(resourceId, request.getFields()))
                         .thenCatchAsync(adaptLdapException(SearchResultEntry.class))
                         .thenAsync(new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
                             @Override
                             public Promise<ResourceResponse, ResourceException> apply(SearchResultEntry entry) {
                                 final Resource subType = resource.resolveSubTypeFromObjectClasses(entry);
                                 return encodeResourceResponse(connection, subType, entry);
                             }
                         });
    }
    @Override
    public Promise<ResourceResponse, ResourceException> updateInstance(
    Promise<ResourceResponse, ResourceException> update(
            final Context context, final String resourceId, final UpdateRequest request) {
        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
        List<JsonPointer> attrs = Collections.emptyList();
        SearchRequest searchRequest = searchRequest(connection, resourceId, attrs);
        final Connection connection = connectionFrom(context);
        final AtomicReference<Entry> entryHolder = new AtomicReference<>();
        final AtomicReference<Resource> subTypeHolder = new AtomicReference<>();
        return connection
                .searchSingleEntryAsync(searchRequest)
                .thenAsync(new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
                .searchSingleEntryAsync(searchRequestForUnknownType(resourceId, Collections.<JsonPointer>emptyList()))
                .thenCatchAsync(adaptLdapException(SearchResultEntry.class))
                .thenAsync(new AsyncFunction<SearchResultEntry, List<Modification>, ResourceException>() {
                    @Override
                    public Promise<ResourceResponse, ResourceException> apply(
                            final SearchResultEntry entry) {
                        try {
                            // Fail-fast if there is a version mismatch.
                            ensureMvccVersionMatches(entry, request.getRevision());
                    public Promise<List<Modification>, ResourceException> apply(final SearchResultEntry entry)
                            throws ResourceException {
                        entryHolder.set(entry);
                            // Create the modify request.
                            final ModifyRequest modifyRequest = newModifyRequest(entry.getName());
                            if (config.readOnUpdatePolicy() == CONTROLS) {
                                final String[] attributes =
                                        getLdapAttributes(connection, request.getFields());
                                modifyRequest.addControl(
                                        PostReadRequestControl.newControl(false, attributes));
                            }
                            if (config.usePermissiveModify()) {
                                modifyRequest.addControl(
                                        PermissiveModifyRequestControl.newControl(true));
                            }
                            addAssertionControl(modifyRequest, request.getRevision());
                        // Fail-fast if there is a version mismatch.
                        ensureMvccVersionMatches(entry, request.getRevision());
                            // Determine the set of changes that need to be performed.
                            return propertyMapper.update(
                                    connection, new JsonPointer(), entry, request.getContent())
                                                 .thenAsync(new AsyncFunction<
                                            List<Modification>, ResourceResponse, ResourceException>() {
                                        @Override
                                        public Promise<ResourceResponse, ResourceException> apply(
                                                List<Modification> modifications)
                                                throws ResourceException {
                                            if (modifications.isEmpty()) {
                                                // No changes to be performed so just return
                                                // the entry that we read.
                                                return adaptEntry(connection, entry);
                                            }
                                            // Perform the modify operation.
                                            modifyRequest.getModifications().addAll(modifications);
                                            return connection
                                                    .applyChangeAsync(modifyRequest)
                                                    .thenAsync(
                                                            postUpdateResultAsyncFunction(connection),
                                                            Exceptions.<ResourceResponse>toResourceException());
                                        }
                                    });
                        } catch (final Exception e) {
                            return Promises.newExceptionPromise(asResourceException(e));
                        }
                        // Determine the type of resource and set of changes that need to be performed.
                        final Resource subType = resource.resolveSubTypeFromObjectClasses(entry);
                        subTypeHolder.set(subType);
                        final PropertyMapper propertyMapper = subType.getPropertyMapper();
                        return propertyMapper.update(connection, subType , ROOT, entry, request.getContent());
                    }
                }, Exceptions.<ResourceResponse>toResourceException());
                }).thenAsync(new AsyncFunction<List<Modification>, ResourceResponse, ResourceException>() {
                    @Override
                    public Promise<ResourceResponse, ResourceException> apply(List<Modification> modifications)
                            throws ResourceException {
                        final Resource subType = subTypeHolder.get();
                        if (modifications.isEmpty()) {
                            // No changes to be performed so just return the entry that we read.
                            return encodeResourceResponse(connection, subType, entryHolder.get());
                        }
                        // Perform the modify operation.
                        final ModifyRequest modifyRequest = newModifyRequest(entryHolder.get().getName());
                        if (readOnUpdatePolicy == CONTROLS) {
                            final Set<String> attributes = getLdapAttributesForKnownType(request.getFields(), subType);
                            modifyRequest.addControl(PostReadRequestControl.newControl(false, attributes));
                        }
                        if (usePermissiveModify) {
                            modifyRequest.addControl(PermissiveModifyRequestControl.newControl(true));
                        }
                        addAssertionControl(modifyRequest, request.getRevision());
                        modifyRequest.getModifications().addAll(modifications);
                        return connection.applyChangeAsync(modifyRequest)
                                         .thenAsync(encodeUpdateResourceResponse(connection, subType),
                                                    adaptLdapException(ResourceResponse.class));
                    }
                });
    }
    private Promise<ResourceResponse, ResourceException> adaptEntry(final Connection connection, final Entry entry) {
        final String actualResourceId = namingStrategy.getResourceId(connection, entry);
        final String revision = getRevisionFromEntry(entry);
        return propertyMapper.read(connection, new JsonPointer(), entry)
    private Promise<ResourceResponse, ResourceException> encodeResourceResponse(
            final Connection connection, final Resource resource, final Entry entry) {
        final PropertyMapper propertyMapper = resource.getPropertyMapper();
        return propertyMapper.read(connection, resource, ROOT, entry)
                             .then(new Function<JsonValue, ResourceResponse, ResourceException>() {
                                  @Override
                                  public ResourceResponse apply(final JsonValue value) {
                                      return newResourceResponse(
                                              actualResourceId, revision, new JsonValue(value));
                                  }
                                 @Override
                                 public ResourceResponse apply(final JsonValue value) {
                                     final String revision = getRevisionFromEntry(entry);
                                     final String actualResourceId = namingStrategy.decodeResourceId(entry);
                                     return newResourceResponse(actualResourceId, revision, new JsonValue(value));
                                 }
                             });
    }
@@ -819,40 +895,34 @@
            throws ResourceException {
        if (expectedRevision != null) {
            ensureMvccSupported();
            request.addControl(AssertionRequestControl.newControl(true, Filter.equality(
                    etagAttribute.toString(), expectedRevision)));
            final Filter filter = Filter.equality(etagAttribute.toString(), expectedRevision);
            request.addControl(AssertionRequestControl.newControl(true, filter));
        }
    }
    private Promise<DN, ResourceException> doUpdateFunction(final Connection connection, final String resourceId,
            final String revision) {
        final String ldapAttribute = (etagAttribute != null && revision != null) ? etagAttribute.toString() : "1.1";
        final SearchRequest searchRequest = namingStrategy.createSearchRequest(connection, getBaseDn(), resourceId)
                                                          .addAttribute(ldapAttribute);
        if (searchRequest.getScope().equals(SearchScope.BASE_OBJECT)) {
            // There's no point in doing a search because we already know the DN.
            return Promises.newResultPromise(searchRequest.getName());
    private Promise<RoutingContext, ResourceException> resolveResourceDnAndType(
            final Context context, final Connection connection, final String resourceId, final String revision) {
        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 connection
                .searchSingleEntryAsync(searchRequest)
                .thenAsync(new AsyncFunction<SearchResultEntry, DN, ResourceException>() {
                    @Override
                    public Promise<DN, ResourceException> apply(SearchResultEntry entry) throws ResourceException {
                        try {
                            // Fail-fast if there is a version mismatch.
                            ensureMvccVersionMatches(entry, revision);
                            // Perform update operation.
                            return Promises.newResultPromise(entry.getName());
                        } catch (final Exception e) {
                            return Promises.newExceptionPromise(asResourceException(e));
                        }
                    }
                }, new AsyncFunction<LdapException, DN, ResourceException>() {
                    @Override
                    public Promise<DN, ResourceException> apply(LdapException ldapException) throws ResourceException {
                        return Promises.newExceptionPromise(asResourceException(ldapException));
                    }
                });
        if (etagAttribute != null && revision != null) {
            searchRequest.addAttribute(etagAttribute.toString());
        }
        // The resource type will be resolved from the LDAP entry's objectClass.
        searchRequest.addAttribute("objectClass");
        return connection.searchSingleEntryAsync(searchRequest)
                         .thenAsync(new AsyncFunction<SearchResultEntry, RoutingContext, ResourceException>() {
                             @Override
                             public Promise<RoutingContext, ResourceException> apply(final SearchResultEntry entry)
                                     throws ResourceException {
                                 // 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));
                             }
                         }, adaptLdapException(RoutingContext.class));
    }
    private void ensureMvccSupported() throws NotSupportedException {
@@ -874,101 +944,92 @@
        }
    }
    private DN getBaseDn() {
        return baseDn;
    private Set<String> getLdapAttributesForUnknownType(final Collection<JsonPointer> fields) {
        final Set<String> ldapAttributes = getLdapAttributesForKnownType(fields, resource);
        getLdapAttributesForUnknownType(fields, resource, ldapAttributes);
        return ldapAttributes;
    }
    /**
     * Determines the set of LDAP attributes to request in an LDAP read (search,
     * post-read), based on the provided list of JSON pointers.
     *
     * @param connection
     *          The request state.
     * @param requestedAttributes
     *          The list of resource attributes to be read.
     * @return The set of LDAP attributes associated with the resource
     *         attributes.
     */
    private String[] getLdapAttributes(final Connection connection, final Collection<JsonPointer> requestedAttributes) {
        // Get all the LDAP attributes required by the property mappers.
        final Set<String> requestedLDAPAttributes;
        if (requestedAttributes.isEmpty()) {
    private void getLdapAttributesForUnknownType(final Collection<JsonPointer> fields, final Resource resource,
                                                 final Set<String> ldapAttributes) {
        for (final Resource subType : resource.getSubTypes()) {
            addLdapAttributesForFields(fields, subType, ldapAttributes);
            getLdapAttributesForUnknownType(fields, subType, ldapAttributes);
        }
    }
    private Set<String> getLdapAttributesForKnownType(final Collection<JsonPointer> fields, final Resource resource) {
        // Includes the LDAP attributes required by the type, etag, and name strategies.
        final Set<String> ldapAttributes = new LinkedHashSet<>();
        ldapAttributes.add("objectClass");
        final String resourceIdLdapAttribute = namingStrategy.getResourceIdLdapAttribute();
        if (resourceIdLdapAttribute != null) {
            ldapAttributes.add(resourceIdLdapAttribute);
        }
        if (etagAttribute != null) {
            ldapAttributes.add(etagAttribute.toString());
        }
        addLdapAttributesForFields(fields, resource, ldapAttributes);
        return ldapAttributes;
    }
    /** Includes the LDAP attributes required for the specified JSON fields for all sub-types. */
    private void addLdapAttributesForFields(final Collection<JsonPointer> fields, final Resource resource,
                                            final Set<String> ldapAttributes) {
        final PropertyMapper propertyMapper = resource.getPropertyMapper();
        if (fields.isEmpty()) {
            // Full read.
            requestedLDAPAttributes = new LinkedHashSet<>();
            propertyMapper.getLdapAttributes(connection, new JsonPointer(), new JsonPointer(),
                                             requestedLDAPAttributes);
            propertyMapper.getLdapAttributes(ROOT, ROOT, ldapAttributes);
        } else {
            // Partial read.
            requestedLDAPAttributes = new LinkedHashSet<>(requestedAttributes.size());
            for (final JsonPointer requestedAttribute : requestedAttributes) {
                propertyMapper.getLdapAttributes(connection, new JsonPointer(), requestedAttribute,
                                                 requestedLDAPAttributes);
            for (final JsonPointer field : fields) {
                propertyMapper.getLdapAttributes(ROOT, field, ldapAttributes);
            }
        }
        // Get the LDAP attributes required by the Etag and name stategies.
        namingStrategy.getLdapAttributes(connection, requestedLDAPAttributes);
        if (etagAttribute != null) {
            requestedLDAPAttributes.add(etagAttribute.toString());
        }
        return requestedLDAPAttributes.toArray(new String[requestedLDAPAttributes.size()]);
    }
    private String getRevisionFromEntry(final Entry entry) {
        return etagAttribute != null ? entry.parseAttribute(etagAttribute).asString() : null;
    }
    private AsyncFunction<Result, ResourceResponse, ResourceException> postUpdateResultAsyncFunction(
            final Connection connection) {
        // The handler which will be invoked for the LDAP add result.
    private AsyncFunction<Result, ResourceResponse, ResourceException> encodeUpdateResourceResponse(
            final Connection connection, final Resource resource) {
        return new AsyncFunction<Result, ResourceResponse, ResourceException>() {
            @Override
            public Promise<ResourceResponse, ResourceException> apply(Result result) throws ResourceException {
            public Promise<ResourceResponse, ResourceException> apply(Result result) {
                // FIXME: handle USE_SEARCH policy.
                Entry entry;
                try {
                    final PostReadResponseControl postReadControl =
                        result.getControl(PostReadResponseControl.DECODER, config.decodeOptions());
                            result.getControl(PostReadResponseControl.DECODER, decodeOptions);
                    if (postReadControl != null) {
                        entry = postReadControl.getEntry();
                    } else {
                        final PreReadResponseControl preReadControl =
                            result.getControl(PreReadResponseControl.DECODER, config.decodeOptions());
                        if (preReadControl != null) {
                            entry = preReadControl.getEntry();
                        } else {
                            entry = null;
                        }
                        return encodeResourceResponse(connection, resource, postReadControl.getEntry());
                    }
                    final PreReadResponseControl preReadControl =
                            result.getControl(PreReadResponseControl.DECODER, decodeOptions);
                    if (preReadControl != null) {
                        return encodeResourceResponse(connection, resource, preReadControl.getEntry());
                    }
                } catch (final DecodeException e) {
                    logger.error(ERR_DECODING_CONTROL.get(e.getLocalizedMessage()), e);
                    entry = null;
                }
                if (entry != null) {
                    return adaptEntry(connection, entry);
                } else {
                    return Promises.newResultPromise(
                            newResourceResponse(null, null, new JsonValue(Collections.emptyMap())));
                }
                // Return an empty resource response.
                return newResourceResponse(null, null, new JsonValue(Collections.emptyMap())).asPromise();
            }
        };
    }
    private SearchRequest searchRequest(
            final Connection connection, final String resourceId, final List<JsonPointer> requestedAttributes) {
        final String[] attributes = getLdapAttributes(connection, requestedAttributes);
        return namingStrategy.createSearchRequest(connection, getBaseDn(), resourceId).addAttribute(attributes);
    private SearchRequest searchRequestForUnknownType(final String resourceId, final List<JsonPointer> fields) {
        final String[] attributes = getLdapAttributesForUnknownType(fields).toArray(new String[0]);
        return namingStrategy.createSearchRequest(baseDn, resourceId).addAttribute(attributes);
    }
    private static final class Exceptions {
        private static <R> AsyncFunction<LdapException, R, ResourceException> toResourceException() {
            // The handler which will be invoked for the LDAP add result.
            return new AsyncFunction<LdapException, R, ResourceException>() {
                @Override
                public Promise<R, ResourceException> apply(final LdapException ldapException) throws ResourceException {
                    return Promises.newExceptionPromise(asResourceException(ldapException));
                }
            };
        }
    @SuppressWarnings("unused")
    private static <R> AsyncFunction<LdapException, R, ResourceException> adaptLdapException(final Class<R> clazz) {
        return new AsyncFunction<LdapException, R, ResourceException>() {
            @Override
            public Promise<R, ResourceException> apply(final LdapException ldapException) {
                return asResourceException(ldapException).asPromise();
            }
        };
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
New file
@@ -0,0 +1,292 @@
/*
 * 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 2016 ForgeRock AS.
 *
 */
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.http.routing.RoutingMode.EQUALS;
import static org.forgerock.http.routing.RoutingMode.STARTS_WITH;
import static org.forgerock.json.resource.RouteMatchers.requestUriMatcher;
import static org.forgerock.opendj.ldap.Filter.objectClassPresent;
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.util.promise.Promises.newResultPromise;
import org.forgerock.json.resource.ActionRequest;
import org.forgerock.json.resource.ActionResponse;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.CreateRequest;
import org.forgerock.json.resource.DeleteRequest;
import org.forgerock.json.resource.PatchRequest;
import org.forgerock.json.resource.QueryRequest;
import org.forgerock.json.resource.QueryResourceHandler;
import org.forgerock.json.resource.QueryResponse;
import org.forgerock.json.resource.ReadRequest;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.Router;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.services.context.Context;
import org.forgerock.util.AsyncFunction;
import org.forgerock.util.Function;
import org.forgerock.util.promise.Promise;
/**
 * Represents a one to one relationship between a parent resource and a child sub-resource. Removal of the parent
 * resource implies that the child (the sub-resource) is also removed. Singletons only support read, update, patch, and
 * action requests.
 */
public final class SubResourceSingleton extends SubResource {
    /**
     * A simple naming strategy that allows singletons to use the same processing logic as collections. The passed in
     * resource ID will always be {@code null}.
     */
    private static final NamingStrategy SINGLETON_NAMING_STRATEGY = new NamingStrategy() {
        @Override
        public SearchRequest createSearchRequest(final DN baseDn, final String resourceId) {
            return newSearchRequest(baseDn, BASE_OBJECT, objectClassPresent());
        }
        @Override
        public String getResourceIdLdapAttribute() {
            // Nothing to do.
            return null;
        }
        @Override
        public String decodeResourceId(final Entry entry) {
            // It's safe to return null. The resource response will default to the _id field if present.
            return null;
        }
        @Override
        public void encodeResourceId(final DN baseDn, final String resourceId, final Entry entry)
                throws ResourceException {
            // Nothing to do because singletons cannot be created.
        }
    };
    SubResourceSingleton(final String resourceId) {
        super(resourceId);
    }
    /**
     * Sets the relative URL template of the single sub-resource. The template must comprise of at least one path
     * element. Any URL template variables will be substituted into the {@link #dnTemplate(String) DN template}.
     *
     * @param urlTemplate
     *         The relative URL template.
     * @return A reference to this object.
     */
    public SubResourceSingleton urlTemplate(final String urlTemplate) {
        this.urlTemplate = urlTemplate;
        return this;
    }
    /**
     * Sets the relative DN template of the single sub-resource LDAP entry. The template must comprise of at least one
     * RDN. Any DN template variables will be substituted using values extracted from the {@link #urlTemplate(String)
     * URL template}.
     *
     * @param dnTemplate
     *         The relative DN template.
     * @return A reference to this object.
     */
    public SubResourceSingleton dnTemplate(final String dnTemplate) {
        this.dnTemplate = dnTemplate;
        return this;
    }
    /**
     * Indicates whether this sub-resource singleton only supports read operations.
     *
     * @param readOnly
     *         {@code true} if this sub-resource singleton is read-only.
     * @return A reference to this object.
     */
    public SubResourceSingleton isReadOnly(final boolean readOnly) {
        isReadOnly = readOnly;
        return this;
    }
    @Override
    Router addRoutes(final Router router) {
        router.addRoute(requestUriMatcher(EQUALS, urlTemplate), readOnly(new InstanceHandler()));
        router.addRoute(requestUriMatcher(STARTS_WITH, urlTemplate), readOnly(new SubResourceHandler()));
        return router;
    }
    private Promise<RoutingContext, ResourceException> route(final Context context) {
        return newResultPromise(new RoutingContext(context, dnFrom(context), resource));
    }
    private SubResourceImpl singleton(final Context context) {
        return new SubResourceImpl(rest2Ldap, dnFrom(context), null, SINGLETON_NAMING_STRATEGY, resource);
    }
    /**
     * Responsible for processing instance requests (RUPA) against this singleton and collection requests (CQ) to
     * any collections sharing the same base URL as this singleton. More specifically, given the
     * URL template /singleton/{child} then this handler processes requests against /singleton since it is
     * both a singleton and also a collection of {child}.
     */
    private final class InstanceHandler extends AbstractRequestHandler {
        private InstanceHandler() {
            super(new BadRequestException(ERR_UNSUPPORTED_REQUEST_AGAINST_SINGLETON.get().toString()));
        }
        @Override
        public Promise<ActionResponse, ResourceException> handleAction(final Context context,
                                                                       final ActionRequest request) {
            return singleton(context).action(context, null, request);
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handleCreate(final Context context,
                                                                         final CreateRequest request) {
            return route(context)
                    .thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
                        @Override
                        public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) {
                            return subResourceRouterFrom(context).handleCreate(context, request);
                        }
                    }).thenCatch(this.<ResourceResponse>convert404To400());
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handlePatch(final Context context,
                                                                        final PatchRequest request) {
            return singleton(context).patch(context, null, request);
        }
        @Override
        public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request,
                                                                     final QueryResourceHandler handler) {
            return route(context)
                    .thenAsync(new AsyncFunction<RoutingContext, QueryResponse, ResourceException>() {
                        @Override
                        public Promise<QueryResponse, ResourceException> apply(final RoutingContext context) {
                            return subResourceRouterFrom(context).handleQuery(context, request, handler);
                        }
                    }).thenCatch(this.<QueryResponse>convert404To400());
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handleRead(final Context context,
                                                                       final ReadRequest request) {
            return singleton(context).read(context, null, request);
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handleUpdate(final Context context,
                                                                         final UpdateRequest request) {
            return singleton(context).update(context, null, request);
        }
        private <T> Function<ResourceException, T, ResourceException> convert404To400() {
            return SubResource.convert404To400(ERR_UNSUPPORTED_REQUEST_AGAINST_SINGLETON.get());
        }
    }
    /**
     * Responsible for routing requests to sub-resources of this singleton. More specifically, given
     * the URL template /singleton then this handler processes all requests beneath /singleton.
     */
    private final class SubResourceHandler implements RequestHandler {
        @Override
        public Promise<ActionResponse, ResourceException> handleAction(final Context context,
                                                                       final ActionRequest request) {
            return route(context).thenAsync(new AsyncFunction<RoutingContext, ActionResponse, ResourceException>() {
                @Override
                public Promise<ActionResponse, ResourceException> apply(final RoutingContext context) {
                    return subResourceRouterFrom(context).handleAction(context, request);
                }
            });
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handleCreate(final Context context,
                                                                         final CreateRequest request) {
            return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
                @Override
                public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) {
                    return subResourceRouterFrom(context).handleCreate(context, request);
                }
            });
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handleDelete(final Context context,
                                                                         final DeleteRequest request) {
            return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
                @Override
                public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) {
                    return subResourceRouterFrom(context).handleDelete(context, request);
                }
            });
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handlePatch(final Context context,
                                                                        final PatchRequest request) {
            return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
                @Override
                public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) {
                    return subResourceRouterFrom(context).handlePatch(context, request);
                }
            });
        }
        @Override
        public Promise<QueryResponse, ResourceException> handleQuery(final Context context, final QueryRequest request,
                                                                     final QueryResourceHandler handler) {
            return route(context).thenAsync(new AsyncFunction<RoutingContext, QueryResponse, ResourceException>() {
                @Override
                public Promise<QueryResponse, ResourceException> apply(final RoutingContext context) {
                    return subResourceRouterFrom(context).handleQuery(context, request, handler);
                }
            });
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handleRead(final Context context,
                                                                       final ReadRequest request) {
            return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
                @Override
                public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) {
                    return subResourceRouterFrom(context).handleRead(context, request);
                }
            });
        }
        @Override
        public Promise<ResourceResponse, ResourceException> handleUpdate(final Context context,
                                                                         final UpdateRequest request) {
            return route(context).thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
                @Override
                public Promise<ResourceResponse, ResourceException> apply(final RoutingContext context) {
                    return subResourceRouterFrom(context).handleUpdate(context, request);
                }
            });
        }
    }
}
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
@@ -27,11 +27,6 @@
import static org.forgerock.opendj.ldap.schema.CoreSchema.getIntegerSyntax;
import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_JSON_VALUE;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Locale;
@@ -73,12 +68,6 @@
                }
            };
    static String readPasswordFromFile(String fileName) throws IOException {
        try (final BufferedReader reader = new BufferedReader(new FileReader(new File(fileName)))) {
            return reader.readLine();
        }
    }
    static Function<Object, ByteString, NeverThrowsException> base64ToByteString() {
        return BASE64_TO_BYTESTRING;
    }
@@ -106,19 +95,6 @@
        };
    }
    /**
     * Stub formatter for i18n strings.
     *
     * @param format
     *            The format string.
     * @param args
     *            The string arguments.
     * @return The formatted string.
     */
    static String i18n(final String format, final Object... args) {
        return String.format(format, args);
    }
    private static boolean isJsonPrimitive(final Object value) {
        return value instanceof String || value instanceof Boolean || value instanceof Number;
    }
@@ -138,7 +114,7 @@
            }
            return a;
        } else {
            throw newLocalizedIllegalArgumentException(ERR_UNRECOGNIZED_JSON_VALUE.get(value.getClass().getName()));
            throw new LocalizedIllegalArgumentException(ERR_UNRECOGNIZED_JSON_VALUE.get(value.getClass().getName()));
        }
    }
@@ -154,8 +130,8 @@
                        return ByteString.valueOfObject(value);
                    }
                } else {
                    throw newLocalizedIllegalArgumentException(
                            ERR_UNRECOGNIZED_JSON_VALUE.get(value.getClass().getName()));
                    throw new LocalizedIllegalArgumentException(ERR_UNRECOGNIZED_JSON_VALUE.get(value.getClass()
                                                                                                     .getName()));
                }
            }
        };
@@ -201,10 +177,6 @@
        return new JsonValueException(value, message.toString());
    }
    static LocalizedIllegalArgumentException newLocalizedIllegalArgumentException(final LocalizableMessage message) {
        return new LocalizedIllegalArgumentException(message);
    }
    static BadRequestException newBadRequestException(final LocalizableMessage message) {
        return newBadRequestException(message, null);
    }
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Utils.java
@@ -33,6 +33,13 @@
import org.forgerock.util.promise.Promises;
final class Utils {
    private static final AsyncFunction<LdapException, Response, NeverThrowsException> HANDLE_CONNECTION_FAILURE =
            new AsyncFunction<LdapException, Response, NeverThrowsException>() {
                @Override
                public Promise<Response, NeverThrowsException> apply(final LdapException exception) {
                    return asErrorResponse(exception);
                }
            };
    private Utils() { }
@@ -58,19 +65,13 @@
    }
    static AsyncFunction<LdapException, Response, NeverThrowsException> handleConnectionFailure() {
        return new AsyncFunction<LdapException, Response, NeverThrowsException>() {
            @Override
            public Promise<Response, NeverThrowsException> apply(final LdapException exception) {
                return asErrorResponse(exception);
            }
        };
        return HANDLE_CONNECTION_FAILURE;
    }
    static Promise<Response, NeverThrowsException> asErrorResponse(final Throwable t) {
        final ResourceException e = asResourceException(t);
        final Response response = new Response()
                .setStatus(Status.valueOf(e.getCode()))
                .setEntity(e.toJsonValue().getObject());
        final Response response = new Response().setStatus(Status.valueOf(e.getCode()))
                                                .setEntity(e.toJsonValue() .getObject());
        if (response.getStatus() == Status.UNAUTHORIZED) {
            response.getHeaders().put("WWW-Authenticate", "Basic");
        }
opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties
@@ -97,7 +97,7 @@
ERR_ENCODING_VALUES_FOR_FIELD_52=The request cannot be processed because an error occurred while encoding \
 the values for the field '%s': '%s'
ERR_UNRECOGNIZED_JSON_VALUE_53=Unrecognized type of JSON value: '%s'
ERR_CLIENT_PROVIDER_RESOURCE_ID_MISSING_54=Resources cannot be created without a client provided resource ID
ERR_CLIENT_PROVIDED_RESOURCE_ID_MISSING_54=Resources cannot be created without a client provided resource ID
ERR_NOT_YET_IMPLEMENTED_55=Not yet implemented
ERR_ACTION_NOT_SUPPORTED_56=The action '%s' is not supported
ERR_PASSWORD_MODIFY_SECURE_CONNECTION_57=Password modify requires a secure connection
@@ -109,3 +109,30 @@
 A password modify request may contain two string valued fields 'oldPassword' and 'newPassword'
ERR_CONFIG_INVALID_TRUST_MANAGER_63=The trust-manager defined in '%s' is invalid: %s
ERR_CONFIG_INVALID_KEY_MANAGER_64=The key-manager defined in '%s' is invalid: %s
ERR_MISSING_TYPE_PROPERTY_IN_CREATE_65=The resource cannot be created because it does not contain the \
  type property '%s'
ERR_UNRECOGNIZED_TYPE_IN_CREATE_66=The resource cannot be created because it specified an unrecognized resource \
  type '%s'. Please specify one of the following types: %s
ERR_ABSTRACT_TYPE_IN_CREATE_67=The resource cannot be created because it specified the abstract resource type '%s'. \
  Please specify one of the following non-abstract types: %s
ERR_QUERY_BY_ID_OR_EXPRESSION_NOT_SUPPORTED_68=Queries using _queryId or _queryExpression are not supported. Use \
  _queryFilter instead
ERR_READ_ONLY_ENDPOINT_69=This endpoint is read-only and only supports read and query requests
ERR_UNSUPPORTED_REQUEST_AGAINST_COLLECTION_70=The targeted resource is a resource collection which only \
  supports create and query requests
ERR_UNSUPPORTED_REQUEST_AGAINST_INSTANCE_71=The targeted resource is a resource instance which only supports \
  read, update, delete, patch, and action requests
ERR_UNSUPPORTED_REQUEST_AGAINST_SINGLETON_72=The targeted resource is a resource singleton which only supports \
  read, update, patch, and action requests
ERR_SERVER_PROVIDED_RESOURCE_ID_UNEXPECTED_73=Resources cannot be created with a client provided resource ID. The \
  server will automatically generate a resource ID
ERR_COLLECTION_ACTIONS_NOT_SUPPORTED_74=Collections only support create or query requests
ERR_UNRECOGNIZED_RESOURCE_SUPER_TYPE_75=The resource '%s' has an unrecognized super-type '%s'
ERR_UNRECOGNIZED_SUB_RESOURCE_TYPE_76=The resource '%s' references an unrecognized sub-resource '%s'
ERR_MISSING_REQUIRED_FIELD_77=The create request cannot be processed because it does not include the required field '%s'
ERR_INVALID_ENDPOINTS_DIRECTORY_78=The endpoints configuration directory '%s' either does not exist, is not a \
  directory or cannot be read
ERR_INVALID_ENDPOINT_DIRECTORY_79=The endpoint configuration directory '%s' either does not exist, is not a \
  directory or cannot be read
INFO_REST2LDAP_STARTING_80=Rest2Ldap starting with configuration directory '%s'
INFO_REST2LDAP_CREATING_ENDPOINT_81=Rest2Ldap created endpoint '%s' version %s
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
@@ -24,21 +24,22 @@
import static org.forgerock.json.resource.PatchOperation.increment;
import static org.forgerock.json.resource.PatchOperation.remove;
import static org.forgerock.json.resource.PatchOperation.replace;
import static org.forgerock.json.resource.Requests.newDeleteRequest;
import static org.forgerock.json.resource.Requests.newPatchRequest;
import static org.forgerock.json.resource.Requests.newQueryRequest;
import static org.forgerock.json.resource.Requests.newReadRequest;
import static org.forgerock.json.resource.Requests.newUpdateRequest;
import static org.forgerock.json.resource.Resources.newCollection;
import static org.forgerock.json.resource.Requests.*;
import static org.forgerock.json.resource.Resources.newInternalConnection;
import static org.forgerock.opendj.ldap.Connections.newInternalConnectionFactory;
import static org.forgerock.opendj.ldap.Functions.byteStringToInteger;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.collectionOf;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.constant;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.rest2Ldap;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.object;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.resource;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.simple;
import static org.forgerock.opendj.rest2ldap.TestUtils.asResource;
import static org.forgerock.opendj.rest2ldap.TestUtils.content;
import static org.forgerock.opendj.rest2ldap.TestUtils.ctx;
import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.CREATE_ONLY;
import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_ONLY;
import static org.forgerock.util.Options.defaultOptions;
import java.io.IOException;
import java.util.ArrayList;
@@ -56,7 +57,6 @@
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.IntermediateResponseHandler;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.LdapResultHandler;
import org.forgerock.opendj.ldap.MemoryBackend;
import org.forgerock.opendj.ldap.RequestContext;
@@ -76,7 +76,6 @@
import org.forgerock.opendj.ldap.responses.ExtendedResult;
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.ldif.LDIFEntryReader;
import org.forgerock.opendj.rest2ldap.Rest2Ldap.Builder;
import org.forgerock.services.context.Context;
import org.forgerock.testng.ForgeRockTestCase;
import org.forgerock.util.query.QueryFilter;
@@ -208,7 +207,7 @@
    public void testPatchEmpty() throws Exception {
        final List<Request> requests = new LinkedList<>();
        final Context context = newAuthConnectionContext(requests);
        final Connection connection = newConnection(requests);
        final Connection connection = newConnection();
        final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1"));
        checkResourcesAreEqual(resource1, getTestUser1(12345));
@@ -486,7 +485,7 @@
    @Test
    public void testUpdateNoChange() throws Exception {
        final List<Request> requests = new LinkedList<>();
        final Connection connection = newConnection(requests);
        final Connection connection = newConnection();
        final Context context = newAuthConnectionContext(requests);
        final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", getTestUser1(12345)));
@@ -604,51 +603,40 @@
    }
    private Connection newConnection() throws IOException {
        return newConnection(new LinkedList<Request>());
        return newInternalConnection(usersApi().newRequestHandlerFor("api"));
    }
    private Connection newConnection(final List<Request> requests) throws IOException {
        return newInternalConnection(newCollection(builder(requests).build()));
    }
    private Builder builder(final List<Request> requests) throws IOException {
        return Rest2Ldap.builder()
                        .baseDN("dc=test")
                        .useEtagAttribute()
                        .useClientDNNaming("uid")
                        .readOnUpdatePolicy(ReadOnUpdatePolicy.CONTROLS)
                        .additionalLDAPAttribute("objectClass", "top", "person")
                        .mapper(object()
                        .attribute("schemas", constant(asList("urn:scim:schemas:core:1.0")))
                        .attribute("_id", simple("uid").isSingleValued()
                                                       .isRequired()
                                                       .writability(WritabilityPolicy.CREATE_ONLY))
                        .attribute("name", object().attribute("displayName", simple("cn").isSingleValued()
                                                                                         .isRequired())
                                                    .attribute("surname", simple("sn").isSingleValued().isRequired()))
                        .attribute("_rev", simple("etag").isSingleValued()
                                                         .isRequired()
                                                         .writability(WritabilityPolicy.READ_ONLY))
                        .attribute("description", simple("description"))
                        .attribute("singleNumber", simple("singleNumber").decoder(byteStringToInteger())
                                                                         .isSingleValued())
                        .attribute("multiNumber", simple("multiNumber").decoder(byteStringToInteger())));
    private Rest2Ldap usersApi() throws IOException {
        return rest2Ldap(defaultOptions(),
                         resource("api").subResource(collectionOf("user").dnTemplate("dc=test")
                                                                         .useClientDnNaming("uid")),
                         resource("user").objectClasses("top", "person")
                                         .property("schemas", constant(asList("urn:scim:schemas:core:1.0")))
                                         .property("_id", simple("uid").isRequired(true).writability(CREATE_ONLY))
                                         .property("name", object().property("displayName",
                                                                             simple("cn").isRequired(true))
                                                                   .property("surname", simple("sn").isRequired(true)))
                                         .property("_rev", simple("etag").isRequired(true).writability(READ_ONLY))
                                         .property("description", simple("description").isMultiValued(true))
                                         .property("singleNumber",
                                                   simple("singleNumber").decoder(byteStringToInteger()))
                                         .property("multiNumber",
                                                   simple("multiNumber").isMultiValued(true)
                                                                        .decoder(byteStringToInteger())));
    }
    private void checkResourcesAreEqual(final ResourceResponse actual, final JsonValue expected) {
        final ResourceResponse expectedResource = asResource(expected);
        assertThat(actual.getId()).isEqualTo(expectedResource.getId());
        assertThat(actual.getRevision()).isEqualTo(expectedResource.getRevision());
        assertThat(actual.getContent().getObject()).isEqualTo(
                expectedResource.getContent().getObject());
        assertThat(actual.getContent().getObject()).isEqualTo(expectedResource.getContent().getObject());
    }
    private AuthenticatedConnectionContext newAuthConnectionContext() throws LdapException, IOException {
    private AuthenticatedConnectionContext newAuthConnectionContext() throws IOException {
        return newAuthConnectionContext(new ArrayList<Request>());
    }
    private AuthenticatedConnectionContext newAuthConnectionContext(List<Request> requests)
            throws LdapException, IOException {
    private AuthenticatedConnectionContext newAuthConnectionContext(List<Request> requests) throws IOException {
        return new AuthenticatedConnectionContext(ctx(), getConnectionFactory(requests).getConnection());
    }
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapTest.java
New file
@@ -0,0 +1,465 @@
/*
 * 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 2016 ForgeRock AS.
 *
 */
package org.forgerock.opendj.rest2ldap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.forgerock.json.JsonValue.array;
import static org.forgerock.json.JsonValue.json;
import static org.forgerock.json.resource.PatchOperation.replace;
import static org.forgerock.json.resource.Requests.*;
import static org.forgerock.json.resource.Responses.newResourceResponse;
import static org.forgerock.opendj.ldap.Connections.newInternalConnection;
import static org.forgerock.opendj.rest2ldap.Rest2Ldap.*;
import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.CREATE_ONLY;
import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_ONLY;
import static org.forgerock.util.Options.defaultOptions;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.CreateRequest;
import org.forgerock.json.resource.DeleteRequest;
import org.forgerock.json.resource.NotFoundException;
import org.forgerock.json.resource.PatchRequest;
import org.forgerock.json.resource.QueryRequest;
import org.forgerock.json.resource.QueryResourceHandler;
import org.forgerock.json.resource.QueryResponse;
import org.forgerock.json.resource.ReadRequest;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.Requests;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.IntermediateResponseHandler;
import org.forgerock.opendj.ldap.LdapResultHandler;
import org.forgerock.opendj.ldap.MemoryBackend;
import org.forgerock.opendj.ldap.RequestContext;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.requests.AddRequest;
import org.forgerock.opendj.ldap.requests.BindRequest;
import org.forgerock.opendj.ldap.requests.CompareRequest;
import org.forgerock.opendj.ldap.requests.ExtendedRequest;
import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.responses.BindResult;
import org.forgerock.opendj.ldap.responses.CompareResult;
import org.forgerock.opendj.ldap.responses.ExtendedResult;
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.ldif.EntryReader;
import org.forgerock.opendj.ldif.LDIFEntryReader;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.RootContext;
import org.forgerock.testng.ForgeRockTestCase;
import org.forgerock.util.query.QueryFilter;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@SuppressWarnings({ "javadoc" })
@Test
public final class Rest2LdapTest extends ForgeRockTestCase {
    // TODO: unit test for DN template variables
    // TODO: unit test for nested sub-resources
    // TODO: unit test for singletons
    // TODO: unit test for read-only
    private enum UseCase {
        CLIENT_ID_PRIMARY_VIEW {
            @Override
            RequestHandler handler() {
                return rest2Ldap(defaultOptions(),
                                 top(),
                                 userUidResource(),
                                 resource("test")
                                         .subResources(collectionOf("userUid").useClientDnNaming("uid")
                                                                              .dnTemplate("dc=test")
                                                                              .urlTemplate("users")))
                        .newRequestHandlerFor("test");
            }
            @Override
            CreateRequest createRequest() {
                return newCreateRequest("users", "bjensen", userJson("bjensen", null, "bjensen", "created"));
            }
            @Override
            ResourceResponse createdResource() {
                return newResourceResponse("bjensen", "1", userJson("bjensen", "1", "bjensen", "created"));
            }
        },
        CLIENT_ID_SECONDARY_VIEW {
            @Override
            RequestHandler handler() {
                return rest2Ldap(defaultOptions(),
                                 top(),
                                 userUidResource(),
                                 resource("test")
                                         .subResources(collectionOf("userUid").useClientNaming("uid", "mail")
                                                                              .dnTemplate("dc=test")
                                                                              .urlTemplate("users")))
                        .newRequestHandlerFor("test");
            }
            @Override
            CreateRequest createRequest() {
                return newCreateRequest("users", "bjensen@test.com", userJson("bjensen", null, "bjensen", "created"));
            }
            @Override
            ResourceResponse createdResource() {
                return newResourceResponse("bjensen@test.com", "1", userJson("bjensen", "1", "bjensen", "created"));
            }
        },
        SERVER_ID_PRIMARY_VIEW {
            @Override
            RequestHandler handler() {
                return rest2Ldap(defaultOptions(),
                                 top(),
                                 userEntryUuidResource(),
                                 resource("test")
                                         .subResources(collectionOf("userEntryUuid").useServerEntryUuidNaming("uid")
                                                                                    .dnTemplate("dc=test")
                                                                                    .urlTemplate("users")))
                        .newRequestHandlerFor("test");
            }
            @Override
            CreateRequest createRequest() {
                return newCreateRequest("users", null, userJson(null, null, "bjensen", "created"));
            }
            @Override
            ResourceResponse createdResource() {
                return newResourceResponse(ENTRY_UUID, "1", userJson(ENTRY_UUID, "1", "bjensen", "created"));
            }
        },
        SERVER_ID_SECONDARY_VIEW {
            @Override
            RequestHandler handler() {
                return rest2Ldap(defaultOptions(),
                                 top(),
                                 userEntryUuidResource(),
                                 resource("test")
                                         .subResources(collectionOf("userEntryUuid").useClientNaming("uid", "mail")
                                                                                    .dnTemplate("dc=test")
                                                                                    .urlTemplate("users")))
                        .newRequestHandlerFor("test");
            }
            @Override
            CreateRequest createRequest() {
                return newCreateRequest("users", "bjensen@test.com", userJson(null, null, "bjensen", "created"));
            }
            @Override
            ResourceResponse createdResource() {
                return newResourceResponse("bjensen@test.com", "1", userJson(ENTRY_UUID, "1", "bjensen", "created"));
            }
        };
        abstract ResourceResponse createdResource();
        Context ctx() throws Exception {
            final EntryReader ldif = new LDIFEntryReader("dn: dc=test",
                                                         "objectClass: top",
                                                         "objectClass: domain",
                                                         "dc: test");
            final Connection connection = newInternalConnection(updateMeta(new MemoryBackend(ldif)));
            return new AuthenticatedConnectionContext(new RootContext(), connection);
        }
        abstract RequestHandler handler();
        abstract CreateRequest createRequest();
    }
    private static final String ENTRY_UUID = UUID.randomUUID().toString();
    private static final String USER_SCHEMA_URI = "fr:opendj:user:1.0";
    // Field values may be null.
    private static JsonValue userJson(final String id, final String rev, final String uid, final String description) {
        return json(o(f("_id", id),
                      f("_rev", rev),
                      f("schema", USER_SCHEMA_URI),
                      f("uid", uid),
                      f("email", uid + "@test.com"),
                      f("name", o(f("displayName", uid + " displayName"), f("surname", uid + " surname"))),
                      f("description", array(description))));
    }
    private static Map.Entry<String, Object> f(final String k, final Object v) {
        return v != null ? JsonValue.field(k, v) : null;
    }
    private static Object o(Map.Entry<?, ?>... fields) {
        return JsonValue.object(fields);
    }
    private static Resource top() {
        return resource("top").isAbstract(true)
                              .objectClass("top")
                              .property("_rev", simple("etag").isRequired(true).writability(READ_ONLY));
    }
    private static Resource userEntryUuidResource() {
        return userResource("userEntryUuid", simple("entryUUID").writability(READ_ONLY));
    }
    private static Resource userUidResource() {
        return userResource("userUid", simple("uid").isRequired(true).writability(CREATE_ONLY));
    }
    private static Resource userResource(final String resourceId, final PropertyMapper id) {
        return resource(resourceId).superType("top")
                                   .objectClasses("person", "organizationalPerson", "inetOrgPerson")
                                   .property("schema", constant(USER_SCHEMA_URI))
                                   .property("_id", id)
                                   .property("uid", simple("uid").isRequired(true).writability(CREATE_ONLY))
                                   .property("email", simple("mail"))
                                   .property("name", object().property("displayName", simple("cn").isRequired(true))
                                                             .property("surname", simple("sn").isRequired(true)))
                                   .property("description", simple("description").isMultiValued(true));
    }
    @Test(dataProvider = "useCases")
    public void canCreateResources(UseCase useCase) throws Exception {
        // Given
        RequestHandler handler = useCase.handler();
        Context ctx = useCase.ctx();
        // When
        ResourceResponse actual = handler.handleCreate(ctx, useCase.createRequest()).getOrThrowUninterruptibly();
        // Then
        assertThatExpectedResourceWasReturned(actual, useCase.createdResource());
    }
    @Test(dataProvider = "useCases", dependsOnMethods = "canCreateResources")
    public void canReadResources(UseCase useCase) throws Exception {
        // Given
        RequestHandler handler = useCase.handler();
        Context ctx = useCase.ctx();
        CreateRequest createRequest = useCase.createRequest();
        ResourceResponse resource = handler.handleCreate(ctx, createRequest).getOrThrowUninterruptibly();
        // When
        ReadRequest readRequest = newReadRequest(createRequest.getResourcePath(), resource.getId());
        ResourceResponse actual = handler.handleRead(ctx, readRequest).getOrThrowUninterruptibly();
        // Then
        assertThatExpectedResourceWasReturned(actual, resource);
    }
    @Test(dataProvider = "useCases", dependsOnMethods = "canCreateResources")
    public void canUpdateResources(UseCase useCase) throws Exception {
        // Given
        RequestHandler handler = useCase.handler();
        Context ctx = useCase.ctx();
        CreateRequest createRequest = useCase.createRequest();
        ResourceResponse resource = handler.handleCreate(ctx, createRequest).getOrThrowUninterruptibly();
        // When
        JsonValue newContent = resource.getContent().copy();
        newContent.put("description", array("updated"));
        UpdateRequest updateRequest = newUpdateRequest(createRequest.getResourcePath(), resource.getId(), newContent);
        ResourceResponse actual = handler.handleUpdate(ctx, updateRequest).getOrThrowUninterruptibly();
        // Then
        newContent.put("_rev", "2");
        ResourceResponse expected = newResourceResponse(resource.getId(), "2", newContent);
        assertThatExpectedResourceWasReturned(actual, expected);
        ReadRequest readRequest = newReadRequest(createRequest.getResourcePath(), resource.getId());
        ResourceResponse actual2 = handler.handleRead(ctx, readRequest).getOrThrowUninterruptibly();
        assertThatExpectedResourceWasReturned(actual2, expected);
    }
    @Test(dataProvider = "useCases", dependsOnMethods = "canCreateResources")
    public void canDeleteResources(UseCase useCase) throws Exception {
        // Given
        RequestHandler handler = useCase.handler();
        Context ctx = useCase.ctx();
        CreateRequest createRequest = useCase.createRequest();
        ResourceResponse resource = handler.handleCreate(ctx, createRequest).getOrThrowUninterruptibly();
        // When
        DeleteRequest deleteRequest = Requests.newDeleteRequest(createRequest.getResourcePath(), resource.getId());
        ResourceResponse actual = handler.handleDelete(ctx, deleteRequest).getOrThrowUninterruptibly();
        // Then
        assertThatExpectedResourceWasReturned(actual, resource);
        ReadRequest readRequest = newReadRequest(createRequest.getResourcePath(), resource.getId());
        try {
            handler.handleRead(ctx, readRequest).getOrThrowUninterruptibly();
            fail("Deleted resource can still be read");
        } catch (NotFoundException e) {
            // Expected.
        }
    }
    @Test(dataProvider = "useCases", dependsOnMethods = "canCreateResources")
    public void canPatchResources(UseCase useCase) throws Exception {
        // Given
        RequestHandler handler = useCase.handler();
        Context ctx = useCase.ctx();
        CreateRequest createRequest = useCase.createRequest();
        ResourceResponse resource = handler.handleCreate(ctx, createRequest).getOrThrowUninterruptibly();
        // When
        PatchRequest patchRequest = newPatchRequest(createRequest.getResourcePath(),
                                                    resource.getId(),
                                                    replace("description", array("patched")));
        ResourceResponse actual = handler.handlePatch(ctx, patchRequest).getOrThrowUninterruptibly();
        // Then
        JsonValue newContent = resource.getContent().copy();
        newContent.put("description", array("patched"));
        newContent.put("_rev", "2");
        ResourceResponse expected = newResourceResponse(resource.getId(), "2", newContent);
        assertThatExpectedResourceWasReturned(actual, expected);
        ReadRequest readRequest = newReadRequest(createRequest.getResourcePath(), resource.getId());
        ResourceResponse actual2 = handler.handleRead(ctx, readRequest).getOrThrowUninterruptibly();
        assertThatExpectedResourceWasReturned(actual2, expected);
    }
    @Test(dataProvider = "useCases", dependsOnMethods = "canCreateResources")
    public void canQueryResources(UseCase useCase) throws Exception {
        // Given
        RequestHandler handler = useCase.handler();
        Context ctx = useCase.ctx();
        CreateRequest createRequest = useCase.createRequest();
        ResourceResponse resource = handler.handleCreate(ctx, createRequest).getOrThrowUninterruptibly();
        // When
        QueryRequest queryRequest = newQueryRequest(createRequest.getResourcePath());
        queryRequest.setQueryFilter(QueryFilter.<JsonPointer>alwaysTrue());
        final AtomicReference<ResourceResponse> actualResource = new AtomicReference<>();
        QueryResponse actualResponse = handler.handleQuery(ctx, queryRequest, new QueryResourceHandler() {
            @Override
            public boolean handleResource(final ResourceResponse resource) {
                if (!actualResource.compareAndSet(null, resource)) {
                    fail("Too many resources returned during query");
                }
                return true;
            }
        }).getOrThrowUninterruptibly();
        // Then
        assertThat(actualResponse).isNotNull();
        assertThatExpectedResourceWasReturned(actualResource.get(), resource);
    }
    @DataProvider
    Object[][] useCases() throws Exception {
        UseCase[] values = UseCase.values();
        Object[][] data = new Object[values.length][];
        for (int i = 0; i < values.length; i++) {
            data[i] = new Object[] { values[i] };
        }
        return data;
    }
    private void assertThatExpectedResourceWasReturned(final ResourceResponse actual, final ResourceResponse expected) {
        assertThat(actual.getId()).isEqualTo(expected.getId());
        assertThat(actual.getRevision()).isEqualTo(expected.getRevision());
        assertThat(actual.getContent().asMap()).isEqualTo(expected.getContent().asMap());
    }
    private static org.forgerock.opendj.ldap.RequestHandler<RequestContext> updateMeta(final MemoryBackend delegate) {
        return new org.forgerock.opendj.ldap.RequestHandler<RequestContext>() {
            public void handleAdd(final RequestContext requestContext, final AddRequest request,
                                  final IntermediateResponseHandler intermediateResponseHandler,
                                  final LdapResultHandler<Result> resultHandler) {
                request.addAttribute("entryUuid", ENTRY_UUID);
                request.addAttribute("etag", 1);
                delegate.handleAdd(requestContext, request, intermediateResponseHandler, resultHandler);
            }
            public void handleBind(final RequestContext requestContext, final int version, final BindRequest request,
                                   final IntermediateResponseHandler intermediateResponseHandler,
                                   final LdapResultHandler<BindResult> resultHandler) {
                delegate.handleBind(requestContext, version, request, intermediateResponseHandler, resultHandler);
            }
            public void handleCompare(final RequestContext requestContext, final CompareRequest request,
                                      final IntermediateResponseHandler intermediateResponseHandler,
                                      final LdapResultHandler<CompareResult> resultHandler) {
                delegate.handleCompare(requestContext, request, intermediateResponseHandler, resultHandler);
            }
            public void handleDelete(final RequestContext requestContext,
                                     final org.forgerock.opendj.ldap.requests.DeleteRequest request,
                                     final IntermediateResponseHandler intermediateResponseHandler,
                                     final LdapResultHandler<Result> resultHandler) {
                delegate.handleDelete(requestContext, request, intermediateResponseHandler, resultHandler);
            }
            public <R extends ExtendedResult> void handleExtendedRequest(final RequestContext requestContext,
                                                                         final ExtendedRequest<R> request,
                                                                         final IntermediateResponseHandler
                                                                                 intermediateResponseHandler,
                                                                         final LdapResultHandler<R> resultHandler) {
                delegate.handleExtendedRequest(requestContext, request, intermediateResponseHandler, resultHandler);
            }
            public void handleModify(final RequestContext requestContext, final ModifyRequest request,
                                     final IntermediateResponseHandler intermediateResponseHandler,
                                     final LdapResultHandler<Result> resultHandler) {
                incrementEtag(request.getName());
                delegate.handleModify(requestContext, request, intermediateResponseHandler, resultHandler);
            }
            private void incrementEtag(final DN name) {
                final Entry entry = delegate.get(name);
                if (entry != null) {
                    final int etag = entry.parseAttribute("etag").asInteger(1);
                    entry.replaceAttribute("etag", etag + 1);
                }
            }
            public void handleModifyDN(final RequestContext requestContext, final ModifyDNRequest request,
                                       final IntermediateResponseHandler intermediateResponseHandler,
                                       final LdapResultHandler<Result> resultHandler) {
                incrementEtag(request.getName());
                delegate.handleModifyDN(requestContext, request, intermediateResponseHandler, resultHandler);
            }
            public void handleSearch(final RequestContext requestContext, final SearchRequest request,
                                     final IntermediateResponseHandler intermediateResponseHandler,
                                     final SearchResultHandler entryHandler,
                                     final LdapResultHandler<Result> resultHandler) {
                delegate.handleSearch(requestContext,
                                      request,
                                      intermediateResponseHandler,
                                      entryHandler,
                                      resultHandler);
            }
        };
    }
}
opendj-server-legacy/resource/config/config.ldif
@@ -386,7 +386,7 @@
ds-cfg-enabled: true
ds-cfg-java-class: org.opends.server.protocols.http.rest2ldap.Rest2LdapEndpoint
ds-cfg-base-path: /api
ds-cfg-config-url: config/http-config.json
ds-cfg-config-directory: config/rest2ldap/endpoints/api
ds-cfg-http-authorization-mechanism: cn=HTTP Basic,cn=HTTP Authorization Mechanisms,cn=config
dn: cn=HTTP Authorization Mechanisms,cn=config
opendj-server-legacy/resource/config/http-config.json
File was deleted
opendj-server-legacy/resource/config/rest2ldap/endpoints/api/users-and-groups-v1.json
New file
@@ -0,0 +1,234 @@
{
    // This file defines an example Rest2Ldap API mapping exposing a multi-tenant deployment exposing users,
    // POSIX users, and groups, as follows:
    //
    // /api/{tenant}/users/{uid} - users for a given tenant, e.g. "/api/example/users/bjensen"
    // /api/{tenant}/groups/{cn} - groups for a given tenant, e.g. "/api/example/groups/administrators"
    //
    "version": "1.0",
    // This section defines all of the resources, their inheritance, and relationships.
    "resourceTypes": {
        // This resource represents the entry point into the user/group API. It only defines sub-resources and
        // does not have any properties itself. The URL and DN templates include a template variable allowing
        // this API to support multi-tenancy. Multiple template variables are permitted.
        "users-and-groups-v1": {
            "subResources": {
                "{tenant}/users": {
                    "type": "collection",
                    "dnTemplate": "ou=people,dc={tenant},dc=com",
                    "resource": "frapi:opendj:rest2ldap:user:1.0",
                    "namingStrategy": {
                        "type": "clientDnNaming",
                        "dnAttribute": "uid"
                    }
                },
                "{tenant}/groups": {
                    "type": "collection",
                    "dnTemplate": "ou=groups,dc={tenant},dc=com",
                    "resource": "frapi:opendj:rest2ldap:group:1.0",
                    "namingStrategy": {
                        "type": "clientDNNaming",
                        "dnAttribute": "cn"
                    }
                }
            }
        },
        // This resource will act as the common parent of all resources that have a JSON representation.
        "frapi:opendj:rest2ldap:object:1.0": {
            "isAbstract": true,
            "objectClasses": [ "top" ],
            // This property will store type information in a resource's JSON representation. It is the
            // equivalent of the "objectClass" attribute, except that it is single valued and will contain
            // the resource name, e.g. "frapi:opendj:rest2ldap:user:1.0" or "frapi:opendj:rest2ldap:group:1.0".
            "resourceTypeProperty": "_schema",
            "properties": {
                // Resource type property mappers store the resource's type and don't have any configuration.
                "_schema": {
                    "type": "resourceType"
                },
                "_rev": {
                    "type": "simple",
                    "ldapAttribute": "etag",
                    "writability": "readOnly"
                },
                "_meta": {
                    "type": "object",
                    "properties": {
                        "created": {
                            "type": "simple",
                            "ldapAttribute": "createTimestamp",
                            "writability": "readOnly"
                        },
                        "lastModified": {
                            "type": "simple",
                            "ldapAttribute": "modifyTimestamp",
                            "writability": "readOnly"
                        }
                    }
                }
            }
        },
        // A "user" resource includes property mapping for the inetOrgPerson LDAP object class and is identified by
        // the "uid" LDAP attribute. Users have a single sub-type representing users with POSIX account information.
        "frapi:opendj:rest2ldap:user:1.0": {
            "superType": "frapi:opendj:rest2ldap:object:1.0",
            "objectClasses": [ "person", "organizationalPerson", "inetOrgPerson" ],
            "supportedActions": [ "passwordModify" ],
            "properties": {
                "_id": {
                    "type": "simple",
                    "ldapAttribute": "uid",
                    "isRequired": true,
                    "writability": "createOnly"
                },
                "userName": {
                    "type": "simple",
                    "ldapAttribute": "mail"
                },
                "displayName": {
                    "type": "simple",
                    "ldapAttribute": "cn",
                    "isMultiValued": true,
                    "isRequired": true
                },
                "name": {
                    "type": "object",
                    "properties": {
                        "givenName": {
                            "type": "simple"
                        },
                        "familyName": {
                            "type": "simple",
                            "ldapAttribute": "sn",
                            "isRequired": true
                        }
                    }
                },
                "description": {
                    "type": "simple"
                },
                "manager": {
                    "type": "reference",
                    "ldapAttribute": "manager",
                    "baseDn": "ou=people,dc=example,dc=com",
                    "primaryKey": "uid",
                    "mapper": {
                        "type": "object",
                        "properties": {
                            "_id": {
                                "type": "simple",
                                "ldapAttribute": "uid",
                                "isRequired": true
                            },
                            "displayName": {
                                "type": "simple",
                                "ldapAttribute": "cn",
                                "writability": "readOnlyDiscardWrites"
                            }
                        }
                    }
                },
                "groups": {
                    "type": "reference",
                    "ldapAttribute": "isMemberOf",
                    "baseDn": "ou=groups,dc=example,dc=com",
                    "isMultiValued": true,
                    "writability": "readOnly",
                    "primaryKey": "cn",
                    "mapper": {
                        "type": "object",
                        "properties": {
                            "_id": {
                                "type": "simple",
                                "ldapAttribute": "cn"
                            }
                        }
                    }
                },
                "contactInformation": {
                    "type": "object",
                    "properties": {
                        "telephoneNumber": {
                            "type": "simple"
                        },
                        "emailAddress": {
                            "type": "simple",
                            "ldapAttribute": "mail"
                        }
                    }
                }
            }
        },
        // A user with POSIX account information.
        "frapi:opendj:rest2ldap:posixUser:1.0": {
            "superType": "frapi:opendj:rest2ldap:user:1.0",
            "objectClasses": [ "posixAccount" ],
            "properties": {
                "uidNumber": {
                    "type": "simple",
                    "isRequired": true
                },
                "gidNumber": {
                    "type": "simple",
                    "isRequired": true
                },
                "homeDirectory": {
                    "type": "simple",
                    "isRequired": true
                },
                "loginShell": {
                    "type": "simple"
                },
                "gecos": {
                    "type": "simple"
                }
            }
        },
        // A "group" resource includes property mapping for the inetOrgPerson LDAP object class and is identified by
        // the "uid" LDAP attribute. Users have a single sub-type representing users with POSIX account information.
        "frapi:opendj:rest2ldap:group:1.0": {
            "superType": "frapi:opendj:rest2ldap:object:1.0",
            "objectClasses": [ "groupOfUniqueNames" ],
            "properties": {
                "_id": {
                    "type": "simple",
                    "ldapAttribute": "cn",
                    "isRequired": true,
                    "writability": "createOnly"
                },
                "displayName": {
                    "type": "simple",
                    "ldapAttribute": "cn",
                    "isRequired": true,
                    "writability": "readOnly"
                },
                "description": {
                    "type": "simple"
                },
                "members": {
                    "type": "reference",
                    "ldapAttribute": "uniqueMember",
                    "baseDn": "dc=example,dc=com",
                    "primaryKey": "uid",
                    "isMultiValued": true,
                    "mapper": {
                        "type": "object",
                        "properties": {
                            "_id": {
                                "type": "simple",
                                "ldapAttribute": "uid",
                                "isRequired": true
                            },
                            "displayName": {
                                "type": "simple",
                                "ldapAttribute": "cn",
                                "writability": "readOnlyDiscardWrites"
                            }
                        }
                    }
                }
            }
        }
    }
}
opendj-server-legacy/resource/schema/02-config.ldif
@@ -3844,7 +3844,7 @@
  SINGLE-VALUE
  X-ORIGIN 'OpenDJ Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.161
  NAME 'ds-cfg-config-url'
  NAME 'ds-cfg-config-directory'
  EQUALITY caseIgnoreMatch
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
  SINGLE-VALUE
@@ -5997,7 +5997,7 @@
  NAME 'ds-cfg-rest2ldap-endpoint'
  SUP ds-cfg-http-endpoint
  STRUCTURAL
  MUST ( ds-cfg-config-url $ ds-cfg-resource )
  MUST ( ds-cfg-config-directory )
  X-ORIGIN 'OpenDJ Directory Server' )
objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.36
  NAME 'ds-cfg-http-authorization-mechanism'
opendj-server-legacy/src/main/assembly/opendj-archive-component.xml
@@ -12,7 +12,7 @@
  Header, with the fields enclosed by brackets [] replaced by your own identifying
  information: "Portions Copyright [year] [name of copyright owner]".
  Copyright 2015 ForgeRock AS.
  Copyright 2015-2016 ForgeRock AS.
-->
<!-- OpenDJ final archive content descriptor -->
<component>
@@ -202,9 +202,6 @@
    <fileSet>
      <directory>${basedir}/resource/config</directory>
      <outputDirectory>template/config</outputDirectory>
      <includes>
        <include>*.*</include>
      </includes>
      <excludes>
        <exclude>config.ldif</exclude>
      </excludes>
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java
@@ -15,27 +15,23 @@
 */
package org.opends.server.protocols.http.rest2ldap;
import static org.forgerock.http.util.Json.readJsonLenient;
import static org.opends.messages.ConfigMessages.*;
import static org.forgerock.json.resource.http.CrestHttp.newHttpHandler;
import static org.forgerock.opendj.rest2ldap.Rest2LdapJsonConfigurator.configureEndpoint;
import static org.forgerock.util.Options.defaultOptions;
import static org.opends.messages.ConfigMessages.ERR_CONFIG_REST2LDAP_INVALID;
import static org.opends.messages.ConfigMessages.ERR_CONFIG_REST2LDAP_UNABLE_READ;
import static org.opends.messages.ConfigMessages.ERR_CONFIG_REST2LDAP_UNEXPECTED_JSON;
import static org.opends.server.util.StaticUtils.getFileForPath;
import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import org.forgerock.http.Handler;
import org.forgerock.http.HttpApplication;
import org.forgerock.http.HttpApplicationException;
import org.forgerock.http.io.Buffer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.JsonValueException;
import org.forgerock.json.resource.Router;
import org.forgerock.json.resource.http.CrestHttp;
import org.forgerock.opendj.rest2ldap.Rest2Ldap;
import org.forgerock.opendj.server.config.server.Rest2ldapEndpointCfg;
import org.forgerock.util.Factory;
import org.opends.server.api.HttpEndpoint;
@@ -45,13 +41,13 @@
/**
 * Encapsulates configuration required to start a REST2LDAP application embedded
 * in this LDAP server. Acts as a factory for {@link Rest2LDAPHttpApplication}.
 * in this LDAP server. Acts as a factory for {@link HttpApplication}.
 */
public final class Rest2LdapEndpoint extends HttpEndpoint<Rest2ldapEndpointCfg>
{
  /**
   * Create a new Rest2LdapEnpoint with the supplied configuration.
   * Create a new Rest2LdapEndpoint with the supplied configuration.
   *
   * @param configuration
   *          Configuration to use for the {@link HttpApplication}
@@ -70,71 +66,32 @@
  }
  /**
   * Specialized {@link Rest2LDAPHttpApplication} using internal connections to
   * this local LDAP server.
   * Specialized {@link HttpApplication} using internal connections to this local LDAP server.
   */
  private final class InternalRest2LDAPHttpApplication implements HttpApplication
  {
    private final URL configURL;
    InternalRest2LDAPHttpApplication() throws InitializationException
    {
      try
      {
        final URI configURI = new URI(configuration.getConfigUrl());
        configURL = configURI.isAbsolute()
            ? configURI.toURL()
            : getFileForPath(configuration.getConfigUrl()).toURI().toURL();
      }
      catch (MalformedURLException | URISyntaxException e)
      {
        throw new InitializationException(
            ERR_CONFIG_REST2LDAP_MALFORMED_URL.get(configuration.dn(), stackTraceToSingleLineString(e)));
      }
    }
    @Override
    public Handler start() throws HttpApplicationException
    {
      JsonValue mappingConfiguration;
      final File endpointConfig = getFileForPath(configuration.getConfigDirectory(), serverContext);
      try
      {
        mappingConfiguration = readJson(configURL);
        return newHttpHandler(configureEndpoint(endpointConfig, defaultOptions()));
      }
      catch (IOException e)
      {
        throw new LocalizedHttpApplicationException(
            ERR_CONFIG_REST2LDAP_UNABLE_READ.get(configURL, configuration.dn(), stackTraceToSingleLineString(e)), e);
      }
      final JsonValue mappings = mappingConfiguration.get("mappings").required();
      final Router router = new Router();
      try
      {
        for (final String mappingUrl : mappings.keys())
        {
          final JsonValue mapping = mappings.get(mappingUrl);
          router.addRoute(Router.uriTemplate(mappingUrl), Rest2Ldap.builder().configureMapping(mapping).build());
        }
        throw new LocalizedHttpApplicationException(ERR_CONFIG_REST2LDAP_UNABLE_READ.get(
                endpointConfig, configuration.dn(), stackTraceToSingleLineString(e)), e);
      }
      catch (JsonValueException e)
      {
        throw new LocalizedHttpApplicationException(
            ERR_CONFIG_REST2LDAP_UNEXPECTED_JSON.get(e.getJsonValue().getPointer(), configURL, configuration.dn(),
                                                     stackTraceToSingleLineString(e)), e);
        throw new LocalizedHttpApplicationException(ERR_CONFIG_REST2LDAP_UNEXPECTED_JSON.get(
                e.getJsonValue().getPointer(), endpointConfig, configuration.dn(), stackTraceToSingleLineString(e)), e);
      }
      catch (IllegalArgumentException e)
      {
        throw new LocalizedHttpApplicationException(
            ERR_CONFIG_REST2LDAP_INVALID.get(configURL, configuration.dn(), stackTraceToSingleLineString(e)), e);
      }
      return CrestHttp.newHttpHandler(router);
    }
    private JsonValue readJson(final URL resource) throws IOException
    {
      try (InputStream in = resource.openStream())
      {
        return new JsonValue(readJsonLenient(in));
        throw new LocalizedHttpApplicationException(ERR_CONFIG_REST2LDAP_INVALID.get(
                endpointConfig, configuration.dn(), stackTraceToSingleLineString(e)), e);
      }
    }