Alfresco · Listener genérico para tareas de Activiti

En ocasiones los mecanismos de extensión de Alfresco no cubren las necesidades de una petición concreta. No obstante, siempre resulta aconsejable implementar nuevas funcionalidades de una manera desacoplada respecto al código de Alfresco para evitar problemas en las actualizaciones del producto.

En el caso de los flujos de trabajo Activi, la inclusión de listeners adicionales está restringida a la inicialización del bean AlfrescoProcessEngineConfiguration en el fichero alfresco/WEB-INF/classes/alfresco/activiti-context.xml, por lo que no es posible extenderlo por los mecanismos habituales.

A continuación se muestra la manera de inyectar listeners a este objeto sin modificar el código base de Alfresco.

Configuración de Spring

Se declara en el fichero de contexto de Spring un nuevo bean de tipo BeanFactoryPostProcessor, para poder modificar los atributos del bean base de Alfresco una vez éste haya sido cargado en la inicialización de Spring.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'>

<beans>

	<!-- Define Custom Task Listener -->
    <bean id="customUserTaskListener" class="es.keensoft.repo.workflow.activiti.CustomUserTaskListener" />

	<!-- Add custom listener to 'activitiProcessEngineConfiguration.customPreBPMNParseListeners' list -->
	<bean id="keensoftCustomUserTaskListener" class="es.keensoft.repo.workflow.activiti.CustomUserTaskListenerInit">
		<!-- bean id for AlfrescoProcessEngineConfiguration on 'alfresco/WEB-INF/classes/alfresco/activiti-context.xml' file -->
		<property name="beanName" value="activitiProcessEngineConfiguration" />
		<!-- property name for listeners list on AlfrescoProcessEngineConfiguration bean -->
		<property name="propertyName" value="customPreBPMNParseListeners" />
		<property name="customUserTaskListener" ref="customUserTaskListener"/>
        <property name="propsLocation" value="customTaskListener.properties"/>
	</bean>

</beans>

Bean de inicialización
Se implementa el inyector de listeners de Spring, que carga el nombre de estos listeners de un fichero de propiedades.

package es.keensoft.repo.workflow.activiti;

import java.io.InputStream;
import java.util.List;
import java.util.Properties;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class CustomUserTaskListenerInit implements BeanFactoryPostProcessor, ApplicationContextAware {

    private static ApplicationContext ctx = null;

    public static ApplicationContext getApplicationContext() {
        return ctx;
    }
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
         ctx = applicationContext;
    }

	private static final Log logger = LogFactory.getLog(CustomUserTaskListenerInit.class);

	private String beanName;
	private String propertyName;
	private CustomUserTaskListener customUserTaskListener;
	private String propsLocation;

	private static Properties conf;

	public String getBeanName() {
		return beanName;
	}
	public void setBeanName(String beanName) {
		this.beanName = beanName;
	}
	public String getPropertyName() {
		return propertyName;
	}
	public void setPropertyName(String propertyName) {
		this.propertyName = propertyName;
	}
	public CustomUserTaskListener getCustomUserTaskListener() {
		return customUserTaskListener;
	}
	public void setCustomUserTaskListener(
			CustomUserTaskListener customUserTaskListener) {
		this.customUserTaskListener = customUserTaskListener;
	}

	public String getPropsLocation() {
		return propsLocation;
	}
	public void setPropsLocation(String propsLocation) {
		this.propsLocation = propsLocation;
	}
	public static Properties getConf() {
		return conf;
	}
	public static void setConf(Properties conf) {
		CustomUserTaskListenerInit.conf = conf;
	}

	@Override
	@SuppressWarnings("unchecked")
	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    	// Add my custom listener
		String[] beans = beanFactory.getBeanDefinitionNames();
        for (String bName : beans) {
            if (bName.equals(beanName)) {
            	BeanDefinition def = beanFactory.getBeanDefinition(beanName);
				List<Object> mapped = (List<Object>)  def.getPropertyValues().getPropertyValue(propertyName).getValue();
            	mapped.add(customUserTaskListener);
            }
        }

        // Load properties bundle
        conf = getPropertiesFromClasspath(getPropsLocation());
	}

	private Properties getPropertiesFromClasspath(String propFileName) {
	    Properties props = new Properties();
	    InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(propFileName);
	    try {
	        props.load(inputStream);
	    } catch (Exception e) {
	    	logger.error("property file '" + propFileName+ "' not found in the classpath");
	    }
	    return props;
	}

}

Implementación de los listeners
Se implementan los listeners requeridos. Se incluye a continuación un ejemplo de un TaskListener para la creación de la tarea que invoca a un servicio web externo para informarle de los parámetros de creación de la misma.

package es.keensoft.repo.workflow.activiti;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.activiti.engine.delegate.DelegateTask;
import org.activiti.engine.delegate.TaskListener;
import org.alfresco.repo.site.SiteModel;
import org.alfresco.repo.workflow.activiti.ActivitiScriptNode;
import org.alfresco.repo.workflow.activiti.script.ActivitiScriptBase;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.site.SiteInfo;
import org.alfresco.service.namespace.QName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import es.keensoft.bne.ws.client.WSTareasExternaSoap12Endpoint_Client;

public class CustomTaskCreateListener extends ActivitiScriptBase implements TaskListener {

	private static final Log logger = LogFactory.getLog(CustomTaskCreateListener.class);
	private static final String USER_INITIATOR_ID = "userName";
	private static final SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");

	@Override
	public void notify(DelegateTask task) {

    	// Recover initiator user
		String idInitiator = null;
    	if (task.getVariable("initiator") != null) {
    		ActivitiScriptNode initiator = (ActivitiScriptNode)task.getVariable("initiator");
    		for (String key : initiator.getProperties().keySet()) {
    			if (key.indexOf(USER_INITIATOR_ID) != -1) {
    				idInitiator = (String) initiator.getProperties().get(key);
    				break;

    			}
    		}
    	}

    	// Recover Site Name
		String siteName = null;
		ActivitiScriptNode scriptNode = (ActivitiScriptNode)task.getVariable("bpm_package");
		NodeRef packageNodeRef = scriptNode.getNodeRef();
		List childRefList = getServiceRegistry().getNodeService().getChildAssocs(packageNodeRef);

		// Not interested in all the documents, just take the first one to search for Site Name
		if (childRefList.size() > 0) {

		    NodeRef nodeRef = childRefList.get(0).getChildRef();

	        // Work out what site it's in, and what container in the site
	        SiteInfo site = null;
	        NodeRef current = nodeRef;
	        while (current != null) {
	        	QName type = getServiceRegistry().getNodeService().getType(current);
	        	if (getServiceRegistry().getDictionaryService().isSubClass(type, SiteModel.TYPE_SITE)) {
	        		site = getServiceRegistry().getSiteService().getSite(current);
	        		break;
	        	}
	        	current = getServiceRegistry().getNodeService().getPrimaryParent(current).getParentRef();
	        }

	        // Site info
			siteName = site.getTitle();
		}

    	// Invoke ws asynchronously
    	ExecutorService executorService = Executors.newSingleThreadExecutor();
    	executorService.execute(new WSInvocatorTask(task, idInitiator, siteName));
    	executorService.shutdown();

	}

	private class WSInvocatorTask implements Runnable {

		private static final String URL_PATH_TO_TASK = "/task-edit?taskId=activiti$";
		private DelegateTask task;
		private String idInitiator;
		private String siteName;

		public WSInvocatorTask(DelegateTask task, String idInitiator, String siteName) {
			this.task = task;
			this.idInitiator = idInitiator;
			this.siteName = siteName;
		}

		@Override
		public void run() {

	    	String idWorkflowExterno = CustomUserTaskListenerInit.getConf().getProperty("intranet.ws.proceso.externo.id");
	    	String fechatarea = sdf.format(task.getCreateTime());
	    	ArrayList vUsuario = new ArrayList();
	    	vUsuario.add(task.getAssignee());
	    	String urlAcceso = CustomUserTaskListenerInit.getConf().getProperty("alfresco.share.server.url") + URL_PATH_TO_TASK + task.getId();
	    	String idProcesoExterno = task.getId();

	    	// If no document is attached to the task, Site can't be reached
	    	String sTexto = (siteName == null ? "" : siteName + ": ") + task.getDescription();

            /**
               WS Invocation
               ...
            */
	}

}

Conclusiones
Existen técnicas adecuadas para realizar nuevas funcionalidades sobre Alfresco sin afectar al código base de la plataforma, aunque en muchas ocasiones es necesario conocer en profundidad la configuración de los diferentes módulos para poder aplicarlas.

Nota
El código incluido ha sido comprobado en Alfresco 4.2.c

Un comentario en “Alfresco · Listener genérico para tareas de Activiti

  1. Pingback: Alfresco · Listener genérico para tareas de Activiti (II) « Programming and So

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s