pair : postCopyAssocs)
{
AssociationRef assocRef = pair.getFirst();
AssocCopyTargetAction action = pair.getSecond();
// Was the original target copied?
NodeRef newSourceForAssoc = copiedNodeRefs.get(assocRef.getSourceRef());
if (newSourceForAssoc == null)
{
// Developer #fail
throw new IllegalStateException("Post-copy association has a source that was NOT copied.");
}
NodeRef oldTargetForAssoc = assocRef.getTargetRef();
NodeRef newTargetForAssoc = copiedNodeRefs.get(oldTargetForAssoc); // May be null
QName assocTypeQName = assocRef.getTypeQName();
switch (action)
{
case USE_ORIGINAL_TARGET:
internalNodeService.createAssociation(newSourceForAssoc, oldTargetForAssoc, assocTypeQName);
break;
case USE_COPIED_TARGET:
// Do nothing if the target was not copied
if (newTargetForAssoc != null)
{
internalNodeService.createAssociation(newSourceForAssoc, newTargetForAssoc, assocTypeQName);
}
break;
case USE_COPIED_OTHERWISE_ORIGINAL_TARGET:
if (newTargetForAssoc == null)
{
internalNodeService.createAssociation(newSourceForAssoc, oldTargetForAssoc, assocTypeQName);
}
else
{
internalNodeService.createAssociation(newSourceForAssoc, newTargetForAssoc, assocTypeQName);
}
break;
default:
throw new IllegalStateException("Unknown association action: " + action);
}
}
}
/**
* Gets the copy details. This calls the appropriate policies that have been registered
* against the node and aspect types in order to pick-up any type specific copy behaviour.
*
* The full {@link NodeService} is used for property retrieval. After this, read permission
* can be assumed to have passed on the source node - at least w.r.t. properties and aspects.
*
* NOTE: If a target node is not supplied, then one is created in the same store as the
* target parent node. This allows behavioural code always know which node will
* be copied to, even if the node does not exist.
*/
private CopyDetails getCopyDetails(
NodeRef sourceNodeRef,
NodeRef targetParentNodeRef,
NodeRef targetNodeRef,
QName assocTypeQName,
QName assocQName)
{
// The first call will fail permissions, so there is no point doing permission checks with
// the other calls
QName sourceNodeTypeQName = nodeService.getType(sourceNodeRef);
// ALF-730: MLText is not fully carried during cut-paste or copy-paste
// Use the internalNodeService to fetch the properties. It should be mlAwareNodeService.
Map sourceNodeProperties = internalNodeService.getProperties(sourceNodeRef);
Set sourceNodeAspectQNames = internalNodeService.getAspects(sourceNodeRef);
// Create a target node, if necessary
boolean targetNodeIsNew = false;
if (targetNodeRef == null)
{
targetNodeRef = new NodeRef(targetParentNodeRef.getStoreRef(), GUID.generate());
targetNodeIsNew = true;
}
CopyDetails copyDetails = new CopyDetails(
sourceNodeRef,
sourceNodeTypeQName,
sourceNodeAspectQNames,
sourceNodeProperties,
targetParentNodeRef,
targetNodeRef,
targetNodeIsNew,
assocTypeQName,
assocQName);
// Done
return copyDetails;
}
/**
* @return Returns a map of all the copy behaviours keyed by type and aspect qualified names
*/
private Map getCallbacks(CopyDetails copyDetails)
{
QName sourceNodeTypeQName = copyDetails.getSourceNodeTypeQName();
Map callbacks = new HashMap(11);
// Get the type-specific behaviour
CopyBehaviourCallback callback = getCallback(sourceNodeTypeQName, copyDetails);
callbacks.put(sourceNodeTypeQName, callback);
// Get the source aspects
for (QName sourceNodeAspectQName : copyDetails.getSourceNodeAspectQNames())
{
callback = getCallback(sourceNodeAspectQName, copyDetails);
callbacks.put(sourceNodeAspectQName, callback);
}
return callbacks;
}
/**
* @return Returns the copy callback for the given criteria
*/
private CopyBehaviourCallback getCallback(QName sourceClassQName, CopyDetails copyDetails)
{
Collection policies = this.onCopyNodeDelegate.getList(sourceClassQName);
ClassDefinition sourceClassDef = dictionaryService.getClass(sourceClassQName);
CopyBehaviourCallback callback = null;
if (sourceClassDef == null)
{
// Do nothing as the type is not in the dictionary
callback = DoNothingCopyBehaviourCallback.getInstance();
}
if (policies.isEmpty())
{
// Default behaviour
callback = DefaultCopyBehaviourCallback.getInstance();
}
else if (policies.size() == 1)
{
callback = policies.iterator().next().getCopyCallback(sourceClassQName, copyDetails);
}
else
{
// There are multiple
CompoundCopyBehaviourCallback compoundCallback = new CompoundCopyBehaviourCallback(sourceClassQName);
for (CopyServicePolicies.OnCopyNodePolicy policy : policies)
{
CopyBehaviourCallback nestedCallback = policy.getCopyCallback(sourceClassQName, copyDetails);
compoundCallback.addBehaviour(nestedCallback);
}
callback = compoundCallback;
}
// Done
if (logger.isDebugEnabled())
{
logger.debug(
"Fetched copy callback: \n" +
" Class: " + sourceClassQName + "\n" +
" Details: " + copyDetails + "\n" +
" Callback: " + callback);
}
return callback;
}
/**
* Copies the properties for the node type or aspect onto the destination node.
*/
private void copyProperties(
CopyDetails copyDetails,
NodeRef targetNodeRef,
QName classQName,
Map callbacks)
{
ClassDefinition targetClassDef = dictionaryService.getClass(classQName);
if (targetClassDef == null)
{
return; // Ignore unknown types
}
// First check if the aspect must be copied at all
CopyBehaviourCallback callback = callbacks.get(classQName);
if (callback == null)
{
throw new IllegalStateException("Source node class has no callback: " + classQName);
}
// Ignore if not present or if not scheduled for a copy
if (!callback.getMustCopy(classQName, copyDetails))
{
// Do nothing with this
return;
}
// Compile the properties to copy, even if they are empty
Map classProperties = buildCopyProperties(
copyDetails,
Collections.singleton(classQName),
callbacks);
// We don't need permissions as we've just created the node
if (targetClassDef.isAspect())
{
internalNodeService.addAspect(targetNodeRef, classQName, classProperties);
}
else
{
internalNodeService.addProperties(targetNodeRef, classProperties);
}
}
/**
* Copy properties that do not belong to the source node's type or any of the aspects.
*/
private void copyResidualProperties(
CopyDetails copyDetails,
NodeRef targetNodeRef)
{
Map residualProperties = new HashMap();
// Start with the full set
residualProperties.putAll(copyDetails.getSourceNodeProperties());
QName sourceNodeTypeQName = copyDetails.getSourceNodeTypeQName();
Set knownClassQNames = new HashSet(13);
// We add the default aspects, source-applied aspects and the source node type
knownClassQNames.addAll(getDefaultAspects(sourceNodeTypeQName));
knownClassQNames.addAll(copyDetails.getSourceNodeAspectQNames());
knownClassQNames.add(sourceNodeTypeQName);
for (QName knownClassQName : knownClassQNames)
{
ClassDefinition classDef = dictionaryService.getClass(knownClassQName);
if (classDef == null)
{
continue;
}
// Remove defined properties form the residual list
for (QName definedPropQName : classDef.getProperties().keySet())
{
residualProperties.remove(definedPropQName);
// We've removed them all, so shortcut out
if (residualProperties.size() == 0)
{
break;
}
}
}
// Add the residual properties to the node
if (residualProperties.size() > 0)
{
internalNodeService.addProperties(targetNodeRef, residualProperties);
}
}
/**
* Copies aspects from the source to the target node.
*/
private void copyAspects(
CopyDetails copyDetails,
NodeRef targetNodeRef,
Set aspectsToIgnore,
Map callbacks)
{
Set sourceAspectQNames = copyDetails.getSourceNodeAspectQNames();
for (QName aspectQName : sourceAspectQNames)
{
if (aspectsToIgnore.contains(aspectQName))
{
continue;
}
// Double check that the aspect must be copied at all
CopyBehaviourCallback callback = callbacks.get(aspectQName);
if (callback == null)
{
throw new IllegalStateException("Source aspect class has no callback: " + aspectQName);
}
if (!callback.getMustCopy(aspectQName, copyDetails))
{
continue;
}
copyProperties(copyDetails, targetNodeRef, aspectQName, callbacks);
}
}
/**
* @param copyChildren false if the client selected not to recurse
*/
private void copyChildren(
CopyDetails copyDetails,
NodeRef copyTarget,
boolean copyTargetIsNew,
boolean copyChildren,
Map copiesByOriginals,
Set copies,
Map callbacks)
{
QName sourceNodeTypeQName = copyDetails.getSourceNodeTypeQName();
Set sourceNodeAspectQNames = copyDetails.getSourceNodeAspectQNames();
// First check associations on the type
copyChildren(
copyDetails,
sourceNodeTypeQName,
copyTarget,
copyTargetIsNew,
copyChildren,
copiesByOriginals,
copies,
callbacks);
// Check associations for the aspects
for (QName aspectQName : sourceNodeAspectQNames)
{
AspectDefinition aspectDef = dictionaryService.getAspect(aspectQName);
if (aspectDef == null)
{
continue;
}
copyChildren(
copyDetails,
aspectQName,
copyTarget,
copyTargetIsNew,
copyChildren,
copiesByOriginals,
copies,
callbacks);
}
}
private static final String KEY_POST_COPY_ASSOCS = "CopyServiceImpl.postCopyAssocs";
/**
* @param copyChildren false if the client selected not to recurse
*/
private void copyChildren(
CopyDetails copyDetails,
QName classQName,
NodeRef copyTarget,
boolean copyTargetIsNew,
boolean copyChildren,
Map copiesByOriginals,
Set copies,
Map callbacks)
{
NodeRef sourceNodeRef = copyDetails.getSourceNodeRef();
ClassDefinition classDef = dictionaryService.getClass(classQName);
if (classDef == null)
{
// Ignore missing types
return;
}
// Check the behaviour
CopyBehaviourCallback callback = callbacks.get(classQName);
if (callback == null)
{
throw new IllegalStateException("Source node class has no callback: " + classQName);
}
// Prepare storage for post-copy association handling
List> postCopyAssocs =
TransactionalResourceHelper.getList(KEY_POST_COPY_ASSOCS);
// Handle peer associations.
for (Map.Entry entry : classDef.getAssociations().entrySet())
{
QName assocTypeQName = entry.getKey();
AssociationDefinition assocDef = entry.getValue();
if (assocDef.isChild())
{
continue; // Ignore child assocs
}
boolean haveRemovedFromCopyTarget = false;
// Get the associations
List assocRefs = nodeService.getTargetAssocs(sourceNodeRef, assocTypeQName);
for (AssociationRef assocRef : assocRefs)
{
// Get the copy action for the association instance
CopyAssociationDetails assocCopyDetails = new CopyAssociationDetails(
assocRef,
copyTarget,
copyTargetIsNew);
Pair assocCopyAction = callback.getAssociationCopyAction(
classQName,
copyDetails,
assocCopyDetails);
// Consider the source side first
switch (assocCopyAction.getFirst())
{
case IGNORE:
continue; // Do nothing
case COPY_REMOVE_EXISTING:
if (!copyTargetIsNew && !haveRemovedFromCopyTarget)
{
// Only do this if we are copying over an existing node and we have NOT
// already cleaned up for this association type
haveRemovedFromCopyTarget = true;
for (AssociationRef assocToRemoveRef : internalNodeService.getTargetAssocs(copyTarget, assocTypeQName))
{
internalNodeService.removeAssociation(assocToRemoveRef.getSourceRef(), assocToRemoveRef.getTargetRef(), assocTypeQName);
}
}
// Fall through to copy
case COPY:
// Record the type of target behaviour that is expected
switch (assocCopyAction.getSecond())
{
case USE_ORIGINAL_TARGET:
case USE_COPIED_TARGET:
case USE_COPIED_OTHERWISE_ORIGINAL_TARGET:
// Have to save for later to see if the target node is copied, too
postCopyAssocs.add(new Pair(assocRef, assocCopyAction.getSecond()));
break;
default:
throw new IllegalStateException("Unknown association target copy action: " + assocCopyAction);
}
break;
default:
throw new IllegalStateException("Unknown association source copy action: " + assocCopyAction);
}
}
}
// Handle child associations. These need special attention due to their recursive nature.
for (Map.Entry childEntry : classDef.getChildAssociations().entrySet())
{
QName childAssocTypeQName = childEntry.getKey();
ChildAssociationDefinition childAssocDef = childEntry.getValue();
if (!childAssocDef.isChild())
{
continue; // Ignore non-child assocs
}
// Get the child associations
List childAssocRefs = nodeService.getChildAssocs(
sourceNodeRef, childAssocTypeQName, RegexQNamePattern.MATCH_ALL);
for (ChildAssociationRef childAssocRef : childAssocRefs)
{
NodeRef childNodeRef = childAssocRef.getChildRef();
QName assocQName = childAssocRef.getQName();
CopyChildAssociationDetails childAssocCopyDetails = new CopyChildAssociationDetails(
childAssocRef,
copyTarget,
copyTargetIsNew,
copyChildren);
// Handle nested copies
if (copies.contains(childNodeRef))
{
// The node was already copied i.e. we are seeing a copy produced by some earlier
// copy process.
// The first way this can occur is if a hierarchy is copied into some lower part
// of the hierarchy. We avoid the copied part.
// The other way this could occur is if there are multiple assocs between a
// parent and child. Calls to this method are scoped by class, so the newly-created
// node will not be found because it will have been created using a different assoc
// type.
// A final edge case is where there are multiple assocs between parent and child
// of the same type. This is ignorable.
continue;
}
// Get the copy action for the association instance
ChildAssocCopyAction childAssocCopyAction = callback.getChildAssociationCopyAction(
classQName,
copyDetails,
childAssocCopyDetails);
switch (childAssocCopyAction)
{
case IGNORE:
break;
case COPY_ASSOC:
nodeService.addChild(copyTarget, childNodeRef, childAssocTypeQName, assocQName);
break;
case COPY_CHILD:
// Handle potentially cyclic relationships
if (copiesByOriginals.containsKey(childNodeRef))
{
// This is either a cyclic relationship or there are multiple different
// types of associations between the same parent and child.
// Just hook the child up with the association.
nodeService.addChild(copyTarget, childNodeRef, childAssocTypeQName, assocQName);
}
else
{
// Find out whether to force a recursion
ChildAssocRecurseAction childAssocRecurseAction = callback.getChildAssociationRecurseAction(
classQName,
copyDetails,
childAssocCopyDetails);
switch (childAssocRecurseAction)
{
case RESPECT_RECURSE_FLAG:
// Keep child copy flag the same
break;
case FORCE_RECURSE:
// Force recurse
copyChildren = true;
break;
default:
throw new IllegalStateException("Unrecognized enum");
}
// This copy may fail silently
copyImpl(
childNodeRef, copyTarget,
childAssocTypeQName, assocQName,
copyChildren, false, // Keep child names for deep copies
copiesByOriginals, copies);
}
break;
default:
throw new IllegalStateException("Unrecognized enum");
}
}
}
}
/**
* Callback behaviour retrieval for the 'copiedFrom' aspect.
*
* @return Returns {@link DoNothingCopyBehaviourCallback} always
*/
public CopyBehaviourCallback getCallbackForCopiedFromAspect(QName classRef, CopyDetails copyDetails)
{
return DoNothingCopyBehaviourCallback.getInstance();
}
/**
* Callback behaviour retrieval for {@link ContentModel#TYPE_FOLDER} aspect.
*
* @return Returns {@link FolderTypeCopyBehaviourCallback}
*/
public CopyBehaviourCallback getCallbackForFolderType(QName classRef, CopyDetails copyDetails)
{
return FolderTypeCopyBehaviourCallback.INSTANCE;
}
/**
* cm:folder behaviour
*
* @author Derek Hulley
* @since 3.2
*/
private static class FolderTypeCopyBehaviourCallback extends DefaultCopyBehaviourCallback
{
private static final CopyBehaviourCallback INSTANCE = new FolderTypeCopyBehaviourCallback();
/**
* Respects the copyChildren
flag. Child nodes are copied fully if the association
* is primary otherwise secondary associations are duplicated.
*/
@Override
public ChildAssocCopyAction getChildAssociationCopyAction(
QName classQName,
CopyDetails copyDetails,
CopyChildAssociationDetails childAssocCopyDetails)
{
ChildAssociationRef childAssocRef = childAssocCopyDetails.getChildAssocRef();
boolean copyChildren = childAssocCopyDetails.isCopyChildren();
if (childAssocRef.getTypeQName().equals(ContentModel.ASSOC_CONTAINS))
{
if (!copyChildren)
{
return ChildAssocCopyAction.IGNORE;
}
if (childAssocRef.isPrimary())
{
return ChildAssocCopyAction.COPY_CHILD;
}
else
{
return ChildAssocCopyAction.COPY_ASSOC;
}
}
else
{
throw new IllegalStateException(
"Behaviour should have been invoked: \n" +
" Aspect: " + this.getClass().getName() + "\n" +
" Assoc: " + childAssocRef + "\n" +
" " + copyDetails);
}
}
}
/**
* Callback behaviour retrieval for the 'ownable' aspect.
*
* @return Returns {@link DoNothingCopyBehaviourCallback} always
*/
public CopyBehaviourCallback getCallbackForOwnableAspect(QName classRef, CopyDetails copyDetails)
{
return DoNothingCopyBehaviourCallback.getInstance();
}
}