/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2012-2015 ForgeRock AS.
*/
package org.forgerock.openidm.workflow.activiti.impl;
import static org.forgerock.json.JsonValue.json;
import static org.forgerock.json.JsonValue.object;
import static org.forgerock.json.resource.Responses.newActionResponse;
import static org.forgerock.json.resource.Responses.newQueryResponse;
import static org.forgerock.json.resource.Responses.newResourceResponse;
import static org.forgerock.openidm.util.ResourceUtil.*;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.activiti.engine.ActivitiObjectNotFoundException;
import org.activiti.engine.task.IdentityLink;
import org.forgerock.services.context.Context;
import org.forgerock.json.resource.ActionRequest;
import org.forgerock.json.resource.ActionResponse;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.CollectionResourceProvider;
import org.forgerock.json.resource.CreateRequest;
import org.forgerock.json.resource.DeleteRequest;
import org.forgerock.json.resource.InternalServerErrorException;
import org.forgerock.json.resource.NotFoundException;
import org.forgerock.json.resource.NotSupportedException;
import org.forgerock.json.resource.PatchRequest;
import org.forgerock.json.resource.QueryRequest;
import org.forgerock.json.resource.QueryResourceHandler;
import org.forgerock.json.resource.QueryResponse;
import org.forgerock.json.resource.ReadRequest;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.services.context.SecurityContext;
import org.forgerock.json.resource.SortKey;
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.openidm.workflow.activiti.ActivitiConstants;
import java.util.Map;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import org.activiti.engine.ProcessEngine;
import org.activiti.engine.TaskService;
import org.activiti.engine.form.FormProperty;
import org.activiti.engine.form.TaskFormData;
import org.activiti.engine.impl.identity.Authentication;
import org.activiti.engine.impl.persistence.entity.TaskEntity;
import org.activiti.engine.task.DelegationState;
import org.activiti.engine.task.Task;
import org.activiti.engine.task.TaskQuery;
import org.forgerock.json.JsonValue;
import org.forgerock.openidm.workflow.activiti.impl.mixin.TaskEntityMixIn;
import org.forgerock.util.promise.Promise;
/**
* Resource implementation of TaskInstance related Activiti operations
*/
public class TaskInstanceResource implements CollectionResourceProvider {
private final static ObjectMapper mapper;
private ProcessEngine processEngine;
static {
mapper = new ObjectMapper();
mapper.addMixIn(TaskEntity.class, TaskEntityMixIn.class);
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
}
public TaskInstanceResource(ProcessEngine processEngine) {
this.processEngine = processEngine;
}
public void setProcessEngine(ProcessEngine processEngine) {
this.processEngine = processEngine;
}
@Override
public Promise<ActionResponse, ResourceException> actionCollection(Context context, ActionRequest request) {
return notSupportedOnCollection(request).asPromise();
}
@Override
public Promise<ActionResponse, ResourceException> actionInstance(Context context, String resourceId, ActionRequest request) {
try {
Authentication.setAuthenticatedUserId(context.asContext(SecurityContext.class).getAuthenticationId());
TaskService taskService = processEngine.getTaskService();
Task task = processEngine.getTaskService().createTaskQuery().taskId(resourceId).singleResult();
if (task == null) {
return new NotFoundException().asPromise();
} else {
if ("claim".equals(request.getAction())) {
taskService.claim(resourceId, request.getContent().expect(Map.class).asMap().get("userId").toString());
} else if ("complete".equals(request.getAction())) {
taskService.complete(resourceId, request.getContent().expect(Map.class).asMap());
} else {
return new BadRequestException("Unknown action").asPromise();
}
Map<String, String> result = new HashMap<String, String>(1);
result.put("Task action performed", request.getAction());
return newActionResponse(new JsonValue(result)).asPromise();
}
} catch (Exception ex) {
return new InternalServerErrorException(ex.getMessage(), ex).asPromise();
}
}
@Override
public Promise<ResourceResponse, ResourceException> createInstance(Context context, CreateRequest request) {
return notSupportedOnInstance(request).asPromise();
}
@Override
public Promise<ResourceResponse, ResourceException> deleteInstance(Context context, String resourceId, DeleteRequest request) {
try {
Authentication.setAuthenticatedUserId(context.asContext(SecurityContext.class).getAuthenticationId());
Task task = processEngine.getTaskService().createTaskQuery().taskId(resourceId).singleResult();
if (task == null) {
return new NotFoundException("Task " + resourceId + " not found.").asPromise();
}
Map<String, Object> deletedTask = mapper.convertValue(task, Map.class);
processEngine.getTaskService()
.deleteTask(resourceId, request.getAdditionalParameter(ActivitiConstants.ACTIVITI_DELETEREASON));
return newResourceResponse(task.getId(), null, new JsonValue(deletedTask)).asPromise();
} catch (ActivitiObjectNotFoundException ex) {
return new NotFoundException(ex.getMessage()).asPromise();
} catch (Exception ex) {
return new InternalServerErrorException(ex.getMessage(), ex).asPromise();
}
}
@Override
public Promise<ResourceResponse, ResourceException> patchInstance(Context context, String resourceId, PatchRequest request) {
return notSupportedOnInstance(request).asPromise();
}
@Override
public Promise<QueryResponse, ResourceException> queryCollection(Context context, QueryRequest request, QueryResourceHandler handler) {
try {
Authentication.setAuthenticatedUserId(context.asContext(SecurityContext.class).getAuthenticationId());
TaskQuery query = processEngine.getTaskService().createTaskQuery();
if (ActivitiConstants.QUERY_FILTERED.equals(request.getQueryId())
|| ActivitiConstants.QUERY_ALL_IDS.equals(request.getQueryId())) {
if (ActivitiConstants.QUERY_FILTERED.equals(request.getQueryId())) {
setTaskParams(query, request);
}
setSortKeys(query, request);
List<Task> list = query.list();
for (Task taskInstance : list) {
Map value = mapper.convertValue(taskInstance, HashMap.class);
ResourceResponse r = newResourceResponse(taskInstance.getId(), null, new JsonValue(value));
if (taskInstance.getDelegationState() == DelegationState.PENDING) {
r.getContent().add(ActivitiConstants.ACTIVITI_DELEGATE, taskInstance.getAssignee());
} else {
r.getContent().add(ActivitiConstants.ACTIVITI_ASSIGNEE, taskInstance.getAssignee());
}
handler.handleResource(r);
}
return newQueryResponse().asPromise();
} else {
return new BadRequestException("Unknown query-id").asPromise();
}
} catch (NotSupportedException e) {
return e.asPromise();
} catch (Exception ex) {
return new InternalServerErrorException(ex.getMessage(), ex).asPromise();
}
}
@Override
public Promise<ResourceResponse, ResourceException> readInstance(Context context, String resourceId, ReadRequest request) {
try {
Authentication.setAuthenticatedUserId(context.asContext(SecurityContext.class).getAuthenticationId());
TaskQuery query = processEngine.getTaskService().createTaskQuery();
query.taskId(resourceId);
Task task = query.singleResult();
if (task == null) {
return new NotFoundException().asPromise();
} else {
Map value = mapper.convertValue(task, HashMap.class);
TaskFormData data = processEngine.getFormService().getTaskFormData(task.getId());
List<Map> propertyValues = new ArrayList<Map>();
for (FormProperty p : data.getFormProperties()) {
Map<String, String> entry = new HashMap<String, String>();
entry.put(p.getId(), p.getValue());
propertyValues.add(entry);
}
value.put(ActivitiConstants.FORMPROPERTIES, propertyValues);
if (task.getDelegationState() == DelegationState.PENDING) {
value.put(ActivitiConstants.ACTIVITI_DELEGATE, task.getAssignee());
} else {
value.put(ActivitiConstants.ACTIVITI_ASSIGNEE, task.getAssignee());
}
Map<String, Object> variables = processEngine.getTaskService().getVariables(task.getId());
if (variables.containsKey(ActivitiConstants.OPENIDM_CONTEXT)){
variables.remove(ActivitiConstants.OPENIDM_CONTEXT);
}
value.put(ActivitiConstants.ACTIVITI_VARIABLES, variables);
value.put("candidates", getCandidateIdentities(task).getObject());
return newResourceResponse(task.getId(), null, new JsonValue(value)).asPromise();
}
} catch (Exception ex) {
return new InternalServerErrorException(ex.getMessage(), ex).asPromise();
}
}
/**
* Retrieves candidate users and groups from a Task.
*
* @param task Task that needs to be searched
* @return JsonValue of candidates
*/
private JsonValue getCandidateIdentities(Task task) {
JsonValue candidates = json(object())
.add("candidateUsers", new HashSet<>())
.add("candidateGroups", new HashSet<>());
List<IdentityLink> candidateIdentity = processEngine.getTaskService().getIdentityLinksForTask(task.getId());
for (IdentityLink identityLink : candidateIdentity) {
if (identityLink.getUserId() != null) {
candidates.get("candidateUsers").asSet().add(identityLink.getUserId());
}
if (identityLink.getGroupId() != null) {
candidates.get("candidateGroups").asSet().add(identityLink.getGroupId());
}
}
return candidates;
}
@Override
public Promise<ResourceResponse, ResourceException> updateInstance(Context context, String resourceId, UpdateRequest request) {
try {
Authentication.setAuthenticatedUserId(context.asContext(SecurityContext.class).getAuthenticationId());
Task task = processEngine.getTaskService().createTaskQuery().taskId(resourceId).singleResult();
if (task == null) {
return new NotFoundException().asPromise();
} else {
Map value = request.getContent().expect(Map.class).asMap();
if (value.containsKey(ActivitiConstants.ACTIVITI_ASSIGNEE)) {
task.setAssignee(value.get(ActivitiConstants.ACTIVITI_ASSIGNEE) == null
? null : value.get(ActivitiConstants.ACTIVITI_ASSIGNEE).toString());
}
if (value.get(ActivitiConstants.ACTIVITI_DESCRIPTION) != null) {
task.setDescription(value.get(ActivitiConstants.ACTIVITI_DESCRIPTION).toString());
}
if (value.get(ActivitiConstants.ACTIVITI_NAME) != null) {
task.setName(value.get(ActivitiConstants.ACTIVITI_NAME).toString());
}
if (value.get(ActivitiConstants.ACTIVITI_OWNER) != null) {
task.setOwner(value.get(ActivitiConstants.ACTIVITI_OWNER).toString());
}
processEngine.getTaskService().saveTask(task);
Map<String, String> result = new HashMap<String, String>(1);
result.put("Task updated", resourceId);
return newResourceResponse(resourceId, null, new JsonValue(result)).asPromise();
}
} catch (Exception ex) {
return new InternalServerErrorException(ex.getMessage(), ex).asPromise();
}
}
/**
* Process the query parameters of the request and set it on the TaskQuery.
*
* @param query Query to update
* @param request incoming request
*/
private void setTaskParams(TaskQuery query, QueryRequest request) {
for (Map.Entry<String, String> param : request.getAdditionalParameters().entrySet()) {
switch (param.getKey()) {
case ActivitiConstants.ACTIVITI_EXECUTIONID:
query.executionId(param.getValue());
break;
case ActivitiConstants.ACTIVITI_PROCESSDEFINITIONID:
query.processDefinitionId(param.getValue());
break;
case ActivitiConstants.ACTIVITI_PROCESSDEFINITIONKEY:
query.processDefinitionKey(param.getValue());
break;
case ActivitiConstants.ACTIVITI_PROCESSINSTANCEID:
query.processInstanceId(param.getValue());
break;
case ActivitiConstants.ACTIVITI_ASSIGNEE:
query.taskAssignee(param.getValue());
break;
case ActivitiConstants.ACTIVITI_CANDIDATEGROUP:
String taskCandidateGroup = param.getValue();
String[] taskCandidateGroups = taskCandidateGroup.split(",");
if (taskCandidateGroups.length > 1) {
query.taskCandidateGroupIn(Arrays.asList(taskCandidateGroups));
} else {
query.taskCandidateGroup(taskCandidateGroup);
}
break;
case ActivitiConstants.ACTIVITI_CANDIDATEUSER:
query.taskCandidateUser(param.getValue());
break;
case ActivitiConstants.ID:
query.taskId(param.getValue());
break;
case ActivitiConstants.ACTIVITI_NAME:
query.taskName(param.getValue());
break;
case ActivitiConstants.ACTIVITI_OWNER:
query.taskOwner(param.getValue());
break;
case ActivitiConstants.ACTIVITI_DESCRIPTION:
query.taskDescription(param.getValue());
break;
case ActivitiConstants.ACTIVITI_PRIORITY:
query.taskPriority(Integer.parseInt(param.getValue()));
break;
case ActivitiConstants.ACTIVITI_UNASSIGNED:
if (Boolean.parseBoolean(param.getValue())) {
query.taskUnassigned();
}
break;
case ActivitiConstants.ACTIVITI_TENANTID:
query.taskTenantId(param.getValue());
break;
}
}
Map<String, String> wfParams = ActivitiUtil.fetchVarParams(request);
Iterator<Map.Entry<String, String>> itWf = wfParams.entrySet().iterator();
while (itWf.hasNext()) {
Map.Entry<String, String> e = itWf.next();
query = query.processVariableValueEquals(e.getKey(), e.getValue());
}
}
/**
* Sets what the result set should be filtered by.
*
* @param query TaskQuery that needs to be modified for filtering
* @param request incoming request
* @throws NotSupportedException
*/
private void setSortKeys(TaskQuery query, QueryRequest request) throws NotSupportedException {
for (SortKey key : request.getSortKeys()) {
if (key.getField() != null && !key.getField().isEmpty()) {
switch (key.getField().toString().substring(1)) { // remove leading JsonPointer slash
case ActivitiConstants.ID:
query.orderByTaskId();
break;
case ActivitiConstants.ACTIVITI_NAME:
query.orderByTaskName();
break;
case ActivitiConstants.ACTIVITI_DESCRIPTION:
query.orderByTaskDescription();
break;
case ActivitiConstants.ACTIVITI_PRIORITY:
query.orderByTaskPriority();
break;
case ActivitiConstants.ACTIVITI_ASSIGNEE:
query.orderByTaskAssignee();
break;
case ActivitiConstants.ACTIVITI_CREATETIME:
query.orderByTaskCreateTime();
break;
case ActivitiConstants.ACTIVITI_PROCESSINSTANCEID:
query.orderByProcessInstanceId();
break;
case ActivitiConstants.ACTIVITI_EXECUTIONID:
query.orderByExecutionId();
break;
case ActivitiConstants.ACTIVITI_DUEDATE:
query.orderByDueDate();
break;
case ActivitiConstants.ACTIVITI_TENANTID:
query.orderByTenantId();
break;
default:
throw new NotSupportedException("Sort key: " + key.getField().toString().substring(1) + " is not valid");
}
query = key.isAscendingOrder() ? query.asc() : query.desc();
}
}
}
}