package com.psddev.cms.db;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.psddev.cms.tool.ToolPageContext;
import com.psddev.dari.db.Modification;
import com.psddev.dari.db.ObjectField;
import com.psddev.dari.db.ObjectIndex;
import com.psddev.dari.db.ObjectType;
import com.psddev.dari.db.Query;
import com.psddev.dari.db.Record;
import com.psddev.dari.db.Recordable;
import com.psddev.dari.db.State;
import com.psddev.dari.db.VisibilityLabel;
import com.psddev.dari.db.VisibilityValues;
@ToolUi.IconName("object-workflow")
@Record.BootstrapPackages(value = "Workflows", depends = ObjectType.class)
public class Workflow extends Record implements Global, Managed {
@Indexed(unique = true)
@Required
private String name;
@Indexed
@ToolUi.Note("Leave blank to apply this workflow to all sites.")
private Set<Site> sites;
@Indexed
@Required
private Set<ObjectType> contentTypes;
@Indexed(unique = true)
@ToolUi.Hidden
private Set<String> siteContentTypeIds;
@ToolUi.FieldDisplayType("workflowActions")
private Map<String, Object> actions;
/** Returns the name. */
public String getName() {
return name;
}
/** Sets the name. */
public void setName(String name) {
this.name = name;
}
public Set<Site> getSites() {
if (sites == null) {
sites = new LinkedHashSet<>();
}
return sites;
}
public void setSites(Set<Site> sites) {
this.sites = sites;
}
public Set<ObjectType> getContentTypes() {
if (contentTypes == null) {
contentTypes = new LinkedHashSet<ObjectType>();
}
return contentTypes;
}
public void setContentTypes(Set<ObjectType> contentTypes) {
this.contentTypes = contentTypes;
}
public Map<String, Object> getActions() {
if (actions == null) {
actions = new LinkedHashMap<String, Object>();
}
return actions;
}
public void setActions(Map<String, Object> actions) {
this.actions = actions;
}
/**
* Returns a set of all states in this workflow.
*
* @return Never {@code null}. Modifiable.
*/
public Set<WorkflowState> getStates() {
@SuppressWarnings({ "rawtypes", "unchecked" })
Map<String, List<Map<String, Object>>> actions = (Map) getActions();
List<Map<String, Object>> rawStates = actions.get("states");
Set<WorkflowState> states = new HashSet<WorkflowState>();
if (rawStates != null) {
for (Map<String, Object> s : rawStates) {
WorkflowState state = new WorkflowState();
state.setName((String) s.get("name"));
state.setDisplayName((String) s.get("displayName"));
states.add(state);
}
}
return states;
}
public Map<String, WorkflowTransition> getTransitions() {
@SuppressWarnings({ "rawtypes", "unchecked" })
Map<String, List<Map<String, Object>>> actions = (Map) getActions();
List<Map<String, Object>> rawStates = actions.get("states");
List<Map<String, Object>> rawTransitions = actions.get("transitions");
Map<String, WorkflowTransition> transitions = new HashMap<String, WorkflowTransition>();
if (rawStates != null && rawTransitions != null) {
Map<String, WorkflowState> states = new HashMap<String, WorkflowState>();
WorkflowState state;
for (Map<String, Object> s : rawStates) {
state = new WorkflowState();
state.setName((String) s.get("name"));
state.setDisplayName((String) s.get("displayName"));
states.put((String) s.get("id"), state);
}
state = new WorkflowState();
state.setName("New");
states.put("initial", state);
state = new WorkflowState();
state.setName("Published");
states.put("final", state);
for (Map<String, Object> t : rawTransitions) {
WorkflowTransition transition = new WorkflowTransition();
String name = (String) t.get("name");
transition.setName(name);
transition.setDisplayName((String) t.get("displayName"));
transition.setSource(states.get(t.get("source")));
transition.setTarget(states.get(t.get("target")));
transitions.put(name, transition);
}
}
return transitions;
}
public Map<String, WorkflowTransition> getTransitionsFrom(String from) {
if (from == null) {
from = "New";
}
@SuppressWarnings({ "rawtypes", "unchecked" })
Map<String, List<Map<String, Object>>> actions = (Map) getActions();
List<Map<String, Object>> rawStates = actions.get("states");
List<Map<String, Object>> rawTransitions = actions.get("transitions");
Map<String, WorkflowTransition> transitions = new HashMap<String, WorkflowTransition>();
if (rawStates != null && rawTransitions != null) {
Map<String, WorkflowState> states = new HashMap<String, WorkflowState>();
WorkflowState state;
for (Map<String, Object> s : rawStates) {
state = new WorkflowState();
state.setName((String) s.get("name"));
state.setDisplayName((String) s.get("displayName"));
states.put((String) s.get("id"), state);
}
state = new WorkflowState();
state.setName("New");
states.put("initial", state);
state = new WorkflowState();
state.setName("Published");
states.put("final", state);
for (Map<String, Object> t : rawTransitions) {
WorkflowTransition transition = new WorkflowTransition();
String name = (String) t.get("name");
WorkflowState source = states.get(t.get("source"));
transition.setName(name);
transition.setDisplayName((String) t.get("displayName"));
transition.setSource(source);
transition.setTarget(states.get(t.get("target")));
if (!(source == null
|| !from.equals(source.getName())
|| "Published".equals(transition.getTarget().getName()))) {
transitions.put(name, transition);
}
}
}
return transitions;
}
public Map<String, WorkflowTransition> getTransitionsTo(String to) {
if (to == null) {
to = "Published";
}
@SuppressWarnings({ "rawtypes", "unchecked" })
Map<String, List<Map<String, Object>>> actions = (Map) getActions();
List<Map<String, Object>> rawStates = actions.get("states");
List<Map<String, Object>> rawTransitions = actions.get("transitions");
Map<String, WorkflowTransition> transitions = new HashMap<>();
if (rawStates != null && rawTransitions != null) {
Map<String, WorkflowState> states = new HashMap<>();
WorkflowState state;
for (Map<String, Object> s : rawStates) {
state = new WorkflowState();
state.setName((String) s.get("name"));
state.setDisplayName((String) s.get("displayName"));
states.put((String) s.get("id"), state);
}
state = new WorkflowState();
state.setName("New");
states.put("initial", state);
state = new WorkflowState();
state.setName("Published");
states.put("final", state);
for (Map<String, Object> t : rawTransitions) {
WorkflowTransition transition = new WorkflowTransition();
String name = (String) t.get("name");
WorkflowState target = states.get(t.get("target"));
transition.setName(name);
transition.setDisplayName((String) t.get("displayName"));
transition.setSource(states.get(t.get("source")));
transition.setTarget(target);
if (!(target == null
|| !to.equals(target.getName())
|| "New".equals(transition.getSource().getName()))) {
transitions.put(name, transition);
}
}
}
return transitions;
}
public String getStateDisplayName(String workflowState) {
return getStates()
.stream()
.filter(st -> st != null && workflowState.equals(st.getName()))
.map(WorkflowState::getDisplayName)
.findFirst()
.orElse(workflowState);
}
@Override
protected void beforeSave() {
super.beforeSave();
siteContentTypeIds = new LinkedHashSet<>();
for (Site site : getSites()) {
for (ObjectType contentType : getContentTypes()) {
siteContentTypeIds.add(site.getId() + ":" + contentType.getId());
}
}
}
public static Workflow findWorkflow(Site site, State state) {
if (state == null) {
return null;
}
Workflow workflow = null;
ObjectType type = state.getType();
if (site != null) {
workflow = Query
.from(Workflow.class)
.and("sites = ?", site)
.and("contentTypes = ?", type)
.first();
}
if (workflow == null) {
workflow = Query
.from(Workflow.class)
.and("sites = missing")
.and("contentTypes = ?", type)
.first();
}
if (workflow == null) {
Site owner = state.as(Site.ObjectModification.class).getOwner();
if (owner != null) {
workflow = Query
.from(Workflow.class)
.and("sites = ?", owner)
.and("contentTypes = ?", type)
.first();
}
}
return workflow;
}
@Override
public String createManagedEditUrl(ToolPageContext page) {
return page.cmsUrl("/admin/workflows.jsp", "id", getId());
}
@FieldInternalNamePrefix("cms.workflow.")
public static class Data extends Modification<Object> implements VisibilityLabel, VisibilityValues {
@Indexed(visibility = true)
@ToolUi.Hidden
private String currentState;
@Embedded
@ToolUi.Hidden
private WorkflowLog currentLog;
public String getCurrentState() {
return currentState;
}
public WorkflowLog getCurrentLog() {
return currentLog;
}
public void setCurrentLog(WorkflowLog currentLog) {
this.currentLog = currentLog;
}
/**
* @param transition If {@code null}, makes the object visible.
* @param user May be {@code null}.
* @param log May be {@code null}.
* @return New workflow state. May be {@code null}.
*/
public String changeState(WorkflowTransition transition, Object user, WorkflowLog log) {
String previousState = currentState;
String transitionName;
String transitionTarget;
if (transition == null) {
transitionName = "Publish";
transitionTarget = null;
} else {
transitionName = transition.getName();
transitionTarget = transition.getTarget().getName();
if ("Published".equals(transitionTarget)) {
transitionTarget = null;
}
}
currentState = transitionTarget;
if (transition != null || previousState != null) {
if (log == null) {
log = new WorkflowLog();
}
log.setObject(getOriginalObject());
log.setDate(new Date());
log.setTransition(transitionName);
log.setOldWorkflowState(previousState);
log.setNewWorkflowState(currentState);
if (user != null) {
if (user instanceof Recordable) {
log.setUserId(((Recordable) user).getState().getId().toString());
} else {
log.setUserId(user.toString());
}
}
log.save();
}
return currentState;
}
/**
* @param transition If {@code null}, makes the object visible.
* @param user May be {@code null}.
* @param comment May be {@code null}.
* @return New workflow state. May be {@code null}.
* @deprecated Use {@link #changeState(WorkflowTransition, Object, WorkflowLog)} instead.
*/
@Deprecated
public String changeState(WorkflowTransition transition, Object user, String comment) {
WorkflowLog log = new WorkflowLog();
log.setComment(comment);
return changeState(transition, user, log);
}
/**
* @param state If {@code null}, makes the object visible.
*/
public void revertState(String state) {
this.currentState = state;
}
// --- VisibilityLabel support ---
@Override
public String createVisibilityLabel(ObjectField field) {
String currentState = getCurrentState();
if (currentState != null) {
Workflow workflow = findWorkflow(as(Site.ObjectModification.class).getOwner(), getState());
if (workflow != null) {
for (WorkflowState s : workflow.getStates()) {
if (currentState.equals(s.getName())) {
return s.getDisplayName();
}
}
}
}
return currentState;
}
@Override
public Iterable<?> findVisibilityValues(ObjectIndex index) {
Set<Object> visibilityValues = new HashSet<Object>();
for (Workflow workflow : Query.from(Workflow.class).where("contentTypes = ?", State.getInstance(getOriginalObject()).getType()).selectAll()) {
visibilityValues.add(workflow.getStates());
}
return visibilityValues;
}
}
}