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

Matthew Swift
15.42.2016 a08c81f677247ec9eb7721a86250c663065e9930
OPENDJ-2871 Add support for sub-resources and inheritance

* Rest2Ldap - is now instantiable and is the entry point for configuring
Rest2Ldap data models. Options are injected via the commons Options
class, resources types are defined using the Resource class, and their
relationships with other resources are defined using SubResources. There
is documentation describing the construction process in the Rest2Ldap
class

* Rest2LdapJsonConfigurator - entry point for constructing Rest2Ldap
instances from JSON configuration. Used by the gateway

* TypePropertyMapper - a new attribute mapper responsible for mapping
LDAP object classes to a JSON "type" field

* ObjectPropertyMapper - now supports a "default" attribute mapping mode
(OPENDJ-3100)

* Renamed all AttributeMappers to PropertyMappers to reflect change in
terminology ("properties" instead of "attributes") which more closely
reflects the terminology used in JSON schema and FR API descriptors

* Minor misc changes, e.g. converting classes/methods to camel-case.
2 files deleted
14 files added
1 files renamed
19 files modified
7167 ■■■■■ changed files
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/Rest2ldapEndpointConfiguration.xml 16 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/config.json 242 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/users-and-groups-v1.json 234 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/rest2ldap.json 8 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLdapPropertyMapper.java 79 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractRequestHandler.java 84 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Action.java 44 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java 81 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JsonConstantPropertyMapper.java 46 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NamingStrategy.java 79 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectPropertyMapper.java 264 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/PropertyMapper.java 33 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java 54 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java 186 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java 447 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ResourceTypePropertyMapper.java 123 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java 1171 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapHttpApplication.java 175 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfigurator.java 658 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RoutingContext.java 43 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimplePropertyMapper.java 87 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java 151 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java 530 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java 993 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java 292 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java 34 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Utils.java 19 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties 29 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java 72 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapTest.java 465 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/resource/config/config.ldif 2 ●●● patch | view | raw | blame | history
opendj-server-legacy/resource/config/http-config.json 104 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/resource/config/rest2ldap/endpoints/api/users-and-groups-v1.json 234 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/resource/schema/02-config.ldif 4 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/assembly/opendj-archive-component.xml 5 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java 79 ●●●● patch | view | raw | blame | history
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);
      }
    }