[DS-2058] First cut at adding curation to Configurable Workflow

This commit is contained in:
Mark H. Wood
2017-03-24 17:16:05 -04:00
committed by Mark H. Wood
parent 5cb853d92b
commit a5dc6d1c34
5 changed files with 718 additions and 5 deletions

View File

@@ -0,0 +1,298 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.curate;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Collection;
import org.dspace.content.Item;
import org.dspace.content.service.CollectionService;
import org.dspace.core.Context;
import org.dspace.core.LogManager;
import org.dspace.curate.service.XmlWorkflowCuratorService;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.GroupService;
import org.dspace.services.ConfigurationService;
import org.dspace.workflow.CurationTaskConfig;
import org.dspace.xmlworkflow.RoleMembers;
import org.dspace.xmlworkflow.WorkflowConfigurationException;
import org.dspace.xmlworkflow.factory.XmlWorkflowFactory;
import org.dspace.xmlworkflow.factory.XmlWorkflowServiceFactory;
import org.dspace.xmlworkflow.service.XmlWorkflowService;
import org.dspace.xmlworkflow.state.Step;
import org.dspace.xmlworkflow.state.Workflow;
import org.dspace.xmlworkflow.storedcomponents.ClaimedTask;
import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem;
import org.dspace.xmlworkflow.storedcomponents.service.ClaimedTaskService;
import org.dspace.xmlworkflow.storedcomponents.service.XmlWorkflowItemService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* Manage interactions between curation and workflow. A curation task can be
* attached to a workflow step, to be executed during the step.
*
* @see CurationTaskConfig
* @author mwood
*/
@Service
public class XmlWorkflowCuratorServiceImpl
implements XmlWorkflowCuratorService
{
private static final Logger LOG
= LoggerFactory.getLogger(XmlWorkflowCuratorServiceImpl.class);
@Autowired(required = true)
protected XmlWorkflowFactory workflowFactory;
@Autowired(required = true)
protected XmlWorkflowServiceFactory workflowServiceFactory;
@Autowired(required = true)
protected ConfigurationService configurationService;
@Autowired(required = true)
protected GroupService groupService;
@Autowired(required = true)
protected EPersonService ePersonService;
@Autowired(required = true)
protected CollectionService collectionService;
@Autowired(required = true)
protected ClaimedTaskService claimedTaskService;
protected XmlWorkflowService workflowService;
protected XmlWorkflowItemService workflowItemService;
/**
* Initialize the bean (after dependency injection has already taken place).
* Called by "init-method" in Spring configuration.
*
* @throws Exception passed through.
*/
@PostConstruct
public void init()
throws Exception
{
workflowService = workflowServiceFactory.getXmlWorkflowService();
workflowItemService = workflowServiceFactory.getXmlWorkflowItemService();
}
@Override
public boolean needsCuration(XmlWorkflowItem wfi)
{
throw new UnsupportedOperationException("Not supported yet."); // TODO
}
@Override
public boolean doCuration(Context c, XmlWorkflowItem wfi)
throws AuthorizeException, IOException, SQLException
{
Curator curator = new Curator();
return curate(curator, c, wfi);
}
@Override
public boolean curate(Curator curator, Context c, String wfId)
throws AuthorizeException, IOException, SQLException
{
XmlWorkflowItem wfi = workflowItemService.find(c, Integer.parseInt(wfId));
if (wfi != null) {
return curate(curator, c, wfi);
} else {
LOG.warn(LogManager.getHeader(c, "No workflow item found for id: {}", null), wfId);
}
return false;
}
@Override
public boolean curate(Curator curator, Context c, XmlWorkflowItem wfi)
throws AuthorizeException, IOException, SQLException
{
CurationTaskConfig.FlowStep step = getFlowStep(c, wfi);
if (step != null) {
// assign collection to item in case task needs it
Item item = wfi.getItem();
item.setOwningCollection(wfi.getCollection());
for (CurationTaskConfig.Task task : step.tasks) {
curator.addTask(task.name);
curator.curate(item);
int status = curator.getStatus(task.name);
String result = curator.getResult(task.name);
String action = "none";
switch (status)
{
case Curator.CURATE_FAIL:
// task failed - notify any contacts the task has assigned
if (task.powers.contains("reject"))
{
action = "reject";
}
notifyContacts(c, wfi, task, "fail", action, result);
// if task so empowered, reject submission and terminate
if ("reject".equals(action))
{
workflowService.sendWorkflowItemBackSubmission(c, wfi,
c.getCurrentUser(), null,
task.name + ": " + result);
return false;
}
break;
case Curator.CURATE_SUCCESS:
if (task.powers.contains("approve"))
{
action = "approve";
}
notifyContacts(c, wfi, task, "success", action, result);
if ("approve".equals(action))
{
// cease further task processing and advance submission
return true;
}
break;
case Curator.CURATE_ERROR:
notifyContacts(c, wfi, task, "error", action, result);
break;
default:
break;
}
curator.clear();
}
}
return true;
}
/**
* Find the flow step occupied by a work flow item.
* @param c session context.
* @param wfi the work flow item in question.
* @return the current flow step for the item, or null.
* @throws SQLException
* @throws IOException
*/
protected CurationTaskConfig.FlowStep getFlowStep(Context c, XmlWorkflowItem wfi)
throws SQLException, IOException
{
Collection coll = wfi.getCollection();
String key = CurationTaskConfig.containsKey(coll.getHandle()) ? coll.getHandle() : "default";
ClaimedTask claimedTask
= claimedTaskService.findByWorkflowIdAndEPerson(c, wfi, c.getCurrentUser());
CurationTaskConfig.TaskSet ts = CurationTaskConfig.findTaskSet(key);
if (ts != null)
{
for (CurationTaskConfig.FlowStep fstep : ts.steps)
{
if (fstep.step.equals(claimedTask.getStepID()))
{
return fstep;
}
}
}
return null;
}
/**
* Send email to people who should be notified when curation tasks are run.
*
* @param c session context.
* @param wfi the work flow item being curated.
* @param task the curation task being applied.
* @param status status returned by the task.
* @param action action to be taken as a result of task status.
* @param message anything the code wants to say about the task run.
* @throws AuthorizeException passed through.
* @throws IOException passed through.
* @throws SQLException passed through.
*/
protected void notifyContacts(Context c, XmlWorkflowItem wfi,
CurationTaskConfig.Task task,
String status, String action, String message)
throws AuthorizeException, IOException, SQLException
{
List<EPerson> epa = resolveContacts(c, task.getContacts(status), wfi);
if (epa.size() > 0) {
workflowService.notifyOfCuration(c, wfi, epa, task.name, action, message);
}
}
/**
* Develop a list of EPerson from a list of perhaps symbolic "contact" names.
*
* @param c session context.
* @param contacts the list of concrete and symbolic groups to resolve.
* @param wfi the work flow item associated with these groups via its current work flow step.
* @return the EPersons associated with the current state of {@code wfi}
* @throws AuthorizeException passed through.
* @throws IOException passed through.
* @throws SQLException passed through.
*/
protected List<EPerson> resolveContacts(Context c, List<String> contacts,
XmlWorkflowItem wfi)
throws AuthorizeException, IOException, SQLException
{
List<EPerson> epList = new ArrayList<>();
for (String contact : contacts) {
// decode contacts
if ("$flowgroup".equals(contact)) {
// special literal for current flowgoup
ClaimedTask claimedTask = claimedTaskService.findByWorkflowIdAndEPerson(c, wfi, c.getCurrentUser());
String stepID = claimedTask.getStepID();
Step step;
try {
Workflow workflow = workflowFactory.getWorkflow(wfi.getCollection());
step = workflow.getStep(stepID);
} catch (WorkflowConfigurationException e) {
LOG.error("Failed to locate current workflow step for workflow item "
+ String.valueOf(wfi.getID()), e);
return epList;
}
RoleMembers roleMembers = step.getRole().getMembers(c, wfi);
for (EPerson ep : roleMembers.getEPersons())
epList.add(ep);
for (Group group : roleMembers.getGroups())
epList.addAll(group.getMembers());
} else if ("$colladmin".equals(contact)) {
Group adGroup = wfi.getCollection().getAdministrators();
if (adGroup != null) {
epList.addAll(groupService.allMembers(c, adGroup));
}
} else if ("$siteadmin".equals(contact)) {
EPerson siteEp = ePersonService.findByEmail(c,
configurationService.getProperty("mail.admin"));
if (siteEp != null) {
epList.add(siteEp);
}
} else if (contact.indexOf("@") > 0) {
// little shaky heuristic here - assume an eperson email name
EPerson ep = ePersonService.findByEmail(c, contact);
if (ep != null) {
epList.add(ep);
}
} else {
// assume it is an arbitrary group name
Group group = groupService.findByName(c, contact);
if (group != null) {
epList.addAll(groupService.allMembers(c, group));
}
}
}
return epList;
}
}

View File

@@ -0,0 +1,81 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.curate.service;
import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context;
import org.dspace.curate.Curator;
import org.dspace.workflowbasic.BasicWorkflowItem;
import java.io.IOException;
import java.sql.SQLException;
import org.dspace.workflow.WorkflowItem;
import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem;
/**
* Manage interactions between curation and workflow.
* Specifically, it is invoked in XmlWorkflowService to allow the
* performance of curation tasks during workflow.
*
* Copied from {@link WorkflowCurator} with minor refactoring.
*
* @author mwood
*/
public interface XmlWorkflowCuratorService
{
/**
* Does this workflow item need curation now?
*
* @param wfi the item in question.
* @return true if the item is in a state needing curation.
*/
public boolean needsCuration(XmlWorkflowItem wfi);
/**
* Determines and executes curation on a Workflow item.
*
* @param c the context
* @param wfi the workflow item
* @return true if curation was completed or not required,
* false if tasks were queued for later completion,
* or item was rejected
* @throws AuthorizeException if authorization error
* @throws IOException if IO error
* @throws SQLException if database error
*/
public boolean doCuration(Context c, XmlWorkflowItem wfi)
throws AuthorizeException, IOException, SQLException;
/**
* Determines and executes curation of a Workflow item by ID.
*
* @param curator the curation context
* @param c the user context
* @param wfId the workflow item's ID
* @return true if TODO
* @throws AuthorizeException if authorization error
* @throws IOException if IO error
* @throws SQLException if database error
*/
public boolean curate(Curator curator, Context c, String wfId)
throws AuthorizeException, IOException, SQLException;
/**
* Determines and executes curation of a Workflow item.
*
* @param curator the curation context
* @param c the user context
* @param wfi the workflow item
* @return true if TODO
* @throws AuthorizeException if authorization error
* @throws IOException if IO error
* @throws SQLException if database error
*/
public boolean curate(Curator curator, Context c, XmlWorkflowItem wfi)
throws AuthorizeException, IOException, SQLException;
}

View File

@@ -0,0 +1,232 @@
package org.dspace.workflow;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import static javax.xml.stream.XMLStreamConstants.CHARACTERS;
import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;
/**
* Represent the mapping between collection workflows and curation tasks.
* This mapping is defined in {@code [DSpace]/config/workflow-curation.xml}.
*
* <p>Copied from {@link WorkflowCuratorServiceImpl}.
*
* @author mwood
*/
public class CurationTaskConfig
{
private static final Map<String, TaskSet> tsMap = new HashMap<>();
/**
* Look up a TaskSet by name.
*
* @param setName name of the sought TaskSet: collection handle or "default".
* @return the named TaskSet, or the default TaskSet if not found, or
* {@code null} if there is no default either.
* @throws IOException passed through.
*/
static public TaskSet findTaskSet(String setName)
throws IOException
{
if (tsMap.isEmpty())
{
ConfigurationService configurationService
= DSpaceServicesFactory.getInstance().getConfigurationService();
File cfgFile = new File(configurationService.getProperty("dspace.dir") +
File.separator + "config" + File.separator +
"workflow-curation.xml");
loadTaskConfig(cfgFile);
}
if (tsMap.containsKey(setName))
return tsMap.get(setName);
else
return tsMap.get("default");
}
/**
* Is this task set name defined?
*
* @param name name of the task set sought.
* @return true if a set by that name is known.
*/
public static boolean containsKey(String name)
{
return tsMap.containsKey(name);
}
@SuppressWarnings("null")
static protected void loadTaskConfig(File cfgFile) throws IOException
{
final Map<String, String> collMap = new HashMap<>();
final Map<String, TaskSet> setMap = new HashMap<>();
TaskSet taskSet = null;
FlowStep flowStep = null;
Task task = null;
String type = null;
try {
XMLInputFactory factory = XMLInputFactory.newInstance();
XMLStreamReader reader = factory.createXMLStreamReader(
new FileInputStream(cfgFile), "UTF-8");
while (reader.hasNext())
{
switch (reader.next())
{
case START_ELEMENT:
{
String eName = reader.getLocalName();
if (null != eName) switch (eName)
{
case "mapping":
collMap.put(reader.getAttributeValue(0),
reader.getAttributeValue(1));
break;
case "taskset":
taskSet = new TaskSet(reader.getAttributeValue(0));
break;
case "flowstep":
int count = reader.getAttributeCount();
String queue = (count == 2) ?
reader.getAttributeValue(1) : null;
flowStep = new FlowStep(reader.getAttributeValue(0), queue);
break;
case "task":
task = new Task(reader.getAttributeValue(0));
break;
case "workflow":
type = "power";
break;
case "notify":
type = reader.getAttributeValue(0);
break;
default:
break;
}
break;
}
case CHARACTERS:
if (task != null) {
if ("power".equals(type)) {
task.addPower(reader.getText());
} else {
task.addContact(type, reader.getText());
}
}
break;
case END_ELEMENT:
{
String eName = reader.getLocalName();
if (null != eName) switch (eName)
{
case "task":
flowStep.addTask(task);
task = null;
break;
case "flowstep":
taskSet.addStep(flowStep);
break;
case "taskset":
setMap.put(taskSet.setName, taskSet);
break;
default:
break;
} break;
}
default:
break;
}
}
reader.close();
// stitch maps together
for (Map.Entry<String, String> collEntry : collMap.entrySet())
{
if (! "none".equals(collEntry.getValue()) && setMap.containsKey(collEntry.getValue()))
{
tsMap.put(collEntry.getKey(), setMap.get(collEntry.getValue()));
}
}
} catch (XMLStreamException xsE) {
throw new IOException(xsE.getMessage(), xsE);
}
}
static public class TaskSet
{
public String setName = null;
public List<FlowStep> steps = null;
public TaskSet(String setName)
{
this.setName = setName;
steps = new ArrayList<>();
}
public void addStep(FlowStep step)
{
steps.add(step);
}
}
static public class FlowStep
{
public String step = null;
public String queue = null;
public List<Task> tasks = null;
public FlowStep(String stepStr, String queueStr)
{
this.step = stepStr;
this.queue = queueStr;
tasks = new ArrayList<>();
}
public void addTask(Task task)
{
tasks.add(task);
}
}
static public class Task
{
public String name = null;
public List<String> powers = new ArrayList<>();
public Map<String, List<String>> contacts = new HashMap<>();
public Task(String name) { this.name = name; }
public void addPower(String power) {
powers.add(power);
}
public void addContact(String status, String contact)
{
List<String> sContacts = contacts.get(status);
if (sContacts == null)
{
sContacts = new ArrayList<>();
contacts.put(status, sContacts);
}
sContacts.add(contact);
}
public List<String> getContacts(String status)
{
List<String> ret = contacts.get(status);
return (ret != null) ? ret : new ArrayList<String>();
}
}
}

View File

@@ -22,6 +22,7 @@ import javax.mail.MessagingException;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.logging.log4j.Logger;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.ResourcePolicy;
@@ -45,6 +46,7 @@ import org.dspace.core.Context;
import org.dspace.core.Email;
import org.dspace.core.I18nUtil;
import org.dspace.core.LogManager;
import org.dspace.curate.service.XmlWorkflowCuratorService;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.eperson.service.GroupService;
@@ -124,6 +126,8 @@ public class XmlWorkflowServiceImpl implements XmlWorkflowService {
protected BitstreamService bitstreamService;
@Autowired(required = true)
protected ConfigurationService configurationService;
@Autowired(required = true)
protected XmlWorkflowCuratorService xmlWorkflowCuratorService;
protected XmlWorkflowServiceImpl() {
@@ -318,9 +322,12 @@ public class XmlWorkflowServiceImpl implements XmlWorkflowService {
* Executes an action and returns the next.
*/
@Override
public WorkflowActionConfig doState(Context c, EPerson user, HttpServletRequest request, int workflowItemId,
Workflow workflow, WorkflowActionConfig currentActionConfig)
throws SQLException, AuthorizeException, IOException, WorkflowException {
public WorkflowActionConfig doState(Context c, EPerson user,
HttpServletRequest request, int workflowItemId, Workflow workflow,
WorkflowActionConfig currentActionConfig)
throws SQLException, AuthorizeException, IOException,
MessagingException, WorkflowException
{
try {
XmlWorkflowItem wi = xmlWorkflowItemService.find(c, workflowItemId);
Step currentStep = currentActionConfig.getStep();
@@ -333,6 +340,7 @@ public class XmlWorkflowServiceImpl implements XmlWorkflowService {
}
c.addEvent(new Event(Event.MODIFY, Constants.ITEM, wi.getItem().getID(), null,
itemService.getIdentifiers(c, wi.getItem())));
xmlWorkflowCuratorService.doCuration(c, wi);
return processOutcome(c, user, workflow, currentStep, currentActionConfig, outcome, wi, false);
} else {
throw new AuthorizeException("You are not allowed to to perform this task.");
@@ -631,6 +639,68 @@ public class XmlWorkflowServiceImpl implements XmlWorkflowService {
}
}
// send notices of curation activity
@Override
public void notifyOfCuration(Context c, XmlWorkflowItem wi,
List<EPerson> ePeople, String taskName, String action, String message)
throws SQLException, IOException
{
try
{
// Get the item title
String title = getItemTitle(wi);
// Get the submitter's name
String submitter = getSubmitterName(wi);
// Get the collection
Collection coll = wi.getCollection();
for (EPerson epa : ePeople)
{
Locale supportedLocale = I18nUtil.getEPersonLocale(epa);
Email email = Email.getEmail(I18nUtil.getEmailFilename(supportedLocale, "flowtask_notify"));
email.addArgument(title);
email.addArgument(coll.getName());
email.addArgument(submitter);
email.addArgument(taskName);
email.addArgument(message);
email.addArgument(action);
email.addRecipient(epa.getEmail());
email.send();
}
}
catch (MessagingException e)
{
log.warn(LogManager.getHeader(c, "notifyOfCuration",
"cannot email users of workflow_item_id " + wi.getID()
+ ": " + e.getMessage()));
}
}
protected String getItemTitle(XmlWorkflowItem wi) throws SQLException
{
Item myitem = wi.getItem();
String title = myitem.getName();
// only return the first element, or "Untitled"
if (StringUtils.isNotBlank(title))
{
return title;
}
else
{
return I18nUtil.getMessage("org.dspace.workflow.WorkflowManager.untitled ");
}
}
protected String getSubmitterName(XmlWorkflowItem wi) throws SQLException
{
EPerson e = wi.getSubmitter();
return getEPersonName(e);
}
/***********************************
* WORKFLOW TASK MANAGEMENT
**********************************/

View File

@@ -32,7 +32,7 @@ import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem;
* When an item is submitted and is somewhere in a workflow, it has a row in the
* WorkflowItem table pointing to it.
*
* Once the item has completed the workflow it will be archived
* Once the item has completed the workflow it will be archived.
*
* @author Bram De Schouwer (bram.deschouwer at dot com)
* @author Kevin Van de Velde (kevin at atmire dot com)
@@ -47,7 +47,23 @@ public interface XmlWorkflowService extends WorkflowService<XmlWorkflowItem> {
public WorkflowActionConfig doState(Context c, EPerson user, HttpServletRequest request, int workflowItemId,
Workflow workflow, WorkflowActionConfig currentActionConfig)
throws SQLException, AuthorizeException, IOException, MessagingException, WorkflowException;
/**
* Execute the actions associated with a state, and return the next state.
*
* @param c session context.
* @param user current user.
* @param workflow item is in this workflow.
* @param currentStep workflow step being executed.
* @param currentActionConfig describes the current step's action.
* @param currentOutcome the result of executing the current step (accept/reject/etc).
* @param wfi the Item being processed through workflow.
* @param enteredNewStep is the Item advancing to a new workflow step?
* @return the next step's action.
* @throws SQLException passed through.
* @throws AuthorizeException passed through.
* @throws IOException passed through.
* @throws WorkflowException if the current step's outcome is unrecognized.
*/
public WorkflowActionConfig processOutcome(Context c, EPerson user, Workflow workflow, Step currentStep,
WorkflowActionConfig currentActionConfig, ActionResult currentOutcome,
XmlWorkflowItem wfi, boolean enteredNewStep)
@@ -75,5 +91,21 @@ public interface XmlWorkflowService extends WorkflowService<XmlWorkflowItem> {
public void removeUserItemPolicies(Context context, Item item, EPerson e) throws SQLException, AuthorizeException;
/**
* Send email to interested parties when curation tasks run.
*
* @param c session context.
* @param wi the item being curated.
* @param ePeople the interested parties.
* @param taskName the task that has been run.
* @param action the action indicated by the task (reject, approve, etc.)
* @param message anything the code wants to say about the task.
* @throws SQLException passed through.
* @throws IOException passed through.
*/
public void notifyOfCuration(Context c, XmlWorkflowItem wi,
List<EPerson> ePeople, String taskName, String action, String message)
throws SQLException, IOException;
public String getEPersonName(EPerson ePerson);
}