/*
* Copyright (c) 2010-2013 Evolveum
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.evolveum.midpoint.wf.impl.activiti.dao;
import com.evolveum.midpoint.prism.PrismContext;
import com.evolveum.midpoint.prism.PrismPropertyValue;
import com.evolveum.midpoint.prism.PrismReferenceValue;
import com.evolveum.midpoint.prism.PrismValue;
import com.evolveum.midpoint.prism.path.ItemPath;
import com.evolveum.midpoint.prism.query.ObjectFilter;
import com.evolveum.midpoint.prism.query.ObjectPaging;
import com.evolveum.midpoint.prism.query.ObjectQuery;
import com.evolveum.midpoint.prism.xml.XmlTypeConverter;
import com.evolveum.midpoint.schema.GetOperationOptions;
import com.evolveum.midpoint.schema.SearchResultList;
import com.evolveum.midpoint.schema.SelectorOptions;
import com.evolveum.midpoint.schema.constants.ObjectTypes;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.schema.util.ObjectTypeUtil;
import com.evolveum.midpoint.schema.util.WfContextUtil;
import com.evolveum.midpoint.task.api.TaskManager;
import com.evolveum.midpoint.util.exception.ObjectNotFoundException;
import com.evolveum.midpoint.util.exception.SchemaException;
import com.evolveum.midpoint.util.exception.SystemException;
import com.evolveum.midpoint.util.logging.LoggingUtils;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.wf.impl.WorkflowManagerImpl;
import com.evolveum.midpoint.wf.impl.activiti.ActivitiEngine;
import com.evolveum.midpoint.wf.impl.messages.TaskEvent;
import com.evolveum.midpoint.wf.impl.processes.ProcessInterfaceFinder;
import com.evolveum.midpoint.wf.impl.processes.ProcessMidPointInterface;
import com.evolveum.midpoint.wf.impl.processes.common.ActivitiUtil;
import com.evolveum.midpoint.wf.impl.processes.common.CommonProcessVariableNames;
import com.evolveum.midpoint.wf.impl.util.MiscDataUtil;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectReferenceType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.WorkItemType;
import org.activiti.engine.TaskService;
import org.activiti.engine.task.IdentityLink;
import org.activiti.engine.task.IdentityLinkType;
import org.activiti.engine.task.Task;
import org.activiti.engine.task.TaskQuery;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;
import static com.evolveum.midpoint.schema.constants.ObjectTypes.TASK;
import static com.evolveum.midpoint.schema.constants.ObjectTypes.USER;
import static com.evolveum.midpoint.schema.util.ObjectQueryUtil.FilterComponents;
import static com.evolveum.midpoint.schema.util.ObjectQueryUtil.factorOutQuery;
import static com.evolveum.midpoint.schema.util.ObjectTypeUtil.createObjectRef;
import static com.evolveum.midpoint.xml.ns._public.common.common_3.WorkItemType.*;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
/**
* Used to retrieve (and provide) data about work items.
*
* @author mederly
*/
@Component
public class WorkItemProvider {
private static final transient Trace LOGGER = TraceManager.getTrace(WorkItemProvider.class);
@Autowired private ActivitiEngine activitiEngine;
@Autowired private MiscDataUtil miscDataUtil;
@Autowired private PrismContext prismContext;
@Autowired private ProcessInterfaceFinder processInterfaceFinder;
@Autowired private TaskManager taskManager;
private static final String DOT_CLASS = WorkflowManagerImpl.class.getName() + ".";
private static final String OPERATION_ACTIVITI_TASK_TO_WORK_ITEM = DOT_CLASS + "activitiTaskToWorkItem";
public Integer countWorkItems(ObjectQuery query, Collection<SelectorOptions<GetOperationOptions>> options, OperationResult result) throws SchemaException {
TaskQuery taskQuery = createTaskQuery(query, false, options, result);
return taskQuery != null ? (int) taskQuery.count() : 0;
}
public SearchResultList<WorkItemType> searchWorkItems(ObjectQuery query, Collection<SelectorOptions<GetOperationOptions>> options, OperationResult result)
throws SchemaException {
TaskQuery taskQuery = createTaskQuery(query, true, options, result);
if (taskQuery == null) {
return new SearchResultList<>(Collections.emptyList());
}
Integer offset = query != null ? query.getOffset() : null;
Integer maxSize = query != null ? query.getMaxSize() : null;
List<Task> tasks;
if (offset == null && maxSize == null) {
tasks = taskQuery.list();
} else {
tasks = taskQuery.listPage(defaultIfNull(offset, 0), defaultIfNull(maxSize, Integer.MAX_VALUE));
}
boolean getAllVariables = true; // TODO implement based on options
// there's no need to fill-in assignee details ; but candidates are necessary to fill-in; TODO implement based on options (resolve)
return tasksToWorkItems(tasks, null, false, false, true, getAllVariables, result);
}
// primitive 'query interpreter'
// returns null if no results should be returned
private TaskQuery createTaskQuery(ObjectQuery query, boolean includeVariables, Collection<SelectorOptions<GetOperationOptions>> options, OperationResult result) throws SchemaException {
FilterComponents components = factorOutQuery(query, F_ASSIGNEE_REF, F_CANDIDATE_REF, F_EXTERNAL_ID);
List<ObjectFilter> remainingClauses = components.getRemainderClauses();
if (!remainingClauses.isEmpty()) {
throw new SchemaException("Unsupported clause(s) in search filter: " + remainingClauses);
}
final ItemPath WORK_ITEM_ID_PATH = new ItemPath(F_EXTERNAL_ID);
final ItemPath ASSIGNEE_PATH = new ItemPath(F_ASSIGNEE_REF);
final ItemPath CANDIDATE_PATH = new ItemPath(F_CANDIDATE_REF);
final ItemPath CREATED_PATH = new ItemPath(WorkItemType.F_CREATE_TIMESTAMP);
final Map.Entry<ItemPath, Collection<? extends PrismValue>> workItemIdFilter = components.getKnownComponent(WORK_ITEM_ID_PATH);
final Map.Entry<ItemPath, Collection<? extends PrismValue>> assigneeFilter = components.getKnownComponent(ASSIGNEE_PATH);
final Map.Entry<ItemPath, Collection<? extends PrismValue>> candidateRolesFilter = components.getKnownComponent(CANDIDATE_PATH);
TaskQuery taskQuery = activitiEngine.getTaskService().createTaskQuery();
if (workItemIdFilter != null) {
Collection<? extends PrismValue> filterValues = workItemIdFilter.getValue();
if (filterValues.size() > 1) {
throw new IllegalArgumentException("In a query there must be exactly one value for workItemId: " + filterValues);
}
taskQuery = taskQuery.taskId(((PrismPropertyValue<String>) filterValues.iterator().next()).getValue());
}
if (assigneeFilter != null) {
taskQuery = addAssigneesToQuery(taskQuery, assigneeFilter);
}
if (candidateRolesFilter != null) {
// TODO what about candidate users? (currently these are not supported)
List<String> candidateGroups = MiscDataUtil.prismRefsToStrings((Collection<PrismReferenceValue>) candidateRolesFilter.getValue());
if (!candidateGroups.isEmpty()) {
taskQuery = taskQuery.taskCandidateGroupIn(candidateGroups);
} else {
return null; // no groups -> no result
}
}
if (query != null && query.getPaging() != null) {
ObjectPaging paging = query.getPaging();
if (paging.getOrderingInstructions().size() > 1) {
throw new UnsupportedOperationException("Ordering by more than one property is not supported: " + paging.getOrderingInstructions());
} else if (paging.getOrderingInstructions().size() == 1) {
ItemPath orderBy = paging.getOrderBy();
if (CREATED_PATH.equivalent(orderBy)) {
taskQuery = taskQuery.orderByTaskCreateTime();
} else {
throw new UnsupportedOperationException("Ordering by " + orderBy + " is not currently supported");
}
switch (paging.getDirection()) {
case DESCENDING: taskQuery = taskQuery.desc(); break;
case ASCENDING:
default: taskQuery = taskQuery.asc(); break;
}
}
}
if (includeVariables) {
return taskQuery
.includeTaskLocalVariables()
.includeProcessVariables();
} else {
return taskQuery;
}
}
private TaskQuery addAssigneesToQuery(TaskQuery taskQuery, Map.Entry<ItemPath, Collection<? extends PrismValue>> assigneeFilter) {
@SuppressWarnings("unchecked")
Collection<PrismReferenceValue> assigneeRefs = (Collection<PrismReferenceValue>) assigneeFilter.getValue();
if (isEmpty(assigneeRefs)) {
return taskQuery.taskUnassigned();
} else {
List<String> values = MiscDataUtil.prismRefsToStrings(assigneeRefs);
return taskQuery.taskInvolvedUser(StringUtils.join(values, ';'));
}
}
// special interface for ProcessInstanceProvider - TODO align with other interfaces
public SearchResultList<WorkItemType> getWorkItemsForProcessInstanceId(String processInstanceId, OperationResult result) {
TaskService ts = activitiEngine.getTaskService();
List<Task> tasks = ts.createTaskQuery()
.processInstanceId(processInstanceId)
.includeTaskLocalVariables()
.includeProcessVariables()
.list();
return tasksToWorkItems(tasks, null, false, true, true, true, result);
}
public WorkItemType getWorkItem(String workItemId, OperationResult result) {
TaskService ts = activitiEngine.getTaskService();
Task task = ts.createTaskQuery()
.taskId(workItemId)
.includeTaskLocalVariables()
.includeProcessVariables()
.singleResult();
return taskToWorkItem(task, null, false, false, false, false, result);
}
private SearchResultList<WorkItemType> tasksToWorkItems(List<Task> tasks, Map<String, Object> processVariables,
boolean resolveTask, boolean resolveAssignee, boolean resolveCandidates, boolean fetchAllVariables, OperationResult result) {
SearchResultList<WorkItemType> retval = new SearchResultList<>(new ArrayList<WorkItemType>());
for (Task task : tasks) {
try {
retval.add(taskToWorkItem(task, processVariables, resolveTask, resolveAssignee, resolveCandidates, fetchAllVariables, result));
} catch (RuntimeException e) {
// operation result already contains corresponding error record
LoggingUtils.logUnexpectedException(LOGGER, "Couldn't get information on activiti task {}", e, task.getId());
}
}
return retval;
}
@NotNull
public List<ObjectReferenceType> getMidpointAssignees(TaskExtract taskExtract) {
List<ObjectReferenceType> rv = new ArrayList<>();
for (IdentityLink link : taskExtract.getTaskIdentityLinks()) {
if (CommonProcessVariableNames.MIDPOINT_ASSIGNEE.equals(link.getType())) {
if (link.getUserId() != null) {
rv.add(MiscDataUtil.stringToRef(link.getUserId()));
}
if (link.getGroupId() != null) { // just for completeness (currently we don't expect groups to be here)
rv.add(MiscDataUtil.stringToRef(link.getGroupId()));
}
}
}
return rv;
}
/**
* Helper class to carry relevant data from both Task and DelegateTask (to avoid code duplication)
*/
private class TaskExtract {
private String id;
private String assignee;
private String name;
private String processInstanceId;
private Date createTime;
private Date dueDate;
private String owner;
private String executionId;
private Map<String,Object> variables;
private List<String> candidateUsers;
private List<String> candidateGroups;
private final List<IdentityLink> taskIdentityLinks;
TaskExtract(TaskEvent task, Map<String, Object> processVariables, List<IdentityLink> taskIdentityLinks) {
id = task.getTaskId();
assignee = task.getAssigneeOid();
name = task.getTaskName();
processInstanceId = task.getProcessInstanceId();
createTime = task.getCreateTime();
dueDate = task.getDueDate();
owner = task.getOwner();
executionId = task.getExecutionId();
variables = task.getVariables();
candidateUsers = task.getCandidateUsers();
candidateGroups = task.getCandidateGroups();
addProcessVariables(processVariables);
this.taskIdentityLinks = taskIdentityLinks;
}
TaskExtract(Task task, Map<String, Object> processVariables, List<IdentityLink> taskIdentityLinks) {
id = task.getId();
assignee = task.getAssignee();
name = task.getName();
processInstanceId = task.getProcessInstanceId();
createTime = task.getCreateTime();
dueDate = task.getDueDate();
owner = task.getOwner();
executionId = task.getExecutionId();
variables = new HashMap<>();
if (task.getProcessVariables() != null) {
variables.putAll(task.getProcessVariables());
}
if (task.getTaskLocalVariables() != null) {
variables.putAll(task.getTaskLocalVariables());
}
candidateUsers = new ArrayList<>();
candidateGroups = new ArrayList<>();
for (IdentityLink link : taskIdentityLinks) {
if (IdentityLinkType.CANDIDATE.equals(link.getType())) {
if (link.getUserId() != null) {
candidateUsers.add(link.getUserId());
} else if (link.getGroupId() != null) {
candidateGroups.add(link.getGroupId());
}
}
}
addProcessVariables(processVariables);
this.taskIdentityLinks = taskIdentityLinks;
}
private void addProcessVariables(Map<String, Object> processVariables) {
if (processVariables != null) {
for (Map.Entry<String, Object> variable: processVariables.entrySet()) {
if (!variables.containsKey(variable.getKey())) {
variables.put(variable.getKey(), variable.getValue());
}
}
}
}
String getId() {
return id;
}
ObjectReferenceType getAssignee() {
return assignee != null ? MiscDataUtil.stringToRef(assignee) : null;
}
String getName() {
return name;
}
String getProcessInstanceId() {
return processInstanceId;
}
Date getCreateTime() {
return createTime;
}
Date getDueDate() {
return dueDate;
}
String getOwner() {
return owner;
}
String getExecutionId() {
return executionId;
}
Map<String,Object> getVariables() {
return variables;
}
public List<String> getCandidateUsers() {
return candidateUsers;
}
public List<String> getCandidateGroups() {
return candidateGroups;
}
public List<IdentityLink> getTaskIdentityLinks() {
return taskIdentityLinks;
}
@Override
public String toString() {
return "Task{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", processInstanceId='" + processInstanceId + '\'' +
'}';
}
}
private WorkItemType taskToWorkItem(Task task, Map<String, Object> processVariables, boolean resolveTask, boolean resolveAssignee,
boolean resolveCandidates, boolean fetchAllVariables, OperationResult result) {
if (task == null) {
return null;
}
TaskExtract taskExtract = new TaskExtract(task, processVariables, getTaskIdentityLinks(task.getId()));
return taskExtractToWorkItem(taskExtract, resolveTask, resolveAssignee, resolveCandidates, fetchAllVariables, result);
}
private List<IdentityLink> getTaskIdentityLinks(String taskId) {
return activitiEngine.getTaskService().getIdentityLinksForTask(taskId);
}
public WorkItemType taskEventToWorkItemNew(TaskEvent taskEvent, Map<String, Object> processVariables, boolean resolveTask,
boolean resolveAssignee, boolean resolveCandidates, OperationResult result) {
TaskExtract taskExtract = new TaskExtract(taskEvent, processVariables, getTaskIdentityLinks(taskEvent.getTaskId()));
return taskExtractToWorkItem(taskExtract, resolveTask, resolveAssignee, resolveCandidates, false, result);
}
public WorkItemType taskExtractToWorkItem(TaskExtract task, boolean resolveTask, boolean resolveAssignee,
boolean resolveCandidates, boolean fetchAllVariables, OperationResult parentResult) {
OperationResult result = parentResult.createSubresult(OPERATION_ACTIVITI_TASK_TO_WORK_ITEM);
result.addParams(new String [] { "activitiTaskId", "resolveTask", "resolveAssignee", "resolveCandidates" },
task.getId(), resolveTask, resolveAssignee, resolveCandidates);
try {
WorkItemType wi = new WorkItemType(prismContext);
final Map<String, Object> variables = task.getVariables();
wi.setExternalId(task.getId());
wi.setName(task.getName());
wi.setCreateTimestamp(XmlTypeConverter.createXMLGregorianCalendar(task.getCreateTime()));
wi.setDeadline(XmlTypeConverter.createXMLGregorianCalendar(task.getDueDate()));
String taskOid = ActivitiUtil.getRequiredVariable(variables, CommonProcessVariableNames.VARIABLE_MIDPOINT_TASK_OID, String.class, null);
com.evolveum.midpoint.task.api.Task mpTask = null;
try {
mpTask = taskManager.getTask(taskOid, result);
} catch (ObjectNotFoundException|SchemaException e) {
throw new SystemException("Couldn't retrieve owning task for " + wi + ": " + e.getMessage(), e); // TODO more gentle treatment
}
if (mpTask.getWorkflowContext() == null) {
throw new IllegalStateException("No workflow context in task " + mpTask + " that owns " + wi);
}
mpTask.getWorkflowContext().getWorkItem().add(wi);
// assignees
wi.getAssigneeRef().addAll(getMidpointAssignees(task));
String originalAssigneeString = ActivitiUtil.getVariable(variables,
CommonProcessVariableNames.VARIABLE_ORIGINAL_ASSIGNEE, String.class, prismContext);
if (originalAssigneeString != null) {
wi.setOriginalAssigneeRef(MiscDataUtil.stringToRef(originalAssigneeString));
}
if (resolveAssignee) {
miscDataUtil.resolveAndStoreObjectReferences(wi.getAssigneeRef(), result);
miscDataUtil.resolveAndStoreObjectReference(wi.getOriginalAssigneeRef(), result);
}
// candidates
task.getCandidateUsers().forEach(s -> wi.getCandidateRef().add(createObjectRef(s, USER)));
task.getCandidateGroups().forEach(s -> wi.getCandidateRef().add(MiscDataUtil.stringToRef(s)));
if (resolveCandidates) {
miscDataUtil.resolveAndStoreObjectReferences(wi.getCandidateRef(), result);
}
// other
ProcessMidPointInterface pmi = processInterfaceFinder.getProcessInterface(variables);
wi.setOutput(pmi.extractWorkItemResult(variables));
String completedBy = ActivitiUtil.getVariable(variables, CommonProcessVariableNames.VARIABLE_WORK_ITEM_COMPLETED_BY, String.class, prismContext);
if (completedBy != null) {
wi.setPerformerRef(ObjectTypeUtil.createObjectRef(completedBy, ObjectTypes.USER));
}
wi.setStageNumber(pmi.getStageNumber(variables));
wi.setEscalationLevel(WfContextUtil.createEscalationLevel(pmi.getEscalationLevelNumber(variables),
pmi.getEscalationLevelName(variables),
pmi.getEscalationLevelDisplayName(variables)));
// This is just because 'variables' switches in task query DO NOT fetch all required variables...
if (fetchAllVariables) { // TODO can we do this e.g. in the task completion listener?
Map<String, Object> allVariables = activitiEngine.getTaskService().getVariables(task.getId());
wi.setProcessSpecificPart(pmi.extractProcessSpecificWorkItemPart(allVariables));
wi.getAdditionalInformation().addAll(pmi.getAdditionalInformation(allVariables));
}
return wi;
} catch (RuntimeException e) {
result.recordFatalError("Couldn't convert activiti task " + task.getId() + " to midPoint WorkItem: " + e.getMessage(), e);
throw e;
} finally {
result.computeStatusIfUnknown();
}
}
}