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.
*/
import static org.forgerock.openidm.sync.impl.SynchronizationService.SyncServiceAction.notifyUpdate;
public abstract class RelationshipProvider {
/**
* Setup logging for the {@link RelationshipProvider}.
*/
/**
* 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 */
/** The resource container that property associated with this provider is a field of. This will typically be a
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 */
/** A query field representing the full path of the managed object instance of this relationship field */
/** A query field representing the field name of this relationship field */
/** The name of the firstId field in the repo */
/** The name of the secondId field in the repo */
/** The name of the firstPropertyName field in the repo */
/** The name of the secondPropertyName field in the repo */
/** The name of the properties field coming out of the repo service */
/** The name of the parameter to be used carry the managed object's ID in the Request and/or Context */
/** The name of the properties field in resource response */
/** The name of the secondId field in resource response */
/** The name of the field containing the id */
/** The name of the field containing the 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)
*/
}
};
}
/**
* 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",
* "firstPropertyName": "roles",
* "properties": { ... }
* }
* </pre>
* <p/>
* To a provider response format of:
*
* <pre>
* {
* "_refProperties": {
* "_id": "someId",
* "_rev": "someRev",
* ...
* },
* "_refError": true,
* "_refErrorMessage": "some error message"
* }
* </pre>
*/
protected Function<ResourceResponse, ResourceResponse, NeverThrowsException> formatResponseNoException(
// set the field reference
} else {
}
if (repoProperties != null) {
}
// If has error, append error flag and message.
}
// Return the resource without _id or _rev
}
};
}
/**
* On a create of a relationship, this will sync the referenced object after the update is completed.
*/
}
};
/**
* On a update of a relationship, this will sync the referenced object after the update is completed.
*/
}
};
/**
* On a delete of a relationship, this will sync the referenced object after the delete is completed.
*/
}
};
/**
* 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
*/
final ResourcePath resourcePath, final SchemaField relationshipField, final ActivityLogger activityLogger,
if (relationshipField.isArray()) {
} else {
}
}
/**
* Create a new relationship set for the given managed resource
*
* @param connectionFactory Connection factory used to access the repository
* @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,
this.connectionFactory = connectionFactory;
this.resourceContainer = resourcePath;
this.schemaField = schemaField;
this.activityLogger = activityLogger;
? 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,
/**
* 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,
/**
* 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
*
*/
/**
* 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
*/
final CreateRequest request) {
try {
createRequest.setContent(convertToRepoObject(firstResourcePath(context, request), request.getContent()));
// If the request is from ManagedObjectSet then create and return the promise after formatting.
}
// Get the before value of the managed object
// Create the relationship
.getOrThrow();
// Get the before value of the managed object
// 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(),
// Changes to the relationship will trigger sync on the managed object that this field belongs to.
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
}
}
/**
* 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,
try {
// If the request is from ManagedObjectSet then create and return the promise after formatting.
return promise;
}
// Read the relationship
// Get the value of the managed object
// Do activity logging.
activityLogger.log(context, request, "read", getManagedObjectPath(context), null, value.getContent(),
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
}
}
/**
* 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
*/
try {
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.
return getConnection()
// current resource in the db
// update once we have the current record
throws ResourceException {
}
});
}
// Get the before value of the managed object
// Read the relationship
// Perform update
result = updateIfChanged(context, request, relationshipId, rev, oldResource, newValue).getOrThrow();
// Get the after value of the managed object
// Do activity logging.
activityLogger.log(context, request, "update", getManagedObjectPath(context), beforeValue.getContent(),
// Changes to the relationship will trigger sync on the managed object that this field belongs to.
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
}
}
/**
* 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
*/
try {
// If the request is from ManagedObjectSet then delete and return the promise after formatting.
}
// The result of the delete
// Get the before value of the managed object
// Perform the delete and wait for result
// Get the after value of the managed object
// Do activity logging.
activityLogger.log(context, request, "delete", getManagedObjectPath(context), beforeValue.getContent(),
// Changes to the relationship will trigger sync on the managed object that this field belongs to.
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
}
}
/**
* 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.
/**
* 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
*/
throws ResourceException {
}
}
} 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,
// Find changes, ignoring ID and REV as the newValue won't have those set.
// resource has not changed, return the old resource
return newResourceResponse(oldResource.getId(), oldResource.getRevision(), oldResource.getContent())
.asPromise()
} else {
// resource has changed, update the relationship
}
}
/**
* 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,
boolean retry = forceUpdate;
do {
try {
// Read in object
// If we haven't defined a revision, we need to get the current revision
}
if (!fromManagedObjectSet) {
// Get the before value of the managed object
}
if (!modified) {
return newResultPromise(null);
}
// Update (if changed) and format
// If the request is from ManagedObjectSet then return the promise
if (fromManagedObjectSet) {
return promise;
}
// Get the before value of the managed object
// Do activity logging.
// Changes to the relationship will trigger sync on the managed object that this field belongs to.
retry = false;
} catch (PreconditionFailedException e) {
if (forceUpdate) {
} 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) {
}
} 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,
}
/**
* 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
*/
throws BadRequestException {
final String uriFirstId =
final String firstId = uriFirstId != null ? uriFirstId : request.getAdditionalParameter(PARAM_MANAGED_OBJECT_ID);
} else {
}
}
/**
* 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
*/
try {
} catch (BadRequestException 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) {
if (properties != null) {
// Remove "soft" fields that were placed in properties for the ResourceResponse
}
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.
));
} else {
));
}
} else {
));
}
}
/**
* 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.
*/
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.
*/
}
/**
* 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.
*/
}
/**
* Returns a {@link Connection} object.
*
* @return a {@link Connection} object.
* @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,
} else {
}
}
// Perform the field expansion
}
} else {
}
}
}
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"
* </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,
// If not in a bidirectional (aka reverse) relationship, then referenced object doesn't need to sync.
if (!isReverseSyncNeeded()) {
}
}
/**
* 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
*/
// If not in a bidirectional (aka reverse) relationship, then referenced object doesn't need to sync.
if (!isReverseSyncNeeded()) {
}
// First read the state of the referenced object to save the before.
//make the request
throws ResourceException {
// Perform the sync after reading the new state of the referenced obj.
}
throws ResourceException {
// Since the read failed, the sync can't happen, but we still want to proceed with
// the request on the relationship.
}
});
}
/**
* 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 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.referenceToSync = referenceToSync;
}
try {
// now re-read the referenced object to see the aftermath of the request
// now perform the sync
json(
));
} catch (Exception e) {
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() {
}
/**
* 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",
* "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
*/
return relationshipJson.get(REPO_FIELD_FIRST_PROPERTY_NAME).asString().equals(schemaField.getReversePropertyName());
}
public SchemaField getSchemaField() {
return schemaField;
}
}