nz.co.senanque.workflow.WorkflowManagerImpl.java Source code

Java tutorial

Introduction

Here is the source code for nz.co.senanque.workflow.WorkflowManagerImpl.java

Source

/*******************************************************************************
 * 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.schemaparser.SchemaParserImpl;
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<Audit> 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<Lock> 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<Audit> 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());
            SchemaParserImpl schemaParser = new SchemaParserImpl();
            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;
    }

}