Obsolete Pages{{Obsolete}}
The official documentation is at: http://docs.alfresco.com
Back to Developer Guide
An action is a unit of work that can be performed against a node. It is configured through a content rule. For example, moving a node, copying a node, checking a node in, transfoming the contents of a node, etc.
There are a number of standard action types provided within the Alfresco repository, but it is also possible to add your own custom actions. This page is intended to give a 'How To' guide for those wanting to create custom actions.
First you will need to decide what you want your action to do. For the sake of this tutorial I will use the simple example of an action that applies an aspect to node.
I will call this action AddAspect. The real life implementaion of this action can be found in the repository (org.alfresco.repo.action.executer.AddFeaturesActionExecuter
) and is called 'AddFeatures'.
An action executer contains the implementation of an action. It's where you put the code that is going to do the work with the node.
An action executer must implement the interface org.alfresco.repo.action.executer.ActionExecuter
.
public interface ActionExecuter
{
/**
* Get the action definition for the action
*
* @return the action definition
*/
public ActionDefinition getActionDefinition();
/**
* Execute the action executer
*
* @param action the action
* @param actionedUponNodeRef the actioned upon node reference
*/
public void execute(
Action action,
NodeRef actionedUponNodeRef);
}
As you can see there are two parts to the contract:
This interface can be extended directly, but there is some additional work you will need to do in order for the action service to recognize the action as available to the client (see Extending The Action Executer Interface Directly for more details), but in most situations it is best to extend from ActionExecuterAbstractBase found in the same package.
The abstract base class, ActionExecuterAbstractBase
, does a lot of the extra work for you and provides some built in behavior and a couple of abstract methods to implement making life a bit easier.
Back to our example, if we're going to create the 'AddAspect' action the first thing we're going to do is create a new class extending ActionExecuterAbstractBase
.
public class AddAspectActionExecuter extends ActionExecuterAbstractBase
{
/**
* @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(Action, NodeRef)
*/
@Override
public void executeImpl(Action action, NodeRef actionedUponNodeRef)
{
// TODO fill in implementation
}
/**
* @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefinitions(java.util.List)
*/
@Override
protected void addParameterDefinitions(List<ParameterDefinition> paramList)
{
// TODO fill in action parameter definitions
}
}
We can now start filling in the implementation of our action and to do this we are going to need a couple of things:
Lets deal with the node service. Since action executors are intended to be configured in Spring it means getting hold of any of the repository services is a simple matter of adding the setter methods that will be used by the Spring config to inject the required services.
public class AddAspectActionExecuter extends ActionExecuterAbstractBase
{
/**
* the node service
*/
private NodeService nodeService;
/**
* Set the node service
*
* @param nodeService the node service
*/
public void setNodeService(NodeService nodeService)
{
this.nodeService = nodeService;
}
}
Next we need to parametrize the action executer so that we can get hold of the details of the aspect to apply when the action is executed.
When an action is created it is based upon an action definition (which, as we have seen, relates to an action executer). An instance of an action will contain values for the parameters that the action definition defines.
In this example we want an action of type 'AddAspect' to provide a parameter containing the QName of the aspect to apply. We specify this by adding a parameter definition in the method addParameterDefintions
.
public class AddAspectActionExecuter extends ActionExecuterAbstractBase
{
public static final String NAME = 'add-aspect';
public static final String PARAM_ASPECT_NAME = 'aspect-name';
/**
* @see org.alfresco.repo.action.ParameterizedItemAbstractBase#addParameterDefinitions(java.util.List)
*/
@Override
protected void addParameterDefinitions(List<ParameterDefinition> paramList)
{
// Add definitions for action parameters
paramList.add(
new ParameterDefinitionImpl( // Create a new parameter defintion to add to the list
PARAM_ASPECT_NAME, // The name used to identify the parameter
DataTypeDefinition.QNAME, // The parameter value type
true, // Indicates whether the parameter is mandatory
getParamDisplayLabel(PARAM_ASPECT_NAME))); // The parameters display label
}
}
Now, when getActionDefinition
is called from the base class, an action definition class will be returned containing the correct parameter definitions. This is taken care of by ParameterizedItemAbstractBase
, which is in the inheritance hierarchy.
With this done we can fill in the implementation of the action executer.
public class AddAspectActionExecuter extends ActionExecuterAbstractBase
{
/**
* @see org.alfresco.repo.action.executer.ActionExecuterAbstractBase#executeImpl(Action, NodeRef)
*/
public void executeImpl(Action action, NodeRef actionedUponNodeRef)
{
// Check that the node still exists
if (this.nodeService.exists(actionedUponNodeRef) == true)
{
// Get the qname of the aspect to apply, we know it must have been set since it is mandatory parameter
QName aspectQName = (QName)action.getParameterValue(PARAM_ASPECT_NAME);
// Use the node service to apply the aspect to the node
this.nodeService.addAspect(actionedUponNodeRef, aspectQName, null);
}
}
}
Associated with an action is a title and description. In addition, each parameter has a displayable title. All these string should be retrieved from i18n bundles to ensure that the repository remains localisable.
The abstract base class, ActionExecuterAbstractBase
, looks somewhere in a loaded bundle, the entries <action-class-name>.title and <action-class-name>.description.
Also the method getParamDisplayLabel
is used to provide a message id for a parameter. It will generate a message id of the form <action-class-name>.<param-name>.display-label. This message should be used when creating a parameter defintion. (see example above).
In our example this means in order to internationalise our custom action we must add the following lines to a resource bundle that is being loaded.
# Custom action messages
add-aspect.title=Add aspect to item
add-aspect.description=This will add an aspect to the matched item.
add-aspect.aspect-name.display-label=The name of the aspect to apply to the node.
If you want to learn more about how the respositories' I18N features work, go to Multilingual Document Support.
In order to make our custom action available, we need to configure the action as a Spring bean. This can be done by adding the following configuration into the classes/alfresco/action-services-context.xml
configuration file.
<bean id='add-aspect' class='org.alfresco.repo.action.executer.AddAspectActionExecuter' parent='action-executer'>
<property name='nodeService'>
<ref bean='nodeService' />
</property>
</bean>
Alternatively, equivalent configuration can be added to your own Spring configuration file.
In order to prevent the UI from picking this new action up before we have added the UI to support it we can configure the action to be private. The action can still be used in the repository but the web client will not present it in the list of available action types.
<bean id='add-aspect' class='org.alfresco.repo.action.executer.AddAspectActionExecuter' parent='action-executer'>
<property name='nodeService'>
<ref bean='nodeService' />
</property>
<property name='publicAction'>
<value>false</value>
</property>
</bean>
When writing a custom action we highly recommend that you write a unit test to accompany it. The following code shows an example unit test for the add aspect custom action we have been developing.
As you will see we have used the BaseSpringTest
class as a basis for our tests, and you will probably find it useful to do so too.
/**
* Add aspect action execution test
*
* @author Roy Wetherall
*/
public class AddAspectActionExecuterTest extends BaseSpringTest
{
/**
* The node service
*/
private NodeService nodeService;
/**
* The store reference
*/
private StoreRef testStoreRef;
/**
* The root node reference
*/
private NodeRef rootNodeRef;
/**
* The test node reference
*/
private NodeRef nodeRef;
/**
* The add aspect action executer
*/
private AddAspectActionExecuter executer;
/**
* Id used to identify the test action created
*/
private final static String ID = GUID.generate();
/**
* Called at the begining of all tests
*/
@Override
protected void onSetUpInTransaction() throws Exception
{
this.nodeService = (NodeService)this.applicationContext.getBean('nodeService');
// Create the store and get the root node
this.testStoreRef = this.nodeService.createStore(
StoreRef.PROTOCOL_WORKSPACE, 'Test_'
+ System.currentTimeMillis());
this.rootNodeRef = this.nodeService.getRootNode(this.testStoreRef);
// Create the node used for tests
this.nodeRef = this.nodeService.createNode(
this.rootNodeRef,
ContentModel.ASSOC_CHILDREN,
QName.createQName('{test}testnode'),
ContentModel.TYPE_CONTENT).getChildRef();
// Get the executer instance
this.executer = (AddAspectActionExecuter)this.applicationContext.getBean(AddAspectActionExecuter.NAME);
}
/**
* Test execution
*/
public void testExecution()
{
// Check that the node does not have the classifiable aspect
assertFalse(this.nodeService.hasAspect(this.nodeRef, ContentModel.ASPECT_CLASSIFIABLE));
// Execute the action
ActionImpl action = new ActionImpl(ID, AddAspectActionExecuter.NAME, null);
action.setParameterValue(AddAspectActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_CLASSIFIABLE);
this.executer.execute(action, this.nodeRef);
// Check that the node now has the classifiable aspect applied
assertTrue(this.nodeService.hasAspect(this.nodeRef, ContentModel.ASPECT_CLASSIFIABLE));
}
}