From fde0fcb4a258c7b80be59b183a79068ce602167f Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Wed, 10 Apr 2013 10:31:19 +0000
Subject: [PATCH] Partial fix for OPENDJ-693: Implement modify/update support
---
opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java | 169 +-----
opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java | 12
opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java | 276 ++++-------
opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java | 15
opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java | 217 +++++++++
opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java | 19
opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java | 19
opendj-sdk/opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json | 26
opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java | 26
opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java | 243 +++++++--
opendj-sdk/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java | 143 +++++
opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java | 105 ++-
opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java | 5
opendj-sdk/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java | 134 ++++-
14 files changed, 934 insertions(+), 475 deletions(-)
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json b/opendj-sdk/opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json
index 422fd95..faf424b 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json
+++ b/opendj-sdk/opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json
@@ -134,12 +134,13 @@
// The REST APIs and their LDAP attribute mappings.
"mappings" : {
"/users" : {
- "baseDN" : "ou=people,dc=example,dc=com",
- "readOnUpdatePolicy" : "controls",
- "useSubtreeDelete" : true,
- "etagAttribute" : "etag",
- "namingStrategy" : {
- "strategy" : "clientDNNaming",
+ "baseDN" : "ou=people,dc=example,dc=com",
+ "readOnUpdatePolicy" : "controls",
+ "useSubtreeDelete" : true,
+ "usePermissiveModify" : true,
+ "etagAttribute" : "etag",
+ "namingStrategy" : {
+ "strategy" : "clientDNNaming",
"dnAttribute" : "uid"
},
"additionalLDAPAttributes" : [
@@ -192,12 +193,13 @@
}
},
"/groups" : {
- "baseDN" : "ou=groups,dc=example,dc=com",
- "readOnUpdatePolicy" : "controls",
- "useSubtreeDelete" : true,
- "etagAttribute" : "etag",
- "namingStrategy" : {
- "strategy" : "clientDNNaming",
+ "baseDN" : "ou=groups,dc=example,dc=com",
+ "readOnUpdatePolicy" : "controls",
+ "useSubtreeDelete" : true,
+ "usePermissiveModify" : true,
+ "etagAttribute" : "etag",
+ "namingStrategy" : {
+ "strategy" : "clientDNNaming",
"dnAttribute" : "cn"
},
"additionalLDAPAttributes" : [
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
new file mode 100644
index 0000000..0a5398f
--- /dev/null
+++ b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
@@ -0,0 +1,217 @@
+/*
+ * 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 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.forgerock.opendj.ldap.Attributes.emptyAttribute;
+import static org.forgerock.opendj.rest2ldap.Utils.i18n;
+import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import org.forgerock.json.fluent.JsonPointer;
+import org.forgerock.json.fluent.JsonValue;
+import org.forgerock.json.resource.BadRequestException;
+import org.forgerock.json.resource.ResourceException;
+import org.forgerock.json.resource.ResultHandler;
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LinkedAttribute;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.opendj.ldap.ModificationType;
+
+/**
+ * An abstract LDAP attribute mapper which provides a simple mapping from a JSON
+ * value to a single LDAP attribute.
+ */
+abstract class AbstractLDAPAttributeMapper<T extends AbstractLDAPAttributeMapper<T>> extends
+ AttributeMapper {
+ Object defaultJSONValue = null;
+ List<Object> defaultJSONValues = emptyList();
+ final AttributeDescription ldapAttributeName;
+ private boolean isRequired = false;
+ private boolean isSingleValued = false;
+ private WritabilityPolicy writabilityPolicy = READ_WRITE;
+
+ AbstractLDAPAttributeMapper(final AttributeDescription ldapAttributeName) {
+ this.ldapAttributeName = ldapAttributeName;
+ }
+
+ /**
+ * Indicates that the LDAP attribute is mandatory and must be provided
+ * during create requests.
+ *
+ * @return This attribute mapper.
+ */
+ public final T isRequired() {
+ this.isRequired = true;
+ return getThis();
+ }
+
+ /**
+ * Indicates that multi-valued LDAP attribute should be represented as a
+ * single-valued JSON value, rather than an array of values.
+ *
+ * @return This attribute mapper.
+ */
+ public final T isSingleValued() {
+ this.isSingleValued = true;
+ return getThis();
+ }
+
+ /**
+ * Indicates whether or not the LDAP attribute supports updates. The default
+ * is {@link WritabilityPolicy#READ_WRITE}.
+ *
+ * @param policy
+ * The writability policy.
+ * @return This attribute mapper.
+ */
+ public final T writability(final WritabilityPolicy policy) {
+ this.writabilityPolicy = policy;
+ return getThis();
+ }
+
+ boolean attributeIsRequired() {
+ return isRequired && defaultJSONValue == null;
+ }
+
+ boolean attributeIsSingleValued() {
+ return isSingleValued || ldapAttributeName.getAttributeType().isSingleValue();
+ }
+
+ @Override
+ void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath,
+ final Set<String> ldapAttributes) {
+ ldapAttributes.add(ldapAttributeName.toString());
+ }
+
+ abstract void getNewLDAPAttributes(Context c, JsonPointer path, List<Object> newValues,
+ ResultHandler<Attribute> h);
+
+ abstract T getThis();
+
+ @Override
+ void toLDAP(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
+ final ResultHandler<List<Modification>> h) {
+ // Make following code readable.
+ final boolean isUpdateRequest = e != null;
+ final boolean isCreateRequest = !isUpdateRequest;
+
+ // Get the existing LDAP attribute.
+ final Attribute oldLDAPAttribute;
+ if (isCreateRequest) {
+ oldLDAPAttribute = emptyAttribute(ldapAttributeName);
+ } else {
+ final Attribute tmp = e.getAttribute(ldapAttributeName);
+ oldLDAPAttribute = tmp != null ? tmp : emptyAttribute(ldapAttributeName);
+ }
+
+ if (v != null && v.isList() && attributeIsSingleValued()) {
+ // Single-valued field violation.
+ h.handleError(new BadRequestException(i18n(
+ "The request cannot be processed because an array of values was "
+ + "provided for the single valued field '%s'", path)));
+ } else {
+ getNewLDAPAttributes(c, path, asList(v), new ResultHandler<Attribute>() {
+ @Override
+ public void handleError(final ResourceException error) {
+ h.handleError(error);
+ }
+
+ @Override
+ public void handleResult(final Attribute newLDAPAttribute) {
+ /*
+ * If the attribute is read-only then handle the following
+ * cases:
+ *
+ * 1) new values are provided and they are the same as the
+ * existing values
+ *
+ * 2) no new values are provided.
+ */
+ if (isCreateRequest && !writabilityPolicy.canCreate(ldapAttributeName)
+ || isUpdateRequest && !writabilityPolicy.canWrite(ldapAttributeName)) {
+ if (newLDAPAttribute.isEmpty()
+ || (isUpdateRequest && newLDAPAttribute.equals(oldLDAPAttribute))
+ || writabilityPolicy.discardWrites()) {
+ // No change.
+ h.handleResult(Collections.<Modification> emptyList());
+ } else {
+ h.handleError(new BadRequestException(i18n(
+ "The request cannot be processed because it attempts to modify "
+ + "the read-only field '%s'", path)));
+ }
+ } else {
+ // Compute the changes to the attribute.
+ final List<Modification> modifications;
+ if (oldLDAPAttribute.isEmpty() && newLDAPAttribute.isEmpty()) {
+ // No change.
+ modifications = Collections.<Modification> emptyList();
+ } else if (oldLDAPAttribute.isEmpty() || newLDAPAttribute.isEmpty()) {
+ // Delete or add.
+ modifications =
+ singletonList(new Modification(ModificationType.REPLACE,
+ newLDAPAttribute));
+ } else {
+ /*
+ * We could do a replace, but try to save bandwidth
+ * and send diffs instead. Perform deletes first in
+ * case we don't have an appropriate normalizer:
+ * permissive add(x) followed by delete(x) is
+ * destructive, whereas delete(x) followed by add(x)
+ * is idempotent when adding/removing the same
+ * value.
+ */
+ modifications = new ArrayList<Modification>(2);
+
+ final Attribute deletedValues = new LinkedAttribute(oldLDAPAttribute);
+ deletedValues.removeAll(newLDAPAttribute);
+ if (!deletedValues.isEmpty()) {
+ modifications.add(new Modification(ModificationType.DELETE,
+ deletedValues));
+ }
+
+ final Attribute addedValues = new LinkedAttribute(newLDAPAttribute);
+ addedValues.removeAll(oldLDAPAttribute);
+ if (!addedValues.isEmpty()) {
+ modifications.add(new Modification(ModificationType.ADD,
+ addedValues));
+ }
+ }
+ h.handleResult(modifications);
+ }
+ }
+ });
+ }
+ }
+
+ private List<Object> asList(final JsonValue v) {
+ if (v == null || v.isNull() || (v.isList() && v.size() == 0)) {
+ return defaultJSONValues;
+ } else if (v.isList()) {
+ return v.asList();
+ } else {
+ return singletonList(v.getObject());
+ }
+ }
+
+}
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
index 70f699a..02cc5d6 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
+++ b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
@@ -21,9 +21,9 @@
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.ResultHandler;
-import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.Modification;
/**
* An attribute mapper is responsible for converting JSON values to and from
@@ -56,7 +56,8 @@
* The set into which the required LDAP attribute names should be
* put.
*/
- abstract void getLDAPAttributes(Context c, JsonPointer jsonAttribute, Set<String> ldapAttributes);
+ abstract void getLDAPAttributes(Context c, JsonPointer path, JsonPointer subPath,
+ Set<String> ldapAttributes);
/**
* Transforms the provided REST comparison filter parameters to an LDAP
@@ -86,7 +87,7 @@
* @param h
* The result handler.
*/
- abstract void getLDAPFilter(Context c, FilterType type, JsonPointer jsonAttribute,
+ abstract void getLDAPFilter(Context c, JsonPointer path, JsonPointer subPath, FilterType type,
String operator, Object valueAssertion, ResultHandler<Filter> h);
/**
@@ -113,7 +114,7 @@
* @param h
* The result handler.
*/
- abstract void toJSON(Context c, Entry e, ResultHandler<JsonValue> h);
+ abstract void toJSON(Context c, JsonPointer path, Entry e, ResultHandler<JsonValue> h);
/**
* Maps a JSON value to one or more LDAP attributes, invoking a completion
@@ -139,7 +140,8 @@
* @param h
* The result handler.
*/
- abstract void toLDAP(Context c, JsonValue v, ResultHandler<List<Attribute>> h);
+ abstract void toLDAP(Context c, JsonPointer path, Entry e, JsonValue v,
+ ResultHandler<List<Modification>> h);
// TODO: methods for obtaining schema information (e.g. name, description,
// type information).
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java
index 78b8d96..84e67c6 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java
+++ b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java
@@ -16,6 +16,7 @@
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull;
+import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.Context;
@@ -88,7 +89,7 @@
AuthenticatedConnectionContext(final JsonValue savedContext, final PersistenceConfig config)
throws ResourceException {
super(savedContext, config);
- throw new InternalServerErrorException("Cached LDAP connections cannot be restored");
+ throw new InternalServerErrorException(i18n("Cached LDAP connections cannot be restored"));
}
/**
@@ -98,7 +99,7 @@
protected void saveToJson(final JsonValue savedContext, final PersistenceConfig config)
throws ResourceException {
super.saveToJson(savedContext, config);
- throw new InternalServerErrorException("Cached LDAP connections cannot be persisted");
+ throw new InternalServerErrorException(i18n("Cached LDAP connections cannot be persisted"));
}
/**
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java
index 50e5472..680797e 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java
+++ b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java
@@ -16,6 +16,7 @@
package org.forgerock.opendj.rest2ldap;
+import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.isJSONPrimitive;
import java.util.ArrayList;
@@ -53,8 +54,8 @@
DN.valueOf(authzId.substring(3), schema);
} catch (final IllegalArgumentException e) {
throw new ForbiddenException(
- "The request could not be authorized because the required security principal "
- + " was not a valid LDAP DN");
+ i18n("The request could not be authorized because the required "
+ + "security principal was not a valid LDAP DN"));
}
return authzId;
}
@@ -135,15 +136,13 @@
if (isJSONPrimitive(value)) {
values[i] = String.valueOf(value);
} else if (value == null) {
- // FIXME: i18n.
- throw new ForbiddenException(
- "The request could not be authorized because the required security principal "
- + key + " could not be determined");
+ throw new ForbiddenException(i18n(
+ "The request could not be authorized because the required "
+ + "security principal '%s' could not be determined", key));
} else {
- // FIXME: i18n.
- throw new ForbiddenException(
- "The request could not be authorized because the required security principal "
- + key + " had an invalid data type");
+ throw new ForbiddenException(i18n(
+ "The request could not be authorized because the required "
+ + "security principal '%s' had an invalid data type", key));
}
}
return values;
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
index cca57bd..101e53d 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
+++ b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
@@ -30,15 +30,17 @@
private final ReadOnUpdatePolicy readOnUpdatePolicy;
private final Schema schema;
private final boolean useSubtreeDelete;
+ private final boolean usePermissiveModify;
Config(final ConnectionFactory factory, final ReadOnUpdatePolicy readOnUpdatePolicy,
final AuthorizationPolicy authzPolicy, final AuthzIdTemplate proxiedAuthzTemplate,
- final boolean useSubtreeDelete, final Schema schema) {
+ final boolean useSubtreeDelete, final boolean usePermissiveModify, final Schema schema) {
this.factory = factory;
this.readOnUpdatePolicy = readOnUpdatePolicy;
this.authzPolicy = authzPolicy;
this.proxiedAuthzTemplate = proxiedAuthzTemplate;
this.useSubtreeDelete = useSubtreeDelete;
+ this.usePermissiveModify = usePermissiveModify;
this.schema = schema;
this.options = new DecodeOptions().setSchema(schema);
}
@@ -88,6 +90,17 @@
}
/**
+ * 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.
*
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
index 85c706a..e87bf66 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
+++ b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
@@ -27,9 +27,9 @@
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.ResultHandler;
-import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.Modification;
/**
* An attribute mapper which maps a single JSON attribute to a fixed value.
@@ -42,16 +42,17 @@
}
@Override
- void getLDAPAttributes(final Context c, final JsonPointer jsonAttribute,
+ void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath,
final Set<String> ldapAttributes) {
// Nothing to do.
}
@Override
- void getLDAPFilter(final Context c, final FilterType type, final JsonPointer jsonAttribute,
- final String operator, final Object valueAssertion, final ResultHandler<Filter> h) {
+ void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath,
+ final FilterType type, final String operator, final Object valueAssertion,
+ final ResultHandler<Filter> h) {
final Filter filter;
- final JsonValue subValue = value.get(jsonAttribute);
+ final JsonValue subValue = value.get(subPath);
if (subValue == null) {
filter = alwaysFalse();
} else if (type == FilterType.PRESENT) {
@@ -86,14 +87,16 @@
}
@Override
- void toJSON(final Context c, final Entry e, final ResultHandler<JsonValue> h) {
+ void toJSON(final Context c, final JsonPointer path, final Entry e,
+ final ResultHandler<JsonValue> h) {
h.handleResult(value.copy());
}
@Override
- void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) {
+ void toLDAP(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
+ final ResultHandler<List<Modification>> h) {
// FIXME: should we check if the provided value matches the constant?
- h.handleResult(Collections.<Attribute> emptyList());
+ h.handleResult(Collections.<Modification> emptyList());
}
private <T extends Comparable<T>> Filter compare(final Context c, final FilterType type,
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
index a3c6824..83f0215 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
+++ b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
@@ -17,9 +17,14 @@
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.rest2ldap.ReadOnUpdatePolicy.CONTROLS;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
import static org.forgerock.opendj.rest2ldap.Utils.accumulate;
+import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.toFilter;
import static org.forgerock.opendj.rest2ldap.Utils.transform;
@@ -39,8 +44,10 @@
import org.forgerock.json.resource.CollectionResourceProvider;
import org.forgerock.json.resource.CreateRequest;
import org.forgerock.json.resource.DeleteRequest;
+import org.forgerock.json.resource.InternalServerErrorException;
import org.forgerock.json.resource.NotSupportedException;
import org.forgerock.json.resource.PatchRequest;
+import org.forgerock.json.resource.PreconditionFailedException;
import org.forgerock.json.resource.QueryFilter;
import org.forgerock.json.resource.QueryFilterVisitor;
import org.forgerock.json.resource.QueryRequest;
@@ -61,16 +68,19 @@
import org.forgerock.opendj.ldap.ErrorResultException;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Function;
+import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.opendj.ldap.ModificationType;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.controls.AssertionRequestControl;
+import org.forgerock.opendj.ldap.controls.PermissiveModifyRequestControl;
import org.forgerock.opendj.ldap.controls.PostReadRequestControl;
import org.forgerock.opendj.ldap.controls.PostReadResponseControl;
import org.forgerock.opendj.ldap.controls.PreReadRequestControl;
import org.forgerock.opendj.ldap.controls.PreReadResponseControl;
import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl;
import org.forgerock.opendj.ldap.requests.AddRequest;
-import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
@@ -126,22 +136,31 @@
@Override
public void run() {
// Calculate entry content.
- attributeMapper.toLDAP(c, request.getContent(),
- new ResultHandler<List<Attribute>>() {
+ attributeMapper.toLDAP(c, new JsonPointer(), null, request.getContent(),
+ new ResultHandler<List<Modification>>() {
@Override
public void handleError(final ResourceException error) {
h.handleError(error);
}
@Override
- public void handleResult(final List<Attribute> result) {
+ public void handleResult(final List<Modification> result) {
// Perform add operation.
- final AddRequest addRequest = Requests.newAddRequest(DN.rootDN());
+ final AddRequest addRequest = newAddRequest(DN.rootDN());
for (final Attribute attribute : additionalLDAPAttributes) {
addRequest.addAttribute(attribute);
}
- for (final Attribute attribute : result) {
- addRequest.addAttribute(attribute);
+ for (final Modification modification : result) {
+ if (modification.getModificationType() == ModificationType.ADD
+ || modification.getModificationType() == ModificationType.REPLACE) {
+ addRequest.addAttribute(modification.getAttribute());
+ } else {
+ // Attribute mappers must return add/replace updates.
+ h.handleError(new InternalServerErrorException(
+ i18n("Attribute mapper returned a modification which "
+ + "does not add an attribute")));
+ return;
+ }
}
try {
nameStrategy.setResourceId(c, getBaseDN(c), request
@@ -175,9 +194,12 @@
@Override
public void run() {
// Find the entry and then delete it.
+ final String ldapAttribute =
+ (etagAttribute != null && request.getRevision() != null) ? etagAttribute
+ .toString() : "1.1";
final SearchRequest searchRequest =
nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId).addAttribute(
- "1.1");
+ ldapAttribute);
c.getConnection().searchSingleEntryAsync(searchRequest,
new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() {
@Override
@@ -188,9 +210,12 @@
@Override
public void handleResult(final SearchResultEntry entry) {
try {
+ // Fail-fast if there is a version mismatch.
+ ensureMVCCVersionMatches(entry, request.getRevision());
+
// Perform delete operation.
final ChangeRecord deleteRequest =
- Requests.newDeleteRequest(entry.getName());
+ newDeleteRequest(entry.getName());
if (config.readOnUpdatePolicy() == CONTROLS) {
final String[] attributes =
getLDAPAttributes(c, request.getFields());
@@ -225,7 +250,10 @@
final Context c = wrap(context);
final QueryResultHandler h = wrap(c, handler);
- // Get the connection, then calculate the search filter, then perform the search.
+ /*
+ * Get the connection, then calculate the search filter, then perform
+ * the search.
+ */
c.run(h, new Runnable() {
@Override
public void run() {
@@ -238,17 +266,19 @@
@Override
public void handleResult(final Filter ldapFilter) {
- // Avoid performing a search if the filter could not be mapped or if it will never match.
+ /*
+ * Avoid performing a search if the filter could not be
+ * mapped or if it will never match.
+ */
if (ldapFilter == null || ldapFilter == alwaysFalse()) {
h.handleResult(new QueryResult());
} else {
// Perform the search.
final String[] attributes = getLDAPAttributes(c, request.getFields());
final SearchRequest request =
- Requests.newSearchRequest(getBaseDN(c),
- SearchScope.SINGLE_LEVEL, ldapFilter == Filter
- .alwaysTrue() ? Filter.objectClassPresent()
- : ldapFilter, attributes);
+ newSearchRequest(getBaseDN(c), SearchScope.SINGLE_LEVEL,
+ ldapFilter == Filter.alwaysTrue() ? Filter
+ .objectClassPresent() : ldapFilter, attributes);
c.getConnection().searchAsync(request, null, new SearchResultHandler() {
private final AtomicInteger pendingResourceCount =
new AtomicInteger();
@@ -291,7 +321,7 @@
};
pendingResourceCount.incrementAndGet();
- attributeMapper.toJSON(c, entry, mapHandler);
+ attributeMapper.toJSON(c, new JsonPointer(), entry, mapHandler);
return true;
}
@@ -373,31 +403,132 @@
@Override
public void updateInstance(final ServerContext context, final String resourceId,
final UpdateRequest request, final ResultHandler<Resource> handler) {
- handler.handleError(new NotSupportedException("Not yet implemented"));
+ /*
+ * Update operations are a bit awkward because there is not direct
+ * mapping to LDAP. We need to convert the update request into an LDAP
+ * modify operation which means reading the current LDAP entry,
+ * generating the new entry content, then comparing the two in order to
+ * obtain a set of changes. We also need to handle read-only fields
+ * correctly: if a read-only field is included with the new resource
+ * then it must match exactly the value of the existing field.
+ */
+ final Context c = wrap(context);
+ final ResultHandler<Resource> h = wrap(c, handler);
+
+ // Get connection then perform the search.
+ c.run(h, new Runnable() {
+ @Override
+ public void run() {
+ // First of all read the existing entry.
+ final String[] attributes =
+ getLDAPAttributes(c, Collections.<JsonPointer> emptyList());
+ final SearchRequest searchRequest =
+ nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId).addAttribute(
+ attributes);
+ c.getConnection().searchSingleEntryAsync(searchRequest,
+ new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() {
+ @Override
+ public void handleErrorResult(final ErrorResultException error) {
+ h.handleError(asResourceException(error));
+ }
+
+ @Override
+ public void handleResult(final SearchResultEntry entry) {
+ try {
+ // Fail-fast if there is a version mismatch.
+ ensureMVCCVersionMatches(entry, request.getRevision());
+
+ // Create the modify request.
+ final ModifyRequest modifyRequest =
+ newModifyRequest(entry.getName());
+ if (config.readOnUpdatePolicy() == CONTROLS) {
+ final String[] attributes =
+ getLDAPAttributes(c, request.getFields());
+ modifyRequest.addControl(PostReadRequestControl.newControl(
+ false, attributes));
+ }
+ if (config.usePermissiveModify()) {
+ modifyRequest.addControl(PermissiveModifyRequestControl
+ .newControl(true));
+ }
+ addAssertionControl(modifyRequest, request.getRevision());
+
+ /*
+ * Determine the set of changes that need to
+ * be performed.
+ */
+ attributeMapper.toLDAP(c, new JsonPointer(), entry, request
+ .getNewContent(),
+ new ResultHandler<List<Modification>>() {
+ @Override
+ public void handleError(
+ final ResourceException error) {
+ h.handleError(error);
+ }
+
+ @Override
+ public void handleResult(
+ final List<Modification> result) {
+ // Perform the modify operation.
+ modifyRequest.getModifications().addAll(result);
+ c.getConnection().applyChangeAsync(
+ modifyRequest, null,
+ postUpdateHandler(c, h));
+ }
+ });
+ } catch (final Exception e) {
+ h.handleError(asResourceException(e));
+ }
+ }
+ });
+ }
+ });
}
private void adaptEntry(final Context c, final Entry entry,
final ResultHandler<Resource> handler) {
final String actualResourceId = nameStrategy.getResourceId(c, entry);
final String revision = getRevisionFromEntry(entry);
- attributeMapper.toJSON(c, entry, transform(new Function<JsonValue, Resource, Void>() {
- @Override
- public Resource apply(final JsonValue value, final Void p) {
- return new Resource(actualResourceId, revision, new JsonValue(value));
- }
- }, handler));
+ attributeMapper.toJSON(c, new JsonPointer(), entry, transform(
+ new Function<JsonValue, Resource, Void>() {
+ @Override
+ public Resource apply(final JsonValue value, final Void p) {
+ return new Resource(actualResourceId, revision, new JsonValue(value));
+ }
+ }, handler));
}
- private void addAssertionControl(final ChangeRecord request, final String revision)
- throws NotSupportedException {
- if (revision != null) {
- if (etagAttribute != null) {
- request.addControl(AssertionRequestControl.newControl(true, Filter.equality(
- etagAttribute.toString(), revision)));
- } else {
- // FIXME: i18n
- throw new NotSupportedException(
- "Multi-version concurrency control is not supported by this resource");
+ private void addAssertionControl(final ChangeRecord request, final String expectedRevision)
+ throws ResourceException {
+ if (expectedRevision != null) {
+ ensureMVCCSupported();
+ request.addControl(AssertionRequestControl.newControl(true, Filter.equality(
+ etagAttribute.toString(), expectedRevision)));
+ }
+ }
+
+ private void ensureMVCCSupported() throws NotSupportedException {
+ if (etagAttribute == null) {
+ throw new NotSupportedException(
+ i18n("Multi-version concurrency control is not supported by this resource"));
+ }
+ }
+
+ private void ensureMVCCVersionMatches(final Entry entry, final String expectedRevision)
+ throws ResourceException {
+ if (expectedRevision != null) {
+ ensureMVCCSupported();
+ final String actualRevision = entry.parseAttribute(etagAttribute).asString();
+ if (actualRevision == null) {
+ throw new PreconditionFailedException(i18n(
+ "The resource could not be accessed because it did not contain any "
+ + "version information, when the version '%s' was expected",
+ expectedRevision));
+ } else if (!expectedRevision.equals(actualRevision)) {
+ throw new PreconditionFailedException(i18n(
+ "The resource could not be accessed because the expected version '%s' "
+ + "does not match the current version '%s'", expectedRevision,
+ actualRevision));
}
}
}
@@ -422,12 +553,14 @@
if (requestedAttributes.isEmpty()) {
// Full read.
requestedLDAPAttributes = new LinkedHashSet<String>();
- attributeMapper.getLDAPAttributes(c, new JsonPointer(), requestedLDAPAttributes);
+ attributeMapper.getLDAPAttributes(c, new JsonPointer(), new JsonPointer(),
+ requestedLDAPAttributes);
} else {
// Partial read.
requestedLDAPAttributes = new LinkedHashSet<String>(requestedAttributes.size());
for (final JsonPointer requestedAttribute : requestedAttributes) {
- attributeMapper.getLDAPAttributes(c, requestedAttribute, requestedLDAPAttributes);
+ attributeMapper.getLDAPAttributes(c, new JsonPointer(), requestedAttribute,
+ requestedLDAPAttributes);
}
}
@@ -488,16 +621,16 @@
@Override
public Void visitContainsFilter(final ResultHandler<Filter> p,
final JsonPointer field, final Object valueAssertion) {
- attributeMapper.getLDAPFilter(c, FilterType.CONTAINS, field, null,
- valueAssertion, p);
+ attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
+ FilterType.CONTAINS, null, valueAssertion, p);
return null;
}
@Override
public Void visitEqualsFilter(final ResultHandler<Filter> p,
final JsonPointer field, final Object valueAssertion) {
- attributeMapper.getLDAPFilter(c, FilterType.EQUAL_TO, field, null,
- valueAssertion, p);
+ attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
+ FilterType.EQUAL_TO, null, valueAssertion, p);
return null;
}
@@ -505,40 +638,40 @@
public Void visitExtendedMatchFilter(final ResultHandler<Filter> p,
final JsonPointer field, final String operator,
final Object valueAssertion) {
- attributeMapper.getLDAPFilter(c, FilterType.EXTENDED, field, operator,
- valueAssertion, p);
+ attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
+ FilterType.EXTENDED, operator, valueAssertion, p);
return null;
}
@Override
public Void visitGreaterThanFilter(final ResultHandler<Filter> p,
final JsonPointer field, final Object valueAssertion) {
- attributeMapper.getLDAPFilter(c, FilterType.GREATER_THAN, field, null,
- valueAssertion, p);
+ attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
+ FilterType.GREATER_THAN, null, valueAssertion, p);
return null;
}
@Override
public Void visitGreaterThanOrEqualToFilter(final ResultHandler<Filter> p,
final JsonPointer field, final Object valueAssertion) {
- attributeMapper.getLDAPFilter(c, FilterType.GREATER_THAN_OR_EQUAL_TO,
- field, null, valueAssertion, p);
+ attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
+ FilterType.GREATER_THAN_OR_EQUAL_TO, null, valueAssertion, p);
return null;
}
@Override
public Void visitLessThanFilter(final ResultHandler<Filter> p,
final JsonPointer field, final Object valueAssertion) {
- attributeMapper.getLDAPFilter(c, FilterType.LESS_THAN, field, null,
- valueAssertion, p);
+ attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
+ FilterType.LESS_THAN, null, valueAssertion, p);
return null;
}
@Override
public Void visitLessThanOrEqualToFilter(final ResultHandler<Filter> p,
final JsonPointer field, final Object valueAssertion) {
- attributeMapper.getLDAPFilter(c, FilterType.LESS_THAN_OR_EQUAL_TO, field,
- null, valueAssertion, p);
+ attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
+ FilterType.LESS_THAN_OR_EQUAL_TO, null, valueAssertion, p);
return null;
}
@@ -598,20 +731,24 @@
@Override
public Void visitPresentFilter(final ResultHandler<Filter> p,
final JsonPointer field) {
- attributeMapper.getLDAPFilter(c, FilterType.PRESENT, field, null, null, p);
+ attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
+ FilterType.PRESENT, null, null, p);
return null;
}
@Override
public Void visitStartsWithFilter(final ResultHandler<Filter> p,
final JsonPointer field, final Object valueAssertion) {
- attributeMapper.getLDAPFilter(c, FilterType.STARTS_WITH, field, null,
- valueAssertion, p);
+ attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
+ FilterType.STARTS_WITH, null, valueAssertion, p);
return null;
}
};
- // Note that the returned LDAP filter may be null if it could not be mapped by any attribute mappers.
+ /*
+ * Note that the returned LDAP filter may be null if it could not be
+ * mapped by any attribute mappers.
+ */
queryFilter.accept(visitor, h);
}
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
index 0251974..d4ba83f 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
+++ b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
@@ -17,6 +17,7 @@
import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
import static org.forgerock.opendj.rest2ldap.Utils.accumulate;
+import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.toLowerCase;
import static org.forgerock.opendj.rest2ldap.Utils.transform;
@@ -32,10 +33,10 @@
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.ResultHandler;
-import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Function;
+import org.forgerock.opendj.ldap.Modification;
/**
* An attribute mapper which maps JSON objects to LDAP attributes.
@@ -50,6 +51,11 @@
this.name = name;
this.mapper = mapper;
}
+
+ @Override
+ public String toString() {
+ return name + " -> " + mapper;
+ }
}
private final Map<String, Mapping> mappings = new LinkedHashMap<String, Mapping>();
@@ -74,43 +80,50 @@
}
@Override
- void getLDAPAttributes(final Context c, final JsonPointer jsonAttribute,
+ void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath,
final Set<String> ldapAttributes) {
- if (jsonAttribute.isEmpty()) {
+ if (subPath.isEmpty()) {
// Request all subordinate mappings.
for (final Mapping mapping : mappings.values()) {
- mapping.mapper.getLDAPAttributes(c, jsonAttribute, ldapAttributes);
+ mapping.mapper.getLDAPAttributes(c, path.child(mapping.name), subPath,
+ ldapAttributes);
}
} else {
// Request single subordinate mapping.
- final Mapping mapping = getMapping(jsonAttribute);
+ final Mapping mapping = getMapping(subPath);
if (mapping != null) {
- final JsonPointer relativePointer = jsonAttribute.relativePointer();
- mapping.mapper.getLDAPAttributes(c, relativePointer, ldapAttributes);
+ mapping.mapper.getLDAPAttributes(c, path.child(subPath.get(0)), subPath
+ .relativePointer(), ldapAttributes);
}
}
}
@Override
- void getLDAPFilter(final Context c, final FilterType type, final JsonPointer jsonAttribute,
- final String operator, final Object valueAssertion, final ResultHandler<Filter> h) {
- final Mapping mapping = getMapping(jsonAttribute);
+ void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath,
+ final FilterType type, final String operator, final Object valueAssertion,
+ final ResultHandler<Filter> h) {
+ final Mapping mapping = getMapping(subPath);
if (mapping != null) {
- final JsonPointer relativePointer = jsonAttribute.relativePointer();
- mapping.mapper.getLDAPFilter(c, type, relativePointer, operator, valueAssertion, h);
+ mapping.mapper.getLDAPFilter(c, path.child(subPath.get(0)), subPath.relativePointer(),
+ type, operator, valueAssertion, h);
} 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.
+ /*
+ * 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.
+ */
h.handleResult(alwaysFalse());
}
}
@Override
- void toJSON(final Context c, final Entry e, final ResultHandler<JsonValue> h) {
- // 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.
+ void toJSON(final Context c, final JsonPointer path, final Entry e,
+ final ResultHandler<JsonValue> h) {
+ /*
+ * 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 ResultHandler<Map.Entry<String, JsonValue>> handler =
accumulate(mappings.size(), transform(
new Function<List<Map.Entry<String, JsonValue>>, JsonValue, Void>() {
@@ -118,11 +131,16 @@
public JsonValue apply(final List<Map.Entry<String, JsonValue>> value,
final Void p) {
if (value.isEmpty()) {
- // No subordinate attributes, so omit the entire JSON object
- // from the resource.
+ /*
+ * No subordinate attributes, so omit the
+ * entire JSON object from the resource.
+ */
return null;
} else {
- // Combine the sub-attributes into a single JSON object.
+ /*
+ * Combine the sub-attributes into a single
+ * JSON object.
+ */
final Map<String, Object> result =
new LinkedHashMap<String, Object>(value.size());
for (final Map.Entry<String, JsonValue> e : value) {
@@ -134,7 +152,7 @@
}, h));
for (final Mapping mapping : mappings.values()) {
- mapping.mapper.toJSON(c, e, transform(
+ mapping.mapper.toJSON(c, path.child(mapping.name), e, transform(
new Function<JsonValue, Map.Entry<String, JsonValue>, Void>() {
@Override
public Map.Entry<String, JsonValue> apply(final JsonValue value,
@@ -147,30 +165,39 @@
}
@Override
- void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) {
- // Fail immediately if the JSON value has the wrong type or contains unknown attributes.
+ void toLDAP(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
+ final ResultHandler<List<Modification>> h) {
+ /*
+ * Fail immediately if the JSON value has the wrong type or contains
+ * unknown attributes.
+ */
final Map<String, Mapping> missingMappings = new LinkedHashMap<String, Mapping>(mappings);
if (v != null && !v.isNull()) {
if (v.isMap()) {
for (final String attribute : v.asMap().keySet()) {
if (missingMappings.remove(toLowerCase(attribute)) == null) {
- h.handleError(new BadRequestException("unrecognized attribute '"
- + attribute + "'"));
+ h.handleError(new BadRequestException(i18n(
+ "The request cannot be processed because the JSON resource "
+ + "contains an unrecognized field '%s'", path
+ .child(attribute))));
return;
}
}
} else {
- h.handleError(new BadRequestException("JSON object expected"));
+ h.handleError(new BadRequestException(i18n(
+ "The request cannot be processed because the JSON resource "
+ + "contains the field '%s' whose value is the wrong type: "
+ + "an object is expected", path)));
return;
}
}
// Accumulate the results of the subordinate mappings.
- final ResultHandler<List<Attribute>> handler =
+ final ResultHandler<List<Modification>> handler =
accumulate(mappings.size(), transform(
- new Function<List<List<Attribute>>, List<Attribute>, Void>() {
+ new Function<List<List<Modification>>, List<Modification>, Void>() {
@Override
- public List<Attribute> apply(final List<List<Attribute>> value,
+ public List<Modification> apply(final List<List<Modification>> value,
final Void p) {
switch (value.size()) {
case 0:
@@ -178,9 +205,9 @@
case 1:
return value.get(0);
default:
- final List<Attribute> attributes =
- new ArrayList<Attribute>(value.size());
- for (final List<Attribute> a : value) {
+ final List<Modification> attributes =
+ new ArrayList<Modification>(value.size());
+ for (final List<Modification> a : value) {
attributes.addAll(a);
}
return attributes;
@@ -190,16 +217,16 @@
// Invoke mappings for which there are values provided.
if (v != null && !v.isNull()) {
- for (final Map.Entry<String, Object> e : v.asMap().entrySet()) {
- final Mapping mapping = getMapping(e.getKey());
- final JsonValue subValue = new JsonValue(e.getValue());
- mapping.mapper.toLDAP(c, subValue, handler);
+ for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
+ final Mapping mapping = getMapping(me.getKey());
+ final JsonValue subValue = new JsonValue(me.getValue());
+ mapping.mapper.toLDAP(c, path.child(me.getKey()), e, subValue, handler);
}
}
// Invoke mappings for which there were no values provided.
for (final Mapping mapping : missingMappings.values()) {
- mapping.mapper.toLDAP(c, null, handler);
+ mapping.mapper.toLDAP(c, path.child(mapping.name), e, null, handler);
}
}
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
index 51b3a09..3ac297f 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
+++ b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
@@ -15,16 +15,15 @@
*/
package org.forgerock.opendj.rest2ldap;
-import static java.util.Collections.singletonList;
import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
+import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
import static org.forgerock.opendj.rest2ldap.Utils.accumulate;
import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull;
+import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.transform;
-import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
@@ -39,6 +38,7 @@
import org.forgerock.json.resource.ResultHandler;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.Attributes;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
@@ -47,11 +47,11 @@
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Function;
import org.forgerock.opendj.ldap.LinkedAttribute;
+import org.forgerock.opendj.ldap.Modification;
import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.SearchScope;
-import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
@@ -61,7 +61,8 @@
* An attribute mapper which provides a mapping from a JSON value to a single DN
* valued LDAP attribute.
*/
-public final class ReferenceAttributeMapper extends AttributeMapper {
+public final class ReferenceAttributeMapper extends
+ AbstractLDAPAttributeMapper<ReferenceAttributeMapper> {
/**
* The maximum number of candidate references to allow in search filters.
*/
@@ -69,45 +70,19 @@
private final DN baseDN;
private Filter filter = null;
- private boolean isRequired = false;
- private boolean isSingleValued = false;
- private final AttributeDescription ldapAttributeName;
private final AttributeMapper mapper;
private final AttributeDescription primaryKey;
private SearchScope scope = SearchScope.WHOLE_SUBTREE;
- private WritabilityPolicy writabilityPolicy = READ_WRITE;
ReferenceAttributeMapper(final AttributeDescription ldapAttributeName, final DN baseDN,
final AttributeDescription primaryKey, final AttributeMapper mapper) {
- this.ldapAttributeName = ldapAttributeName;
+ super(ldapAttributeName);
this.baseDN = baseDN;
this.primaryKey = primaryKey;
this.mapper = mapper;
}
/**
- * Indicates that the LDAP attribute is mandatory and must be provided
- * during create requests.
- *
- * @return This attribute mapper.
- */
- public ReferenceAttributeMapper isRequired() {
- this.isRequired = true;
- return this;
- }
-
- /**
- * Indicates that multi-valued LDAP attribute should be represented as a
- * single-valued JSON value, rather than an array of values.
- *
- * @return This attribute mapper.
- */
- public ReferenceAttributeMapper isSingleValued() {
- this.isSingleValued = true;
- return this;
- }
-
- /**
* Sets the filter which should be used when searching for referenced LDAP
* entries. The default is {@code (objectClass=*)}.
*
@@ -148,30 +123,12 @@
return this;
}
- /**
- * Indicates whether or not the LDAP attribute supports updates. The default
- * is {@link WritabilityPolicy#READ_WRITE}.
- *
- * @param policy
- * The writability policy.
- * @return This attribute mapper.
- */
- public ReferenceAttributeMapper writability(final WritabilityPolicy policy) {
- this.writabilityPolicy = policy;
- return this;
- }
-
@Override
- void getLDAPAttributes(final Context c, final JsonPointer jsonAttribute,
- final Set<String> ldapAttributes) {
- ldapAttributes.add(ldapAttributeName.toString());
- }
-
- @Override
- void getLDAPFilter(final Context c, final FilterType type, final JsonPointer jsonAttribute,
- final String operator, final Object valueAssertion, final ResultHandler<Filter> h) {
+ void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath,
+ final FilterType type, final String operator, final Object valueAssertion,
+ final ResultHandler<Filter> h) {
// Construct a filter which can be used to find referenced resources.
- mapper.getLDAPFilter(c, type, jsonAttribute, operator, valueAssertion,
+ mapper.getLDAPFilter(c, path, subPath, type, operator, valueAssertion,
new ResultHandler<Filter>() {
@Override
public void handleError(final ResourceException error) {
@@ -224,86 +181,27 @@
}
@Override
- void toJSON(final Context c, final Entry e, final ResultHandler<JsonValue> h) {
- final Attribute attribute = e.getAttribute(ldapAttributeName);
- if (attribute == null || attribute.isEmpty()) {
- h.handleResult(null);
- } else if (attributeIsSingleValued()) {
- try {
- final DN dn = attribute.parse().usingSchema(c.getConfig().schema()).asDN();
- readEntry(c, dn, h);
- } catch (final Exception ex) {
- // The LDAP attribute could not be decoded.
- h.handleError(asResourceException(ex));
- }
- } else {
- try {
- final Set<DN> dns =
- attribute.parse().usingSchema(c.getConfig().schema()).asSetOfDN();
- final ResultHandler<JsonValue> handler =
- accumulate(dns.size(), transform(
- new Function<List<JsonValue>, JsonValue, Void>() {
- @Override
- public JsonValue apply(final List<JsonValue> value, final Void p) {
- if (value.isEmpty()) {
- // No values, so omit the entire JSON object from the resource.
- return null;
- } else {
- // Combine values into a single JSON array.
- final List<Object> result =
- new ArrayList<Object>(value.size());
- for (final JsonValue e : value) {
- result.add(e.getObject());
- }
- return new JsonValue(result);
- }
- }
- }, h));
- for (final DN dn : dns) {
- readEntry(c, dn, handler);
- }
- } catch (final Exception ex) {
- // The LDAP attribute could not be decoded.
- h.handleError(asResourceException(ex));
- }
+ void getNewLDAPAttributes(final Context c, final JsonPointer path,
+ final List<Object> newValues, final ResultHandler<Attribute> h) {
+ // No need to do anything if there are no values.
+ if (newValues.isEmpty()) {
+ h.handleResult(Attributes.emptyAttribute(ldapAttributeName));
+ return;
}
- }
- @Override
- void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) {
- try {
- if (v == null || v.isNull()) {
- if (attributeIsRequired()) {
- // FIXME: improve error message.
- throw new BadRequestException("no value provided");
- } else {
- h.handleResult(Collections.<Attribute> emptyList());
- }
- } else if (v.isList() && attributeIsSingleValued()) {
- // FIXME: improve error message.
- throw new BadRequestException("expected single value, but got multiple values");
- } else if (!writabilityPolicy.canCreate(ldapAttributeName)) {
- if (writabilityPolicy.discardWrites()) {
- h.handleResult(Collections.<Attribute> emptyList());
- } else {
- // FIXME: improve error message.
- throw new BadRequestException("attempted to create a read-only value");
- }
- } else {
- /*
- * For each value use the subordinate mapper to obtain the LDAP
- * primary key, the perform a search for each one to find the
- * corresponding entries.
- */
- final JsonValue valueList =
- v.isList() ? v : new JsonValue(singletonList(v.getObject()));
- final Attribute reference = new LinkedAttribute(ldapAttributeName);
- final AtomicInteger pendingSearches = new AtomicInteger(valueList.size());
- final AtomicReference<ResourceException> exception =
- new AtomicReference<ResourceException>();
+ /*
+ * For each value use the subordinate mapper to obtain the LDAP primary
+ * key, the perform a search for each one to find the corresponding
+ * entries.
+ */
+ final Attribute newLDAPAttribute = new LinkedAttribute(ldapAttributeName);
+ final AtomicInteger pendingSearches = new AtomicInteger(newValues.size());
+ final AtomicReference<ResourceException> exception =
+ new AtomicReference<ResourceException>();
- for (final JsonValue value : valueList) {
- mapper.toLDAP(c, value, new ResultHandler<List<Attribute>>() {
+ for (final Object value : newValues) {
+ mapper.toLDAP(c, path, null /* force create */, new JsonValue(value),
+ new ResultHandler<List<Modification>>() {
@Override
public void handleError(final ResourceException error) {
@@ -311,32 +209,29 @@
}
@Override
- public void handleResult(final List<Attribute> result) {
+ public void handleResult(final List<Modification> result) {
Attribute primaryKeyAttribute = null;
- for (final Attribute attribute : result) {
- if (attribute.getAttributeDescription().equals(primaryKey)) {
- primaryKeyAttribute = attribute;
+ for (final Modification modification : result) {
+ if (modification.getAttribute().getAttributeDescription().equals(
+ primaryKey)) {
+ primaryKeyAttribute = modification.getAttribute();
break;
}
}
- if (primaryKeyAttribute == null) {
- // FIXME: improve error message.
- h.handleError(new BadRequestException(
- "reference primary key attribute is missing"));
- return;
- }
- if (primaryKeyAttribute.isEmpty()) {
- // FIXME: improve error message.
+ if (primaryKeyAttribute == null || primaryKeyAttribute.isEmpty()) {
h.handleError(new BadRequestException(
- "reference primary key attribute is empty"));
+ i18n("The request cannot be processed because the reference "
+ + "field '%s' contains a value which does not contain "
+ + "a primary key", path)));
return;
}
if (primaryKeyAttribute.size() > 1) {
- // FIXME: improve error message.
h.handleError(new BadRequestException(
- "reference primary key attribute contains multiple values"));
+ i18n("The request cannot be processed because the reference "
+ + "field '%s' contains a value which contains multiple "
+ + "primary keys", path)));
return;
}
@@ -356,7 +251,7 @@
ResourceException re;
try {
throw error;
- } catch (EntryNotFoundException e) {
+ } catch (final EntryNotFoundException e) {
// FIXME: improve error message.
re =
new BadRequestException(
@@ -364,7 +259,7 @@
+ primaryKeyValue
.toString()
+ "' does not exist");
- } catch (MultipleEntriesFoundException e) {
+ } catch (final MultipleEntriesFoundException e) {
// FIXME: improve error message.
re =
new BadRequestException(
@@ -372,7 +267,7 @@
+ primaryKeyValue
.toString()
+ "' is ambiguous");
- } catch (ErrorResultException e) {
+ } catch (final ErrorResultException e) {
re = asResourceException(e);
}
exception.compareAndSet(null, re);
@@ -382,8 +277,8 @@
@Override
public void handleResult(
final SearchResultEntry result) {
- synchronized (reference) {
- reference.add(result.getName());
+ synchronized (newLDAPAttribute) {
+ newLDAPAttribute.add(result.getName());
}
completeIfNecessary();
}
@@ -393,40 +288,80 @@
private void completeIfNecessary() {
if (pendingSearches.decrementAndGet() == 0) {
if (exception.get() == null) {
- h.handleResult(singletonList(reference));
+ h.handleResult(newLDAPAttribute);
} else {
h.handleError(exception.get());
}
}
}
});
- }
- }
- } catch (final ResourceException e) {
- h.handleError(e);
- } catch (final Exception e) {
- // FIXME: improve error message.
- h.handleError(new BadRequestException(e.getMessage()));
}
}
- private boolean attributeIsRequired() {
- return isRequired;
+ @Override
+ ReferenceAttributeMapper getThis() {
+ return this;
}
- private boolean attributeIsSingleValued() {
- return isSingleValued || ldapAttributeName.getAttributeType().isSingleValue();
+ @Override
+ void toJSON(final Context c, final JsonPointer path, final Entry e,
+ final ResultHandler<JsonValue> h) {
+ final Attribute attribute = e.getAttribute(ldapAttributeName);
+ if (attribute == null || attribute.isEmpty()) {
+ h.handleResult(null);
+ } else if (attributeIsSingleValued()) {
+ try {
+ final DN dn = attribute.parse().usingSchema(c.getConfig().schema()).asDN();
+ readEntry(c, path, dn, h);
+ } catch (final Exception ex) {
+ // The LDAP attribute could not be decoded.
+ h.handleError(asResourceException(ex));
+ }
+ } else {
+ try {
+ final Set<DN> dns =
+ attribute.parse().usingSchema(c.getConfig().schema()).asSetOfDN();
+ final ResultHandler<JsonValue> handler =
+ accumulate(dns.size(), transform(
+ new Function<List<JsonValue>, JsonValue, Void>() {
+ @Override
+ public JsonValue apply(final List<JsonValue> value, final Void p) {
+ if (value.isEmpty()) {
+ /*
+ * No values, so omit the entire
+ * JSON object from the resource.
+ */
+ return null;
+ } else {
+ // Combine values into a single JSON array.
+ final List<Object> result =
+ new ArrayList<Object>(value.size());
+ for (final JsonValue e : value) {
+ result.add(e.getObject());
+ }
+ return new JsonValue(result);
+ }
+ }
+ }, h));
+ for (final DN dn : dns) {
+ readEntry(c, path, dn, handler);
+ }
+ } catch (final Exception ex) {
+ // The LDAP attribute could not be decoded.
+ h.handleError(asResourceException(ex));
+ }
+ }
}
private SearchRequest createSearchRequest(final Filter result) {
final Filter searchFilter = filter != null ? Filter.and(filter, result) : result;
- final SearchRequest request = Requests.newSearchRequest(baseDN, scope, searchFilter, "1.1");
- return request;
+ return newSearchRequest(baseDN, scope, searchFilter, "1.1");
}
- private void readEntry(final Context c, final DN dn, final ResultHandler<JsonValue> handler) {
+ private void readEntry(final Context c, final JsonPointer path, final DN dn,
+ final ResultHandler<JsonValue> handler) {
final Set<String> requestedLDAPAttributes = new LinkedHashSet<String>();
- mapper.getLDAPAttributes(c, new JsonPointer(), requestedLDAPAttributes);
+ mapper.getLDAPAttributes(c, path, new JsonPointer(), requestedLDAPAttributes);
c.getConnection().readEntryAsync(dn, requestedLDAPAttributes,
new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() {
@@ -435,14 +370,17 @@
if (!(error instanceof EntryNotFoundException)) {
handler.handleError(asResourceException(error));
} else {
- // The referenced entry does not exist so ignore it since it cannot be mapped.
+ /*
+ * The referenced entry does not exist so ignore it
+ * since it cannot be mapped.
+ */
handler.handleResult(null);
}
}
@Override
public void handleResult(final SearchResultEntry result) {
- mapper.toJSON(c, result, handler);
+ mapper.toJSON(c, path, result, handler);
}
});
}
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
index 7add0d5..9de51bd 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
+++ b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
@@ -44,6 +44,7 @@
import org.forgerock.opendj.ldap.ConnectionException;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.Connections;
+import org.forgerock.opendj.ldap.ConstraintViolationException;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.EntryNotFoundException;
@@ -86,6 +87,7 @@
private AttributeMapper rootMapper;
private Schema schema = Schema.getDefaultSchema();
private boolean useSubtreeDelete;
+ private boolean usePermissiveModify;
Builder() {
useEtagAttribute();
@@ -144,7 +146,7 @@
}
return new LDAPCollectionResourceProvider(baseDN, rootMapper, nameStrategy,
etagAttribute, new Config(factory, readOnUpdatePolicy, authzPolicy,
- proxiedAuthzTemplate, useSubtreeDelete, schema),
+ proxiedAuthzTemplate, useSubtreeDelete, usePermissiveModify, schema),
additionalLDAPAttributes);
}
@@ -196,10 +198,14 @@
useEtagAttribute(etagAttribute.asString());
}
- if (configuration.get("useSubtreeDelete").required().asBoolean()) {
+ if (configuration.get("useSubtreeDelete").defaultTo(false).asBoolean()) {
useSubtreeDelete();
}
+ if (configuration.get("usePermissiveModify").defaultTo(false).asBoolean()) {
+ usePermissiveModify();
+ }
+
mapper(configureObjectMapper(configuration.get("attributes").required()));
return this;
@@ -305,6 +311,11 @@
return this;
}
+ public Builder usePermissiveModify() {
+ this.usePermissiveModify = true;
+ return this;
+ }
+
private AttributeDescription ad(final String attribute) {
return AttributeDescription.valueOf(attribute, schema);
}
@@ -323,9 +334,6 @@
if (config.isDefined("defaultJSONValue")) {
s.defaultJSONValue(config.get("defaultJSONValue").getObject());
}
- if (config.isDefined("defaultLDAPValue")) {
- s.defaultLDAPValue(config.get("defaultLDAPValue").getObject());
- }
if (config.get("isBinary").defaultTo(false).asBoolean()) {
s.isBinary();
}
@@ -507,6 +515,14 @@
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) {
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
index 4706fbe..b59180f 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
+++ b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
@@ -16,8 +16,6 @@
package org.forgerock.opendj.rest2ldap;
import static java.util.Collections.emptyList;
-import static java.util.Collections.emptySet;
-import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
import static org.forgerock.opendj.ldap.Functions.fixedFunction;
@@ -25,20 +23,17 @@
import static org.forgerock.opendj.rest2ldap.Utils.base64ToByteString;
import static org.forgerock.opendj.rest2ldap.Utils.byteStringToBase64;
import static org.forgerock.opendj.rest2ldap.Utils.byteStringToJson;
+import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.jsonToAttribute;
import static org.forgerock.opendj.rest2ldap.Utils.jsonToByteString;
import static org.forgerock.opendj.rest2ldap.Utils.toFilter;
-import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE;
-import java.util.Collection;
-import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.BadRequestException;
-import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResultHandler;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.AttributeDescription;
@@ -46,25 +41,18 @@
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Function;
-import org.forgerock.opendj.ldap.LinkedAttribute;
/**
* An attribute mapper which provides a simple mapping from a JSON value to a
* single LDAP attribute.
*/
-public final class SimpleAttributeMapper extends AttributeMapper {
+public final class SimpleAttributeMapper extends AbstractLDAPAttributeMapper<SimpleAttributeMapper> {
private Function<ByteString, ?, Void> decoder = null;
- private Object defaultJSONValue = null;
- private Collection<Object> defaultJSONValues = Collections.emptySet();
- private ByteString defaultLDAPValue = null;
+
private Function<Object, ByteString, Void> encoder = null;
- private boolean isRequired = false;
- private boolean isSingleValued = false;
- private final AttributeDescription ldapAttributeName;
- private WritabilityPolicy writabilityPolicy = READ_WRITE;
SimpleAttributeMapper(final AttributeDescription ldapAttributeName) {
- this.ldapAttributeName = ldapAttributeName;
+ super(ldapAttributeName);
}
/**
@@ -90,20 +78,7 @@
*/
public SimpleAttributeMapper defaultJSONValue(final Object defaultValue) {
this.defaultJSONValue = defaultValue;
- this.defaultJSONValues = defaultValue != null ? singleton(defaultValue) : emptySet();
- return this;
- }
-
- /**
- * Sets the default LDAP value which should be substituted when the JSON
- * attribute is not found in the JSON value.
- *
- * @param defaultValue
- * The default LDAP value.
- * @return This attribute mapper.
- */
- public SimpleAttributeMapper defaultLDAPValue(final Object defaultValue) {
- this.defaultLDAPValue = defaultValue != null ? ByteString.valueOf(defaultValue) : null;
+ this.defaultJSONValues = defaultValue != null ? singletonList(defaultValue) : emptyList();
return this;
}
@@ -137,61 +112,21 @@
return this;
}
- /**
- * Indicates that the LDAP attribute is mandatory and must be provided
- * during create requests.
- *
- * @return This attribute mapper.
- */
- public SimpleAttributeMapper isRequired() {
- this.isRequired = true;
- return this;
- }
-
- /**
- * Indicates that multi-valued LDAP attribute should be represented as a
- * single-valued JSON value, rather than an array of values.
- *
- * @return This attribute mapper.
- */
- public SimpleAttributeMapper isSingleValued() {
- this.isSingleValued = true;
- return this;
- }
-
- /**
- * Indicates whether or not the LDAP attribute supports updates. The default
- * is {@link WritabilityPolicy#READ_WRITE}.
- *
- * @param policy
- * The writability policy.
- * @return This attribute mapper.
- */
- public SimpleAttributeMapper writability(final WritabilityPolicy policy) {
- this.writabilityPolicy = policy;
- return this;
- }
-
@Override
- void getLDAPAttributes(final Context c, final JsonPointer jsonAttribute,
- final Set<String> ldapAttributes) {
- ldapAttributes.add(ldapAttributeName.toString());
- }
-
- @Override
- void getLDAPFilter(final Context c, final FilterType type, final JsonPointer jsonAttribute,
- final String operator, final Object valueAssertion, final ResultHandler<Filter> h) {
- if (jsonAttribute.isEmpty()) {
+ void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath,
+ final FilterType type, final String operator, final Object valueAssertion,
+ final ResultHandler<Filter> h) {
+ if (subPath.isEmpty()) {
try {
final ByteString va =
valueAssertion != null ? encoder().apply(valueAssertion, null) : null;
h.handleResult(toFilter(c, type, ldapAttributeName.toString(), va));
- } catch (Exception e) {
+ } catch (final Exception e) {
// Invalid assertion value - bad request.
-
- // FIXME: improve error message.
- h.handleError(new BadRequestException("Invalid filter assertion value '"
- + String.valueOf(valueAssertion) + "'", e));
+ h.handleError(new BadRequestException(i18n(
+ "The request cannot be processed because it contained an "
+ + "illegal filter assertion value '%s' for field '%s'", String
+ .valueOf(valueAssertion), path), e));
}
} else {
// This attribute mapper does not support partial filtering.
@@ -200,7 +135,25 @@
}
@Override
- void toJSON(final Context c, final Entry e, final ResultHandler<JsonValue> h) {
+ void getNewLDAPAttributes(final Context c, final JsonPointer path,
+ final List<Object> newValues, final ResultHandler<Attribute> h) {
+ try {
+ h.handleResult(jsonToAttribute(newValues, ldapAttributeName, encoder()));
+ } catch (final Exception ex) {
+ h.handleError(new BadRequestException(i18n(
+ "The request cannot be processed because an error occurred while "
+ + "encoding the values for the field '%s': %s", path, ex.getMessage())));
+ }
+ }
+
+ @Override
+ SimpleAttributeMapper getThis() {
+ return this;
+ }
+
+ @Override
+ void toJSON(final Context c, final JsonPointer path, final Entry e,
+ final ResultHandler<JsonValue> h) {
try {
final Object value;
if (attributeIsSingleValued()) {
@@ -211,66 +164,12 @@
value = s.isEmpty() ? null : s;
}
h.handleResult(value != null ? new JsonValue(value) : null);
- } catch (Exception ex) {
+ } catch (final Exception ex) {
// The LDAP attribute could not be decoded.
h.handleError(asResourceException(ex));
}
}
- @Override
- void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) {
- try {
- final List<Attribute> result;
- if (v == null || v.isNull()) {
- if (attributeIsRequired()) {
- // FIXME: improve error message.
- throw new BadRequestException("no value provided");
- } else if (defaultLDAPValue != null) {
- result =
- singletonList((Attribute) new LinkedAttribute(ldapAttributeName,
- defaultLDAPValue));
- } else {
- result = emptyList();
- }
- } else if (v.isList() && attributeIsSingleValued()) {
- // FIXME: improve error message.
- throw new BadRequestException("expected single value, but got multiple values");
- } else if (!writabilityPolicy.canCreate(ldapAttributeName)) {
- if (writabilityPolicy.discardWrites()) {
- result = emptyList();
- } else {
- // FIXME: improve error message.
- throw new BadRequestException("attempted to create a read-only value");
- }
- } else {
- final Object value = v.getObject();
- if (value != null) {
- result = singletonList(jsonToAttribute(value, ldapAttributeName, encoder()));
- } else if (defaultLDAPValue != null) {
- result =
- singletonList((Attribute) new LinkedAttribute(ldapAttributeName,
- defaultLDAPValue));
- } else {
- result = emptyList();
- }
- }
- h.handleResult(result);
- } catch (final ResourceException e) {
- h.handleError(e);
- } catch (final Exception e) {
- // FIXME: improve error message.
- h.handleError(new BadRequestException(e.getMessage()));
- }
- }
-
- private boolean attributeIsRequired() {
- return isRequired && defaultJSONValue == null;
- }
-
- private boolean attributeIsSingleValued() {
- return isSingleValued || ldapAttributeName.getAttributeType().isSingleValue();
- }
-
private Function<ByteString, ? extends Object, Void> decoder() {
return decoder == null ? fixedFunction(byteStringToJson(), ldapAttributeName) : decoder;
}
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
index 28a8851..2123a74 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
+++ b/opendj-sdk/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
@@ -19,20 +19,27 @@
import static org.fest.assertions.Fail.fail;
import static org.forgerock.json.resource.Requests.newDeleteRequest;
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.Resources.newInternalConnection;
import static org.forgerock.opendj.ldap.Connections.newInternalConnectionFactory;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.object;
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.TestUtils.field;
+import static org.forgerock.opendj.rest2ldap.TestUtils.object;
import java.io.IOException;
+import org.forgerock.json.fluent.JsonValue;
+import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.Connection;
import org.forgerock.json.resource.NotFoundException;
import org.forgerock.json.resource.PreconditionFailedException;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.Resource;
-import org.forgerock.json.resource.RootContext;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.MemoryBackend;
import org.forgerock.opendj.ldif.LDIFEntryReader;
@@ -54,10 +61,10 @@
public void testDelete() throws Exception {
final RequestHandler handler = newCollection(builder().build());
final Connection connection = newInternalConnection(handler);
- final Resource resource = connection.delete(c(), newDeleteRequest("/test1"));
- checkTestUser1(resource);
+ final Resource resource = connection.delete(ctx(), newDeleteRequest("/test1"));
+ checkResourcesAreEqual(resource, getTestUser1(12345));
try {
- connection.read(c(), newReadRequest("/test1"));
+ connection.read(ctx(), newReadRequest("/test1"));
fail("Read succeeded unexpectedly");
} catch (final NotFoundException e) {
// Expected.
@@ -69,10 +76,10 @@
final RequestHandler handler = newCollection(builder().build());
final Connection connection = newInternalConnection(handler);
final Resource resource =
- connection.delete(c(), newDeleteRequest("/test1").setRevision("12345"));
- checkTestUser1(resource);
+ connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12345"));
+ checkResourcesAreEqual(resource, getTestUser1(12345));
try {
- connection.read(c(), newReadRequest("/test1"));
+ connection.read(ctx(), newReadRequest("/test1"));
fail("Read succeeded unexpectedly");
} catch (final NotFoundException e) {
// Expected.
@@ -83,50 +90,50 @@
public void testDeleteMVCCNoMatch() throws Exception {
final RequestHandler handler = newCollection(builder().build());
final Connection connection = newInternalConnection(handler);
- connection.delete(c(), newDeleteRequest("/test1").setRevision("12346"));
+ connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12346"));
}
@Test(expectedExceptions = NotFoundException.class)
public void testDeleteNotFound() throws Exception {
final RequestHandler handler = newCollection(builder().build());
final Connection connection = newInternalConnection(handler);
- connection.delete(c(), newDeleteRequest("/missing"));
+ connection.delete(ctx(), newDeleteRequest("/missing"));
}
@Test
public void testRead() throws Exception {
final RequestHandler handler = newCollection(builder().build());
final Resource resource =
- newInternalConnection(handler).read(c(), newReadRequest("/test1"));
- checkTestUser1(resource);
+ newInternalConnection(handler).read(ctx(), newReadRequest("/test1"));
+ checkResourcesAreEqual(resource, getTestUser1(12345));
}
@Test(expectedExceptions = NotFoundException.class)
public void testReadNotFound() throws Exception {
final RequestHandler handler = newCollection(builder().build());
- newInternalConnection(handler).read(c(), newReadRequest("/missing"));
+ newInternalConnection(handler).read(ctx(), newReadRequest("/missing"));
}
@Test
public void testReadSelectAllFields() throws Exception {
final RequestHandler handler = newCollection(builder().build());
final Resource resource =
- newInternalConnection(handler).read(c(), newReadRequest("/test1").addField("/"));
- checkTestUser1(resource);
+ newInternalConnection(handler).read(ctx(), newReadRequest("/test1").addField("/"));
+ checkResourcesAreEqual(resource, getTestUser1(12345));
}
@Test
public void testReadSelectPartial() throws Exception {
final RequestHandler handler = newCollection(builder().build());
final Resource resource =
- newInternalConnection(handler).read(c(),
+ newInternalConnection(handler).read(ctx(),
newReadRequest("/test1").addField("surname"));
assertThat(resource.getId()).isEqualTo("test1");
assertThat(resource.getRevision()).isEqualTo("12345");
- assertThat(resource.getContent().get("id").asString()).isNull();
+ assertThat(resource.getContent().get("_id").asString()).isNull();
assertThat(resource.getContent().get("displayName").asString()).isNull();
assertThat(resource.getContent().get("surname").asString()).isEqualTo("user 1");
- assertThat(resource.getContent().get("rev").asString()).isNull();
+ assertThat(resource.getContent().get("_rev").asString()).isNull();
}
// Disabled - see CREST-86 (Should JSON resource fields be case insensitive?)
@@ -134,14 +141,60 @@
public void testReadSelectPartialInsensitive() throws Exception {
final RequestHandler handler = newCollection(builder().build());
final Resource resource =
- newInternalConnection(handler).read(c(),
+ newInternalConnection(handler).read(ctx(),
newReadRequest("/test1").addField("SURNAME"));
assertThat(resource.getId()).isEqualTo("test1");
assertThat(resource.getRevision()).isEqualTo("12345");
- assertThat(resource.getContent().get("id").asString()).isNull();
+ assertThat(resource.getContent().get("_id").asString()).isNull();
assertThat(resource.getContent().get("displayName").asString()).isNull();
assertThat(resource.getContent().get("surname").asString()).isEqualTo("user 1");
- assertThat(resource.getContent().get("rev").asString()).isNull();
+ assertThat(resource.getContent().get("_rev").asString()).isNull();
+ }
+
+ @Test
+ public void testUpdate() throws Exception {
+ final RequestHandler handler = newCollection(builder().build());
+ final Connection connection = newInternalConnection(handler);
+ final Resource resource1 =
+ connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)));
+ checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
+ final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+ checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
+ }
+
+ @Test
+ public void testUpdateMVCCMatch() throws Exception {
+ final RequestHandler handler = newCollection(builder().build());
+ final Connection connection = newInternalConnection(handler);
+ final Resource resource1 =
+ connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345))
+ .setRevision("12345"));
+ checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
+ final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+ checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
+ }
+
+ @Test(expectedExceptions = PreconditionFailedException.class)
+ public void testUpdateMVCCNoMatch() throws Exception {
+ final RequestHandler handler = newCollection(builder().build());
+ final Connection connection = newInternalConnection(handler);
+ connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345))
+ .setRevision("12346"));
+ }
+
+ @Test(expectedExceptions = NotFoundException.class)
+ public void testUpdateNotFound() throws Exception {
+ final RequestHandler handler = newCollection(builder().build());
+ final Connection connection = newInternalConnection(handler);
+ connection.update(ctx(), newUpdateRequest("/missing", getTestUser1Updated(12345)));
+ }
+
+ @Test(expectedExceptions = BadRequestException.class)
+ public void testUpdateReadOnlyAttribute() throws Exception {
+ final RequestHandler handler = newCollection(builder().build());
+ final Connection connection = newInternalConnection(handler);
+ // Etag is read-only.
+ connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(99999)));
}
private Builder builder() throws IOException {
@@ -149,24 +202,23 @@
.useEtagAttribute().useClientDNNaming("uid").readOnUpdatePolicy(
ReadOnUpdatePolicy.CONTROLS).authorizationPolicy(AuthorizationPolicy.NONE)
.additionalLDAPAttribute("objectClass", "top", "person").mapper(
- object().attribute("id", simple("uid").isSingleValued().isRequired())
- .attribute("displayName",
- simple("cn").isSingleValued().isRequired()).attribute(
- "surname", simple("sn").isSingleValued().isRequired())
- .attribute("rev", simple("etag").isSingleValued().isRequired()));
+ object().attribute(
+ "_id",
+ simple("uid").isSingleValued().isRequired().writability(
+ WritabilityPolicy.CREATE_ONLY)).attribute("displayName",
+ simple("cn").isSingleValued().isRequired()).attribute("surname",
+ simple("sn").isSingleValued().isRequired()).attribute(
+ "_rev",
+ simple("etag").isSingleValued().isRequired().writability(
+ WritabilityPolicy.READ_ONLY)));
}
- private RootContext c() {
- return new RootContext();
- }
-
- private void checkTestUser1(final Resource resource) {
- assertThat(resource.getId()).isEqualTo("test1");
- assertThat(resource.getRevision()).isEqualTo("12345");
- assertThat(resource.getContent().get("id").asString()).isEqualTo("test1");
- assertThat(resource.getContent().get("displayName").asString()).isEqualTo("test user 1");
- assertThat(resource.getContent().get("surname").asString()).isEqualTo("user 1");
- assertThat(resource.getContent().get("rev").asString()).isEqualTo("12345");
+ private void checkResourcesAreEqual(final Resource actual, final JsonValue expected) {
+ final Resource expectedResource = asResource(expected);
+ assertThat(actual.getId()).isEqualTo(expectedResource.getId());
+ assertThat(actual.getRevision()).isEqualTo(expectedResource.getRevision());
+ assertThat(actual.getContent().getObject()).isEqualTo(
+ expectedResource.getContent().getObject());
}
private ConnectionFactory getConnectionFactory() throws IOException {
@@ -200,4 +252,14 @@
return newInternalConnectionFactory(backend);
}
+
+ private JsonValue getTestUser1(final int rev) {
+ return content(object(field("_id", "test1"), field("_rev", String.valueOf(rev)), field(
+ "displayName", "test user 1"), field("surname", "user 1")));
+ }
+
+ private JsonValue getTestUser1Updated(final int rev) {
+ return content(object(field("_id", "test1"), field("_rev", String.valueOf(rev)), field(
+ "displayName", "changed"), field("surname", "user 1")));
+ }
}
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java
new file mode 100644
index 0000000..7dcc575
--- /dev/null
+++ b/opendj-sdk/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java
@@ -0,0 +1,143 @@
+/*
+ * 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 ForgeRock Inc.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.forgerock.json.fluent.JsonPointer;
+import org.forgerock.json.fluent.JsonValue;
+import org.forgerock.json.resource.Resource;
+import org.forgerock.json.resource.RootContext;
+
+/**
+ * Unit test utility methods, including fluent methods for creating JSON
+ * objects.
+ */
+public final class TestUtils {
+
+ /**
+ * Creates a JSON array object.
+ *
+ * @param objects
+ * The array elements.
+ * @return A JSON array.
+ */
+ public static Object array(final Object... objects) {
+ return Arrays.asList(objects);
+ }
+
+ /**
+ * Returns a {@code Resource} containing the provided JSON content. The ID
+ * and revision will be taken from the "_id" and "_rev" fields respectively.
+ *
+ * @param content
+ * The JSON content.
+ * @return A {@code Resource} containing the provided JSON content.
+ */
+ public static Resource asResource(final JsonValue content) {
+ return new Resource(content.get("_id").asString(), content.get("_rev").asString(), content);
+ }
+
+ /**
+ * Creates a JSON value for the provided object.
+ *
+ * @param object
+ * The object.
+ * @return The JSON value.
+ */
+ public static JsonValue content(final Object object) {
+ return new JsonValue(object);
+ }
+
+ /**
+ * Creates a root context to be passed in with client requests.
+ *
+ * @return The root context.
+ */
+ public static RootContext ctx() {
+ return new RootContext();
+ }
+
+ /**
+ * Creates a JSON value for the provided object. This is the same as
+ * {@link #content(Object)} but can yield more readable test data in data
+ * providers.
+ *
+ * @param object
+ * The object.
+ * @return The JSON value.
+ */
+ public static JsonValue expected(final Object object) {
+ return content(object);
+ }
+
+ /**
+ * Creates a JSON field for inclusion in a JSON object using
+ * {@link #object(java.util.Map.Entry...)}.
+ *
+ * @param key
+ * The JSON field name.
+ * @param value
+ * The JSON field value.
+ * @return The JSON field for inclusion in a JSON object.
+ */
+ public static Map.Entry<String, Object> field(final String key, final Object value) {
+ return new AbstractMap.SimpleImmutableEntry<String, Object>(key, value);
+ }
+
+ /**
+ * Creates a list of JSON pointers from the provided string representations.
+ *
+ * @param fields
+ * The list of JSON pointer strings.
+ * @return The list of parsed JSON pointers.
+ */
+ public static List<JsonPointer> filter(final String... fields) {
+ final List<JsonPointer> result = new ArrayList<JsonPointer>(fields.length);
+ for (final String field : fields) {
+ result.add(new JsonPointer(field));
+ }
+ return result;
+ }
+
+ /**
+ * Creates a JSON object comprised of the provided JSON
+ * {@link #field(String, Object) fields}.
+ *
+ * @param fields
+ * The list of {@link #field(String, Object) fields} to include
+ * in the JSON object.
+ * @return The JSON object.
+ */
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public static Object object(final Map.Entry... fields) {
+ final Map<String, Object> object = new LinkedHashMap<String, Object>(fields.length);
+ for (final Map.Entry<String, Object> field : fields) {
+ object.put(field.getKey(), field.getValue());
+ }
+ return object;
+ }
+
+ private TestUtils() {
+ // Prevent instantiation.
+ }
+
+}
--
Gitblit v1.10.0