From 4ca1423e387874accc55c1d0ffcada3eddb833c5 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Wed, 01 May 2013 00:12:40 +0000
Subject: [PATCH] Partial fix for CREST-3: Add patch support
---
opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java | 123 +++++
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java | 3
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java | 203 ++++++--
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java | 90 +++
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java | 41 +
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java | 210 ++++++---
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java | 431 ++++++++++++++-----
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java | 178 +++----
8 files changed, 920 insertions(+), 359 deletions(-)
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
index cd5bb12..7c1186e 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
@@ -18,8 +18,10 @@
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.Rest2LDAP.asResourceException;
import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.isNullOrEmpty;
+import static org.forgerock.opendj.rest2ldap.Utils.transform;
import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE;
import java.util.ArrayList;
@@ -30,12 +32,14 @@
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.BadRequestException;
+import org.forgerock.json.resource.NotSupportedException;
+import org.forgerock.json.resource.PatchOperation;
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.Attributes;
import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.Function;
import org.forgerock.opendj.ldap.LinkedAttribute;
import org.forgerock.opendj.ldap.Modification;
import org.forgerock.opendj.ldap.ModificationType;
@@ -96,6 +100,12 @@
}
@Override
+ void create(final Context c, final JsonPointer path, final JsonValue v,
+ final ResultHandler<List<Attribute>> h) {
+ getNewLDAPAttributes(c, path, v, createAttributeHandler(path, h));
+ }
+
+ @Override
void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath,
final Set<String> ldapAttributes) {
ldapAttributes.add(ldapAttributeName.toString());
@@ -107,130 +117,323 @@
abstract T getThis();
@Override
- void toLDAP(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
+ void patch(final Context c, final JsonPointer path, final PatchOperation operation,
final ResultHandler<List<Modification>> h) {
- // Make following code readable.
- final boolean isUpdateRequest = e != null;
- final boolean isCreateRequest = !isUpdateRequest;
+ try {
+ final JsonPointer field = operation.getField();
+ final JsonValue v = operation.getValue();
- // 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 {
- final ResultHandler<Attribute> attributeHandler = 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()) {
- // The attribute is being added.
- modifications =
- singletonList(new Modification(ModificationType.REPLACE,
- newLDAPAttribute));
- } else if (newLDAPAttribute.isEmpty()) {
- /*
- * The attribute is being deleted - this is not
- * allowed if the attribute is required.
- */
- if (isRequired) {
- h.handleError(new BadRequestException(i18n(
- "The request cannot be processed because it attempts to remove "
- + "the required field '%s'", path)));
- return;
- } else {
- 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);
- }
- }
- };
-
- final List<Object> newValues = asList(v);
- if (newValues.isEmpty()) {
- // Skip sub-class implementation if there are no values.
- attributeHandler.handleResult(Attributes.emptyAttribute(ldapAttributeName));
- } else {
- getNewLDAPAttributes(c, path, asList(v), attributeHandler);
+ /*
+ * 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 new BadRequestException(i18n(
+ "The request cannot be processed because it attempts to modify "
+ + "the read-only field '%s'", path));
}
+
+ switch (field.size()) {
+ case 0:
+ /*
+ * The patch operation targets the entire mapping. If this
+ * mapping is multi-valued, then the patch value must be a list
+ * of values to be added, removed, or replaced. If it is
+ * single-valued then the patch value must not be a list.
+ */
+ if (attributeIsSingleValued()) {
+ if (v != null && v.isList()) {
+ // Single-valued field violation.
+ throw new BadRequestException(i18n(
+ "The request cannot be processed because an array of values was "
+ + "provided for the single valued field '%s'", path));
+ }
+ } else if (v != null && !v.isList() && !operation.isIncrement()
+ && !(v.isNull() && (operation.isReplace() || operation.isRemove()))) {
+ // Multi-valued field violation.
+ throw new BadRequestException(i18n(
+ "The request cannot be processed because an array of values was "
+ + "not provided for the multi-valued field '%s'", path));
+ }
+ break;
+ case 1:
+ /*
+ * The patch operation targets a sub-field. If the sub-field
+ * name is a number then it is an attempt to patch a single
+ * value at a specific index. Rest2LDAP cannot support indexed
+ * updates because LDAP attribute values are unordered. We will,
+ * however, support the special index "-" indicating that a
+ * value should be appended.
+ */
+ final String fieldName = field.get(0);
+ if (fieldName.equals("-") && operation.isAdd()) {
+ // Append a single value.
+ if (attributeIsSingleValued()) {
+ throw new BadRequestException(i18n(
+ "The request cannot be processed because it attempts to append a "
+ + "value to the single valued field '%s'", path));
+ } else if (v.isList()) {
+ throw new BadRequestException(i18n(
+ "The request cannot be processed because it attempts to "
+ + "perform an indexed append of an array of values to "
+ + "the multi-valued field '%s'", path.child(fieldName)));
+ }
+ } else if (fieldName.matches("[0-9]+")) {
+ // Array index - not allowed.
+ throw new NotSupportedException(i18n(
+ "The request cannot be processed because it included "
+ + "an indexed patch operation '%s' which is not supported "
+ + "by this resource provider", path.child(fieldName)));
+ } else {
+ throw new BadRequestException(i18n(
+ "The request cannot be processed because it included "
+ + "an unrecognized field '%s'", path.child(fieldName)));
+ }
+ break;
+ default:
+ /*
+ * The patch operation targets the child of a sub-field. This is
+ * not possible for a LDAP attribute mapper.
+ */
+ throw new BadRequestException(i18n(
+ "The request cannot be processed because it included "
+ + "an unrecognized field '%s'", path.child(field.get(0))));
+ }
+
+ // Check that the values are compatible with the type of patch operation.
+ final List<Object> newValues = asList(v, Collections.emptyList());
+ final ModificationType modType;
+ if (operation.isAdd()) {
+ /*
+ * Use a replace for single valued fields in case the underlying
+ * LDAP attribute is multi-valued, or the attribute already
+ * contains a value.
+ */
+ modType =
+ attributeIsSingleValued() ? ModificationType.REPLACE : ModificationType.ADD;
+ if (newValues.isEmpty()) {
+ throw new BadRequestException(i18n(
+ "The request cannot be processed because it included "
+ + "an add patch operation but no value(s) for field '%s'", path
+ .child(field.get(0))));
+ }
+ } else if (operation.isRemove()) {
+ modType = ModificationType.DELETE;
+ } else if (operation.isReplace()) {
+ modType = ModificationType.REPLACE;
+ } else if (operation.isIncrement()) {
+ modType = ModificationType.INCREMENT;
+ if (newValues.isEmpty()) {
+ throw new BadRequestException(i18n(
+ "The request cannot be processed because it included "
+ + "an increment patch operation but no value for field '%s'",
+ path.child(field.get(0))));
+ } else if (newValues.size() > 1) {
+ throw new BadRequestException(
+ i18n("The request cannot be processed because it included "
+ + "an increment patch operation with multiple values for field '%s'",
+ path.child(field.get(0))));
+ }
+ } else {
+ throw new NotSupportedException(i18n(
+ "The request cannot be processed because it included "
+ + "an unsupported type of patch operation '%s'", operation
+ .getOperation()));
+ }
+
+ // Create the modification.
+ if (newValues.isEmpty()) {
+ // Deleting the attribute.
+ if (isRequired) {
+ h.handleError(new BadRequestException(i18n(
+ "The request cannot be processed because it attempts to remove "
+ + "the required field '%s'", path)));
+ } else {
+ h.handleResult(singletonList(new Modification(modType,
+ emptyAttribute(ldapAttributeName))));
+ }
+ } else {
+ getNewLDAPAttributes(c, path, newValues, transform(
+ new Function<Attribute, List<Modification>, Void>() {
+ @Override
+ public List<Modification> apply(final Attribute value, final Void p) {
+ return singletonList(new Modification(modType, value));
+ }
+ }, h));
+ }
+ } catch (final Exception e) {
+ h.handleError(asResourceException(e));
}
}
- private List<Object> asList(final JsonValue v) {
+ @Override
+ void update(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
+ final ResultHandler<List<Modification>> h) {
+ getNewLDAPAttributes(c, path, v, updateAttributeHandler(path, e, h));
+ }
+
+ private List<Object> asList(final JsonValue v, final List<Object> defaultValues) {
if (isNullOrEmpty(v)) {
- return defaultJSONValues;
+ return defaultValues;
} else if (v.isList()) {
return v.asList();
} else {
return singletonList(v.getObject());
}
}
+
+ private void checkSchema(final JsonPointer path, final JsonValue v) throws BadRequestException {
+ if (attributeIsSingleValued()) {
+ if (v != null && v.isList()) {
+ // Single-valued field violation.
+ throw new BadRequestException(i18n(
+ "The request cannot be processed because an array of values was "
+ + "provided for the single valued field '%s'", path));
+ }
+ } else if (v != null && !v.isList()) {
+ // Multi-valued field violation.
+ throw new BadRequestException(i18n(
+ "The request cannot be processed because an array of values was "
+ + "not provided for the multi-valued field '%s'", path));
+ }
+ }
+
+ private ResultHandler<Attribute> createAttributeHandler(final JsonPointer path,
+ final ResultHandler<List<Attribute>> h) {
+ return new ResultHandler<Attribute>() {
+ @Override
+ public void handleError(final ResourceException error) {
+ h.handleError(error);
+ }
+
+ @Override
+ public void handleResult(final Attribute newLDAPAttribute) {
+ if (!writabilityPolicy.canCreate(ldapAttributeName)) {
+ if (newLDAPAttribute.isEmpty() || writabilityPolicy.discardWrites()) {
+ h.handleResult(Collections.<Attribute> emptyList());
+ } else {
+ h.handleError(new BadRequestException(i18n(
+ "The request cannot be processed because it attempts to create "
+ + "the read-only field '%s'", path)));
+ }
+ } else if (newLDAPAttribute.isEmpty()) {
+ if (isRequired) {
+ h.handleError(new BadRequestException(i18n(
+ "The request cannot be processed because it attempts to remove "
+ + "the required field '%s'", path)));
+ return;
+ } else {
+ h.handleResult(Collections.<Attribute> emptyList());
+ }
+ } else {
+ h.handleResult(singletonList(newLDAPAttribute));
+ }
+ }
+ };
+ }
+
+ private void getNewLDAPAttributes(final Context c, final JsonPointer path, final JsonValue v,
+ final ResultHandler<Attribute> attributeHandler) {
+ 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.
+ attributeHandler.handleResult(emptyAttribute(ldapAttributeName));
+ } else {
+ getNewLDAPAttributes(c, path, newValues, attributeHandler);
+ }
+ } catch (final Exception ex) {
+ attributeHandler.handleError(asResourceException(ex));
+ }
+ }
+
+ private ResultHandler<Attribute> updateAttributeHandler(final JsonPointer path, final Entry e,
+ final ResultHandler<List<Modification>> h) {
+ // Get the existing LDAP attribute.
+ final Attribute tmp = e.getAttribute(ldapAttributeName);
+ final Attribute oldLDAPAttribute = tmp != null ? tmp : emptyAttribute(ldapAttributeName);
+ return 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 (!writabilityPolicy.canWrite(ldapAttributeName)) {
+ if (newLDAPAttribute.isEmpty() || 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()) {
+ // The attribute is being added.
+ modifications =
+ singletonList(new Modification(ModificationType.REPLACE,
+ newLDAPAttribute));
+ } else if (newLDAPAttribute.isEmpty()) {
+ /*
+ * The attribute is being deleted - this is not allowed
+ * if the attribute is required.
+ */
+ if (isRequired) {
+ h.handleError(new BadRequestException(i18n(
+ "The request cannot be processed because it attempts to remove "
+ + "the required field '%s'", path)));
+ return;
+ } else {
+ 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);
+ }
+ }
+ };
+ }
}
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
index 02cc5d6..53a7602 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
@@ -20,7 +20,9 @@
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
+import org.forgerock.json.resource.PatchOperation;
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;
@@ -40,6 +42,33 @@
}
/**
+ * Maps a JSON value to one or more LDAP attributes, invoking a completion
+ * handler once the transformation has completed. This method is invoked
+ * when a REST resource is created using a create request.
+ * <p>
+ * If the JSON value corresponding to this mapper is not present in the
+ * resource then this method will be invoked with a value of {@code null}.
+ * It is the responsibility of the mapper implementation to take appropriate
+ * action in this case, perhaps by substituting default LDAP values, or by
+ * rejecting the update by invoking the result handler's
+ * {@link ResultHandler#handleError handleError} method.
+ *
+ * @param c
+ * The context.
+ * @param path
+ * The pointer from the root of the JSON resource to this
+ * attribute mapper. This may be used when constructing error
+ * messages.
+ * @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.
+ * @param h
+ * The result handler.
+ */
+ abstract void create(Context c, JsonPointer path, JsonValue v, ResultHandler<List<Attribute>> h);
+
+ /**
* Adds the names of the LDAP attributes required by this attribute mapper
* to the provided set.
* <p>
@@ -48,10 +77,14 @@
*
* @param c
* The context.
- * @param jsonAttribute
- * The name of the requested sub-attribute within this mapper or
+ * @param path
+ * The pointer from the root of the JSON resource to this
+ * attribute mapper. This may be used when constructing error
+ * messages.
+ * @param subPath
+ * The targeted JSON field relative to this attribute mapper, or
* root if all attributes associated with this mapper have been
- * requested.
+ * targeted.
* @param ldapAttributes
* The set into which the required LDAP attribute names should be
* put.
@@ -71,12 +104,16 @@
*
* @param c
* The context.
+ * @param path
+ * The pointer from the root of the JSON resource to this
+ * attribute mapper. This may be used when constructing error
+ * messages.
+ * @param subPath
+ * The targeted JSON field relative to this attribute mapper, or
+ * root if all attributes associated with this mapper have been
+ * targeted.
* @param type
* The type of REST comparison filter.
- * @param jsonAttribute
- * The name of the targeted sub-attribute within this mapper or
- * root if all attributes associated with this mapper have been
- * targeted by the filter.
* @param operator
* The name of the extended operator to use for the comparison,
* or {@code null} if {@code type} is not
@@ -91,6 +128,28 @@
String operator, Object valueAssertion, ResultHandler<Filter> h);
/**
+ * Maps a JSON patch operation to one or more LDAP modifications, invoking a
+ * completion handler once the transformation has completed. This method is
+ * invoked when a REST resource is modified using a patch request.
+ *
+ * @param c
+ * The context.
+ * @param path
+ * The pointer from the root of the JSON resource to this
+ * attribute mapper. This may be used when constructing error
+ * messages.
+ * @param operation
+ * The JSON patch operation to be converted to LDAP
+ * modifications. The targeted JSON field will be relative to
+ * this attribute mapper, or root if all attributes associated
+ * with this mapper have been targeted.
+ * @param h
+ * The result handler.
+ */
+ abstract void patch(Context c, JsonPointer path, PatchOperation operation,
+ ResultHandler<List<Modification>> h);
+
+ /**
* Maps one or more LDAP attributes to their JSON representation, invoking a
* completion handler once the transformation has completed.
* <p>
@@ -109,20 +168,21 @@
*
* @param c
* The context.
+ * @param path
+ * The pointer from the root of the JSON resource to this
+ * attribute mapper. This may be used when constructing error
+ * messages.
* @param e
* The LDAP entry to be converted to JSON.
* @param h
* The result handler.
*/
- abstract void toJSON(Context c, JsonPointer path, Entry e, ResultHandler<JsonValue> h);
+ abstract void read(Context c, JsonPointer path, Entry e, ResultHandler<JsonValue> h);
/**
- * Maps a JSON value to one or more LDAP attributes, invoking a completion
- * handler once the transformation has completed.
- * <p>
- * This method is invoked whenever a REST resource is converted to an LDAP
- * entry or LDAP modification, i.e. when performing create, put, or patch
- * requests.
+ * Maps a JSON value to one or more LDAP modifications, invoking a
+ * completion handler once the transformation has completed. This method is
+ * invoked when a REST resource is modified using an update request.
* <p>
* If the JSON value corresponding to this mapper is not present in the
* resource then this method will be invoked with a value of {@code null}.
@@ -140,7 +200,7 @@
* @param h
* The result handler.
*/
- abstract void toLDAP(Context c, JsonPointer path, Entry e, JsonValue v,
+ abstract void update(Context c, JsonPointer path, Entry e, JsonValue v,
ResultHandler<List<Modification>> h);
// TODO: methods for obtaining schema information (e.g. name, description,
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
index da36b92..a1eee8d 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
@@ -29,7 +29,9 @@
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.BadRequestException;
+import org.forgerock.json.resource.PatchOperation;
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;
@@ -45,6 +47,18 @@
}
@Override
+ void create(final Context c, final JsonPointer path, final JsonValue v,
+ final ResultHandler<List<Attribute>> h) {
+ if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) {
+ h.handleError(new BadRequestException(i18n(
+ "The request cannot be processed because it attempts to create "
+ + "the read-only field '%s'", path)));
+ } else {
+ h.handleResult(Collections.<Attribute> emptyList());
+ }
+ }
+
+ @Override
void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath,
final Set<String> ldapAttributes) {
// Nothing to do.
@@ -90,24 +104,29 @@
}
@Override
- void toJSON(final Context c, final JsonPointer path, final Entry e,
+ void patch(final Context c, final JsonPointer path, final PatchOperation operation,
+ final ResultHandler<List<Modification>> h) {
+ h.handleError(new BadRequestException(i18n(
+ "The request cannot be processed because it attempts to patch "
+ + "the read-only field '%s'", path)));
+ }
+
+ @Override
+ void read(final Context c, final JsonPointer path, final Entry e,
final ResultHandler<JsonValue> h) {
h.handleResult(value.copy());
}
@Override
- void toLDAP(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
+ void update(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
final ResultHandler<List<Modification>> h) {
- if (!isNullOrEmpty(v)) {
- // A value was provided so it must match.
- if (!v.getObject().equals(value.getObject())) {
- h.handleError(new BadRequestException(i18n(
- "The request cannot be processed because it attempts to modify "
- + "the read-only field '%s'", path)));
- return;
- }
+ if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) {
+ h.handleError(new BadRequestException(i18n(
+ "The request cannot be processed because it attempts to modify "
+ + "the read-only field '%s'", path)));
+ } else {
+ h.handleResult(Collections.<Modification> emptyList());
}
- h.handleResult(Collections.<Modification> emptyList());
}
private <T extends Comparable<T>> Filter compare(final Context c, final FilterType type,
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
index 83f0215..2beb935 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
@@ -44,8 +44,8 @@
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.PatchOperation;
import org.forgerock.json.resource.PatchRequest;
import org.forgerock.json.resource.PreconditionFailedException;
import org.forgerock.json.resource.QueryFilter;
@@ -69,7 +69,6 @@
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;
@@ -136,31 +135,22 @@
@Override
public void run() {
// Calculate entry content.
- attributeMapper.toLDAP(c, new JsonPointer(), null, request.getContent(),
- new ResultHandler<List<Modification>>() {
+ attributeMapper.create(c, new JsonPointer(), request.getContent(),
+ new ResultHandler<List<Attribute>>() {
@Override
public void handleError(final ResourceException error) {
h.handleError(error);
}
@Override
- public void handleResult(final List<Modification> result) {
+ public void handleResult(final List<Attribute> result) {
// Perform add operation.
final AddRequest addRequest = newAddRequest(DN.rootDN());
for (final Attribute attribute : additionalLDAPAttributes) {
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;
- }
+ for (final Attribute attribute : result) {
+ addRequest.addAttribute(attribute);
}
try {
nameStrategy.setResourceId(c, getBaseDN(c), request
@@ -189,59 +179,104 @@
final Context c = wrap(context);
final ResultHandler<Resource> h = wrap(c, handler);
- // Get connection then perform the search.
- c.run(h, new Runnable() {
+ // Get connection, search if needed, then delete.
+ c.run(h, doUpdate(c, resourceId, request.getRevision(), new ResultHandler<DN>() {
@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(
- ldapAttribute);
- 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());
-
- // Perform delete operation.
- final ChangeRecord deleteRequest =
- newDeleteRequest(entry.getName());
- if (config.readOnUpdatePolicy() == CONTROLS) {
- final String[] attributes =
- getLDAPAttributes(c, request.getFields());
- deleteRequest.addControl(PreReadRequestControl.newControl(
- false, attributes));
- }
- if (config.useSubtreeDelete()) {
- deleteRequest.addControl(SubtreeDeleteRequestControl
- .newControl(true));
- }
- addAssertionControl(deleteRequest, request.getRevision());
- c.getConnection().applyChangeAsync(deleteRequest, null,
- postUpdateHandler(c, h));
- } catch (final Exception e) {
- h.handleError(asResourceException(e));
- }
- }
- });
+ public void handleError(final ResourceException error) {
+ h.handleError(error);
}
- });
+
+ @Override
+ public void handleResult(final DN dn) {
+ try {
+ final ChangeRecord deleteRequest = newDeleteRequest(dn);
+ if (config.readOnUpdatePolicy() == CONTROLS) {
+ final String[] attributes = getLDAPAttributes(c, request.getFields());
+ deleteRequest.addControl(PreReadRequestControl
+ .newControl(false, attributes));
+ }
+ if (config.useSubtreeDelete()) {
+ deleteRequest.addControl(SubtreeDeleteRequestControl.newControl(true));
+ }
+ addAssertionControl(deleteRequest, request.getRevision());
+ c.getConnection()
+ .applyChangeAsync(deleteRequest, null, postUpdateHandler(c, h));
+ } catch (final Exception e) {
+ h.handleError(asResourceException(e));
+ }
+ }
+ }));
}
@Override
public void patchInstance(final ServerContext context, final String resourceId,
final PatchRequest request, final ResultHandler<Resource> handler) {
- handler.handleError(new NotSupportedException("Not yet implemented"));
+ final Context c = wrap(context);
+ final ResultHandler<Resource> h = wrap(c, handler);
+
+ /*
+ * Get the connection, search if needed, then determine modifications,
+ * then perform modify.
+ */
+ c.run(h, doUpdate(c, resourceId, request.getRevision(), new ResultHandler<DN>() {
+ @Override
+ public void handleError(final ResourceException error) {
+ h.handleError(error);
+ }
+
+ @Override
+ public void handleResult(final DN dn) {
+ // Convert the patch operations to LDAP modifications.
+ final ResultHandler<List<Modification>> handler =
+ accumulate(request.getPatchOperations().size(),
+ new ResultHandler<List<List<Modification>>>() {
+ @Override
+ public void handleError(final ResourceException error) {
+ h.handleError(error);
+ }
+
+ @Override
+ public void handleResult(final List<List<Modification>> result) {
+ // The patch operations have been converted successfully.
+ try {
+ final ModifyRequest modifyRequest =
+ newModifyRequest(dn);
+ 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());
+
+ // Add the modifications.
+ for (final List<Modification> modifications : result) {
+ if (modifications != null) {
+ modifyRequest.getModifications().addAll(
+ modifications);
+ }
+ }
+
+ // Perform the modify request.
+ c.getConnection().applyChangeAsync(modifyRequest, null,
+ postUpdateHandler(c, h));
+ } catch (final Exception e) {
+ h.handleError(asResourceException(e));
+ }
+ }
+ });
+
+ for (final PatchOperation operation : request.getPatchOperations()) {
+ attributeMapper.patch(c, new JsonPointer(), operation, handler);
+ }
+ }
+ }));
}
@Override
@@ -321,7 +356,7 @@
};
pendingResourceCount.incrementAndGet();
- attributeMapper.toJSON(c, new JsonPointer(), entry, mapHandler);
+ attributeMapper.read(c, new JsonPointer(), entry, mapHandler);
return true;
}
@@ -404,7 +439,7 @@
public void updateInstance(final ServerContext context, final String resourceId,
final UpdateRequest request, final ResultHandler<Resource> handler) {
/*
- * Update operations are a bit awkward because there is not direct
+ * Update operations are a bit awkward because there is no 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
@@ -415,11 +450,10 @@
final Context c = wrap(context);
final ResultHandler<Resource> h = wrap(c, handler);
- // Get connection then perform the search.
+ // Get connection then, search for the existing entry, then modify.
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 =
@@ -457,7 +491,7 @@
* Determine the set of changes that need to
* be performed.
*/
- attributeMapper.toLDAP(c, new JsonPointer(), entry, request
+ attributeMapper.update(c, new JsonPointer(), entry, request
.getNewContent(),
new ResultHandler<List<Modification>>() {
@Override
@@ -489,7 +523,7 @@
final ResultHandler<Resource> handler) {
final String actualResourceId = nameStrategy.getResourceId(c, entry);
final String revision = getRevisionFromEntry(entry);
- attributeMapper.toJSON(c, new JsonPointer(), entry, transform(
+ attributeMapper.read(c, new JsonPointer(), entry, transform(
new Function<JsonValue, Resource, Void>() {
@Override
public Resource apply(final JsonValue value, final Void p) {
@@ -507,6 +541,46 @@
}
}
+ private Runnable doUpdate(final Context c, final String resourceId, final String revision,
+ final ResultHandler<DN> updateHandler) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ final String ldapAttribute =
+ (etagAttribute != null && revision != null) ? etagAttribute.toString()
+ : "1.1";
+ final SearchRequest searchRequest =
+ nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId).addAttribute(
+ ldapAttribute);
+ if (searchRequest.getScope().equals(SearchScope.BASE_OBJECT)) {
+ // There's no point in doing a search because we already know the DN.
+ updateHandler.handleResult(searchRequest.getName());
+ } else {
+ c.getConnection().searchSingleEntryAsync(searchRequest,
+ new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() {
+ @Override
+ public void handleErrorResult(final ErrorResultException error) {
+ updateHandler.handleError(asResourceException(error));
+ }
+
+ @Override
+ public void handleResult(final SearchResultEntry entry) {
+ try {
+ // Fail-fast if there is a version mismatch.
+ ensureMVCCVersionMatches(entry, revision);
+
+ // Perform update operation.
+ updateHandler.handleResult(entry.getName());
+ } catch (final Exception e) {
+ updateHandler.handleError(asResourceException(e));
+ }
+ }
+ });
+ }
+ }
+ };
+ }
+
private void ensureMVCCSupported() throws NotSupportedException {
if (etagAttribute == null) {
throw new NotSupportedException(
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
index d4ba83f..c05677f 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
@@ -15,7 +15,9 @@
*/
package org.forgerock.opendj.rest2ldap;
+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.accumulate;
import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.toLowerCase;
@@ -32,7 +34,10 @@
import org.forgerock.json.fluent.JsonPointer;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.BadRequestException;
+import org.forgerock.json.resource.PatchOperation;
+import org.forgerock.json.resource.ResourceException;
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;
@@ -80,6 +85,37 @@
}
@Override
+ void create(final Context c, final JsonPointer path, final JsonValue v,
+ final ResultHandler<List<Attribute>> h) {
+ 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);
+
+ // Accumulate the results of the subordinate mappings.
+ final ResultHandler<List<Attribute>> handler = accumulator(h);
+
+ // Invoke mappings for which there are values provided.
+ if (v != null && !v.isNull()) {
+ 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.create(c, path.child(me.getKey()), subValue, handler);
+ }
+ }
+
+ // Invoke mappings for which there were no values provided.
+ for (final Mapping mapping : missingMappings.values()) {
+ mapping.mapper.create(c, path.child(mapping.name), null, handler);
+ }
+ } catch (final Exception e) {
+ h.handleError(asResourceException(e));
+ }
+ }
+
+ @Override
void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath,
final Set<String> ldapAttributes) {
if (subPath.isEmpty()) {
@@ -117,7 +153,55 @@
}
@Override
- void toJSON(final Context c, final JsonPointer path, final Entry e,
+ void patch(final Context c, final JsonPointer path, final PatchOperation operation,
+ final ResultHandler<List<Modification>> h) {
+ try {
+ final JsonPointer field = operation.getField();
+ final JsonValue v = operation.getValue();
+
+ if (field.isEmpty()) {
+ /*
+ * The patch operation applies to this object. We'll handle this
+ * by allowing the JSON value to be a partial object and
+ * add/remove/replace only the provided values.
+ */
+ checkMapping(path, operation.getValue());
+
+ // Accumulate the results of the subordinate mappings.
+ final ResultHandler<List<Modification>> handler = accumulator(h);
+
+ // Invoke the sub-mappers using a new patch operation targeted at each field.
+ for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
+ final Mapping mapping = getMapping(me.getKey());
+ final JsonValue subValue = new JsonValue(me.getValue());
+ final PatchOperation subOperation =
+ operation(operation.getOperation(), field /* empty */, subValue);
+ mapping.mapper.patch(c, path.child(me.getKey()), subOperation, handler);
+ }
+ } else {
+ /*
+ * The patch operation targets a subordinate field. Create a new
+ * patch operation targeting the field and forward it to the
+ * appropriate mapper.
+ */
+ final String fieldName = field.get(0);
+ final Mapping mapping = getMapping(fieldName);
+ if (mapping == null) {
+ throw new BadRequestException(i18n(
+ "The request cannot be processed because it included "
+ + "an unrecognized field '%s'", path.child(fieldName)));
+ }
+ final PatchOperation subOperation =
+ operation(operation.getOperation(), field.relativePointer(), v);
+ mapping.mapper.patch(c, path.child(fieldName), subOperation, h);
+ }
+ } catch (final Exception ex) {
+ h.handleError(asResourceException(ex));
+ }
+ }
+
+ @Override
+ void read(final Context c, final JsonPointer path, final Entry e,
final ResultHandler<JsonValue> h) {
/*
* Use an accumulator which will aggregate the results from the
@@ -152,7 +236,7 @@
}, h));
for (final Mapping mapping : mappings.values()) {
- mapping.mapper.toJSON(c, path.child(mapping.name), e, transform(
+ mapping.mapper.read(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,
@@ -165,69 +249,80 @@
}
@Override
- void toLDAP(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
+ void update(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.
- */
+ 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);
+
+ // Accumulate the results of the subordinate mappings.
+ final ResultHandler<List<Modification>> handler = accumulator(h);
+
+ // Invoke mappings for which there are values provided.
+ if (v != null && !v.isNull()) {
+ 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.update(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.update(c, path.child(mapping.name), e, null, handler);
+ }
+ } catch (final Exception ex) {
+ h.handleError(asResourceException(ex));
+ }
+ }
+
+ private <T> ResultHandler<List<T>> accumulator(final ResultHandler<List<T>> h) {
+ return accumulate(mappings.size(), transform(new Function<List<List<T>>, List<T>, Void>() {
+ @Override
+ public List<T> apply(final List<List<T>> value, final Void p) {
+ switch (value.size()) {
+ case 0:
+ return Collections.emptyList();
+ case 1:
+ return value.get(0);
+ default:
+ final List<T> attributes = new ArrayList<T>(value.size());
+ for (final List<T> a : value) {
+ attributes.addAll(a);
+ }
+ return attributes;
+ }
+ }
+ }, h));
+ }
+
+ /*
+ * 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 {
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(i18n(
- "The request cannot be processed because the JSON resource "
- + "contains an unrecognized field '%s'", path
- .child(attribute))));
- return;
+ throw new BadRequestException(i18n(
+ "The request cannot be processed because it included "
+ + "an unrecognized field '%s'", path.child(attribute)));
}
}
} else {
- 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;
+ throw new BadRequestException(i18n(
+ "The request cannot be processed because it included "
+ + "the field '%s' whose value is the wrong type: "
+ + "an object is expected", path));
}
}
-
- // Accumulate the results of the subordinate mappings.
- final ResultHandler<List<Modification>> handler =
- accumulate(mappings.size(), transform(
- new Function<List<List<Modification>>, List<Modification>, Void>() {
- @Override
- public List<Modification> apply(final List<List<Modification>> value,
- final Void p) {
- switch (value.size()) {
- case 0:
- return Collections.emptyList();
- case 1:
- return value.get(0);
- default:
- final List<Modification> attributes =
- new ArrayList<Modification>(value.size());
- for (final List<Modification> a : value) {
- attributes.addAll(a);
- }
- return attributes;
- }
- }
- }, h));
-
- // Invoke mappings for which there are values provided.
- if (v != null && !v.isNull()) {
- 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, path.child(mapping.name), e, null, handler);
- }
+ return missingMappings;
}
private Mapping getMapping(final JsonPointer jsonAttribute) {
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
index c899a5d..b84037d 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
@@ -46,7 +46,6 @@
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;
@@ -193,105 +192,94 @@
new AtomicReference<ResourceException>();
for (final Object value : newValues) {
- mapper.toLDAP(c, path, null /* force create */, new JsonValue(value),
- new ResultHandler<List<Modification>>() {
+ mapper.create(c, path, new JsonValue(value), new ResultHandler<List<Attribute>>() {
- @Override
- public void handleError(final ResourceException error) {
- h.handleError(error);
+ @Override
+ public void handleError(final ResourceException error) {
+ h.handleError(error);
+ }
+
+ @Override
+ public void handleResult(final List<Attribute> result) {
+ Attribute primaryKeyAttribute = null;
+ for (final Attribute attribute : result) {
+ if (attribute.getAttributeDescription().equals(primaryKey)) {
+ primaryKeyAttribute = attribute;
+ break;
}
+ }
- @Override
- public void handleResult(final List<Modification> result) {
- Attribute primaryKeyAttribute = null;
- for (final Modification modification : result) {
- if (modification.getAttribute().getAttributeDescription().equals(
- primaryKey)) {
- primaryKeyAttribute = modification.getAttribute();
- break;
+ if (primaryKeyAttribute == null || primaryKeyAttribute.isEmpty()) {
+ h.handleError(new BadRequestException(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) {
+ h.handleError(new BadRequestException(i18n(
+ "The request cannot be processed because the reference "
+ + "field '%s' contains a value which contains multiple "
+ + "primary keys", 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);
+ c.getConnection().searchSingleEntryAsync(search,
+ new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() {
+
+ @Override
+ public void handleErrorResult(final ErrorResultException error) {
+ ResourceException re;
+ try {
+ throw error;
+ } catch (final EntryNotFoundException e) {
+ re =
+ new BadRequestException(i18n(
+ "The request cannot be processed "
+ + "because the resource '%s' "
+ + "referenced in field '%s' does "
+ + "not exist", primaryKeyValue
+ .toString(), path));
+ } catch (final MultipleEntriesFoundException e) {
+ re =
+ new BadRequestException(i18n(
+ "The request cannot be processed "
+ + "because the resource '%s' "
+ + "referenced in field '%s' is "
+ + "ambiguous", primaryKeyValue
+ .toString(), path));
+ } catch (final ErrorResultException e) {
+ re = asResourceException(e);
+ }
+ exception.compareAndSet(null, re);
+ completeIfNecessary();
}
- }
- if (primaryKeyAttribute == null || primaryKeyAttribute.isEmpty()) {
- h.handleError(new BadRequestException(
- 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) {
- h.handleError(new BadRequestException(
- i18n("The request cannot be processed because the reference "
- + "field '%s' contains a value which contains multiple "
- + "primary keys", 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);
- c.getConnection()
- .searchSingleEntryAsync(
- search,
- new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() {
-
- @Override
- public void handleErrorResult(
- final ErrorResultException error) {
- ResourceException re;
- try {
- throw error;
- } catch (final EntryNotFoundException e) {
- re =
- new BadRequestException(
- i18n("The request cannot be processed "
- + "because the resource '%s' "
- + "referenced in field '%s' does "
- + "not exist",
- primaryKeyValue
- .toString(),
- path));
- } catch (final MultipleEntriesFoundException e) {
- re =
- new BadRequestException(
- i18n("The request cannot be processed "
- + "because the resource '%s' "
- + "referenced in field '%s' is "
- + "ambiguous",
- primaryKeyValue
- .toString(),
- path));
- } catch (final ErrorResultException e) {
- re = asResourceException(e);
- }
- exception.compareAndSet(null, re);
- completeIfNecessary();
- }
-
- @Override
- public void handleResult(
- final SearchResultEntry result) {
- synchronized (newLDAPAttribute) {
- newLDAPAttribute.add(result.getName());
- }
- completeIfNecessary();
- }
- });
- }
-
- private void completeIfNecessary() {
- if (pendingSearches.decrementAndGet() == 0) {
- if (exception.get() == null) {
- h.handleResult(newLDAPAttribute);
- } else {
- h.handleError(exception.get());
+ @Override
+ public void handleResult(final SearchResultEntry result) {
+ synchronized (newLDAPAttribute) {
+ newLDAPAttribute.add(result.getName());
+ }
+ completeIfNecessary();
}
- }
+ });
+ }
+
+ private void completeIfNecessary() {
+ if (pendingSearches.decrementAndGet() == 0) {
+ if (exception.get() == null) {
+ h.handleResult(newLDAPAttribute);
+ } else {
+ h.handleError(exception.get());
}
- });
+ }
+ }
+ });
}
}
@@ -301,7 +289,7 @@
}
@Override
- void toJSON(final Context c, final JsonPointer path, final Entry e,
+ void read(final Context c, final JsonPointer path, final Entry e,
final ResultHandler<JsonValue> h) {
final Attribute attribute = e.getAttribute(ldapAttributeName);
if (attribute == null || attribute.isEmpty()) {
@@ -377,7 +365,7 @@
@Override
public void handleResult(final SearchResultEntry result) {
- mapper.toJSON(c, path, result, handler);
+ mapper.read(c, path, result, handler);
}
});
}
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
index c7bc8c2..8ed9a7b 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
@@ -49,7 +49,6 @@
*/
public final class SimpleAttributeMapper extends AbstractLDAPAttributeMapper<SimpleAttributeMapper> {
private Function<ByteString, ?, Void> decoder = null;
-
private Function<Object, ByteString, Void> encoder = null;
SimpleAttributeMapper(final AttributeDescription ldapAttributeName) {
@@ -152,7 +151,7 @@
}
@Override
- void toJSON(final Context c, final JsonPointer path, final Entry e,
+ void read(final Context c, final JsonPointer path, final Entry e,
final ResultHandler<JsonValue> h) {
try {
final Object value;
diff --git a/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java b/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
index 05d119c..0efe249 100644
--- a/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
+++ b/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
@@ -20,7 +20,10 @@
import static org.fest.assertions.Fail.fail;
import static org.forgerock.json.fluent.JsonValue.field;
import static org.forgerock.json.fluent.JsonValue.object;
+import static org.forgerock.json.resource.PatchOperation.add;
+import static org.forgerock.json.resource.PatchOperation.remove;
import static org.forgerock.json.resource.Requests.newDeleteRequest;
+import static org.forgerock.json.resource.Requests.newPatchRequest;
import static org.forgerock.json.resource.Requests.newReadRequest;
import static org.forgerock.json.resource.Requests.newUpdateRequest;
import static org.forgerock.json.resource.Resources.newCollection;
@@ -103,6 +106,126 @@
}
@Test
+ public void testPatch() throws Exception {
+ final RequestHandler handler = newCollection(builder().build());
+ final Connection connection = newInternalConnection(handler);
+ final Resource resource1 =
+ connection.patch(ctx(), newPatchRequest("/test1", add("displayName", "changed")));
+ checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
+ final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+ checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
+ }
+
+ @Test
+ public void testPatchAddOptionalAttribute() throws Exception {
+ final RequestHandler handler = newCollection(builder().build());
+ final Connection connection = newInternalConnection(handler);
+ final JsonValue newContent = getTestUser1(12345);
+ newContent.put("description", asList("one", "two"));
+ final Resource resource1 =
+ connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one",
+ "two"))));
+ checkResourcesAreEqual(resource1, newContent);
+ final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+ checkResourcesAreEqual(resource2, newContent);
+ }
+
+ @Test(expectedExceptions = BadRequestException.class)
+ public void testPatchConstantAttribute() throws Exception {
+ final RequestHandler handler = newCollection(builder().build());
+ final Connection connection = newInternalConnection(handler);
+ connection.patch(ctx(), newPatchRequest("/test1", add("/schemas", asList("junk"))));
+ }
+
+ @Test
+ public void testPatchDeleteOptionalAttribute() throws Exception {
+ final RequestHandler handler = newCollection(builder().build());
+ final Connection connection = newInternalConnection(handler);
+ connection.patch(ctx(),
+ newPatchRequest("/test1", add("/description", asList("one", "two"))));
+ final Resource resource1 =
+ connection.patch(ctx(), newPatchRequest("/test1", remove("/description")));
+ checkResourcesAreEqual(resource1, getTestUser1(12345));
+ final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+ checkResourcesAreEqual(resource2, getTestUser1(12345));
+ }
+
+ @Test(expectedExceptions = BadRequestException.class)
+ public void testPatchMissingRequiredAttribute() throws Exception {
+ final RequestHandler handler = newCollection(builder().build());
+ final Connection connection = newInternalConnection(handler);
+ connection.patch(ctx(), newPatchRequest("/test1", remove("/surname")));
+ }
+
+ @Test
+ public void testPatchModifyOptionalAttribute() throws Exception {
+ final RequestHandler handler = newCollection(builder().build());
+ final Connection connection = newInternalConnection(handler);
+ connection.patch(ctx(),
+ newPatchRequest("/test1", add("/description", asList("one", "two"))));
+ final Resource resource1 =
+ connection.patch(ctx(), newPatchRequest("/test1", add("/description",
+ asList("three"))));
+ final JsonValue newContent = getTestUser1(12345);
+ newContent.put("description", asList("one", "two", "three"));
+ checkResourcesAreEqual(resource1, newContent);
+ final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+ checkResourcesAreEqual(resource2, newContent);
+ }
+
+ @Test
+ public void testPatchMVCCMatch() throws Exception {
+ final RequestHandler handler = newCollection(builder().build());
+ final Connection connection = newInternalConnection(handler);
+ final Resource resource1 =
+ connection.patch(ctx(), newPatchRequest("/test1", add("displayName", "changed"))
+ .setRevision("12345"));
+ checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
+ final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+ checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
+ }
+
+ @Test(expectedExceptions = PreconditionFailedException.class)
+ public void testPatchMVCCNoMatch() throws Exception {
+ final RequestHandler handler = newCollection(builder().build());
+ final Connection connection = newInternalConnection(handler);
+ connection.patch(ctx(), newPatchRequest("/test1", add("displayName", "changed"))
+ .setRevision("12346"));
+ }
+
+ @Test(expectedExceptions = NotFoundException.class)
+ public void testPatchNotFound() throws Exception {
+ final RequestHandler handler = newCollection(builder().build());
+ final Connection connection = newInternalConnection(handler);
+ connection.patch(ctx(), newPatchRequest("/missing", add("displayName", "changed")));
+ }
+
+ @Test(expectedExceptions = BadRequestException.class)
+ public void testPatchReadOnlyAttribute() throws Exception {
+ final RequestHandler handler = newCollection(builder().build());
+ final Connection connection = newInternalConnection(handler);
+ // Etag is read-only.
+ connection.patch(ctx(), newPatchRequest("/test1", add("_rev", "99999")));
+ }
+
+ @Test(expectedExceptions = BadRequestException.class)
+ public void testPatchSingleValuedAttributeWithMultipleValues() throws Exception {
+ final RequestHandler handler = newCollection(builder().build());
+ final Connection connection = newInternalConnection(handler);
+ connection.patch(ctx(),
+ newPatchRequest("/test1", add("/surname", asList("black", "white"))));
+ }
+
+ @Test(expectedExceptions = BadRequestException.class)
+ public void testPatchUnknownAttribute() throws Exception {
+ final RequestHandler handler = newCollection(builder().build());
+ final Connection connection = newInternalConnection(handler);
+ final JsonValue newContent = getTestUser1Updated(12345);
+ newContent.add("dummy", "junk");
+ connection.patch(ctx(), newPatchRequest("/test1", add("/dummy", "junk")));
+ }
+
+ @Test
public void testRead() throws Exception {
final RequestHandler handler = newCollection(builder().build());
final Resource resource =
--
Gitblit v1.10.0