org.dspace.workflowbasic.BasicWorkflowServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.dspace.workflowbasic.BasicWorkflowServiceImpl.java

Source

/**
 * 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.workflowbasic;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.ResourcePolicy;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.Bitstream;
import org.dspace.content.Bundle;
import org.dspace.content.Collection;
import org.dspace.content.DCDate;
import org.dspace.content.Item;
import org.dspace.content.MetadataSchema;
import org.dspace.content.WorkspaceItem;
import org.dspace.content.service.CollectionService;
import org.dspace.content.service.InstallItemService;
import org.dspace.content.service.ItemService;
import org.dspace.content.service.WorkspaceItemService;
import org.dspace.core.Constants;
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.WorkflowCuratorService;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.eperson.service.GroupService;
import org.dspace.handle.service.HandleService;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.usage.UsageWorkflowEvent;
import org.dspace.workflowbasic.service.BasicWorkflowItemService;
import org.dspace.workflowbasic.service.BasicWorkflowService;
import org.dspace.workflowbasic.service.TaskListItemService;
import org.springframework.beans.factory.annotation.Autowired;

import javax.mail.MessagingException;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.UUID;

public class BasicWorkflowServiceImpl implements BasicWorkflowService {

    @Autowired(required = true)
    protected AuthorizeService authorizeService;
    @Autowired(required = true)
    protected CollectionService collectionService;
    @Autowired(required = true)
    protected GroupService groupService;
    @Autowired(required = true)
    protected HandleService handleService;
    @Autowired(required = true)
    protected InstallItemService installItemService;
    @Autowired(required = true)
    protected ItemService itemService;
    @Autowired(required = true)
    protected TaskListItemService taskListItemService;
    @Autowired(required = true)
    protected WorkflowCuratorService workflowCuratorService;
    @Autowired(required = true)
    protected BasicWorkflowItemService workflowItemService;
    @Autowired(required = true)
    protected WorkspaceItemService workspaceItemService;
    @Autowired(required = true)
    protected ConfigurationService configurationService;

    protected BasicWorkflowServiceImpl() {

    }

    /** Symbolic names of workflow steps. */
    protected final String workflowText[] = { "SUBMIT", // 0
            "STEP1POOL", // 1
            "STEP1", // 2
            "STEP2POOL", // 3
            "STEP2", // 4
            "STEP3POOL", // 5
            "STEP3", // 6
            "ARCHIVE" // 7
    };

    /* support for 'no notification' */
    protected Map<UUID, Boolean> noEMail = new HashMap<>();

    /** log4j logger */
    private final Logger log = Logger.getLogger(BasicWorkflowServiceImpl.class);

    @Override
    public int getWorkflowID(String state) {
        for (int i = 0; i < workflowText.length; ++i) {
            if (state.equalsIgnoreCase(workflowText[i])) {
                return i;
            }
        }
        return -1;
    }

    /**
     * This methods grants the appropriate permissions to reviewers so that they
     * can read and edit metadata and read files and edit files if allowed by 
     * configuration.
     * In most cases this method must be called within a try-finally-block that
     * temporary disables the authentication system. This is not done by this
     * method as it should be done carefully and only in contexts in which
     * granting the permissions is authorized by some previous checks.
     * 
     * @param context
     * @param wfi While all policies are granted on item, bundle or bitstream 
     *            level, this method takes an workflowitem for convenience and 
     *            uses wfi.getItem() to get the actual item.
     * @param reviewer EPerson to grant the rights to.
     * @throws SQLException
     * @throws AuthorizeException 
     */
    protected void grantReviewerPolicies(Context context, BasicWorkflowItem wfi, EPerson reviewer)
            throws SQLException, AuthorizeException {
        if (reviewer == null) {
            return;
        }

        // get item and bundle "ORIGINAL"
        Item item = wfi.getItem();
        Bundle originalBundle;
        try {
            originalBundle = itemService.getBundles(item, "ORIGINAL").get(0);
        } catch (IndexOutOfBoundsException ex) {
            originalBundle = null;
        }

        // grant item level policies
        for (int action : new int[] { Constants.READ, Constants.WRITE, Constants.ADD, Constants.REMOVE,
                Constants.DELETE }) {
            authorizeService.addPolicy(context, item, action, reviewer, ResourcePolicy.TYPE_WORKFLOW);
        }

        // set bitstream and bundle policies
        if (originalBundle != null) {
            authorizeService.addPolicy(context, originalBundle, Constants.READ, reviewer,
                    ResourcePolicy.TYPE_WORKFLOW);

            // shall reviewers be able to edit files?
            boolean editFiles = configurationService.getBooleanProperty("workflow.reviewer.file-edit", false);
            // if a reviewer should be able to edit bitstreams, we need add
            // permissions regarding the bundle "ORIGINAL" and its bitstreams
            if (editFiles) {
                authorizeService.addPolicy(context, originalBundle, Constants.ADD, reviewer,
                        ResourcePolicy.TYPE_WORKFLOW);
                authorizeService.addPolicy(context, originalBundle, Constants.REMOVE, reviewer,
                        ResourcePolicy.TYPE_WORKFLOW);
                // Whenever a new bitstream is added, it inherit the policies of the bundle.
                // So we need to add all policies newly created bitstreams should get.
                authorizeService.addPolicy(context, originalBundle, Constants.WRITE, reviewer,
                        ResourcePolicy.TYPE_WORKFLOW);
                authorizeService.addPolicy(context, originalBundle, Constants.DELETE, reviewer,
                        ResourcePolicy.TYPE_WORKFLOW);
            }
            for (Bitstream bitstream : originalBundle.getBitstreams()) {
                authorizeService.addPolicy(context, bitstream, Constants.READ, reviewer,
                        ResourcePolicy.TYPE_WORKFLOW);

                // add further rights if reviewer should be able to edit bitstreams
                if (editFiles) {
                    authorizeService.addPolicy(context, bitstream, Constants.WRITE, reviewer,
                            ResourcePolicy.TYPE_WORKFLOW);
                    authorizeService.addPolicy(context, bitstream, Constants.DELETE, reviewer,
                            ResourcePolicy.TYPE_WORKFLOW);
                }
            }
        }
    }

    /**
     * This methods revokes any permission granted by the basic workflow systems
     * on the item specified as attribute. At time of writing this method these
     * permissions will all be granted by
     * {@link #grantReviewerPolicies(org.dspace.core.Context, org.dspace.workflowbasic.BasicWorkflowItem, org.dspace.eperson.EPerson)}.
     * In most cases this method must be called within a try-finally-block that
     * temporary disables the authentication system. This is not done by this
     * method as it should be done carefully and only in contexts in which
     * revoking the permissions is authorized by some previous checks.
     * 
     * @param context
     * @param item
     * @throws SQLException
     * @throws AuthorizeException 
     */
    protected void revokeReviewerPolicies(Context context, Item item) throws SQLException, AuthorizeException {
        // get bundle "ORIGINAL"
        Bundle originalBundle;
        try {
            originalBundle = itemService.getBundles(item, "ORIGINAL").get(0);
        } catch (IndexOutOfBoundsException ex) {
            originalBundle = null;
        }

        // remove bitstream and bundle level policies
        if (originalBundle != null) {
            // We added policies for Bitstreams of the bundle "original" only
            for (Bitstream bitstream : originalBundle.getBitstreams()) {
                authorizeService.removeAllPoliciesByDSOAndType(context, bitstream, ResourcePolicy.TYPE_WORKFLOW);
            }

            authorizeService.removeAllPoliciesByDSOAndType(context, originalBundle, ResourcePolicy.TYPE_WORKFLOW);
        }

        // remove item level policies
        authorizeService.removeAllPoliciesByDSOAndType(context, item, ResourcePolicy.TYPE_WORKFLOW);
    }

    @Override
    public BasicWorkflowItem start(Context context, WorkspaceItem wsi)
            throws SQLException, AuthorizeException, IOException {
        Item myitem = wsi.getItem();
        Collection collection = wsi.getCollection();

        log.info(LogManager.getHeader(context, "start_workflow", "workspace_item_id=" + wsi.getID() + "item_id="
                + myitem.getID() + "collection_id=" + collection.getID()));

        // record the start of the workflow w/provenance message
        recordStart(context, myitem);

        // create the WorkflowItem
        BasicWorkflowItem wfi = workflowItemService.create(context, myitem, collection);
        wfi.setMultipleFiles(wsi.hasMultipleFiles());
        wfi.setMultipleTitles(wsi.hasMultipleTitles());
        wfi.setPublishedBefore(wsi.isPublishedBefore());

        // remove the WorkspaceItem
        workspaceItemService.deleteWrapper(context, wsi);

        // now get the workflow started
        wfi.setState(WFSTATE_SUBMIT);
        advance(context, wfi, null);

        // Return the workflow item
        return wfi;
    }

    @Override
    public BasicWorkflowItem startWithoutNotify(Context c, WorkspaceItem wsi)
            throws SQLException, AuthorizeException, IOException {
        // make a hash table entry with item ID for no notify
        // notify code checks no notify hash for item id
        noEMail.put(wsi.getItem().getID(), Boolean.TRUE);

        return start(c, wsi);
    }

    @Override
    public List<BasicWorkflowItem> getOwnedTasks(Context context, EPerson e) throws java.sql.SQLException {
        return workflowItemService.findByOwner(context, e);
    }

    @Override
    public List<BasicWorkflowItem> getPooledTasks(Context context, EPerson e) throws SQLException {
        return workflowItemService.findPooledTasks(context, e);
    }

    @Override
    public void claim(Context context, BasicWorkflowItem workflowItem, EPerson e)
            throws SQLException, IOException, AuthorizeException {
        int taskstate = workflowItem.getState();

        switch (taskstate) {
        case WFSTATE_STEP1POOL:

            authorizeService.authorizeAction(context, e, workflowItem.getCollection(), Constants.WORKFLOW_STEP_1,
                    true);
            doState(context, workflowItem, WFSTATE_STEP1, e);

            break;

        case WFSTATE_STEP2POOL:

            authorizeService.authorizeAction(context, e, workflowItem.getCollection(), Constants.WORKFLOW_STEP_2,
                    true);
            doState(context, workflowItem, WFSTATE_STEP2, e);

            break;

        case WFSTATE_STEP3POOL:

            authorizeService.authorizeAction(context, e, workflowItem.getCollection(), Constants.WORKFLOW_STEP_3,
                    true);
            doState(context, workflowItem, WFSTATE_STEP3, e);

            break;

        default:
            throw new IllegalArgumentException("Workflow Step " + taskstate + " is out of range.");
        }

        log.info(LogManager.getHeader(context, "claim_task",
                "workflow_item_id=" + workflowItem.getID() + "item_id=" + workflowItem.getItem().getID()
                        + "collection_id=" + workflowItem.getCollection().getID() + "newowner_id="
                        + workflowItem.getOwner().getID() + "old_state=" + taskstate + "new_state="
                        + workflowItem.getState()));
    }

    @Override
    public void advance(Context context, BasicWorkflowItem workflowItem, EPerson e)
            throws SQLException, IOException, AuthorizeException {
        advance(context, workflowItem, e, true, true);
    }

    @Override
    public boolean advance(Context context, BasicWorkflowItem workflowItem, EPerson e, boolean curate,
            boolean record) throws SQLException, IOException, AuthorizeException {
        int taskstate = workflowItem.getState();
        boolean archived = false;

        // perform curation tasks if needed
        if (curate && workflowCuratorService.needsCuration(workflowItem)) {
            if (!workflowCuratorService.doCuration(context, workflowItem)) {
                // don't proceed - either curation tasks queued, or item rejected
                log.info(LogManager.getHeader(context, "advance_workflow",
                        "workflow_item_id=" + workflowItem.getID() + ",item_id=" + workflowItem.getItem().getID()
                                + ",collection_id=" + workflowItem.getCollection().getID() + ",old_state="
                                + taskstate + ",doCuration=false"));
                return false;
            }
        }

        switch (taskstate) {
        case WFSTATE_SUBMIT:
            archived = doState(context, workflowItem, WFSTATE_STEP1POOL, e);

            break;

        case WFSTATE_STEP1:
            // advance(...) will call itself if no workflow step group exists
            // so we need to check permissions only if a workflow step group is
            // in place.
            if (workflowItem.getCollection().getWorkflowStep1() != null && e != null) {
                authorizeService.authorizeAction(context, e, workflowItem.getCollection(),
                        Constants.WORKFLOW_STEP_1, true);
            }

            // Record provenance
            if (record) {
                recordApproval(context, workflowItem, e);
            }
            archived = doState(context, workflowItem, WFSTATE_STEP2POOL, e);

            break;

        case WFSTATE_STEP2:
            // advance(...) will call itself if no workflow step group exists
            // so we need to check permissions only if a workflow step group is
            // in place.
            if (workflowItem.getCollection().getWorkflowStep2() != null && e != null) {
                authorizeService.authorizeAction(context, e, workflowItem.getCollection(),
                        Constants.WORKFLOW_STEP_2, true);
            }

            // Record provenance
            if (record) {
                recordApproval(context, workflowItem, e);
            }
            archived = doState(context, workflowItem, WFSTATE_STEP3POOL, e);

            break;

        case WFSTATE_STEP3:
            // advance(...) will call itself if no workflow step group exists
            // so we need to check permissions only if a workflow step group is
            // in place.
            if (workflowItem.getCollection().getWorkflowStep3() != null && e != null) {
                authorizeService.authorizeAction(context, e, workflowItem.getCollection(),
                        Constants.WORKFLOW_STEP_3, true);
            }

            // We don't record approval for editors, since they can't reject,
            // and thus didn't actually make a decision
            archived = doState(context, workflowItem, WFSTATE_ARCHIVE, e);

            break;

        // error handling? shouldn't get here
        }

        log.info(LogManager.getHeader(context, "advance_workflow",
                "workflow_item_id=" + workflowItem.getID() + ",item_id=" + workflowItem.getItem().getID()
                        + ",collection_id=" + workflowItem.getCollection().getID() + ",old_state=" + taskstate
                        + ",new_state=" + workflowItem.getState()));
        return archived;
    }

    @Override
    public void unclaim(Context context, BasicWorkflowItem workflowItem, EPerson e)
            throws SQLException, IOException, AuthorizeException {
        int taskstate = workflowItem.getState();

        switch (taskstate) {
        case WFSTATE_STEP1:

            doState(context, workflowItem, WFSTATE_STEP1POOL, e);

            break;

        case WFSTATE_STEP2:

            doState(context, workflowItem, WFSTATE_STEP2POOL, e);

            break;

        case WFSTATE_STEP3:

            doState(context, workflowItem, WFSTATE_STEP3POOL, e);

            break;
        default:
            throw new IllegalStateException("WorkflowItem reach an unknown state.");
        }

        log.info(LogManager.getHeader(context, "unclaim_workflow",
                "workflow_item_id=" + workflowItem.getID() + ",item_id=" + workflowItem.getItem().getID()
                        + ",collection_id=" + workflowItem.getCollection().getID() + ",old_state=" + taskstate
                        + ",new_state=" + workflowItem.getState()));
    }

    @Override
    public WorkspaceItem abort(Context context, BasicWorkflowItem workflowItem, EPerson e)
            throws SQLException, AuthorizeException, IOException {
        // authorize a DSpaceActions.ABORT
        if (!authorizeService.isAdmin(context)) {
            throw new AuthorizeException("You must be an admin to abort a workflow");
        }

        // stop workflow regardless of its state
        taskListItemService.deleteByWorkflowItem(context, workflowItem);

        log.info(LogManager.getHeader(context, "abort_workflow",
                "workflow_item_id=" + workflowItem.getID() + "item_id=" + workflowItem.getItem().getID()
                        + "collection_id=" + workflowItem.getCollection().getID() + "eperson_id=" + e.getID()));

        // convert into personal workspace
        return returnToWorkspace(context, workflowItem);
    }

    protected boolean doState(Context context, BasicWorkflowItem workflowItem, int newstate, EPerson newowner)
            throws SQLException, IOException, AuthorizeException {
        Collection collection = workflowItem.getCollection();

        //Gather our old data for launching the workflow event
        int oldState = workflowItem.getState();

        // in case we don't want to inform reviewers about tasks returned to
        // the pool by other reviewers, we'll ne to know whether they were owned
        // before. => keep this information before setting the new owner.
        EPerson oldOwner = workflowItem.getOwner();

        switch (newstate) {
        case WFSTATE_STEP1POOL:
            return pool(context, workflowItem, 1);

        case WFSTATE_STEP1:
            assignToReviewer(context, workflowItem, 1, newowner);
            return false;

        case WFSTATE_STEP2POOL:
            return pool(context, workflowItem, 2);

        case WFSTATE_STEP2:
            assignToReviewer(context, workflowItem, 2, newowner);
            return false;

        case WFSTATE_STEP3POOL:
            return pool(context, workflowItem, 3);

        case WFSTATE_STEP3:
            assignToReviewer(context, workflowItem, 3, newowner);
            return false;

        case WFSTATE_ARCHIVE:
            // put in archive in one transaction
            // remove workflow tasks
            taskListItemService.deleteByWorkflowItem(context, workflowItem);

            collection = workflowItem.getCollection();

            Item myitem = archive(context, workflowItem);

            // now email notification
            notifyOfArchive(context, myitem, collection);

            // remove any workflow policies left
            try {
                context.turnOffAuthorisationSystem();
                revokeReviewerPolicies(context, myitem);
            } finally {
                context.restoreAuthSystemState();
            }

            logWorkflowEvent(context, workflowItem.getItem(), workflowItem, context.getCurrentUser(), newstate,
                    newowner, collection, oldState, null);

            return true;

        default:
            throw new IllegalArgumentException("BasicWorkflowService cannot handle workflowItemState " + newstate);

        }
    }

    /**
     * Helper method to take an item out of the pool, to assign it to a reviewer and to deal with reviewer policies.
     * Don't use this method directly. Instead: use {@link #start(Context, WorkspaceItem)} to start a workflow and
     * {@link #claim(Context, BasicWorkflowItem, EPerson)} as those methods handles the internal states and checks for
     * the appropriate permissions.
     * @param context DSpace context object
     * @param workflowItem The item that shall be pooled.
     * @param step The step (1-3) of the pool the item should be put to.
     * @param newowner The EPerson that should do the review.
     * @return True if the item was archived because no reviewers were assigned to any of the following workflow steps, false otherwise.
     * @throws SQLException
     * @throws AuthorizeException
     * @throws IOException
     * @throws IllegalArgumentException If {@code param} has another value than either 1, 2, or 3.
        
     */
    protected void assignToReviewer(Context context, BasicWorkflowItem workflowItem, int step, EPerson newowner)
            throws AuthorizeException, SQLException {
        // shortcut to the collection
        Collection collection = workflowItem.getCollection();

        // from the step we can recoginze the new state and the corresponding policy action.
        int newState;
        int correspondingAction;
        if (step == 1) {
            newState = WFSTATE_STEP1;
            correspondingAction = Constants.WORKFLOW_STEP_1;
        } else if (step == 2) {
            newState = WFSTATE_STEP2;
            correspondingAction = Constants.WORKFLOW_STEP_2;
        } else if (step == 3) {
            newState = WFSTATE_STEP3;
            correspondingAction = Constants.WORKFLOW_STEP_3;
        } else {
            throw new IllegalArgumentException("Got a task to pool with an improperly or unknown state.");
        }

        // Gather the old state for logging
        int oldState = workflowItem.getState();

        // if there is a workflow state group and it contains any members,
        // then we have to check permissions first
        if ((collectionService.getWorkflowGroup(collection, step) != null)
                && !(groupService.isEmpty(collectionService.getWorkflowGroup(collection, step)))
                && newowner != null) {
            authorizeService.authorizeAction(context, newowner, collection, correspondingAction, true);
        }

        // give the owner the appropriate permissions
        try {
            context.turnOffAuthorisationSystem();
            // maybe unnecessary, but revoke any previously granted permissions
            revokeReviewerPolicies(context, workflowItem.getItem());
            // finally grant the new permissions
            grantReviewerPolicies(context, workflowItem, newowner);
        } finally {
            context.restoreAuthSystemState();
        }

        // remove task from tasklist as someone is working on it now
        taskListItemService.deleteByWorkflowItem(context, workflowItem);
        // assign new owner
        workflowItem.setState(newState);
        workflowItem.setOwner(newowner);

        logWorkflowEvent(context, workflowItem.getItem(), workflowItem, context.getCurrentUser(), newState,
                newowner, collection, oldState, null);
    }

    /**
     * Helper method that manages state, policies, owner, notifies, tasklistitems and so on whenever an WorkflowItem
     * should be added to a workflow step pool. Don't use this method directly. Either use
     * {@link #unclaim(Context, BasicWorkflowItem, EPerson)} if the item is claimed,
     * {@link #start(Context, WorkspaceItem)} to start the workflow or {@link #advance(Context, BasicWorkflowItem, EPerson)}
     * to move an item to the next state.
     * @param context DSpace context object
     * @param workflowItem The item that shall be pooled.
     * @param step The step (1-3) of the pool the item should be put to.
     * @return True if the item was archived because no reviewers were assigned to any of the following workflow steps, false otherwise.
     * @throws SQLException
     * @throws AuthorizeException
     * @throws IOException
     * @throws IllegalArgumentException If {@code param} has another value than either 1, 2, or 3.
     */
    protected boolean pool(Context context, BasicWorkflowItem workflowItem, int step)
            throws SQLException, AuthorizeException, IOException {
        // shortcut to the collection
        Collection collection = workflowItem.getCollection();

        // from the step we can recoginze the new state and the corresponding state.
        // the new state is the pool of the step
        // the corresponding state is the state an item gets when it gets claimed. That is important to recognize if we
        // have to send notifications and if we have to skip a pool.
        int newState;
        int correspondingState;
        if (step == 1) {
            newState = WFSTATE_STEP1POOL;
            correspondingState = WFSTATE_STEP1;
        } else if (step == 2) {
            newState = WFSTATE_STEP2POOL;
            correspondingState = WFSTATE_STEP2;
        } else if (step == 3) {
            newState = WFSTATE_STEP3POOL;
            correspondingState = WFSTATE_STEP3;
        } else {
            throw new IllegalArgumentException("Got a task to pool with an improperly or unknown state.");
        }

        // Gather our old owner and state as we need those as well to determine if we have to send notifies
        int oldState = workflowItem.getState();
        EPerson oldOwner = workflowItem.getOwner();

        // clear owner
        workflowItem.setOwner(null);
        // don't revoke the reviewer policies yet, they may be needed to advance the item

        // any approvers?
        // if so, add them to tasklist
        // if not, skip to next state
        Group workflowStepGroup = collectionService.getWorkflowGroup(collection, step);

        if ((workflowStepGroup != null) && !(groupService.isEmpty(workflowStepGroup))) {
            // set new item state
            workflowItem.setState(newState);

            // revoke previously granted reviewer policies and grant read permissions
            try {
                context.turnOffAuthorisationSystem();
                // revoke previously granted policies
                revokeReviewerPolicies(context, workflowItem.getItem());

                // JSPUI offers a preview to every task before a reviewer claims it.
                // So we need to grant permissions in advance, so that all possible reviewers can read the item and all
                // bitstreams in the bundle "ORIGINAL".
                authorizeService.addPolicy(context, workflowItem.getItem(), Constants.READ, workflowStepGroup,
                        ResourcePolicy.TYPE_WORKFLOW);
                Bundle originalBundle;
                try {
                    originalBundle = itemService.getBundles(workflowItem.getItem(), "ORIGINAL").get(0);
                } catch (IndexOutOfBoundsException ex) {
                    originalBundle = null;
                }
                if (originalBundle != null) {
                    authorizeService.addPolicy(context, originalBundle, Constants.READ, workflowStepGroup,
                            ResourcePolicy.TYPE_WORKFLOW);
                    for (Bitstream bitstream : originalBundle.getBitstreams()) {
                        authorizeService.addPolicy(context, bitstream, Constants.READ, workflowStepGroup,
                                ResourcePolicy.TYPE_WORKFLOW);
                    }
                }
            } finally {
                context.restoreAuthSystemState();
            }

            // get a list of all epeople in group (or any subgroups)
            List<EPerson> epa = groupService.allMembers(context, workflowStepGroup);

            // there were reviewers, change the state
            // and add them to the list
            createTasks(context, workflowItem, epa);

            if (configurationService.getBooleanProperty("workflow.notify.returned.tasks", true)
                    || oldState != correspondingState || oldOwner == null) {
                // email notification
                notifyGroupOfTask(context, workflowItem, workflowStepGroup, epa);
            }
            logWorkflowEvent(context, workflowItem.getItem(), workflowItem, context.getCurrentUser(), newState,
                    null, collection, oldState, workflowStepGroup);
            return false;
        } else {
            // no reviewers, skip ahead
            workflowItem.setState(correspondingState);
            boolean archived = advance(context, workflowItem, null, true, false);
            if (archived) {
                // remove any workflow policies that may have left over
                try {
                    context.turnOffAuthorisationSystem();
                    revokeReviewerPolicies(context, workflowItem.getItem());
                } finally {
                    context.restoreAuthSystemState();
                }
            }
            return archived;
        }
    }

    protected void logWorkflowEvent(Context context, Item item, BasicWorkflowItem workflowItem, EPerson actor,
            int newstate, EPerson newOwner, Collection mycollection, int oldState, Group newOwnerGroup) {
        if (newstate == WFSTATE_ARCHIVE || newstate == WFSTATE_STEP1POOL || newstate == WFSTATE_STEP2POOL
                || newstate == WFSTATE_STEP3POOL) {
            //Clear the newowner variable since this one isn't owned anymore !
            newOwner = null;
        }

        UsageWorkflowEvent usageWorkflowEvent = new UsageWorkflowEvent(context, item, workflowItem,
                workflowText[newstate], workflowText[oldState], mycollection, actor);
        if (newOwner != null) {
            usageWorkflowEvent.setEpersonOwners(newOwner);
        }
        if (newOwnerGroup != null) {
            usageWorkflowEvent.setGroupOwners(newOwnerGroup);
        }
        DSpaceServicesFactory.getInstance().getEventService().fireEvent(usageWorkflowEvent);
    }

    @Override
    public String getWorkflowText(int state) {
        if (state > -1 && state < workflowText.length) {
            return workflowText[state];
        }

        throw new IllegalArgumentException("Invalid workflow state passed");
    }

    /**
     * Commit the contained item to the main archive. The item is associated
     * with the relevant collection, added to the search index, and any other
     * tasks such as assigning dates are performed.
     *
     * @param context
     *     The relevant DSpace Context.
     * @param workflowItem
     *     which workflow item to archive
     * @return the fully archived item.
     * @throws IOException
     *     A general class of exceptions produced by failed or interrupted I/O operations.
     * @throws SQLException
     *     An exception that provides information on a database access error or other errors.
     * @throws AuthorizeException
     *     Exception indicating the current user of the context does not have permission
     *     to perform a particular action.
     */
    protected Item archive(Context context, BasicWorkflowItem workflowItem)
            throws SQLException, IOException, AuthorizeException {
        Item item = workflowItem.getItem();
        Collection collection = workflowItem.getCollection();

        log.info(LogManager.getHeader(context, "archive_item", "workflow_item_id=" + workflowItem.getID()
                + "item_id=" + item.getID() + "collection_id=" + collection.getID()));

        installItemService.installItem(context, workflowItem);

        // Log the event
        log.info(LogManager.getHeader(context, "install_item",
                "workflow_id=" + workflowItem.getID() + ", item_id=" + item.getID() + "handle=FIXME"));

        return item;
    }

    /**
     * notify the submitter that the item is archived
     *
     * @param context
     *     The relevant DSpace Context.
     * @param item
     *     which item was archived
     * @param coll
     *     collection name to display in template
     * @throws SQLException
     *     An exception that provides information on a database access error or other errors.
     * @throws IOException
     *     A general class of exceptions produced by failed or interrupted I/O operations.
     */
    protected void notifyOfArchive(Context context, Item item, Collection coll) throws SQLException, IOException {
        try {
            // Get submitter
            EPerson ep = item.getSubmitter();
            // Get the Locale
            Locale supportedLocale = I18nUtil.getEPersonLocale(ep);
            Email email = Email.getEmail(I18nUtil.getEmailFilename(supportedLocale, "submit_archive"));

            // Get the item handle to email to user
            String handle = handleService.findHandle(context, item);

            // Get title
            String title = item.getName();
            if (StringUtils.isBlank(title)) {
                try {
                    title = I18nUtil.getMessage("org.dspace.workflow.WorkflowManager.untitled");
                } catch (MissingResourceException e) {
                    title = "Untitled";
                }
            }

            email.addRecipient(ep.getEmail());
            email.addArgument(title);
            email.addArgument(coll.getName());
            email.addArgument(handleService.getCanonicalForm(handle));

            email.send();
        } catch (MessagingException e) {
            log.warn(LogManager.getHeader(context, "notifyOfArchive",
                    "cannot email user; item_id=" + item.getID() + ":  " + e.getMessage()));
        }
    }

    /**
     * Return the workflow item to the workspace of the submitter. The workflow
     * item is removed, and a workspace item created.
     *
     * @param c
     *            Context
     * @param wfi
     *            WorkflowItem to be 'dismantled'
     * @return the workspace item
     * @throws SQLException
     *     An exception that provides information on a database access error or other errors.
     * @throws IOException
     *     A general class of exceptions produced by failed or interrupted I/O operations.
     * @throws AuthorizeException
     *     Exception indicating the current user of the context does not have permission
     *     to perform a particular action.
     */
    protected WorkspaceItem returnToWorkspace(Context c, BasicWorkflowItem wfi)
            throws SQLException, IOException, AuthorizeException {
        // Regarding auth: this method is protected.
        // Authorization should be checked in all public methods calling this one.
        // FIXME: How should this interact with the workflow system?
        // FIXME: Remove license
        // FIXME: Provenance statement?
        // Create the new workspace item row
        WorkspaceItem workspaceItem = workspaceItemService.create(c, wfi);

        workspaceItem.setMultipleFiles(wfi.hasMultipleFiles());
        workspaceItem.setMultipleTitles(wfi.hasMultipleTitles());
        workspaceItem.setPublishedBefore(wfi.isPublishedBefore());
        workspaceItemService.update(c, workspaceItem);

        //myitem.update();
        log.info(LogManager.getHeader(c, "return_to_workspace",
                "workflow_item_id=" + wfi.getID() + "workspace_item_id=" + workspaceItem.getID()));

        // Now remove the workflow object manually from the database
        workflowItemService.deleteWrapper(c, wfi);

        return workspaceItem;
    }

    @Override
    public WorkspaceItem sendWorkflowItemBackSubmission(Context context, BasicWorkflowItem workflowItem,
            EPerson ePerson, String provenancePrefix, String rejection_message)
            throws SQLException, AuthorizeException, IOException {
        int oldState = workflowItem.getState();

        switch (oldState) {
        case WFSTATE_STEP1:
            authorizeService.authorizeActionBoolean(context, ePerson, workflowItem.getItem(),
                    Constants.WORKFLOW_STEP_1, true);
            break;
        case WFSTATE_STEP2:
            authorizeService.authorizeActionBoolean(context, ePerson, workflowItem.getItem(),
                    Constants.WORKFLOW_STEP_2, true);
            break;
        case WFSTATE_STEP3:
            authorizeService.authorizeActionBoolean(context, ePerson, workflowItem.getItem(),
                    Constants.WORKFLOW_STEP_3, true);
            break;
        default:
            throw new IllegalArgumentException("Workflow Step " + oldState + " is out of range.");
        }

        // stop workflow
        taskListItemService.deleteByWorkflowItem(context, workflowItem);

        // rejection provenance
        Item myitem = workflowItem.getItem();

        // Get current date
        String now = DCDate.getCurrent().toString();

        // Get user's name + email address
        String usersName = getEPersonName(ePerson);

        // Here's what happened
        String provDescription = "Rejected by " + usersName + ", reason: " + rejection_message + " on " + now
                + " (GMT) ";

        // Add to item as a DC field
        itemService.addMetadata(context, myitem, MetadataSchema.DC_SCHEMA, "description", "provenance", "en",
                provDescription);
        itemService.update(context, myitem);

        // convert into personal workspace
        WorkspaceItem wsi = returnToWorkspace(context, workflowItem);

        // notify that it's been rejected
        notifyOfReject(context, workflowItem, ePerson, rejection_message);

        log.info(LogManager.getHeader(context, "reject_workflow",
                "workflow_item_id=" + workflowItem.getID() + "item_id=" + workflowItem.getItem().getID()
                        + "collection_id=" + workflowItem.getCollection().getID() + "eperson_id="
                        + ePerson.getID()));

        logWorkflowEvent(context, wsi.getItem(), workflowItem, ePerson, WFSTATE_SUBMIT, null, wsi.getCollection(),
                oldState, null);

        return wsi;
    }

    // creates workflow tasklist entries for a workflow
    // for all the given EPeople
    protected void createTasks(Context c, BasicWorkflowItem wi, List<EPerson> epa) throws SQLException {
        // create a tasklist entry for each eperson
        for (EPerson anEpa : epa) {
            // can we get away without creating a tasklistitem class?
            // do we want to?
            taskListItemService.create(c, wi, anEpa);
        }
    }

    // send notices of curation activity
    @Override
    public void notifyOfCuration(Context c, BasicWorkflowItem 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 void notifyGroupOfTask(Context c, BasicWorkflowItem wi, Group mygroup, List<EPerson> epa)
            throws SQLException, IOException {
        // check to see if notification is turned off
        // and only do it once - delete key after notification has
        // been suppressed for the first time
        UUID myID = wi.getItem().getID();

        if (noEMail.containsKey(myID)) {
            // suppress email, and delete key
            noEMail.remove(myID);
        } else {
            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();

                String message = "";

                for (EPerson anEpa : epa) {
                    Locale supportedLocale = I18nUtil.getEPersonLocale(anEpa);
                    Email email = Email.getEmail(I18nUtil.getEmailFilename(supportedLocale, "submit_task"));
                    email.addArgument(title);
                    email.addArgument(coll.getName());
                    email.addArgument(submitter);

                    ResourceBundle messages = ResourceBundle.getBundle("Messages", supportedLocale);
                    switch (wi.getState()) {
                    case WFSTATE_STEP1POOL:
                        message = messages.getString("org.dspace.workflow.WorkflowManager.step1");

                        break;

                    case WFSTATE_STEP2POOL:
                        message = messages.getString("org.dspace.workflow.WorkflowManager.step2");

                        break;

                    case WFSTATE_STEP3POOL:
                        message = messages.getString("org.dspace.workflow.WorkflowManager.step3");

                        break;
                    }
                    email.addArgument(message);
                    email.addArgument(getMyDSpaceLink());
                    email.addRecipient(anEpa.getEmail());
                    email.send();
                }
            } catch (MessagingException e) {
                String gid = (mygroup != null) ? String.valueOf(mygroup.getID()) : "none";
                log.warn(LogManager.getHeader(c, "notifyGroupofTask", "cannot email user group_id=" + gid
                        + " workflow_item_id=" + wi.getID() + ":  " + e.getMessage()));
            }
        }
    }

    @Override
    public String getMyDSpaceLink() {
        return configurationService.getProperty("dspace.url") + "/mydspace";
    }

    protected void notifyOfReject(Context context, BasicWorkflowItem workflowItem, EPerson e, String reason) {
        try {
            // Get the item title
            String title = getItemTitle(workflowItem);

            // Get the collection
            Collection coll = workflowItem.getCollection();

            // Get rejector's name
            String rejector = getEPersonName(e);
            Locale supportedLocale = I18nUtil.getEPersonLocale(e);
            Email email = Email.getEmail(I18nUtil.getEmailFilename(supportedLocale, "submit_reject"));

            email.addRecipient(workflowItem.getSubmitter().getEmail());
            email.addArgument(title);
            email.addArgument(coll.getName());
            email.addArgument(rejector);
            email.addArgument(reason);
            email.addArgument(getMyDSpaceLink());

            email.send();
        } catch (RuntimeException re) {
            // log this email error
            log.warn(LogManager.getHeader(context, "notify_of_reject",
                    "cannot email user eperson_id=" + e.getID() + " eperson_email=" + e.getEmail()
                            + " workflow_item_id=" + workflowItem.getID() + ":  " + re.getMessage()));

            throw re;
        } catch (Exception ex) {
            // log this email error
            log.warn(LogManager.getHeader(context, "notify_of_reject",
                    "cannot email user eperson_id=" + e.getID() + " eperson_email=" + e.getEmail()
                            + " workflow_item_id=" + workflowItem.getID() + ":  " + ex.getMessage()));
        }
    }

    @Override
    public String getItemTitle(BasicWorkflowItem 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 ");
        }
    }

    @Override
    public String getSubmitterName(BasicWorkflowItem wi) throws SQLException {
        EPerson e = wi.getSubmitter();

        return getEPersonName(e);
    }

    protected String getEPersonName(EPerson e) throws SQLException {
        if (e == null) {
            return "Unknown";
        }
        String submitter = e.getFullName();

        submitter = submitter + " (" + e.getEmail() + ")";

        return submitter;
    }

    // Record approval provenance statement
    protected void recordApproval(Context context, BasicWorkflowItem workflowItem, EPerson e)
            throws SQLException, IOException, AuthorizeException {
        Item item = workflowItem.getItem();

        // Get user's name + email address
        String usersName = getEPersonName(e);

        // Get current date
        String now = DCDate.getCurrent().toString();

        // Here's what happened
        String provDescription = "Approved for entry into archive by " + usersName + " on " + now + " (GMT) ";

        // add bitstream descriptions (name, size, checksums)
        provDescription += installItemService.getBitstreamProvenanceMessage(context, item);

        // Add to item as a DC field
        itemService.addMetadata(context, item, MetadataSchema.DC_SCHEMA, "description", "provenance", "en",
                provDescription);
        itemService.update(context, item);
    }

    // Create workflow start provenance message
    protected void recordStart(Context context, Item myitem) throws SQLException, IOException, AuthorizeException {
        // get date
        DCDate now = DCDate.getCurrent();

        // Create provenance description
        String provmessage;

        if (myitem.getSubmitter() != null) {
            provmessage = "Submitted by " + myitem.getSubmitter().getFullName() + " ("
                    + myitem.getSubmitter().getEmail() + ") on " + now.toString() + "\n";
        } else
        // null submitter
        {
            provmessage = "Submitted by unknown (probably automated) on" + now.toString() + "\n";
        }

        // add sizes and checksums of bitstreams
        provmessage += installItemService.getBitstreamProvenanceMessage(context, myitem);

        // Add message to the DC
        itemService.addMetadata(context, myitem, MetadataSchema.DC_SCHEMA, "description", "provenance", "en",
                provmessage);
        itemService.update(context, myitem);
    }

    @Override
    public void deleteCollection(Context context, Collection collection)
            throws SQLException, IOException, AuthorizeException {
        authorizeService.authorizeAction(context, collection, Constants.WRITE);
        collection.setWorkflowGroup(context, 1, null);
        collection.setWorkflowGroup(context, 2, null);
        collection.setWorkflowGroup(context, 3, null);
        workflowItemService.deleteByCollection(context, collection);
    }

    @Override
    public List<String> getEPersonDeleteConstraints(Context context, EPerson ePerson) throws SQLException {
        List<String> resultList = new ArrayList<>();
        List<BasicWorkflowItem> workflowItems = workflowItemService.findByOwner(context, ePerson);
        if (CollectionUtils.isNotEmpty(workflowItems)) {
            resultList.add("workflowitem");
        }
        List<TaskListItem> taskListItems = taskListItemService.findByEPerson(context, ePerson);
        if (CollectionUtils.isNotEmpty(taskListItems)) {
            resultList.add("tasklistitem");
        }
        return resultList;
    }

    @Override
    public Group getWorkflowRoleGroup(Context context, Collection collection, String roleName, Group roleGroup)
            throws SQLException, AuthorizeException {
        if ("WF_STEP1".equals(roleName)) {
            roleGroup = collection.getWorkflowStep1();
            if (roleGroup == null)
                roleGroup = collectionService.createWorkflowGroup(context, collection, 1);

        } else if ("WF_STEP2".equals(roleName)) {
            roleGroup = collection.getWorkflowStep2();
            if (roleGroup == null)
                roleGroup = collectionService.createWorkflowGroup(context, collection, 2);
        } else if ("WF_STEP3".equals(roleName)) {
            roleGroup = collection.getWorkflowStep3();
            if (roleGroup == null)
                roleGroup = collectionService.createWorkflowGroup(context, collection, 3);

        }
        return roleGroup;
    }

    @Override
    public List<String> getFlywayMigrationLocations() {
        return Collections.emptyList();
    } // TODO
}