Partial fix for CREST-3: Add patch support
* initial commit of Rest2LDAP support
* includes basic unit tests (more to come)
* need to re-align memory backend implementation and check against patch operation spec in Javadoc.
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | } |
| | | |
| | | @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()); |
| | |
| | | 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); |
| | | /* |
| | | * 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)); |
| | | } |
| | | |
| | | if (v != null && v.isList() && attributeIsSingleValued()) { |
| | | 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. |
| | | h.handleError(new BadRequestException(i18n( |
| | | throw new BadRequestException(i18n( |
| | | "The request cannot be processed because an array of values was " |
| | | + "provided for the single valued field '%s'", path))); |
| | | + "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 { |
| | | final ResultHandler<Attribute> attributeHandler = new ResultHandler<Attribute>() { |
| | | 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)); |
| | | } |
| | | } |
| | | |
| | | @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 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); |
| | |
| | | * |
| | | * 2) no new values are provided. |
| | | */ |
| | | if (isCreateRequest && !writabilityPolicy.canCreate(ldapAttributeName) |
| | | || isUpdateRequest && !writabilityPolicy.canWrite(ldapAttributeName)) { |
| | | if (newLDAPAttribute.isEmpty() |
| | | || (isUpdateRequest && newLDAPAttribute.equals(oldLDAPAttribute)) |
| | | if (!writabilityPolicy.canWrite(ldapAttributeName)) { |
| | | if (newLDAPAttribute.isEmpty() || newLDAPAttribute.equals(oldLDAPAttribute) |
| | | || writabilityPolicy.discardWrites()) { |
| | | // No change. |
| | | h.handleResult(Collections.<Modification> emptyList()); |
| | |
| | | newLDAPAttribute)); |
| | | } else if (newLDAPAttribute.isEmpty()) { |
| | | /* |
| | | * The attribute is being deleted - this is not |
| | | * allowed if the attribute is required. |
| | | * The attribute is being deleted - this is not allowed |
| | | * if the attribute is required. |
| | | */ |
| | | if (isRequired) { |
| | | h.handleError(new BadRequestException(i18n( |
| | |
| | | } |
| | | } 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. |
| | | * 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 addedValues = new LinkedAttribute(newLDAPAttribute); |
| | | addedValues.removeAll(oldLDAPAttribute); |
| | | if (!addedValues.isEmpty()) { |
| | | modifications.add(new Modification(ModificationType.ADD, |
| | | addedValues)); |
| | | 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); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private List<Object> asList(final JsonValue v) { |
| | | if (isNullOrEmpty(v)) { |
| | | return defaultJSONValues; |
| | | } else if (v.isList()) { |
| | | return v.asList(); |
| | | } else { |
| | | return singletonList(v.getObject()); |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | 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; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 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> |
| | |
| | | * |
| | | * @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. |
| | |
| | | * |
| | | * @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 |
| | |
| | | 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> |
| | |
| | | * |
| | | * @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}. |
| | |
| | | * @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, |
| | |
| | | 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; |
| | |
| | | } |
| | | |
| | | @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. |
| | |
| | | } |
| | | |
| | | @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())) { |
| | | 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))); |
| | | return; |
| | | } |
| | | } |
| | | } else { |
| | | h.handleResult(Collections.<Modification> emptyList()); |
| | | } |
| | | } |
| | | |
| | | private <T extends Comparable<T>> Filter compare(final Context c, final FilterType type, |
| | | final T v1, final T v2) { |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | @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 |
| | |
| | | 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)); |
| | | public void handleError(final ResourceException error) { |
| | | h.handleError(error); |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult(final SearchResultEntry entry) { |
| | | public void handleResult(final DN dn) { |
| | | try { |
| | | // Fail-fast if there is a version mismatch. |
| | | ensureMVCCVersionMatches(entry, request.getRevision()); |
| | | 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)); |
| | | } |
| | | } |
| | | })); |
| | | } |
| | | |
| | | // Perform delete operation. |
| | | final ChangeRecord deleteRequest = |
| | | newDeleteRequest(entry.getName()); |
| | | @Override |
| | | public void patchInstance(final ServerContext context, final String resourceId, |
| | | final PatchRequest request, final ResultHandler<Resource> handler) { |
| | | 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()); |
| | | deleteRequest.addControl(PreReadRequestControl.newControl( |
| | | false, attributes)); |
| | | modifyRequest.addControl(PostReadRequestControl |
| | | .newControl(false, attributes)); |
| | | } |
| | | if (config.useSubtreeDelete()) { |
| | | deleteRequest.addControl(SubtreeDeleteRequestControl |
| | | if (config.usePermissiveModify()) { |
| | | modifyRequest |
| | | .addControl(PermissiveModifyRequestControl |
| | | .newControl(true)); |
| | | } |
| | | addAssertionControl(deleteRequest, request.getRevision()); |
| | | c.getConnection().applyChangeAsync(deleteRequest, null, |
| | | 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)); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @Override |
| | | public void patchInstance(final ServerContext context, final String resourceId, |
| | | final PatchRequest request, final ResultHandler<Resource> handler) { |
| | | handler.handleError(new NotSupportedException("Not yet implemented")); |
| | | for (final PatchOperation operation : request.getPatchOperations()) { |
| | | attributeMapper.patch(c, new JsonPointer(), operation, handler); |
| | | } |
| | | } |
| | | })); |
| | | } |
| | | |
| | | @Override |
| | |
| | | }; |
| | | |
| | | pendingResourceCount.incrementAndGet(); |
| | | attributeMapper.toJSON(c, new JsonPointer(), entry, mapHandler); |
| | | attributeMapper.read(c, new JsonPointer(), entry, mapHandler); |
| | | return true; |
| | | } |
| | | |
| | |
| | | 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 |
| | |
| | | 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 = |
| | |
| | | * 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 |
| | |
| | | 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) { |
| | |
| | | } |
| | | } |
| | | |
| | | 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( |
| | |
| | | */ |
| | | 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; |
| | |
| | | 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; |
| | |
| | | } |
| | | |
| | | @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()) { |
| | |
| | | } |
| | | |
| | | @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 |
| | |
| | | }, 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, |
| | |
| | | } |
| | | |
| | | @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) { |
| | | try { |
| | | /* |
| | | * Fail immediately if the JSON value has the wrong type or contains |
| | | * unknown attributes. |
| | | * 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 = 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; |
| | | } |
| | | } |
| | | } 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; |
| | | } |
| | | } |
| | | final Map<String, Mapping> missingMappings = checkMapping(path, v); |
| | | |
| | | // 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)); |
| | | 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.toLDAP(c, path.child(me.getKey()), e, subValue, handler); |
| | | 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.toLDAP(c, path.child(mapping.name), e, null, handler); |
| | | 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) { |
| | | throw new BadRequestException(i18n( |
| | | "The request cannot be processed because it included " |
| | | + "an unrecognized field '%s'", path.child(attribute))); |
| | | } |
| | | } |
| | | } else { |
| | | 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)); |
| | | } |
| | | } |
| | | return missingMappings; |
| | | } |
| | | |
| | | private Mapping getMapping(final JsonPointer jsonAttribute) { |
| | |
| | | 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; |
| | |
| | | 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) { |
| | |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult(final List<Modification> result) { |
| | | public void handleResult(final List<Attribute> result) { |
| | | Attribute primaryKeyAttribute = null; |
| | | for (final Modification modification : result) { |
| | | if (modification.getAttribute().getAttributeDescription().equals( |
| | | primaryKey)) { |
| | | primaryKeyAttribute = modification.getAttribute(); |
| | | for (final Attribute attribute : result) { |
| | | if (attribute.getAttributeDescription().equals(primaryKey)) { |
| | | primaryKeyAttribute = attribute; |
| | | break; |
| | | } |
| | | } |
| | | |
| | | if (primaryKeyAttribute == null || primaryKeyAttribute.isEmpty()) { |
| | | h.handleError(new BadRequestException( |
| | | i18n("The request cannot be processed because the reference " |
| | | 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 " |
| | | 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 Filter filter = Filter.equality(primaryKey.toString(), primaryKeyValue); |
| | | final SearchRequest search = createSearchRequest(filter); |
| | | c.getConnection() |
| | | .searchSingleEntryAsync( |
| | | search, |
| | | c.getConnection().searchSingleEntryAsync(search, |
| | | new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() { |
| | | |
| | | @Override |
| | | public void handleErrorResult( |
| | | final ErrorResultException error) { |
| | | public void handleErrorResult(final ErrorResultException error) { |
| | | ResourceException re; |
| | | try { |
| | | throw error; |
| | | } catch (final EntryNotFoundException e) { |
| | | re = |
| | | new BadRequestException( |
| | | i18n("The request cannot be processed " |
| | | new BadRequestException(i18n( |
| | | "The request cannot be processed " |
| | | + "because the resource '%s' " |
| | | + "referenced in field '%s' does " |
| | | + "not exist", |
| | | primaryKeyValue |
| | | .toString(), |
| | | path)); |
| | | + "not exist", primaryKeyValue |
| | | .toString(), path)); |
| | | } catch (final MultipleEntriesFoundException e) { |
| | | re = |
| | | new BadRequestException( |
| | | i18n("The request cannot be processed " |
| | | new BadRequestException(i18n( |
| | | "The request cannot be processed " |
| | | + "because the resource '%s' " |
| | | + "referenced in field '%s' is " |
| | | + "ambiguous", |
| | | primaryKeyValue |
| | | .toString(), |
| | | path)); |
| | | + "ambiguous", primaryKeyValue |
| | | .toString(), path)); |
| | | } catch (final ErrorResultException e) { |
| | | re = asResourceException(e); |
| | | } |
| | |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult( |
| | | final SearchResultEntry result) { |
| | | public void handleResult(final SearchResultEntry result) { |
| | | synchronized (newLDAPAttribute) { |
| | | newLDAPAttribute.add(result.getName()); |
| | | } |
| | |
| | | } |
| | | |
| | | @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()) { |
| | |
| | | |
| | | @Override |
| | | public void handleResult(final SearchResultEntry result) { |
| | | mapper.toJSON(c, path, result, handler); |
| | | mapper.read(c, path, result, handler); |
| | | } |
| | | }); |
| | | } |
| | |
| | | */ |
| | | public final class SimpleAttributeMapper extends AbstractLDAPAttributeMapper<SimpleAttributeMapper> { |
| | | private Function<ByteString, ?, Void> decoder = null; |
| | | |
| | | private Function<Object, ByteString, Void> encoder = null; |
| | | |
| | | SimpleAttributeMapper(final AttributeDescription ldapAttributeName) { |
| | |
| | | } |
| | | |
| | | @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; |
| | |
| | | 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; |
| | |
| | | } |
| | | |
| | | @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 = |