/*
* Copyright (C) 2005-2010 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see .
*/
package org.alfresco.repo.action;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.action.evaluator.ActionConditionEvaluator;
import org.alfresco.repo.action.executer.ActionExecuter;
import org.alfresco.repo.copy.CopyBehaviourCallback;
import org.alfresco.repo.copy.CopyDetails;
import org.alfresco.repo.copy.CopyServicePolicies;
import org.alfresco.repo.copy.DefaultCopyBehaviourCallback;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.security.authentication.AuthenticationContext;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.TransactionListenerAdapter;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ActionCondition;
import org.alfresco.service.cmr.action.ActionConditionDefinition;
import org.alfresco.service.cmr.action.ActionDefinition;
import org.alfresco.service.cmr.action.ActionList;
import org.alfresco.service.cmr.action.ActionService;
import org.alfresco.service.cmr.action.ActionServiceException;
import org.alfresco.service.cmr.action.ActionStatus;
import org.alfresco.service.cmr.action.CompositeAction;
import org.alfresco.service.cmr.action.CompositeActionCondition;
import org.alfresco.service.cmr.action.ParameterConstraint;
import org.alfresco.service.cmr.action.ParameterizedItem;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.namespace.DynamicNamespacePrefixResolver;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.GUID;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.alfresco.util.PropertyCheck;
/**
* Action service implementation
*
* @author Roy Wetherall
*/
public class ActionServiceImpl implements ActionService, RuntimeActionService, ApplicationContextAware,
CopyServicePolicies.OnCopyNodePolicy, CopyServicePolicies.OnCopyCompletePolicy
{
/**
* Transaction resource name
*/
private static final String POST_TRANSACTION_PENDING_ACTIONS = "postTransactionPendingActions";
/**
* Error message
*/
private static final String ERR_FAIL = "The action failed to execute due to an error.";
/**
* The logger
*/
private static Log logger = LogFactory.getLog(ActionServiceImpl.class);
/**
* Thread local containing the current action chain
*/
ThreadLocal> currentActionChain = new ThreadLocal>();
/**
* The application context used to retrieve defined actions
*/
private ApplicationContext applicationContext;
private NodeService nodeService;
private SearchService searchService;
private DictionaryService dictionaryService;
private TransactionService transactionService;
private AuthenticationContext authenticationContext;
private PolicyComponent policyComponent;
/**
* The asynchronous action execution queues map of name, queue
*/
private Map asynchronousActionExecutionQueues;
/**
* Action transaction listener
*/
private ActionTransactionListener transactionListener = new ActionTransactionListener(this);
/**
* All the condition definitions currently registered
*/
private Map conditionDefinitions = new HashMap();
/**
* All the action definitions currently registered
*/
private Map actionDefinitions = new HashMap();
/**
* All the parameter constraints
*/
private Map parameterConstraints = new HashMap();
/**
* Set the application context
*
* @param applicationContext the application context
*/
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
{
this.applicationContext = applicationContext;
}
/**
* Set the node service
*
* @param nodeService the node service
*/
public void setNodeService(NodeService nodeService)
{
this.nodeService = nodeService;
}
/**
* Set the search service
*
* @param searchService the search service
*/
public void setSearchService(SearchService searchService)
{
this.searchService = searchService;
}
/**
* Set the authentication component
*
* @param authenticationContext the authentication component
*/
public void setAuthenticationContext(AuthenticationContext authenticationContext)
{
this.authenticationContext = authenticationContext;
}
/**
* Set the dictionary service
*
* @param dictionaryService the dictionary service
*/
public void setDictionaryService(DictionaryService dictionaryService)
{
this.dictionaryService = dictionaryService;
}
/**
* Set the transaction service
*
* @param transactionService the transaction service
*/
public void setTransactionService(TransactionService transactionService)
{
this.transactionService = transactionService;
}
/**
* @param policyComponent used to set up the action-based policy behaviour
*/
public void setPolicyComponent(PolicyComponent policyComponent)
{
this.policyComponent = policyComponent;
}
/**
* Set the asynchronous action execution queues
*
* @param asynchronousActionExecutionQueue the asynchronous action execution
* queues
*/
public void setAsynchronousActionExecutionQueues(
Map asynchronousActionExecutionQueues)
{
this.asynchronousActionExecutionQueues = asynchronousActionExecutionQueues;
}
public void init()
{
PropertyCheck.mandatory(this, "policyComponent", policyComponent);
this.policyComponent.bindClassBehaviour(QName.createQName(NamespaceService.ALFRESCO_URI, "getCopyCallback"),
ActionModel.TYPE_ACTION_PARAMETER, new JavaBehaviour(this, "getCopyCallback"));
this.policyComponent.bindClassBehaviour(QName.createQName(NamespaceService.ALFRESCO_URI, "onCopyComplete"),
ActionModel.TYPE_ACTION_PARAMETER, new JavaBehaviour(this, "onCopyComplete"));
}
/**
* Gets the saved action folder reference
*
* @param nodeRef the node reference
* @return the node reference
*/
private NodeRef getSavedActionFolderRef(NodeRef nodeRef)
{
List assocs = this.nodeService.getChildAssocs(nodeRef, RegexQNamePattern.MATCH_ALL,
ActionModel.ASSOC_ACTION_FOLDER);
if (assocs.size() != 1)
{
throw new ActionServiceException("Unable to retrieve the saved action folder reference.");
}
return assocs.get(0).getChildRef();
}
/**
* @see org.alfresco.service.cmr.action.ActionService#getActionDefinition(java.lang.String)
*/
public ActionDefinition getActionDefinition(String name)
{
// get direct access to action definition (i.e. ignoring public flag of
// executer)
ActionDefinition definition = null;
Object bean = this.applicationContext.getBean(name);
if (bean != null && bean instanceof ActionExecuter)
{
ActionExecuter executer = (ActionExecuter) bean;
definition = executer.getActionDefinition();
}
return definition;
}
/**
* @see org.alfresco.service.cmr.action.ActionService#getActionDefinitions()
*/
public List getActionDefinitions()
{
return new ArrayList(this.actionDefinitions.values());
}
/**
* @see org.alfresco.service.cmr.action.ActionService#getActionDefinitions(org.alfresco.service.cmr.repository.NodeRef)
*/
public List getActionDefinitions(NodeRef nodeRef)
{
if (nodeRef == null)
{
return getActionDefinitions();
}
else
{
// TODO for now we will only filter by type, we will introduce
// filtering by aspect later
QName nodeType = this.nodeService.getType(nodeRef);
List result = new ArrayList();
for (ActionDefinition actionDefinition : getActionDefinitions())
{
List appliciableTypes = actionDefinition.getApplicableTypes();
if (appliciableTypes != null && appliciableTypes.isEmpty() == false)
{
for (QName applicableType : actionDefinition.getApplicableTypes())
{
if (this.dictionaryService.isSubClass(nodeType, applicableType))
{
result.add(actionDefinition);
break;
}
}
}
else
{
result.add(actionDefinition);
}
}
return result;
}
}
/**
* @see org.alfresco.service.cmr.action.ActionService#getActionConditionDefinition(java.lang.String)
*/
public ActionConditionDefinition getActionConditionDefinition(String name)
{
return this.conditionDefinitions.get(name);
}
/**
* @see org.alfresco.service.cmr.action.ActionService#getActionConditionDefinitions()
*/
public List getActionConditionDefinitions()
{
return new ArrayList(this.conditionDefinitions.values());
}
/**
* @see org.alfresco.service.cmr.action.ActionService#getParameterConstraint(java.lang.String)
*/
public ParameterConstraint getParameterConstraint(String name)
{
return this.parameterConstraints.get(name);
}
/**
* @see org.alfresco.service.cmr.action.ActionService#getParameterConstraints()
*/
public List getParameterConstraints()
{
return new ArrayList(this.parameterConstraints.values());
}
/**
* @see org.alfresco.service.cmr.action.ActionService#createActionCondition(java.lang.String)
*/
public ActionCondition createActionCondition(String name)
{
if (logger.isDebugEnabled())
logger.debug("Creating Action Condition - [" + name + "]");
if (CompositeActionCondition.COMPOSITE_CONDITION.equals(name))
{
return new CompositeActionConditionImpl(GUID.generate());
}
return new ActionConditionImpl(GUID.generate(), name);
}
/**
* @see org.alfresco.service.cmr.action.ActionService#createActionCondition(java.lang.String,
* java.util.Map)
*/
public ActionCondition createActionCondition(String name, Map params)
{
ActionCondition condition = createActionCondition(name);
condition.setParameterValues(params);
return condition;
}
/**
* @see org.alfresco.service.cmr.action.ActionService#createAction()
*/
public Action createAction(String name)
{
return new ActionImpl(null, GUID.generate(), name, null);
}
/**
* @see org.alfresco.service.cmr.action.ActionService#createAction(java.lang.String,
* java.util.Map)
*/
public Action createAction(String name, Map params)
{
Action action = createAction(name);
action.setParameterValues(params);
return action;
}
/**
* @see org.alfresco.service.cmr.action.ActionService#createCompositeAction()
*/
public CompositeAction createCompositeAction()
{
return new CompositeActionImpl(null, GUID.generate());
}
/**
* @see org.alfresco.service.cmr.action.ActionService#createCompositeActionCondition()
*/
public CompositeActionCondition createCompositeActionCondition()
{
return new CompositeActionConditionImpl(GUID.generate());
}
/**
* @see org.alfresco.service.cmr.action.ActionService#evaluateAction(org.alfresco.service.cmr.action.Action,
* org.alfresco.service.cmr.repository.NodeRef)
*/
public boolean evaluateAction(Action action, NodeRef actionedUponNodeRef)
{
boolean result = true;
if (action.hasActionConditions() == true)
{
List actionConditions = action.getActionConditions();
for (ActionCondition condition : actionConditions)
{
boolean tempresult = evaluateActionCondition(condition, actionedUponNodeRef);
if (logger.isDebugEnabled())
logger.debug("\tCondition " + condition.getActionConditionDefinitionName() + " Result - "
+ tempresult);
result = result && tempresult;
}
}
if (logger.isDebugEnabled())
logger.debug("\tAll Condition Evaluation Result - " + result);
return result;
}
/**
* Evaluates the actions by finding corresponding actionEvaluators in
* applicationContext (registered through Spring). Composite conditions are
* evaluated here as well. It is also possible to have composite actions
* inside composite actions.
*
* @see org.alfresco.service.cmr.action.ActionService#evaluateActionCondition(org.alfresco.service.cmr.action.ActionCondition,
* org.alfresco.service.cmr.repository.NodeRef)
*/
public boolean evaluateActionCondition(ActionCondition condition, NodeRef actionedUponNodeRef)
{
if (condition instanceof CompositeActionCondition)
{
CompositeActionCondition compositeCondition = (CompositeActionCondition) condition;
if (logger.isDebugEnabled())
{
logger.debug("Evaluating Composite Condition - BOOLEAN CONDITION IS "
+ (compositeCondition.isORCondition() ? "OR" : "AND"));
}
if (!compositeCondition.hasActionConditions())
{
throw new IllegalStateException("CompositeActionCondition has no subconditions.");
}
boolean result;
if (compositeCondition.isORCondition())
{
result = false;
}
else
{
result = true;
}
for (ActionCondition simplecondition : compositeCondition.getActionConditions())
{
if (logger.isDebugEnabled())
{
logger
.debug("Evaluating composite condition "
+ simplecondition.getActionConditionDefinitionName());
}
if (compositeCondition.isORCondition())
{
result = result || evaluateSimpleCondition(simplecondition, actionedUponNodeRef);
// Short circuit for the OR condition
if (result)
break;
}
else
{
result = result && evaluateSimpleCondition(simplecondition, actionedUponNodeRef);
// Short circuit for the AND condition
if (!result)
break;
}
}
if (compositeCondition.getInvertCondition())
{
return !result;
}
else
{
return result;
}
}
else
{
return evaluateSimpleCondition(condition, actionedUponNodeRef);
}
}
private boolean evaluateSimpleCondition(ActionCondition condition, NodeRef actionedUponNodeRef)
{
if (logger.isDebugEnabled())
{
logger.debug("Evaluating simple condition " + condition.getActionConditionDefinitionName());
}
// Evaluate the condition
ActionConditionEvaluator evaluator = (ActionConditionEvaluator) this.applicationContext.getBean(condition
.getActionConditionDefinitionName());
return evaluator.evaluate(condition, actionedUponNodeRef);
}
/**
* @see org.alfresco.service.cmr.action.ActionService#executeAction(org.alfresco.service.cmr.action.Action,
* org.alfresco.service.cmr.repository.NodeRef, boolean)
*/
public void executeAction(Action action, NodeRef actionedUponNodeRef, boolean checkConditions)
{
executeAction(action, actionedUponNodeRef, checkConditions, action.getExecuteAsychronously());
}
/**
* @see org.alfresco.service.cmr.action.ActionService#executeAction(org.alfresco.service.cmr.action.Action,
* org.alfresco.service.cmr.repository.NodeRef, boolean)
*/
public void executeAction(Action action, NodeRef actionedUponNodeRef, boolean checkConditions,
boolean executeAsychronously)
{
Set actionChain = this.currentActionChain.get();
if (executeAsychronously == false)
{
executeActionImpl(action, actionedUponNodeRef, checkConditions, false, actionChain);
}
else
{
// Add to the post transaction pending action list
addPostTransactionPendingAction(action, actionedUponNodeRef, checkConditions, actionChain);
}
}
/**
* called by transaction service.
*/
public void postCommit()
{
for (PendingAction pendingAction : getPostTransactionPendingActions())
{
queueAction(pendingAction);
}
}
/**
*
*/
private void queueAction(PendingAction action)
{
// Get the right queue
AsynchronousActionExecutionQueue queue = getQueue(action.action);
// Queue the action for execution
queue.executeAction(this, action.getAction(), action.getActionedUponNodeRef(), action.getCheckConditions(),
action.getActionChain());
}
/**
* @param compensatingAction
* @param actionedUponNodeRef
*/
private void queueAction(Action compensatingAction, NodeRef actionedUponNodeRef)
{
// Get the right queue
AsynchronousActionExecutionQueue queue = getQueue(compensatingAction);
// Queue the action for execution
queue.executeAction(this, compensatingAction, actionedUponNodeRef, false, null);
}
private AsynchronousActionExecutionQueue getQueue(Action action)
{
ActionExecuter executer = (ActionExecuter) this.applicationContext.getBean(action.getActionDefinitionName());
AsynchronousActionExecutionQueue queue = null;
String queueName = executer.getQueueName();
if (queueName == null)
{
queue = asynchronousActionExecutionQueues.get("");
}
else
{
queue = asynchronousActionExecutionQueues.get(queueName);
}
if (queue == null)
{
// can't get queue
throw new ActionServiceException("Unable to get AsynchronousActionExecutionQueue name: " + queueName);
}
return queue;
}
/**
* @see org.alfresco.repo.action.RuntimeActionService#executeActionImpl(org.alfresco.service.cmr.action.Action,
* org.alfresco.service.cmr.repository.NodeRef, boolean,
* org.alfresco.service.cmr.repository.NodeRef)
*/
public void executeActionImpl(Action action, NodeRef actionedUponNodeRef, boolean checkConditions,
boolean executedAsynchronously, Set actionChain)
{
if (logger.isDebugEnabled() == true)
{
StringBuilder builder = new StringBuilder("Execute action impl action chain = ");
if (actionChain == null)
{
builder.append("null");
}
else
{
for (String value : actionChain)
{
builder.append(value).append(" ");
}
}
logger.debug(builder.toString());
logger.debug("Current action = " + action.getId());
}
// get the current user early in case the process fails and we are
// unable to do it later
String currentUserName = this.authenticationContext.getCurrentUserName();
if (actionChain == null || actionChain.contains(action.getId()) == false)
{
if (logger.isDebugEnabled() == true)
{
logger.debug("Doing executeActionImpl");
}
try
{
Set origActionChain = null;
if (actionChain == null)
{
actionChain = new HashSet();
}
else
{
origActionChain = new HashSet(actionChain);
}
actionChain.add(action.getId());
this.currentActionChain.set(actionChain);
if (logger.isDebugEnabled() == true)
{
logger.debug("Adding " + action.getId() + " to action chain.");
}
try
{
// Check and execute now
if (checkConditions == false || evaluateAction(action, actionedUponNodeRef) == true)
{
// Mark the action as starting
((ActionImpl)action).setExecutionStartDate(new Date());
((ActionImpl)action).setExecutionStatus(ActionStatus.Running);
// Execute the action
directActionExecution(action, actionedUponNodeRef);
// Mark it as having worked
((ActionImpl)action).setExecutionEndDate(new Date());
((ActionImpl)action).setExecutionStatus(ActionStatus.Completed);
((ActionImpl)action).setExecutionFailureMessage(null);
if(action.getNodeRef() != null)
{
saveActionImpl(action.getNodeRef(), action);
}
}
}
finally
{
if (origActionChain == null)
{
this.currentActionChain.remove();
}
else
{
this.currentActionChain.set(origActionChain);
}
if (logger.isDebugEnabled() == true)
{
logger.debug("Resetting the action chain.");
}
}
}
catch (Throwable exception)
{
// DH: No logging of the exception. Leave the logging decision
// to the client code,
// which can handle the rethrown exception.
if (executedAsynchronously == true)
{
// If one is specified, queue the compensating action ready
// for execution
Action compensatingAction = action.getCompensatingAction();
if (compensatingAction != null)
{
// Set the current user
((ActionImpl) compensatingAction).setRunAsUser(currentUserName);
queueAction(compensatingAction, actionedUponNodeRef);
}
}
// Have the failure logged on the action
recordActionFailure(action, exception);
// Rethrow the exception
if (exception instanceof RuntimeException)
{
throw (RuntimeException) exception;
}
else
{
throw new ActionServiceException(ERR_FAIL, exception);
}
}
}
}
/**
* Schedule the recording of the action failure to occur
* in another transaction
*/
protected void recordActionFailure(Action action, Throwable exception)
{
if (logger.isDebugEnabled() == true)
{
logger.debug("Will shortly record failure of action " + action + " due to " + exception.getMessage());
}
((ActionImpl)action).setExecutionEndDate(new Date());
((ActionImpl)action).setExecutionStatus(ActionStatus.Failed);
((ActionImpl)action).setExecutionFailureMessage(exception.getMessage());
if(action.getNodeRef() != null)
{
// Take a local copy of the details
// (That way, if someone has a reference to the
// action and plays with it, we still save the
// correct information)
final String actionId = action.getId();
final Date startedAt = action.getExecutionStartDate();
final Date endedAt = action.getExecutionEndDate();
final String message = action.getExecutionFailureMessage();
final NodeRef actionNode = action.getNodeRef();
// Have the details updated on the action as soon
// as the transaction has finished rolling back
AlfrescoTransactionSupport.bindListener(
new TransactionListenerAdapter() {
public void afterRollback()
{
transactionService.getRetryingTransactionHelper().doInTransaction(
new RetryingTransactionCallback