/** * 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 org.dspace.content.Item; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.sql.SQLException; 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.apache.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; import org.dspace.content.service.CollectionService; import org.dspace.core.Context; import org.dspace.core.LogManager; import org.dspace.curate.service.WorkflowCuratorService; 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.factory.WorkflowServiceFactory; import org.dspace.workflowbasic.BasicWorkflowItem; import org.dspace.workflowbasic.BasicWorkflowServiceImpl; import org.dspace.workflowbasic.service.BasicWorkflowItemService; import org.dspace.workflowbasic.service.BasicWorkflowService; import org.springframework.beans.factory.annotation.Autowired; // Warning - static import ahead! import static javax.xml.stream.XMLStreamConstants.*; /** * WorkflowCurator manages interactions between curation and workflow. * Specifically, it is invoked in WorkflowManager to allow the * performance of curation tasks during workflow. * * @author richardrodgers */ public class WorkflowCuratorServiceImpl implements WorkflowCuratorService { /** log4j logger */ private Logger log = Logger.getLogger(WorkflowCuratorServiceImpl.class); protected Map<String, TaskSet> tsMap = new HashMap<String, TaskSet>(); protected final String[] flowSteps = { "step1", "step2", "step3", "archive" }; @Autowired(required = true) protected CollectionService collectionService; @Autowired(required = true) protected EPersonService ePersonService; @Autowired(required = true) protected GroupService groupService; protected BasicWorkflowItemService basicWorkflowItemService; protected BasicWorkflowService basicWorkflowService; @Autowired(required = true) protected WorkflowServiceFactory workflowServiceFactory; @Autowired(required = true) protected ConfigurationService configurationService; /** * Initialize the bean (after dependency injection has already taken place). * Ensures the configurationService is injected, so that we can read the * settings from configuration * Called by "init-method" in Spring config. * @throws Exception ... */ public void init() throws Exception { File cfgFile = new File(configurationService.getProperty("dspace.dir") + File.separator + "config" + File.separator + "workflow-curation.xml"); try { loadTaskConfig(cfgFile); if (workflowServiceFactory.getWorkflowService() instanceof BasicWorkflowItemService) { basicWorkflowService = (BasicWorkflowService) workflowServiceFactory.getWorkflowService(); basicWorkflowItemService = (BasicWorkflowItemService) workflowServiceFactory.getWorkflowItemService(); } } catch (IOException e) { // debug e.printStackTrace(); log.fatal("Unable to load config: " + cfgFile.getAbsolutePath()); } } protected WorkflowCuratorServiceImpl() { } @Override public boolean needsCuration(BasicWorkflowItem wfi) { return getFlowStep(wfi) != null; } @Override public boolean doCuration(Context c, BasicWorkflowItem wfi) throws AuthorizeException, IOException, SQLException { FlowStep step = getFlowStep(wfi); if (step != null) { Curator curator = new Curator(); // are we going to perform, or just put on queue? if (step.queue != null) { for (Task task : step.tasks) { curator.addTask(task.name); } curator.queue(c, String.valueOf(wfi.getID()), step.queue); basicWorkflowItemService.update(c, wfi); return false; } else { return curate(curator, c, wfi); } } return true; } @Override public boolean curate(Curator curator, Context c, String wfId) throws AuthorizeException, IOException, SQLException { BasicWorkflowItem wfi = basicWorkflowItemService.find(c, Integer.parseInt(wfId)); if (wfi != null) { if (curate(curator, c, wfi)) { basicWorkflowService.advance(c, wfi, c.getCurrentUser(), false, true); return true; } } else { log.warn(LogManager.getHeader(c, "No workflow item found for id: " + wfId, null)); } return false; } @Override public boolean curate(Curator curator, Context c, BasicWorkflowItem wfi) throws AuthorizeException, IOException, SQLException { FlowStep step = getFlowStep(wfi); if (step != null) { // assign collection to item in case task needs it Item item = wfi.getItem(); item.setOwningCollection(wfi.getCollection()); for (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"; if (status == 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)) { basicWorkflowService.sendWorkflowItemBackSubmission(c, wfi, c.getCurrentUser(), null, task.name + ": " + result); return false; } } else if (status == 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; } } else if (status == Curator.CURATE_ERROR) { notifyContacts(c, wfi, task, "error", action, result); } curator.clear(); } } return true; } protected void notifyContacts(Context c, BasicWorkflowItem wfi, 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) { basicWorkflowService.notifyOfCuration(c, wfi, epa, task.name, action, message); } } protected List<EPerson> resolveContacts(Context c, List<String> contacts, BasicWorkflowItem wfi) throws AuthorizeException, IOException, SQLException { List<EPerson> epList = new ArrayList<EPerson>(); for (String contact : contacts) { // decode contacts if ("$flowgroup".equals(contact)) { // special literal for current flowgoup int step = state2step(wfi.getState()); // make sure this step exists if (step < 4) { Group wfGroup = collectionService.getWorkflowGroup(wfi.getCollection(), step); if (wfGroup != null) { epList.addAll(groupService.allMembers(c, wfGroup)); } } } 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; } protected FlowStep getFlowStep(BasicWorkflowItem wfi) { Collection coll = wfi.getCollection(); String key = tsMap.containsKey(coll.getHandle()) ? coll.getHandle() : "default"; TaskSet ts = tsMap.get(key); if (ts != null) { int myStep = state2step(wfi.getState()); for (FlowStep fstep : ts.steps) { if (fstep.step == myStep) { return fstep; } } } return null; } protected int state2step(int state) { if (state <= BasicWorkflowServiceImpl.WFSTATE_STEP1POOL) { return 1; } if (state <= BasicWorkflowServiceImpl.WFSTATE_STEP2POOL) { return 2; } if (state <= BasicWorkflowServiceImpl.WFSTATE_STEP3POOL) { return 3; } return 4; } protected int stepName2step(String name) { for (int i = 0; i < flowSteps.length; i++) { if (flowSteps[i].equals(name)) { return i + 1; } } // invalid stepName - log log.warn("Invalid step: '" + name + "' provided"); return -1; } protected void loadTaskConfig(File cfgFile) throws IOException { Map<String, String> collMap = new HashMap<String, String>(); Map<String, TaskSet> setMap = new HashMap<String, TaskSet>(); 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()) { int event = reader.next(); if (event == START_ELEMENT) { String eName = reader.getLocalName(); if ("mapping".equals(eName)) { collMap.put(reader.getAttributeValue(0), reader.getAttributeValue(1)); } else if ("taskset".equals(eName)) { taskSet = new TaskSet(reader.getAttributeValue(0)); } else if ("flowstep".equals(eName)) { int count = reader.getAttributeCount(); String queue = (count == 2) ? reader.getAttributeValue(1) : null; flowStep = new FlowStep(reader.getAttributeValue(0), queue); } else if ("task".equals(eName)) { task = new Task(reader.getAttributeValue(0)); } else if ("workflow".equals(eName)) { type = "power"; } else if ("notify".equals(eName)) { type = reader.getAttributeValue(0); } } else if (event == CHARACTERS) { if (task != null) { if ("power".equals(type)) { task.addPower(reader.getText()); } else { task.addContact(type, reader.getText()); } } } else if (event == END_ELEMENT) { String eName = reader.getLocalName(); if ("task".equals(eName)) { flowStep.addTask(task); task = null; } else if ("flowstep".equals(eName)) { taskSet.addStep(flowStep); } else if ("taskset".equals(eName)) { setMap.put(taskSet.setName, taskSet); } } } 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); } } protected class TaskSet { public String setName = null; public List<FlowStep> steps = null; public TaskSet(String setName) { this.setName = setName; steps = new ArrayList<FlowStep>(); } public void addStep(FlowStep step) { steps.add(step); } } protected class FlowStep { public int step = -1; public String queue = null; public List<Task> tasks = null; public FlowStep(String stepStr, String queueStr) { this.step = stepName2step(stepStr); this.queue = queueStr; tasks = new ArrayList<Task>(); } public void addTask(Task task) { tasks.add(task); } } protected class Task { public String name = null; public List<String> powers = new ArrayList<String>(); public Map<String, List<String>> contacts = new HashMap<String, List<String>>(); 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<String>(); 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>(); } } }