/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */

package org.alfresco.repo.workflow.activiti;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.activiti.bpmn.model.BpmnModel;
import org.activiti.engine.ActivitiException;
import org.activiti.engine.FormService;
import org.activiti.engine.HistoryService;
import org.activiti.engine.ManagementService;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.delegate.event.ActivitiEventType;
import org.activiti.engine.delegate.event.impl.ActivitiEventBuilder;
import org.activiti.engine.form.StartFormData;
import org.activiti.engine.history.HistoricProcessInstance;
import org.activiti.engine.history.HistoricProcessInstanceQuery;
import org.activiti.engine.history.HistoricTaskInstance;
import org.activiti.engine.history.HistoricTaskInstanceQuery;
import org.activiti.engine.impl.RepositoryServiceImpl;
import org.activiti.engine.impl.bpmn.behavior.ReceiveTaskActivityBehavior;
import org.activiti.engine.impl.bpmn.behavior.UserTaskActivityBehavior;
import org.activiti.engine.impl.bpmn.deployer.BpmnDeployer;
import org.activiti.engine.impl.context.Context;
import org.activiti.engine.impl.form.DefaultTaskFormHandler;
import org.activiti.engine.impl.form.TaskFormHandler;
import org.activiti.engine.impl.identity.Authentication;
import org.activiti.engine.impl.interceptor.CommandContext;
import org.activiti.engine.impl.persistence.entity.ExecutionEntity;
import org.activiti.engine.impl.persistence.entity.ProcessDefinitionEntity;
import org.activiti.engine.impl.persistence.entity.TimerEntity;
import org.activiti.engine.impl.pvm.PvmActivity;
import org.activiti.engine.impl.pvm.ReadOnlyProcessDefinition;
import org.activiti.engine.impl.pvm.process.ActivityImpl;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.engine.repository.ProcessDefinitionQuery;
import org.activiti.engine.runtime.Execution;
import org.activiti.engine.runtime.Job;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.activiti.engine.task.TaskQuery;
import org.activiti.image.ProcessDiagramGenerator;
import org.activiti.image.impl.DefaultProcessDiagramGenerator;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.i18n.MessageService;
import org.alfresco.repo.model.Repository;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.tenant.TenantService;
import org.alfresco.repo.tenant.TenantUtil;
import org.alfresco.repo.workflow.BPMEngine;
import org.alfresco.repo.workflow.WorkflowAuthorityManager;
import org.alfresco.repo.workflow.WorkflowConstants;
import org.alfresco.repo.workflow.WorkflowDeployer;
import org.alfresco.repo.workflow.WorkflowEngine;
import org.alfresco.repo.workflow.WorkflowModel;
import org.alfresco.repo.workflow.WorkflowNodeConverter;
import org.alfresco.repo.workflow.WorkflowNotificationUtils;
import org.alfresco.repo.workflow.WorkflowObjectFactory;
import org.alfresco.repo.workflow.activiti.properties.ActivitiPropertyConverter;
import org.alfresco.repo.workflow.activiti.variable.ScriptNodeVariableType;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.dictionary.TypeDefinition;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.cmr.workflow.LazyActivitiWorkflowTask;
import org.alfresco.service.cmr.workflow.WorkflowDefinition;
import org.alfresco.service.cmr.workflow.WorkflowDeployment;
import org.alfresco.service.cmr.workflow.WorkflowException;
import org.alfresco.service.cmr.workflow.WorkflowInstance;
import org.alfresco.service.cmr.workflow.WorkflowInstanceQuery;
import org.alfresco.service.cmr.workflow.WorkflowInstanceQuery.DatePosition;
import org.alfresco.service.cmr.workflow.WorkflowPath;
import org.alfresco.service.cmr.workflow.WorkflowTask;
import org.alfresco.service.cmr.workflow.WorkflowTaskDefinition;
import org.alfresco.service.cmr.workflow.WorkflowTaskQuery;
import org.alfresco.service.cmr.workflow.WorkflowTaskQuery.OrderBy;
import org.alfresco.service.cmr.workflow.WorkflowTaskState;
import org.alfresco.service.cmr.workflow.WorkflowTimer;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.GUID;
import org.alfresco.util.collections.Function;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xerces.parsers.DOMParser;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

/**
 * @author Nick Smith
 * @author Frederik Heremans
 * @since 3.4.e
 */
public class ActivitiWorkflowEngine extends BPMEngine implements WorkflowEngine
{
	private static Log logger = LogFactory.getLog(ActivitiWorkflowEngine.class);
	
    // Workflow Component Messages
    private static final String ERR_DEPLOY_WORKFLOW = "activiti.engine.deploy.workflow.error";
    private static final String ERR_IS_WORKFLOW_DEPLOYED = "activiti.engine.is.workflow.deployed.error";
    private static final String ERR_UNDEPLOY_WORKFLOW = "activiti.engine.undeploy.workflow.error";
    private static final String ERR_UNDEPLOY_WORKFLOW_UNEXISTING = "activiti.engine.undeploy.workflow.unexisting.error";
    private static final String ERR_GET_WORKFLOW_DEF = "activiti.engine.get.workflow.definition.error";
    private static final String ERR_GET_WORKFLOW_DEF_BY_ID = "activiti.engine.get.workflow.definition.by.id.error";
    private static final String ERR_GET_WORKFLOW_DEF_BY_NAME = "activiti.engine.get.workflow.definition.by.name.error";
    private static final String ERR_GET_ALL_DEFS_BY_NAME = "activiti.engine.get.all.workflow.definitions.by.name.error";
    private static final String ERR_GET_DEF_IMAGE = "activiti.engine.get.workflow.definition.image.error";
    private static final String ERR_GET_DEF_UNEXISTING_IMAGE = "activiti.engine.get.workflow.definition.unexisting.image.error";
    private static final String ERR_START_WORKFLOW = "activiti.engine.start.workflow.error";
    private static final String ERR_GET_WORKFLOW_INSTS = "activiti.engine.get.workflows.error";
    private static final String ERR_GET_ACTIVE_WORKFLOW_INSTS = "activiti.engine.get.active.workflows.error";
    private static final String ERR_GET_COMPLETED_WORKFLOW_INSTS = "activiti.engine.get.completed.workflows.error";
    private static final String ERR_GET_WORKFLOW_PATHS = "activiti.engine.get.workflow.paths.error";
    private static final String ERR_CANCEL_WORKFLOW = "activiti.engine.cancel.workflow.error";
    private static final String ERR_CANCEL_UNEXISTING_WORKFLOW = "activiti.engine.cancel.unexisting.workflow.error";
    private static final String ERR_DELETE_WORKFLOW = "activiti.engine.delete.workflow.error";
    private static final String ERR_DELETE_UNEXISTING_WORKFLOW = "activiti.engine.delete.unexisting.workflow.error";
    protected static final String ERR_FIRE_EVENT_NOT_SUPPORTED = "activiti.engine.event.unsupported";
    private static final String ERR_GET_TASKS_FOR_PATH = "activiti.engine.get.tasks.for.path.error";
    private static final String ERR_GET_TIMERS = "activiti.engine.get.timers.error";
    protected static final String ERR_FIND_COMPLETED_TASK_INSTS = "activiti.engine.find.completed.task.instances.error";
    private static final String ERR_GET_WORKFLOW_TOKEN_INVALID = "activiti.engine.get.workflow.token.invalid";
    private static final String ERR_GET_WORKFLOW_TOKEN_NULL = "activiti.engine.get.workflow.token.is.null";
    
    // Task Component Messages
    private static final String ERR_GET_ASSIGNED_TASKS = "activiti.engine.get.assigned.tasks.error";
    private static final String ERR_GET_POOLED_TASKS = "activiti.engine.get.pooled.tasks.error";
    private static final String ERR_UPDATE_TASK = "activiti.engine.update.task.error";
    private static final String ERR_UPDATE_TASK_UNEXISTING = "activiti.engine.update.task.unexisting.error";
    private static final String ERR_UPDATE_START_TASK = "activiti.engine.update.starttask.illegal.error";
    private static final String ERR_END_UNEXISTING_TASK = "activiti.engine.end.task.unexisting.error";
    private static final String ERR_GET_TASK_BY_ID = "activiti.engine.get.task.by.id.error";
    private static final String ERR_END_TASK_INVALID_TRANSITION = "activiti.engine.end.task.invalid.transition";
    
    public static final QName QNAME_INITIATOR = QName.createQName(NamespaceService.DEFAULT_URI, WorkflowConstants.PROP_INITIATOR);
    
    private RepositoryService repoService;
    private RuntimeService runtimeService;
    private TaskService taskService;
    private HistoryService historyService;
    private ManagementService managementService;
    private FormService formService;
    protected ActivitiUtil activitiUtil;
    
    private DictionaryService dictionaryService;
    private NodeService nodeService;
    private PersonService personService;
    private ActivitiTypeConverter typeConverter;
    private ActivitiPropertyConverter propertyConverter;
    private WorkflowAuthorityManager authorityManager;
    private WorkflowNodeConverter nodeConverter;
    private WorkflowObjectFactory factory;
    
    private MessageService messageService;
    private TenantService tenantService;
    private NamespaceService namespaceService;
    private Repository repositoryHelper;
    
    public ActivitiWorkflowEngine()
    {
        super();
    }

    /**
     * {@inheritDoc}
     */
     @Override
     public void afterPropertiesSet() throws Exception
     {
         super.afterPropertiesSet();
         this.repoService = activitiUtil.getRepositoryService();
         this.runtimeService = activitiUtil.getRuntimeService();
         this.taskService = activitiUtil.getTaskService();
         this.formService = activitiUtil.getFormService();
         this.historyService = activitiUtil.getHistoryService();
         this.managementService = activitiUtil.getManagementService();
     }

     
    /* (non-Javadoc)
     * @see org.alfresco.repo.workflow.WorkflowComponent#cancelWorkflows(java.util.List)
     */
    @Override
    public List<WorkflowInstance> cancelWorkflows(List<String> workflowIds)
    {
        List<WorkflowInstance> result = new ArrayList<WorkflowInstance>(workflowIds.size());
        for (String workflowId : workflowIds)
        {
            result.add(cancelWorkflow(workflowId));
        }
        return result;
    }

    /**
    * {@inheritDoc}
    */
    public WorkflowInstance cancelWorkflow(String workflowId)
    {
        String localId = createLocalId(workflowId);
        try
        {
            ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(localId).singleResult();
            if (processInstance == null)
            {
                throw new WorkflowException(messageService.getMessage(ERR_CANCEL_UNEXISTING_WORKFLOW));
            }

            // TODO: Cancel VS delete?
            // Delete the process instance
            runtimeService.deleteProcessInstance(processInstance.getId(), WorkflowConstants.PROP_CANCELLED);

            // Convert historic process instance
            HistoricProcessInstance deletedInstance = historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstance.getId())
                    .singleResult();
            WorkflowInstance result = typeConverter.convert(deletedInstance);

            // Delete the historic process instance
            // MNT-15498
            if (!activitiUtil.isRetentionHistoricProcessInstanceEnabled())
            {
                historyService.deleteHistoricProcessInstance(deletedInstance.getId());
            }

            return result;
        }
        catch (ActivitiException ae)
        {
            String msg = messageService.getMessage(ERR_CANCEL_WORKFLOW);
            if (logger.isDebugEnabled())
            {
                logger.debug(msg, ae);
            }
            throw new WorkflowException(msg, ae);
        }
    }

    /**
    * {@inheritDoc}
    */
    public WorkflowInstance deleteWorkflow(String workflowId)
    {
        String localId = createLocalId(workflowId);
        try
        {
            // Delete the runtime process instance if still running, this calls the end-listeners if any
            ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(localId).singleResult();
            if(processInstance != null) 
            {
                runtimeService.deleteProcessInstance(processInstance.getId(), ActivitiConstants.DELETE_REASON_DELETED);
            }
            
            // Convert historic process instance
            HistoricProcessInstance deletedInstance = historyService.createHistoricProcessInstanceQuery()
                .processInstanceId(localId)
                .singleResult();
            
            if(deletedInstance == null) {
                throw new WorkflowException(messageService.getMessage(ERR_DELETE_UNEXISTING_WORKFLOW, localId));
            }
            
            WorkflowInstance result =  typeConverter.convert(deletedInstance);
            
            // Delete the historic process instance
            historyService.deleteHistoricProcessInstance(deletedInstance.getId());
            
            return result;
        } 
        catch(ActivitiException ae) 
        {
            String msg = messageService.getMessage(ERR_DELETE_WORKFLOW);
            if(logger.isDebugEnabled())
            {
            	logger.debug(msg, ae);
            }
            throw new WorkflowException(msg, ae);
        }
    }

    /**
    * {@inheritDoc}
    */
    public WorkflowDeployment deployDefinition(InputStream workflowDefinition, String mimetype)
    {
        return deployDefinition(workflowDefinition, mimetype, null);
    }
    
    /**
     * {@inheritDoc}
     */
     public WorkflowDeployment deployDefinition(InputStream workflowDefinition, String mimetype, String name)
     {
         try 
         {
             String resourceName = GUID.generate() + BpmnDeployer.BPMN_RESOURCE_SUFFIXES[0];
             
             Deployment deployment = repoService.createDeployment()
                 .addInputStream(resourceName, workflowDefinition)
                 .name(name)
                 .deploy();
             
             List<ProcessDefinition> definitionList = repoService.createProcessDefinitionQuery().deploymentId(deployment.getId()).list();
             if (definitionList != null && definitionList.size() > 0)
             {
                 boolean internalCategory = true;
                 for (ProcessDefinition processDefinition : definitionList)
                 {
                    if (WorkflowDeployer.CATEGORY_ALFRESCO_INTERNAL.equals(processDefinition.getCategory()) == false)
                    {
                        internalCategory = false;
                        break;
                    }
                 }
                 
                 if (internalCategory)
                 {
                     repoService.setDeploymentCategory(deployment.getId(), WorkflowDeployer.CATEGORY_ALFRESCO_INTERNAL);
                 }
             }
             
             // No problems can be added to the WorkflowDeployment, warnings are
             // not exposed
             return typeConverter.convert(deployment);
         } 
         catch(Exception ae) 
         {
             String msg = messageService.getMessage(ERR_DEPLOY_WORKFLOW);
             if(logger.isDebugEnabled())
             {
             	logger.debug(msg, ae);
             }
             throw new WorkflowException(msg, ae);
         }
     }

    /**
    * {@inheritDoc}
    */
    public WorkflowPath fireEvent(String pathId, String event)
    {
       String message = messageService.getMessage(ERR_FIRE_EVENT_NOT_SUPPORTED);
       throw new WorkflowException(message);
    }

    /**
    * {@inheritDoc}
    */
    public List<WorkflowInstance> getActiveWorkflows()
    {
        try
        {
            return getWorkflows(new WorkflowInstanceQuery(true));
        }
        catch(ActivitiException ae) 
        {
            String message = messageService.getMessage(ERR_GET_ACTIVE_WORKFLOW_INSTS, "");
            if(logger.isDebugEnabled())
            {
            	logger.debug(message, ae);
            }
            throw new WorkflowException(message, ae);
        }
    }
    
    /**
    * {@inheritDoc}
    */
    @Override
    public List<WorkflowInstance> getCompletedWorkflows()
    {
        try
        {
            return getWorkflows(new WorkflowInstanceQuery(false));
        }
        catch(ActivitiException ae) 
        {
            String message = messageService.getMessage(ERR_GET_COMPLETED_WORKFLOW_INSTS, "");
            if(logger.isDebugEnabled())
            {
            	logger.debug(message, ae);
            }
            throw new WorkflowException(message, ae);
        }
    }

    /**
    * {@inheritDoc}
    */
    @Override
    public List<WorkflowInstance> getWorkflows()
    {
        try
        {
            return getWorkflows(new WorkflowInstanceQuery());
        }
        catch(ActivitiException ae) 
        {
            String message = messageService.getMessage(ERR_GET_WORKFLOW_INSTS, "");
            if(logger.isDebugEnabled())
            {
            	logger.debug(message, ae);
            }
            throw new WorkflowException(message, ae);
        }
    }
    
    /**
    * {@inheritDoc}
    */
    public List<WorkflowInstance> getActiveWorkflows(String workflowDefinitionId)
    {
        try
        {
            return getWorkflows(new WorkflowInstanceQuery(workflowDefinitionId, true));
        }
        catch(ActivitiException ae) 
        {
            String message = messageService.getMessage(ERR_GET_ACTIVE_WORKFLOW_INSTS, workflowDefinitionId);
            if(logger.isDebugEnabled())
            {
            	logger.debug(message, ae);
            }
            throw new WorkflowException(message, ae);
        }
    }

    /**
    * {@inheritDoc}
    */
    public List<WorkflowDefinition> getAllDefinitions()
    {
        try 
        {
            ProcessDefinitionQuery query = repoService.createProcessDefinitionQuery();
            if(activitiUtil.isMultiTenantWorkflowDeploymentEnabled() && !TenantUtil.isCurrentDomainDefault()) 
            {
                query.processDefinitionKeyLike("@" + TenantUtil.getCurrentDomain() + "%");
            }
            return getValidWorkflowDefinitions(query.list());
        } 
        catch (ActivitiException ae)
        {
            String msg = messageService.getMessage(ERR_GET_WORKFLOW_DEF);
            if(logger.isDebugEnabled())
            {
            	logger.debug(msg, ae);
            }
            throw new WorkflowException(msg, ae);
        }
    }

    /**
    * {@inheritDoc}
    */
    public List<WorkflowDefinition> getAllDefinitionsByName(String workflowName)
    {
        try 
        {
            String key = factory.getProcessKey(workflowName);
            List<ProcessDefinition> definitions = repoService.createProcessDefinitionQuery()
                .processDefinitionKey(key)
                .list();
            return getValidWorkflowDefinitions(definitions);          
        }
        catch (ActivitiException ae)
        {
            String msg = messageService.getMessage(ERR_GET_ALL_DEFS_BY_NAME, workflowName);
            if(logger.isDebugEnabled())
            {
            	logger.debug(msg, ae);
            }
            throw new WorkflowException(msg, ae);
        }
    }

    /**
    * {@inheritDoc}
    */
    public List<WorkflowInstance> getCompletedWorkflows(String workflowDefinitionId)
    {
        try
        {
            return getWorkflows(new WorkflowInstanceQuery(workflowDefinitionId, false));
        }
        catch(ActivitiException ae) 
        {
            String message = messageService.getMessage(ERR_GET_COMPLETED_WORKFLOW_INSTS, workflowDefinitionId);
            if(logger.isDebugEnabled())
            {
            	logger.debug(message, ae);
            }
            throw new WorkflowException(message, ae);
        }
    }

    /**
    * {@inheritDoc}
    */
    public WorkflowDefinition getDefinitionById(String workflowDefinitionId)
    {
        try
        {
            String localId = createLocalId(workflowDefinitionId);
            ProcessDefinition procDef = repoService.createProcessDefinitionQuery()
                .processDefinitionId(localId )
                .singleResult();
            
            if(activitiUtil.isMultiTenantWorkflowDeploymentEnabled() && procDef != null)
            {
                factory.checkDomain(procDef.getKey());
            }
            
            return typeConverter.convert(procDef);
        }
        catch (ActivitiException ae)
        {
            String msg = messageService.getMessage(ERR_GET_WORKFLOW_DEF_BY_ID, workflowDefinitionId);
            if(logger.isDebugEnabled())
            {
            	logger.debug(msg, ae);
            }
            throw new WorkflowException(msg, ae);
        }
    }

    /**
    * {@inheritDoc}
    */
    public WorkflowDefinition getDefinitionByName(String workflowName)
    {
        try 
        {
            String key = factory.getLocalEngineId(workflowName);
            if(activitiUtil.isMultiTenantWorkflowDeploymentEnabled())
            {
                key = factory.getDomainProcessKey(workflowName);
            }
            ProcessDefinition definition = activitiUtil.getProcessDefinitionByKey(key);
            return typeConverter.convert(definition);
        }
        catch (ActivitiException ae)
        {
            String msg = messageService.getMessage(ERR_GET_WORKFLOW_DEF_BY_NAME, workflowName);
            if(logger.isDebugEnabled())
            {
            	logger.debug(msg, ae);
            }
            throw new WorkflowException(msg, ae);
        }
    }

    /**
    * {@inheritDoc}
    */
    public byte[] getDefinitionImage(String workflowDefinitionId)
    {
       try
       {
           String processDefinitionId = createLocalId(workflowDefinitionId);
           ProcessDefinition processDefinition = repoService.createProcessDefinitionQuery()
               .processDefinitionId(processDefinitionId)
               .singleResult();
           
           if(processDefinition == null)
           {
               throw new WorkflowException(messageService.getMessage(ERR_GET_DEF_UNEXISTING_IMAGE, workflowDefinitionId));
           }
           
           String diagramResourceName = ((ReadOnlyProcessDefinition)processDefinition).getDiagramResourceName();
           if(diagramResourceName != null)
           {
               ByteArrayOutputStream out = new ByteArrayOutputStream();
               InputStream resourceInputStream = repoService.getResourceAsStream(processDefinitionId, diagramResourceName);
               // Write the resource to a ByteArrayOutpurStream
               IOUtils.copy(resourceInputStream, out);
               return out.toByteArray();
           }
           // No image was found for the process definition
           return null;
       }
       catch(IOException ioe)
       {
           String msg = messageService.getMessage(ERR_GET_DEF_IMAGE, workflowDefinitionId);
           if(logger.isDebugEnabled())
           {
               logger.debug(msg, ioe);
           }
           throw new WorkflowException(msg, ioe);
       }
       catch(ActivitiException ae)
       {
           String msg = messageService.getMessage(ERR_GET_DEF_IMAGE, workflowDefinitionId);
           if(logger.isDebugEnabled())
           {
               logger.debug(msg, ae);
           }
           throw new WorkflowException(msg, ae);
       }
    }

    /**
    * {@inheritDoc}
    */
    public List<WorkflowDefinition> getDefinitions()
    {
        try 
        {
            ProcessDefinitionQuery query = repoService.createProcessDefinitionQuery().latestVersion();
            if(activitiUtil.isMultiTenantWorkflowDeploymentEnabled() && !TenantUtil.isCurrentDomainDefault()) 
            {
                query.processDefinitionKeyLike("@" + TenantUtil.getCurrentDomain() + "%");
            }
            return getValidWorkflowDefinitions(query.list());
        }
        catch (ActivitiException ae)
        {
            String msg = messageService.getMessage(ERR_GET_WORKFLOW_DEF);
            if(logger.isDebugEnabled())
            {
            	logger.debug(msg, ae);
            }
            throw new WorkflowException(msg, ae);
        }
    }

    /**
    * {@inheritDoc}
    */
    public Map<QName, Serializable> getPathProperties(String pathId)
    {
        String executionId = createLocalId(pathId);
        return propertyConverter.getPathProperties(executionId);
    }

    /**
    * {@inheritDoc}
    */
    public List<WorkflowTaskDefinition> getTaskDefinitions(String workflowDefinitionId)
    {
        List<WorkflowTaskDefinition> defs = new ArrayList<WorkflowTaskDefinition>();
        String processDefinitionId = createLocalId(workflowDefinitionId);
        
        // This should return all task definitions, including the start-task
        ReadOnlyProcessDefinition processDefinition =((RepositoryServiceImpl)repoService).getDeployedProcessDefinition(processDefinitionId);

        String processName = ((ProcessDefinition)processDefinition).getKey();
        factory.checkDomain(processName);
        
        // Process start task definition
        PvmActivity startEvent = processDefinition.getInitial();
        
        String startTaskName = null;
        StartFormData startFormData = formService.getStartFormData(processDefinition.getId());
        if(startFormData != null) 
        {
            startTaskName = startFormData.getFormKey();
        }
        
        // Add start task definition
        defs.add(typeConverter.getTaskDefinition(startEvent, startTaskName, processDefinition.getId(), true));
        
        // Now, continue through process, finding all user-tasks
        Collection<PvmActivity> taskActivities = typeConverter.findUserTasks(startEvent);
        for(PvmActivity act : taskActivities)
        {
            String formKey = typeConverter.getFormKey(act, processDefinition);
            defs.add(typeConverter.getTaskDefinition(act, formKey, processDefinition.getId(), false));
        }
        
       return defs;
    }
    
    private String getFormKey(PvmActivity act)
    {
        if(act instanceof ActivityImpl) 
        {
            ActivityImpl actImpl = (ActivityImpl) act;
            if (actImpl.getActivityBehavior() instanceof UserTaskActivityBehavior)        
            {
                UserTaskActivityBehavior uta = (UserTaskActivityBehavior) actImpl.getActivityBehavior();
                TaskFormHandler handler = uta.getTaskDefinition().getTaskFormHandler();
                if(handler != null && handler instanceof DefaultTaskFormHandler)
                {
                    // We cast to DefaultTaskFormHandler since we do not configure our own
                    return ((DefaultTaskFormHandler)handler).getFormKey().getExpressionText();
                }
                
            }
        }
        return null;
    }

    private boolean isReceiveTask(PvmActivity act)
    {
        if(act instanceof ActivityImpl) 
        {
            ActivityImpl actImpl = (ActivityImpl) act;
            return (actImpl.getActivityBehavior() instanceof ReceiveTaskActivityBehavior);        
        }
        return false;
    }

    private boolean isFirstActivity(PvmActivity activity, ReadOnlyProcessDefinition procDef)
    {
        if(procDef.getInitial().getOutgoingTransitions().size() == 1) 
        {
            if (procDef.getInitial().getOutgoingTransitions().get(0).getDestination().equals(activity)) 
            {
                return true;
            }
        }
        return false;
    }

    /**
    * {@inheritDoc}
    */
    public List<WorkflowTask> getTasksForWorkflowPath(String pathId)
    {
        try
        {
            // Extract the Activiti ID from the path
            String executionId = createLocalId(pathId);
            if (executionId == null)
            {
                throw new WorkflowException(messageService.getMessage(ERR_GET_WORKFLOW_TOKEN_INVALID, pathId));
            }

            // Check if the execution exists
            Execution execution = runtimeService.createExecutionQuery().executionId(executionId).singleResult();
            if (execution == null)
            {
                throw new WorkflowException(messageService.getMessage(ERR_GET_WORKFLOW_TOKEN_NULL, pathId));
            }

            String processInstanceId = execution.getProcessInstanceId();
            ArrayList<WorkflowTask> resultList = new ArrayList<WorkflowTask>();
            if(!activitiUtil.isMultiTenantWorkflowDeploymentEnabled() && false == typeConverter.isCorrectTenantRuntime(processInstanceId))
            {
                return resultList; //Wrong tenant
            }
            // Check if workflow's start task has been completed. If not, return
            // the virtual task
            // Otherwise, just return the runtime Activiti tasks
            if (typeConverter.isStartTaskActive(processInstanceId))
            {
                resultList.add(typeConverter.getVirtualStartTask(processInstanceId, true));
            }
            else
            {
                List<Task> tasks = taskService.createTaskQuery().executionId(executionId).list();
                for (Task task : tasks)
                {
                    resultList.add(typeConverter.convert(task));
                }
            }
            return resultList;
        }
        catch (ActivitiException ae)
        {
        	
            String msg = messageService.getMessage(ERR_GET_TASKS_FOR_PATH, pathId);
            if(logger.isDebugEnabled())
            {
            	logger.debug(msg, ae);
            }
            throw new WorkflowException(msg, ae);
        }
    }


    /**
    * {@inheritDoc}
    */
    public List<WorkflowTimer> getTimers(String workflowId)
    {
         try 
         {
             List<WorkflowTimer> timers = new ArrayList<WorkflowTimer>();
             
             String processInstanceId = createLocalId(workflowId);
             List<Job> timerJobs = managementService.createJobQuery()
                 .processInstanceId(processInstanceId)
                 .timers()
                 .list();
             
             // Only fetch process-instance when timers are available, to prevent extra unneeded query
             ProcessInstance jobsProcessInstance = null;
             if(timerJobs.size() > 0)
             {
                 // Reuse the process-instance, is used from WorkflowPath creation
                 jobsProcessInstance = runtimeService.createProcessInstanceQuery()
                      .processInstanceId(processInstanceId).singleResult();
             }
             
             // Convert the timerJobs to WorkflowTimers
             for(Job job : timerJobs)
             {
                 Execution jobExecution = runtimeService.createExecutionQuery()
                     .executionId(job.getExecutionId()).singleResult();
                 
                 WorkflowPath path = typeConverter.convert(jobExecution, jobsProcessInstance);
                 WorkflowTask workflowTask = getTaskForTimer(job, jobsProcessInstance, jobExecution);
                 
                 WorkflowTimer workflowTimer = factory.createWorkflowTimer(job.getId(), job.getId(),
                         job.getExceptionMessage(), job.getDuedate(), path, workflowTask);
                 timers.add(workflowTimer);
             }
             
             return timers;
             
         }
         catch (ActivitiException ae)
         {
             String msg = messageService.getMessage(ERR_GET_TIMERS, workflowId);
             if(logger.isDebugEnabled())
             {
             	logger.debug(msg, ae);
             }
             throw new WorkflowException(msg, ae);
         }
    }

    private WorkflowTask getTaskForTimer(Job job, ProcessInstance processInstance, Execution jobExecution) 
    {
        if (job instanceof TimerEntity) 
        {
            ReadOnlyProcessDefinition def = activitiUtil.getDeployedProcessDefinition(processInstance.getProcessDefinitionId());
            List<String> activeActivityIds = runtimeService.getActiveActivityIds(jobExecution.getId());
            
            if(activeActivityIds.size() == 1)
            {
                PvmActivity targetActivity = def.findActivity(activeActivityIds.get(0));
                if(targetActivity != null)
                {
                    // Only get tasks of active activity is a user-task 
                    String activityType = (String) targetActivity.getProperty(ActivitiConstants.NODE_TYPE);
                    if(ActivitiConstants.USER_TASK_NODE_TYPE.equals(activityType))
                    {
                        Task task = taskService.createTaskQuery().executionId(job.getExecutionId()).singleResult();
                        return typeConverter.convert(task);
                    }
                }
            }
        }
        return null;
    }

    /**
    * {@inheritDoc}
    */
    public WorkflowInstance getWorkflowById(String workflowId)
    {
        try 
        {
            WorkflowInstance instance = null;
            
            String processInstanceId = createLocalId(workflowId);
            ProcessInstance processIntance = runtimeService.createProcessInstanceQuery()
                .processInstanceId(processInstanceId)
                .singleResult();
            
            if(processIntance != null) 
            {
                instance = typeConverter.convert(processIntance);
            } 
            else 
            {
                // The process instance can be finished
                HistoricProcessInstance historicInstance = historyService
                    .createHistoricProcessInstanceQuery()
                    .processInstanceId(processInstanceId)
                    .singleResult();
                
                if(historicInstance != null) 
                {
                    instance = typeConverter.convert(historicInstance);
                }
            }
            return instance;
        }
        catch (ActivitiException ae)
        {
            String msg = messageService.getMessage(ERR_GET_WORKFLOW_DEF);
            if(logger.isDebugEnabled())
            {
            	logger.debug(msg, ae);
            }
            throw new WorkflowException(msg, ae);
        }
    }

    /**
    * {@inheritDoc}
    */
    public List<WorkflowPath> getWorkflowPaths(String workflowId)
    {
        try 
        {           
            String processInstanceId = createLocalId(workflowId);
            
            List<Execution> executions = runtimeService
                .createExecutionQuery()
                .processInstanceId(processInstanceId)
                .list();
            
            return typeConverter.convertExecution(executions);
        }
        catch (ActivitiException ae)
        {
            String msg = messageService.getMessage(ERR_GET_WORKFLOW_PATHS);
            if(logger.isDebugEnabled())
            {
            	logger.debug(msg, ae);
            }
            throw new WorkflowException(msg, ae);
        }
    }

    /**
    * {@inheritDoc}
    */
    public List<WorkflowInstance> getWorkflows(String workflowDefinitionId)
    {
        try
        {
            return getWorkflows(new WorkflowInstanceQuery(workflowDefinitionId));
        }
        catch(ActivitiException ae) 
        {
            String message = messageService.getMessage(ERR_GET_WORKFLOW_INSTS, workflowDefinitionId);
            if(logger.isDebugEnabled())
            {
            	logger.debug(message, ae);
            }
            throw new WorkflowException(message, ae);
        }
    }

    /**
    * {@inheritDoc}
    */
    public boolean isDefinitionDeployed(InputStream workflowDefinition, String mimetype)
    {
        try
        {
            String key = getProcessKey(workflowDefinition);
            return null != activitiUtil.getProcessDefinitionByKey(key);
        }
        catch (Exception ae)
        {
            String msg = messageService.getMessage(ERR_IS_WORKFLOW_DEPLOYED);
            if(logger.isDebugEnabled())
            {
            	logger.debug(msg, ae);
            }
            throw new WorkflowException(msg, ae);
        }
    }

    private String getProcessKey(InputStream workflowDefinition) throws Exception
    {
        try 
        {
            InputSource inputSource = new InputSource(workflowDefinition);
            DOMParser parser = new DOMParser();
            parser.parse(inputSource);
            Document document = parser.getDocument();
            NodeList elemnts = document.getElementsByTagName("process");
            if (elemnts.getLength() < 1)
            {
                throw new IllegalArgumentException("The input stream does not contain a process definition!");
            }
            NamedNodeMap attributes = elemnts.item(0).getAttributes();
            Node idAttrib = attributes.getNamedItem("id");
            if (idAttrib == null)
            {
                throw new IllegalAccessError("The process definition does not have an id!");
            }
            
            if(activitiUtil.isMultiTenantWorkflowDeploymentEnabled())
            {
                // Workflow-definition is deployed tenant-aware, key should be altered
                return factory.getDomainProcessKey(idAttrib.getNodeValue());
            }
            else
            {
                return idAttrib.getNodeValue();
            }
        }
        finally
        {
            workflowDefinition.close();
        }
    }

    /**
    * {@inheritDoc}
    */
    public WorkflowPath signal(String pathId, String transitionId)
    {
        String execId = createLocalId(pathId);
        Execution oldExecution = activitiUtil.getExecution(execId);
        runtimeService.signal(execId);
        Execution execution = activitiUtil.getExecution(execId);
        if(execution !=null)
        {
            return typeConverter.convert(execution);
        }
        return typeConverter.buildCompletedPath(execId, oldExecution.getProcessInstanceId());
    }

    /**
    * {@inheritDoc}
    */
    public WorkflowPath startWorkflow(String workflowDefinitionId, Map<QName, Serializable> parameters)
    {
        try
        {
            String currentUserName = AuthenticationUtil.getFullyAuthenticatedUser();
            Authentication.setAuthenticatedUserId(currentUserName);
            
            String processDefId = createLocalId(workflowDefinitionId);
            
            // Set start task properties. This should be done before instance is started, since it's id will be used
            Map<String, Object> variables = propertyConverter.getStartVariables(processDefId, parameters);
            variables.put(WorkflowConstants.PROP_CANCELLED, Boolean.FALSE);
            
            // Add company home
            Object companyHome = nodeConverter.convertNode(getCompanyHome());
            variables.put(WorkflowConstants.PROP_COMPANY_HOME, companyHome);
             
            // Add the initiator
            NodeRef initiator = getPersonNodeRef(currentUserName);
            if (initiator != null)
            {
                variables.put(WorkflowConstants.PROP_INITIATOR, nodeConverter.convertNode(initiator));
                // Also add the initiator home reference, if one exists
                NodeRef initiatorHome = (NodeRef) nodeService.getProperty(initiator, ContentModel.PROP_HOMEFOLDER);
                if (initiatorHome != null)
                {
                    variables.put(WorkflowConstants.PROP_INITIATOR_HOME, nodeConverter.convertNode(initiatorHome));
                }
            }
            
            // Start the process-instance
            CommandContext context = Context.getCommandContext();
            boolean isContextSuspended = false;
            if (context != null && context.getException() == null)
            {
                // MNT-11926: push null context to stack to avoid context reusage when new instance is not flushed
                Context.setCommandContext(null);
                isContextSuspended = true;
            }
            try
            {
                ProcessInstance instance = runtimeService.startProcessInstanceById(processDefId, variables);
                if (instance.isEnded())
                {
                    return typeConverter.buildCompletedPath(instance.getId(), instance.getId());
                }
                else
                {
                    WorkflowPath path = typeConverter.convert((Execution) instance);
                    endStartTaskAutomatically(path, instance);
                    return path;
                }
            }
            finally
            {
                if (isContextSuspended)
                {
                    // pop null context out of stack
                    Context.removeCommandContext();
                }
            }
        }
        catch (ActivitiException ae)
        {
            String msg = messageService.getMessage(ERR_START_WORKFLOW, workflowDefinitionId);
            if(logger.isDebugEnabled())
            {
            	logger.debug(msg, ae);
            }
            throw new WorkflowException(msg, ae);
        }
    }

    /**
     * @param path WorkflowPath
     * @param instance ProcessInstance
     */
    private void endStartTaskAutomatically(WorkflowPath path, ProcessInstance instance)
    {
        // Check if StartTask Needs to be ended automatically
        WorkflowDefinition definition = path.getInstance().getDefinition();
        TypeDefinition metadata = definition.getStartTaskDefinition().getMetadata();
        Set<QName> aspects = metadata.getDefaultAspectNames();
        if(aspects.contains(WorkflowModel.ASPECT_END_AUTOMATICALLY))
        {
            String taskId = ActivitiConstants.START_TASK_PREFIX + instance.getId();
            endStartTask(taskId);
        }
    }
    
    /**
    * {@inheritDoc}
    */
    public void undeployDefinition(String workflowDefinitionId)
    {
        try 
        {
            String procDefId = createLocalId(workflowDefinitionId);
            ProcessDefinition procDef = repoService.createProcessDefinitionQuery()
                .processDefinitionId(procDefId)
                .singleResult();
            if (procDef == null)     
            {
                String msg = messageService.getMessage(ERR_UNDEPLOY_WORKFLOW_UNEXISTING, workflowDefinitionId);
                throw new WorkflowException(msg);
            }
            String deploymentId = procDef.getDeploymentId();
            repoService.deleteDeployment(deploymentId);
        }
        catch (ActivitiException ae)
        {
            String msg = messageService.getMessage(ERR_UNDEPLOY_WORKFLOW, workflowDefinitionId);
            if(logger.isDebugEnabled())
            {
            	logger.debug(msg, ae);
            }
            throw new WorkflowException(msg, ae);
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean hasWorkflowImage(String workflowInstanceId)
    {
        boolean hasImage = false;
        
        String processInstanceId = createLocalId(workflowInstanceId);
        ExecutionEntity pi = (ExecutionEntity) runtimeService.createProcessInstanceQuery()
                    .processInstanceId(processInstanceId).singleResult();

        // If the process is finished, there is no diagram available
        if (pi != null)
        {
            // Fetch the process-definition. Not using query API, since the returned
            // processdefinition isn't initialized with all activities
            ProcessDefinitionEntity processDefinition = (ProcessDefinitionEntity) ((RepositoryServiceImpl) repoService)
                        .getDeployedProcessDefinition(pi.getProcessDefinitionId());

            hasImage = (processDefinition != null && processDefinition.isGraphicalNotationDefined()); 
        }
        
        return hasImage;
    }
    
    /**
     * {@inheritDoc}
     */
    public InputStream getWorkflowImage(String workflowInstanceId)
    {
        String processInstanceId = createLocalId(workflowInstanceId);
        ExecutionEntity pi = (ExecutionEntity) runtimeService.createProcessInstanceQuery()
                    .processInstanceId(processInstanceId).singleResult();

        // If the process is finished, there is no diagram available
        if (pi != null)
        {
            // Fetch the bpmn model
            BpmnModel model = repoService.getBpmnModel(pi.getProcessDefinitionId());

            if (model != null && model.getLocationMap().size() > 0) 
            {              
                ProcessDiagramGenerator generator = new DefaultProcessDiagramGenerator();
                return generator.generateDiagram(model,
                      ActivitiConstants.PROCESS_INSTANCE_IMAGE_FORMAT,
                      runtimeService.getActiveActivityIds(processInstanceId)); 
            }
        }
        return null;
    }

    /**
     * Converts the given list of {@link ProcessDefinition}s to a list of {@link WorkflowDefinition}s
     * that have a valid domain.
     * @param definitions List<ProcessDefinition>
     */
    private List<WorkflowDefinition> getValidWorkflowDefinitions(List<ProcessDefinition> definitions)
    {
        return typeConverter.filterByDomainAndConvert(definitions, new Function<ProcessDefinition, String>()
        {
            public String apply(ProcessDefinition value)
            {
                return value.getKey();
            }
        });
    }
    
    /**
     * Converts the given list of {@link Task}s to a list of
     * {@link WorkflowTask}s that have a valid domain.
     * 
     * Note that this method is not very performant. It will do many DB queries
     * for each of the Task item in the list of tasks as parameter. Make sure
     * that you limit the size of the tasks list depending on your needs. Hint:
     * use the WorkflowTaskQuery query parameter setLimit() method to limit the
     * number of Tasks to process
     * 
     * @param tasks List<Task>
     */
    private List<WorkflowTask> getValidWorkflowTasks(List<Task> tasks)
    {
        return typeConverter.filterByDomainAndConvert(tasks, new Function<Task, String>()
        {
            public String apply(Task task)
            {
                //TODO This probably isn't very performant!
                String defId = task.getProcessDefinitionId();
                ProcessDefinition definition = repoService.createProcessDefinitionQuery().processDefinitionId(defId)
                        .singleResult();
                return definition.getKey();
            }
        });
    }

    /**
     * Converts the given list of {@link Task}s to a list of {@link WorkflowTask}s
     * that have a valid domain.
     * @param tasks List<HistoricTaskInstance>
     */
    private List<WorkflowTask> getValidHistoricTasks(List<HistoricTaskInstance> tasks)
    {
        return typeConverter.filterByDomainAndConvert(tasks, new Function<HistoricTaskInstance, String>()
        {
            public String apply(HistoricTaskInstance task)
            {
                String defId = task.getProcessDefinitionId();
                ProcessDefinition definition = (ProcessDefinition) activitiUtil.getDeployedProcessDefinition(defId);
                return definition.getKey();
            }
        });
    }

    /**
     * Gets the Company Home
     * 
     * @return company home node ref
     */
    private NodeRef getCompanyHome()
    {
        return repositoryHelper.getCompanyHome();
    }
    
    /**
     * Gets an Alfresco Person reference for the given name.
     * 
     * @param name the person name
     * @return the Alfresco person. Returns null, if no person is found with the
     *         given name.
     */
    private NodeRef getPersonNodeRef(String name)
    {
        NodeRef authority = null;
        if (name != null)
        {
            if (personService.personExists(name))
            {
                authority = personService.getPerson(name);
            }
        }
        return authority;
    }
 
    /**
     * @param propertyConverter the propertyConverter to set
     */
    public void setPropertyConverter(ActivitiPropertyConverter propertyConverter)
    {
        this.propertyConverter = propertyConverter;
    }
    
    /**
     * Sets the Dictionary Service
     * @param dictionaryService DictionaryService
     */
    public void setDictionaryService(DictionaryService dictionaryService)
    {
        this.dictionaryService = dictionaryService;
    }

    /**
     * Sets the Node Service
     * 
     * @param nodeService NodeService
     */
    public void setNodeService(NodeService nodeService)
    {
        this.nodeService = nodeService;
    }

    /**
     * @param repositoryHelper the repositoryHelper to set
     */
    public void setRepositoryHelper(Repository repositoryHelper)
    {
        this.repositoryHelper = repositoryHelper;
    }
    
    /**
     * Sets the Person Service
     * 
     * @param personService PersonService
     */
    public void setPersonService(PersonService personService)
    {
        this.personService = personService;
    }
    
    /**
     * Sets the Authority DAO
    /**
     * @param authorityManager the authorityManager to set
     */
    public void setAuthorityManager(WorkflowAuthorityManager authorityManager)
    {
        this.authorityManager = authorityManager;
    }

    ///////////// Task Component //////////
    
    /**
    * {@inheritDoc}
    */
    public WorkflowTask endTask(String taskId, String transition)
    {
        String localTaskId = createLocalId(taskId);
        // Check if the task is a virtual start task
        if(localTaskId.startsWith(ActivitiConstants.START_TASK_PREFIX))
        {
            return endStartTask(localTaskId);
        }
        
        return endNormalTask(taskId, localTaskId, transition);
    }

    private WorkflowTask endNormalTask(String taskId, String localTaskId, String transition)
    {
        // Retrieve task
        Task task = taskService.createTaskQuery().taskId(localTaskId).singleResult();
        
        if(task == null)
        {
            String msg = messageService.getMessage(ERR_END_UNEXISTING_TASK, taskId);
            throw new WorkflowException(msg);
        }
        
        // Check if the assignee is equal to the current logged-in user. If not, assign task before ending
        String currentUserName = AuthenticationUtil.getFullyAuthenticatedUser();
        if(task.getAssignee() == null || !task.getAssignee().equals(currentUserName)) {
            taskService.setAssignee(localTaskId, currentUserName);
            // Also update pojo used to set the outcome, this will read assignee as wel
            task.setAssignee(currentUserName);
            
            // Re-fetch the task-entity since it's revision has been updated by the setAssignee() call
            task = taskService.createTaskQuery().taskId(localTaskId).singleResult();
        }
        
        setOutcome(task, transition);
        taskService.complete(localTaskId);
        // The task should have a historicTaskInstance
        HistoricTaskInstance historicTask = historyService.createHistoricTaskInstanceQuery().taskId(task.getId()).singleResult();
        return typeConverter.convert(historicTask);
    }

    private void setOutcome(Task task, String transition)
    {
        String outcomeValue = ActivitiConstants.DEFAULT_TRANSITION_NAME;
        HashMap<QName, Serializable> updates = new HashMap<QName, Serializable>();

        boolean isDefaultTransition = transition == null || ActivitiConstants.DEFAULT_TRANSITION_NAME.equals(transition);

        Map<QName, Serializable> properties = propertyConverter.getTaskProperties(task);
        QName outcomePropName = (QName) properties.get(WorkflowModel.PROP_OUTCOME_PROPERTY_NAME);
        if(outcomePropName !=null)
        {
            if(isDefaultTransition == false)
            {
                outcomeValue = transition;
                Serializable transitionValue = propertyConverter.convertValueToPropertyType(task, transition, outcomePropName);
                updates.put(outcomePropName, transitionValue);
            }
            else
            {
                Serializable rawOutcome = properties.get(outcomePropName);
                if(rawOutcome != null)
                {
                    outcomeValue = DefaultTypeConverter.INSTANCE.convert(String.class, rawOutcome);
                }
            }
        }
        else if (isDefaultTransition==false)
        {
            // Only 'Next' is supported as transition.
            String taskId = createGlobalId(task.getId());
            String msg = messageService.getMessage(ERR_END_TASK_INVALID_TRANSITION, transition, taskId, ActivitiConstants.DEFAULT_TRANSITION_NAME);
            throw new WorkflowException(msg);
        }
        updates.put(WorkflowModel.PROP_OUTCOME, outcomeValue);
        propertyConverter.updateTask(task, updates, null, null);
    }

    private WorkflowTask endStartTask(String localTaskId)
    {
        // We don't end a task, we set a variable on the process-instance 
        // to indicate that it's started
        String processInstanceId = localTaskId.replace(ActivitiConstants.START_TASK_PREFIX, "");
        if(false == typeConverter.isStartTaskActive(processInstanceId))
        {
            return typeConverter.getVirtualStartTask(processInstanceId, false);
        }
        
        // Set start task end date on the process
        runtimeService.setVariable(processInstanceId, ActivitiConstants.PROP_START_TASK_END_DATE, new Date());
        
        // Check if the current activity is a signalTask and the first activity in the process,
        // this is a workaround for processes without any task/waitstates that should otherwise end
        // when they are started.
        ProcessInstance processInstance = activitiUtil.getProcessInstance(processInstanceId);
        String currentActivity = ((ExecutionEntity)processInstance).getActivityId();
        
        ReadOnlyProcessDefinition procDef = activitiUtil.getDeployedProcessDefinition(processInstance.getProcessDefinitionId());
        PvmActivity activity = procDef.findActivity(currentActivity);
        if(isReceiveTask(activity) && isFirstActivity(activity, procDef)) 
        {
            // Signal the process to start flowing, beginning from the recieve task
            runtimeService.signal(processInstanceId);
            
            // It's possible the process has ended after signalling the receive task
        }
        // Return virtual start task for the execution, it's safe to use the
        // processInstanceId
        return typeConverter.getVirtualStartTask(processInstanceId, false);
    }

    /**
    * {@inheritDoc}
    */
    public List<WorkflowTask> getAssignedTasks(String authority, WorkflowTaskState state, boolean lazyInitialization)
    {
        try
        {
            if(state == WorkflowTaskState.IN_PROGRESS)
            {
                TaskQuery taskQuery = taskService.createTaskQuery()
                    .taskAssignee(authority);
                
                if(!activitiUtil.isMultiTenantWorkflowDeploymentEnabled())
                {
                    taskQuery.processVariableValueEquals(ActivitiConstants.VAR_TENANT_DOMAIN, TenantUtil.getCurrentDomain());
                }
                List<Task> tasks = taskQuery.list();
                
                List<WorkflowTask> resultingTasks = new ArrayList<WorkflowTask>();
                for(Task task : tasks) {
                    
                    if(lazyInitialization)
                    {
                        resultingTasks.add(new LazyActivitiWorkflowTask(task, typeConverter, tenantService, 
                                typeConverter.getWorkflowDefinitionName(task.getProcessDefinitionId())));
                    }
                    else
                    {
                        resultingTasks.add(typeConverter.convert(task));
                    }
                }
                return resultingTasks;
            }
            else
            {
                HistoricTaskInstanceQuery taskQuery = historyService.createHistoricTaskInstanceQuery()
                    .taskAssignee(authority)
                    .finished();
                
                if(!activitiUtil.isMultiTenantWorkflowDeploymentEnabled())
                {
                    taskQuery.processVariableValueEquals(ActivitiConstants.VAR_TENANT_DOMAIN, TenantUtil.getCurrentDomain());
                }
                List<HistoricTaskInstance> historicTasks =taskQuery.list();
                
                List<WorkflowTask> resultingTasks = new ArrayList<WorkflowTask>();
                for(HistoricTaskInstance historicTask : historicTasks) {
                    
                    if(lazyInitialization)
                    {
                        resultingTasks.add(new LazyActivitiWorkflowTask(historicTask, typeConverter, tenantService));
                    }
                    else
                    {
                        resultingTasks.add(typeConverter.convert(historicTask));
                    }
                }
                return resultingTasks;
            }
        }
        catch (ActivitiException ae)
        {
            String msg = messageService.getMessage(ERR_GET_ASSIGNED_TASKS);
            if(logger.isDebugEnabled())
            {
            	logger.debug(msg, ae);
            }
            throw new WorkflowException(msg, ae);
        }
    }

    /**
    * {@inheritDoc}
    */
    public List<WorkflowTask> getPooledTasks(List<String> authorities, boolean lazyInitialization)
    {
        try 
        {
            if (authorities != null && authorities.size() > 0) 
            {
                // As an optimisation, we assume the first authority CAN be a user. All the 
                // others are groups to which the user (or group) belongs. This way, we don't have to
                // check for the type of the authority.
                
                String firstAuthority = authorities.get(0);
                // Use a map, can be that a task has multiple candidate-groups, which are inside the list
                // of authorities
                Map<String, Task> resultingTasks = new HashMap<String, Task>();
                if (authorityManager.isUser(firstAuthority))
                {
                    // Candidate user
                    addTasksForCandidateUser(firstAuthority, resultingTasks);
                    if(authorities.size() > 1) {
                        List<String> remainingAuthorities = authorities.subList(1, authorities.size());
                        addTasksForCandidateGroups(remainingAuthorities, resultingTasks);
                    }
                } 
                else
                {
                    // Candidate group
                    addTasksForCandidateGroups(authorities, resultingTasks);
                }
                List<WorkflowTask> tasks = new ArrayList<WorkflowTask>();
                WorkflowTask currentTask = null;
                // Only tasks that have NO assignee, should be returned
                for(Task task : resultingTasks.values()) 
                {
                    if(task.getAssignee() == null) 
                    {
                        // ALF-12264: filter out tasks from other domain, can occur when tenants
                        // have a group with the same name
                        if(lazyInitialization)
                        {
                            String workflowDefinitionName = typeConverter.getWorkflowDefinitionName(task.getProcessDefinitionId());
                            try
                            {
                                workflowDefinitionName = tenantService.getBaseName(workflowDefinitionName);
                                currentTask = new LazyActivitiWorkflowTask(task, typeConverter, tenantService, workflowDefinitionName);
                            }
                            catch(RuntimeException re)
                            {
                                // Domain mismatch, don't use this task
                                currentTask = null;
                            }
                        }
                        else
                        {
                            currentTask = typeConverter.convert(task, true);
                        }
                        if(currentTask != null)
                        {
                            tasks.add(currentTask);
                        }
                    }
                }
                return tasks;
            }
            
            return Collections.emptyList();
        }
        catch(ActivitiException ae)
        {
            String authorityString = null;
            if(authorities != null)
            {
                authorityString = StringUtils.join(authorities.iterator(), ", ");
            }
            String msg = messageService.getMessage(ERR_GET_POOLED_TASKS, authorityString);
            if(logger.isDebugEnabled())
            {
            	logger.debug(msg, ae);
            }
            throw new WorkflowException(msg, ae);
        }
    }

    private void addTasksForCandidateGroups(List<String> groupNames, Map<String, Task> resultingTasks)
    {
        if(groupNames != null && groupNames.size() > 0) {
            
            TaskQuery query = taskService.createTaskQuery().taskCandidateGroupIn(groupNames);
            
            // Additional filtering on the tenant-property in case workflow-definitions are shared across tenants
            if(!activitiUtil.isMultiTenantWorkflowDeploymentEnabled() && tenantService.isEnabled()) {
                query.processVariableValueEquals(ActivitiConstants.VAR_TENANT_DOMAIN, TenantUtil.getCurrentDomain());
            }
            
            List<Task> tasks =query.list();
            for(Task task : tasks)
            {
                resultingTasks.put(task.getId(), task);
            }
        }
    }

    private void addTasksForCandidateUser(String userName, Map<String, Task> resultingTasks)
    {
        TaskQuery query = taskService.createTaskQuery().taskCandidateUser(userName);
        
        // Additional filtering on the tenant-property in case workflow-definitions are shared across tenants
        if(!activitiUtil.isMultiTenantWorkflowDeploymentEnabled() && tenantService.isEnabled()) {
            query.processVariableValueEquals(ActivitiConstants.VAR_TENANT_DOMAIN, TenantUtil.getCurrentDomain());
        }
        
        List<Task> tasks = query.list();
        for(Task task : tasks)
        {
            resultingTasks.put(task.getId(), task);
        }
    }

    /**
    * {@inheritDoc}
    */
    public WorkflowTask getTaskById(String taskId)
    {
        try
        {
            String localId = createLocalId(taskId);
            if(localId.startsWith(ActivitiConstants.START_TASK_PREFIX)) 
            {
                String processInstanceId = localId.replace(ActivitiConstants.START_TASK_PREFIX ,"");
                return typeConverter.getVirtualStartTask(processInstanceId, null);
            } 
            else
            {
                Task task = activitiUtil.getTaskInstance(localId);
                if(task != null) 
                {
                    return typeConverter.convert(task);
                }
                HistoricTaskInstance historicTask = activitiUtil.getHistoricTaskInstance(localId);
                return typeConverter.convert(historicTask);
            }
        }
        catch (ActivitiException ae)
        {
            String msg = messageService.getMessage(ERR_GET_TASK_BY_ID);
            if(logger.isDebugEnabled())
            {
            	logger.debug(msg, ae);
            }
            throw new WorkflowException(msg, ae);
        }
    }

    
    /* (non-Javadoc)
     * @see org.alfresco.repo.workflow.TaskComponent#queryTasks(org.alfresco.service.cmr.workflow.WorkflowTaskQuery, boolean)
     */
    @Override
    public List<WorkflowTask> queryTasks(WorkflowTaskQuery query, boolean sameSession)
    {
        return queryTasks(query);
    }
    
    @Override
    public long countTasks(WorkflowTaskQuery query) {
        long totalCount = 0;
        
        WorkflowTaskState taskState = query.getTaskState();
        if(WorkflowTaskState.COMPLETED.equals(taskState) == false)
        {
           totalCount += createRuntimeTaskQuery(query).count();    
        }   
        
         // Depending on the state, history should be included/excluded as well
        if(WorkflowTaskState.IN_PROGRESS.equals(taskState) == false)
        {
            totalCount += createHistoricTaskQuery(query).count();
        }
        return totalCount;
    }

    /**
     * {@inheritDoc}
     */
     public List<WorkflowTask> queryTasks(WorkflowTaskQuery query)
     {
         ArrayList<WorkflowTask> result = new ArrayList<WorkflowTask>();
         WorkflowTaskState taskState = query.getTaskState();
         if(WorkflowTaskState.COMPLETED.equals(taskState) == false)
         {
             result.addAll(queryRuntimeTasks(query));           
         }       
         
         // Depending on the state, history should be included/excluded as wel
         if(WorkflowTaskState.IN_PROGRESS.equals(taskState) == false)
         {
             result.addAll(queryHistoricTasks(query));
             result.addAll(queryStartTasks(query));
         }
         return result;
     }
     
     private List<WorkflowTask> queryRuntimeTasks(WorkflowTaskQuery query)
     {
        // Runtime-tasks only exist on process-instances that are active
        // so no use in querying runtime tasks if not active
        if (!Boolean.FALSE.equals(query.isActive()))
        {
            TaskQuery taskQuery = createRuntimeTaskQuery(query);

            List<Task> results;
            int limit = query.getLimit();
            if (limit > 0)
            {
                results = taskQuery.listPage(0, limit);
            }
            else
            {
                results = taskQuery.list();
            }
            return getValidWorkflowTasks(results);
        }
        return new ArrayList<WorkflowTask>();
     }
     
     private void addProcessPropertiesToQuery(
             Map<QName, Object> processCustomProps, TaskQuery taskQuery) 
     {
         for(Entry<QName, Object> customProperty : processCustomProps.entrySet()) 
         {
             String name =factory.mapQNameToName(customProperty.getKey());

             // Exclude the special "VAR_TENANT_DOMAIN" variable, this cannot be queried by users
             if(name != ActivitiConstants.VAR_TENANT_DOMAIN)
             {
                 // Perform minimal property conversions
                 Object converted = propertyConverter.convertPropertyToValue(customProperty.getValue());
                 taskQuery.processVariableValueEquals(name, converted);
             }
         }
     }
     
     private String getProcessNameMTSafe(QName processNameQName)
     {
         String key = processNameQName.toPrefixString(namespaceService);
         return factory.getProcessKey(key);
     }
     
     private void orderQuery(TaskQuery taskQuery, OrderBy[] orderBy) 
     {
         for (WorkflowTaskQuery.OrderBy orderByPart : orderBy)
         {
             if (orderByPart == WorkflowTaskQuery.OrderBy.TaskActor_Asc)
             {
                 taskQuery.orderByTaskAssignee().asc();
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskActor_Desc)
             {
                 taskQuery.orderByTaskAssignee().desc();
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskCreated_Asc)
             {
                 taskQuery.orderByTaskCreateTime().asc();
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskCreated_Desc)
             {
                 taskQuery.orderByTaskCreateTime().desc();
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskDue_Asc)
             {
                 // TODO: order by dueDate? It's a task-variable
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskDue_Desc)
             {
                 // TODO: order by duedate? It's a task-variable
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskId_Asc)
             {
                 taskQuery.orderByTaskId().asc();
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskId_Desc)
             {
                 taskQuery.orderByTaskId().desc();
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskName_Asc)
             {
                 taskQuery.orderByTaskName().asc();
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskName_Desc)
             {
                 taskQuery.orderByTaskName().desc();
             }
             // All workflows are active, no need to order on WorkflowTaskQuery.OrderBy.TaskState_Asc
         }
     }
     
     private void orderQuery(HistoricTaskInstanceQuery taskQuery, OrderBy[] orderBy) 
     {
         for (WorkflowTaskQuery.OrderBy orderByPart : orderBy)
         {
             if (orderByPart == WorkflowTaskQuery.OrderBy.TaskActor_Asc)
             {
                 taskQuery.orderByTaskAssignee().asc();
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskActor_Desc)
             {
                 taskQuery.orderByTaskAssignee().desc();
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskCreated_Asc)
             {
                 taskQuery.orderByHistoricActivityInstanceStartTime().asc();
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskCreated_Desc)
             {
                 taskQuery.orderByHistoricActivityInstanceStartTime().desc();
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskDue_Asc)
             {
                 // TODO: order by dueDate? It's a task-variable
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskDue_Desc)
             {
                 // TODO: order by duedate? It's a task-variable
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskId_Asc)
             {
                 taskQuery.orderByTaskId().asc();
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskId_Desc)
             {
                 taskQuery.orderByTaskId().desc();
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskName_Asc)
             {
                 taskQuery.orderByTaskName().asc();
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskName_Desc)
             {
                 taskQuery.orderByTaskName().desc();
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskState_Asc)
             {
                 taskQuery.orderByHistoricTaskInstanceEndTime().asc();
             }
             else if (orderByPart == WorkflowTaskQuery.OrderBy.TaskState_Desc)
             {
                 taskQuery.orderByHistoricTaskInstanceEndTime().asc();
             }
         }
     }

     private void addTaskPropertiesToQuery(Map<QName, Object> taskCustomProps, TaskQuery taskQuery)
     {
         for(Entry<QName, Object> customProperty : taskCustomProps.entrySet()) 
         {
             String name = factory.mapQNameToName(customProperty.getKey());
             
             // Perform minimal property conversions
             Object converted = propertyConverter.convertPropertyToValue(customProperty.getValue());
             taskQuery.taskVariableValueEquals(name, converted);
         }
     }
     
     private TaskQuery createRuntimeTaskQuery(WorkflowTaskQuery query) 
     {
         // Add task name
         TaskQuery taskQuery = taskService.createTaskQuery();
         
         if (!activitiUtil.isMultiTenantWorkflowDeploymentEnabled())
         {
             // Filter by tenant domain.
             taskQuery.processVariableValueEquals(ActivitiConstants.VAR_TENANT_DOMAIN, TenantUtil.getCurrentDomain());
         } else if(tenantService.isEnabled() && !TenantUtil.isCurrentDomainDefault() && !StringUtils.isEmpty(TenantUtil.getCurrentDomain())) {
             // Process definition keys are prefixed with the tenant ID, in case MT is enabled and
             // deployments are done in tenant-context
             taskQuery.processDefinitionKeyLike("@" + TenantUtil.getCurrentDomain() + "@%");
         }
         
         if (query.getTaskName() != null)
         {
             // Task 'key' is stored as variable on task
             String formKey = query.getTaskName().toPrefixString(namespaceService);
             taskQuery.taskVariableValueEquals(ActivitiConstants.PROP_TASK_FORM_KEY, formKey);
         }

         if (query.getProcessId() != null)
         {
             String processInstanceId = createLocalId(query.getProcessId());
             taskQuery.processInstanceId(processInstanceId);
         }

         if (query.getProcessName() != null)
         {
             String processName = getProcessNameMTSafe(query.getProcessName());
             taskQuery.processDefinitionKey(processName);
         }
         
         if (query.getWorkflowDefinitionName() != null)
         {
             String processName = factory.getProcessKey(query.getWorkflowDefinitionName());
             taskQuery.processDefinitionKey(processName);
         }

         if (query.getActorId() != null)
         {
             taskQuery.taskAssignee(query.getActorId());
         }

         if (query.getTaskId() != null)
         {
             String taskId = createLocalId(query.getTaskId());
             taskQuery.taskId(taskId);
         }

         // Custom task properties
         if (query.getTaskCustomProps() != null)
         {
             addTaskPropertiesToQuery(query.getTaskCustomProps(), taskQuery);
         }

         if (query.getProcessCustomProps() != null)
         {
             addProcessPropertiesToQuery(query.getProcessCustomProps(), taskQuery);
         }
         // Add ordering
         if (query.getOrderBy() != null)
         {
             WorkflowTaskQuery.OrderBy[] orderBy = query.getOrderBy();
             orderQuery(taskQuery, orderBy);
         }
         
         return taskQuery;
     }

     private List<WorkflowTask> queryHistoricTasks(WorkflowTaskQuery query)
     {
         HistoricTaskInstanceQuery historicQuery = createHistoricTaskQuery(query);

        List<HistoricTaskInstance> results;
        int limit = query.getLimit();
        if (limit > 0)
        {
            results = historicQuery.listPage(0, limit);
        }
        else
        {
            results = historicQuery.list();
        }
        return getValidHistoricTasks(results);
     }
     
     private HistoricTaskInstanceQuery createHistoricTaskQuery(WorkflowTaskQuery query) 
     {
         HistoricTaskInstanceQuery historicQuery = historyService.createHistoricTaskInstanceQuery().finished();
         
         if (!activitiUtil.isMultiTenantWorkflowDeploymentEnabled())
         {
             // Filter by tenant domain
             historicQuery.processVariableValueEquals(ActivitiConstants.VAR_TENANT_DOMAIN, TenantUtil.getCurrentDomain());
         } else if(tenantService.isEnabled() && !TenantUtil.isCurrentDomainDefault() && !StringUtils.isEmpty(TenantUtil.getCurrentDomain())) {
             // Process definition keys are prefixed with the tenant ID, in case MT is enabled and
             // deployments are done in tenant-context
             historicQuery.processDefinitionKeyLike("@" + TenantUtil.getCurrentDomain() + "@%");
         }

         if (query.getTaskId() != null)
         {
             String taskId = createLocalId(query.getTaskId());
             historicQuery.taskId(taskId);
         }

         if (query.getProcessId() != null)
         {
             String processInstanceId = createLocalId(query.getProcessId());
             historicQuery.processInstanceId(processInstanceId);
         }

         if (query.getTaskName() != null)
         {
             historicQuery.taskDefinitionKey(query.getTaskName().toPrefixString());
         }

         if (query.getActorId() != null)
         {
             historicQuery.taskAssignee(query.getActorId());
         }

         if (query.getProcessName() != null)
         {
             String processName = getProcessNameMTSafe(query.getProcessName());
             historicQuery.processDefinitionKey(processName);
         }
         
         if (query.getWorkflowDefinitionName() != null)
         {
             String processName = factory.getProcessKey(query.getWorkflowDefinitionName());
             historicQuery.processDefinitionKey(processName);
         }

         if (query.getTaskCustomProps() != null)
         {
             addTaskPropertiesToQuery(query.getTaskCustomProps(), historicQuery);
         }

         if (query.getProcessCustomProps() != null)
         {
             addProcessPropertiesToQuery(query.getProcessCustomProps(), historicQuery);
         }

         if (query.isActive() != null)
         {
             if (query.isActive())
             {
                 historicQuery.processUnfinished();
             }
             else
             {
                 historicQuery.processFinished();
             }
         }

         // Order query
         if (query.getOrderBy() != null)
         {
             orderQuery(historicQuery, query.getOrderBy());
         }
         
         return historicQuery;
     }
     
     private void addTaskPropertiesToQuery(Map<QName, Object> taskCustomProps, 
                 HistoricTaskInstanceQuery taskQuery) 
     {
         for(Entry<QName, Object> customProperty : taskCustomProps.entrySet()) 
         {
             String name =factory.mapQNameToName(customProperty.getKey());
             
             // Perform minimal property conversions
             Object converted = propertyConverter.convertPropertyToValue(customProperty.getValue());
             taskQuery.taskVariableValueEquals(name, converted);
         }
     }
     
     private void addProcessPropertiesToQuery(Map<QName, Object> processCustomProps, 
                 HistoricTaskInstanceQuery taskQuery) 
     {
         for(Entry<QName, Object> customProperty : processCustomProps.entrySet()) 
         {
             String name =factory.mapQNameToName(customProperty.getKey());
             
             // Exclude the special "VAR_TENANT_DOMAIN" variable, this cannot be queried by users
             if(name != ActivitiConstants.VAR_TENANT_DOMAIN)
             {
                 // Perform minimal property conversions
                 Object converted = propertyConverter.convertPropertyToValue(customProperty.getValue());
                 taskQuery.processVariableValueEquals(name, converted);
             }
         }
     }
     
     private List<WorkflowTask> queryStartTasks(WorkflowTaskQuery query)
     {
         List<WorkflowTask> startTasks =  new ArrayList<WorkflowTask>();

         String processInstanceId = null;
         String taskId = query.getTaskId();
         if(taskId != null )
         {
             String localTaskId = createLocalId(taskId);
             if(localTaskId.startsWith(ActivitiConstants.START_TASK_PREFIX))
             processInstanceId = localTaskId.substring(ActivitiConstants.START_TASK_PREFIX.length());
         }
         else
         {
             String processId = query.getProcessId();
             if(processId != null)
             {
                 // Start task for a specific process
                 processInstanceId = createLocalId(processId);
             }
         }
         
         // Only return start-task when a process or task id is set
         if(processInstanceId != null)
         {
             WorkflowTask workflowTask = typeConverter.getVirtualStartTask(processInstanceId, null);
             if(workflowTask != null)
             {
                boolean startTaskMatches = isStartTaskMatching(workflowTask, query);
                if(startTaskMatches)
                {
                    startTasks.add(workflowTask);
                }
             }
         }
         return startTasks;
     }
         
     
    private boolean isStartTaskMatching(WorkflowTask workflowTask, WorkflowTaskQuery query) 
    {
        if(query.isActive() != null)
        {
            if(query.isActive() && !workflowTask.getPath().isActive()) 
            {
                return false;
            }
            if(!query.isActive() && workflowTask.getPath().isActive()) 
            {
                return false;
            }
        }
        
        if(query.getActorId() != null && !query.getActorId().equals(workflowTask.getProperties().get(ContentModel.PROP_OWNER)))
        {
            return false;
        }
    	
        // Do NOT include start-task when the process for the given processId is already ended
        // See MNT-10931
        if(query.getProcessCustomProps() != null &&
                !WorkflowTaskState.COMPLETED.equals(workflowTask.getState()))
        {
            // Get properties for process instance, based on path of start task, which is process-instance
            Map<QName, Serializable> props = getPathProperties(workflowTask.getPath().getId());
            if(!checkPropertiesPresent(query.getProcessCustomProps(), props))
            {
                return false;
            }
        }
            
        if(query.getProcessId() != null)
        {
            if(!query.getProcessId().equals(workflowTask.getPath().getInstance().getId()))
            {
                return false;
            }
        }
        
        // Query by process name deprecated, but still implemented.
        if(query.getProcessName() != null)
        {
            String processName = factory.mapQNameToName(query.getProcessName());
            if(!processName.equals(workflowTask.getPath().getInstance().getDefinition().getName()))
            {
                return false;
            }
        }
        
        if(query.getWorkflowDefinitionName() != null)
        {
            if(!query.getWorkflowDefinitionName().equals(workflowTask.getPath().getInstance().getDefinition().getName()))
            {
                return false;
            }
        }
        
        if(query.getTaskCustomProps() != null)
        {
            if(!checkPropertiesPresent(query.getTaskCustomProps(), workflowTask.getProperties()))
            {
                return false;
            }
        }
        
        if(query.getTaskId() != null)
        {
            if(!query.getTaskId().equals(workflowTask.getId()))
            {
                return false;
            }
        }
        
        if(query.getTaskName() != null)
        {
            if(!query.getTaskName().equals(workflowTask.getDefinition().getMetadata().getName()))
            {
                return false;
            }
        }
        
        if(query.getTaskState() != null)
        {
            if(!query.getTaskState().equals(workflowTask.getState()))
            {
                return false;
            }
        }
        
        // If we fall through, start task matches the query
        return true;
    }
    
    private boolean checkPropertiesPresent(Map<QName, Object> expectedProperties, Map<QName, Serializable> props)
    {
        for(Map.Entry<QName, Object> entry : expectedProperties.entrySet())
        {
            if(props.containsKey(entry.getKey())) 
            {
                Object requiredValue = entry.getValue();
                Object actualValue = props.get(entry.getKey());
                
                if(requiredValue != null)
                {
                    if(!requiredValue.equals(actualValue))
                    {
                        return false;
                    }
                    break;
                }
                else
                {
                    if(actualValue != null)
                    {
                        return false;
                    }
                    break;
                }
            }
            if(entry.getValue() != null)
            {
                // If variable is not found and required value is non null, start-task doesn't match
                return false;                    
            }
        }
        
        return true;
    }
    
    
    
    /* (non-Javadoc)
     * @see org.alfresco.repo.workflow.TaskComponent#getStartTasks(java.util.List, boolean)
     */
    @Override
    public List<WorkflowTask> getStartTasks(List<String> workflowInstanceIds, boolean sameSession)
    {
        List<WorkflowTask> result = new ArrayList<WorkflowTask>(workflowInstanceIds.size());
        for (String workflowInstanceId : workflowInstanceIds)
        {
            WorkflowTask startTask = getStartTask(workflowInstanceId);
            if (startTask != null)
            {
                result.add(startTask);
            }
        }
        return result;
    }

    /**
    * {@inheritDoc}
    */
    public WorkflowTask getStartTask(String workflowInstanceId)
    {
        String instanceId = createLocalId(workflowInstanceId);
        return typeConverter.getVirtualStartTask(instanceId, null);
    }

    /**
    * {@inheritDoc}
    */
    public WorkflowTask startTask(String taskId)
    {
        throw new UnsupportedOperationException();
    }

    /**
    * {@inheritDoc}
    */
    
    public WorkflowTask suspendTask(String taskId)
    {
        throw new UnsupportedOperationException();
    }

    /**
    * {@inheritDoc}
    */
    public WorkflowTask updateTask(String taskId, Map<QName, Serializable> properties, Map<QName, List<NodeRef>> add,
                Map<QName, List<NodeRef>> remove)
    {
        try
        {
            if(taskId.startsWith(ActivitiConstants.START_TASK_PREFIX))
            {
                // Known limitation, start-tasks cannot be updated
                String msg = messageService.getMessage(ERR_UPDATE_START_TASK, taskId);
                throw new WorkflowException(msg);
            }
            
            Task task = taskService.createTaskQuery().taskId(createLocalId(taskId)).singleResult();
            if(task != null)
            {
                Task updatedTask = propertyConverter.updateTask(task, properties, add, remove);
                return typeConverter.convert(updatedTask);
            }
            else
            {
                String msg = messageService.getMessage(ERR_UPDATE_TASK_UNEXISTING, taskId);
                throw new WorkflowException(msg);
            }            
        }
        catch(ActivitiException ae)
        {
            String msg = messageService.getMessage(ERR_UPDATE_TASK, taskId);
            throw new WorkflowException(msg, ae);
        }
    }
    
    @Override
    public List<WorkflowInstance> getWorkflows(WorkflowInstanceQuery workflowInstanceQuery)
    {
        return getWorkflows(workflowInstanceQuery, 0, 0);
    }
    
    @Override
    public List<WorkflowInstance> getWorkflows(WorkflowInstanceQuery workflowInstanceQuery, int maxItems, int skipCount)
    {
        LinkedList<WorkflowInstance> results = new LinkedList<WorkflowInstance>();
        if (Boolean.FALSE.equals(workflowInstanceQuery.getActive()) == false)
        {
            //Add active. 
            results.addAll(getWorkflowsInternal(workflowInstanceQuery, true, maxItems, skipCount));
        }
        if (Boolean.TRUE.equals(workflowInstanceQuery.getActive()) == false)
        {
            //Add complete
            results.addAll(getWorkflowsInternal(workflowInstanceQuery, false, maxItems, skipCount));
        }
        
        return results;
    }
    
    @SuppressWarnings("unchecked")
    private List<WorkflowInstance> getWorkflowsInternal(WorkflowInstanceQuery workflowInstanceQuery, boolean isActive, int maxItems, int skipCount)
    {
        // MNT-9074 My Tasks fails to render if tasks quantity is excessive
        HistoricProcessInstanceQuery query = createQuery(workflowInstanceQuery, isActive);

        LinkedList<WorkflowInstance> results = new LinkedList<WorkflowInstance>();

        List<HistoricProcessInstance> completedInstances;
        if (maxItems > 0)
        {
            completedInstances = query.orderByProcessInstanceDuration().desc().listPage(skipCount, maxItems);
        }
        else
        {
            completedInstances = query.list();
        }

        List<WorkflowInstance> completedResults = typeConverter.doSpecialTenantFilterAndSafeConvert(completedInstances, new Function<HistoricProcessInstance, String>()
        {
            public String apply(HistoricProcessInstance historicProcessInstance)
            {
                ProcessDefinition procDef = activitiUtil.getProcessDefinition(historicProcessInstance.getProcessDefinitionId());
                return procDef.getKey();
            }
        });

        results.addAll(completedResults);
        return results;
    }
    
    @Override
    public long countWorkflows(WorkflowInstanceQuery workflowInstanceQuery)
    {
        // MNT-9074 My Tasks fails to render if tasks quantity is excessive
        long total = 0;
        if (Boolean.FALSE.equals(workflowInstanceQuery.getActive()) == false)
        {
            // Add active.
            total += createQuery(workflowInstanceQuery, true).count();
        }
        if (Boolean.TRUE.equals(workflowInstanceQuery.getActive()) == false)
        {
            // Add complete
            total += createQuery(workflowInstanceQuery, false).count();
        }

        return total;
    }
     
    @SuppressWarnings("unchecked")
    private HistoricProcessInstanceQuery createQuery(WorkflowInstanceQuery workflowInstanceQuery, boolean isActive)
    {
        // MNT-9074 My Tasks fails to render if tasks quantity is excessive
        String processDefId = workflowInstanceQuery.getWorkflowDefinitionId() == null ? null : createLocalId(workflowInstanceQuery.getWorkflowDefinitionId());

        HistoricProcessInstanceQuery query;
        if (isActive)
        {
            // Don't use ProcessInstanceQuery here because in any case they will be converted to WorkflowInstance thro HistoricProcessInstance.
            query = historyService.createHistoricProcessInstanceQuery().unfinished();
        }
        else
        {
            query = historyService.createHistoricProcessInstanceQuery().finished();
        }
        
        if(activitiUtil.isMultiTenantWorkflowDeploymentEnabled() && tenantService.isEnabled() && !TenantUtil.isCurrentDomainDefault() && !StringUtils.isEmpty(TenantUtil.getCurrentDomain())) {
            // Process definition keys are prefixed with the tenant ID, in case MT is enabled and
            // deployments are done in tenant-context
            query.processDefinitionKey("@" + TenantUtil.getCurrentDomain() + "@%");
        }
        
        if(processDefId!=null)
        {
            query = query.processDefinitionId(processDefId);
        }

        if(workflowInstanceQuery.getExcludedDefinitions() != null)
        {
            List<String> exDefIds = new ArrayList<String>();
            for (String excludedDef : workflowInstanceQuery.getExcludedDefinitions())
            {
                String exDef = createLocalId(excludedDef);
                exDef = exDef.replaceAll("\\*", "%");
                exDefIds.add(exDef);
            }
    
            if(exDefIds.size() > 0)
            {
                query.processDefinitionKeyNotIn(exDefIds);
            }
        }

        // Check start range
        if (workflowInstanceQuery.getStartBefore() != null)
        {
            query.startedBefore(workflowInstanceQuery.getStartBefore());

        }
        if (workflowInstanceQuery.getStartAfter() != null)
        {
            query.startedAfter(workflowInstanceQuery.getStartAfter());
        }

        // check end range
        if (workflowInstanceQuery.getEndBefore() != null)
        {
            query.finishedBefore(workflowInstanceQuery.getEndBefore());
        }
        if (workflowInstanceQuery.getEndAfter() != null)
        {
            query.finishedAfter(workflowInstanceQuery.getEndAfter());
        }

        if (workflowInstanceQuery.getCustomProps() != null)
        {
            Map<QName, Object> customProps = workflowInstanceQuery.getCustomProps();
            
            // CLOUD-667: Extract initiator-property and use 'startedBy' instead
            Object initiatorObject = customProps.get(QNAME_INITIATOR);
            if(initiatorObject != null && initiatorObject instanceof NodeRef)
            {
                // Extract username from person-node
                NodeRef initiator = (NodeRef) initiatorObject;
                if(this.nodeService.exists(initiator))
                {
                    String initiatorUserName = (String) nodeService.getProperty(initiator, ContentModel.PROP_USERNAME);
                    query.startedBy(initiatorUserName);
                    
                    // Clone properties map and remove initiator
                    customProps = new HashMap<QName, Object>();
                    customProps.putAll(workflowInstanceQuery.getCustomProps());
                    customProps.remove(QNAME_INITIATOR);
                }
            }
            
            for (Map.Entry<QName, Object> prop : customProps.entrySet())
            {
                String propertyName = factory.mapQNameToName(prop.getKey());
                if (prop.getValue() == null)
                {
                    query.variableValueEquals(propertyName, null);
                }
                else
                {
                    PropertyDefinition propertyDefinition = dictionaryService.getProperty(prop.getKey());
                    if (propertyDefinition == null)
                    {
                        Object converted = propertyConverter.convertPropertyToValue(prop.getValue());
                        query.variableValueEquals(propertyName, converted);
                    }
                    else
                    {
                        String propertyType = propertyDefinition.getDataType().getJavaClassName();
                        if (propertyType.equals("java.util.Date"))
                        {
                            Map<DatePosition, Date> dateProps = (Map<DatePosition, Date>) prop.getValue();
                            for (Map.Entry<DatePosition, Date> dateProp : dateProps.entrySet())
                            {
                                if (dateProp.getValue() != null)
                                {
                                    if (dateProp.getKey() == DatePosition.BEFORE)
                                    {
                                        query.variableValueLessThanOrEqual(propertyName, dateProp.getValue());
                                    }
                                    if (dateProp.getKey() == DatePosition.AFTER)
                                    {
                                        query.variableValueGreaterThanOrEqual(propertyName, dateProp.getValue());
                                    }
                                }
                            }
                        }
                        else
                        {
                            Object convertedValue = DefaultTypeConverter.INSTANCE.convert(propertyDefinition.getDataType(), prop.getValue());
                            query.variableValueEquals(propertyName, convertedValue);
                        }
                    }
                }
            }
        }

        return query;
    }

    public void dispatchPackageUpdatedEvent(Object variableValue, String taskId, String executionId, String processInstanceId, String processDefinitionId)
    {
        runtimeService.dispatchEvent(
           ActivitiEventBuilder.createVariableEvent(ActivitiEventType.VARIABLE_UPDATED, WorkflowNotificationUtils.PROP_PACKAGE,
                            variableValue, new ScriptNodeVariableType(), 
                            taskId, executionId, processInstanceId, processDefinitionId));
    }
    
    /**
     * @param nodeConverter the nodeConverter to set
     */
    public void setNodeConverter(WorkflowNodeConverter nodeConverter)
    {
        this.nodeConverter = nodeConverter;
    }
    
    
    /**
     * @param factory the factory to set
     */
    public void setFactory(WorkflowObjectFactory factory)
    {
        this.factory = factory;
    }
    
    /**
     * @param messageService the messageService to set
     */
    public void setMessageService(MessageService messageService)
    {
        this.messageService = messageService;
    }
    
    /**
     * @param tenantService the tenantService to set
     */
    public void setTenantService(TenantService tenantService)
    {
        this.tenantService = tenantService;
    }
    
    /**
     * @param typeConverter the typeConverter to set
     */
    public void setTypeConverter(ActivitiTypeConverter typeConverter)
    {
        this.typeConverter = typeConverter;
    }
    
    /**
     * @param activitiUtil the activitiUtil to set
     */
    public void setActivitiUtil(ActivitiUtil activitiUtil)
    {
        this.activitiUtil = activitiUtil;
    }
    
    /**
     * @param namespaceService the namespaceService to set
     */
    public void setNamespaceService(NamespaceService namespaceService)
    {
        this.namespaceService = namespaceService;
    }
}
