/*
* (C) Copyright 2012-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* 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.
*
* Contributors:
* ldoguin, Antoine Taillefer
*/
package org.nuxeo.ecm.platform.task.core.service;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.nuxeo.common.utils.Path;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentNotFoundException;
import org.nuxeo.ecm.core.api.DocumentRef;
import org.nuxeo.ecm.core.api.IdRef;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.api.NuxeoGroup;
import org.nuxeo.ecm.core.api.NuxeoPrincipal;
import org.nuxeo.ecm.core.api.PathRef;
import org.nuxeo.ecm.core.api.SortInfo;
import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner;
import org.nuxeo.ecm.core.api.security.ACE;
import org.nuxeo.ecm.core.api.security.ACL;
import org.nuxeo.ecm.core.api.security.ACP;
import org.nuxeo.ecm.core.api.security.SecurityConstants;
import org.nuxeo.ecm.platform.ec.notification.NotificationConstants;
import org.nuxeo.ecm.platform.task.Task;
import org.nuxeo.ecm.platform.task.TaskConstants;
import org.nuxeo.ecm.platform.task.TaskEventNames;
import org.nuxeo.ecm.platform.task.TaskPersisterDescriptor;
import org.nuxeo.ecm.platform.task.TaskProvider;
import org.nuxeo.ecm.platform.task.TaskProviderDescriptor;
import org.nuxeo.ecm.platform.task.TaskService;
import org.nuxeo.runtime.model.ComponentContext;
import org.nuxeo.runtime.model.ComponentInstance;
import org.nuxeo.runtime.model.ComponentName;
import org.nuxeo.runtime.model.DefaultComponent;
/**
* @author <a href="mailto:ldoguin@nuxeo.com">Laurent Doguin</a>
* @since 5.5
*/
public class TaskServiceImpl extends DefaultComponent implements TaskService {
private static final long serialVersionUID = 1L;
public static final ComponentName NAME = new ComponentName("org.nuxeo.ecm.platform.task.core.TaskService");
public static final String DEFAULT_TASK_PROVIDER = "documentTaskProvider";
private static final String TASK_PROVIDER_XP = "taskProvider";
private static final String TASK_PERSISTER_XP = "taskPersister";
private Map<String, TaskProvider> tasksProviders;
private String parentPath = "/task-root";
@Override
public void activate(ComponentContext context) {
super.activate(context);
tasksProviders = new HashMap<>();
}
@Override
public void deactivate(ComponentContext context) {
super.deactivate(context);
tasksProviders = null;
}
@Override
public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
if (extensionPoint.equals(TASK_PROVIDER_XP)) {
if (contribution instanceof TaskProviderDescriptor) {
TaskProviderDescriptor taskProviderDescriptor = (TaskProviderDescriptor) contribution;
String providerId = taskProviderDescriptor.getId();
if (taskProviderDescriptor.isEnabled()) {
tasksProviders.put(providerId, taskProviderDescriptor.getNewInstance());
} else {
if (tasksProviders.get(providerId) != null) {
tasksProviders.remove(providerId);
}
}
}
} else if (extensionPoint.equals(TASK_PERSISTER_XP)) {
if (contribution instanceof TaskPersisterDescriptor) {
TaskPersisterDescriptor taskPersisterDescriptor = (TaskPersisterDescriptor) contribution;
parentPath = taskPersisterDescriptor.getPath();
}
}
}
@Override
public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
if (extensionPoint.equals(TASK_PROVIDER_XP)) {
if (contribution instanceof TaskProviderDescriptor) {
TaskProviderDescriptor taskProviderDescriptor = (TaskProviderDescriptor) contribution;
String providerId = taskProviderDescriptor.getId();
if (tasksProviders.get(providerId) != null) {
tasksProviders.remove(providerId);
}
}
}
}
@Override
public List<Task> createTask(CoreSession coreSession, NuxeoPrincipal principal, DocumentModel document,
String taskName, List<String> actorIds, boolean createOneTaskPerActor, String directive, String comment,
Date dueDate, Map<String, String> taskVariables, String parentPath) {
return createTask(coreSession, principal, document, taskName, null, null, actorIds, createOneTaskPerActor,
directive, comment, dueDate, taskVariables, parentPath);
}
/**
* @since 5.6
*/
@Override
public List<Task> createTask(CoreSession coreSession, NuxeoPrincipal principal, DocumentModel document,
String taskDocumentType, String taskName, String taskType, String processId, List<String> actorIds,
boolean createOneTaskPerActor, String directive, String comment, Date dueDate,
Map<String, String> taskVariables, String parentPath, Map<String, Serializable> eventInfo) {
List<DocumentModel> docs = new ArrayList<>();
docs.add(document);
return createTaskForProcess(coreSession, principal, docs, taskDocumentType, taskName, taskType, processId, null,
actorIds, createOneTaskPerActor, directive, comment, dueDate, taskVariables, parentPath, eventInfo);
}
/**
* @since 5.6
*/
@Override
public List<Task> createTask(CoreSession coreSession, NuxeoPrincipal principal, DocumentModel document,
String taskName, String taskType, String processId, List<String> prefixedActorIds,
boolean createOneTaskPerActor, String directive, String comment, Date dueDate,
Map<String, String> taskVariables, String parentPath) {
return createTask(coreSession, principal, document, TaskConstants.TASK_TYPE_NAME, taskName, taskType, processId,
prefixedActorIds, createOneTaskPerActor, directive, comment, dueDate, taskVariables, parentPath, null);
}
@Override
public String acceptTask(CoreSession coreSession, NuxeoPrincipal principal, Task task, String comment) {
return endTask(coreSession, principal, task, comment, TaskEventNames.WORKFLOW_TASK_COMPLETED, true);
}
@Override
public String rejectTask(CoreSession coreSession, NuxeoPrincipal principal, Task task, String comment) {
return endTask(coreSession, principal, task, comment, TaskEventNames.WORKFLOW_TASK_REJECTED, false);
}
/**
* Use the task provider held by the {@link Task#TASK_PROVIDER_KEY} task variable to end the {@code task}. If null
* use the {@link #DEFAULT_TASK_PROVIDER}.
*/
@Override
public String endTask(CoreSession coreSession, NuxeoPrincipal principal, Task task, String comment,
String eventName, boolean isValidated) {
if (!canEndTask(principal, task)) {
throw new NuxeoException(String.format("User with id '%s' cannot end this task", principal.getName()));
}
String taskProviderId = task.getVariable(Task.TASK_PROVIDER_KEY);
if (taskProviderId == null) {
taskProviderId = DEFAULT_TASK_PROVIDER;
}
TaskProvider taskProvider = tasksProviders.get(taskProviderId);
if (taskProvider == null) {
throw new NuxeoException(String.format(
"No task provider registered, cannot end task. Please contribute at least the default task provider: %s.",
DEFAULT_TASK_PROVIDER));
}
return taskProvider.endTask(coreSession, principal, task, comment, eventName, isValidated);
}
@Override
public boolean canEndTask(NuxeoPrincipal principal, Task task) {
if (task != null && (!task.isCancelled() && !task.hasEnded())) {
return principal.isAdministrator() || principal.getName().equals(task.getInitiator())
|| isTaskAssignedToUser(task, principal, true);
}
return false;
}
protected boolean isTaskAssignedToUser(Task task, NuxeoPrincipal user, boolean checkDelegatedActors) {
if (task != null && user != null) {
// user actors
List<String> actors = user.getAllGroups();
actors.add(user.getName());
// initiator
if (actors.contains(task.getInitiator())) {
return true;
}
// users
List<String> users = task.getActors();
if (checkDelegatedActors) {
users.addAll(task.getDelegatedActors());
}
if (users != null) {
for (String userName : users) {
if (userName.startsWith(NuxeoPrincipal.PREFIX)) {
if (actors.contains(userName.substring(NuxeoPrincipal.PREFIX.length()))) {
return true;
}
} else if (userName.startsWith(NuxeoGroup.PREFIX)) {
if (actors.contains(userName.substring(NuxeoGroup.PREFIX.length()))) {
return true;
}
} else if (actors.contains(userName)) {
return true;
}
}
}
}
return false;
}
@Override
public Task getTask(CoreSession coreSession, String taskId) {
DocumentRef docRef = new IdRef(taskId);
DocumentModel taskDoc = coreSession.getDocument(docRef);
if (taskDoc != null) {
Task task = taskDoc.getAdapter(Task.class);
if (task != null) {
return task;
}
}
return null;
}
@Override
public void deleteTask(CoreSession coreSession, String taskId) {
final DocumentRef docRef = new IdRef(taskId);
UnrestrictedSessionRunner runner = new UnrestrictedSessionRunner(coreSession) {
@Override
public void run() {
session.removeDocument(docRef);
}
};
runner.runUnrestricted();
}
@Override
public DocumentModel getTargetDocumentModel(Task task, CoreSession coreSession) {
try {
// TODO handle while target documents from task
return coreSession.getDocument(new IdRef(task.getTargetDocumentsIds().get(0)));
} catch (DocumentNotFoundException e) {
return null;
}
}
@Override
public List<Task> getCurrentTaskInstances(CoreSession coreSession) {
List<Task> tasks = new ArrayList<>();
List<Task> newTasks;
for (TaskProvider taskProvider : tasksProviders.values()) {
newTasks = taskProvider.getCurrentTaskInstances(coreSession);
if (newTasks != null) {
tasks.addAll(newTasks);
}
}
return tasks;
}
/**
* Provide @param sortInfo to handle sort page-provider contributions (see {@link #getCurrentTaskInstances})
*
* @since 5.9.3
*/
@Override
public List<Task> getCurrentTaskInstances(CoreSession coreSession, List<SortInfo> sortInfos) {
List<Task> tasks = new ArrayList<>();
List<Task> newTasks;
for (TaskProvider taskProvider : tasksProviders.values()) {
newTasks = taskProvider.getCurrentTaskInstances(coreSession, sortInfos);
if (newTasks != null) {
tasks.addAll(newTasks);
}
}
return tasks;
}
@Override
public List<Task> getAllCurrentTaskInstances(CoreSession coreSession, List<SortInfo> sortInfos) {
List<Task> tasks = new ArrayList<>();
List<Task> newTasks;
for (TaskProvider taskProvider : tasksProviders.values()) {
newTasks = taskProvider.getAllCurrentTaskInstances(coreSession, sortInfos);
if (newTasks != null) {
tasks.addAll(newTasks);
}
}
return tasks;
}
/**
* Returns a list of task instances assigned to one of the actors in the list or to its pool.
*
* @param actors a list used as actorId to retrieve the tasks.
*/
@Override
public List<Task> getCurrentTaskInstances(List<String> actors, CoreSession coreSession) {
List<Task> tasks = new ArrayList<>();
List<Task> newTasks;
for (TaskProvider taskProvider : tasksProviders.values()) {
newTasks = taskProvider.getCurrentTaskInstances(actors, coreSession);
if (newTasks != null) {
tasks.addAll(newTasks);
}
}
return tasks;
}
/**
* Provide @param sortInfo to handle sort page-provider contributions (see {@link #getCurrentTaskInstances})
*
* @since 5.9.3
*/
@Override
public List<Task> getCurrentTaskInstances(List<String> actors, CoreSession coreSession, List<SortInfo> sortInfos) {
List<Task> tasks = new ArrayList<>();
List<Task> newTasks;
for (TaskProvider taskProvider : tasksProviders.values()) {
newTasks = taskProvider.getCurrentTaskInstances(actors, coreSession, sortInfos);
if (newTasks != null) {
tasks.addAll(newTasks);
}
}
return tasks;
}
@Override
public List<Task> getTaskInstances(DocumentModel dm, NuxeoPrincipal user, CoreSession coreSession) {
List<Task> tasks = new ArrayList<>();
List<Task> newTasks;
for (TaskProvider taskProvider : tasksProviders.values()) {
newTasks = taskProvider.getTaskInstances(dm, user, coreSession);
if (newTasks != null) {
tasks.addAll(newTasks);
}
}
return tasks;
}
@Override
public List<Task> getTaskInstances(DocumentModel dm, List<String> actors, CoreSession coreSession) {
List<Task> tasks = new ArrayList<>();
List<Task> newTasks;
for (TaskProvider taskProvider : tasksProviders.values()) {
newTasks = taskProvider.getTaskInstances(dm, actors, coreSession);
if (newTasks != null) {
tasks.addAll(newTasks);
}
}
return tasks;
}
@Override
public List<Task> getAllTaskInstances(String processId, CoreSession session) {
List<Task> tasks = new ArrayList<>();
List<Task> newTasks;
for (TaskProvider taskProvider : tasksProviders.values()) {
newTasks = taskProvider.getAllTaskInstances(processId, session);
if (newTasks != null) {
tasks.addAll(newTasks);
}
}
return tasks;
}
@Override
public List<Task> getAllTaskInstances(String processId, NuxeoPrincipal user, CoreSession session) {
List<Task> tasks = new ArrayList<>();
List<Task> newTasks;
for (TaskProvider taskProvider : tasksProviders.values()) {
newTasks = taskProvider.getAllTaskInstances(processId, user, session);
if (newTasks != null) {
tasks.addAll(newTasks);
}
}
return tasks;
}
@Override
public List<Task> getAllTaskInstances(String processId, List<String> actors, CoreSession session) {
List<Task> tasks = new ArrayList<>();
List<Task> newTasks;
for (TaskProvider taskProvider : tasksProviders.values()) {
newTasks = taskProvider.getAllTaskInstances(processId, actors, session);
if (newTasks != null) {
tasks.addAll(newTasks);
}
}
return tasks;
}
@Override
public String getTaskRootParentPath(CoreSession coreSession) {
GetTaskRootParentPathUnrestricted runner = new GetTaskRootParentPathUnrestricted(coreSession);
runner.runUnrestricted();
return runner.getParentPath();
}
public class GetTaskRootParentPathUnrestricted extends UnrestrictedSessionRunner {
protected DocumentModel taskRootDoc;
public GetTaskRootParentPathUnrestricted(CoreSession session) {
super(session);
}
@Override
public void run() {
DocumentRef pathRef = new PathRef(parentPath);
if (session.exists(pathRef)) {
taskRootDoc = session.getDocument(pathRef);
} else {
Path path = new Path(parentPath);
taskRootDoc = session.createDocumentModel(path.removeLastSegments(1).toString(), path.lastSegment(),
TaskConstants.TASK_ROOT_TYPE_NAME);
taskRootDoc = session.createDocument(taskRootDoc);
ACP acp = taskRootDoc.getACP();
ACL acl = acp.getOrCreateACL(ACL.LOCAL_ACL);
acl.add(new ACE("Everyone", "Everything", false));
taskRootDoc.setACP(acp, true);
taskRootDoc = session.saveDocument(taskRootDoc);
}
}
public DocumentModel getTaskRootDoc() {
return taskRootDoc;
}
public String getParentPath() {
return taskRootDoc.getPathAsString();
}
}
@Override
public List<Task> getAllTaskInstances(String processId, String nodeId, CoreSession session) {
List<Task> tasks = new ArrayList<>();
List<Task> newTasks;
for (TaskProvider taskProvider : tasksProviders.values()) {
newTasks = taskProvider.getAllTaskInstances(processId, nodeId, session);
if (newTasks != null) {
tasks.addAll(newTasks);
}
}
return tasks;
}
@Override
public void reassignTask(CoreSession session, final String taskId, final List<String> newActors,
final String comment) {
new UnrestrictedSessionRunner(session) {
@Override
public void run() {
DocumentModel taskDoc = session.getDocument(new IdRef(taskId));
Task task = taskDoc.getAdapter(Task.class);
if (task == null) {
throw new NuxeoException("Invalid taskId: " + taskId);
}
List<String> currentAssignees = task.getActors();
List<String> currentActors = new ArrayList<>();
for (String currentAssignee : currentAssignees) {
if (currentAssignee.startsWith(NotificationConstants.GROUP_PREFIX)
|| currentAssignee.startsWith(NotificationConstants.USER_PREFIX)) {
// prefixed assignees with "user:" or "group:"
currentActors.add(currentAssignee.substring(currentAssignee.indexOf(":") + 1));
} else {
currentActors.add(currentAssignee);
}
}
String taskInitator = task.getInitiator();
// remove ACLs set for current assignees
ACP acp = taskDoc.getACP();
ACL acl = acp.getOrCreateACL(ACL.LOCAL_ACL);
List<ACE> toRemove = new ArrayList<>();
for (ACE ace : acl.getACEs()) {
if (currentActors.contains(ace.getUsername()) || taskInitator.equals(ace.getUsername())) {
toRemove.add(ace);
}
}
acl.removeAll(toRemove);
// grant EVERYTHING on task doc to the new actors
List<String> actorIds = new ArrayList<>();
for (String actor : newActors) {
if (actor.startsWith(NotificationConstants.GROUP_PREFIX)
|| actor.startsWith(NotificationConstants.USER_PREFIX)) {
// prefixed assignees with "user:" or "group:"
actorIds.add(actor.substring(actor.indexOf(":") + 1));
} else {
actorIds.add(actor);
}
}
for (String actorId : actorIds) {
acl.add(new ACE(actorId, SecurityConstants.EVERYTHING, true));
}
taskDoc.setACP(acp, true);
task.setActors(actorIds);
String currentUser = ((NuxeoPrincipal) session.getPrincipal()).getActingUser();
task.addComment(currentUser, comment);
session.saveDocument(taskDoc);
List<DocumentModel> docs = new ArrayList<>();
for (String string : task.getTargetDocumentsIds()) {
docs.add(session.getDocument(new IdRef(string)));
}
notifyEvent(session, task, docs, TaskEventNames.WORKFLOW_TASK_REASSIGNED, new HashMap<>(), comment,
(NuxeoPrincipal) session.getPrincipal(), actorIds);
}
}.runUnrestricted();
}
@Override
public void delegateTask(CoreSession session, final String taskId, final List<String> delegatedActors,
final String comment) {
new UnrestrictedSessionRunner(session) {
@Override
public void run() {
DocumentModel taskDoc = session.getDocument(new IdRef(taskId));
Task task = taskDoc.getAdapter(Task.class);
if (task == null) {
throw new NuxeoException("Invalid taskId: " + taskId);
}
// grant EVERYTHING on task doc to the delegated actors
List<String> actorIds = new ArrayList<>();
ACP acp = taskDoc.getACP();
ACL acl = acp.getOrCreateACL(ACL.LOCAL_ACL);
for (String actor : delegatedActors) {
if (actor.startsWith(NotificationConstants.GROUP_PREFIX)
|| actor.startsWith(NotificationConstants.USER_PREFIX)) {
// prefixed assignees with "user:" or "group:"
actorIds.add(actor.substring(actor.indexOf(":") + 1));
} else {
actorIds.add(actor);
}
}
for (String actorId : actorIds) {
ACE ace = new ACE(actorId, SecurityConstants.EVERYTHING, true);
if (!acl.contains(ace)) {
acl.add(ace);
}
}
taskDoc.setACP(acp, true);
List<String> allDelegatedActors = new ArrayList<>();
allDelegatedActors.addAll(task.getDelegatedActors());
for (String actor : actorIds) {
if (!allDelegatedActors.contains(actor)) {
allDelegatedActors.add(actor);
}
}
task.setDelegatedActors(allDelegatedActors);
String currentUser = ((NuxeoPrincipal) session.getPrincipal()).getActingUser();
task.addComment(currentUser, comment);
session.saveDocument(taskDoc);
List<DocumentModel> docs = new ArrayList<>();
for (String string : task.getTargetDocumentsIds()) {
docs.add(session.getDocument(new IdRef(string)));
}
notifyEvent(session, task, docs, TaskEventNames.WORKFLOW_TASK_DELEGATED, new HashMap<>(),
String.format("Task delegated by '%s' to '%s'", currentUser, StringUtils.join(actorIds, ","))
+ (!StringUtils.isEmpty(comment) ? " with the following comment: " + comment : ""),
(NuxeoPrincipal) session.getPrincipal(), actorIds);
}
}.runUnrestricted();
}
protected void notifyEvent(CoreSession session, Task task, List<DocumentModel> docs, String event,
Map<String, Serializable> eventInfo, String comment, NuxeoPrincipal principal, List<String> actorIds) {
Map<String, Serializable> eventProperties = new HashMap<>();
ArrayList<String> notificationRecipients = new ArrayList<>();
notificationRecipients.addAll(actorIds);
eventProperties.put(NotificationConstants.RECIPIENTS_KEY,
notificationRecipients.toArray(new String[notificationRecipients.size()]));
if (eventInfo != null) {
eventProperties.putAll(eventInfo);
}
for (DocumentModel doc : docs) {
TaskEventNotificationHelper.notifyEvent(session, doc, principal, task, event, eventProperties, comment,
null);
}
}
@Override
public List<Task> getTaskInstances(DocumentModel dm, List<String> actors, boolean includeDelegatedTasks,
CoreSession session) {
List<Task> tasks = new ArrayList<>();
for (TaskProvider taskProvider : tasksProviders.values()) {
tasks.addAll(taskProvider.getTaskInstances(dm, actors, includeDelegatedTasks, session));
}
return tasks;
}
/**
* @since 5.8
* @deprecated since 7.4 use
* {@link #createTaskForProcess(CoreSession, NuxeoPrincipal, List, String, String, String, String, String, List, boolean, String, String, Date, Map, String, Map)}
* instead
*/
@Override
@Deprecated
public List<Task> createTask(CoreSession coreSession, NuxeoPrincipal principal, List<DocumentModel> documents,
String taskDocumentType, String taskName, String taskType, String processId, List<String> actorIds,
boolean createOneTaskPerActor, String directive, String comment, Date dueDate,
Map<String, String> taskVariables, String parentPath, Map<String, Serializable> eventInfo) {
return createTaskForProcess(coreSession, principal, documents, taskDocumentType, taskName, taskType, processId,
null, actorIds, createOneTaskPerActor, directive, comment, dueDate, taskVariables, parentPath,
eventInfo);
}
/**
* @since 7.4
*/
@Override
public List<Task> createTaskForProcess(CoreSession coreSession, NuxeoPrincipal principal,
List<DocumentModel> documents, String taskDocumentType, String taskName, String taskType, String processId,
String processName, List<String> actorIds, boolean createOneTaskPerActor, String directive, String comment,
Date dueDate, Map<String, String> taskVariables, String parentPath, Map<String, Serializable> eventInfo) {
if (StringUtils.isBlank(parentPath)) {
parentPath = getTaskRootParentPath(coreSession);
}
CreateTaskUnrestricted runner = new CreateTaskUnrestricted(coreSession, principal, documents, taskDocumentType,
taskName, taskType, processId, processName, actorIds, createOneTaskPerActor, directive, comment,
dueDate, taskVariables, parentPath);
runner.runUnrestricted();
List<Task> tasks = runner.getTasks();
for (Task task : tasks) {
// notify
notifyEvent(coreSession, task, documents, TaskEventNames.WORKFLOW_TASK_ASSIGNED, eventInfo, comment,
principal, task.getActors());
}
return tasks;
}
}