From a08c81f677247ec9eb7721a86250c663065e9930 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Wed, 22 Jun 2016 22:12:03 +0000
Subject: [PATCH] OPENDJ-2871 Add support for sub-resources and inheritance
---
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfigurator.java | 658 +++++
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java | 530 ++++
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java | 72
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RoutingContext.java | 43
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/Rest2ldapEndpointConfiguration.xml | 16
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java | 79
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectPropertyMapper.java | 264 +
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ResourceTypePropertyMapper.java | 123 +
opendj-server-legacy/resource/schema/02-config.ldif | 4
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NamingStrategy.java | 79
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java | 151 +
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java | 1171 ++-------
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLdapPropertyMapper.java | 79
opendj-server-legacy/resource/config/config.ldif | 2
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/rest2ldap.json | 8
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/PropertyMapper.java | 33
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Utils.java | 19
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Action.java | 44
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java | 292 ++
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java | 54
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractRequestHandler.java | 84
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimplePropertyMapper.java | 87
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JsonConstantPropertyMapper.java | 46
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapHttpApplication.java | 175 -
opendj-server-legacy/src/main/assembly/opendj-archive-component.xml | 5
opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties | 29
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java | 186
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java | 34
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java | 447 +++
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/config.json | 242 --
/dev/null | 104
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapTest.java | 465 +++
opendj-server-legacy/resource/config/rest2ldap/endpoints/api/users-and-groups-v1.json | 234 +
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java | 993 ++++---
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/users-and-groups-v1.json | 234 +
35 files changed, 4,726 insertions(+), 2,360 deletions(-)
diff --git a/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/Rest2ldapEndpointConfiguration.xml b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/Rest2ldapEndpointConfiguration.xml
index 7c8da6a..644feb7 100644
--- a/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/Rest2ldapEndpointConfiguration.xml
+++ b/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>
diff --git a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/config.json
similarity index 62%
rename from opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
rename to opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/config.json
index 1eab5b3..50722af 100644
--- a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
+++ b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/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"
- }
- }
- }
- }
- }
- }
}
}
diff --git a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/users-and-groups-v1.json b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/users-and-groups-v1.json
new file mode 100644
index 0000000..dbdfe7d
--- /dev/null
+++ b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/endpoints/api/users-and-groups-v1.json
@@ -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"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/rest2ldap.json b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/rest2ldap.json
new file mode 100644
index 0000000..49ce74e
--- /dev/null
+++ b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/rest2ldap/rest2ldap.json
@@ -0,0 +1,8 @@
+{
+ // Options controlling how Rest2Ldap interacts with LDAP servers.
+ "useMvcc": true,
+ "mvccAttribute": "etag",
+ "readOnUpdatePolicy": "controls",
+ "useSubtreeDelete": true,
+ "usePermissiveModify": true
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLdapPropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLdapPropertyMapper.java
index 66fb8d1..23f37fd 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLdapPropertyMapper.java
+++ b/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();
}
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractRequestHandler.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractRequestHandler.java
new file mode 100644
index 0000000..b0f427b
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractRequestHandler.java
@@ -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();
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Action.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Action.java
new file mode 100644
index 0000000..1cd7ee6
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Action.java
@@ -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;
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
deleted file mode 100644
index 8a98cb0..0000000
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * 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 2013-2016 ForgeRock AS.
- */
-package org.forgerock.opendj.rest2ldap;
-
-import org.forgerock.opendj.ldap.DecodeOptions;
-import org.forgerock.opendj.ldap.schema.Schema;
-
-/**
- * Common configuration options.
- */
-final class Config {
- private final DecodeOptions options;
- private final ReadOnUpdatePolicy readOnUpdatePolicy;
- private final boolean useSubtreeDelete;
- private final boolean usePermissiveModify;
-
- Config(final ReadOnUpdatePolicy readOnUpdatePolicy, final boolean useSubtreeDelete,
- final boolean usePermissiveModify, final Schema schema) {
- this.readOnUpdatePolicy = readOnUpdatePolicy;
- this.useSubtreeDelete = useSubtreeDelete;
- this.usePermissiveModify = usePermissiveModify;
- this.options = new DecodeOptions().setSchema(schema);
- }
-
- /**
- * Returns the decoding options which should be used when decoding controls
- * in responses.
- *
- * @return The decoding options which should be used when decoding controls
- * in responses.
- */
- DecodeOptions decodeOptions() {
- return options;
- }
-
- /**
- * Returns {@code true} if modify requests should include the permissive
- * modify control.
- *
- * @return {@code true} if modify requests should include the permissive
- * modify control.
- */
- boolean usePermissiveModify() {
- return usePermissiveModify;
- }
-
- /**
- * Returns {@code true} if delete requests should include the subtree delete
- * control.
- *
- * @return {@code true} if delete requests should include the subtree delete
- * control.
- */
- boolean useSubtreeDelete() {
- return useSubtreeDelete;
- }
-
- /**
- * Returns the policy which should be used in order to read an entry before
- * it is deleted, or after it is added or modified.
- *
- * @return The policy which should be used in order to read an entry before
- * it is deleted, or after it is added or modified.
- */
- ReadOnUpdatePolicy readOnUpdatePolicy() {
- return readOnUpdatePolicy;
- }
-}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JsonConstantPropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JsonConstantPropertyMapper.java
index 230d2b7..f171ad8 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JsonConstantPropertyMapper.java
+++ b/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.
}
}
-
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NamingStrategy.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NamingStrategy.java
index e180964..52366ef 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NamingStrategy.java
+++ b/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;
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectPropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectPropertyMapper.java
index 47aab51..4aa4ab1 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectPropertyMapper.java
+++ b/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));
+ }
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/PropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/PropertyMapper.java
index 7714364..3755d3f 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/PropertyMapper.java
+++ b/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.
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java
new file mode 100644
index 0000000..b7cc7d3
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnlyRequestHandler.java
@@ -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);
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java
index 52291a1..e01c530 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferencePropertyMapper.java
@@ -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
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java
new file mode 100644
index 0000000..ab76ccd
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Resource.java
@@ -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;
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ResourceTypePropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ResourceTypePropertyMapper.java
new file mode 100644
index 0000000..845f821
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ResourceTypePropertyMapper.java
@@ -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());
+ }
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java
index e7fa676..e0f8920 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java
@@ -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);
}
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapHttpApplication.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapHttpApplication.java
index d5f56be..7953a1e 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapHttpApplication.java
+++ b/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()));
}
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfigurator.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfigurator.java
new file mode 100644
index 0000000..33d9d44
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapJsonConfigurator.java
@@ -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.
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RoutingContext.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RoutingContext.java
new file mode 100644
index 0000000..fb2a3ae
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RoutingContext.java
@@ -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;
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimplePropertyMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimplePropertyMapper.java
index 403056d..3abe8cd 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimplePropertyMapper.java
+++ b/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();
}
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java
new file mode 100644
index 0000000..8d8acbc
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java
@@ -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();
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
new file mode 100644
index 0000000..b420b0d
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
@@ -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);
+ }
+ });
+ }
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java
index 4f5403a..a1da30e 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java
@@ -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();
+ }
+ };
}
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
new file mode 100644
index 0000000..d0dd95c
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
@@ -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);
+ }
+ });
+ }
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
index a3038c0..5ace36c 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
+++ b/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);
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Utils.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Utils.java
index 433d4f2..27c586f 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Utils.java
+++ b/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");
}
diff --git a/opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties b/opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties
index bc38afb..befdf97 100644
--- a/opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties
+++ b/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
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
index 0602c63..343ed60 100644
--- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
+++ b/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());
}
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapTest.java
new file mode 100644
index 0000000..f3d7dce
--- /dev/null
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/Rest2LdapTest.java
@@ -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);
+ }
+ };
+ }
+}
diff --git a/opendj-server-legacy/resource/config/config.ldif b/opendj-server-legacy/resource/config/config.ldif
index 86af4bb..0d8d11d 100644
--- a/opendj-server-legacy/resource/config/config.ldif
+++ b/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
diff --git a/opendj-server-legacy/resource/config/http-config.json b/opendj-server-legacy/resource/config/http-config.json
deleted file mode 100644
index 1287afe..0000000
--- a/opendj-server-legacy/resource/config/http-config.json
+++ /dev/null
@@ -1,104 +0,0 @@
-{
- // 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" } }
- } }
- }
- }
- }
-}
-
diff --git a/opendj-server-legacy/resource/config/rest2ldap/endpoints/api/users-and-groups-v1.json b/opendj-server-legacy/resource/config/rest2ldap/endpoints/api/users-and-groups-v1.json
new file mode 100644
index 0000000..dbdfe7d
--- /dev/null
+++ b/opendj-server-legacy/resource/config/rest2ldap/endpoints/api/users-and-groups-v1.json
@@ -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"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/opendj-server-legacy/resource/schema/02-config.ldif b/opendj-server-legacy/resource/schema/02-config.ldif
index 181bb47..79cacd5 100644
--- a/opendj-server-legacy/resource/schema/02-config.ldif
+++ b/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'
diff --git a/opendj-server-legacy/src/main/assembly/opendj-archive-component.xml b/opendj-server-legacy/src/main/assembly/opendj-archive-component.xml
index 56442ac..3193beb 100644
--- a/opendj-server-legacy/src/main/assembly/opendj-archive-component.xml
+++ b/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>
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java
index f49c0c6..b328186 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java
+++ b/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);
}
}
--
Gitblit v1.10.0