RelationshipProvider.java revision d9b1fcb16f23fb4b520e5f13687b744deeebb03f
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2015 ForgeRock AS.
 */
package org.forgerock.openidm.managed;
import static org.forgerock.json.JsonValue.*;
import static org.forgerock.json.resource.ResourcePath.resourcePath;
import static org.forgerock.json.resource.ResourceResponse.*;
import static org.forgerock.json.resource.Responses.newResourceResponse;
import static org.forgerock.openidm.sync.impl.SynchronizationService.*;
import static org.forgerock.openidm.sync.impl.SynchronizationService.SyncServiceAction.notifyUpdate;
import static org.forgerock.openidm.util.RelationshipUtil.*;
import static org.forgerock.openidm.util.ResourceUtil.*;
import static org.forgerock.util.promise.Promises.newResultPromise;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.forgerock.http.routing.UriRouterContext;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.ActionRequest;
import org.forgerock.json.resource.ActionResponse;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.Connection;
import org.forgerock.json.resource.ConnectionFactory;
import org.forgerock.json.resource.CreateRequest;
import org.forgerock.json.resource.DeleteRequest;
import org.forgerock.json.resource.InternalServerErrorException;
import org.forgerock.json.resource.PatchRequest;
import org.forgerock.json.resource.PreconditionFailedException;
import org.forgerock.json.resource.QueryRequest;
import org.forgerock.json.resource.ReadRequest;
import org.forgerock.json.resource.Request;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.Requests;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourcePath;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.openidm.audit.util.ActivityLogger;
import org.forgerock.openidm.audit.util.Status;
import org.forgerock.openidm.patch.JsonValuePatch;
import org.forgerock.services.context.Context;
import org.forgerock.util.AsyncFunction;
import org.forgerock.util.Function;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.ResultHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class RelationshipProvider {
/**
* Setup logging for the {@link RelationshipProvider}.
*/
private static final Logger logger = LoggerFactory.getLogger(RelationshipProvider.class);
/**
* The activity logger.
*/
protected final ActivityLogger activityLogger;
/**
* A service for sending sync events on managed objects
*/
protected final ManagedObjectSyncService managedObjectSyncService;
/** Used for accessing the repo */
protected final ConnectionFactory connectionFactory;
/** Path to this resource in the repo */
protected static final ResourcePath REPO_RESOURCE_PATH = new ResourcePath("repo", "relationships");
/** The resource container that property associated with this provider is a field of. This will typically be a
* managed object path such as: "managed/user" or "managed/role". */
protected final ResourcePath resourceContainer;
/** The schemaField representing this relationship */
protected final SchemaField schemaField;
/** A {@link JsonPointer} to the property representing this relationship in the parent object. */
protected final JsonPointer propertyPtr;
/** An optimized relationship query ID */
protected static final String RELATIONSHIP_QUERY_ID = "find-relationships-for-resource";
/** A query field representing the full path of the managed object instance of this relationship field */
protected static final String QUERY_FIELD_RESOURCE_PATH = "fullResourceId";
/** A query field representing the field name of this relationship field */
protected static final String QUERY_FIELD_FIELD_NAME = "resourceFieldName";
/** The name of the firstId field in the repo */
protected static final String REPO_FIELD_FIRST_ID = "firstId";
/** The name of the secondId field in the repo */
protected static final String REPO_FIELD_SECOND_ID = "secondId";
/** The name of the firstPropertyName field in the repo */
protected static final String REPO_FIELD_FIRST_PROPERTY_NAME = "firstPropertyName";
/** The name of the secondPropertyName field in the repo */
protected static final String REPO_FIELD_SECOND_PROPERTY_NAME = "secondPropertyName";
/** The name of the properties field coming out of the repo service */
protected static final String REPO_FIELD_PROPERTIES = "properties";
/** The name of the parameter to be used carry the managed object's ID in the Request and/or Context */
public static final String PARAM_MANAGED_OBJECT_ID = "managedObjectId";
/** The name of the properties field in resource response */
public static final JsonPointer FIELD_PROPERTIES = SchemaField.FIELD_PROPERTIES;
/** The name of the secondId field in resource response */
public static final JsonPointer FIELD_REFERENCE = SchemaField.FIELD_REFERENCE;
/** The name of the field containing the id */
public static final JsonPointer FIELD_ID = FIELD_PROPERTIES.child(FIELD_CONTENT_ID);
/** The name of the field containing the revision */
public static final JsonPointer FIELD_REV = FIELD_PROPERTIES.child(FIELD_CONTENT_REVISION);
/**
* The validator responsible for testing if the relationship request is valid.
*/
protected final RelationshipValidator relationshipValidator;
/**
* Returns a Function to format a resource from the repository to that expected by the provider consumer. This is
* simply a wrapper of {@link #formatResponseNoException} with a {@link ResourceException} in the signature to
* allow for use against {@code Promise<ResourceResponse, ResourceException>}
*
* @see #formatResponseNoException(Context, Request)
*/
protected Function<ResourceResponse, ResourceResponse, ResourceException> formatResponse(
final Context context, final Request request) {
return new Function<ResourceResponse, ResourceResponse, ResourceException>() {
@Override
public ResourceResponse apply(ResourceResponse resourceResponse) throws ResourceException {
return formatResponseNoException(context, request).apply(resourceResponse);
}
};
}
/**
* Returns a Function to format a resource from the repository to that expected by the provider consumer. First
* object properties are removed and {@code secondId} (or {@code firstId} if it's a reverse relationship)
* will be converted to {@code _ref}
* <p/>
* This will convert repo resources in the format of:
* <pre>
* {
* "_id": "someId",
* "_rev": "someRev",
* "firstId": "/managed/object/uuid",
* "firstPropertyName": "roles",
* "secondId": "/managed/roles/uuid"
* "properties": { ... }
* }
* </pre>
* <p/>
* To a provider response format of:
*
* <pre>
* {
* "_ref": "/managed/roles/uuid",
* "_refProperties": {
* "_id": "someId",
* "_rev": "someRev",
* ...
* },
* "_refError": true,
* "_refErrorMessage": "some error message"
* }
* </pre>
*/
protected Function<ResourceResponse, ResourceResponse, NeverThrowsException> formatResponseNoException(
final Context context, final Request request) {
return new Function<ResourceResponse, ResourceResponse, NeverThrowsException>() {
public String resourceFullPath = getResourceFullPath(context, request).toString();
@Override
public ResourceResponse apply(final ResourceResponse raw) {
final JsonValue rawContent = raw.getContent();
final JsonValue formatted = json(object());
final Map<String, Object> properties = new LinkedHashMap<>();
final Map<String, Object> repoProperties = rawContent.get(REPO_FIELD_PROPERTIES).asMap();
final String ref;
// set the field reference
if (schemaField.isReverseRelationship()
&& !rawContent.get(REPO_FIELD_FIRST_ID).asString().equals(resourceFullPath)) {
ref = rawContent.get(REPO_FIELD_FIRST_ID).asString();
} else {
ref = rawContent.get(REPO_FIELD_SECOND_ID).asString();
}
if (repoProperties != null) {
properties.putAll(repoProperties);
}
properties.put(FIELD_CONTENT_ID, raw.getId());
properties.put(FIELD_CONTENT_REVISION, raw.getRevision());
formatted.put(SchemaField.FIELD_REFERENCE, ref);
formatted.put(SchemaField.FIELD_PROPERTIES, properties);
// If has error, append error flag and message.
if (rawContent.get(REFERENCE_ERROR).defaultTo(false).asBoolean()) {
formatted.put(REFERENCE_ERROR, true);
formatted.put(REFERENCE_ERROR_MESSAGE,
rawContent.get(REFERENCE_ERROR_MESSAGE).defaultTo("").asString());
}
// Return the resource without _id or _rev
return newResourceResponse(null, null, formatted);
}
};
}
/**
* On a create of a relationship, this will sync the referenced object after the update is completed.
*/
private final SyncReferencedObjectRequestHandler<CreateRequest> syncReferencedObjectCreateHandler =
new SyncReferencedObjectRequestHandler<CreateRequest>() {
@Override
protected Promise<ResourceResponse, ResourceException> invokeRequest(Context context,
CreateRequest request) throws ResourceException {
return getConnection().createAsync(context, request);
}
};
/**
* On a update of a relationship, this will sync the referenced object after the update is completed.
*/
private final SyncReferencedObjectRequestHandler<UpdateRequest> syncReferencedObjectUpdateHandler =
new SyncReferencedObjectRequestHandler<UpdateRequest>() {
@Override
protected Promise<ResourceResponse, ResourceException> invokeRequest(Context context,
UpdateRequest request) throws ResourceException {
return getConnection().updateAsync(context, request);
}
};
/**
* On a delete of a relationship, this will sync the referenced object after the delete is completed.
*/
private final SyncReferencedObjectRequestHandler<DeleteRequest> syncReferencedObjectDeleteHandler =
new SyncReferencedObjectRequestHandler<DeleteRequest>() {
@Override
protected Promise<ResourceResponse, ResourceException> invokeRequest(Context context,
DeleteRequest request) throws ResourceException {
return getConnection().deleteAsync(context, request);
}
};
/**
* Get a new {@link RelationshipProvider} instance associated with the given resource path and field
*
* @param connectionFactory The connection factory used to access the repository
* @param resourcePath Path of the resource to provide relationships for
* @param relationshipField Field on the resource representing the provided relationship
* @return A new relationship provider instance
*/
public static RelationshipProvider newProvider(final ConnectionFactory connectionFactory,
final ResourcePath resourcePath, final SchemaField relationshipField, final ActivityLogger activityLogger,
final ManagedObjectSyncService managedObjectSyncService) {
if (relationshipField.isArray()) {
return new CollectionRelationshipProvider(connectionFactory, resourcePath, relationshipField,
activityLogger, managedObjectSyncService);
} else {
return new SingletonRelationshipProvider(connectionFactory, resourcePath, relationshipField,
activityLogger, managedObjectSyncService);
}
}
/**
* Create a new relationship set for the given managed resource
*
* @param connectionFactory Connection factory used to access the repository
* @param resourcePath Name of the resource we are handling relationships for eg. managed/user
* @param schemaField The field used to represent this relationship in the parent object
* @param activityLogger The audit activity logger to use
* @param managedObjectSyncService Service to send sync events to
*/
protected RelationshipProvider(final ConnectionFactory connectionFactory, final ResourcePath resourcePath,
final SchemaField schemaField, final ActivityLogger activityLogger,
final ManagedObjectSyncService managedObjectSyncService) {
this.connectionFactory = connectionFactory;
this.resourceContainer = resourcePath;
this.schemaField = schemaField;
this.propertyPtr = new JsonPointer(schemaField.getName());
this.activityLogger = activityLogger;
this.managedObjectSyncService = managedObjectSyncService;
this.relationshipValidator = (schemaField.isReverseRelationship())
? new ReverseRelationshipValidator(this)
: new ForwardRelationshipValidator(this);
}
/**
* Return a {@link RequestHandler} instance representing this provider
* @return a {@link RequestHandler} instance representing this provider
*/
public abstract RequestHandler asRequestHandler();
/**
* Get the full relationship representation for this provider as a JsonValue.
*
* @param context Context of this request
* @param resourceId Id of resource to fetch relationships on
*
* @return A promise containing the full representation of the relationship on the supplied resourceId
* or a ResourceException if an error occurred
*/
public abstract Promise<JsonValue, ResourceException> getRelationshipValueForResource(Context context,
String resourceId);
/**
* Set the supplied {@link JsonValue} as the current state of this relationship. This will support updating any
* existing relationship (_id is present) and remove any relationship not present in the value from the repository.
*
* @param clearExisting If existing (non-present) relationships should be cleared
* @param context The context of this request
* @param resourceId Id of the resource relation fields in value are to be memebers of
* @param value A {@link JsonValue} map of relationship fields and their values
*
* @throws NullPointerException If the supplied value is null
*
* @return A promise containing a JsonValue of the persisted relationship(s) for the given resourceId or
* ResourceException if an error occurred
*/
public abstract Promise<JsonValue, ResourceException> setRelationshipValueForResource(boolean clearExisting,
final Context context, final String resourceId, final JsonValue value);
/**
* Clear any relationship associated with the given resource. This could be used if for example a resource no longer
* exists.
*
* @param context The current context.
* @param resourceId The resource whose relationship we wish to clear
*
*/
public abstract Promise<JsonValue, ResourceException> clear(Context context, String resourceId);
/**
* Tests that all references in the relationship field are valid according to this provider's validator.
*
* @param context context of the request working with the relationship.
* @param oldValue old value of field to refer to during validation of the newValue
* @param newValue new value of field to validate
* @throws ResourceException BadRequestException if the relationship is found to be not valid, otherwise for other
* issues.
*/
public abstract void validateRelationshipField(Context context, JsonValue oldValue, JsonValue newValue)
throws ResourceException;
/**
* Creates a relationship object.
*
* @param context The current context.
* @param request The current request.
* @return A promise containing the created relationship object
*/
public Promise<ResourceResponse, ResourceException> createInstance(final Context context,
final CreateRequest request) {
try {
final CreateRequest createRequest = Requests.copyOfCreateRequest(request);
createRequest.setResourcePath(REPO_RESOURCE_PATH);
createRequest.setContent(convertToRepoObject(firstResourcePath(context, request), request.getContent()));
// If the request is from ManagedObjectSet then create and return the promise after formatting.
if (context.containsContext(ManagedObjectContext.class)) {
return syncReferencedObjectCreateHandler
.performRequest(request.getContent().get(REFERENCE_ID).asString(), createRequest, context)
.then(formatResponse(context, request));
}
// Get the before value of the managed object
final ResourceResponse beforeValue = getManagedObject(context);
// Create the relationship
ResourceResponse response = syncReferencedObjectCreateHandler
.performRequest(request.getContent().get(REFERENCE_ID).asString(), createRequest, context)
.then(formatResponse(context, request))
.getOrThrow();
// Get the before value of the managed object
final ResourceResponse afterValue = getManagedObject(context);
// Do activity logging.
// Log an "update" for the managed object, even though this is a "create" request on relationship field.
activityLogger.log(context, request, "update", getManagedObjectPath(context), beforeValue.getContent(),
afterValue.getContent(), Status.SUCCESS);
// Changes to the relationship will trigger sync on the managed object that this field belongs to.
managedObjectSyncService.performSyncAction(context, request, getManagedObjectId(context),
notifyUpdate, beforeValue.getContent(), afterValue.getContent());
return expandFields(context, request, response);
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
return new InternalServerErrorException(e.getMessage(), e).asPromise();
}
}
/**
* Reads a relationship object.
*
* @param context The current context.
* @param relationshipId The relationship id.
* @param request The current request.
* @return A promise containing the relationship object
*/
public Promise<ResourceResponse, ResourceException> readInstance(Context context, String relationshipId,
ReadRequest request) {
try {
final ReadRequest readRequest = Requests.newReadRequest(REPO_RESOURCE_PATH.child(relationshipId));
Promise<ResourceResponse, ResourceException> promise =
getConnection().readAsync(context, readRequest).then(formatResponse(context, request));
// If the request is from ManagedObjectSet then create and return the promise after formatting.
if (context.containsContext(ManagedObjectContext.class)) {
return promise;
}
// Read the relationship
final ResourceResponse response = promise.getOrThrow();
// Get the value of the managed object
final ResourceResponse value = getManagedObject(context);
// Do activity logging.
activityLogger.log(context, request, "read", getManagedObjectPath(context), null, value.getContent(),
Status.SUCCESS);
return expandFields(context, request, response);
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
return new InternalServerErrorException(e.getMessage(), e).asPromise();
}
}
/**
* Updates a relationship object.
*
* @param context The current context.
* @param relationshipId The relationship id.
* @param request The current request.
* @return A promise containing the updated relationship object
*/
public Promise<ResourceResponse, ResourceException> updateInstance(final Context context,
final String relationshipId, final UpdateRequest request) {
try {
final String rev = request.getRevision();
final ReadRequest readRequest = Requests.newReadRequest(REPO_RESOURCE_PATH.child(relationshipId));
final JsonValue newValue = convertToRepoObject(firstResourcePath(context, request), request.getContent());
// If the request is from ManagedObjectSet then update (if changed) and return the promise after formatting.
if (context.containsContext(ManagedObjectContext.class)) {
return getConnection()
// current resource in the db
.readAsync(context, readRequest)
// update once we have the current record
.thenAsync(new AsyncFunction<ResourceResponse, ResourceResponse, ResourceException>() {
@Override
public Promise<ResourceResponse, ResourceException> apply(ResourceResponse oldResource)
throws ResourceException {
return updateIfChanged(context, request, relationshipId, rev, oldResource, newValue);
}
});
}
ResourceResponse result;
// Get the before value of the managed object
final ResourceResponse beforeValue = getManagedObject(context);
// Read the relationship
final ResourceResponse oldResource = getConnection().readAsync(context, readRequest).getOrThrow();
// Perform update
result = updateIfChanged(context, request, relationshipId, rev, oldResource, newValue).getOrThrow();
// Get the after value of the managed object
final ResourceResponse afterValue = getManagedObject(context);
// Do activity logging.
activityLogger.log(context, request, "update", getManagedObjectPath(context), beforeValue.getContent(),
afterValue.getContent(), Status.SUCCESS);
// Changes to the relationship will trigger sync on the managed object that this field belongs to.
managedObjectSyncService.performSyncAction(context, request, getManagedObjectId(context),
notifyUpdate, beforeValue.getContent(), afterValue.getContent());
return expandFields(context, request, result);
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
return new InternalServerErrorException(e.getMessage(), e).asPromise();
}
}
/**
* Deletes a relationship object.
*
* @param context The current context.
* @param relationshipId The relationship id.
* @param request The current request.
* @return A promise containing the deleted relationship object
*/
public Promise<ResourceResponse, ResourceException> deleteInstance(final Context context,
final String relationshipId, final DeleteRequest request) {
final ResourcePath path = REPO_RESOURCE_PATH.child(relationshipId);
final DeleteRequest deleteRequest = Requests.copyOfDeleteRequest(request);
deleteRequest.setResourcePath(path);
try {
// If the request is from ManagedObjectSet then delete and return the promise after formatting.
if (context.containsContext(ManagedObjectContext.class)) {
return deleteAsync(context, path, deleteRequest);
}
// The result of the delete
ResourceResponse result;
// Get the before value of the managed object
final ResourceResponse beforeValue = getManagedObject(context);
// Perform the delete and wait for result
result = deleteAsync(context, path, deleteRequest).getOrThrow();
// Get the after value of the managed object
final ResourceResponse afterValue = getManagedObject(context);
// Do activity logging.
activityLogger.log(context, request, "delete", getManagedObjectPath(context), beforeValue.getContent(),
null, Status.SUCCESS);
// Changes to the relationship will trigger sync on the managed object that this field belongs to.
managedObjectSyncService.performSyncAction(context, request, getManagedObjectId(context),
notifyUpdate, beforeValue.getContent(), afterValue.getContent());
return expandFields(context, request, result);
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
return new InternalServerErrorException(e.getMessage(), e).asPromise();
}
}
/**
* Performs the deletion of a relationship object.
*
* @param context the current context
* @param path the path to the relationship field
* @param deleteRequest the current delete request
* @return a Promise representing the result of the delete operation
*/
private Promise<ResourceResponse, ResourceException> deleteAsync(final Context context, final ResourcePath path,
final DeleteRequest deleteRequest) {
try {
// Read the relationship that needs to be deleted.
return getConnection().readAsync(context, Requests.newReadRequest(path))
.thenAsync(new AsyncFunction<ResourceResponse, ResourceResponse, ResourceException>() {
/**
* Sets the revision on the request if needed, and then performs the delete via the
* syncReferencedObjectDeleteHandler.
*
* @param readResponse the response from reading the relationship.
* @return the promise of the delete request.
* @throws ResourceException
* @see SyncReferencedObjectRequestHandler
*/
public Promise<ResourceResponse, ResourceException> apply(final ResourceResponse readResponse)
throws ResourceException {
if (deleteRequest.getRevision() == null) {
deleteRequest.setRevision(readResponse.getRevision());
}
return syncReferencedObjectDeleteHandler.performRequest(readResponse.getContent(),
deleteRequest, context);
}
}).then(formatResponse(context, deleteRequest));
} catch (ResourceException e) {
return e.asPromise();
}
}
/**
* Updates the relationship object if the value has changed and returns a formatted result.
*
* @param context the current context
* @param request The current request.
* @param id the id of the relationship object
* @param rev the revision of the relationship object
* @param oldResource the old value of the relationship object
* @param newValue the new value of the relationship object
* @return the updated, formatted relationship object
* @throws ResourceException
*/
private Promise<ResourceResponse, ResourceException> updateIfChanged(final Context context, Request request,
String id, String rev, ResourceResponse oldResource, JsonValue newValue) throws ResourceException {
// Find changes, ignoring ID and REV as the newValue won't have those set.
if (isEqual(oldResource.getContent(), newValue)) {
// resource has not changed, return the old resource
return newResourceResponse(oldResource.getId(), oldResource.getRevision(), oldResource.getContent())
.asPromise()
.then(formatResponse(context, request));
} else {
// resource has changed, update the relationship
UpdateRequest updateRequest =
Requests.newUpdateRequest(REPO_RESOURCE_PATH.child(id), newValue).setRevision(rev);
return syncReferencedObjectUpdateHandler
.performRequest(newValue, updateRequest, context)
.then(formatResponse(context, request));
}
}
/**
* Patch a relationship instance. Used by RequestHandler child classes.
*
* @param context the current context
* @param relationshipId The id of the relationship instance to patch
* @param request The patch request
* @return A promised patch response or exception
*/
public Promise<ResourceResponse, ResourceException> patchInstance(Context context, String relationshipId,
PatchRequest request) {
Promise<ResourceResponse, ResourceException> promise = null;
String revision = request.getRevision();
boolean forceUpdate = (revision == null);
boolean retry = forceUpdate;
boolean fromManagedObjectSet = context.containsContext(ManagedObjectContext.class);
do {
logger.debug("Attempting to patch relationship {}", request.getResourcePath());
try {
// Read in object
ReadRequest readRequest = Requests.newReadRequest(REPO_RESOURCE_PATH.child(relationshipId));
ResourceResponse oldResource = connectionFactory.getConnection().readAsync(context, readRequest)
.then(formatResponse(context, request)).getOrThrow();
// If we haven't defined a revision, we need to get the current revision
if (revision == null) {
revision = oldResource.getRevision();
}
ResourceResponse managedObjectBefore = null;
if (!fromManagedObjectSet) {
// Get the before value of the managed object
managedObjectBefore = getManagedObject(context);
}
JsonValue newValue = oldResource.getContent().copy();
boolean modified = JsonValuePatch.apply(newValue, request.getPatchOperations());
if (!modified) {
logger.debug("Patching did not modify the relatioship {}", request.getResourcePath());
return newResultPromise(null);
}
// Update (if changed) and format
promise = updateIfChanged(context, request, relationshipId, revision,
newResourceResponse(oldResource.getId(), oldResource.getRevision(),
convertToRepoObject(firstResourcePath(context, request), oldResource.getContent())),
convertToRepoObject(firstResourcePath(context, request), newValue));
// If the request is from ManagedObjectSet then return the promise
if (fromManagedObjectSet) {
return promise;
}
// Get the before value of the managed object
final ResourceResponse managedObjectAfter = getManagedObject(context);
// Do activity logging.
activityLogger.log(context, request, "update", getManagedObjectPath(context),
managedObjectBefore.getContent(), managedObjectAfter.getContent(), Status.SUCCESS);
// Changes to the relationship will trigger sync on the managed object that this field belongs to.
managedObjectSyncService.performSyncAction(context, request, getManagedObjectId(context),
notifyUpdate, managedObjectBefore.getContent(),
managedObjectAfter.getContent());
retry = false;
logger.debug("Patch retationship successful!");
} catch (PreconditionFailedException e) {
if (forceUpdate) {
logger.debug("Unable to update due to revision conflict. Retrying.");
} else {
// If it fails and we're not trying to force an update, we gave it our best shot
return e.asPromise();
}
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
return new InternalServerErrorException(e.getMessage(), e).asPromise();
}
} while (retry);
// Return the result
return promise;
}
/**
* Perform an action on a relationship instance. Used by child RequsetHandler classes.
*
* @param context the current context
* @param relationshipId The id of the relationship instance to perform the action on
* @param request The action request
* @return A promised action response or exception
*/
public Promise<ActionResponse, ResourceException> actionInstance(Context context, String relationshipId,
ActionRequest request) {
return notSupportedOnInstance(request).asPromise();
}
/**
* Returns the path of the first resource in this relationship using the firstId parameter from either the URI or
* the Request. If firstId is not found in the URI context then the request parameter is used.
*
* @param context Context containing a {@link UriRouterContext} to check for template variables
* @param request Request containing a fall-back firstId parameter
*
* @see #resourceContainer
*
* @return The resource path of the first resource as a child of resourcePath
*/
protected final ResourcePath firstResourcePath(final Context context, final Request request)
throws BadRequestException {
final String uriFirstId =
context.asContext(UriRouterContext.class).getUriTemplateVariables().get(PARAM_MANAGED_OBJECT_ID);
final String firstId = uriFirstId != null ? uriFirstId : request.getAdditionalParameter(PARAM_MANAGED_OBJECT_ID);
if (StringUtils.isNotBlank(firstId)) {
return resourceContainer.child(firstId);
} else {
throw new BadRequestException("Required either URI parameter " + PARAM_MANAGED_OBJECT_ID +
" or request paremeter " + PARAM_MANAGED_OBJECT_ID + " but none were found.");
}
}
/**
* Returns the full path of the resource in this relationship using the managedObjectId parameter from either the
* URI or the Request. If managedObejctId is not found in the URI context then the request parameter is used.
*
* @param context Context containing a {@link UriRouterContext} to check for template variables
* @param request Request containing a fall-back firstId parameter
*
* @see #resourceContainer
*
* @return The resource path of the first resource as a child of resourcePath
*/
protected final ResourcePath getResourceFullPath(final Context context, final Request request) {
try {
return firstResourcePath(context, request);
} catch (BadRequestException e) {
logger.error("Error getting resource path", e);
}
return null;
}
/**
* Convert the given incoming request object to repo format.
*
* This converts _ref fields to secondId and populates first* fields.
*
* @param firstResourcePath The path of the first object in a relationship instance
* @param object A {@link JsonValue} object from a resource response or incoming request to be converted for
* storage in the repo
*
* @return A new JsonValue containing the converted object in a format accepted by the repo
* @see #formatResponseNoException(Context, Request)
*/
protected JsonValue convertToRepoObject(final ResourcePath firstResourcePath, final JsonValue object) {
final JsonValue properties = object.get(FIELD_PROPERTIES);
if (properties != null) {
// Remove "soft" fields that were placed in properties for the ResourceResponse
properties.remove(FIELD_CONTENT_ID);
properties.remove(FIELD_CONTENT_REVISION);
}
if (schemaField.isReverseRelationship()) {
// Compare the resource paths and set firstId/secondId and firstPropertyName/secondPropertyName based on
// lexicographic ordering to ensure consistency for reverse (bidirectional) relationships.
// We want to prevent the case where a bidirectional relationship may be updated from a operation on either
// end resulting in the firstId/firstPropName and secondId/secondPropName getting swapped.
if (firstResourcePath.toString().compareTo(object.get(FIELD_REFERENCE).asString()) < 0) {
return json(object(
field(REPO_FIELD_FIRST_ID, firstResourcePath.toString()),
field(REPO_FIELD_FIRST_PROPERTY_NAME, schemaField.getName()),
field(REPO_FIELD_SECOND_ID, object.get(FIELD_REFERENCE).asString()),
field(REPO_FIELD_SECOND_PROPERTY_NAME, schemaField.getReversePropertyName()),
field(REPO_FIELD_PROPERTIES, properties == null ? null : properties.asMap())
));
} else {
return json(object(
field(REPO_FIELD_FIRST_ID, object.get(FIELD_REFERENCE).asString()),
field(REPO_FIELD_FIRST_PROPERTY_NAME, schemaField.getReversePropertyName()),
field(REPO_FIELD_SECOND_ID, firstResourcePath.toString()),
field(REPO_FIELD_SECOND_PROPERTY_NAME, schemaField.getName()),
field(REPO_FIELD_PROPERTIES, properties == null ? null : properties.asMap())
));
}
} else {
return json(object(
field(REPO_FIELD_FIRST_ID, firstResourcePath.toString()),
field(REPO_FIELD_FIRST_PROPERTY_NAME, schemaField.getName()),
field(REPO_FIELD_SECOND_ID, object.get(FIELD_REFERENCE).asString()),
field(REPO_FIELD_SECOND_PROPERTY_NAME, null),
field(REPO_FIELD_PROPERTIES, properties == null ? null : properties.asMap())
));
}
}
/**
* Returns the managed object's ID corresponding to the passed in {@link Context}.
*
* @param context the Context object.
* @return a String representing the managed object's ID.
*/
protected String getManagedObjectId(Context context) {
return context.asContext(UriRouterContext.class).getUriTemplateVariables().get(PARAM_MANAGED_OBJECT_ID);
}
/**
* Returns the managed object's full path corresponding to the passed in {@link Context}.
*
* @param context the {@link Context} object.
* @return a String representing the managed object's ID.
*/
protected String getManagedObjectPath(Context context) {
return resourceContainer.child(getManagedObjectId(context)).toString();
}
/**
* Reads and returns the managed object associated with the specified context.
*
* @param context the {@link Context} object.
* @return the managed object.
* @throws ResourceException if an error was encountered while reading the managed object.
*/
protected ResourceResponse getManagedObject(Context context) throws ResourceException {
String managedObjectPath = resourceContainer.child(getManagedObjectId(context)).toString();
return getConnection().read(context, Requests.newReadRequest(managedObjectPath));
}
/**
* Returns a {@link Connection} object.
*
* @return a {@link Connection} object.
* @throws ResourceException
*/
protected Connection getConnection() throws ResourceException {
return connectionFactory.getConnection();
}
/**
* Performs resourceExpansion on the supplied response based on the fields specified in the current request.
*
* @param context the current {@link Context} object
* @param request the current {@link Request} object
* @param response the {@link ResourceResponse} to expand fields on.
* @return A promise containing the response with the expanded fields, if any.
* @throws ResourceException
*/
protected Promise<ResourceResponse, ResourceException> expandFields(final Context context, final Request request,
ResourceResponse response) throws ResourceException {
List<JsonPointer> refFields = new ArrayList<JsonPointer>();
List<JsonPointer> otherFields = new ArrayList<JsonPointer>();
for (JsonPointer field : request.getFields()) {
if (!field.toString().startsWith(SchemaField.FIELD_REFERENCE.toString())
&& !field.toString().startsWith(SchemaField.FIELD_PROPERTIES.toString())) {
refFields.add(field);
} else {
otherFields.add(field);
}
}
if (!refFields.isEmpty()) {
// Perform the field expansion
ReadRequest readRequest =
Requests.newReadRequest(response.getContent().get(SchemaField.FIELD_REFERENCE).asString());
readRequest.addField(refFields.toArray(new JsonPointer[refFields.size()]));
ResourceResponse readResponse = getConnection().read(context, readRequest);
response.getContent().asMap().putAll(readResponse.getContent().asMap());
for (JsonPointer field : otherFields) {
response.addField(field);
}
for (JsonPointer field : refFields) {
if (field.equals(new JsonPointer("*"))) {
response.addField(new JsonPointer(""));
} else {
response.addField(field);
}
}
}
return newResultPromise(response);
}
/**
* When modifying bidirectional relationships, the objects that refer to the relationship should be sync'd when
* the request is made. The direct object will already be sync'd. Using this class will also sync the
* referenced object.
* <p/>
* For example: removing a role from a user via the following command will need to also sync the user linked by
* the member id.
* <pre>
* curl -X DELETE -H "X-OpenIDM-Password: openidm-admin" -H "X-OpenIDM-Username: openidm-admin"
* -H "Content-Type: application/json" -H "Cache-Control: no-cache"
* 'http://localhost:8080/openidm/managed/role/sample-role-1/members/4dc21ceb-ef4c-4006-9188-06236f11c0b1'
* </pre>
* <p/>
* The steps to perform the request and sync the referenced object is:
* <ol>
* <li>Determine the ID of the referenced object on the opposite side of 'this' relationship.</li>
* <li>Read the referenced object before the request is made, to save the 'before'.</li>
* <li>invoke the request. invokeRequest()</li>
* <li>Read the referenced object to retrieve the 'after'.</li>
* <li>Perform sync on the referenced object.</li>
* <li>Return the results of invokeRequest()</li>
* </ol>
*
* @param <T> The Type of Request that will be made.
*/
private abstract class SyncReferencedObjectRequestHandler<T extends Request> {
/**
* Determines the reverse property ID using the reversePropertyName for the relationship being supported,
* then calls #performRequest(String, Request, Context) with the id.
*
* @param relationshipJson the relationship json to determine the referenceToSync.
* @param request the request to make on the relationship.
* @param context context of the call.
* @return the results of the invokeRequest once the sync is promised.
* @throws ResourceException
* @see #performRequest(String, Request, Context)
*/
public final Promise<ResourceResponse, ResourceException> performRequest(final JsonValue relationshipJson,
final T request, final Context context) throws ResourceException {
// If not in a bidirectional (aka reverse) relationship, then referenced object doesn't need to sync.
if (!isReverseSyncNeeded()) {
return invokeRequest(context, request);
}
return performRequest(isReversePropertyFirst(relationshipJson)
? relationshipJson.get(REPO_FIELD_FIRST_ID).asString()
: relationshipJson.get(REPO_FIELD_SECOND_ID).asString(),
request, context);
}
/**
* Performs the request once it has gathered the before state of the referenced object and then will execute
* a sync on the referenced object.
*
* @param refToSync the reference to the reverse property that the sync will be applied to.
* @param request the request to make on the relationship.
* @param context context of the call.
* @return the results of the invokeRequest once the sync is promised.
* @throws ResourceException
*/
public final Promise<ResourceResponse, ResourceException> performRequest(final String refToSync,
final T request, final Context context) throws ResourceException {
// If not in a bidirectional (aka reverse) relationship, then referenced object doesn't need to sync.
if (!isReverseSyncNeeded()) {
return invokeRequest(context, request);
}
// First read the state of the referenced object to save the before.
return getConnection().readAsync(context, Requests.newReadRequest(refToSync))
.thenAsync(
//make the request
new AsyncFunction<ResourceResponse, ResourceResponse, ResourceException>() {
@Override
public Promise<ResourceResponse, ResourceException> apply(final ResourceResponse before)
throws ResourceException {
return invokeRequest(context, request)
// Perform the sync after reading the new state of the referenced obj.
.thenOnResult(
new PerformSyncHandler<>(context, refToSync, request, before));
}
}, new AsyncFunction<ResourceException, ResourceResponse, ResourceException>() {
@Override
public Promise<ResourceResponse, ResourceException> apply(ResourceException e)
throws ResourceException {
// Since the read failed, the sync can't happen, but we still want to proceed with
// the request on the relationship.
logger.warn("Unable to read '{}', no sync will occur", refToSync);
return invokeRequest(context, request);
}
});
}
/**
* Implementers should invoke the call that actually performs the request on the relationship that affects
* the referenced side of this relationship.
*
* @param context context of the request.
* @param request the request to be made.
* @return the promise of the request execution.
* @throws ResourceException
*/
protected abstract Promise<ResourceResponse, ResourceException> invokeRequest(Context context, T request)
throws ResourceException;
/**
* After the makeRequest is made this function will when lookup the referenced object to collect the 'after'
* state; then this will call a sync on the referenced object.
*
* @param <U> The Type of Request that was made.
*/
private class PerformSyncHandler<U extends Request> implements ResultHandler<ResourceResponse> {
private final Context context;
private final String referenceToSync;
private final U request;
private final ResourceResponse before;
/**
* Constructs the Sync handler with state needed to call sync on the referenced object.
*
* @param context context of the request made on the relationship.
* @param request the original request made on the relationship to be sent to the sync call.
* @param before the state of the referenced object before the request on the relationship is made.
* @param referenceToSync the resource path the the object that needs to get synced.
*/
public PerformSyncHandler(Context context, String referenceToSync, U request, ResourceResponse before) {
this.context = context;
this.referenceToSync = referenceToSync;
this.request = request;
this.before = before;
}
@Override
public void handleResult(ResourceResponse invokeResponse) {
try {
// now re-read the referenced object to see the aftermath of the request
ResourceResponse afterResponse = getConnection()
.read(context, Requests.newReadRequest(referenceToSync));
// now perform the sync
logger.debug("after relationship change on {}{}, making sync request on {}", resourceContainer,
schemaField.getName(), referenceToSync);
ResourcePath resourcePath = resourcePath(referenceToSync);
final ActionRequest syncRequest = Requests.newActionRequest("sync", notifyUpdate.name())
.setAdditionalParameter(ACTION_PARAM_RESOURCE_CONTAINER, resourcePath.parent().toString())
.setAdditionalParameter(ACTION_PARAM_RESOURCE_ID, resourcePath.leaf())
.setContent(
json(
object(
field("oldValue", before.getContent().getObject()),
field("newValue", afterResponse.getContent().getObject()))
));
getConnection().action(context, syncRequest);
} catch (Exception e) {
logger.warn("request on relationship was successful, however the reverse referenced object " +
referenceToSync + " failed to request a sync.", e);
}
}
}
}
/**
* Sync on the reverse relationship is only possible and needed on reverse relationships and if the reverse
* property name is set correctly.
*
* @return true if isReverseRelationship and the reversePropertyName is set.
*/
private boolean isReverseSyncNeeded() {
return schemaField.isReverseRelationship() && null != schemaField.getReversePropertyName();
}
/**
* Given a relationship Json this will determine if "this" relationship's reversePropertyName is equal to the
* relationship's first property.
* <p/>
* For example: given the reverse property name of "/members" in a role managed object and relationshipJson like
* below, this would return true.
* <pre>
* {
* "firstId": "managed/role/sample-role-1",
* "firstPropertyName": "/members",
* "secondId": "managed/user/test",
* "secondPropertyName": "/roles",
* "properties": { "name": "samplerole1" },
* "_id": "56733e76-8ca2-4df2-ad3c-bfd7a9d88d47",
* "_rev": "1"
* }
* </pre>
*
* @param relationshipJson The repo json of the relationship.
* @return true if the repo json's firstPropertyName matches this relationship's reversePropertyName
*/
private boolean isReversePropertyFirst(JsonValue relationshipJson) {
return relationshipJson.get(REPO_FIELD_FIRST_PROPERTY_NAME).asString().equals(schemaField.getReversePropertyName());
}
public SchemaField getSchemaField() {
return schemaField;
}
}