diff --git a/api/src/org/labkey/api/workflow/Action.java b/api/src/org/labkey/api/workflow/Action.java new file mode 100644 index 00000000000..204d8356115 --- /dev/null +++ b/api/src/org/labkey/api/workflow/Action.java @@ -0,0 +1,130 @@ +package org.labkey.api.workflow; + +import org.apache.commons.lang3.StringUtils; +import org.json.JSONObject; +import org.labkey.api.data.CreatedModified; +import org.labkey.api.util.GUID; + +import java.util.LinkedHashMap; +import java.util.Map; + +public abstract class Action extends CreatedModified +{ + public static final String ASSAY_TYPES_KEY = "assayTypes"; + protected Long _rowId; + protected GUID _containerId; + protected String _name; + protected boolean _isUpdatable = false; + protected Long _taskId; + protected WorkflowService.ActionType _type; + protected JSONObject _inputParameters; + + + public Long getRowId() + { + return _rowId; + } + + public void setRowId(Long rowId) + { + _rowId = rowId; + } + + public GUID getContainerId() + { + return _containerId; + } + + public void setContainerId(GUID containerId) + { + _containerId = containerId; + } + + public String getName() + { + return _name; + } + + public void setName(String name) + { + _name = name; + } + + public boolean getIsUpdatable() + { + return _isUpdatable; + } + + public void setIsUpdatable(boolean updatable) + { + _isUpdatable = updatable; + } + + public Long getTaskId() + { + return _taskId; + } + + public void setTaskId(Long taskId) + { + _taskId = taskId; + } + + public WorkflowService.ActionType getType() + { + return _type; + } + + public void setType(WorkflowService.ActionType type) + { + _type = type; + } + + public JSONObject getInputParameters() + { + return _inputParameters; + } + + public void setInputParameters(JSONObject inputParameters) + { + _inputParameters = inputParameters; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Action action = (Action) o; + + // GitHub Issue 799: Workflow Automation: Attempting to add a sample filter on an existing template errors + // Migration script generated action.name, but they are currently not used. Allow name to be changed to null or empty string. + if (!java.util.Objects.equals(_name, action._name) && !StringUtils.isEmpty(action.getName())) + return false; + + return _isUpdatable == action._isUpdatable && + java.util.Objects.equals(_rowId, action._rowId) && + java.util.Objects.equals(_taskId, action._taskId) && + java.util.Objects.equals(_type, action._type) && + java.util.Objects.equals( + _inputParameters == null ? null : _inputParameters.toString(), + action._inputParameters == null ? null : action._inputParameters.toString() + ); + } + + public Map toAuditDetailMap() + { + Map map = new LinkedHashMap<>(); + map.put("rowId", _rowId); + map.put("name", _name); + map.put("isUpdatable", _isUpdatable); + map.put("taskId", _taskId); + if (_type != null) + map.put("type", _type.name()); + if (_inputParameters != null) + map.put("inputParameters", _inputParameters.toString()); + return map; + } + + public abstract Task getTask(); +} diff --git a/api/src/org/labkey/api/workflow/Job.java b/api/src/org/labkey/api/workflow/Job.java new file mode 100644 index 00000000000..7765baf45d0 --- /dev/null +++ b/api/src/org/labkey/api/workflow/Job.java @@ -0,0 +1,365 @@ +package org.labkey.api.workflow; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jetbrains.annotations.NotNull; +import org.json.JSONObject; +import org.labkey.api.attachments.AttachmentFile; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.CreatedModified; +import org.labkey.api.exp.Identifiable; +import org.labkey.api.exp.Lsid; +import org.labkey.api.exp.ObjectProperty; +import org.labkey.api.exp.PropertyDescriptor; +import org.labkey.api.exp.api.ExpMaterial; +import org.labkey.api.query.ValidationException; +import org.labkey.api.security.Group; +import org.labkey.api.security.SecurityManager; +import org.labkey.api.security.User; +import org.labkey.api.security.UserManager; +import org.labkey.api.util.GUID; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public abstract class Job extends CreatedModified implements Identifiable +{ + protected Long _rowId; + protected GUID _containerId; + protected Container _container; + protected GUID _entityId; + + protected String _name; + protected String _id; + protected String _description; + protected Date _startDate; + protected Date _dueDate; + protected Integer _priority; + protected Long _templateId; + protected Integer _assignee; + protected List _notifyList; + protected boolean _isTemplate; + protected Job _template; + protected Integer _jobCount; // only applies to templates + protected Integer _domainId; + protected String _lsid; // needed for attaching domain properties + protected List _tasks; // ordered by ordinal value + protected List _attachments; + protected List> _attachmentData = new ArrayList<>(); // ?? used for template instead of attachments? + protected List _entities; + protected final Map> _entityTypes = new HashMap<>(); + protected Map _domainProperties; + protected Map _domainPropertyValues; + protected boolean _hasMedia = false; + + public Long getRowId() + { + return _rowId; + } + + public void setRowId(Long rowId) + { + _rowId = rowId; + } + + public GUID getContainerId() + { + return _containerId; + } + + public void setContainerId(GUID containerId) + { + _containerId = containerId; + } + + @JsonIgnore + public Container getContainer() + { + if (_containerId == null) + return null; + if (_container == null) + _container = ContainerManager.getForId(_containerId); + return _container; + } + + public GUID getEntityId() + { + return _entityId; + } + + public void setEntityId(GUID entityId) + { + _entityId = entityId; + } + + public String getName() + { + return _name; + } + + public void setName(String name) + { + _name = name; + } + + public String getId() + { + return _id; + } + + public void setId(String id) + { + _id = id; + } + + public String getDescription() + { + return _description; + } + + public void setDescription(String description) + { + _description = description; + } + + public Date getStartDate() + { + return _startDate; + } + + public void setStartDate(Date startDate) + { + _startDate = startDate; + } + + public Date getDueDate() + { + return _dueDate; + } + + public void setDueDate(Date dueDate) + { + _dueDate = dueDate; + } + + public Integer getPriority() + { + return _priority; + } + + public void setPriority(Integer priority) + { + _priority = priority; + } + + public Long getTemplateId() + { + return _templateId; + } + + public void setTemplateId(Long templateId) + { + _templateId = templateId; + } + + @JsonProperty("template") + public abstract Job getTemplate(Container container, User user); + + public static JSONObject getAssigneeJSON(Integer assigneeId) + { + if (assigneeId == null) + return null; + User user = UserManager.getUser(assigneeId); + if (user != null) + return user.getUserProps(); + + Group group = SecurityManager.getGroup(assigneeId); + if (group != null) + { + JSONObject props = new JSONObject(); + props.put("id", group.getUserId()); + props.put("displayName", group.getName()); + return props; + } + + return null; + } + + @JsonProperty("assignee") + public void setAssignee(Integer assignee) + { + _assignee = assignee; + } + + public Integer getAssignee() + { + return _assignee; + } + + @JsonProperty("assignee") + public JSONObject getAssigneeJSON() + { + return getAssigneeJSON(_assignee); + } + + public abstract List getNotifyList(); + + public void setNotifyList(List notifyList) + { + _notifyList = notifyList; + } + + @JsonProperty("notifyList") + public List getNotifyListJSON() + { + if (getNotifyList() == null) + return null; + + return getNotifyList().stream() + .map(Job::getAssigneeJSON) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + public boolean getIsTemplate() + { + return _isTemplate; + } + + public void setIsTemplate(boolean template) + { + _isTemplate = template; + } + + public Integer getJobCount() + { + return _jobCount; + } + + public void setJobCount(Integer jobCount) + { + _jobCount = jobCount; + } + + @Override + public String getLSID() + { + return _lsid; + } + + public void setLSID(String lsid) + { + _lsid = lsid; + } + + public void setLSID(Lsid lsid) + { + _lsid = lsid.toString(); + } + + public Integer getDomainId() + { + return _domainId; + } + + public void setDomainId(Integer domainId) + { + _domainId = domainId; + } + + public abstract List getTasks(); + + public void setTasks(List tasks) + { + _tasks = tasks; + } + + @JsonIgnore + public List getAttachments() + { + return _attachments; + } + + public void setAttachments(List attachments) + { + _attachments = attachments; + } + + public abstract List getEntities(); + + @JsonIgnore + public abstract @NotNull List getSamples(); + + public void setEntities(List entities) + { + _entities = entities; + } + + @JsonProperty("containerPath") + public String getContainerPath() + { + Container container = getContainer(); + return container == null ? null : container.getPath(); + } + + @JsonProperty("hasMedia") + public boolean isHasMedia() + { + return _hasMedia; + } + + public void setHasMedia(boolean hasMedia) + { + _hasMedia = hasMedia; + } + + public abstract Map toMap(); + + @JsonIgnore + public List getSubsequentTasks(long taskId) + { + List orderedTasks = getTasks().stream().sorted().toList(); + int index = orderedTasks.stream().map(Task::getRowId).toList().indexOf(taskId); + if (index == -1 || index == orderedTasks.size() - 1) + return Collections.emptyList(); + return orderedTasks.subList(index+1, orderedTasks.size()); + } + + public Task getNextTask(long taskId) + { + List subsequent = getSubsequentTasks(taskId); + if (subsequent.isEmpty()) + return null; + return subsequent.get(0); + } + + public boolean isComplete() + { + if (getTasks().isEmpty()) + return true; + return getTasks().stream().allMatch(Task::isCompleted); + } + + public List> getAttachmentData() + { + return _attachmentData; + } + + public void setAttachmentData(List> attachmentData) + { + _attachmentData = attachmentData; + } + + public abstract Map toAuditDetailMap(); + + public abstract Object getDomainProperty(PropertyDescriptor prop); + + public abstract void setProperty(User user, PropertyDescriptor pd, Object value) throws ValidationException; +} diff --git a/api/src/org/labkey/api/workflow/Task.java b/api/src/org/labkey/api/workflow/Task.java new file mode 100644 index 00000000000..627fec251f0 --- /dev/null +++ b/api/src/org/labkey/api/workflow/Task.java @@ -0,0 +1,274 @@ +package org.labkey.api.workflow; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jetbrains.annotations.NotNull; +import org.json.JSONObject; +import org.labkey.api.data.Container; +import org.labkey.api.data.CreatedModified; +import org.labkey.api.util.GUID; + +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class Task extends CreatedModified implements Comparable +{ + protected Long _rowId; + protected GUID _containerId; + protected Container _container; + protected GUID _entityId; + + protected String _name; + + protected String _description; + protected String _entityFilter; + protected Integer _status; + protected Date _startDate; + protected Date _endDate; + protected Date _dueDate; + protected int _ordinal; + protected Integer _assignee; + protected Long _jobId; + protected Job _job; + protected List _actions = null; + + public Long getRowId() + { + return _rowId; + } + + public void setRowId(Long rowId) + { + _rowId = rowId; + } + + public String getName() + { + return _name; + } + + public void setName(String name) + { + _name = name; + } + + public String getDescription() + { + return _description; + } + + public void setDescription(String description) + { + _description = description; + } + + public String getEntityFilter() + { + return _entityFilter; + } + + public void setEntityFilter(String entityFilter) + { + _entityFilter = entityFilter; + } + + @JsonProperty("assignee") + public JSONObject getAssigneeJSON() + { + return Job.getAssigneeJSON(_assignee); + } + + public Integer getAssignee() + { + return _assignee; + } + + @JsonProperty("assignee") + public void setAssignee(Integer assignee) + { + _assignee = assignee; + } + + public Integer getStatus() + { + return _status; + } + + public void setStatus(Integer status) + { + _status = status; + } + + public int getOrdinal() + { + return _ordinal; + } + + public void setOrdinal(int ordinal) + { + _ordinal = ordinal; + } + + public Date getStartDate() + { + return _startDate; + } + + public void setStartDate(Date startDate) + { + _startDate = startDate; + } + + public Date getEndDate() + { + return _endDate; + } + + public void setEndDate(Date endDate) + { + _endDate = endDate; + } + + public Date getDueDate() + { + return _dueDate; + } + + public void setDueDate(Date dueDate) + { + _dueDate = dueDate; + } + + public GUID getEntityId() + { + return _entityId; + } + + public void setEntityId(GUID entityId) + { + _entityId = entityId; + } + + public GUID getContainerId() + { + return _containerId; + } + + public void setContainerId(GUID containerId) + { + _containerId = containerId; + } + + @JsonIgnore + public Container getContainer() + { + return _container; + } + + public void setContainer(Container container) + { + _container = container; + } + + public abstract boolean isCompleted(); + + public abstract boolean isActive(); + + public abstract boolean isPending(); + + public abstract List getActions(); + + public void setActions(List actions) + { + _actions = actions; + } + + public Long getJobId() + { + return _jobId; + } + + public void setJobId(Long jobId) + { + _jobId = jobId; + } + + public abstract Job getJob(); + + public abstract Map toMap(); + + // Determine if the template task with existing jobs can be updated to the new task definition + // Only entityFilter field is allowed to be changed for a referenced template task + public boolean canUpdateUsedTemplateTask(Task task) + { + if (this == task) return true; + if (task == null || getClass() != task.getClass()) return false; + + if (!Objects.equals(_name, task._name) || + !Objects.equals(_description, task._description)) + return false; + + List existingActions = this.getActions(); + List newActions = task.getActions(); + if (existingActions.size() != newActions.size()) + return false; + + for (int i = 0; i < existingActions.size(); i++) + { + Action existingAction = existingActions.get(i); + Action newAction = newActions.get(i); + if (!existingAction.equals(newAction)) + { + return false; + } + } + + return true; + } + + @JsonIgnore + public boolean hasUpdatableAssaysAction() + { + return getActions().stream().anyMatch(action -> action.getType() == WorkflowService.ActionType.AssayImport && action.getIsUpdatable()); + } + + public Map toAuditDetailMap() + { + Map map = new LinkedHashMap<>(); + map.put("rowId", getRowId()); + map.put("name", getName()); + map.put("description", getDescription()); + map.put("entityFilter", getEntityFilter()); + map.put("status", getStatus()); + if (getStartDate() != null) + map.put("startDate", getStartDate()); + if (getEndDate() != null) + map.put("endDate", getEndDate()); + if (getDueDate() != null) + map.put("dueDate", getDueDate()); + if (getEntityId() != null) + map.put("entityId", getEntityId().toString()); + map.put("ordinal", getOrdinal()); + int actionIndex = 1; + for (Action action : getActions()) + { + Map actionMap = action.toAuditDetailMap(); + for (Map.Entry entry : actionMap.entrySet()) + map.put("action" + actionIndex + "." + entry.getKey(), entry.getValue()); + actionIndex++; + } + return map; + } + + @Override + public int compareTo(@NotNull Task o) + { + return Integer.compare(getOrdinal(), o.getOrdinal()); + } + +} diff --git a/api/src/org/labkey/api/workflow/WorkEntity.java b/api/src/org/labkey/api/workflow/WorkEntity.java new file mode 100644 index 00000000000..629ce889eb9 --- /dev/null +++ b/api/src/org/labkey/api/workflow/WorkEntity.java @@ -0,0 +1,199 @@ +package org.labkey.api.workflow; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.json.JSONObject; +import org.labkey.api.exp.api.ExpMaterial; +import org.labkey.api.security.User; +import org.labkey.api.security.UserManager; +import org.labkey.api.util.GUID; + +import java.util.Date; +import java.util.Map; + +public class WorkEntity +{ + public enum WorkType + { + Job, + Task, + Action + } + + public enum EntityType + { + Sample, + Source, + Plate, + } + + protected Long _rowId; + protected GUID _containerId; + protected WorkType _workType; + protected Long _workRowId; + protected EntityType _entityType; + protected Long _entityValue; + protected Long _created; + protected User _createdBy; + + public WorkEntity() + {} + + public WorkEntity(Map map) + { + _rowId = MapUtils.getLong(map, "rowId"); + if (map.get("workType") instanceof String) + _workType = WorkType.valueOf((String) map.get("workType")); + else + _workType = (WorkType) map.get("workType"); + _workRowId = MapUtils.getLong(map, "workRowId"); + if (map.get("entityType") instanceof String) + _entityType = EntityType.valueOf((String) map.get("entityType")); + else + _entityType = (EntityType) map.get("entityType"); + _entityValue = MapUtils.getLong(map, "entityValue"); + if (map.get("Container") != null) + this.setContainerId(new GUID((String) map.get("Container"))); + } + + public WorkEntity(ExpMaterial sample) + { + _entityType = EntityType.Sample; + _entityValue = sample.getRowId(); + } + + public WorkEntity(ExpMaterial sample, WorkType workType, Long workRowId) + { + this(sample); + _workType = workType; + _workRowId = workRowId; + } + + + public Long getRowId() + { + return _rowId; + } + + public void setRowId(Long rowId) + { + _rowId = rowId; + } + + public GUID getContainerId() + { + return _containerId; + } + + public void setContainerId(GUID containerId) + { + _containerId = containerId; + } + + @JsonProperty("created") + public Long getCreated() + { + return _created; + } + + @JsonIgnore + public Date getCreatedDate() + { + return _created == null ? null : new Date(_created); + } + + public void setCreated(Long created) + { + _created = created; + } + + @JsonIgnore // created is serialized as Long + public void setCreated(Date created) + { + if (created != null) + setCreated(created.getTime()); + } + + @JsonProperty("createdBy") + public JSONObject getCreatedBy() + { + if (_createdBy == null) + return null; + return _createdBy.getUserProps(); + } + + @JsonIgnore + public User getCreatedByUser() + { + return _createdBy; + } + + public void setCreatedBy(User createdBy) + { + _createdBy = createdBy; + } + + public void setCreatedBy(Integer createdById) + { + if (createdById != null) + _createdBy = UserManager.getUser(createdById); + } + + public WorkType getWorkType() + { + return _workType; + } + + public void setWorkType(WorkType workType) + { + _workType = workType; + } + + public void setWorkType(String workTypeStr) + { + _workType = StringUtils.isEmpty(StringUtils.trimToEmpty(workTypeStr)) ? null : WorkType.valueOf(workTypeStr.trim()); + } + + public Long getWorkRowId() + { + return _workRowId; + } + + public void setWorkRowId(Long workRowId) + { + _workRowId = workRowId; + } + + public EntityType getEntityType() + { + return _entityType; + } + + public void setEntityType(EntityType entityType) + { + _entityType = entityType; + } + + public void setValueType(String valueTypeStr) + { + _entityType = EntityType.valueOf(valueTypeStr); + } + + public Long getEntityValue() + { + return _entityValue; + } + + public void setEntityValue(Long entityValue) + { + _entityValue = entityValue; + } + + @JsonIgnore + public String getKey() + { + return _entityType + ":" + _entityValue; + } +} diff --git a/api/src/org/labkey/api/workflow/WorkflowService.java b/api/src/org/labkey/api/workflow/WorkflowService.java index 55e22cb6602..ca1ea3a7deb 100644 --- a/api/src/org/labkey/api/workflow/WorkflowService.java +++ b/api/src/org/labkey/api/workflow/WorkflowService.java @@ -1,7 +1,9 @@ package org.labkey.api.workflow; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.labkey.api.data.Container;import org.labkey.api.security.User;import org.labkey.api.services.ServiceRegistry; +import org.labkey.api.util.GUID; public interface WorkflowService { @@ -41,4 +43,10 @@ static WorkflowService get() void onActionComplete(@NotNull Container container, @NotNull User user, @NotNull Long actionId); void onActionComplete(@NotNull Container container, @NotNull User user, @NotNull Long taskId, @NotNull ActionType actionType); + + @Nullable + Job getJob(Long jobId); + + @Nullable + Job getELNReferencePlaceholderJob(Container container); }