ManagedObjectSet.java revision 419f8f6b732e539d44450291405f8b9f0dee1647
/*
* 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]".
*
* Portions copyright 2011-2015 ForgeRock AS.
*/
/**
* Provides access to a set of managed objects of a given type: managed/[type]/{id}.
*
*/
class ManagedObjectSet implements CollectionResourceProvider, ScriptListener, ManagedObjectSyncService {
/** Actions supported by this resource provider */
enum Action {
}
/** Supported script hooks */
enum ScriptHook {
/** Script to execute when the creation of an object is being requested. */
/** Script to execute when the read of an object is being requested. */
/** Script to execute when the update of an object is being requested. */
/** Script to execute when the deletion of an object is being requested. */
/** Script to execute after the create of an object has completed. */
/** Script to execute after the update of an object has completed. */
/** Script to execute after the delete of an object has completed. */
/** Script to execute when a managed object requires validation. */
/** Script to execute once an object is retrieved from the repository. */
/** Script to execute when an object is about to be stored in the repository. */
/** Script to execute when synchronization of managed objects to external targets is complete. */
}
/**
* Setup logging for the {@link ManagedObjectSet}.
*/
/** The managed objects service that instantiated this managed object set. */
private final CryptoService cryptoService;
/** The connection factory for access to the router */
private final IDMConnectionFactory connectionFactory;
/** Audit Activity Log helper */
private final ActivityLogger activityLogger;
/** Name of the managed object type. */
private final ResourcePath managedObjectPath;
/** The schema to use to validate the structure and content of the managed object. */
private final ManagedObjectSchema schema;
/** Map of scripts to execute on specific {@link ScriptHook}s. */
private final Map<ScriptHook, ScriptEntry> scriptHooks = new EnumMap<ScriptHook, ScriptEntry>(ScriptHook.class);
/** reference to the sync service route; used to decided whether or not to perform a sync action */
/** Map of relationship property names and their accompanying sets */
/** Flag for indicating if policy enforcement is enabled */
private final boolean enforcePolicies;
/**
* Constructs a new managed object set.
*
* @param scriptRegistry
* the script registry
* @param cryptoService
* the cryptographic service
* @param syncRoute
* a reference to the RouteService on "sync"
* @param connectionFactory
* the router connection factory
* @param config
* configuration object to use to initialize managed object set.
* @throws JsonValueException
* when the configuration is malformed
* @throws ScriptException
* when the script configuration is malformed or the script is
* invalid.
*/
final AtomicReference<RouteService> syncRoute, IDMConnectionFactory connectionFactory, JsonValue config)
throws JsonValueException, ScriptException {
this.cryptoService = cryptoService;
this.connectionFactory = connectionFactory;
}
this.schema = new ManagedObjectSchema(config.get("schema").expect(Map.class), scriptRegistry, cryptoService);
}
}
}
}
/**
* Generates a fully-qualified object identifier for the managed object.
*
* @param resourceId
* the local managed object identifier to qualify.
* @return the fully-qualified managed object identifier.
*/
return resourceId != null
}
/**
* Generates a fully-qualified object identifier for the repository.
*
* @param resourceId
* the local managed object identifier to qualify.
* @return the fully-qualified repository object identifier.
*/
}
/**
* Executes a script if it exists, populating an {@code "object"} property in the root scope.
*
* @param hook
* the script-hook to execute
* @param value
* the object to be populated in the script scope.
* @param additionalProps
* a Map of additional properties to add the the script scope
* @throws ForbiddenException
* if the script throws an exception.
* @throws InternalServerErrorException
* if any other exception is encountered.
*/
private void execScript(final Context context, ScriptHook hook, JsonValue value, JsonValue additionalProps)
throws ResourceException {
EventEntry measure = Publisher.start(Name.get("openidm/internal/managed/" + this.getName() + "/execScript/" + hook.name()), null, null);
try {
}
}
try {
} catch (ScriptThrownException ste) {
// Allow for scripts to set their own exception
} catch (ScriptException se) {
}
}
} finally {
}
}
/**
* Prepares a map of additional bindings for the script hook invocation.
*
* @param context the current Context
* @param request the Request being processed
* @param resourceId the resourceId of the object being manipulated
* @param oldObject the old object value
* @param newObject the new object value
* @return a JsonValue map of script bindings
*/
// TODO once SCRIPT-1 is implemented, this can be removed and the resourceName can be obtained via context.router.getBaseUri()
return scriptBindings;
}
/**
* Executes all of the necessary trigger scripts when an object is retrieved from the repository.
*
* @param context the current Context
* @param request the Request being processed
* @param resourceId the resourceId of the object being manipulated
* @param value
* the JSON value that was retrieved from the repository.
* @throws ForbiddenException
* if a validation trigger throws an exception.
* @throws InternalServerErrorException
* if any other exception occurs.
*/
private void onRetrieve(Context context, Request request, String resourceId, ResourceResponse value) throws ResourceException {
}
}
private void populateVirtualProperties(final Context context, final Request request, final JsonValue content) throws ForbiddenException,
// Only populate if field is returned by default or explicitly requested
}
}
}
/**
* Executes all of the necessary trigger scripts when an object is to be stored in the repository.
*
* @param value
* the JSON value to be stored in the repository.
* @throws ForbiddenException
* if a validation trigger throws an exception.
* @throws InternalServerErrorException
* if any other exception occurs.
*/
// Execute all individual onValidate scripts
}
// Execute the root onValidate script
// Execute all individual onStore scripts
}
// Execute the root onStore script
}
/**
* Decrypt the value
*
* @param value
* a json value with potentially encrypted value(s)
* @return object with values decrypted
* @throws InternalServerErrorException
* if decryption failed for any reason
*/
try {
} catch (JsonException je) {
throw new InternalServerErrorException(je);
}
}
/**
* Decrypt the value
*
* @param value
* a json value with potentially encrypted value(s)
* @return object with values decrypted
* @throws InternalServerErrorException
* if decryption failed for any reason
*/
private ResourceResponse decrypt(final ResourceResponse value) throws InternalServerErrorException {
try {
// makes a copy, which we can modify
} catch (JsonException je) {
throw new InternalServerErrorException(je);
}
}
/**
* Forbid the use of sub objects
*
* @param id
* the identifier to check
* @throws ForbiddenException
* if the identifier identifies a sub object
*/
throw new ForbiddenException("Sub-objects are not supported");
}
}
/**
* Forbid operation without id, on the whole object set
*
* @param id
* the identifier to check
* @throws ForbiddenException
* if there is no identifier.
*/
throw new ForbiddenException("Operation not allowed on entire object set");
}
}
/**
* Update a resource as part of an update or patch request.
*
* @param context the current Context
* @param request the source Request
* @param resourceId the resource id of the object being modified
* @param rev the revision of hte object being modified
* @param oldValue the old value of the object
* @param newValue the new value of the object
* @param relationshipFields a set of relationship fields to persist
* @return a {@link ResourceResponse} object representing the updated resource
* @throws ResourceException
*/
private ResourceResponse update(final Context context, Request request, String resourceId, String rev,
throws ResourceException {
}
// Execute the onUpdate script if configured
// Validate relationships before persisting
// Populate the virtual properties (so they are updated for sync-ing)
// Remove relationships so they don't get persisted in the repository with the managed object details.
// Perform pre-property encryption
// Perform update
// Put relationships back in before we respond
// Persists all relationship fields that are present in the new value and updates their values.
responseContent.asMap().putAll(persistRelationships(false, context, resourceId, responseContent, relationshipFields)
.asMap());
// Execute the postUpdate script if configured
performSyncAction(context, request, resourceId, SynchronizationService.SyncServiceAction.notifyUpdate,
return response;
}
/**
* Persist all relationship fields contained in the JsonValue map to their accompanying
* {@link #relationshipProviders}
*
* @param clearExisting If existing (those not present in the object) should be cleared
* @param context The current context
* @param resourceId The id of the resource these relationships are associated with
* @param json A JsonValue map that contains relationship fields and value(s) to be persisted
* @param relationshipFields a set of relationship fields to persist
* @return A {@link JsonValue} map containing each relationship field and its persisted value(s)
*/
private JsonValue persistRelationships(final boolean clearExisting, Context context, String resourceId, final JsonValue json,
EventEntry measurement = Publisher.start(Name.get("openidm/internal/managedobjectset/persistRelationships"), json, context);
try {
// value of the relationship in the managed object
// Relationships not present in the request will be null
// Relationships present in the request but set to null will be JsonValue(null)
if (relationshipValue != null) {
}
}
));
}
}
// Join json maps
}
return joined;
}
} finally {
measurement.end();
}
}
/**
* Applies a patch document to an object, or by finding an object in the object set itself via query parameters. As
* this is an action, the patch document to be applied is in the {@code _entity} parameter.
*
* @param context the current Context
* @param request the {@link ActionRequest}
* @return a {@link ResourceResponse} representing the patched object.
* @throws ResourceException
*/
private ResourceResponse patchAction(final Context context, final ActionRequest request) throws ResourceException {
throw new BadRequestException(
"The request could not be processed because the provided content is not a JSON array");
}
// Build query request from action parameters looking for query parameters
// use JsonValue to coerce Map<String, String> to Map<String, Object> - blech
new QueryResourceHandler() {
return true;
}
});
try {
} catch (ResourceException e) {
throw e;
} catch (Exception e) {
throw new InternalServerErrorException(e.getMessage(), e);
}
throw new InternalServerErrorException("Query result must yield one matching object");
} else {
throw new NotFoundException("Query returned no results");
}
}
public Promise<ResourceResponse, ResourceException> createInstance(Context context, CreateRequest request) {
// Check if the new id is specified in content, and use it if it is.
}
try {
// decrypt any incoming encrypted properties
// Execute onCreate script
// Validate relationships before persisting
// Populate the virtual properties (so they are available for sync-ing)
// Remove relationships so they don't get persisted in the repository with the managed object details.
// includes per-property encryption
// Persist the managed object in the repository
ResourceResponse createResponse = connectionFactory.getConnection().create(managedContext, createRequest);
activityLogger.log(managedContext, request, "create", managedId(resourceId).toString(), null, content,
// Place stripped relationships back in content
// Persists all relationship fields and place their persisted values in content
// Execute the postCreate script if configured
// Sync any targets after managed object is created
performSyncAction(managedContext, request, resourceId, SynchronizationService.SyncServiceAction.notifyCreate,
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
}
}
public Promise<ResourceResponse, ResourceException> readInstance(final Context context, String resourceId,
try {
ResourceResponse readResponse = connectionFactory.getConnection().read(managedContext, readRequest);
final JsonValue relationships = fetchRelationshipFields(managedContext, resourceId, request.getFields());
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
}
}
/**
* Fetch the current relationship(s) for relationship fields set to be returned by default
* or specified in the {@link ReadRequest#getFields()}
*
* @param context The current context
* @param resourceId The id of the resource to fetch relationships of
* @param requestFields The fields requested in the initial request
* @return A {@link JsonValue} map containing all relationship fields and their values
* @throws ResourceException
*/
EventEntry measure = Publisher.start(Name.get("openidm/internal/managed/set/fetchRealtionshipFields"), resourceId, context);
try {
/*
* Create set only containing the head of request fields
* Allows for a relationship to be fetched when only an expansion is requested.
*/
// A blank _fields param can yield a single '/' (empty) pointer
}
}
try {
} catch (NotFoundException e) {
}
} else {
// relationship was not requested or set to return by default
}
}
return joined;
} finally {
}
}
/**
* This will traverse the jsonValue and validate that all relationship references are valid.
*
* @param oldValue previous state of the json.
* @param newValue json object that will get its relationship fields validated.
* @param context context of the request that is in progress.
* @throws ResourceException BadRequestException when the first invalid relationship reference is discovered,
* otherwise for other issues.
*/
throws ResourceException {
}
}
}
public Promise<ResourceResponse, ResourceException> updateInstance(final Context context, final String resourceId,
final UpdateRequest request) {
try {
// decrypt any incoming encrypted properties
}
}
ResourceResponse readResponse = connectionFactory.getConnection().read(managedContext, readRequest);
ResourceResponse updatedResponse = update(managedContext, request, resourceId, request.getRevision(),
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
}
}
public Promise<ResourceResponse, ResourceException> deleteInstance(final Context context, final String resourceId,
final DeleteRequest request) {
try {
// Populate the relationship fields in the read resource
final JsonValue relationships = fetchRelationshipFields(managedContext, resourceId, request.getFields());
// Delete the resource
} else {
}
// Delete any relationships associated with this resource
}
// Wait for deletions to complete before continuing
// Execute the postDelete script if configured
execScript(managedContext, ScriptHook.postDelete, null, prepareScriptBindings(managedContext, request, resourceId,
// Perform notifyDelete synchronization
performSyncAction(managedContext, request, resourceId, SynchronizationService.SyncServiceAction.notifyDelete,
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
}
}
public Promise<ResourceResponse, ResourceException> patchInstance(Context context, String resourceId,
try {
} catch (ResourceException e) {
return e.asPromise();
}
}
/**
* Patches the given resource and will also remove private properties if it is an external call based upon context.
*
* @param context
* @param request
* @param resourceId
* @param revision Expected revision of the resource. Patch will fail if non-null and not matching.
* @param patchOperations
*
* @return The patched ResourceResponse with private properties omitted if called externally.
*
* @throws ResourceException
*/
private ResourceResponse patchResourceById(Context context, Request request, String resourceId, String revision,
throws ResourceException {
// Get the oldest value for diffing in the log
// JsonValue oldValue = new JsonValue(cryptoService.getRouter().read(repoId(id)));
}
/**
* Patches the given resource and will also remove private properties if it is an external call based upon context.
*
* @param context
* @param request
* @param resource The resource to be patched
* @param revision
* @param patchOperations
*
* @return The patched ResourceResponse with private properties omitted if called externally.
*
* @throws ResourceException
*/
private ResourceResponse patchResource(Context context, Request request, ResourceResponse resource, String revision,
throws ResourceException {
// FIXME: There's no way to decrypt a patch document. :-( Luckily, it'll work for now with patch action.
boolean retry = forceUpdate;
do {
try {
// decrypt any incoming encrypted properties
// Create a Set containing all the patched relationship fields
// Getting the first token as we currently only support top-level relationship fields
// This allows us to ignore trailing array index's or '-' characters.
}
}
// If we haven't defined a revision, we need to get the current revision
}
// Merge the relationship fields with the fields specified in the request
// Fetch the relationship fields
// Populate the decrypted resource with the relationship fields
if (!modified) {
}
// Check if policies should be enforced
if (enforcePolicies) {
// Build up a map of properties to validate (only the patched properties)
// Getting the first token as we currently only support top-level relationship fields
// This allows us to ignore trailing array index's or '-' characters.
}
// The action request to validate the policy of all the patched properties
// this parameter is used in conjunction with the test in policy.js to ensure that the
// re-authentication policy is enforced.
}
JsonValue result = connectionFactory.getConnection().action(context, policyAction).getJsonContent();
}
}
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
throw e;
}
} catch (ResourceException e) {
throw e;
} catch (Exception e) {
throw new InternalServerErrorException(e.getMessage(), e);
}
} while (retry);
return null;
}
public Promise<QueryResponse, ResourceException> queryCollection(final Context context, final QueryRequest request,
final QueryResourceHandler handler) {
// The "executeOnRetrieve" parameter is used to indicate if is returning a full managed object
// The onRetrieve script should only be run queries that return full managed objects
? false
try {
// Create new QueryRequest to send to the repository
// Does not include any fields specified in the current request
}
new QueryResourceHandler() {
// Check if the onRetrieve script should be run
if (onRetrieve) {
try {
} catch (ResourceException e) {
ex[0] = e;
return false;
}
}
// Don't populate relationships if this is a query-all-ids query.
} else {
// Populate the relationship fields
try {
JsonValue relationships = fetchRelationshipFields(managedContext, resource.getId(), request.getFields());
} catch (ResourceException e) {
ex[0] = e;
return false;
} catch (Exception e) {
return false;
}
}
return handler.handleResource(prepareResponse(managedContext, resourceResponse, request.getFields()));
}
});
}
return queryResponse.asPromise();
} catch (ResourceException e) {
return e.asPromise();
} catch (Exception e) {
}
}
public Promise<ActionResponse, ResourceException> actionInstance(Context context, String resourceId,
try {
case patch:
ResourceResponse patchResponse = patchResourceById(managedContext, request, resourceId, null, operations);
case triggerSyncCheck:
// Sync changes if required
// Read in managed object to get updated virtual attributes. The result of the read request will be
// compared against the last sync'd value stored in the repository (in the updateInstance() request
if (!requestFields.isEmpty()) {
}
ResourceResponse currentResource = connectionFactory.getConnection().read(managedContext, readRequest);
if (!requestFields.isEmpty()) {
}
default:
}
} catch (ResourceException e) {
return e.asPromise();
} catch (IllegalArgumentException e) {
// from getActionAsEnum
} catch (Exception e) {
}
}
/**
* Processes action requests.
* <p>
* If the {@code _action} parameter is {@code patch}, then the request is
* handled as a partial modification to an object, either explicitly
* (identifier is supplied) or by query (query parameters specify the query
* to perform to yield a single object to patch.
*/
public Promise<ActionResponse, ResourceException> actionCollection(Context context, ActionRequest request) {
try {
case patch:
default:
}
} catch (ResourceException e) {
return e.asPromise();
} catch (IllegalArgumentException e) {
// from getActionAsEnum
}
}
// -------- Implements the ScriptListener
case ScriptEvent.UNREGISTERING:
}
}
/**
* Returns the name of the managed object set.
*/
return name;
}
public String getTemplate() {
}
/**
* Prepares the response contents by removing the following: any private properties (if the request is from an
* external call), any virtual or relationship properties that are not set to returnByDefault.
*
* @param context the current ServerContext
* @param resource the Resource to prepare
* @param fields a list of fields to return specified in the request
* @return the prepared Resource object
* @throws ResourceException
*/
private ResourceResponse prepareResponse(Context context, ResourceResponse resource, List<JsonPointer> fields) {
// Return all relationship fields, so remove them from fieldsToRemove map
}
// Allow the field by removing it from the fieldsToRemove list.
// Allow the indexed array field (ex: role/0) by removing it from the fieldsToRemove list.
} else {
// Check for resource expansion and build up map of fields to expand
if (expansionPair != null) {
// Allow the field by removing it from the fieldsToRemove list (if there)
// Add the field to the expansion map
// Initialize the list of fields in the resource expansion map
// Add the relationship field to the fields list (so it is included in the response)
}
// Remove the expanded field from the list of fields (since it will be included as part of
// the relationship field (after resource expansion) in the response.
}
}
}
}
}
// Remove all relationship and virtual fields that are not returned by default, or explicitly listed
}
// List of promises representing results of resource expansion
// Loop over the relationship fields to expand
// The schema for the field to expand
// The list of fields to include from the expanded resource
// The value of the relationship field
try {
// Perform the resource expansion
if (schemaField.isArray()) {
// The field is an array of relationship objects
}
} else {
// The field is a relationship object
}
} else {
}
} catch (ResourceException e) {
}
}
try {
} catch (ResourceException e) {
// Exceptions are already handled in expandResource, so this should never happen.
}
// only cull private properties if this is an external call
}
}
}
// Update the list of fields in the response
}
return resource;
}
/**
* Expands the provided resource represented by a {@link JsonValue} relationship object. A read request will be
* issued for the resource identified by the "_ref" field in the supplied relationship object. A supplied
* {@link List} of fields indicates which fields to read and then merge with the relationship object.
*
* @param context the {@link Context} of the request
* @param value the value of the relationship object
* @param fieldsList the list of fields to read and merge with the relationship object.
* @throws ResourceException if an error is encountered.
*/
private Promise<ResourceResponse, ResourceException> expandResource(Context context, final JsonValue value,
// Create and issue a read request on the referenced resource with the specified list of fields
new ResultHandler<ResourceResponse>() {
// Merge the result with the supplied relationship object
}
}, new ExceptionHandler<ResourceException>() {
}
});
} else {
}
}
/**
* Removes all relationship fields from the supplied {@link JsonValue} instance of a managed object. Returns a
* {@link JsonValue} object containing the stripped fields.
*
* @param value The JsonValue map to strip relationship fields from
* @return A {@link JsonValue} containing the stripped fields.
*/
final Object strippedValue;
if (null != fieldValue) {
}
}
return stripped;
}
public void performSyncAction(final Context context, final Request request, final String resourceId,
final SynchronizationService.SyncServiceAction action, final JsonValue oldValue, final JsonValue newValue)
throws ResourceException {
// The "sync" route may be down (unconfigured) or in the process of being re-configured;
// if this is the case, we don't want a router error on the ActionRequest below. Just log
// the warning and return. When the SynchronizationService comes back up (or when the
// reconfiguration is complete), the AtomicReference<RouteService> in ManagedObjectService
// will get set again.
return;
}
try {
.setAdditionalParameter(SynchronizationService.ACTION_PARAM_RESOURCE_CONTAINER, managedObjectPath.toString())
boolean success = false;
try {
success = true;
} catch (ResourceException e) {
success = false;
} catch (Exception e) {
success = false;
}
try {
// Execute the sync script
} catch (ResourceException e) {
throw e;
}
throw syncScriptError[0];
}
} catch (NotFoundException e) {
throw e;
}
}
/**
* Get the {@link ResourcePath} associated with this set.
* @return The {@link ResourcePath} associated with this object set.
*/
public ResourcePath getPath() {
return managedObjectPath;
}
/**
* Get the {@link ManagedObjectSchema} associated with this set.
* @return The {@link ManagedObjectSchema} associated with this object set.
*/
public ManagedObjectSchema getSchema() {
return schema;
}
/**
* Get the current map of {@link RelationshipProvider} for each relationship field.
*/
return relationshipProviders;
}
}