ObjectMapping.java revision 6c05341ba29056a3071388ba179ef4386f5c03ea
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2011-2015 ForgeRock AS. All rights reserved.
*
* 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
* See the License for the specific language governing
* permission and limitations under the License.
*
* When distributing Covered Code, include this CDDL
* Header Notice in each file and include the License file
* If applicable, add the following below the CDDL Header,
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*/
/**
* An ObjectMapping defines policies between source and target objects and their attributes
* during synchronization and reconciliation. Mappings can also define triggers for validation,
* customization, filtering, and transformation of source and target objects.
*/
class ObjectMapping {
/**
* Event names for monitoring ObjectMapping behavior
*/
public static final Name EVENT_CREATE_OBJ = Name.get("openidm/internal/discovery-engine/sync/create-object");
public static final Name EVENT_SOURCE_ASSESS_SITUATION = Name.get("openidm/internal/discovery-engine/sync/source/assess-situation");
public static final Name EVENT_SOURCE_DETERMINE_ACTION = Name.get("openidm/internal/discovery-engine/sync/source/determine-action");
public static final Name EVENT_SOURCE_PERFORM_ACTION = Name.get("openidm/internal/discovery-engine/sync/source/perform-action");
public static final Name EVENT_CORRELATE_TARGET = Name.get("openidm/internal/discovery-engine/sync/source/correlate-target");
public static final Name EVENT_UPDATE_TARGET = Name.get("openidm/internal/discovery-engine/sync/update-target");
public static final Name EVENT_DELETE_TARGET = Name.get("openidm/internal/discovery-engine/sync/delete-target");
public static final Name EVENT_TARGET_ASSESS_SITUATION = Name.get("openidm/internal/discovery-engine/sync/target/assess-situation");
public static final Name EVENT_TARGET_DETERMINE_ACTION = Name.get("openidm/internal/discovery-engine/sync/target/determine-action");
public static final Name EVENT_TARGET_PERFORM_ACTION = Name.get("openidm/internal/discovery-engine/sync/target/perform-action");
public static final String EVENT_OBJECT_MAPPING_PREFIX = "openidm/internal/discovery-engine/sync/objectmapping/";
/**
* Event names for monitoring Reconciliation behavior
*/
public static final Name EVENT_RECON = Name.get("openidm/internal/discovery-engine/reconciliation");
public static final Name EVENT_RECON_ID_QUERIES = Name.get("openidm/internal/discovery-engine/reconciliation/id-queries-phase");
public static final Name EVENT_RECON_SOURCE = Name.get("openidm/internal/discovery-engine/reconciliation/source-phase");
/** Logger */
/** The mapping name */
/** The raw mapping configuration */
/** The name of the links set to use. Defaults to mapping name. */
private final String linkTypeName;
/** The link type to use */
/**
* Whether to link source IDs in a case sensitive fashion.
* Only effective if this mapping defines links, is ignored if this
* mapping re-uses another mapping's links
* Default to {@code TRUE}
*/
private final Boolean sourceIdsCaseSensitive;
/**
* Whether to link target IDs in a case sensitive fashion.
* Only effective if this mapping defines links, is ignored if this
* mapping re-uses another mapping's links
* Default to {@code TRUE}
*/
private final Boolean targetIdsCaseSensitive;
/** the source system */
private final String sourceObjectSet;
/** the target system */
private String targetObjectSet;
/** a script that determines if a source object is valid to be mapped */
private Script validSource;
/** a script that determines if a target object is valid to be mapped */
private Script validTarget;
/**
* A boolean indicating if paging should be used for recon source queries.
*/
private boolean reconSourceQueryPaging;
/**
* A page size for recon source queries, if paging is used.
*/
private int reconSourceQueryPageSize;
/**
* A container for the correlation queries or script.
*/
private Correlation correlation;
/**
* A {@link List} containing the configured link qualifiers.
*/
/**
* A {@link Script} used for determining the link qualifiers.
*/
/** a script that applies the effective assignments as part of the mapping */
private Script defaultMapping;
/** an array of property-mapping objects */
/**
* a map of {@link Policy} objects.
*/
/** a script to execute when a target object is to be created */
private Script onCreateScript;
/** a script to execute when a target object is to be updated */
private Script onUpdateScript;
/** a script to execute when a target object is to be deleted */
private Script onDeleteScript;
/** a script to execute when a source object is to be linked to a target object */
private Script onLinkScript;
/** a script to execute when a source object and a target object are to be unlinked */
private Script onUnlinkScript;
/** a script to execute on each mapping event regardless of the operation */
private Script resultScript;
/** an additional set of key-value conditions to be met for a source object to be valid to be mapped */
private Condition sourceCondition;
/**
* Whether existing links should be fetched in one go along with the source and target id lists.
* false indicates links should be retrieved individually as they are needed.
*/
private final Boolean prefetchLinks;
/**
* Whether when at the outset of correlation the target set is empty (query all ids returns empty),
* it should try to correlate source entries to target when necessary.
* Default to {@code FALSE}
*/
private final Boolean correlateEmptyTargetSet;
/**
* Whether to maintain links for sync-d targets
* Default to {@code TRUE}
*/
private final Boolean linkingEnabled;
/** The number of processing threads to use in reconciliation */
/** a reference to the {@link SynchronizationService} */
private final SynchronizationService service;
/** Whether synchronization (automatic propagation of changes as they are detected) is enabled on that mapping */
private final Boolean syncEnabled;
/**
* Create an instance of a mapping between source and target
*
* @param service The associated synchronization service
* @param config The configuration for this mapping
* @throws JsonValueException if there is an issue initializing based on the configuration.
*/
if (linkQualifiersValue.isNull()) {
// No link qualifiers configured, so add only the default
} else if (linkQualifiersValue.isMap()) {
} else {
}
}
continue;
}
}
if (confTaskThreads != null) {
}
correlateEmptyTargetSet = config.get("correlateEmptyTargetSet").defaultTo(Boolean.FALSE).asBoolean();
}
/**
* Return whether synchronization is enabled for this mapping.
*
* @return whether synchronization is enabled for this mapping
*/
public boolean isSyncEnabled() {
return syncEnabled.booleanValue();
}
/**
* Return whether linking is enabled for this mapping.
*
* @return whether linking is enabled for this mapping
*/
public boolean isLinkingEnabled() {
return linkingEnabled.booleanValue();
}
/**
* Mappings can share the same link tables.
* Establish the relationship between the mappings and determine the proper
* link type to use
* @param syncSvc the associated synchronization service
* @param allMappings The list of all existing mappings
*/
}
/**
* @return the associated synchronization service
*/
return service;
}
/**
* @return The name of the object mapping
*/
return name;
}
/**
* @return The raw config associated with the object mapping
*/
return config;
}
/**
* Returns the complete set of link Qualifiers.
*
* @return a {@link Set} object representing the complete set of link qualifiers
* @throws SynchronizationException
*/
}
/**
* Returns a set of link qualifiers for a given source. If the returnAll boolean is used to indicate that all linkQualifiers
* should be returned regardless of the source object.
*
* @param object the object's value
* @param oldValue the source object's old value
* @param returnAll true if all link qualifiers should be returned, false otherwise
* @return a {@link Set} object representing the complete set of link qualifiers
* @throws SynchronizationException
*/
private Set<String> getLinkQualifiers(JsonValue object, JsonValue oldValue, boolean returnAll) throws SynchronizationException {
if (linkQualifiersScript != null) {
// Execute script to find the list of link qualifiers
try {
} catch (ScriptException se) {
throw new SynchronizationException(se);
}
} else {
return linkQualifiersList;
}
}
/**
* Returns an object's required ID or null if the object is null.
*
* @param object a source or target object
* @return a String representing the object's ID
*/
: null;
}
/**
* @return The configured name of the link set to use for this object mapping
*/
public String getLinkTypeName() {
return linkTypeName;
}
/**
* @return The resolved name of the link set to use for this object mapping
*/
public LinkType getLinkType() {
return linkType;
}
/**
* @return The mapping source object set
*/
public String getSourceObjectSet() {
return sourceObjectSet;
}
/**
* @return The mapping target object set
*/
public String getTargetObjectSet() {
return targetObjectSet;
}
/**
* @return The setting for whether to link
* source IDs in a case sensitive fashion.
* Only effective if the mapping defines the links,
* not if the mapping re-uses another mapping's links
*/
public boolean getSourceIdsCaseSensitive() {
return sourceIdsCaseSensitive.booleanValue();
}
/**
* @return The setting for whether to link
* target IDs in a case sensitive fashion.
* Only effective if the mapping defines the links,
* not if the mapping re-uses another mapping's links
*/
public boolean getTargetIdsCaseSensitive() {
return targetIdsCaseSensitive.booleanValue();
}
/**
* Convenience function with deleted defaulted to false and oldValue defaulted to null
* @see ObjectMapping#doSourceSync(Context, String, JsonValue, boolean, JsonValue)
*/
private JsonValue doSourceSync(Context context, String resourceId, JsonValue value) throws SynchronizationException {
}
/**
* Source synchronization
*
* @param context the Context to use for this request
* @param resourceId object identifier.
* @param value null to have it query the source state if applicable,
* or JsonValue to tell it the value of the existing source to sync
* @param sourceDeleted Whether the source object has been deleted
* @return sync results of the {@link SyncOperation}
* @throws SynchronizationException if sync-ing fails.
*/
private JsonValue doSourceSync(Context context, String resourceId, JsonValue value, boolean sourceDeleted, JsonValue oldValue)
throws SynchronizationException {
LOGGER.trace("Start source synchronization of {} {}", resourceId, (value == null) ? "without a value" : "with a value");
if (sourceDeleted) {
} else {
}
// Loop over correlation queries, performing a sync for each linkQualifier
// TODO: one day bifurcate this for synchronous and asynchronous source operation
try {
} catch (SynchronizationException e) {
// exception was not intentional, set status to failure
}
throw e;
} finally {
}
}
return results;
}
/**
* TODO: Description.
*
* @param queryParameters TODO.
* @return TODO.
* @throws SynchronizationException TODO.
*/
throws SynchronizationException {
try {
QueryRequest request = RequestUtil.buildQueryRequestFromParameterMap(targetObjectSet, queryParameters);
new QueryResultHandler() {
// ignore
}
return true;
}
//ignore
}
});
return result;
} catch (ResourceException ose) {
throw new SynchronizationException(ose);
}
}
/**
* Issues a request to create an object on the target.
*
* @param context the Context to use for the request
* @param target the target object to create.
* @throws SynchronizationException
*/
private LazyObjectAccessor createTargetObject(Context context, JsonValue target) throws SynchronizationException {
try {
targetObject = new LazyObjectAccessor(service, targetObjectSet, resource.getId(), resource.getContent());
} catch (JsonValueException jve) {
throw new SynchronizationException(jve);
} catch (ResourceException ose) {
throw new SynchronizationException(ose);
} finally {
}
return targetObject;
}
/**
* Issues a request to update an object on the target.
*
* @param context the Context to use for the request
* @param target the target object to create.
* @throws SynchronizationException
*/
private void updateTargetObject(Context context, JsonValue target, String targetId) throws SynchronizationException {
try {
throw new SynchronizationException("target '_id' has changed");
}
} catch (SynchronizationException se) {
throw se;
} catch (JsonValueException jve) {
throw new SynchronizationException(jve);
} catch (ResourceException ose) {
throw new SynchronizationException(ose);
} finally {
}
}
/**
* Issues a request to delete an object on the target.
*
* @param context the Context to use for the request
* @param target the target object to create.
* @throws SynchronizationException
*/
private void deleteTargetObject(Context context, JsonValue target) throws SynchronizationException {
try {
DeleteRequest ur = Requests.newDeleteRequest(targetObjectSet, target.get("_id").required().asString());
} catch (JsonValueException jve) {
throw new SynchronizationException(jve);
} catch (NotFoundException nfe) {
// forgiving delete
} catch (ResourceException ose) {
throw new SynchronizationException(ose);
} finally {
}
}
}
/**
* Apply the configured sync mappings
*
* @param source The current source object
* @param oldSource an optional previous source object before the change(s) that triggered the sync,
* null if not provided
* @param target the current target object to modify
* @param existingTarget the full existing target object
* @throws SynchronizationException if applying the mappings fails.
*/
private void applyMappings(JsonValue source, JsonValue oldSource, JsonValue target, JsonValue existingTarget, String linkQualifier) throws SynchronizationException {
try {
}
// Apply default mapping, if configured
} finally {
}
}
private JsonValue applyDefaultMappings(JsonValue source, JsonValue oldSource, JsonValue target, JsonValue existingTarget, String linkQualifier) throws SynchronizationException {
if (defaultMapping != null) {
}
try {
} catch (ScriptThrownException ste) {
} catch (ScriptException se) {
throw new SynchronizationException(se);
}
}
return result;
}
/**
* @return an event name for monitoring this object mapping
*/
private Name getObjectMappingEventName() {
}
/**
* Returns {@code true} if the specified object identifer is in this mapping's source
* object set.
*/
}
/**
* Notify the target system that an object has been created in a source system.
*
* @param resourceContainer source system
* @param resourceId source object id
* @param value object to synchronize
* @return the SynchronizationOperation result details
* @throws SynchronizationException on failure to synchronize
*/
public JsonValue notifyCreate(Context context, String resourceContainer, String resourceId, JsonValue value)
throws SynchronizationException {
// notification without the actual value
}
}
}
/**
* Notify the target system that an object has been updated in a source system.
*
* @param resourceContainer source system
* @param resourceId source object id
* @param oldValue previous object value
* @param newValue updated object to synchronize
* @return
* @throws SynchronizationException on failure to synchronize
*/
public JsonValue notifyUpdate(Context context, String resourceContainer, String resourceId, JsonValue oldValue, JsonValue newValue)
throws SynchronizationException {
}
if (oldValue == null || oldValue.getObject() == null || JsonPatch.diff(oldValue, newValue).size() > 0) {
} else {
}
}
}
/**
* Notify the target system that an object has been deleted in a source system.
*
* @param resourceContainer source system
* @param resourceId source object id
* @param oldValue deleted object to synchronize
* @return
* @throws SynchronizationException on failure to synchronize
*/
public JsonValue notifyDelete(Context context, String resourceContainer, String resourceId, JsonValue oldValue)
throws SynchronizationException {
}
}
/**
* Perform the reconciliation action on a pre-assessed job.
* <p/>
* For the input parameters see {@link org.forgerock.openidm.sync.impl.ObjectMapping.SourceSyncOperation#toJsonValue()} or
* {@link org.forgerock.openidm.sync.impl.ObjectMapping.TargetSyncOperation#toJsonValue()}.
* <p/>
* Script example:
* <pre>
* try {
* openidm.action('sync',recon.actionParam)
* } catch(e) {
*
* };
* </pre>
* @param params the input parameters to proceed with the pre-assessed job
* includes state from previous pre-assessment (source or target sync operation),
* plus instructions of what to execute. Specifically beyond the pre-asessed state
* it expects changes to params
* - action: the desired action to execute
* - situation (optional): the situation to expect before executing the action.
* To enforce that the action is only executed if the situation didn't change,
* supply the situation from the pre-assessment.
* To attempt execution of the action without enforcing the situation check,
* supply no situation param
* @throws SynchronizationException
*/
// If reconId is set this action is part of a reconciliation run
}
try {
try {
}
//TODO blank check
}
}
} else {
}
}
// IF an expected situation is supplied, compare and reject if current situation changed
throw new SynchronizationException("Expected situation does not match. Expected: "
}
}
op.performAction();
} catch (SynchronizationException se) {
// exception was not intentional
} else {
}
}
}
if (op instanceof SourceSyncOperation) {
}
} else {
}
}
}
} finally {
}
}
}
if (resultScript != null) {
try {
} catch (ScriptThrownException ste) {
} catch (ScriptException se) {
throw new SynchronizationException(se);
}
}
}
/**
* Execute a full reconciliation
*
* @param reconContext the context specific to the reconciliation run
* @throws SynchronizationException if any unforseen failure occurs during the reconciliation
*/
}
/**
* TEMPORARY. Future version will have this break-down into discrete units of work.
* @param reconContext
* @throws SynchronizationException
*/
try {
// Get the relevant source (and optionally target) identifiers before we assess the situations
if (!sourceIter.hasNext()) {
return;
}
}
// If we will handle a target phase, pre-load all relevant target identifiers
} else {
}
// Optionally get all links up front as well
if (prefetchLinks) {
}
}
boolean queryNextPage = false;
do {
// Query next page of results if paging
if (queryNextPage) {
sourceQueryResult = reconContext.querySourceIter(reconSourceQueryPageSize, sourceQueryResult.getPagingCookie());
}
// Perform source recon phase on current set of source ids
queryNextPage = true;
} while (reconSourceQueryPaging && sourceQueryResult.getPagingCookie() != null); // If paging, loop through next pages
measureSource.end();
measureTarget.end();
}
} catch (InterruptedException ex) {
} catch (Exception e) {
throw new SynchronizationException("Synchronization failed", e);
} finally {
}
}
// TODO: cleanup orphan link objects (no matching source or target) here
}
/**
* Sets the LogEntry message and messageDetail with the appropriate information
* from the given Exception.
*
* @param entry the LogEntry
* @param syncException the Exception
*/
// Loop to find original cause and top level ResourceException (if any)
// Check if there is a detail field to use
}
}
}
// Set message and messageDetail
: cause.getMessage());
}
/**
* Reconciliation interface.
* Implementation is passed to ReconPhase and executed by the ReconTask
*
*/
private interface Recon {
/**
* Reconcile a given object ID
* @param id the object id to reconcile
* @param entry an optional value if the given entry was pre-loaded, or null if not
* @param reconContext reconciliation context
* @param rootContext json resource root ctx
* @param allLinks all links if pre-queried, or null for on-demand link querying
* @throws SynchronizationException if there is a failure reported in reconciling this id
*/
Map<String, Map<String, Link>> allLinks, Collection<String> remainingIds) throws SynchronizationException;
}
/**
* Reconcile a given source ID
*/
/**
* {@inheritDoc}
*/
public void recon(String id, JsonValue objectEntry, ReconciliationContext reconContext, Context rootContext,
throws SynchronizationException {
}
try {
} catch (SynchronizationException se) {
}
}
// update statistics with status
// If target system has case insensitive IDs, remove without regard to case
}
try {
if (op.hasTargetObject()) {
op.getTargetObjectId()));
}
} catch (SynchronizationException ex) {
}
}
}
}
};
/**
* Reconcile a given target ID
*/
/**
* {@inheritDoc}
*/
public void recon(String id, JsonValue objectEntry, ReconciliationContext reconContext, Context rootContext, Map<String,
if (objectEntry == null) {
// Load target detail on demand
} else {
// Pre-queried target detail
}
try {
} catch (SynchronizationException se) {
se);
}
}
// update statistics with status
}
}
}
}
};
/**
*/
// This value is null if it wasn't pre-queried
this.reconContext = reconContext;
this.parentContext = parentContext;
this.remainingIds = remainingIds;
}
//TODO I miss the Request Context
try {
} finally {
}
return null;
}
}
/**
*/
class ReconPhase extends ReconFeeder {
super(resultIter, reconContext);
this.parentContext = parentContext;
this.remainingIds = remainingIds;
}
}
}
/**
* @return the configured number of threads to use for processing tasks.
* 0 to process in a single thread.
*/
int getTaskThreads() {
return taskThreads;
}
/**
* Creates an entry in the audit log.
*
* @param entry the entry to create
* @throws SynchronizationException
*/
try {
} catch (ResourceException ose) {
throw new SynchronizationException(ose);
}
}
/**
* Record the start of a new reconciliation.
*
* @param reconContext
* @param context
* @throws SynchronizationException
*/
throws SynchronizationException {
}
/**
* Record the successful completion of a reconciliation.
*
* @param reconContext
* @param context
* @throws SynchronizationException
*/
throws SynchronizationException {
}
/**
* Record the premature failure of a reconciliation.
*
* @param reconContext
* @param context
* @throws SynchronizationException
*/
throws SynchronizationException {
}
/**
* Record a final entry for a reconciliation.
*
* @param reconContext
* @param rootContext
* @param status
* @param loggerMessage
* @throws SynchronizationException
*/
}
/**
* Execute a sync engine action explicitly, without going through situation assessment.
* @param sourceObject the source object if applicable to the action
* @param targetObject the target object if applicable to the action
* @param situation an optional situation that was originally assessed. Null if not the result of an earlier situation assessment.
* @param action the explicit action to invoke
* @param reconId an optional identifier for the recon context if this is done in the context of reconciliation
*/
public void explicitOp(JsonValue sourceObject, JsonValue targetObject, Situation situation, ReconAction action, String reconId)
throws SynchronizationException {
}
}
/**
* Returns a {@link SynchronizationException} that represents the supplied {@link ScriptThrownException}.
* @param ste a ScriptThrownException
* @param type
* @return
*/
private SynchronizationException toSynchronizationException(ScriptThrownException ste, String name, String type) {
return new SynchronizationException(ste.toResourceException(ResourceException.INTERNAL_ERROR, errorMessage));
}
/**
* TODO: Description.
*/
abstract class SyncOperation {
/**
* A reconciliation ID
*/
/**
* An optional reconciliation context
*/
public ReconciliationContext reconContext;
/**
* Access to the source object
*/
/**
* Access to the target object
*/
/**
* Optional value of the source object before the change that triggered this sync, or null if not supplied
*/
/**
* Holds the link representation
* An initialized link can be interpreted as representing state retrieved from the repository,
* i.e. a linkObject with id of null represents a link that does not exist (yet)
*/
/**
* Indicates whether the link was created during this operation.
* (linkObject above may not be set for newly created links)
*/
boolean linkCreated;
/**
* The current sync operation's situation
*/
/**
* The current sync operation's action
*/
public ReconAction action;
/**
* The current sync operation's active policy
*/
/**
* An action ID
*/
/**
* A boolean indicating whether to ignore any configured post action.
*/
public boolean ignorePostAction = false;
/**
* Performs the sync operation.
*
* @return sync results of the {@link SyncOperation}
* @throws SynchronizationException TODO.
*/
protected abstract boolean isSourceToTarget();
/**
* Sets the link qualifier for the current sync operation.
*
* @param linkQualifier a link qualifier.
*/
}
/**
* @throws SynchronizationException if on-demand load of the object failed
*/
return null;
} else {
return sourceObjectAccessor.getObject();
}
}
/**
* @throws SynchronizationException if on-demand load of the object failed
*/
if (targetObjectAccessor == null || (!targetObjectAccessor.isLoaded() && targetObjectAccessor.getLocalId() == null)) {
return null;
} else {
return targetObjectAccessor.getObject();
}
}
/**
* The set unqualified (local) source object ID
* That a source identifier is set does not automatically imply that the source object exists.
* @return local identifier of the source object, or null if none
*/
protected String getSourceObjectId() {
}
/**
* The set unqualified (local) targt object ID
* That a target identifier is set does not automatically imply that the target object exists.
* @return local identifier of the target object, or null if none
*/
protected String getTargetObjectId() {
}
/**
* @return Whether the target representation is loaded, i.e. the getObject represents what it found.
* IF a target was not found, the state is loaded with a payload / object of null.
*/
protected boolean isTargetLoaded() {
}
/**
* Returns the linkQualifier associated with this SyncOperation
* @return a String representing the linkQualifier
*/
protected String getLinkQualifier() {
return linkObject.linkQualifier;
}
/**
* @return Whether the source object exists. May cause the loading of the (lazy) source object,
* the object existed at that point.
* @throws SynchronizationException if on-demand load of the object failed
*/
protected boolean hasSourceObject() throws SynchronizationException {
boolean defined = false;
defined = false;
} else {
defined = true;
// If available, check against all queried existing IDs
} else {
// If no lists of existing ids is available, do a load of the object to check
}
}
return defined;
}
/**
* @return Whether the target object exists. May cause the loading of the (lazy) source object,
* the object existed at that point.
* @throws SynchronizationException if on-demand load of the object failed
*/
protected boolean hasTargetObject() throws SynchronizationException {
boolean defined = false;
if (isTargetLoaded()) {
// If it's not loaded, but no id to load is available it has no target
defined = false;
} else {
// Either check against a list of all targets, or load to check for existence
// If available, check against all queried existing IDs
// If target system has case insensitive IDs, compare without regard to case
} else {
// If no lists of existing ids is available, do a load of the object to check
}
}
return defined;
}
/**
* @return true if it knows there were no objects in the target set during a bulk query
* at the outset of reconciliation.
* false if there were objects, or it does not know.
* Does not take into account objects getting added during reconciliation, or data getting added
* by another process concurrently
*/
protected boolean hadEmptyTargetObjectSet() {
// If available, check against all queried existing IDs
} else {
return false;
}
}
/**
* Returns a String representing the link ID.
*
* @return the found unqualified (local) link ID, null if none
*/
return linkObject._id;
} else {
return null;
}
}
/**
* Initializes the link representation.
*
* @param link the link object for links that were found/exist in the repository, null to represent no existing link
*/
this.linkObject = link;
} else {
// Keep track of the fact that we did not find a link
this.linkObject.clear();
this.linkObject.initialized = true;
}
}
/**
* Returns all possible policies for a given situation.
*
* @param situation to get policies for
* @return List of policies for given situation
*/
return (policies.get(situation.toString()) == null) ? new ArrayList<Policy>() : (policies.get(situation.toString()));
}
/**
* Sets the action and active policy based on the current situation.
*
* @throws SynchronizationException when cannot determine action from script
*/
protected void determineAction() throws SynchronizationException {
// start with a reasonable default
targetObjectAccessor, this, getLinkQualifier());
break;
}
}
}
}
/**
* Performs the current action for this SyncOperation
*
* @throws SynchronizationException TODO.
*/
@SuppressWarnings("fallthrough")
protected void performAction() throws SynchronizationException {
switch (action) {
case CREATE:
case UPDATE:
case LINK:
case DELETE:
case UNLINK:
case EXCEPTION:
try {
switch (action) {
case CREATE:
if (getSourceObject() == null) {
throw new SynchronizationException("no source object to create target from");
}
if (getTargetObject() != null) {
throw new SynchronizationException("target object already exists");
}
applyMappings(getSourceObject(), oldValue, createTargetObject, json(null), linkObject.linkQualifier); // apply property mappings to target
targetObjectAccessor = new LazyObjectAccessor(service, targetObjectSet, createTargetObject.get("_id").asString(), createTargetObject);
// Allow the early link creation as soon as the target identifier is known
if (isLinkingEnabled()) {
// Create and populate the PendingActionContext for the LINK action
context = PendingAction.createPendingActionContext(context, ReconAction.LINK, ObjectMapping.this.name, getSourceObject(), reconId, situation);
}
if (!isLinkingEnabled()) {
LOGGER.debug("Linking disabled for {} during {}, skipping additional link processing", sourceId, reconId);
break;
}
if (wasLinked) {
linkCreated = true;
LOGGER.debug("Pending link for {} during {} has already been created, skipping additional link processing", sourceId, reconId);
break;
} else {
LOGGER.debug("Pending link for {} during {} not yet resolved, proceed to link processing", sourceId, reconId);
}
// falls through to link the newly created target
case UPDATE:
case LINK:
if (getTargetObjectId() == null) {
throw new SynchronizationException("no target object to link");
}
// get a copy of the target before the onLink trigger,
// the onUpdate trigger or the mappings are applied
try {
linkCreated = true;
} catch (SynchronizationException ex) {
// Allow for link to have been created in the meantime, e.g. programmatically
// create would fail with a failed precondition for link already existing
// Try to read again to see if that is the issue
ex);
throw ex; // it was a different issue
}
}
}
linkObject.update();
}
// TODO: Detect change of source id, and update link accordingly.
break; // do not update target
}
}
}
break; // terminate UPDATE
case DELETE:
if (isLinkingEnabled()) {
// Create and populate the PendingActionContext for the UNLINK action
context = PendingAction.createPendingActionContext(context, ReconAction.UNLINK, ObjectMapping.this.name, getSourceObject(), reconId, situation);
}
if (getTargetObjectId() != null && getTargetObject() != null) { // forgiving; does nothing if no target
// Represent as not existing anymore so it gets removed from processed targets
}
if (wasUnlinked) {
LOGGER.debug("Pending unlink for {} during {} has already been performed, skipping additional unlink processing", getTargetObjectId(), reconId);
break;
} else {
LOGGER.debug("Pending unlink for {} during {} not yet performed, proceed to unlink processing", getTargetObjectId(), reconId);
}
// falls through to unlink the deleted target
case UNLINK:
linkObject.delete();
}
break; // terminate DELETE and UNLINK
case EXCEPTION:
throw new SynchronizationException("Situation " + situation + " marked as EXCEPTION"); // aborts change; recon reports
}
} catch (JsonValueException jve) {
throw new SynchronizationException(jve);
}
case REPORT:
case NOREPORT:
if (!ignorePostAction) {
if (null == activePolicy) {
// assigns the first policy found, as active policy
break;
}
}
}
break;
case ASYNC:
case IGNORE:
}
}
/**
* Evaluates a post action
* @param sourceAction sourceAction true if the {@link ReconAction} is determined for the {@link SourceSyncOperation}
* and false if the action is determined for the {@link TargetSyncOperation}.
* @throws SynchronizationException TODO.
*/
if (null != activePolicy) {
}
}
protected void createLink(String sourceId, String targetId, String reconId) throws SynchronizationException {
linkObject.create();
LOGGER.debug("Established link sourceId: {} targetId: {} in reconId: {}", new Object[] {sourceId, targetId, reconId});
}
/**
* Evaluates the source condition on the source object
*
* @param linkQualifier the link qualifier for the current sync operation.
* @return true if the conditions pass, false otherwise
* @throws SynchronizationException
*/
}
/**
* Evaluated source valid on source object
* @see SyncOperation#isSourceValid(JsonValue)
*/
protected boolean isSourceValid() throws SynchronizationException {
return isSourceValid(null);
}
/**
* Evaluates source valid for the supplied sourceObjectOverride, or the source object
* associated with the sync operation if null
*
* @return whether valid for this mapping or not.
* @throws SynchronizationException if evaluation failed.
*/
boolean result = false;
if (validSource != null) {
: getSourceObject();
if (sourceObject == null) {
}
try {
throw new SynchronizationException("Expecting boolean value from validSource");
}
} catch (ScriptThrownException ste) {
} catch (ScriptException se) {
throw new SynchronizationException(se);
}
} else { // no script means true
result = true;
}
}
if (sourceObjectOverride == null) {
}
return result;
}
/**
* TODO: Description.
*
* @return TODO.
* @throws SynchronizationException TODO.
*/
protected boolean isTargetValid() throws SynchronizationException {
boolean result = false;
if (hasTargetObject()) { // must have a target object to qualify
try {
throw new SynchronizationException("Expecting boolean value from validTarget");
}
} catch (ScriptThrownException ste) {
} catch (ScriptException se) {
throw new SynchronizationException(se);
}
} else { // no script means true
//TODO: is this the right result if getTargetObject was null?
result = true;
}
}
return result;
}
/**
* @see #execScript with oldTarget null
*/
}
/**
* Executes the given script with the appropriate context information
*
* @param type The script hook name
* @param script The script to execute
* @param oldTarget optional old target object before any mappings were applied,
* such as before an update
* null if not applicable to this script hook
* @throws SynchronizationException TODO.
*/
private void execScript(String type, Script script, JsonValue oldTarget) throws SynchronizationException {
// TODO: Once script engine can do on-demand get replace these forced loads
if (getSourceObjectId() != null) {
}
// Target may not have ID yet, e.g. an onCreate with the target object defined,
if (getTargetObject() != null) {
}
}
}
}
try {
} catch (ScriptThrownException se) {
} catch (ScriptException se) {
throw new SynchronizationException(new InternalErrorException(name + " " + type + " script encountered exception", se));
}
}
}
public JsonValue toJsonValue() {
: null));
}
}
/**
* Explicit execution of a sync operation where the appropriate
* action is known without having to assess the situation and apply
* policy to decide the action
*/
private class ExplicitSyncOperation extends SyncOperation {
protected boolean isSourceToTarget() {
//TODO: detect by the source id match
return true;
}
public void init(JsonValue sourceObject, JsonValue targetObject, Situation situation, ReconAction action, String reconId) {
this.sourceObjectAccessor = new LazyObjectAccessor(service, sourceObjectSet, sourceObjectId, sourceObject);
this.targetObjectAccessor = new LazyObjectAccessor(service, targetObjectSet, targetObjectId, targetObject);
this.ignorePostAction = true;
switch (action) {
case UNLINK:
try {
if (sourceObjectId != null) {
} else if (targetObjectId != null) {
}
} catch (SynchronizationException e) {
}
default:
break;
}
}
LOGGER.debug("Complected explicit operation call for situation: {}, action: {}", situation, action);
return toJsonValue();
}
}
/**
* TODO: Description.
*/
class SourceSyncOperation extends SyncOperation {
// If it can not uniquely identify a target, the list of ambiguous target ids
@SuppressWarnings("fallthrough")
try {
EventEntry measureSituation = Publisher.start(EVENT_SOURCE_ASSESS_SITUATION, getSourceObjectId(), null);
try {
} finally {
if (targetObjectAccessor != null) {
}
}
EventEntry measureDetermine = Publisher.start(EVENT_SOURCE_DETERMINE_ACTION, getSourceObjectId(), null);
try {
} finally {
}
EventEntry measurePerform = Publisher.start(EVENT_SOURCE_PERFORM_ACTION, getSourceObjectId(), null);
try {
} finally {
if (reconContext != null){
// The link ID presence after the action can not be interpreted as an indication if the link has been created
}
}
return syncResult;
} catch (SynchronizationException e) {
e.setDetail(syncResult);
throw e;
}
}
protected boolean isSourceToTarget() {
return true;
}
/**
* @return all found matching target identifier(s), or a 0 length array if none.
* More than one target identifier is possible for ambiguous matches
*/
public String[] getTargetIds() {
if (ambiguousTargetIds != null) {
} else if (getTargetObjectId() != null) {
} else {
}
return targetIds;
}
/**
* @return the ambiguous target identifier(s), or an empty list if no ambiguous entries are present
*/
public List getAmbiguousTargetIds() {
return ambiguousTargetIds;
}
}
}
sourceObjectAccessor = new LazyObjectAccessor(service, sourceObjectSet, params.get("sourceId").required().asString());
linkObject.setLinkQualifier(params.get("linkQualifier").defaultTo(Link.DEFAULT_LINK_QUALIFIER).asString());
}
/**
* TODO: Description.
*
* @throws SynchronizationException TODO.
*/
private void assessSituation() throws SynchronizationException {
// If linking is disabled, set link to null
if (!isLinkingEnabled()) {
}
// In case the link was not pre-read get it here
}
// If an existing link was found, set the targetObjectAccessor
if (reconContext != null) {
// If there is a pre-loaded target value, use it
if (reconContext.hasTargetsValues()) {
}
}
targetObjectAccessor = new LazyObjectAccessor(service, targetObjectSet, linkObject.targetId, preloaded);
} else {
}
}
if (!hasSourceObject()) {
/*
For sync of delete. For recon these are assessed instead in target phase
no source, link, target & valid target - source missing
no source, link, target & not valid target - target ignored
no source, link, no target - link only
no source, no link - can't correlate (no previous object available) - all gone
no source, no link - (correlate)
no target - all gone
1 target & valid (source) - unassigned
1 target & not valid (source) - target ignored
> 1 target & valid (source) - ambiguous
> 1 target & not valid (source) - unqualified
*/
if (hasTargetObject()) {
if (isTargetValid()) {
} else {
// target is not valid for this mapping; ignore it
}
} else {
}
} else {
// If there is no previous value known we can not correlate
} else {
// Correlate the old value to potential target(s)
// Results null means no correlation query defined, size 0 we know there is no target
if (valid) {
} else {
// target is not valid for this mapping; ignore it
}
if (valid) {
// Note this situation is used both when there is a source and a deleted source
// with multiple matching targets
} else {
}
}
}
}
} else if (isSourceValid() && checkSourceConditions(getLinkQualifier())) { // source is valid for mapping
if (hasTargetObject()) {
} else {
}
} else { // source object not linked to target
} else {
// TODO: consider enhancements:
// For reporting, should it log existing link and source
// What actions should be available for a found, already linked
}
} else {
}
}
} else { // mapping does not qualify for target
} else {
// TODO: Consider if we can optimize out the read for unqualified conditions
}
}
if (reconContext != null) {
}
}
LOGGER.debug("Mapping '{}' assessed situation of {} to be {}", new Object[]{name, getSourceObjectId(), situation});
}
/**
* Correlates (finds an associated) target for the source object
* @see #correlateTarget(JsonValue)
*
* @return JsonValue if found, null if none
* @throws SynchronizationException if the correlation failed.
*/
return correlateTarget(null);
}
/**
* Correlates (finds an associated) target for the given source
* @param sourceObjectOverride optional explicitly supplied source object to correlate,
* or null to use the source object associated with the sync operation
*
* @return JsonValue if found, null if none
* @throws SynchronizationException if the correlation failed.
*/
@SuppressWarnings("unchecked")
// TODO: consider if there are cases where this would better be lazy and not get the full target
if (hasTargetObject()) {
} else if (correlation.hasCorrelation(getLinkQualifier()) && (correlateEmptyTargetSet || !hadEmptyTargetObjectSet())) {
if (sourceObjectOverride != null) {
} else {
}
try {
} finally {
}
}
return result;
}
/**
* Given a result entry from a correlation query get the full correlated target object
* @param resultValue an entry from the correlation query result list.
* May already be the full target object, or just contain the id.
* @return the target object
* @throws SynchronizationException
*/
private LazyObjectAccessor getCorrelatedTarget(JsonValue resultValue) throws SynchronizationException {
// TODO: Optimize to get the entire object with one query if it's sufficient
fullObj = new LazyObjectAccessor(service, targetObjectSet, resultValue.get("_id").required().asString(), resultValue);
} else {
fullObj = new LazyObjectAccessor(service, targetObjectSet, resultValue.get("_id").required().asString());
//fullObj.getObject();
}
return fullObj;
}
/**
* Primitive implementation to decide if the object is a "full" or a partial.
*
* @param keys attribute names of object
* @return true if the {@code keys} has value not starting with "_" char
*/
return true;
}
}
return false;
}
}
enum CorrelationType {
}
/**
* A class used to store and execute correlation queries and scripts.
*/
class Correlation {
/**
* A Map of correlation queries where the keys are {@link String} instances representing link qualifiers and the
* values are the correlation query {@link Script} instances.
*/
/**
* A correlation script which will return a Map object where the keys are {@link String} instances representing link qualifiers and the
* values are the correlation query results.
*/
/**
* The type of the correlation
*/
private CorrelationType type;
/**
* Constructor.
*
* @param config the mapping configuration
* @throws JsonValueException
*/
throw new JsonValueException(config, "Cannot configure both correlationQuery and correlationScript in a single mapping");
} else if (!correlationQueryValue.isNull()) {
if (correlationQueryValue.isList()) {
correlationQueries.put(correlationQuery.get("linkQualifier").defaultTo(Link.DEFAULT_LINK_QUALIFIER).asString(),
}
} else if (correlationQueryValue.isMap()) {
correlationQueries.put(correlationQueryValue.get("linkQualifier").defaultTo(Link.DEFAULT_LINK_QUALIFIER).asString(),
}
} else if (!correlationScriptValue.isNull()) {
} else {
}
}
/**
* Returns true if there is a correlation query or script configured for the given link qualifier, false otherwise.
*
* @param linkQualifier the link qualifier for the current sync operation.
* @return true if correlation is configured, fals otherwise.
*/
switch (type) {
case correlationQuery:
case correlationScript:
return true;
default:
return false;
}
}
/**
* Performs the correlation.
*
* @param scope the scope to use for the correlation script
* @param linkQualifier the link qualifier
* @return a list of results if no correlation is configured
* @throws SynchronizationException if there was an error during correlation
*/
public JsonValue correlate(Map<String, Object> scope, String linkQualifier) throws SynchronizationException {
// Set the link qualifier in the script's scope
try {
switch (type) {
case correlationQuery:
// Execute the correlationQuery and return the results
return json(queryTargetObjectSet(execScript(type.toString(), correlationQueries.get(linkQualifier), scope).asMap()))
case correlationScript:
// Execute the correlationScript and return the results corresponding to the given linkQualifier
default:
return null;
}
} catch (ScriptThrownException ste) {
} catch (ScriptException se) {
throw new SynchronizationException(se);
}
}
/**
* Executes a script of a given type with the given scope.
*
* @param type the type of script (correlationQuery or correlationScript)
* @param script the {@link Script} object representing the script
* @param scope the script's scope
* @return A {@link Map} representing the results
* @throws ScriptException if there was an error during execution
*/
private JsonValue execScript(String type, Script script, Map<String, Object> scope) throws ScriptException {
}
}
/**
* TODO: Description.
*/
class TargetSyncOperation extends SyncOperation {
try {
EventEntry measureSituation = Publisher.start(EVENT_TARGET_ASSESS_SITUATION, targetObjectAccessor, null);
try {
} finally {
}
EventEntry measureDetermine = Publisher.start(EVENT_TARGET_DETERMINE_ACTION, targetObjectAccessor, null);
try {
} finally {
}
EventEntry measurePerform = Publisher.start(EVENT_TARGET_PERFORM_ACTION, targetObjectAccessor, null);
try {
// TODO: Option here to just report what action would be performed?
} finally {
if (reconContext != null) {
}
}
return toJsonValue();
} catch (SynchronizationException e) {
e.setDetail(toJsonValue());
throw e;
}
}
protected boolean isSourceToTarget() {
return false;
}
linkObject.setLinkQualifier(params.get("linkQualifier").defaultTo(Link.DEFAULT_LINK_QUALIFIER).asString());
}
public JsonValue toJsonValue() {
: null));
}
/**
* TODO: Description.
*
* @throws SynchronizationException TODO.
*/
private void assessSituation() throws SynchronizationException {
// May want to consider an optimization to not query
// if we don't need the link for the TARGET_IGNORED action
}
if (!isTargetValid()) { // target is not valid for this mapping; ignore it
}
return;
}
} else {
} else if (!isSourceValid()) {
LOGGER.info("Situation in target reconciliation that indicates source may have changed {} {} {} {}",
} else { // proper link
LOGGER.info("Situation in target reconciliation that indicates source may have changed {} {} {} {}",
}
}
}
}
}