The power of Alfresco Behaviours and Policies
Hi all, back with another post after some busy months on the field that i just could not find time to share a post or two. Since i truly believe that the technology know-how is to be shared, this time i will speak about a very powerful feature of Alfresco. The Alfresco Behaviours and Policies.
I believe that an effective ECM project should be more focused on how creative we are while interacting with the technology than on the technology itself. It may sound weird at first , but the idea behind that thought is “Thrust the technology. Use it Creatively”. This invites us to redirect most of our implementation focus on how creative and effective we are using the technology while implementing our business requirements and facing our project challenges.
Because the Alfresco technology was designed having extensibility and integration in consideration, there are lots of different ways to reach the same business goals. Choosing what component to implement specific goals can be hard. On my years of consulting practice, i’ve seen lots of “killing flies with machine guns” scenarios that could have been implemented with a more simplistic (sometimes much cheaper) approach. When i joined Alfresco, after my technology a deep dive,i felt like a a kid in a party with a huge table full of delicious flavours but just can’t make his mind on what to eat.
If you think about it, the success factors of a project are not just “how good and optimised is my code, how fast are my servers performing, how effective are my processes”. With new technologies such as Alfresco we must factor in “how creative and efective were my choices inside my chosen technology”. I could write a very long post only on this topic, but actually, you are reading a post about Alfresco Behaviours and Policies , a very powerful alfresco feature, sometimes forgotten or not considered in important implementation decisions.
Imagine a business requirement defining that specific mime-types (such as for example big video files and audio files) should be automatically stored on a different (cheaper) disk than all the other contents. How can we accomplish this ? Hopefully i will be able to explain you (and provide you with the code for it) during this post.
Alfresco allows you to fire automated actions over content on its repository. There are many ways of automating those actions on content (rules, scheduled tasks, policies…). For this post we will focus only on behaviours that are parts of business logic binded to repository policies and events.
There are a set of policies that are called from the alfresco services. For example, the policies available in NodeService are listed on the table below. Note the self-explanatory Inner Interfaces names that help deducting the events that trigger them.
Interface | Inner Interface |
NodeServicePolicies | BeforeCreateStorePolicy |
OnCreateStorePolicy | |
BeforeCreateNodePolicy | |
OnCreateNodePolicy | |
BeforeMoveNodePolicy | |
OnMoveNodePolicy | |
BeforeUpdateNodePolicy | |
OnUpdateNodePolicy | |
OnUpdatePropertiesPolicy | |
BeforeDeleteNodePolicy | |
OnDeleteNodePolicy | |
BeforeAddAspectPolicy | |
OnAddAspectPolicy | |
BeforeRemoveAspectPolicy | |
OnRemoveAspectPolicy | |
OnRestoreNodePolicy | |
BeforeCreateNodeAssociationPolicy | |
OnCreateNodeAssociationPolicy | |
OnCreateChildAssociationPolicy | |
BeforeDeleteChildAssociationPolicy | |
OnDeleteChildAssociationPolicy | |
OnCreateAssociationPolicy | |
OnDeleteAssociationPolicy | |
BeforeSetNodeTypePolicy | |
OnSetNodeTypePolicy |
There are also Policies for ContentService, CopyService and VersionService and some others. If you search the Alfresco source code using “*Policies” pattern you will also find policies like CheckOutCheckInServicePolicies, LockServicePolicies, NodeServicePolicies, TransferServicePolicies, StoreSelectorPolicies , AsynchronousActionExecutionQueuePolicies and RecordsManagementPolicies
An alfresco behaviour is simply a Java class that implement one of the policy interfaces. One advantage on using behaviours over rules is that the behaviours are applied globally to the repository while rules can be disabled by configuration (such as in bulk import scenarios). In comparison to scheduled task the behaviour is applied in real-time, while a scheduled task executes at a configured timestamp.
In a nutshell :
- The Alfresco repository lets you inject behaviour into your content.
- Custom behaviours serve as method handlers for node events (policies), they can make the repository react to changes
- Behaviours can be bind to event (policy) for particular Class (Type, Aspect, Association)
- Policies can extend Alfresco beyond content models make extensions smarter by Encapsulating features and business logic.
At the end of this post i will provide deeper technical details on Policy and Behaviours, but now lets focus on our practical and usable example that you can actually use on your projects.
A practical example
Let’s get back to our business requirement that says specific mime-types (such as for example big video files and audio files) should be stored on a different (cheaper) disk than all the other contents.
Step 1 – The content store selector facade
Since we will have more than one content store, we will make use of the well know content store selector facade. Alfresco manages the storage of binaries through one or more content stores, each of which is associated with a single location on a file system accessible to Alfresco e.g. //data/alfresco_content. The Alfresco Content Store Selector allows for the direction of content to specific physical stores based upon the appropriate criteria (folder rules or policies).
Full documentation of the Content Store Selector can be found below with the appropriate configuration examples: http://docs.alfresco.com/4.2/concepts/store-manage-content.html
1.1 – Creating of a new content store
Let’s start by creating a new content store for the mediaFiles. Create a new directory under your <alf_data> directory, or mount a file system that you want to use to the media files. We will call this store mediaStore. Now we need to make alfresco aware of the new store by performing some configuration to alfresco.
<?xml version='1.0' encoding='UTF-8'?> <!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'> <beans> <bean id="mediaSharedFileContentStore" class="org.alfresco.repo.content.filestore.FileContentStore"> <constructor-arg> <value><PATH_TO_YOUR_MEDIA_STORE></value> </constructor-arg> </bean> <bean id="storeSelectorContentStore" parent="storeSelectorContentStoreBase"> <property name="defaultStoreName"> <value>default</value> </property> <property name="storesByName"> <map> <entry key="default"> <ref bean="fileContentStore" /> </entry> <entry key="mediastore"> <ref bean="mediaSharedFileContentStore" /> </entry> </map> </property> </bean> <!-- Point the ContentService to the 'selector' store --> <bean id="contentService" parent="baseContentService"> <property name="store"> <ref bean="storeSelectorContentStore" /> </property> </bean> <!-- Add the other stores to the list of stores for cleaning --> <bean id="eagerContentStoreCleaner" class="org.alfresco.repo.content.cleanup.EagerContentStoreCleaner" init-method="init"> <property name="eagerOrphanCleanup" > <value>${system.content.eagerOrphanCleanup}</value> </property> <property name="stores" > <list> <ref bean="fileContentStore" /> <ref bean="mediaSharedFileContentStore" /> </list> </property> <property name="listeners" > <ref bean="deletedContentBackupListeners" /> </property> </bean> </beans>
1.2 – Configuration of the cm:storeSelector aspect
Now we need to make alfresco share aware of the multiple content stores, to do that we need to configure the cm:storeSelector aspect.
In <tomcat_dir>/shared/classes/alfresco/web-extension rename the spring context file web-client-config-custom.xml.sample to web-client-config-custom.xml and configure the cm:storeSelector aspect as follows:
<!-- Configuring in the cm:storeSelector aspect --> <config evaluator="aspect-name" condition="cm:storeSelector"> <property-sheet> <show-property name="cm:storeName" component-generator="StoreSelectorGenerator" /> </property-sheet> </config> <config evaluator="string-compare" condition="Action Wizards"> <aspects> <aspect name="cm:storeSelector"/> </aspects> </config>
Next we need to merge the following xml snippet on our share-config-custom.xml file to make the content store selector aspect visible in share.
<!-- Configuring in the cm:storeSelector aspect --> <config evaluator="node-type" condition="cm:content"> <forms> <form> <field-visibility> <!-- aspect: cm:storeSelector --> <show id="cm:storeName" /> </field-visibility> <appearance> <!-- Store Selector --> <field id="cm:storeName" label="Store Name" description="Content Store Name" /> </appearance> </form> </forms> </config> <config evaluator="string-compare" condition="DocumentLibrary" replace="true"> <aspects> <!-- Aspects that a user can see --> <visible> <aspect name="cm:storeSelector" /> </visible> </aspects> </config>
1.3 – Some Simple ways of using the new content store
The new content store is set using the cm:storeName property. The cm:storeName property can be set in number of ways:
- Manually, by exposing this property so its value can be set by either Explorer or Share
- Running a script action that sets the cm:storeName property value within the script
- Using a rule that runs a script action to set the property
- Using a Behaviour that automates the choice of the store based on the mime-type of the content (we will see how during this post)
The default behaviour is as follows:
• When the cm:storeSelector aspect is not present or is removed, the content is copied to a new location in the ‘default’ store
• When the cm:storeSelector aspect is added or changed, the content is copied to the named store
• Under normal circumstances, a trail of content will be left in the stores, just as it would be if the content were being modified. The normal processes to clean up the orphaned content will be followed.
To automate the store classification we can write a simple script in JavaScript and call it action_mediastore.js. The script contents would be:
var props = new Array(1);
props[“cm:storeName”] = “mediastore”;
document.addAspect(“cm:storeSelector”, props);
document.save();
We would then save the script in Data Dictionary/Scripts. Note that the script above is adding the storeSelector aspect and assigning a value (in this case mediastore) to the property.
Now we can execute the action over any file or folder and we select “Execute Script”. We then select our script action_mediastore.js
Step 2 – Coding the new behaviour
We will build a custom content policy (Behaviour) that depending on the documents mime-type will apply the content-store-selector aspect to it and choose the appropriate contentStore.
Using the Alfresco sdk, start a new repository amp project (I will focus on how to use the alfresco sdk on a different post).
The first thing we need is to create a behavior binded to the OnContentUpdatePolicy. Note that metadata detection (and mime-type) is post commit, this is why we needs to use a OnContentUpdatePolicy as the method ContentServicePolicies.OnContentUpdatePolicy is fired *after* Tika detection of mimetype. We will be using onContentUpdate, when newContent = true for our use case
2.1 – Behaviour class implementing OnContentUpdatePolicy
Our class definition will be as follows :
public class SetStoreByMimeTypeBehaviour extends TransactionListenerAdapter implements ContentServicePolicies.OnContentUpdatePolicy {
Check that we are implementing one of the ContentService policies , in this case the OnContentUpdatePolicy
Next we define the properties that we will inject via Spring .
private PolicyComponent policyComponent; private ServiceRegistry serviceRegistry; private Map<String, String> mimeToStoreMap;
We need to provide our class with the correspondent getters and setters for the properties that will be injected.
public PolicyComponent getPolicyComponent() { return policyComponent; } public void setPolicyComponent(PolicyComponent policyComponent) { this.policyComponent = policyComponent; } public ServiceRegistry getServiceRegistry() { return serviceRegistry; } public void setServiceRegistry(ServiceRegistry serviceRegistry) { this.serviceRegistry = serviceRegistry; } public void setMimeToStoreTypeMap(Map<String, String> mimeToModelTypeMap) { this.mimeToStoreMap = mimeToModelTypeMap; } public Map<String, String> getMimeToStoreTypeMap() { return mimeToStoreMap; }
The init method is one of the methods that we need to override to implement the behavior. This method initiates the behavior and registers the class with the chosen policy. In this case we are doing it for all content nodes (ContentModel.TYPE_CONTENT=cm:content).
Note the important NotificationFrequency.FIRST_EVENT. Behaviours can be defined with a notification frequency – “every event” (default), “first event”, “transaction commit”. In this case, we want the behavior to fire only on first event. Consider that during a given transaction, certain policies may fire multiple times (ie. “every event”).
public void init() { if (log().isDebugEnabled()) { log().debug("Initializing Behavior"); } this.onContentUpdate = new JavaBehaviour(this, "onContentUpdate", NotificationFrequency.FIRST_EVENT); this.policyComponent.bindClassBehaviour(QNAME_ONCONTENTUPDATE, ContentModel.TYPE_CONTENT, onContentUpdate); }
Last, but not least we need to override the onContentUpdate method to implement our logic. We get the mimeType of the node and assign the content a specific store depending on that. Note that the store name is coming from the mapping implemented on the spring bean configuration (explained on the next section)
@Override public void onContentUpdate(NodeRef nodeRef, boolean newContent) { if (log().isDebugEnabled()) { log().debug("onContentUpdate, new[" + newContent + "]"); } NodeService nodeService = serviceRegistry.getNodeService(); ContentData contentData = (ContentData) nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT); String nodeMimeType = contentData.getMimetype(); log().debug("nodeMimeType is " + nodeMimeType); QName storeName = getQNameMap().get(nodeMimeType); if (storeName != null) { log().debug("storeName is " + storeName.toString()); String name = storeName.toString().substring(2,storeName.toString().length()); log().debug("Stripped storeName is " + name); // add the aspect Map storeSelectorProps = new HashMap(1, 1.0f); storeSelectorProps.put(QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,"storeName"), name); nodeService.addAspect(nodeRef, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "storeSelector"), storeSelectorProps); // Extract meta-data here because it doesn't happen automatically when imported through FTP (for example) ActionService actionService = serviceRegistry.getActionService(); Action extractMeta = actionService.createAction(ContentMetadataExtracter.EXECUTOR_NAME); actionService.executeAction(extractMeta, nodeRef); } else { log().debug("No specific store configured for mimetype [" + nodeMimeType + "]"); } }
The full source code for our class is the following:
package org.alfresco.consulting.behaviours.mimetype; import java.io.Serializable; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import org.alfresco.model.ContentModel; import org.alfresco.repo.action.executer.ContentMetadataExtracter; import org.alfresco.repo.content.ContentServicePolicies; import org.alfresco.repo.policy.Behaviour; import org.alfresco.repo.policy.Behaviour.NotificationFrequency; import org.alfresco.repo.policy.JavaBehaviour; import org.alfresco.repo.policy.PolicyComponent; import org.alfresco.repo.transaction.TransactionListenerAdapter; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.action.Action; import org.alfresco.service.cmr.action.ActionService; import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Behavior that casts the node type to the appropriate store * based on the applied mime type. * @author Luis Cabaceira * */ public class SetStoreByMimeTypeBehaviour extends TransactionListenerAdapter implements ContentServicePolicies.OnContentUpdatePolicy { private static final QName QNAME_ONCONTENTUPDATE = QName.createQName(NamespaceService.ALFRESCO_URI, "onContentUpdate"); private Behaviour onContentUpdate; private PolicyComponent policyComponent; private ServiceRegistry serviceRegistry; private Map<String, String> mimeToStoreMap; private Map<String, QName> qnameMap; public void init() { if (log().isDebugEnabled()) { log().debug("Initializing Behavior"); } this.onContentUpdate = new JavaBehaviour(this, "onContentUpdate", NotificationFrequency.FIRST_EVENT); this.policyComponent.bindClassBehaviour(QNAME_ONCONTENTUPDATE, ContentModel.TYPE_CONTENT, onContentUpdate); } /* * (non-Javadoc) * @see org.alfresco.repo.content.ContentServicePolicies.OnContentUpdatePolicy#onContentUpdate(org.alfresco.service.cmr.repository.NodeRef, boolean) */ @Override public void onContentUpdate(NodeRef nodeRef, boolean newContent) { if (log().isDebugEnabled()) { log().debug("onContentUpdate, new[" + newContent + "]"); } NodeService nodeService = serviceRegistry.getNodeService(); ContentData contentData = (ContentData) nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT); String nodeMimeType = contentData.getMimetype(); log().debug("nodeMimeType is " + nodeMimeType); QName storeName = getQNameMap().get(nodeMimeType); if (storeName != null) { log().debug("storeName is " + storeName.toString()); String name = storeName.toString().substring(2,storeName.toString().length()); log().debug("Stripped storeName is " + name); // add the aspect Map storeSelectorProps = new HashMap(1, 1.0f); storeSelectorProps.put(QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,"storeName"), name); nodeService.addAspect(nodeRef, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "storeSelector"), storeSelectorProps); // Extract meta-data here because it doesn't happen automatically when imported through FTP (for example) ActionService actionService = serviceRegistry.getActionService(); Action extractMeta = actionService.createAction(ContentMetadataExtracter.EXECUTOR_NAME); actionService.executeAction(extractMeta, nodeRef); } else { log().debug("No specific store configured for mimetype [" + nodeMimeType + "]"); } } /** * * @return */ private Map<String, QName> getQNameMap() { if (qnameMap == null) { qnameMap = new HashMap<String, QName>(); // Pre-resolve QNames... for (Entry<String,String> e : mimeToStoreMap.entrySet()) { QName qname = this.qnameFromMimetype(e.getKey()); if (qname != null) { qnameMap.put(e.getKey(), qname); } } } return qnameMap; } /** * * @param mimeType * @return */ private QName qnameFromMimetype(String mimeType) { QName qname = null; String qNameStr = mimeToStoreMap.get(mimeType); qname = QName.createQName(qNameStr, serviceRegistry.getNamespaceService()); return qname; } public PolicyComponent getPolicyComponent() { return policyComponent; } public void setPolicyComponent(PolicyComponent policyComponent) { this.policyComponent = policyComponent; } public ServiceRegistry getServiceRegistry() { return serviceRegistry; } public void setServiceRegistry(ServiceRegistry serviceRegistry) { this.serviceRegistry = serviceRegistry; } public void setMimeToStoreTypeMap(Map<String, String> mimeToModelTypeMap) { this.mimeToStoreMap = mimeToModelTypeMap; } public Map<String, String> getMimeToStoreTypeMap() { return mimeToStoreMap; } protected Log log() { return LogFactory.getLog(this.getClass()); } }
2.2 – Registering the behaviour with Spring
Next step is to register our behavior with Spring, for that we will need a context file (service-context.xml) that will register our bean and that has the mapping between the mimetype of the content and the correspondent store. Note the mapping of the several video formats to the new mediaStore.
<?xml version='1.0' encoding='UTF-8'?> <!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'> <beans> <!-- --> <!-- SetStoreByMimeTypeBehaviour --> <!-- --> <bean id="mime-based-store-selector-behavior" class="org.alfresco.consulting.behaviours.mimetype.SetStoreByMimeTypeBehaviour" init-method="init" depends-on="dictionaryBootstrap"> <property name="policyComponent" ref="policyComponent" /> <property name="serviceRegistry" ref="ServiceRegistry" /> <!-- stores to mimetype map --> <property name="mimeToStoreTypeMap"> <map> <entry key="video/mpeg"><value>mediaStore</value></entry> <entry key="audio/mpeg"><value>mediaStore</value></entry> <entry key="audio/mp4"><value>mediaStore</value></entry> <entry key="video/mp4"><value>mediaStore</value></entry> <entry key="video/x-m4v"><value>mediaStore</value></entry> <entry key="video/mpeg2"><value>mediaStore</value></entry> <entry key="video/mp2t"><value>mediaStore</value></entry> <entry key="video/quicktime"><value>mediaStore</value></entry> <entry key="video/3gpp"><value>mediaStore</value></entry> <entry key="video/3gpp2"><value>mediaStore</value></entry> <entry key="video/x-sgi-movie"><value>mediaStore</value></entry> </map> </property> </bean> </beans>
2.3 – Deploy and Test
Package your class and your context file into an amp file (using the alfresco sdk) and deploy it to your repository with the apply-amps.sh (alfresco-mmt.jar).
You can now test to upload video or audio files and verify that those are being stored on your new mediaStore.
Summary (Technical deep dive) on Policies and Behaviours
We’ve seen that Policies provide hook points to which we can bind behaviours to events based on class or association
Behaviours are (policy) handlers that execute specific business logic, they can be implemented in Java and/or JavaScript. Behaviours can be bound to a type or aspect
org.alfresco.repo.policy.JavaBehaviour
JavaBehaviour(Object instance, String method, NotificationFrequency frequency)
org.alfresco.repo.jscript.ScriptBehaviour
ScriptBehaviour(ServiceRegistry serviceRegistry, ScriptLocation location, NotificationFrequency frequency)
- ClassPolicy (type or aspect)
- AssociationPolicy (peer or parent-child)
- PropertyPolicy (not used)
There are Different Types Of Bindings for Behaviors
Service – called every time
Class – most common, bound to type or aspect
Association – bound to association, useful for adding smarts to custom hierarchies
Properties – bound to property, too granular
Let’s take a look at a register and invoke pattern for Behaviour components
public interface NodeServicePolicies { public interface OnAddAspectPolicy extends ClassPolicy { public static final QName QNAME = QName.createQName(NamespaceService.ALFRESCO_URI, "onAddAspect"); // Called after an <b>aspect</b> has been added to a node public void onAddAspect(NodeRef nodeRef, QName aspectTypeQName); } } public abstract class AbstractNodeServiceImpl implements NodeService { // note: policyComponent is injected … (not shown here) public void init() { // Register the policy onAddAspectDelegate = policyComponent.registerClassPolicy (NodeServicePolicies.OnAddAspectPolicy.class); } protected void invokeOnAddAspect(NodeRef nodeRef, QName aspectTypeQName) { NodeServicePolicies.OnAddAspectPolicy policy = onAddAspectDelegate.get(nodeRef, aspectTypeQName); policy.onAddAspect(nodeRef, aspectTypeQName); } }
Build and Implement pattern for Behaviour components
public class XyzAspect implements NodeServicePolicies.OnAddAspectPolicy, ... { // note: policyComponent is injected … (not shown here) public void init() { // bind to the policy policyComponent.bindClassBehaviour( OnAddAspectPolicy.QNAME, ContentModel.ASPECT_XYZ, new JavaBehaviour(this, "onAddAspect”, Behaviour.NotificationFrequency.TRANSACTION_COMMIT)); } public void onAddAspect(NodeRef nodeRef, QName aspectTypeQName) { // implement behaviour here … (for when aspect XYZ is added) } }
Conclusion
Alfresco Behaviours and Policies are very powerfull features that can make your extensions very smart. I hope you enjoyed this post and that you can make usage of its contents.
Stay tuned for more posts with my field experiences,
One Love,
Luis