Wednesday, August 3, 2011

Using a Template in the Classpath for an Email on Alfresco 3.4

When trying to use a template on the classpath with the MailActionExecutor but without much success.  I was under the impression that all one had to do was provide the relative path to the ftl file via the PARAM_TEMPLATE parameter (can't remember what I read that made me think this... it also doesn't help that there is not a whole lot of Java examples/documentation other than the skimpy javadocs).  I tried this, and was met with the following exception:

org.springframework.mail.MailPreparationException: Could not prepare mail; nested exception is java.lang.ClassCastException: java.lang.String cannot be cast to org.alfresco.service.cmr.repository.NodeRef

A quick look at the source code revealed why:

...
// See if an email template has been specified
String text = null;
NodeRef templateRef = (NodeRef)ruleAction.getParameterValue(PARAM_TEMPLATE);
if (templateRef != null)
...

It doesn't even attempt to differentiate between a template in the repository vs one in the classpath.  It automatically assumes it is in the repo (hence the cast to a NodeRef before anything else is done).  To get around this, I had to manually process the template to generate the text to be used in the email.

To do this, a model needs to be generated for the template to use.  I used the code that generates the model in the MailActionExecutor.java minus a few objects I deemed unnecessary for my use case.  Then I passed in the path to the ftl and the model to the template service and it generated the output string.  I then set it to the PARAM_TEXT parameter for the email.

Here is the code:
private static final String EXPIRE_EMAIL_TEMPLATE = "alfresco/extension/burris/common/templates/expiration-email-template.ftl";
...
// Process the template - the mail action executor expects it to be in the repository which is why this has to be done
Map model = new HashMap( 8, 1.0f );
NodeRef person = personService.getPerson( authenticationService.getCurrentUserName() );
model.put( "person", new TemplateNode( person, serviceRegistry, null ) );
model.put( "document", new TemplateNode( nodeRef, serviceRegistry, null ) );
NodeRef parent = nodeService.getPrimaryParent( nodeRef ).getParentRef();
model.put( "space", new TemplateNode( parent, serviceRegistry, null ) );
model.put( "message", new I18NMessageMethod() );

String text = templateService.processTemplate( "freemarker", EXPIRE_EMAIL_TEMPLATE, model );

// Send email notice detailing the state change
Action emailAction = actionService.createAction( MailActionExecuter.NAME );

emailAction.setParameterValue( MailActionExecuter.PARAM_SUBJECT, subject.toString() );
emailAction.setParameterValue( MailActionExecuter.PARAM_TO_MANY, emailList );
emailAction.setParameterValue( MailActionExecuter.PARAM_TEXT, text );

if ( log.isDebugEnabled() ) log.debug( "Sending email..." );
actionService.executeAction( emailAction, nodeRef );
...

Monday, August 1, 2011

Send an Email to an Entire Alfresco Share Site

I had the need to send the users of an Alfresco Share site an email; however, doing so is not readily apparent.  It is pretty easy once you know the convention:

GROUP_site_siteId


This differs from the Alfresco Explorer groups which follow the pattern:

GROUP_groupId

Here is an example of what a site group identifier should look like if the site id is transportation:

GROUP_site_transportation

If you're working in the javascript world or just want to hardcode things together, you can construct the group identifier using the above as a template.  However, in the java world, Alfresco provides us with the SiteService. If you know the short name of your site, you can retrieve the string identifier for the site group. In my case, I retrieved the short name of the site from the nodeRef passed into a behavior.

Here is the code I used to do this:

// To
 SiteInfo siteInfo = siteService.getSite( nodeRef );
 String siteShortName = siteInfo.getShortName();
 String siteGroup = siteService.getSiteGroup( siteShortName );
   
 // Send email notice detailing the state change
 Action emailAction = actionService.createAction( MailActionExecuter.NAME );
 
 emailAction.setParameterValue( MailActionExecuter.PARAM_SUBJECT, subject );
 emailAction.setParameterValue( MailActionExecuter.PARAM_TO_MANY, siteGroup );
 emailAction.setParameterValue( MailActionExecuter.PARAM_TEXT, "Hello world." );
 
 actionService.executeAction( emailAction, nodeRef );

Friday, June 10, 2011

Alfresco and a Custom TaskControllerHandler

My work has required me to create an advance workflow in Alfresco to facilitate the process of how the users insert documents and apply the corresponding metadata.  The outline of what needed to be done is this:
  1. Upload a document
  2. Start a new workflow for that document
  3. Require the user to select a document type from a list of subtypes of the current document type
Under the covers, Alfresco uses jBPM.  It actually has been integrated quite nicely, but there are certain features that are not readily available after their marriage.  It does not provide a way to display a list of options out of the box, unless it is static.

There are hacks that allow one to do this with a ListConstraint, but a hack isn't always the best way to go (think fitting a square peg in a round hole).  A ListConstraint almost fits the need here for selecting a document type, except its missing one crucial element: access to the node in the workflow (or any node for that matter).  In order to get a list of subtypes, I need to check the node's current type.  There are ways around this, but they aren't exactly best practice.  So this option was a non starter.

I looked into using actions before and after the task to populate the list and change the document type, but this seemed to be a bunch of extra work and also a bit of a kludge.

Another option was to use BeanShell script or Javascript directly in the workflow.  I can't say I'm a big fan of putting code directly in the xml of the workflow, nor am I a fan of server side Javascript (too hard to debug).

Some searching on jBPM revealed that it is possible to map properties to a task from the process context (or whatever source you want) via a TaskControllerHandler.  The task controller excels at allowing the programmer provide data to the task that is not a 1 to 1 mapping of what is in the process context (which is why configuring the default task controller in the workflow was out of the question as it only does 1 to 1).  This seemed to be the option to go with, however, there are several pain points I came across implementing this.  The rest of this blog post will address those points.

The process I outlined of what the controller needed to do:
  • Retrieve the document node and determine its child types
  • Present that to the user via the task instance
  • User is able to select one of the options
  • Upon submission, the document node is changed to the new document subtype
First thing I needed to do was obtain references to the Alfresco services.  At first I wasn't sure how to do this, since the TaskControllerHandler is managed by jBPM and not Spring so dependency injection was out.  I've done some work using workflow actions in Alfresco, which has access to the services via a BeanFactory.  So to the source code I went and figured out how they set that up (javadoc and source for 
JBPMSpringActionHandler).  With this knowledge, I created the following abstract class to extend all my Alfresco task controllers from:

package com.burris.common.bpm.taskcontrollers;

import org.jbpm.taskmgmt.def.TaskControllerHandler;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.access.BeanFactoryLocator;
import org.springframework.beans.factory.access.BeanFactoryReference;
import org.springmodules.workflow.jbpm31.JbpmFactoryLocator;

@SuppressWarnings("serial")
public abstract class AlfrescoTaskControllerHandler implements TaskControllerHandler {

 public AlfrescoTaskControllerHandler() {
  
  BeanFactoryLocator factoryLocator = new JbpmFactoryLocator();
  BeanFactoryReference factoryReference = factoryLocator.useBeanFactory( null );
  BeanFactory factory = factoryReference.getFactory();
  initializeHandler( factory );
 }
 
 protected abstract void initializeHandler( BeanFactory factory );
}

The initializeHandler function is intended to grab and store references to the services required. Now I am ready to create my actual task handler, called DocumentSubtypeTaskController:

package com.burris.common.bpm.taskcontrollers;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.alfresco.repo.workflow.jbpm.JBPMNode;
import org.alfresco.service.cmr.dictionary.ClassDefinition;
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.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.apache.log4j.Logger;
import org.jbpm.context.exe.ContextInstance;
import org.jbpm.graph.exe.Token;
import org.jbpm.taskmgmt.exe.TaskInstance;
import org.springframework.beans.factory.BeanFactory;

@SuppressWarnings( "serial" )
public class DocumentSubtypeTaskController extends AlfrescoTaskControllerHandler {

 public DocumentSubtypeTaskController() {}
 
 @Override
 protected void initializeHandler( BeanFactory factory ) {
  // TODO Auto-generated method stub
  
 }
 
 @Override
 public void initializeTaskVariables( TaskInstance taskInstance, ContextInstance contextInstance, Token token ) {
  // TODO Auto-generated method stub
  
 }
 
 @Override
 public void submitTaskVariables( TaskInstance taskInstance, ContextInstance contextInstance, Token token ) {
  // TODO Auto-generated method stub
  
 }
}

I first implemented my initializeHandler method and added the services to the class:

...
 
 private NodeService nodeService;
 private DictionaryService dictionaryService;
 private NamespaceService namespaceService;

 ...

 @Override
 protected void initializeHandler( BeanFactory factory ) {
  
  this.nodeService = (NodeService) factory.getBean( "nodeService" );
  this.dictionaryService = (DictionaryService) factory.getBean( "dictionaryService" );
  this.namespaceService = (NamespaceService) factory.getBean( "namespaceService" );
 }

Next I implemented the two methods from the TaskControllerHandler interface: initializeTaskVariables and submitTaskVariables. The purpose of these methods is to setup the variables of the task and to retrieve their values to process them, respectively. The values can come from just about anywhere, but usually from the process context. In this case, I need to retrieve the type of the document node in the bpm_package, which is in the the process context.

...
 
 private static final String DOCUMENT_TYPE = "blndwf_documentType";
 
 ...
 
 @Override
 public void initializeTaskVariables( TaskInstance taskInstance, ContextInstance contextInstance, Token token ) {
  
  // Get the document from the workflow package
  Object object = contextInstance.getVariable( "bpm_package" );
  if ( object == null ) return;
  
  NodeRef bpmPackageNodeRef = ((JBPMNode) object).getNodeRef();
  if ( bpmPackageNodeRef == null ) return;
  
  List children = nodeService.getChildAssocs( bpmPackageNodeRef );
  ChildAssociationRef childNodeRef = children.get( 0 );
  if ( childNodeRef == null ) return;
  
  NodeRef documentNodeRef = childNodeRef.getChildRef();
  if ( documentNodeRef == null ) return;
  
  // Get the document's subtypes 
  QName documentQName = nodeService.getType( documentNodeRef );
  Collection subTypes = dictionaryService.getSubTypes( documentQName, false );
  
  List options = new ArrayList();
  for ( QName type : subTypes ) {
   
   ClassDefinition classDef = dictionaryService.getClass( type );
   options.add( type.getPrefixString() + "|" + classDef.getTitle() );
  }
  taskInstance.setVariable( DOCUMENT_TYPE, options );

A couple things you need to note here. The constant DOCUMENT_TYPE value is using an underscore (_). The actual property (which is coming from the workflow task model definition) looks like this: blndwf:documentType. The reason for this is that jBPM does not allow colons (:) in the variable names. I'm not sure which piece does this, but using the underscore automatically maps it to the correct property in the task model.

The other thing to note is the for loop. I setup a list of strings containing the type string and and the title separated by the pipe symbol (|). At the end of the day this list is converted to a string before it gets to the UI. I have created a custom form control that will split these strings (splits on "," and then "|") to use in a select box. The type gets mapped to the value of an option while the title is what is actually displayed. There was no other easy way to do this without string manipulation in the UI.

Lastly, I completed the submitTaskVariables method:

@Override
 public void submitTaskVariables( TaskInstance taskInstance, ContextInstance contextInstance, Token token ) {
  
  // Get the document from the workflow package
  Object object = contextInstance.getVariable( "bpm_package" );
  if ( object == null ) return;
  
  NodeRef bpmPackageNodeRef = ((JBPMNode) object).getNodeRef();
  if ( bpmPackageNodeRef == null ) return;
  
  List children = nodeService.getChildAssocs( bpmPackageNodeRef );
  ChildAssociationRef childNodeRef = children.get( 0 );
  if ( childNodeRef == null ) return;
  
  NodeRef documentNodeRef = childNodeRef.getChildRef();
  if ( documentNodeRef == null ) return;
  
  String selectedType = (String) taskInstance.getVariable( DOCUMENT_TYPE );
  if ( log.isDebugEnabled() ) log.debug( selectedType );
  
  // TODO: Validate user input
  
  // Set the document type if valid
  QName newType = QName.createQName( selectedType, namespaceService );
  nodeService.setType( documentNodeRef, newType );
  
  // TODO: Signal only when data is valid
  token.signal();
 }

After this is done processing the task variables (often just get mapped to the context instance, except not in this case - I actually change the document type) token.signal() needs to be called. This tells jBPM that the task controller is done and the workflow is ready to transition to the next task in the workflow.

That's it for the Java code! This can be jar'd up and put into the lib directory (alfresco.war/WEB-INF/lib). Here is the final class in it's entirety:

package com.burris.common.bpm.taskcontrollers;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.alfresco.repo.workflow.jbpm.JBPMNode;
import org.alfresco.service.cmr.dictionary.ClassDefinition;
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.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.apache.log4j.Logger;
import org.jbpm.context.exe.ContextInstance;
import org.jbpm.graph.exe.Token;
import org.jbpm.taskmgmt.exe.TaskInstance;
import org.springframework.beans.factory.BeanFactory;

@SuppressWarnings( "serial" )
public class DocumentSubtypeTaskController extends AlfrescoTaskControllerHandler {

 private static final String DOCUMENT_TYPE = "blndwf_documentType";
 
 private NodeService nodeService;
 private DictionaryService dictionaryService;
 private NamespaceService namespaceService;
 
 public DocumentSubtypeTaskController() {}
 
 @Override
 protected void initializeHandler( BeanFactory factory ) {
  
  this.nodeService = (NodeService) factory.getBean( "nodeService" );
  this.dictionaryService = (DictionaryService) factory.getBean( "dictionaryService" );
  this.namespaceService = (NamespaceService) factory.getBean( "namespaceService" );
 }
 
 @Override
 public void initializeTaskVariables( TaskInstance taskInstance, ContextInstance contextInstance, Token token ) {
  
  // Get the document from the workflow package
  Object object = contextInstance.getVariable( "bpm_package" );
  if ( object == null ) return;
  
  NodeRef bpmPackageNodeRef = ((JBPMNode) object).getNodeRef();
  if ( bpmPackageNodeRef == null ) return;
  
  List children = nodeService.getChildAssocs( bpmPackageNodeRef );
  ChildAssociationRef childNodeRef = children.get( 0 );
  if ( childNodeRef == null ) return;
  
  NodeRef documentNodeRef = childNodeRef.getChildRef();
  if ( documentNodeRef == null ) return;
  
  // Get the document's subtypes 
  QName documentQName = nodeService.getType( documentNodeRef );
  Collection subTypes = dictionaryService.getSubTypes( documentQName, false );
  
  List options = new ArrayList();
  for ( QName type : subTypes ) {
   
   ClassDefinition classDef = dictionaryService.getClass( type );
   options.add( type.getPrefixString() + "|" + classDef.getTitle() );
  }
  taskInstance.setVariable( DOCUMENT_TYPE, options );
 }

 @Override
 public void submitTaskVariables( TaskInstance taskInstance, ContextInstance contextInstance, Token token ) {
  
  // Get the document from the workflow package
  Object object = contextInstance.getVariable( "bpm_package" );
  if ( object == null ) return;
  }
  
  NodeRef bpmPackageNodeRef = ((JBPMNode) object).getNodeRef();
  if ( bpmPackageNodeRef == null ) {
   
   log.fatal( "nodeRef is null." );
   return;
  }
  
  List children = nodeService.getChildAssocs( bpmPackageNodeRef );
  ChildAssociationRef childNodeRef = children.get( 0 );
  if ( childNodeRef == null ) return;
  
  NodeRef documentNodeRef = childNodeRef.getChildRef();
  if ( documentNodeRef == null ) return;
  
  String selectedType = (String) taskInstance.getVariable( DOCUMENT_TYPE );
  if ( log.isDebugEnabled() ) log.debug( selectedType );
  
  // Validate user input
  
  // Set the document type if valid
  QName newType = QName.createQName( selectedType, namespaceService );
  nodeService.setType( documentNodeRef, newType );
  
  // Signal only when data is valid?
  token.signal();
 }
}

Wednesday, May 25, 2011

Design Patterns

All programmers need to be aware of common design patterns.  What leads me to this all inclusive conclusion?  Because using design patterns buys you many things when writing code and will help to prevent it from showing up on sites like this.

It is so easy when starting a new project to spend little to no time on the design phase and go straight into coding.  This is the fast track to sending your code to the fan.  Take your time and choose your design carefully.  You'll thank yourself later.

What is a design pattern?
Design patterns help us to write code that is reusable, extensible, and maintainable.  It helps programmers to think about code in terms of objects.  While design patterns are not code, they are general solutions that can be applied in many cases.  They can help to save time by giving an approach to common problems.

Reusability
Thinking in terms of a designing a car, it is determined that it needs a wheel.  Therefor a wheel is invented and attached the car.  But low and behold it is decided that a car needs more than just one wheel, so do we reinvent the wheel each time we need one? No, because that would be a waste of time.  We only need to design the wheel once, and create 4 instances of it to put on the car.  But it is important to note that the wheel doesn't, and shouldn't, do everything.  It serves it purpose (in terms of transportation) to facilitate movement on the ground.  But water, air, or space travel do not always make use of wheels for movement.  While it may be possible in some cases to shoe-horn a wheel into a vehicle's design to make it traverse these other mediums, it is not the most efficient, practical, time saving, or easy thing to do.

Extensibility
There is only one constant in any project, and that is change.  Requirements change, members come and go, technical roadblocks can come out of nowhere, etc.  Design patterns may help your code to be extensible so when the customer asks, "Can it do this?" it will be easier to add in that extra functionality. It is good to think about extensibility when designing, but it can be easy to fall into the trap where you try to make everything and its brother extensible.  When working on this aspect of the design, you need to stop and ask, does this need to be extendible or how easy would be to make it extendible down the road?  Otherwise you may end up creating an entire computer just to use it as a simple calculator.

Maintainability
Last but not least (by far).  Ever inherited a project full of spaghetti code or written code that you have no idea what it does a week later?  I have on occasion, and let me tell you, it is a nightmare.  Using established design patterns makes it easier to fix bugs, add functionality, or take functionality out.  They provide inherent organization and make code easier to understand and follow.

Good Resources

Friday, January 7, 2011

Google Docs API: Retrieving Contents of Root Folder

I have been working on a program that interacts with Google Docs, and the need came up to display the contents of the root folder. After some searching around, I found these queries that do what I want them to do (sort of):

Contents of root:
http://docs.google.com/feeds/default/private/full/folder%3Aroot/contents/

Only folders in root:
http://docs.google.com/feeds/default/private/full/folder%3Aroot/contents/-/folder

Everything but folders:
http://docs.google.com/feeds/default/private/full/folder%3Aroot/contents/-/-folder

The most useful one for me in my case was the first one. However, it did not work as I initially thought. I executed the query and I found items in the feed that were in folders. So after a bit of head scratching and searching I found the answer: both the root folder and the other misc folder(s) had an edit link to the file, so naturally it would show up in both feeds. In order to remove these from the root folder, you have to delete the edit link to it in that location.