/*
* Copyright 2013 JBoss Inc
*
* 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 org.overlord.dtgov.taskapi;
import java.security.Principal;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.OptimisticLockException;
import javax.servlet.http.HttpServletRequest;
import javax.transaction.RollbackException;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import org.jboss.seam.transaction.Transactional;
import org.jbpm.kie.services.impl.KModuleDeploymentUnit;
import org.jbpm.services.task.exception.PermissionDeniedException;
import org.jbpm.services.task.utils.ContentMarshallerHelper;
import org.kie.api.runtime.manager.RuntimeEngine;
import org.kie.api.runtime.manager.RuntimeManager;
import org.kie.api.task.TaskService;
import org.kie.api.task.model.Content;
import org.kie.api.task.model.I18NText;
import org.kie.api.task.model.Task;
import org.kie.api.task.model.TaskData;
import org.kie.api.task.model.TaskSummary;
import org.kie.api.task.model.User;
import org.kie.internal.runtime.manager.context.EmptyContext;
import org.overlord.dtgov.jbpm.ProcessOperationException;
import org.overlord.dtgov.jbpm.util.KieJar;
import org.overlord.dtgov.jbpm.util.KieSrampUtil;
import org.overlord.dtgov.jbpm.util.ProcessEngineService;
import org.overlord.dtgov.jbpm.util.WorkflowUtil;
import org.overlord.dtgov.server.i18n.Messages;
import org.overlord.dtgov.taskapi.types.FindTasksRequest;
import org.overlord.dtgov.taskapi.types.FindTasksResponse;
import org.overlord.dtgov.taskapi.types.StatusType;
import org.overlord.dtgov.taskapi.types.TaskDataType;
import org.overlord.dtgov.taskapi.types.TaskDataType.Entry;
import org.overlord.dtgov.taskapi.types.TaskSummaryType;
import org.overlord.dtgov.taskapi.types.TaskType;
import org.overlord.sramp.client.SrampAtomApiClient;
import org.overlord.sramp.governance.SrampAtomApiClientFactory;
/**
*
* @author eric.wittmann@redhat.com
*/
@ApplicationScoped
@Transactional
@Path("/tasks")
public class TaskApi {
private static Boolean hasSRAMPPackageDeployed = Boolean.FALSE;
@Inject
TaskService taskService;
@Inject
@ApplicationScoped
private ProcessEngineService processEngineService;
@PostConstruct
public void configure() {
//we need it to start to startup task management - however
//we don't want it to start before we have the workflow are
//definitions deployed (on first time boot)
synchronized(hasSRAMPPackageDeployed) {
KieSrampUtil kieSrampUtil = new KieSrampUtil();
SrampAtomApiClient client = SrampAtomApiClientFactory.createAtomApiClient();
// With this easy instruction we get all the kie jar artifacts
List<KieJar> workflows = WorkflowUtil.getCurrentKieJar(client);
if(workflows!=null && workflows.size()>0){
// Iterate over all the workflows defined and then initialized
// the runtime manager
for(KieJar workflow:workflows){
if (kieSrampUtil.isSRAMPPackageDeployed(workflow.getGroupId(), workflow.getArtifactId(), workflow.getVersion())) {
KModuleDeploymentUnit unit = new KModuleDeploymentUnit(
workflow.getGroupId(),
workflow.getArtifactId(),
workflow.getVersion(),
workflow.getWorkflowPackage(),
workflow.getWorkflowKSession());
RuntimeManager runtimeManager = kieSrampUtil.getRuntimeManager(processEngineService, unit);
RuntimeEngine runtime = runtimeManager.getRuntimeEngine(EmptyContext.get());
//use toString to make sure CDI initializes the bean
//to make sure the task manager starts up on reboot
runtime.getTaskService().toString();
}
}
}
}
}
/**
* Constructor.
*/
public TaskApi() {}
/**
* Gets a list of all tasks for the authenticated user.
* @param uri
* @throws Exception
*/
@GET
@Path("list")
@Produces(MediaType.APPLICATION_XML)
public FindTasksResponse listTasks(
@Context HttpServletRequest httpRequest,
@QueryParam("startIndex") Integer startIndex,
@QueryParam("endIndex") Integer endIndex,
@QueryParam("orderBy") String orderBy,
@QueryParam("orderAscending") Boolean orderAscending,
@QueryParam("status") String status,
@QueryParam("priority") Integer priority) throws Exception {
FindTasksRequest findTasksReq = new FindTasksRequest();
findTasksReq.setStartIndex(0);
if (startIndex != null) {
findTasksReq.setStartIndex(startIndex);
}
findTasksReq.setEndIndex(19);
if (endIndex != null) {
findTasksReq.setEndIndex(endIndex);
}
findTasksReq.setOrderBy("priority"); //$NON-NLS-1$
if (orderBy != null) {
findTasksReq.setOrderBy(orderBy);
}
findTasksReq.setOrderAscending(false);
if (orderAscending != null) {
findTasksReq.setOrderAscending(orderAscending);
}
findTasksReq.getPriority().clear();
if (priority != null) {
findTasksReq.getPriority().add(priority);
}
findTasksReq.getStatus().clear();
if (status != null) {
findTasksReq.getStatus().add(StatusType.fromValue(status));
}
return findTasks(findTasksReq, httpRequest);
}
/**
* Gets a list of all tasks for the authenticated user. Filters the list based on the
* criteria included in the {@link FindTasksRequest}.
* @param findTasksRequest
* @param httpRequest
* @throws Exception
*/
@POST
@Path("find")
@Produces(MediaType.APPLICATION_XML)
@Consumes(MediaType.APPLICATION_XML)
public FindTasksResponse findTasks(final FindTasksRequest findTasksRequest, @Context HttpServletRequest httpRequest) throws Exception {
String currentUser = assertCurrentUser(httpRequest);
FindTasksResponse response = new FindTasksResponse();
// Get all tasks - the ones assigned as potential owner *and* the ones assigned as owner. If
// there is overlap we'll deal with that during the sort.
String language = "en-UK"; //$NON-NLS-1$
// if (httpRequest.getLocale() != null) {
// language = httpRequest.getLocale().toString();
// }
List<TaskSummary> list = taskService.getTasksAssignedAsPotentialOwner(currentUser, language);
list.addAll(taskService.getTasksOwned(currentUser, language));
final String orderBy = findTasksRequest.getOrderBy() == null ? "priority" : findTasksRequest.getOrderBy(); //$NON-NLS-1$
final boolean ascending = findTasksRequest.isOrderAscending();
TreeSet<TaskSummary> sortedFiltered = new TreeSet<TaskSummary>(new TaskSummaryComparator(orderBy, ascending));
for (TaskSummary task : list) {
if (accepts(task, findTasksRequest)) {
sortedFiltered.add(task);
}
}
int startIdx = findTasksRequest.getStartIndex();
int endIdx = findTasksRequest.getEndIndex();
int idx = 0;
for (TaskSummary task : sortedFiltered) {
if (idx >= startIdx && idx <= endIdx) {
TaskSummaryType taskSummary = new TaskSummaryType();
taskSummary.setId(String.valueOf(task.getId()));
taskSummary.setName(task.getName());
User actualOwner = task.getActualOwner();
if (actualOwner != null) {
taskSummary.setOwner(actualOwner.getId());
}
taskSummary.setPriority(task.getPriority());
taskSummary.setStatus(StatusType.fromValue(task.getStatus().toString()));
response.getTaskSummary().add(taskSummary);
}
idx++;
}
response.setTotalResults(sortedFiltered.size());
return response;
}
/**
* Fetches a single task by its unique ID.
* @param httpRequest
* @param taskId
* @throws Exception
*/
@GET
@Path("get/{taskId}")
@Produces(MediaType.APPLICATION_XML)
public TaskType getTask(@Context HttpServletRequest httpRequest, @PathParam("taskId") long taskId)
throws Exception {
assertCurrentUser(httpRequest);
Task task = taskService.getTaskById(taskId);
TaskType rval = new TaskType();
List<I18NText> descriptions = task.getDescriptions();
if (descriptions != null && !descriptions.isEmpty()) {
rval.setDescription(descriptions.iterator().next().getText());
}
List<I18NText> names = task.getNames();
if (names != null && !names.isEmpty()) {
rval.setName(names.iterator().next().getText());
}
rval.setPriority(task.getPriority());
rval.setId(String.valueOf(task.getId()));
rval.setType(task.getTaskType());
TaskData taskData = task.getTaskData();
if (taskData != null) {
User owner = taskData.getActualOwner();
if (owner != null) {
rval.setOwner(owner.getId());
}
Date expTime = taskData.getExpirationTime();
if (expTime != null) {
GregorianCalendar cal = new GregorianCalendar();
cal.setTime(expTime);
DatatypeFactory dtFactory = DatatypeFactory.newInstance();
rval.setDueDate(dtFactory.newXMLGregorianCalendar(cal));
}
rval.setStatus(StatusType.fromValue(taskData.getStatus().toString()));
}
long docId = taskService.getTaskById(taskId).getTaskData().getDocumentContentId();
if (docId > 0) {
//Set the input params
Content content = taskService.getContentById(docId);
@SuppressWarnings("unchecked")
Map<String,Object> inputParams = (Map<String, Object>) ContentMarshallerHelper.unmarshall(content.getContent(), null);
if (inputParams!=null && inputParams.size() > 0) {
if (rval.getTaskData()==null) rval.setTaskData(new TaskDataType());
for ( String key : inputParams.keySet()) {
Entry entry = new Entry();
entry.setKey(key);
entry.setValue(String.valueOf(inputParams.get(key)));
rval.getTaskData().getEntry().add(entry);
}
}
}
return rval;
}
/**
* Called to claim a task.
* @param httpRequest
* @param taskId
* @throws Exception
*/
@GET
@Path("claim/{taskId}")
@Produces(MediaType.APPLICATION_XML)
public TaskType claimTask(@Context HttpServletRequest httpRequest, @PathParam("taskId") long taskId)
throws Exception {
String currentUser = assertCurrentUser(httpRequest);
try {
taskService.claim(taskId, currentUser);
} catch (Exception e) {
handleException(e);
}
return getTask(httpRequest, taskId);
}
/**
* Called to release a task.
* @param httpRequest
* @param taskId
* @throws Exception
*/
@GET
@Path("release/{taskId}")
@Produces(MediaType.APPLICATION_XML)
public TaskType releaseTask(@Context HttpServletRequest httpRequest, @PathParam("taskId") long taskId)
throws Exception {
String currentUser = assertCurrentUser(httpRequest);
try {
taskService.release(taskId, currentUser);
} catch (Exception e) {
handleException(e);
}
return getTask(httpRequest, taskId);
}
/**
* Called to start a task.
* @param httpRequest
* @param taskId
* @throws Exception
*/
@GET
@Path("start/{taskId}")
@Produces(MediaType.APPLICATION_XML)
public TaskType startTask(@Context HttpServletRequest httpRequest, @PathParam("taskId") long taskId)
throws Exception {
String currentUser = assertCurrentUser(httpRequest);
try {
taskService.start(taskId, currentUser);
} catch (Exception e) {
handleException(e);
}
return getTask(httpRequest, taskId);
}
/**
* Called to stop a task.
* @param httpRequest
* @param taskId
* @throws Exception
*/
@GET
@Path("stop/{taskId}")
@Produces(MediaType.APPLICATION_XML)
public TaskType stopTask(@Context HttpServletRequest httpRequest, @PathParam("taskId") long taskId)
throws Exception {
String currentUser = assertCurrentUser(httpRequest);
try {
taskService.stop(taskId, currentUser);
} catch (Exception e) {
handleException(e);
}
return getTask(httpRequest, taskId);
}
/**
* Called to complete a task.
* @param taskData
* @param httpRequest
* @param taskId
* @throws Exception
*/
@POST
@Path("complete/{taskId}")
@Consumes(MediaType.APPLICATION_XML)
@Produces(MediaType.APPLICATION_XML)
public TaskType completeTask(final TaskDataType taskData, @Context HttpServletRequest httpRequest, @PathParam("taskId") long taskId)
throws Exception {
String currentUser = assertCurrentUser(httpRequest);
try {
Map<String, Object> data = taskDataAsMap(taskData);
taskService.complete(taskId, currentUser, data);
} catch (Exception e) {
handleException(e);
}
return getTask(httpRequest, taskId);
}
/**
* Called to fail a task.
* @param httpRequest
* @param taskId
* @throws Exception
*/
@POST
@Path("fail/{taskId}")
@Consumes(MediaType.APPLICATION_XML)
@Produces(MediaType.APPLICATION_XML)
public TaskType failTask(final TaskDataType taskData, @Context HttpServletRequest httpRequest, @PathParam("taskId") long taskId)
throws Exception {
String currentUser = assertCurrentUser(httpRequest);
try {
Map<String, Object> data = taskDataAsMap(taskData);
taskService.fail(taskId, currentUser, data);
} catch (Exception e) {
handleException(e);
}
return getTask(httpRequest, taskId);
}
/**
* Converts the inbound task data payload into a map useable by jbpm.
* @param taskData
*/
private Map<String, Object> taskDataAsMap(TaskDataType taskData) {
Map<String, Object> data = new HashMap<String, Object>();
// TODO missing type mappings here - can we convert types in some way based on a schema or something? right now everything is a string
for (Entry entry : taskData.getEntry()) {
data.put(entry.getKey(), entry.getValue());
}
return data;
}
/**
* Handles an exception that comes out of one of the task operations. This provides a common
* way to handle transaction rollbacks (when necessary) and also re-throws the appropriate
* exceptions.
* @param error
* @throws Exception
*/
protected void handleException(Exception error) throws Exception {
if (error instanceof RollbackException) {
Throwable cause = error.getCause();
if (cause != null && cause instanceof OptimisticLockException) {
// Concurrent access to the same process instance
throw new ProcessOperationException(Messages.i18n.format("TaskApi.ConcurrentTaskAccessError"), error); //$NON-NLS-1$
}
throw error;
}
if (error instanceof PermissionDeniedException) {
// Probably the task has already been started by other users
throw new ProcessOperationException(Messages.i18n.format("TaskApi.AlreadyClaimed"), error); //$NON-NLS-1$
}
throw error;
}
/**
* Asserts that a user is logged in and then returns the user's id.
* @param httpRequest
* @throws Exception
*/
protected String assertCurrentUser(HttpServletRequest httpRequest) throws Exception {
Principal principal = httpRequest.getUserPrincipal();
if (principal == null) {
throw new Exception(Messages.i18n.format("TaskApi.NoAuthError")); //$NON-NLS-1$
}
return principal.getName();
}
/**
* Returns true if the given task should be included in the result set based on the
* criteria found in the request.
* @param task
* @param findTasksRequest
*/
private boolean accepts(TaskSummary task, FindTasksRequest findTasksRequest) {
Set<Integer> priorities = new HashSet<Integer>(findTasksRequest.getPriority());
Set<StatusType> statuses = new HashSet<StatusType>(findTasksRequest.getStatus());
if (!priorities.isEmpty() && !priorities.contains(task.getPriority())) {
return false;
}
if (!statuses.isEmpty() && !statuses.contains(StatusType.fromValue(task.getStatus().toString()))) {
return false;
}
XMLGregorianCalendar from = findTasksRequest.getDueOnFrom();
if (from != null) {
Date expirationTime = task.getExpirationTime();
if (expirationTime == null) {
return false;
}
if (expirationTime.compareTo(from.toGregorianCalendar().getTime()) < 0) {
return false;
}
}
XMLGregorianCalendar to = findTasksRequest.getDueOnTo();
if (to != null) {
Date expirationTime = task.getExpirationTime();
if (expirationTime == null) {
return false;
}
if (expirationTime.compareTo(to.toGregorianCalendar().getTime()) > 0) {
return false;
}
}
return true;
}
}