org.alfresco.rest.workflow.api.impl.WorkflowRestImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.rest.workflow.api.impl.WorkflowRestImpl.java

Source

/*
 * #%L
 * Alfresco Remote API
 * %%
 * 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.rest.workflow.api.impl;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.activiti.engine.ActivitiObjectNotFoundException;
import org.activiti.engine.ProcessEngine;
import org.activiti.engine.history.HistoricTaskInstance;
import org.activiti.engine.history.HistoricTaskInstanceQuery;
import org.activiti.engine.history.HistoricVariableInstance;
import org.activiti.engine.task.Task;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.dictionary.constraint.ListOfValuesConstraint;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.tenant.TenantService;
import org.alfresco.repo.tenant.TenantUtil;
import org.alfresco.repo.workflow.WorkflowConstants;
import org.alfresco.repo.workflow.WorkflowModel;
import org.alfresco.repo.workflow.activiti.ActivitiConstants;
import org.alfresco.repo.workflow.activiti.ActivitiScriptNode;
import org.alfresco.repo.workflow.activiti.ActivitiWorkflowEngine;
import org.alfresco.rest.framework.core.exceptions.ApiException;
import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Paging;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.rest.workflow.api.model.FormModelElement;
import org.alfresco.rest.workflow.api.model.Item;
import org.alfresco.service.cmr.dictionary.AssociationDefinition;
import org.alfresco.service.cmr.dictionary.ClassDefinition;
import org.alfresco.service.cmr.dictionary.Constraint;
import org.alfresco.service.cmr.dictionary.ConstraintDefinition;
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.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.AuthorityType;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.apache.commons.beanutils.ConversionException;
import org.apache.commons.beanutils.ConvertUtils;

/**
 * Base class for rest-implementations related to workflow. Contains utility-methods that
 * can be used, regardless of the type of resources the implementing class can handle.
 *
 * @author Frederik Heremans
 */
public class WorkflowRestImpl {
    protected static final String BPM_PACKAGE = "bpm_package";

    protected TenantService tenantService;
    protected AuthorityService authorityService;
    protected NamespaceService namespaceService;
    protected DictionaryService dictionaryService;
    protected NodeService nodeService;
    protected ProcessEngine activitiProcessEngine;
    protected boolean deployWorkflowsInTenant;
    protected List<String> excludeModelTypes = new ArrayList<String>(
            Arrays.asList("bpm_priority", "bpm_description", "bpm_dueDate"));
    private ActivitiWorkflowEngine activitiWorkflowEngine;

    static {
        // Register a custom date-converter to cope with ISO8601-parameters
        ISO8601Converter dateConverter = new ISO8601Converter();
        ConvertUtils.register(dateConverter, Date.class);
        ConvertUtils.register(dateConverter, Calendar.class);
    }

    public void setTenantService(TenantService tenantService) {
        this.tenantService = tenantService;
    }

    public void setAuthorityService(AuthorityService authorityService) {
        this.authorityService = authorityService;
    }

    public void setNamespaceService(NamespaceService namespaceService) {
        this.namespaceService = namespaceService;
    }

    public void setDictionaryService(DictionaryService dictionaryService) {
        this.dictionaryService = dictionaryService;
    }

    public void setNodeService(NodeService nodeService) {
        this.nodeService = nodeService;
    }

    public void setActivitiProcessEngine(ProcessEngine activitiProcessEngine) {
        this.activitiProcessEngine = activitiProcessEngine;
    }

    public void setDeployWorkflowsInTenant(boolean deployWorkflowsInTenant) {
        this.deployWorkflowsInTenant = deployWorkflowsInTenant;
    }

    /**
     * Create NodeRef from item id String
     */
    public NodeRef getNodeRef(String itemId) {
        NodeRef nodeRef = null;
        if (NodeRef.isNodeRef(itemId) == false) {
            nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, itemId);
        } else {
            nodeRef = new NodeRef(itemId);
        }
        return nodeRef;
    }

    /**
     * Get all items from the process package variable
     */
    public CollectionWithPagingInfo<Item> getItemsFromProcess(String processId, Paging paging) {
        ActivitiScriptNode packageScriptNode = null;
        try {
            HistoricVariableInstance variableInstance = activitiProcessEngine.getHistoryService()
                    .createHistoricVariableInstanceQuery().processInstanceId(processId).variableName(BPM_PACKAGE)
                    .singleResult();

            if (variableInstance != null) {
                packageScriptNode = (ActivitiScriptNode) variableInstance.getValue();
            } else {
                throw new EntityNotFoundException(processId);
            }
        } catch (ActivitiObjectNotFoundException e) {
            throw new EntityNotFoundException(processId);
        }

        List<Item> page = new ArrayList<Item>();
        if (packageScriptNode != null) {
            List<ChildAssociationRef> documentList = nodeService.getChildAssocs(packageScriptNode.getNodeRef());
            for (ChildAssociationRef childAssociationRef : documentList) {
                Item item = createItemForNodeRef(childAssociationRef.getChildRef());
                page.add(item);
            }
        }

        return CollectionWithPagingInfo.asPaged(paging, page, false, page.size());
    }

    /**
     * Get an item from the process package variable
     */
    public Item getItemFromProcess(String itemId, String processId) {
        NodeRef nodeRef = getNodeRef(itemId);
        ActivitiScriptNode packageScriptNode = null;
        try {
            HistoricVariableInstance variableInstance = activitiProcessEngine.getHistoryService()
                    .createHistoricVariableInstanceQuery().processInstanceId(processId).variableName(BPM_PACKAGE)
                    .singleResult();

            if (variableInstance != null) {
                packageScriptNode = (ActivitiScriptNode) variableInstance.getValue();
            } else {
                throw new EntityNotFoundException(processId);
            }
        } catch (ActivitiObjectNotFoundException e) {
            throw new EntityNotFoundException(processId);
        }

        Item item = null;
        if (packageScriptNode != null) {
            List<ChildAssociationRef> documentList = nodeService.getChildAssocs(packageScriptNode.getNodeRef());
            for (ChildAssociationRef childAssociationRef : documentList) {
                if (childAssociationRef.getChildRef().equals(nodeRef)) {
                    item = createItemForNodeRef(childAssociationRef.getChildRef());
                    break;
                }
            }
        }

        if (item == null) {
            throw new EntityNotFoundException(itemId);
        }

        return item;
    }

    /**
     *  Create a new item in the process package variable
     */
    public Item createItemInProcess(String itemId, String processId) {
        NodeRef nodeRef = getNodeRef(itemId);

        ActivitiScriptNode packageScriptNode = null;
        try {
            packageScriptNode = (ActivitiScriptNode) activitiProcessEngine.getRuntimeService()
                    .getVariable(processId, BPM_PACKAGE);
        } catch (ActivitiObjectNotFoundException e) {
            throw new EntityNotFoundException(processId);
        }

        if (packageScriptNode == null) {
            throw new InvalidArgumentException("process doesn't contain a workflow package variable");
        }

        // check if noderef exists
        try {
            nodeService.getProperties(nodeRef);
        } catch (Exception e) {
            throw new EntityNotFoundException("item with id " + nodeRef.toString() + " not found");
        }

        try {
            QName workflowPackageItemId = QName.createQName("wpi", nodeRef.toString());
            nodeService.addChild(packageScriptNode.getNodeRef(), nodeRef, WorkflowModel.ASSOC_PACKAGE_CONTAINS,
                    workflowPackageItemId);
        } catch (Exception e) {
            throw new ApiException("could not add item to process " + e.getMessage(), e);
        }

        Item responseItem = createItemForNodeRef(nodeRef);
        activitiWorkflowEngine.dispatchPackageUpdatedEvent(packageScriptNode, null, null, processId, null);
        return responseItem;
    }

    /**
     *  Delete an item from the process package variable
     */
    public void deleteItemFromProcess(String itemId, String processId) {
        NodeRef nodeRef = getNodeRef(itemId);
        ActivitiScriptNode packageScriptNode = null;
        try {
            packageScriptNode = (ActivitiScriptNode) activitiProcessEngine.getRuntimeService()
                    .getVariable(processId, BPM_PACKAGE);
        } catch (ActivitiObjectNotFoundException e) {
            throw new EntityNotFoundException(processId);
        }

        if (packageScriptNode == null) {
            throw new InvalidArgumentException("process doesn't contain a workflow package variable");
        }

        boolean itemIdFoundInPackage = false;
        List<ChildAssociationRef> documentList = nodeService.getChildAssocs(packageScriptNode.getNodeRef());
        for (ChildAssociationRef childAssociationRef : documentList) {
            if (childAssociationRef.getChildRef().equals(nodeRef)) {
                itemIdFoundInPackage = true;
                break;
            }
        }

        if (itemIdFoundInPackage == false) {
            throw new EntityNotFoundException("Item " + itemId + " not found in the process package variable");
        }

        try {
            nodeService.removeChild(packageScriptNode.getNodeRef(), nodeRef);
            activitiWorkflowEngine.dispatchPackageUpdatedEvent(packageScriptNode, null, null, processId, null);
        } catch (InvalidNodeRefException e) {
            throw new EntityNotFoundException("Item " + itemId + " not found");
        }
    }

    /**
     * Get the first parameter value, converted to the requested type.
     * @param parameters used to extract parameter value from
     * @param parameterName name of the parameter
     * @param returnType type of object to return
     * @return the converted parameter value. Null, if the parameter has no value.
     * @throws IllegalArgumentException when no conversion for the given returnType is available or if returnType is null.
     * @throws InvalidArgumentException when conversion to the given type was not possible
     */
    @SuppressWarnings("unchecked")
    public <T extends Object> T getParameter(Parameters parameters, String parameterName, Class<T> returnType) {
        if (returnType == null) {
            throw new IllegalArgumentException("ReturnType cannot be null");
        }
        try {
            Object result = null;
            String stringValue = parameters.getParameter(parameterName);
            if (stringValue != null) {
                result = ConvertUtils.convert(stringValue, returnType);
                if (result instanceof String) {
                    // If a string is returned, no converter has been found
                    throw new IllegalArgumentException(
                            "Unable to convert parameter to type: " + returnType.getName());
                }
            }
            return (T) result;
        } catch (ConversionException ce) {
            // Conversion failed, wrap in Illegal
            throw new InvalidArgumentException(
                    "Parameter value for '" + parameterName + "' should be a valid " + returnType.getSimpleName());
        }
    }

    /**
     * @param type the type to get the elements for
     * @param paging Paging
     * @return collection with all valid form-model elements for the given type.
     */
    public CollectionWithPagingInfo<FormModelElement> getFormModelElements(TypeDefinition type, Paging paging) {
        Map<QName, PropertyDefinition> taskProperties = type.getProperties();
        Set<QName> typesToExclude = getTypesToExclude(type);

        List<FormModelElement> page = new ArrayList<FormModelElement>();
        for (Entry<QName, PropertyDefinition> entry : taskProperties.entrySet()) {
            String name = entry.getKey().toPrefixString(namespaceService).replace(':', '_');

            // Only add properties which are not part of an excluded type
            if (!typesToExclude.contains(entry.getValue().getContainerClass().getName())
                    && excludeModelTypes.contains(name) == false) {
                FormModelElement element = new FormModelElement();
                element.setName(name);
                element.setQualifiedName(entry.getKey().toString());
                element.setTitle(entry.getValue().getTitle(dictionaryService));
                element.setRequired(entry.getValue().isMandatory());
                element.setDataType(entry.getValue().getDataType().getName().toPrefixString(namespaceService));
                element.setDefaultValue(entry.getValue().getDefaultValue());
                if (entry.getValue().getConstraints() != null) {
                    for (ConstraintDefinition constraintDef : entry.getValue().getConstraints()) {
                        Constraint constraint = constraintDef.getConstraint();
                        if (constraint != null && constraint instanceof ListOfValuesConstraint) {
                            ListOfValuesConstraint valuesConstraint = (ListOfValuesConstraint) constraint;
                            if (valuesConstraint.getAllowedValues() != null
                                    && valuesConstraint.getAllowedValues().size() > 0) {
                                element.setAllowedValues(valuesConstraint.getAllowedValues());
                            }
                        }
                    }
                }
                page.add(element);
            }
        }

        Map<QName, AssociationDefinition> taskAssociations = type.getAssociations();
        for (Entry<QName, AssociationDefinition> entry : taskAssociations.entrySet()) {
            // Only add associations which are not part of an excluded type
            if (!typesToExclude.contains(entry.getValue().getSourceClass().getName())) {
                FormModelElement element = new FormModelElement();
                element.setName(entry.getKey().toPrefixString(namespaceService).replace(':', '_'));
                element.setQualifiedName(entry.getKey().toString());
                element.setTitle(entry.getValue().getTitle(dictionaryService));
                element.setRequired(entry.getValue().isTargetMandatory());
                element.setDataType(entry.getValue().getTargetClass().getName().toPrefixString(namespaceService));
                page.add(element);
            }
        }

        return CollectionWithPagingInfo.asPaged(paging, page, false, page.size());
    }

    /**
     * @param taskType type of the task
     * @return all types (and aspects) which properties should not be used for form-model elements
     */
    protected Set<QName> getTypesToExclude(TypeDefinition taskType) {
        HashSet<QName> typesToExclude = new HashSet<QName>();

        ClassDefinition parentClassDefinition = taskType.getParentClassDefinition();
        boolean contentClassFound = false;
        while (parentClassDefinition != null) {
            if (contentClassFound) {
                typesToExclude.add(parentClassDefinition.getName());
            } else if (ContentModel.TYPE_CONTENT.equals(parentClassDefinition.getName())) {
                // All parents of "cm:content" should be ignored as well for fetching start-properties 
                typesToExclude.add(ContentModel.TYPE_CONTENT);
                typesToExclude.addAll(parentClassDefinition.getDefaultAspectNames());
                contentClassFound = true;
            }
            parentClassDefinition = parentClassDefinition.getParentClassDefinition();
        }
        return typesToExclude;
    }

    /**
     * Validates if the logged in user is allowed to get information about a specific process instance.
     * If the user is not allowed an exception is thrown.
     * 
     * @param processId identifier of the process instance
     */
    protected List<HistoricVariableInstance> validateIfUserAllowedToWorkWithProcess(String processId) {
        List<HistoricVariableInstance> variableInstances = activitiProcessEngine.getHistoryService()
                .createHistoricVariableInstanceQuery().processInstanceId(processId).list();

        Map<String, Object> variableMap = new HashMap<String, Object>();
        if (variableInstances != null && variableInstances.size() > 0) {
            for (HistoricVariableInstance variableInstance : variableInstances) {
                variableMap.put(variableInstance.getVariableName(), variableInstance.getValue());
            }
        } else {
            throw new EntityNotFoundException(processId);
        }

        if (tenantService.isEnabled()) {
            String tenantDomain = (String) variableMap.get(ActivitiConstants.VAR_TENANT_DOMAIN);
            if (TenantUtil.getCurrentDomain().equals(tenantDomain) == false) {
                throw new PermissionDeniedException("Process is running in another tenant");
            }
        }

        ActivitiScriptNode initiator = (ActivitiScriptNode) variableMap.get(WorkflowConstants.PROP_INITIATOR);
        if (initiator != null && AuthenticationUtil.getRunAsUser().equals(initiator.getNodeRef().getId())) {
            // user is allowed
            return variableInstances;
        }

        String username = AuthenticationUtil.getRunAsUser();
        if (authorityService.isAdminAuthority(username)) {
            // Admin is allowed to read all processes in the current tenant
            return variableInstances;
        } else {
            // MNT-12382 check for membership in the assigned group
            ActivitiScriptNode group = (ActivitiScriptNode) variableMap.get("bpm_groupAssignee");
            if (group != null) {
                // check that the process is unclaimed
                Task task = activitiProcessEngine.getTaskService().createTaskQuery().processInstanceId(processId)
                        .singleResult();
                if ((task != null) && (task.getAssignee() == null) && isUserInGroup(username, group.getNodeRef())) {
                    return variableInstances;
                }
            }

            // If non-admin user, involvement in the task is required (either owner, assignee or externally involved).
            HistoricTaskInstanceQuery query = activitiProcessEngine.getHistoryService()
                    .createHistoricTaskInstanceQuery().processInstanceId(processId)
                    .taskInvolvedUser(AuthenticationUtil.getRunAsUser());

            List<HistoricTaskInstance> taskList = query.list();

            if (org.apache.commons.collections.CollectionUtils.isEmpty(taskList)) {
                throw new PermissionDeniedException(
                        "user is not allowed to access information about process " + processId);
            }
        }

        return variableInstances;
    }

    protected Item createItemForNodeRef(NodeRef nodeRef) {
        Map<QName, Serializable> properties = nodeService.getProperties(nodeRef);
        Item item = new Item();
        String name = (String) properties.get(ContentModel.PROP_NAME);
        String title = (String) properties.get(ContentModel.PROP_TITLE);
        String description = (String) properties.get(ContentModel.PROP_DESCRIPTION);
        Date createdAt = (Date) properties.get(ContentModel.PROP_CREATED);
        String createdBy = (String) properties.get(ContentModel.PROP_CREATOR);
        Date modifiedAt = (Date) properties.get(ContentModel.PROP_MODIFIED);
        String modifiedBy = (String) properties.get(ContentModel.PROP_MODIFIER);

        ContentData contentData = (ContentData) nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT);

        item.setId(nodeRef.getId());
        item.setName(name);
        item.setTitle(title);
        item.setDescription(description);
        item.setCreatedAt(createdAt);
        item.setCreatedBy(createdBy);
        item.setModifiedAt(modifiedAt);
        item.setModifiedBy(modifiedBy);
        if (contentData != null) {
            item.setMimeType(contentData.getMimetype());
            item.setSize(contentData.getSize());
        }
        return item;
    }

    public void setActivitiWorkflowEngine(ActivitiWorkflowEngine activitiWorkflowEngine) {
        this.activitiWorkflowEngine = activitiWorkflowEngine;
    }

    private boolean isUserInGroup(String username, NodeRef group) {
        // Get the group name
        String groupName = (String) nodeService.getProperty(group, ContentModel.PROP_AUTHORITY_NAME);

        // Get all group members
        Set<String> groupMembers = authorityService.getContainedAuthorities(AuthorityType.USER, groupName, false);

        // Check if the user is a group member.
        return (groupMembers != null) && groupMembers.contains(username);
    }
}