All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nz.co.senanque.workflow.WorkflowManagerImpl Maven / Gradle / Ivy

There is a newer version: 2.4.0
Show newest version
/*******************************************************************************
 * Copyright (c)2014 Prometheus Consulting
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
package nz.co.senanque.workflow;

import java.lang.reflect.Method;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.concurrent.locks.Lock;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import nz.co.senanque.forms.FormFactory;
import nz.co.senanque.forms.WorkflowForm;
import nz.co.senanque.locking.LockAction;
import nz.co.senanque.locking.LockFactory;
import nz.co.senanque.locking.LockTemplate;
import nz.co.senanque.logging.HashIdLogger;
import nz.co.senanque.messaging.MessageMapper;
import nz.co.senanque.parser.InputStreamParserSource;
import nz.co.senanque.parser.ParserSource;
import nz.co.senanque.process.instances.ProcessDefinition;
import nz.co.senanque.process.instances.TaskBase;
import nz.co.senanque.process.instances.TaskForm;
import nz.co.senanque.process.instances.TaskIf;
import nz.co.senanque.process.instances.TaskTry;
import nz.co.senanque.process.parser.ParsePackage;
import nz.co.senanque.process.parser.ProcessTextProvider;
import nz.co.senanque.schemaparser.FieldDescriptor;
import nz.co.senanque.schemaparser.SchemaParser;
import nz.co.senanque.validationengine.ValidationEngine;
import nz.co.senanque.workflow.instances.Audit;
import nz.co.senanque.workflow.instances.DeferredEvent;
import nz.co.senanque.workflow.instances.EventType;
import nz.co.senanque.workflow.instances.ProcessInstance;
import nz.co.senanque.workflow.instances.TaskStatus;

import org.jdom.Document;
import org.jdom.input.SAXBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.monitor.IntegrationMBeanExporter;
import org.springframework.messaging.Message;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

/**
 * This implements the non-database functionality of the workflow manager.
 * Injected beans handle the database activity
 * 
 * @author Roger Parkinson
 *
 */
public class WorkflowManagerImpl extends WorkflowManagerAbstract {

	private static final Logger log = LoggerFactory
			.getLogger(WorkflowManagerImpl.class);

	@Autowired(required=false)
    private transient IntegrationMBeanExporter m_integrationMBeanExporter;
	
	@Autowired(required=false)
	private Executor m_executor;
	@Autowired
	private WorkflowDAO m_workflowDAO;
	@Autowired
	private ContextDAO m_contextDAO;
	@Autowired
	private LockFactory m_lockFactory;
    private transient ValidationEngine m_validationEngine;
	@Autowired 
	private FormFactory m_formFactory;
	
	public WorkflowManagerImpl() {
		HashIdLogger.log(this,"constructor");
	}

	/* (non-Javadoc)
	 * @see nz.co.senanque.workflow.WorkflowManager#createContextDescriptor(java.lang.Object)
	 */
	@Override
	public String createContextDescriptor(Object o) {
		return getContextDAO().createContextDescriptor(o);
	}
	/* (non-Javadoc)
	 * @see nz.co.senanque.workflow.WorkflowManager#execute(long)
	 */
	@Transactional
	public void execute(long id) {
		execute(getWorkflowDAO().findProcessInstance(id));
	}

	/* (non-Javadoc)
	 * @see nz.co.senanque.workflow.WorkflowManager#execute(nz.co.senanque.workflow.instances.DeferredEvent)
	 */
	@Transactional
	public void executeDeferredEvent(long deferredEventId) {
		DeferredEvent deferredEvent = getWorkflowDAO().findDeferredEvent(deferredEventId);
		log.debug("fired deferred event {} for {} {}",deferredEvent.getEventType(),deferredEvent.getProcessInstance().getId(),deferredEvent.getComment());
		ProcessInstance processInstance = deferredEvent.getProcessInstance();
		switch (deferredEvent.getEventType()) {
		case DEFERRED: 
			ProcessInstanceUtils.clearQueue(processInstance, TaskStatus.TIMEOUT);
			int lastAuditIndex = processInstance.getAudits().size()-1;
			Audit lastAudit = processInstance.getAudits().get(lastAuditIndex);
			lastAudit.setInterrupted(true);
			lastAudit.setStatus(TaskStatus.TIMEOUT);
			Date now = new Date();
			lastAudit.setComment(trimComment(lastAudit.getComment()+" Timed out at "+now));
			// this sets the task to the TryTask that generated the timeout.
			processInstance.setProcessDefinitionName(deferredEvent.getProcessDefinitionName());
			processInstance.setTaskId(deferredEvent.getTaskId());
			break;
		case FORCE_ABORT: 
			ProcessInstanceUtils.clearQueue(processInstance, TaskStatus.ABORTING);
			processInstance.setComment("Sibling aborted");
			break;
		case SUBPROCESS_END:
			if (processInstance.getWaitCount() == 0) {
				break;
			}
			processInstance.setWaitCount(processInstance.getWaitCount()-1);
			// If this is the last process then kick off the parent, but
			// if any of the siblings aborted then abort the parent.
			if (processInstance.getWaitCount() == 0) {
				if (processInstance.isCyclic()) {
					// but if this is a cyclic there won't be any siblings
					// and we want to re-execute the parent. Use the Retry logic.
					TaskBase previous = getCurrentTask(processInstance).getPreviousTask(processInstance);
					if (previous == null) {
						throw new WorkflowException("Trying to retry a task when there is none");
					}
					previous.loadTask(processInstance);
					processInstance.setStatus(TaskStatus.GO);
				} else {
					processInstance.setStatus(TaskStatus.GO);
					for (ProcessInstance sibling: processInstance.getChildProcesses()) {
						if (sibling.getStatus() == TaskStatus.ABORTED) {
							processInstance.setStatus(TaskStatus.ABORTING); 
							processInstance.setComment("Child aborted");
						}
					}				
				}
			}
			break;
		default:
			log.error("Unexpected event type {} (ignoring)",deferredEvent.getEventType());
			return;
		}
		deferredEvent.setEventType(EventType.DONE);
	}
	/* (non-Javadoc)
	 * @see nz.co.senanque.workflow.WorkflowManager#getContext(java.lang.String)
	 */
	@Transactional
	public Object getContext(String objectInstance) {
		return getContextDAO().getContext(objectInstance);
	}

	/* (non-Javadoc)
	 * @see nz.co.senanque.workflow.WorkflowManager#getField(nz.co.senanque.workflow.instances.ProcessInstance, nz.co.senanque.schemaparser.FieldDescriptor)
	 */
	@Transactional
	public Object getField(ProcessInstance processInstance, FieldDescriptor fd) {
		Object o = getContextDAO().getContext(processInstance.getObjectInstance());
		String prefix="get";
		if (fd.getType().endsWith("Boolean")) {
			prefix="is";
		}
		String name=prefix+StringUtils.capitalize(fd.getName());
		try {
			Method getter = o.getClass().getMethod(name);
			return getter.invoke(o);
		} catch (Exception e) {
			throw new WorkflowException("Problem finding field: "+fd.getName());
		}
	}
	@Transactional
	public ProcessInstance launch(String processName, Object o, String comment, String bundleName) {
		ProcessInstance processInstance = new ProcessInstance();
		return launch(processName, o,comment,bundleName, processInstance);
	}

	@Transactional
	public long launch(WorkflowForm launchForm, String comment, String bundleName) {
		ProcessInstance processInstanceRet = launch(launchForm.getProcessName(), launchForm.getContext(),comment,bundleName, launchForm.getProcessInstance());
		return processInstanceRet.getId();
	}
	/* (non-Javadoc)
	 * @see nz.co.senanque.workflow.WorkflowManager#launch(java.lang.String, java.lang.Object, java.lang.String, java.lang.String)
	 */
	@Transactional
	public ProcessInstance launch(String processName, Object context, String comment,
			String bundleName, ProcessInstance processInstance) {
		ProcessDefinition processDefinition = getProcessDefinition(processName);
		if (processDefinition == null) {
			throw new WorkflowException("Failed to find process definition named "+processName);
		}
		Object mergedContext = getContextDAO().mergeContext(context);
		processInstance.setComment(comment==null?" ":comment);
		processInstance.setBundleName(bundleName);
		processInstance.setObjectInstance(getContextDAO().createContextDescriptor(mergedContext));
		if (!ContextUtils.getContextClass(processInstance.getObjectInstance()).equals(mergedContext.getClass())) {
			throw new WorkflowException("Context object does not match process context in "+processName);
		}
		processDefinition.startProcess(processInstance);

		ProcessInstance processInstanceRet = getWorkflowDAO().mergeProcessInstance(processInstance);
		getWorkflowDAO().flush();
		return processInstanceRet;
	}

	@Transactional
	public long save(WorkflowForm workflowForm) {
		Object context = getContextDAO().mergeContext(workflowForm.getContext());
		ProcessInstance processInstance = workflowForm.getProcessInstance();
		processInstance.setLastUpdated(new Timestamp(System.currentTimeMillis()));
		processInstance.setObjectInstance(createContextDescriptor(context));
		ProcessInstance pi = null;
		if (processInstance.getId() == 0) {
			processInstance.setTaskId(0L);
			TaskBase task = getTask(workflowForm.getProcessDefinition(),0L);
			pi = getWorkflowDAO().mergeProcessInstance(processInstance);
			Audit audit = createAudit(pi, task);
			audit.setStatus(TaskStatus.DONE);
		}
		else {
			TaskBase task = getCurrentTask(processInstance);
			pi = getWorkflowDAO().mergeProcessInstance(processInstance);
			createAudit(pi, task);
		}
		return pi.getId();
	}
	@Transactional
	public ProcessInstance refresh(ProcessInstance processInstance) {
		return getWorkflowDAO().refreshProcessInstance(processInstance);
	}

	
	/* (non-Javadoc)
	 * @see nz.co.senanque.workflow.WorkflowManager#mergeContext(java.lang.Object)
	 */
	@Transactional
	public void mergeContext(Object context) {
		getContextDAO().mergeContext(context);
	}

	@Transactional
	public Collection getAudits(ProcessInstance processInstance) {
		ProcessInstance pi = getWorkflowDAO().mergeProcessInstance(processInstance);
		return pi.getAudits();
	}
	/* (non-Javadoc)
	 * @see nz.co.senanque.workflow.WorkflowManager#processMessage(nz.co.senanque.workflow.instances.ProcessInstance, org.springframework.integration.Message, nz.co.senanque.messaging.MessageMapper)
	 */
	@Transactional
	public void processMessage(ProcessInstance processInstance,
			Message message, MessageMapper messageMapper) {
		Object context = getContextDAO().getContext(processInstance.getObjectInstance());
		processInstance.setStatus(TaskStatus.GO);
		try {
			messageMapper.unpackMessage(message, context);
		} catch (Exception e) {
			processInstance.setStatus(TaskStatus.ABORTING);
			processInstance.setComment(e.getMessage());
		}
		log.debug("set status to processInstance {} to {} {}",processInstance.getId(),processInstance.getStatus(),context);
		getContextDAO().mergeContext(context);
		getWorkflowDAO().mergeProcessInstance(processInstance);
		getWorkflowDAO().flush();
		if (log.isDebugEnabled()) {
			getWorkflowDAO().getActiveProcesses();
		}
	}

	@Transactional
	public void execute(ProcessInstance processInstance) {
		super.execute(processInstance);
		getWorkflowDAO().flush();
	}
	
	/**
	 * This manages the transition of the process instance from Waiting to Busy with appropriate locking
	 * and unlocking. Once it is busy it is ours but we have to check that it was not changed since we last saw it.
	 * The situation we are protecting against is that although the process instance looked to be in wait state
	 * in the table we might have sat on that for a while and someone else may have updated it meanwhile.
	 * In that case we reject the request, unless we are TECHSUPPORT. Those guys can do anything.
	 * @param processInstance
	 * @param techSupport (boolean that indicates if we have TECHSUPPORT privs)
	 * @param userName
	 * @return updated processInstance or null if we failed to get the lock
	 */
	public ProcessInstance lockProcessInstance(final ProcessInstance processInstance, final boolean techSupport, final String userName) {
		List locks = ContextUtils.getLocks(processInstance,getLockFactory(),"nz.co.senanque.workflow.WorkflowClient.lock(ProcessInstance)");
		LockTemplate lockTemplate = new LockTemplate(locks, new LockAction() {
			
			public void doAction() {
				
				String taskId = ProcessInstanceUtils.getTaskId(processInstance);
				ProcessInstance pi = getWorkflowDAO().refreshProcessInstance(processInstance);
//				if (log.isDebugEnabled()) {
//					log.debug("taskId {} ProcessInstanceUtils.getTaskId(pi) {} {}",taskId,ProcessInstanceUtils.getTaskId(pi),(!taskId.equals(ProcessInstanceUtils.getTaskId(pi))));
//					log.debug("pi.getStatus() {} techSupport {} {}",pi.getStatus(),techSupport,((pi.getStatus() != TaskStatus.WAIT) && !techSupport));
//					log.debug("pi.getStatus() {} userName {} pi.getLockedBy() {} {}",pi.getStatus(),userName,pi.getLockedBy(),(pi.getStatus() == TaskStatus.BUSY) && !userName.equals(pi.getLockedBy()) && !techSupport);
//				}
				if (!techSupport) {
					if (!(taskId.equals(ProcessInstanceUtils.getTaskId(pi)) && 
							((pi.getStatus() == TaskStatus.WAIT) || 
									((pi.getStatus() == TaskStatus.BUSY) && userName.equals(pi.getLockedBy()))))) {
//						// In this case we did not actually fail to get the lock but
//						// the process is not in the state
//						// it was in when we saw it in the table because another
//						// user (probably) has updated it.
//						// Therefore it is dangerous to proceed (unless we are tech support)
						throw new RuntimeException("ProcessInstance is already busy");
					}
				}
				pi.setStatus(TaskStatus.BUSY);
				pi.setLockedBy(userName);
				TaskBase task = getCurrentTask(pi);
				Audit audit = createAudit(pi, task);
				getWorkflowDAO().mergeProcessInstance(pi);
			}
		});
		boolean weAreOkay = true;
		try {
			weAreOkay = lockTemplate.doAction();
		} catch (Exception e) {
			weAreOkay = false;
		}
		if (!weAreOkay) {
			return null;
		}
		return getWorkflowDAO().refreshProcessInstance(processInstance);
	}
	protected void tickleParentProcess(ProcessInstance processInstance, TaskStatus status) {
		ProcessInstance parent = processInstance.getParentProcess();
		if (parent != null)
		{
			// we have a parent waiting for subprocesses to exit

			// If this child process was aborted
			// go find all the siblings that are still alive and abort them
			if (status == TaskStatus.ABORTED) {
				for (ProcessInstance sibling: parent.getChildProcesses()) {
					if (sibling.getStatus() != TaskStatus.ABORTED && 
							sibling.getStatus() != TaskStatus.ABORTING && 
							sibling.getStatus() != TaskStatus.DONE) {
						
						DeferredEvent deferredEvent = new DeferredEvent();
						deferredEvent.setEventType(EventType.FORCE_ABORT);
						sibling.setStatus(TaskStatus.ABORTING);
						deferredEvent.setProcessInstance(sibling);
						deferredEvent.setComment("aborting sibling");
						sibling.getDeferredEvents().add(deferredEvent);
					}
				}
			}
			if (parent.getWaitCount() > 0) {
				// this lets the parent know this subprocess is finished.
				log.debug("created an event for SUBPROCESS_END");
				DeferredEvent deferredEvent = new DeferredEvent();
				deferredEvent.setEventType(EventType.SUBPROCESS_END);
				deferredEvent.setProcessInstance(parent);
				parent.getDeferredEvents().add(deferredEvent);
				deferredEvent.setComment("process instance: "+processInstance.getId());
			}
			getWorkflowDAO().mergeProcessInstance(parent);
		}		
	}


	/**
	 * If this is just the end of the handler then return the next task after the handler
	 * If it is the end of the whole process then return null.
	 * @param processInstance
	 * @param currentAudit
	 * @return TaskBase
	 */
	protected TaskBase endOfProcessDetected(ProcessInstance processInstance, Audit currentAudit) {
		TaskBase ret = null;
		TaskBase currentTask = getCurrentTask(processInstance);
		ProcessInstanceUtils.clearQueue(processInstance, TaskStatus.DONE);
		currentAudit.setStatus(TaskStatus.DONE);
		// End of process can mean just the end of a handler process.
		{
			List audits = findHandlerTasks(processInstance);
			for (Audit audit : audits) {
				TaskBase taskBase = getTask(audit);
				audit.setHandler(false);
				if (taskBase instanceof TaskTry) {
					TaskTry taskTry = (TaskTry)taskBase;
					if (taskTry.getTimeoutValue() > -1) {
						// we ended a handler that had a timeout.
						// That means we need to cancel the timeout.
						getWorkflowDAO().removeDeferredEvent(processInstance,taskTry);
					}
					TaskBase nextTask = taskTry.getNextTask(processInstance);
					nextTask.loadTask(processInstance);
				}
				if (taskBase instanceof TaskIf) {
					TaskIf taskIf = (TaskIf)taskBase;
					TaskBase nextTask = taskIf.getNextTask(processInstance);
					nextTask.loadTask(processInstance);
				}
				ret = getCurrentTask(processInstance);
				break;
			}

		}
		// If this is a subprocess then tickle the parent.
		tickleParentProcess(processInstance,TaskStatus.DONE);
		if (ret == currentTask) {
			processInstance.setStatus(TaskStatus.DONE);
		}
		getWorkflowDAO().mergeProcessInstance(processInstance);
		getWorkflowDAO().flush();
		getWorkflowDAO().refreshProcessInstance(processInstance);
		return ret;
	}
	
	/**
	 * Scan resources for workflow files and message definitions
	 */
	@PostConstruct
	public void init() {
		findBeans();
		SAXBuilder builder = new SAXBuilder();
		Document doc = null;
		try {
			doc = builder.build(getSchema().getInputStream());
			SchemaParser schemaParser = new SchemaParser();
			schemaParser.parse(doc);
			ParserSource parserSource = new InputStreamParserSource(getProcesses());
			
			ProcessTextProvider textProvider = new ProcessTextProvider(parserSource,schemaParser,this);
			ParsePackage parsePackage = new ParsePackage();
			parsePackage.parse(textProvider);
		} catch (Exception e) {
			throw new WorkflowException(e);
		}
		HashIdLogger.log(this,"postconstruct");
	}
	/* (non-Javadoc)
	 * @see nz.co.senanque.workflow.WorkflowManager#shutdown()
	 */
	@PreDestroy
	public void shutdown() {
		if (getExecutor() != null) {
			getExecutor().shutdown();
		}
//		if (getIntegrationMBeanExporter() != null) {
//			log.info("Stopping Spring Integration...");
//			getIntegrationMBeanExporter().stopActiveComponents(true, 10000);
//			log.info("Stopping Spring Integration...finished");
//		}
	}
	
	public void finishLaunch(long processId) {
		ProcessInstance processInstance = getWorkflowDAO().findProcessInstance(processId);
		processInstance.setStatus(TaskStatus.GO);
		getWorkflowDAO().mergeProcessInstance(processInstance);
		getWorkflowDAO().flush();
	}
	
	public WorkflowForm getLaunchForm(String processName) {
        ProcessDefinition processDefinition = getProcessDefinition(processName);
        String launchForm = processDefinition.getLaunchForm();
        Object context=null;
		try {
			ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
			context = Class.forName(processDefinition.getFullClassName(),false,classLoader).newInstance();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		WorkflowForm workflowForm = m_formFactory.getForm(launchForm);
		workflowForm.setProcessDefinition(processDefinition);
		workflowForm.setContext(context);
		ProcessInstance processInstance = new ProcessInstance();
		processInstance.setProcessDefinitionName(processDefinition.getName());
		processInstance.setComment(" ");
		workflowForm.setProcessInstance(processInstance);
		return workflowForm;
	}

	public WorkflowForm getCurrentForm(ProcessInstance processInstance) {
        ProcessDefinition processDefinition = getProcessDefinition(processInstance.getProcessDefinitionName());
		String formName = processDefinition.getLaunchForm();
		boolean readOnly = true;
		TaskBase task = getCurrentTask(processInstance);
		if ((task instanceof TaskForm)) {
			formName =  ((TaskForm)task).getFormName();
			readOnly = false;
		}
		WorkflowForm workflowForm = m_formFactory.getForm(formName);
		Object context = getContext(processInstance.getObjectInstance());
		workflowForm.setProcessDefinition(processDefinition);
		workflowForm.setProcessInstance(processInstance);
		workflowForm.setContext(context);
		workflowForm.setReadOnly(readOnly);
		workflowForm.bind();
		return workflowForm;
	}
	public ContextDAO getContextDAO() {
		return m_contextDAO;
	}

	public void setContextDAO(ContextDAO contextDAO) {
		m_contextDAO = contextDAO;
	}

	public WorkflowDAO getWorkflowDAO() {
		return m_workflowDAO;
	}

	public void setWorkflowDAO(WorkflowDAO workflowDAO) {
		m_workflowDAO = workflowDAO;
	}

	public Executor getExecutor() {
		return m_executor;
	}

	public void setExecutor(Executor executor) {
		m_executor = executor;
	}

	public LockFactory getLockFactory() {
		return m_lockFactory;
	}

	public void setLockFactory(LockFactory lockFactory) {
		m_lockFactory = lockFactory;
	}

	public IntegrationMBeanExporter getIntegrationMBeanExporter() {
		return m_integrationMBeanExporter;
	}

	public void setIntegrationMBeanExporter(
			IntegrationMBeanExporter integrationMBeanExporter) {
		m_integrationMBeanExporter = integrationMBeanExporter;
	}

	public FormFactory getFormFactory() {
		return m_formFactory;
	}
	public void setFormFactory(FormFactory formFactory) {
		m_formFactory = formFactory;
	}
	public ValidationEngine getValidationEngine() {
		return m_validationEngine;
	}
	public void setValidationEngine(ValidationEngine validationEngine) {
		m_validationEngine = validationEngine;
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy