package com.psddev.cms.db; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import com.psddev.dari.db.Modification; import com.psddev.dari.db.Predicate; import com.psddev.dari.db.Query; import com.psddev.dari.db.Record; import com.psddev.dari.db.State; import com.psddev.dari.util.ErrorUtils; @SuppressWarnings("rawtypes") @Record.BootstrapPackages(value = "Work Streams", depends = { com.psddev.cms.tool.Search.class, Query.class }) public class WorkStream extends Record { @Required private String name; private String instructions; @Embedded @ToolUi.Hidden private com.psddev.cms.tool.Search search; @ToolUi.Hidden private Query query; @ToolUi.Hidden private boolean incompleteIfMatching; @ToolUi.Hidden private Map<String, UUID> currentItems; @Indexed private Set<ToolEntity> assignedEntities; @ToolUi.Hidden private Map<String, List<UUID>> skippedItems; /** Returns the name. */ public String getName() { return name; } /** Sets the name. */ public void setName(String name) { this.name = name; } public String getInstructions() { return instructions; } public void setInstructions(String instructions) { this.instructions = instructions; } public Set<ToolEntity> getAssignedEntities() { return assignedEntities; } public void setAssignedEntities(Set<ToolEntity> assignedEntities) { this.assignedEntities = assignedEntities; } /** Returns the tool search that can return all items to be worked on. */ public com.psddev.cms.tool.Search getSearch() { return search; } /** Sets the tool search that can return all items to be worked on. */ public void setSearch(com.psddev.cms.tool.Search search) { this.search = search; } /** Returns the query that can return all items to be worked on. */ public Query getQuery() { return query == null && search != null ? search.toQuery() : query; } /** Sets the query that can return all items to be worked on. */ public void setQuery(Query query) { this.query = query; } /** * Returns {@code true} if the item should be considered incomplete as * long as the query matches. */ public boolean isIncompleteIfMatching() { return incompleteIfMatching; } /** * Sets whether the item should be considered incomplete as long as the * query matches. */ public void setIncompleteIfMatching(boolean incompleteIfMatching) { this.incompleteIfMatching = incompleteIfMatching; } /** * Returns all users that are currently working. * * @return Never {@code null} but may be empty. */ public List<ToolUser> getUsers() { return currentItems != null ? Query.from(ToolUser.class).where("_id = ?", currentItems.keySet()).selectAll() : new ArrayList<ToolUser>(); } /** * Returns the item that the given {@code user} is currently working * on. * * @param user Can't be {@code null}. * @return May be {@code null}. */ public Object getCurrentItem(ToolUser user) { ErrorUtils.errorIfNull(user, "user"); return currentItems != null ? Query.from(Object.class).where("_id = ?", currentItems.get(user.getId().toString())).first() : null; } /** Returns the number of remaining items to be worked on. */ public long countIncomplete() { return getQuery().clone() .and("id != ?", Query.from(Object.class).where("cms.workstream.completeIds ^= ?", getId().toString() + ",")) .count(); } /** Returns the number of items completed. */ public long countComplete() { return Query.fromAll() .where("cms.workstream.completeIds ^= ?", getId().toString() + ",") .count(); } /** Returns the total number of items, complete and incomplete */ public long countTotal() { return countIncomplete() + countComplete(); } /** * Returns the number of items completed by the given {@code user}. * * @param user Can't be {@code null}. */ public long countComplete(ToolUser user) { ErrorUtils.errorIfNull(user, "user"); return Query .from(Object.class) .where("cms.workstream.completeIds = ?", getId().toString() + "," + user.getId().toString()) .count(); } /** * Returns the number of items skipped by the given {@code user}. * * @param user Can't be {@code null}. */ public long countSkipped(ToolUser user) { ErrorUtils.errorIfNull(user, "user"); String userId = user.getId().toString(); if (skippedItems != null && skippedItems.get(userId) != null) { return skippedItems.get(userId).size(); } else { return 0L; } } /** * Returns {@code true} if the given {@code user} is currently working. * * @param user Can't be {@code null}. */ public boolean isWorking(ToolUser user) { ErrorUtils.errorIfNull(user, "user"); return currentItems != null ? currentItems.get(user.getId().toString()) != null : false; } /** * Returns the next item that the given {@code user} can work on. * * @param user Can't be {@code null}. */ public Object next(ToolUser user) { ErrorUtils.errorIfNull(user, "user"); String userId = user.getId().toString(); Site site = user.getCurrentSite(); Predicate siteItemsPredicate = null; if (site != null) { siteItemsPredicate = site.itemsPredicate(); } State next = null; if (currentItems != null) { Query nextQuery = Query.from(Object.class) .where("_id = ?", currentItems.get(userId)); if (siteItemsPredicate != null) { nextQuery.and(siteItemsPredicate); } next = State.getInstance(nextQuery.first()); } if (next != null && (next.as(Data.class).isComplete(this) || (skippedItems != null && skippedItems.get(userId) != null && skippedItems.get(userId).contains(next.getId())))) { next = null; } if (next == null) { Query<?> query = getQuery().clone() .and("id != ?", Query.from(Object.class).where("cms.workstream.completeIds ^= ?", getId().toString() + ",")); if (siteItemsPredicate != null) { query.and(siteItemsPredicate); } if (currentItems != null) { Set<UUID> currentItemIds = currentItems.values().stream() .filter(Objects::nonNull) .collect(Collectors.toSet()); if (!currentItemIds.isEmpty()) { query.and("_id != ?", currentItemIds); } } if (skippedItems != null) { List<UUID> skippedItemIds = skippedItems.get(userId); if (skippedItemIds != null) { Set<UUID> uniqueNonNullSkippedItemIds = skippedItemIds.stream() .filter(Objects::nonNull) .collect(Collectors.toSet()); if (!skippedItemIds.isEmpty()) { query.and("_id != ?", uniqueNonNullSkippedItemIds); } } } next = State.getInstance(query.first()); if (next != null) { getState().putAtomically("currentItems/" + userId, next.getId()); save(); } } return next != null ? next.getOriginalObject() : null; } /** * Lets the given {@code user} skip working on the given {@code item}. * * @param user Can't be {@code null}. */ public void skip(ToolUser user, Object item) { ErrorUtils.errorIfNull(user, "user"); getState().addAtomically("skippedItems/" + user.getId().toString(), State.getInstance(item).getId()); save(); } /** * Lets the given {@code user} stop working. * * @param user Can't be {@code null}. */ public void stop(ToolUser user) { ErrorUtils.errorIfNull(user, "user"); String userId = user.getId().toString(); getState().putAtomically("currentItems/" + userId, null); getState().putAtomically("skippedItems/" + userId, null); save(); } @FieldInternalNamePrefix("cms.workstream.") public static class Data extends Modification<Object> { @Indexed @ToolUi.Hidden private Set<String> completeIds; /** * Marks this object complete in the given {@code workStream} by the * given {@code user}. * * @param workStream Can't be {@code null}. * @param user Can't be {@code null}. */ public void complete(WorkStream workStream, ToolUser user) { ErrorUtils.errorIfNull(workStream, "workStream"); ErrorUtils.errorIfNull(user, "user"); if (completeIds == null) { completeIds = new HashSet<String>(); } completeIds.add(workStream.getId().toString() + "," + user.getId().toString()); } /** * Returns {@code true} if this object is marked complete in the given * {@code workStream}. * * @param workStream Can't be {@code null}. */ public boolean isComplete(WorkStream workStream) { ErrorUtils.errorIfNull(workStream, "workStream"); if (completeIds != null) { String workStreamId = workStream.getId().toString() + ","; for (String completeId : completeIds) { if (completeId.startsWith(workStreamId)) { return true; } } } return false; } } }