diff --git a/ccm-cms/src/main/java/org/librecms/workflow/CmsTaskManager.java b/ccm-cms/src/main/java/org/librecms/workflow/CmsTaskManager.java new file mode 100644 index 000000000..61d144ade --- /dev/null +++ b/ccm-cms/src/main/java/org/librecms/workflow/CmsTaskManager.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2016 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.librecms.workflow; + +import com.arsdigita.cms.workflow.TaskURLGenerator; +import com.arsdigita.util.UncheckedWrapperException; + +import org.libreccm.workflow.AssignableTaskManager; +import org.librecms.contentsection.ContentItem; + +import javax.enterprise.context.RequestScoped; + +/** + * + * @author Jens Pelzetter + */ +@RequestScoped +public class CmsTaskManager extends AssignableTaskManager { + + public String getFinishUrl(final ContentItem item, final CmsTask task) { + final Class urlGeneratorClass = task + .getTaskType().getUrlGenerator(); + final TaskURLGenerator urlGenerator; + try { + urlGenerator = urlGeneratorClass.newInstance(); + } catch (IllegalAccessException + | InstantiationException ex) { + throw new UncheckedWrapperException(ex); + } + + return urlGenerator.generateURL(item.getObjectId(), task.getTaskId()); + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/workflow/AssignableTaskManager.java b/ccm-core/src/main/java/org/libreccm/workflow/AssignableTaskManager.java new file mode 100644 index 000000000..eb43887f5 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/workflow/AssignableTaskManager.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2016 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.libreccm.workflow; + +import org.libreccm.core.CoreConstants; +import org.libreccm.security.AuthorizationRequired; +import org.libreccm.security.RequiresPrivilege; +import org.libreccm.security.Role; +import org.libreccm.security.RoleRepository; +import org.libreccm.security.Shiro; +import org.libreccm.security.User; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; +import javax.transaction.Transactional; + +/** + * + * @author Jens Pelzetter + */ +@RequestScoped +public class AssignableTaskManager extends TaskManager { + + @Inject + private EntityManager entityManager; + + @Inject + private WorkflowRepository workflowRepo; + + @Inject + private TaskRepository taskRepo; + + @Inject + private RoleRepository roleRepo; + + @Inject + private Shiro shiro; + + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public void assignTask(final AssignableTask task, final Role role) { + final TaskAssignment assignment = new TaskAssignment(); + assignment.setTask(task); + assignment.setRole(role); + + task.addAssignment(assignment); + role.addAssignedTask(assignment); + + entityManager.persist(assignment); + taskRepo.save(task); + roleRepo.save(role); + } + + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public void retractTask(final AssignableTask task, final Role role) { + final List result = task.getAssignments().stream() + .filter(assigned -> role.equals(assigned.getRole())) + .collect(Collectors.toList()); + + if (!result.isEmpty()) { + final TaskAssignment assignment = result.get(0); + task.removeAssignment(assignment); + role.removeAssignedTask(assignment); + entityManager.remove(assignment); + } + } + + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public void lockTask(final AssignableTask task) { + task.setLocked(true); + task.setLockingUser(shiro.getUser()); + + taskRepo.save(task); + } + + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public void unlockTask(final AssignableTask task) { + task.setLocked(false); + task.setLockingUser(null); + + taskRepo.save(task); + } + + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public List lockedBy(final User user) { + final TypedQuery query = entityManager.createNamedQuery( + "UserTask.findLockedBy", AssignableTask.class); + query.setParameter("user", user); + + return query.getResultList(); + } + + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public void finish(final AssignableTask task) { + final User currentUser = shiro.getUser(); + + if (!currentUser.equals(task.getLockingUser())) { + throw new IllegalArgumentException(String.format( + "Current user %s is not locking user for task %s. Task is" + + "locaked by user %s.", + Objects.toString(currentUser), + Objects.toString(task), + Objects.toString(task.getLockingUser()))); + } + + super.finish(task); + } + + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public void finish(final AssignableTask task, + final String comment) { + addComment(task, comment); + finish(task); + } + + +} diff --git a/ccm-core/src/main/java/org/libreccm/workflow/CircularTaskDependencyException.java b/ccm-core/src/main/java/org/libreccm/workflow/CircularTaskDependencyException.java new file mode 100644 index 000000000..e9a79a03a --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/workflow/CircularTaskDependencyException.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2016 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ + +package org.libreccm.workflow; + +/** + * + * @author Jens Pelzetter + */ +public class CircularTaskDependencyException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Creates a new instance of CircularTaskDependencyException without detail message. + */ + public CircularTaskDependencyException() { + super(); + } + + + /** + * Constructs an instance of CircularTaskDependencyException with the specified detail message. + * + * @param msg The detail message. + */ + public CircularTaskDependencyException(final String msg) { + super(msg); + } + + /** + * Constructs an instance of CircularTaskDependencyException which wraps the + * specified exception. + * + * @param exception The exception to wrap. + */ + public CircularTaskDependencyException(final Exception exception) { + super(exception); + } + + /** + * Constructs an instance of CircularTaskDependencyException with the specified message which also wraps the + * specified exception. + * + * @param msg The detail message. + * @param exception The exception to wrap. + */ + public CircularTaskDependencyException(final String msg, final Exception exception) { + super(msg, exception); + } +} diff --git a/ccm-core/src/main/java/org/libreccm/workflow/TaskComment.java b/ccm-core/src/main/java/org/libreccm/workflow/TaskComment.java new file mode 100644 index 000000000..3260dffac --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/workflow/TaskComment.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2016 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.libreccm.workflow; + +import org.hibernate.annotations.Type; +import org.libreccm.core.CoreConstants; +import org.libreccm.security.User; + +import java.io.Serializable; +import java.util.Objects; + +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.Lob; +import javax.persistence.OneToOne; +import javax.persistence.Table; + +/** + * + * @author Jens Pelzetter + */ +@Entity +@Table(name = "WORKFLOW_TASK_COMMENTS", schema = CoreConstants.DB_SCHEMA) +public class TaskComment implements Serializable { + + private static final long serialVersionUID = 3842991529698351698L; + + @Id + @Column(name = "COMMENT_ID") + @GeneratedValue(strategy = GenerationType.AUTO) + private long commentId; + + @Column(name = "COMMENT") + @Basic + @Lob + @Type(type = "org.hibernate.type.TextType") + private String comment; + + @OneToOne + @JoinColumn(name = "AUTHOR_ID") + private User author; + + public long getCommentId() { + return commentId; + } + + protected void setCommentId(final long commentId) { + this.commentId = commentId; + } + + public String getComment() { + return comment; + } + + protected void setComment(final String comment) { + this.comment = comment; + } + + public User getAuthor() { + return author; + } + + protected void setAuthor(final User author) { + this.author = author; + } + + @Override + public int hashCode() { + int hash = 3; + hash = 67 * hash + (int) (commentId ^ (commentId >>> 32)); + hash = 67 * hash + Objects.hashCode(comment); + hash = 67 * hash + Objects.hashCode(author); + return hash; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof TaskComment)) { + return false; + } + final TaskComment other = (TaskComment) obj; + if (!other.canEqual(this)) { + return false; + } + + if (commentId != other.getCommentId()) { + return false; + } + if (!Objects.equals(comment, other.getComment())) { + return false; + } + return Objects.equals(author, other.getAuthor()); + } + + public boolean canEqual(final Object obj) { + return obj instanceof TaskComment; + } + + @Override + public final String toString() { + return toString(""); + } + + public String toString(final String data) { + return String.format("%s{ " + + "commentId = %d, " + + "comment = \"%s\", " + + "author = %s%s" + + " }", + super.toString(), + commentId, + comment, + Objects.toString(author), + data); + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/workflow/TaskManager.java b/ccm-core/src/main/java/org/libreccm/workflow/TaskManager.java new file mode 100644 index 000000000..463ea6a3a --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/workflow/TaskManager.java @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2016 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.libreccm.workflow; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.libreccm.core.CoreConstants; +import org.libreccm.security.AuthorizationRequired; +import org.libreccm.security.RequiresPrivilege; +import org.libreccm.security.Role; +import org.libreccm.security.RoleRepository; +import org.libreccm.security.Shiro; +import org.libreccm.security.User; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; +import javax.transaction.Transactional; + +/** + * + * @author Jens Pelzetter + */ +@RequestScoped +public class TaskManager { + + private static final Logger LOGGER = LogManager.getLogger(TaskManager.class); + + @Inject + private EntityManager entityManager; + + @Inject + private WorkflowRepository workflowRepo; + + @Inject + private TaskRepository taskRepo; + + @Inject + private RoleRepository roleRepo; + + @Inject + private Shiro shiro; + + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public void addTask(final Workflow workflow, final Task task) { + workflow.addTask(task); + task.setWorkflow(workflow); + + workflowRepo.save(workflow); + taskRepo.save(task); + } + + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public void removeTask(final Workflow workflow, final Task task) { + workflow.removeTask(task); + task.setWorkflow(null); + + workflowRepo.save(workflow); + taskRepo.save(task); + } + + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public void addDependentTask(final Task parent, final Task task) + throws CircularTaskDependencyException { + + checkForCircularDependencies(parent, task); + + parent.addDependentTask(task); + task.addDependsOn(parent); + + taskRepo.save(task); + taskRepo.save(parent); + } + + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public void removeDependentTask(final Task parent, final Task task) { + parent.removeDependentTask(task); + task.removeDependsOn(parent); + + taskRepo.save(task); + taskRepo.save(parent); + } + + private void checkForCircularDependencies(final Task task1, + final Task task2) + throws CircularTaskDependencyException { + + if (dependsOn(task1, task2)) { + throw new CircularTaskDependencyException(); + } + } + + private boolean dependsOn(final Task task, final Task dependsOn) { + for (final Task current : task.getDependsOn()) { + if (current.equals(dependsOn)) { + return true; + } + + if (current.getDependsOn() != null + && !current.getDependsOn().isEmpty()) { + return dependsOn(current, dependsOn); + } + } + + return false; + } + + public void addComment(final Task task, final String comment) { + addComment(task, shiro.getUser(), comment); + } + + public void addComment(final Task task, + final User author, + final String comment) { + final TaskComment taskComment = new TaskComment(); + taskComment.setAuthor(author); + taskComment.setComment(comment); + + task.addComment(taskComment); + + entityManager.persist(taskComment); + taskRepo.save(task); + } + + public void removeComment(final Task task, final TaskComment comment) { + task.removeComment(comment); + taskRepo.save(task); + } + + public void enable(final Task task) { + switch(task.getTaskState()) { + case DISABLED: + task.setTaskState(TaskState.ENABLED); + taskRepo.save(task); + break; + case FINISHED: + task.setTaskState(TaskState.ENABLED); + taskRepo.save(task); + break; + default: + LOGGER.debug("Task {} is in state \"{}\"; doing nothing.", + Objects.toString(task), + Objects.toString(task.getTaskState())); + break; + } + } + + public void disable(final Task task) { + task.setTaskState(TaskState.DISABLED); + taskRepo.save(task); + } + + public void finish(final Task task) { + if (task == null) { + throw new IllegalArgumentException("Can't finished null..."); + } + + if (task.getTaskState() != TaskState.ENABLED) { + throw new IllegalArgumentException(String.format( + "Task %s is not enabled.", + Objects.toString(task))); + } + + task.setTaskState(TaskState.FINISHED); + taskRepo.save(task); + + task.getDependentTasks().forEach(dependent -> updateState(dependent)); + } + + protected void updateState(final Task task) { + LOGGER.debug("Updating state for task {}...", + Objects.toString(task)); + + boolean dependenciesSatisfied = true; + + if (task.getTaskState() == TaskState.DELETED || !task.isActive()) { + return; + } + + for(final Task dependsOnTask : task.getDependsOn()) { + LOGGER.debug("Checking dependency {}...", + Objects.toString(dependsOnTask)); + if (dependsOnTask.getTaskState() != TaskState.FINISHED + && dependsOnTask.isActive()) { + + LOGGER.debug("Dependency is not yet satisfied."); + + dependenciesSatisfied = false; + break; + } + } + + LOGGER.debug("Dependencies state is {}", dependenciesSatisfied); + + // Rollback case. Previously finished task, but parent tasks + // are re-enabled. + if (task.getTaskState() == TaskState.FINISHED) { + if (dependenciesSatisfied) { + enable(task); + return; + } else { + disable(task); + return; + } + } + + if (task.getTaskState() == TaskState.ENABLED) { + if (!dependenciesSatisfied) { + disable(task); + return; + } + } + + if (task.getTaskState() == TaskState.DISABLED) { + if (dependenciesSatisfied) { + enable(task); + } + } + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/workflow/TaskState.java b/ccm-core/src/main/java/org/libreccm/workflow/TaskState.java new file mode 100644 index 000000000..8951cee64 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/workflow/TaskState.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2016 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.libreccm.workflow; + +/** + * + * @author Jens Pelzetter + */ +public enum TaskState { + + ENABLED, + DISABLED, + FINISHED, + DELETED + +} diff --git a/ccm-core/src/main/java/org/libreccm/workflow/WorkflowState.java b/ccm-core/src/main/java/org/libreccm/workflow/WorkflowState.java new file mode 100644 index 000000000..9cb8541d8 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/workflow/WorkflowState.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2016 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.libreccm.workflow; + +/** + * + * @author Jens Pelzetter + */ +public enum WorkflowState { + + STARTED, + STOPPED, + DELETED, + INIT, + NONE + +}