/* * Copyright 2015 Red Hat, Inc. and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * To change this template, choose Tools | Templates * and open the template in the editor. */ package org.jbpm.services.task.internals.lifecycle; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import org.drools.core.util.MVELSafeHelper; import org.jbpm.process.instance.impl.NoOpExecutionErrorHandler; import org.jbpm.services.task.assignment.AssignmentService; import org.jbpm.services.task.assignment.AssignmentServiceProvider; import org.jbpm.services.task.events.TaskEventSupport; import org.jbpm.services.task.exception.PermissionDeniedException; import org.jbpm.services.task.utils.ContentMarshallerHelper; import org.kie.api.runtime.EnvironmentName; import org.kie.api.task.model.Content; import org.kie.api.task.model.Group; import org.kie.api.task.model.OrganizationalEntity; import org.kie.api.task.model.PeopleAssignments; import org.kie.api.task.model.Status; import org.kie.api.task.model.Task; import org.kie.api.task.model.TaskData; import org.kie.api.task.model.User; import org.kie.internal.runtime.error.ExecutionErrorHandler; import org.kie.internal.runtime.error.ExecutionErrorManager; import org.kie.internal.task.api.TaskContentService; import org.kie.internal.task.api.TaskContext; import org.kie.internal.task.api.TaskModelProvider; import org.kie.internal.task.api.TaskPersistenceContext; import org.kie.internal.task.api.model.FaultData; import org.kie.internal.task.api.model.InternalContent; import org.kie.internal.task.api.model.InternalPeopleAssignments; import org.kie.internal.task.api.model.InternalTaskData; import org.kie.internal.task.api.model.Operation; import org.kie.internal.task.exception.TaskException; import org.mvel2.MVEL; import org.mvel2.ParserConfiguration; import org.mvel2.ParserContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * */ public class MVELLifeCycleManager implements LifeCycleManager { private static final Logger logger = LoggerFactory.getLogger(MVELLifeCycleManager.class); private TaskContext context; private TaskPersistenceContext persistenceContext; private TaskContentService taskContentService; private TaskEventSupport taskEventSupport; private static Map<Operation, List<OperationCommand>> operations = initMVELOperations(); public MVELLifeCycleManager() { } public MVELLifeCycleManager(TaskContext context, TaskPersistenceContext persistenceContext, TaskContentService contentService, TaskEventSupport taskEventSupport) { this.context = context; this.persistenceContext = persistenceContext; this.taskContentService = contentService; this.taskEventSupport = taskEventSupport; } public void setPersistenceContext(TaskPersistenceContext persistenceContext) { this.persistenceContext = persistenceContext; } public void setTaskEventSupport(TaskEventSupport taskEventSupport) { this.taskEventSupport = taskEventSupport; } public void setTaskContentService(TaskContentService taskContentService) { this.taskContentService = taskContentService; } void evalCommand(final Operation operation, final List<OperationCommand> commands, final Task task, final User user, final OrganizationalEntity targetEntity, List<String> groupIds, OrganizationalEntity...entities) throws PermissionDeniedException { boolean statusMatched = false; final TaskData taskData = task.getTaskData(); for (OperationCommand command : commands) { // first find out if we have a matching status if (command.getStatus() != null) { for (Status status : command.getStatus()) { if (task.getTaskData().getStatus() == status) { statusMatched = true; // next find out if the user can execute this doOperation if (!isAllowed(command, task, user, groupIds)) { String errorMessage = "User '" + user + "' does not have permissions to execute operation '" + operation + "' on task id " + task.getId(); throw new PermissionDeniedException(errorMessage); } commands(command, task, user, targetEntity, entities); } else { logger.debug("No match on status for task {} :status {} != {}", task.getId(), task.getTaskData().getStatus(), status); } } } if (command.getPreviousStatus() != null) { for (Status status : command.getPreviousStatus()) { if (taskData.getPreviousStatus() == status) { statusMatched = true; // next find out if the user can execute this doOperation if (!isAllowed(command, task, user, groupIds)) { String errorMessage = "User '" + user + "' does not have permissions to execute operation '" + operation + "' on task id " + task.getId(); throw new PermissionDeniedException(errorMessage); } commands(command, task, user, targetEntity, entities); } else { logger.debug("No match on previous status for task {} :status {} != {}", task.getId(), task.getTaskData().getStatus(), status); } } } if (!command.isGroupTargetEntityAllowed() && targetEntity instanceof Group) { String errorMessage = "User '" + user + "' was unable to execute operation '" + operation + "' on task id " + task.getId() + " due to 'target entity cannot be group'"; throw new PermissionDeniedException(errorMessage); } } if (!statusMatched) { String errorMessage = "User '" + user + "' was unable to execute operation '" + operation + "' on task id " + task.getId() + " due to a no 'current status' match"; throw new PermissionDeniedException(errorMessage); } } private boolean isAllowed(final OperationCommand command, final Task task, final User user, List<String> groupIds) { boolean operationAllowed = false; boolean isExcludedOwner = ((InternalPeopleAssignments) task.getPeopleAssignments()).getExcludedOwners().contains(user); for (Allowed allowed : command.getAllowed()) { if (operationAllowed) { break; } switch (allowed) { case Owner: { operationAllowed = !isExcludedOwner && (task.getTaskData().getActualOwner() != null && task.getTaskData().getActualOwner().equals(user)); break; } case Initiator: { operationAllowed = ( !isExcludedOwner && task.getTaskData().getCreatedBy() != null && (task.getTaskData().getCreatedBy().equals(user) || groupIds != null && groupIds.contains(task.getTaskData().getCreatedBy().getId()))); break; } case PotentialOwner: { operationAllowed = !isExcludedOwner && isAllowed(user, groupIds, (List<OrganizationalEntity>) task.getPeopleAssignments().getPotentialOwners()); break; } case BusinessAdministrator: { operationAllowed = isAllowed(user, groupIds, (List<OrganizationalEntity>) task.getPeopleAssignments().getBusinessAdministrators()); break; } case TaskStakeholders: { operationAllowed = !isExcludedOwner && isAllowed(user, groupIds, (List<OrganizationalEntity>) ((InternalPeopleAssignments) task.getPeopleAssignments()).getTaskStakeholders()); break; } case Anyone: { operationAllowed = !isExcludedOwner; break; } } } if (operationAllowed && command.isUserIsExplicitPotentialOwner()) { // if user has rights to execute the command, make sure user is explicitly specified (not as a group) operationAllowed = task.getPeopleAssignments().getPotentialOwners().contains(user); } if (operationAllowed && command.isSkipable()) { operationAllowed = task.getTaskData().isSkipable(); } return operationAllowed; } private boolean isAllowed(final User user, final List<String> groupIds, final List<OrganizationalEntity> entities) { // for now just do a contains, I'll figure out group membership later. for (OrganizationalEntity entity : entities) { if (entity instanceof User && entity.equals(user)) { return true; } if (entity instanceof Group && groupIds != null && groupIds.contains(entity.getId())) { return true; } } return false; } private void commands(final OperationCommand command, final Task task, final User user, final OrganizationalEntity targetEntity, OrganizationalEntity...entities) { final PeopleAssignments people = task.getPeopleAssignments(); final InternalTaskData taskData = (InternalTaskData) task.getTaskData(); if (command.getNewStatus() != null) { taskData.setStatus(command.getNewStatus()); } else if (command.isSetToPreviousStatus()) { taskData.setStatus(taskData.getPreviousStatus()); } if (command.isAddTargetEntityToPotentialOwners() && !people.getPotentialOwners().contains(targetEntity)) { people.getPotentialOwners().add(targetEntity); } if (command.isRemoveUserFromPotentialOwners()) { people.getPotentialOwners().remove(user); } if (command.isSetNewOwnerToUser()) { taskData.setActualOwner(user); } if (command.isSetNewOwnerToNull()) { taskData.setActualOwner(null); } if (command.getExec() != null) { switch (command.getExec()) { case Claim: { taskData.setActualOwner((User) targetEntity); // @TODO: Ical stuff // Task was reserved so owner should get icals // SendIcal.getInstance().sendIcalForTask(task, service.getUserinfo()); break; } case Nominate: { if (entities != null && entities.length > 0) { List<OrganizationalEntity> potentialOwners = new ArrayList<OrganizationalEntity>(Arrays.asList(entities)); ((InternalPeopleAssignments) task.getPeopleAssignments()).setPotentialOwners(potentialOwners); assignOwnerAndStatus((InternalTaskData) task.getTaskData(), potentialOwners); } break; } } } } public void taskOperation(final Operation operation, final long taskId, final String userId, final String targetEntityId, final Map<String, Object> data, List<String> groupIds, OrganizationalEntity...entities) throws TaskException { try { final List<OperationCommand> commands = operations.get(operation); Task task = persistenceContext.findTask(taskId); if (task == null) { String errorMessage = "Task '" + taskId + "' not found"; throw new PermissionDeniedException(errorMessage); } User user = persistenceContext.findUser(userId); OrganizationalEntity targetEntity = null; if (targetEntityId != null && !targetEntityId.equals("")) { targetEntity = persistenceContext.findOrgEntity(targetEntityId); } getExecutionErrorHandler().processing(task); switch (operation) { case Activate: { taskEventSupport.fireBeforeTaskActivated(task, context); break; } case Claim: { taskEventSupport.fireBeforeTaskClaimed(task, context); break; } case Complete: { taskEventSupport.fireBeforeTaskCompleted(task, context); break; } case Delegate: { taskEventSupport.fireBeforeTaskDelegated(task, context); break; } case Exit: { taskEventSupport.fireBeforeTaskExited(task, context); break; } case Fail: { if (data != null) { FaultData faultData = ContentMarshallerHelper.marshalFault(task, data, null); Content content = TaskModelProvider.getFactory().newContent(); ((InternalContent)content).setContent(faultData.getContent()); persistenceContext.persistContent(content); persistenceContext.setFaultToTask(content, faultData, task); } taskEventSupport.fireBeforeTaskFailed(task, context); break; } case Forward: { taskEventSupport.fireBeforeTaskForwarded(task, context); break; } case Nominate: { taskEventSupport.fireBeforeTaskNominated(task, context); break; } case Release: { taskEventSupport.fireBeforeTaskReleased(task, context); break; } case Resume: { taskEventSupport.fireBeforeTaskResumed(task, context); break; } case Skip: { taskEventSupport.fireBeforeTaskSkipped(task, context); break; } case Start: { taskEventSupport.fireBeforeTaskStarted(task, context); break; } case Stop: { taskEventSupport.fireBeforeTaskStopped(task, context); break; } case Suspend: { taskEventSupport.fireBeforeTaskSuspended(task, context); break; } } evalCommand(operation, commands, task, user, targetEntity, groupIds, entities); switch (operation) { case Activate: { taskEventSupport.fireAfterTaskActivated(task, context); break; } case Claim: { taskEventSupport.fireAfterTaskClaimed(task, context); break; } case Complete: { if (data != null) { taskContentService.addOutputContent(taskId, data); } taskEventSupport.fireAfterTaskCompleted(task, context); break; } case Delegate: { // This is a really bad hack to execut the correct behavior ((InternalTaskData) task.getTaskData()).setStatus(Status.Reserved); taskEventSupport.fireAfterTaskDelegated(task, context); break; } case Exit: { taskEventSupport.fireAfterTaskExited(task, context); break; } case Fail: { taskEventSupport.fireAfterTaskFailed(task, context); break; } case Forward: { invokeAssignmentService(task, context, userId); taskEventSupport.fireAfterTaskForwarded(task, context); break; } case Nominate: { invokeAssignmentService(task, context, userId); taskEventSupport.fireAfterTaskNominated(task, context); break; } case Release: { invokeAssignmentService(task, context, userId); taskEventSupport.fireAfterTaskReleased(task, context); break; } case Resume: { taskEventSupport.fireAfterTaskResumed(task, context); break; } case Start: { taskEventSupport.fireAfterTaskStarted(task, context); break; } case Skip: { taskEventSupport.fireAfterTaskSkipped(task, context); break; } case Stop: { taskEventSupport.fireAfterTaskStopped(task, context); break; } case Suspend: { taskEventSupport.fireAfterTaskSuspended(task, context); break; } } getExecutionErrorHandler().processed(task); } catch (RuntimeException re) { throw re; } } protected void invokeAssignmentService(Task taskImpl, TaskContext context, String excludedUser) { // use assignment service to directly assign actual owner if enabled AssignmentService assignmentService = AssignmentServiceProvider.get(); if (assignmentService.isEnabled()) { assignmentService.assignTask(taskImpl, context, excludedUser); } } public static Map<Operation, List<OperationCommand>> initMVELOperations() { Map<String, Object> vars = new HashMap<String, Object>(); // Search operations-dsl.mvel, if necessary using superclass if TaskService is subclassed InputStream is = null; // for (Class<?> c = getClass(); c != null; c = c.getSuperclass()) { is = MVELLifeCycleManager.class.getResourceAsStream("/operations-dsl.mvel"); // if (is != null) { // break; // } //} if (is == null) { throw new RuntimeException("Unable To initialise TaskService, could not find Operations DSL"); } Reader reader = new InputStreamReader(is); try { return (Map<Operation, List<OperationCommand>>) eval(toString(reader), vars); } catch (IOException e) { throw new RuntimeException("Unable To initialise TaskService, could not load Operations DSL"); } } public static String toString(Reader reader) throws IOException { int charValue; StringBuffer sb = new StringBuffer(1024); while ((charValue = reader.read()) != -1) { sb.append((char) charValue); } return sb.toString(); } public static Object eval(Reader reader) { try { return eval(toString(reader), null); } catch (IOException e) { throw new RuntimeException("Exception Thrown", e); } } public static Object eval(Reader reader, Map<String, Object> vars) { try { return eval(toString(reader), vars); } catch (IOException e) { throw new RuntimeException("Exception Thrown", e); } } public static Object eval(String str, Map<String, Object> vars) { ParserConfiguration pconf = new ParserConfiguration(); pconf.addPackageImport("org.kie.internal.task.api.model"); pconf.addPackageImport("org.jbpm.services.task"); pconf.addPackageImport("org.jbpm.services.task.impl.model"); pconf.addPackageImport("org.jbpm.services.task.query"); pconf.addPackageImport("org.jbpm.services.task.internals.lifecycle"); pconf.addImport(Status.class); pconf.addImport(Allowed.class); pconf.addPackageImport("java.util"); ParserContext context = new ParserContext(pconf); Serializable s = MVEL.compileExpression(str.trim(), context); if (vars != null) { return MVELSafeHelper.getEvaluator().executeExpression(s, vars); } else { return MVELSafeHelper.getEvaluator().executeExpression(s); } } /** * This method will potentially assign the actual owner of this TaskData and set the status * of the data. * <li>If there is only 1 potential owner, and it is a <code>User</code>, that will become the actual * owner of the TaskData and the status will be set to <code>Status.Reserved</code>.</li> * <li>f there is only 1 potential owner, and it is a <code>Group</code>, no owner will be assigned * and the status will be set to <code>Status.Ready</code>.</li> * <li>If there are more than 1 potential owners, the status will be set to <code>Status.Ready</code>.</li> * <li>otherwise, the task data will be unchanged</li> * * @param taskdata - task data * @param potentialOwners - list of potential owners * @return current status of task data */ public static Status assignOwnerAndStatus(InternalTaskData taskData, List<OrganizationalEntity> potentialOwners) { if (taskData.getStatus() != Status.Created) { throw new PermissionDeniedException("Can only assign task owner if status is Created!"); } Status assignedStatus = null; if (potentialOwners.size() == 1) { // if there is a single potential owner, assign and set status to Reserved OrganizationalEntity potentialOwner = potentialOwners.get(0); // if there is a single potential user owner, assign and set status to Reserved if (potentialOwner instanceof User) { taskData.setActualOwner((User) potentialOwner); assignedStatus = Status.Reserved; } //If there is a group set as potentialOwners, set the status to Ready ?? if (potentialOwner instanceof Group) { assignedStatus = Status.Ready; } } else if (potentialOwners.size() > 1) { // multiple potential owners, so set to Ready so one can claim. assignedStatus = Status.Ready; } else { //@TODO we have no potential owners } if (assignedStatus != null) { taskData.setStatus(assignedStatus); } else { // status wasn't assigned, so just return the currrent status assignedStatus = taskData.getStatus(); } return assignedStatus; } protected ExecutionErrorHandler getExecutionErrorHandler() { ExecutionErrorManager errorManager = (ExecutionErrorManager) ((org.jbpm.services.task.commands.TaskContext) context).get(EnvironmentName.EXEC_ERROR_MANAGER); if (errorManager == null) { return new NoOpExecutionErrorHandler(); } return errorManager.getHandler(); } }