org.yawlfoundation.yawl.worklet.exception.ExceptionService.java Source code

Java tutorial

Introduction

Here is the source code for org.yawlfoundation.yawl.worklet.exception.ExceptionService.java

Source

/*
 * Copyright (c) 2004-2012 The YAWL Foundation. All rights reserved.
 * The YAWL Foundation is a collaboration of individuals and
 * organisations who are committed to improving workflow technology.
 *
 * This file is part of YAWL. YAWL is free software: you can
 * redistribute it and/or modify it under the terms of the GNU Lesser
 * General Public License as published by the Free Software Foundation.
 *
 * YAWL is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
 * Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with YAWL. If not, see <http://www.gnu.org/licenses/>.
 */

package org.yawlfoundation.yawl.worklet.exception;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.yawlfoundation.yawl.elements.data.YParameter;
import org.yawlfoundation.yawl.engine.YSpecificationID;
import org.yawlfoundation.yawl.engine.interfce.Marshaller;
import org.yawlfoundation.yawl.engine.interfce.TaskInformation;
import org.yawlfoundation.yawl.engine.interfce.WorkItemRecord;
import org.yawlfoundation.yawl.util.JDOMUtil;
import org.yawlfoundation.yawl.util.StringUtil;
import org.yawlfoundation.yawl.worklet.WorkletService;
import org.yawlfoundation.yawl.worklet.rdr.*;
import org.yawlfoundation.yawl.worklet.selection.WorkletRunner;
import org.yawlfoundation.yawl.worklet.support.EventLogger;
import org.yawlfoundation.yawl.worklet.support.WorkletSpecification;

import java.io.IOException;
import java.util.*;

import static org.yawlfoundation.yawl.worklet.rdr.RuleType.*;

/**
 *  The ExceptionService class manages the handling of exceptions that may occur
 *  during the life of a case instance. It receives events from the engine via
 *  InterfaceX at various milestones for constraint checking, and when certain
 *  exceptional events occur. It runs exception handlers when required, which may
 *  involve the running of compensatory worklets. It derives from the
 *  WorkletService class and uses many of the same methods as those used in the
 *  worklet selection process.
 *
 *  @author Michael Adams
 *  @version 0.8, 04-09/2006
 */

public class ExceptionService {

    private static Logger _log;
    private ExceptionActions _actions;
    private final ExletRunnerCache _runners;
    private final WorkletService _wService;
    private final Object _mutex = new Object();

    public ExceptionService(WorkletService workletService) {
        super();
        _log = LogManager.getLogger(ExceptionService.class);
        _runners = new ExletRunnerCache();
        _wService = workletService; // register service with parent
    }

    public void completeInitialisation(boolean persisting) {
        _actions = new ExceptionActions(_wService.getEngineClient());
        if (persisting)
            restoreDataSets(); // reload running cases data
    }

    /*******************************/
    // INTERFACE X IMPLEMENTATIONS //
    /*******************************/

    /**
     * Handles a notification from the Engine that a case has been cancelled.
     * If the case passed has any exception handling worklets running for it,
     * they are also cancelled.
     *
     * @param caseID the case cancelled by the Engine
     */
    public void handleCaseCancellationEvent(String caseID) {

        synchronized (_mutex) {
            _log.info("CASE CANCELLATION EVENT");

            caseID = getIntegralID(caseID); // strip back to basic caseID

            // cancel any/all running worklets for this case
            cancelWorkletsForCase(caseID);

            // if this case was a worklet running for another case, process
            // the worklet cancellation
            if (_runners.isCompensationWorklet(caseID)) {
                handleCompletingExceptionWorklet(caseID, null, true);
            }

            _runners.cancel(caseID);
        }
    }

    /**
     * Handles a notification from the Engine that a workitem is either starting
     * or has completed.
     * Checks the rules for the workitem, and evaluates any pre-constraints or
     * post-constraints (if any), and if a constraint has been violated, raises
     * the appropriate exception.
     *
     * @param wir the workitem that triggered the event
     * @param data the current case data (i.e. immediately prior to item start or
     *             after item completion)
     * @param preCheck true for pre-constraints, false for post-constraints
     */
    public void handleCheckWorkItemConstraintEvent(WorkItemRecord wir, String data, boolean preCheck) {
        synchronized (_mutex) {
            _log.info("CHECK WORKITEM CONSTRAINT EVENT");
            _log.info("Checking {}-constraints for workitem: {}", preCheck ? "pre" : "post", wir.getID());

            checkConstraints(wir, augmentItemData(wir, data), preCheck);
        }
    }

    /**
     * Handles a notification from the Engine that a case is either starting
     * or has completed.
     * Checks the rules for the case and evaluates any pre-constraints or
     * post-constraints (if any), and if a constraint has been violated, raises
     * the appropriate exception.
     *
     * @param specID specification id of the case
     * @param caseID the id for the case
     * @param data the case-level data params
     * @param preCheck true for pre-constraints, false for post-constraints
     */
    public void handleCheckCaseConstraintEvent(YSpecificationID specID, String caseID, String data,
            boolean preCheck) {
        synchronized (_mutex) {

            _log.info("CHECK CASE CONSTRAINT EVENT");

            Element eData = JDOMUtil.stringToElement(data);
            if (preCheck) {
                _log.info("Checking constraints for start of case {} " + "(of specification: {})", caseID, specID);

                checkConstraints(specID, caseID, eData, true);
            } else { // end of case
                _log.info("Checking constraints for end of case {}", caseID);
                checkConstraints(specID, caseID, eData, false);

                // treat this as a case complete event for exception worklets also
                if (_runners.isCompensationWorklet(caseID)) {
                    handleCompletingExceptionWorklet(caseID, eData, false);
                }
            }
        }
    }

    /**
     *  Handles a notification from the Engine that a workitem associated with the
     *  timeService has timed out.
     *  Checks the rules for timeout for the other items associated with this timeout item
     *  and raises thr appropriate exception.
     *
     * @param wir - the item that caused the timeout event
     * @param taskList - a list of taskids of those tasks that were running in
     *        parallel with the timeout task
     */
    public void handleTimeoutEvent(WorkItemRecord wir, String taskList) {

        synchronized (_mutex) {
            _log.info("TIMEOUT EVENT");
            if (taskList != null) {

                // split task list into individual taskids
                taskList = taskList.substring(1, taskList.lastIndexOf(']')); // remove [ ]

                // for each parallel task in the time out set
                for (String taskID : taskList.split(", ")) {

                    // get workitem record for this task & add it to the monitor's data
                    YSpecificationID specID = new YSpecificationID(wir);
                    List<WorkItemRecord> wirs = getWorkItemRecordsForTaskInstance(specID, taskID);
                    if (wirs != null) {
                        handleTimeout(wirs.get(0));
                    } else
                        _log.info("No live work item found for task: {}", taskID);
                }
            } else
                handleTimeout(wir);
        }
    }

    private void handleTimeout(WorkItemRecord wir) {
        handleItemException(wir, wir.getDataListString(), RuleType.ItemTimeout);
    }

    public void handleResourceUnavailableException(String resourceID, WorkItemRecord wir, String caseData,
            boolean primary) {
        handleItemException(wir, caseData, ItemResourceUnavailable);
    }

    public String handleWorkItemAbortException(WorkItemRecord wir, String data) {
        return handleItemException(wir, data, RuleType.ItemAbort);
    }

    public String handleConstraintViolationException(WorkItemRecord wir, String data) {
        return handleItemException(wir, data, ItemConstraintViolation);
    }

    private String handleItemException(WorkItemRecord wir, String dataStr, RuleType ruleType) {
        String msg;
        YSpecificationID specID = new YSpecificationID(wir);
        Element data = augmentItemData(wir, dataStr);

        // get the exception handler for this task (if any)
        RdrPair pair = getExceptionHandler(specID, wir.getTaskID(), data, ruleType);

        // if pair is null there's no rules defined for this type of constraint
        if (pair == null) {
            msg = "No " + ruleType.toLongString() + " rules defined for workitem: " + wir.getTaskID();
        } else {
            if (!pair.hasNullConclusion()) { // we have a handler
                msg = ruleType.toLongString() + " exception raised for work item: " + wir.getID();
                raiseException(wir, pair, data, ruleType);
            } else { // there are rules but the item passes
                msg = "Workitem '" + wir.getID() + "' has passed " + ruleType.toLongString() + " rules.";
            }
        }
        _log.info(msg);
        return StringUtil.wrap(msg, "result");
    }

    //***************************************************************************//

    /**
     * Checks for case-level constraint violations.
     *
     * @param pre true for pre-constraints, false for post-constraints
     */
    private void checkConstraints(YSpecificationID specID, String caseID, Element data, boolean pre) {
        String sType = pre ? "pre" : "post";
        RuleType xType = pre ? RuleType.CasePreconstraint : RuleType.CasePostconstraint;

        // get the exception handler that would result from a constraint violation
        RdrPair pair = getExceptionHandler(specID, null, data, xType);

        // if pair is null there's no rules defined for this type of constraint
        if (pair == null) {
            _log.info("No {}-case constraints defined for spec: {}", sType, specID);
        } else {
            if (!pair.hasNullConclusion()) { // there's been a violation
                _log.info("Case {} failed {}-case constraints", caseID, sType);
                raiseException(caseID, pair, data, xType);
            } else { // there are rules but the case passes
                _wService.getServer().announceConstraintPass(caseID, data, xType);
                _log.info("Case {} passed {}-case constraints", caseID, sType);
            }
        }
    }

    /**
     * Checks for item-level constraint violations.
     *
     * @param wir the WorkItemRecord for the workitem
     * @param pre true for pre-constraints, false for post-constraints
     */
    private void checkConstraints(WorkItemRecord wir, Element data, boolean pre) {
        String itemID = wir.getID();
        String taskID = wir.getTaskID();
        YSpecificationID specID = new YSpecificationID(wir);
        String sType = pre ? "pre" : "post";
        RuleType xType = pre ? RuleType.ItemPreconstraint : RuleType.ItemPostconstraint;

        // get the exception handler that would result from a constraint violation
        RdrPair pair = getExceptionHandler(specID, taskID, data, xType);

        // if pair is null there's no rules defined for this type of constraint
        if (pair == null) {
            _log.info("No {}-task constraints defined for task: {}", sType, taskID);
            if (pre)
                _wService.getEventQueue().notifyExceptionHandlingCompleted(wir);
        } else {
            if (!pair.hasNullConclusion()) { // there's been a violation
                _log.info("Workitem {} failed {}-task constraints", itemID, sType);
                raiseException(wir, pair, data, xType);
            } else { // there are rules but the case passes
                _wService.getServer().announceConstraintPass(wir, data, xType);
                _log.info("Workitem {} passed {}-task constraints", itemID, sType);
                if (pre)
                    _wService.getEventQueue().notifyExceptionHandlingCompleted(wir);
            }
        }
    }

    /**
     * Discovers whether this case or item has rules for this exception type, and if so,
     * returns the result of the rule evaluation. Note that if the conclusion
     * returned from the search is empty, no exception has occurred.
     *
     * @param taskID item's task id, or null for case-level exception
     * @param xType the type of exception triggered
     * @return an RdrConclusion representing an exception handling process,
     *         or null if no rules are defined for these criteria
     */
    private RdrPair getExceptionHandler(YSpecificationID specID, String taskID, Element data, RuleType xType) {
        return _wService.getRdrEvaluator().evaluate(specID, taskID, data, xType);
    }

    /**
     * Raises a case-level exception by creating a ExletRunner for the exception
     * process, then starting the processing of it
     * @param pair represents the exception handling process
     * @param xType the int descriptor of the exception type (WorkletService xType)
     */
    private void raiseException(String caseID, RdrPair pair, Element data, RuleType xType) {
        _log.debug("Invoking exception handling process for case: {}", caseID);
        _wService.getServer().announceException(caseID, data, pair.getLastTrueNode(), xType);
        ExletRunner runner = new ExletRunner(caseID, pair.getConclusion(), xType);
        runner.setRuleNodeID(pair.getLastTrueNode().getNodeId());
        runner.setData(data);
        processException(runner);
    }

    /**
     * Raises an item-level exception - see above for more info
     * @param pair represents the exception handling process
     * @param wir the WorkItemRecord of the item that triggered the event
     * @param xType the int descriptor of the exception type (WorkletService xType)
     */
    private void raiseException(WorkItemRecord wir, RdrPair pair, Element data, RuleType xType) {
        _log.debug("Invoking exception handling process for item: {}", wir.getID());
        _wService.getServer().announceException(wir, data, pair.getLastTrueNode(), xType);
        ExletRunner runner = new ExletRunner(wir, pair.getConclusion(), xType);
        runner.setRuleNodeID(pair.getLastTrueNode().getNodeId());
        runner.setData(data);
        processException(runner);
    }

    /**
     * Raises an external exception - see above for more info
     * @param pair represents the exception handling process
     */
    private void raiseException(ExletRunner runner, RdrPair pair, Element data) {
        _log.debug("Invoking exception handling process for external: {}", runner.getCaseID());
        _wService.getServer().announceException(runner.getWir(), data, pair.getLastTrueNode(),
                runner.getRuleType());
        runner.setRuleNodeID(pair.getLastTrueNode().getNodeId());
        processException(runner);
    }

    /**
     * Begin (or continue after a worklet completes) the exception handling process
     *
     * @param runner the HandlerRunner for this handler process
     */
    private void processException(ExletRunner runner) {
        _runners.add(runner);
        boolean doNextStep = true;

        while (runner.hasNextAction() && doNextStep) {
            doNextStep = doAction(runner); // becomes false if worklets started
            runner.incActionIndex(); // move to next primitive
        }

        // if no more actions to do (or worklets) in runner, remove it
        if (!(runner.hasNextAction() || runner.hasRunningWorklet())) {
            _runners.remove(runner);
            if (runner.getRuleType() == RuleType.ItemPreconstraint) {
                _wService.getEventQueue().notifyExceptionHandlingCompleted(runner.getWir());
            }
        }
    }

    /**
     * Perform a single step in an exception handing process
     * @param runner the HandlerRunner for this exception handling process
     * @return true if ok for processing to continue
     */
    private boolean doAction(ExletRunner runner) {
        String action = runner.getNextAction();
        String target = runner.getNextTarget();
        boolean success = true;

        _log.debug("Exception process step {}. Action = {}, Target = {}", runner.getActionIndex(), action, target);

        // short circuit if action doesn't apply to target
        if (!target.equals("workitem") && ExletAction.fromString(action).isItemOnlyAction()) {
            _log.error("Unexpected target type '{}' for action '{}'. Step ignored", target, action);
            return true;
        }

        switch (ExletAction.fromString(action)) {
        case Continue:
            doContinue(runner);
            break; // un-suspend
        case Suspend:
            doSuspend(runner);
            break;
        case Remove:
            doRemove(runner);
            break; // cancel
        case Restart:
            restartWorkItem(runner.getWir());
            break;
        case Complete:
            forceCompleteWorkItem(runner.getWir(), runner.getWorkItemUpdatedData());
            break;
        case Fail:
            failWorkItem(runner.getWir());
            break;
        case Compensate:

            // launch & run compensatory worklet(s)
            // if launch succeeds, break out of loop while worklet runs
            success = !doCompensate(runner, target);
            break;

        case Rollback: {
            _log.warn("Rollback is not yet implemented - will ignore this step.");
            break;
        }
        default: {
            _log.warn("Unrecognised action type '{}' in exception handling primitive"
                    + " - will ignore this primitive.", action);
        }
        }
        return success; // successful processing of exception primitive
    }

    /**
     * Calls the appropriate continue method for the exception target scope
     *
     * @param runner the HandlerRunner stepping through this exception process
     */
    private void doContinue(ExletRunner runner) {
        String target = runner.getNextTarget();
        switch (ExletTarget.fromString(target)) {
        case Workitem: {
            WorkItemRecord wir = _actions.unsuspendWorkItem(runner.getWir());
            runner.setItem(wir); // refresh item
            runner.unsetItemSuspended();
            break;
        }
        case Case:
        case AllCases:
        case AncestorCases: {
            _actions.unsuspendList(runner);
            runner.clearCaseSuspended();
            break;
        }
        default:
            _log.error("Unexpected target type '{}' " + "for exception handling primitive 'continue'", target);
        }
    }

    /**
     * Calls the appropriate suspend method for the exception target scope
     *
     * @param runner the HandlerRunner stepping through this exception process
     */
    private void doSuspend(ExletRunner runner) {
        String target = runner.getNextTarget();
        switch (ExletTarget.fromString(target)) {
        case Workitem:
            suspendWorkItem(runner);
            break;
        case Case:
            _actions.suspendCase(runner);
            break;
        case AncestorCases:
            _actions.suspendAncestorCases(_runners, runner);
            break;
        case AllCases:
            _actions.suspendAllCases(runner);
            break;
        default:
            _log.error("Unexpected target type '{}' " + "for exception handling primitive 'suspend'", target);
        }
    }

    /**
     * Calls the appropriate remove method for the exception target scope
     *
     * @param runner the HandlerRunner stepping through this exception process
     */
    private void doRemove(ExletRunner runner) {
        String target = runner.getNextTarget();
        switch (ExletTarget.fromString(target)) {
        case Workitem:
            removeWorkItem(runner.getWir());
            break;
        case Case:
            removeCase(runner);
            break;
        case AncestorCases:
            removeAncestorCases(runner);
            break;
        case AllCases:
            removeAllCases(runner);
            break;
        default:
            _log.error("Unexpected target type '{}' " + "for exception handling primitive 'remove'", target);
        }
    }

    private boolean doCompensate(ExletRunner runner, String target) {
        Set<WorkletSpecification> workletList = _wService.getLoader().parseTarget(target);
        Set<WorkletRunner> worklets = _wService.getEngineClient().launchWorkletList(runner.getWir(),
                runner.getDataForCaseLaunch(), workletList, runner.getRuleType());
        if (!worklets.isEmpty()) {
            for (WorkletRunner worklet : worklets) {
                worklet.setParentCaseID(runner.getCaseID());
                worklet.setRuleNodeID(runner.getRuleNodeID());
                if (runner.getRuleType().isCaseLevelType()) {
                    worklet.setParentSpecID(getSpecIDForCaseID(runner.getCaseID()));
                }
                worklet.logLaunchEvent(worklet.getParentSpecID(), null);
            }
            runner.addWorkletRunners(worklets);
        } else
            _log.error("Unable to load compensatory worklet(s), will ignore: {}", target);
        return !worklets.isEmpty();
    }

    /**
     * Deals with the end of an exception worklet case.
     *  @param caseId - the id of the completing case
     *  @param wlCasedata - the completing case's datalist Element
     *  @param cancelled - true if the worklet has been cancelled, false for a normal
     *  completion
     */
    private void handleCompletingExceptionWorklet(String caseId, Element wlCasedata, boolean cancelled) {

        // get and remove the HandlerRunner that launched this worklet
        ExletRunner runner = _runners.getRunnerForWorklet(caseId);
        _log.debug("Worklet ran as exception handler for case: {}", runner.getCaseID());

        if (!cancelled) {
            updateData(runner, wlCasedata);
        }

        WorkletRunner worklet = runner.removeWorklet(caseId);

        // log the worklet's case completion event
        String event = cancelled ? EventLogger.eCancel : EventLogger.eComplete;
        EventLogger.log(event, caseId, worklet.getParentSpecID(), "", runner.getCaseID(), -1);

        //if all worklets have completed, process the next exception primitive
        if (!runner.hasRunningWorklet()) {
            _log.info("All compensatory worklets have finished execution - " + "continuing exception processing.");
            processException(runner);
        }
    }

    /* Updates parent case or work item on completion of a worklet case.
     *
     * Update data of parent workitem/case if allowed and required and not cancelled
     * ASSUMPTION: the output data of the worklet will be used to update the
     * case/item only if:
     *   1. it is a case level exception and the case has been suspended, in
     *      which case the case-level data is updated; or
     *   2. it is an pre-executing item level exception and the item is suspended,
     *      in which case the case-level data is updated (because the item has not
     *      yet received data values from the case before starting); or
     *   3. it is an executing item exception and the item is suspended, in which
     *      case the item-level data is updated
     */
    private void updateData(ExletRunner runner, Element wlCasedata) {
        if (runner.isItemSuspended() && runner.getRuleType().isExecutingItemType()) {
            updateItemData(runner, wlCasedata);
        } else if (runner.isCaseSuspended() || runner.isItemSuspended()) {
            updateCaseData(runner, wlCasedata);
        }
    }

    /**
    * Suspends the specified workitem
    * @param hr the HandlerRunner instance with the workitem to suspend
    */
    private boolean suspendWorkItem(ExletRunner hr) {
        WorkItemRecord wir = hr.getWir();
        if (_actions.suspendWorkItem(wir)) {
            hr.setItemSuspended(); // record the action
            hr.setItem(updateWIR(wir)); // refresh the stored wir
            Set<WorkItemRecord> wirSet = new HashSet<WorkItemRecord>();
            wirSet.add(hr.getWir()); // ... and the list
            hr.setSuspendedItems(wirSet); // record the suspended item
            return true;
        }
        return false;
    }

    public boolean suspendWorkItem(String itemID) {
        return _actions.suspendWorkItem(getWorkItemRecord(itemID));
    }

    public boolean suspendCase(String caseID) {
        return _actions.suspendCase(caseID);
    }

    /**
     * Cancels the workitem specified
     * @param wir the workitem (record) to cancel
     */
    private void removeWorkItem(WorkItemRecord wir) {
        try {
            // only executing items can be removed, so if its only fired or enabled, or
            // if its suspended, move it to executing first
            wir = moveToExecuting(wir);

            if (wir.getStatus().equals(WorkItemRecord.statusExecuting)) {
                String msg = _wService.getEngineClient().cancelWorkItem(wir.getID());
                if (_wService.successful(msg)) {
                    _log.info("WorkItem successfully removed from Engine: {}", wir.getID());
                } else
                    _log.error("Failed to remove work item: {}", msg);
            } else
                _log.error("Can't remove a workitem with a status of {}", wir.getStatus());

        } catch (IOException ioe) {
            _log.error("Failed to remove workitem '{}': {}", wir.getID(), ioe.getMessage());
        }
    }

    /**
     * Cancels the specified case
     * @param hr the HandlerRunner instance with the case to cancel
     */
    private void removeCase(ExletRunner hr) {
        removeCase(hr.getCaseID());
    }

    /**
     * Cancels the specified case
     * @param caseID the id of the case to cancel
     */
    private void removeCase(String caseID) {
        try {
            if (_wService.successful(_wService.getEngineClient().cancelCase(caseID))) {
                _log.info("Case successfully removed from Engine: {}", caseID);
            }
        } catch (IOException ioe) {
            _log.error("Failed to remove case '{}': {}", caseID, ioe.getMessage());
        }
    }

    /**
     * Cancels all running instances of the specification passed
     */
    private void removeAllCases(ExletRunner runner) {
        try {
            YSpecificationID specID = getSpecIDForCaseID(runner.getCaseID());
            String casesForSpec = _wService.getEngineClient().getCases(specID);
            Element eCases = JDOMUtil.stringToElement(casesForSpec);

            for (Element eCase : eCases.getChildren()) {
                removeCase(eCase.getText());
            }
        } catch (IOException ioe) {
            _log.error("Failed to remove all cases for specification: {}", ioe.getMessage());
        }
    }

    /**
     * Cancels all running worklet cases in the hierarchy of handlers
     * @param runner - the runner for the child worklet case
     */
    private void removeAncestorCases(ExletRunner runner) {
        String caseID = getFirstAncestorCase(runner);

        _log.info("The ultimate parent case of this worklet has an id of: {}", caseID);
        _log.info("Removing all child compensatory worklets of case: {}", caseID);

        cancelWorkletsForCase(caseID);
        removeCase(caseID); // remove ultimate parent
    }

    /** returns the ultimate ancestor case of the runner passed */
    private String getFirstAncestorCase(ExletRunner runner) {
        String parentCaseID = null; // i.e. id of parent case

        // if the caseid is a started worklet handler, get it's runner
        while (runner != null) {
            parentCaseID = runner.getCaseID();
            runner = _runners.getRunnerForWorklet(parentCaseID);
        }
        return parentCaseID;
    }

    /**
     * ForceCompletes the specified workitem
     * @param wir the item to ForceComplete
     * @param out the final data params for the workitem
     */
    private void forceCompleteWorkItem(WorkItemRecord wir, Element out) {

        // only executing items can complete, so if its only fired or enabled, or
        // if its suspended, move it to executing first
        wir = moveToExecuting(wir);

        if (wir.getStatus().equals(WorkItemRecord.statusExecuting)) {
            try {
                Element data = mergeCompletionData(wir, wir.getDataList(), out);
                String msg = _wService.getEngineClient().forceCompleteWorkItem(wir, data);
                if (_wService.successful(msg)) {
                    _log.info("Item successfully force completed: {}", wir.getID());
                } else
                    _log.error("Failed to force complete workitem: {}", msg);
            } catch (IOException ioe) {
                _log.error("Failed to force complete workitem: " + wir.getID(), ioe);
            }
        } else
            _log.error("Can't force complete a workitem with a status of {}", wir.getStatus());
    }

    /** restarts the specified workitem */
    private void restartWorkItem(WorkItemRecord wir) {

        // ASSUMPTION: Only an 'executing' workitem may be restarted
        if (wir.getStatus().equals(WorkItemRecord.statusExecuting)) {
            try {
                String msg = _wService.getEngineClient().restartWorkItem(wir.getID());
                if (_wService.successful(msg)) {
                    _log.info("Item successfully restarted: {}", wir.getID());
                } else
                    _log.error("Failed to restart workitem: {}", msg);
            } catch (IOException ioe) {
                _log.error("Exception attempting restart workitem: " + wir.getID(), ioe);
            }
        } else
            _log.error("Can't restart a workitem with a status of {}", wir.getStatus());
    }

    /** Cancels a workitem and marks it as failed */
    private void failWorkItem(WorkItemRecord wir) {
        try {
            // only executing items can be failed, so if its only fired or enabled, or
            // if its suspended, move it to executing first
            wir = moveToExecuting(wir);

            // ASSUMPTION: Only an 'executing' workitem may be failed
            if (wir.getStatus().equals(WorkItemRecord.statusExecuting)) {
                String result = _wService.getEngineClient().failWorkItem(wir);
                if (_wService.successful(result)) {
                    _log.info("WorkItem successfully failed: {}", wir.getID());
                    if (result.contains("cancelled")) {
                        _log.info("Case {} was unable to continue as a consequence of the "
                                + "workItem force fail, and was also cancelled.", wir.getRootCaseID());
                    }
                } else
                    _log.error(StringUtil.unwrap(result));
            } else
                _log.error("Can't fail a workitem with a status of {}", wir.getStatus());
        } catch (IOException ioe) {
            _log.error("Exception attempting to fail workitem: " + wir.getID(), ioe);
        }
    }

    /**
     * Refreshes a locally cached WorkItemRecord with the Engine stored one
     * @param wir the item to refresh
     * @return the refreshed workitem, or the unchanged workitem on exception
     */
    private WorkItemRecord updateWIR(WorkItemRecord wir) {
        try {
            wir = _wService.getEngineStoredWorkItem(wir.getID(), _wService.getEngineClient().getSessionHandle());
        } catch (IOException ioe) {
            _log.error("IO Exception attempting to update WIR: " + wir.getID(), ioe);
        }
        return wir;
    }

    /**
     * Updates a workitem's data param values with the output data of a
     * completing worklet, then copies the updates to the engine stored workitem
     * @param runner the HandlerRunner containing the exception handling process
     * @param wlData the worklet's output data params
     */
    private void updateItemData(ExletRunner runner, Element wlData) {

        // update the items datalist with corresponding values from the worklet
        Element out = _wService.updateDataList(runner.getWorkItemDatalist(), wlData);

        // copy the updated list to the (locally cached) wir
        runner.getWir().setDataList(out);

        // and copy that back to the engine
        try {
            _wService.getEngineClient().updateWorkItemData(runner.getWir(), out);
        } catch (IOException ioe) {
            _log.error("IO Exception calling interface X");
        }
    }

    /**
     * Updates the case-level data params with the output data of a
     * completing worklet, then copies the updates to the engine stored caseData
     * @param runner the HandlerRunner containing the exception handling process
     * @param wlData the worklet's output data params
     */
    private void updateCaseData(ExletRunner runner, Element wlData) {
        try {

            // get engine copy of case data
            Element in = getCaseData(runner.getCaseID());

            // update data values as required
            Element updated = _wService.updateDataList(in, wlData);

            // and copy that back to the engine
            _wService.getEngineClient().updateCaseData(runner.getCaseID(), updated);
        } catch (IOException ioe) {
            _log.error("IO Exception calling interface X");
        }
    }

    // merge the input and output data together
    private Element mergeCompletionData(WorkItemRecord wir, Element in, Element out) throws IOException {
        String mergedOutputData = Marshaller.getMergedOutputData(in, out);
        if (StringUtil.isNullOrEmpty(mergedOutputData)) {
            if (_log.isWarnEnabled()) {
                _log.warn("Problem merging workitem data: In [{}] Out [{}]", JDOMUtil.elementToStringDump(in),
                        JDOMUtil.elementToStringDump(out));
            }
            return (in != null) ? in : out;
        }
        YSpecificationID specID = new YSpecificationID(wir);
        TaskInformation taskInfo = _wService.getTaskInformation(specID, wir.getTaskID(),
                _wService.getEngineClient().getSessionHandle());
        List<YParameter> outputParams = taskInfo.getParamSchema().getOutputParams();
        try {
            return JDOMUtil
                    .stringToElement(Marshaller.filterDataAgainstOutputParams(mergedOutputData, outputParams));
        } catch (JDOMException jde) {
            return (in != null) ? in : out;
        }
    }

    /** cancels all worklets running as exception handlers for a case when that
     *  parent case is cancelled
     */
    private void cancelWorkletsForCase(String caseID) {
        boolean hasRunningWorklet = false; // flag for msg if no runners

        // iterate through all the live runners for this case
        for (ExletRunner runner : _runners.getRunnersForCase(caseID)) {
            hasRunningWorklet = cancelWorkletsForRunner(runner) || hasRunningWorklet;
        }
        if (!hasRunningWorklet) {
            _log.info("No compensatory worklets running for cancelled case: {}", caseID);
        }
    }

    private boolean cancelWorkletsForRunner(ExletRunner runner) {

        // only want runners with live worklet(s)
        if (!runner.hasRunningWorklet())
            return false;

        // cancel each worklet case of this runner
        for (WorkletRunner worklet : runner.getWorkletRunners()) {

            // recursively call this method for each child worklet (in case
            // they also have worklets running)
            String workletCaseID = worklet.getCaseID();
            cancelWorkletsForCase(workletCaseID);

            _log.info("Worklet case running for the cancelled parent case " + "has id of: {}", workletCaseID);

            EventLogger.log(EventLogger.eCancel, workletCaseID, worklet.getWorkletSpecID(), "", runner.getCaseID(),
                    -1);

            removeCase(workletCaseID);
            runner.removeWorklet(worklet);
        }
        return true;
    }

    private WorkItemRecord moveToExecuting(WorkItemRecord wir) {
        if (wir.getStatus().equals(WorkItemRecord.statusSuspended))
            _actions.unsuspendWorkItem(wir);
        if (wir.getStatus().equals(WorkItemRecord.statusFired)
                || wir.getStatus().equals(WorkItemRecord.statusEnabled)) {
            Set<WorkItemRecord> cos = _wService.getEngineClient().checkOutItem(wir);
            if (!cos.isEmpty()) {
                wir = cos.iterator().next();
            }
        }
        return wir;
    }

    public Set<WorkletRunner> getRunningWorklets() {
        return _runners.getAllWorklets();
    }

    public List<String> getExternalTriggersForItem(String itemID) {
        return _wService.getRdrEvaluator().getExternalTriggersForItem(itemID);
    }

    /**
     * Retrieves a list of all workitems that are instances of the specified task
     * within the specified spec
     * @param specID
     * @param taskID
     * @return the list of workitems
     */
    private List<WorkItemRecord> getWorkItemRecordsForTaskInstance(YSpecificationID specID, String taskID) {

        // get all the live work items that are instances of this task
        List<WorkItemRecord> items = _wService.getEngineClient().getLiveWorkItemsForIdentifier("task", taskID);
        List<WorkItemRecord> toRemove = new ArrayList<WorkItemRecord>();

        if (items != null) {

            // filter those for this spec
            for (WorkItemRecord wir : items) {
                YSpecificationID wirSpecID = new YSpecificationID(wir);
                if (!wirSpecID.equals(specID))
                    toRemove.add(wir);
            }
            items.removeAll(toRemove);
        }
        return items;
    }

    /**
     * Strips off the non-integral part of a case id
     * @param id the case id to fix
     * @return the integral part of the caseid passed
     */
    private String getIntegralID(String id) {
        int end = id.indexOf('.'); // where's the decimal point
        if (end == -1)
            return id; // no dec point, no change required
        return id.substring(0, end); // else return its substring
    }

    /** returns the specified wir for the id passed */
    public WorkItemRecord getWorkItemRecord(String itemID) {
        try {
            return _wService.getEngineClient().getEngineStoredWorkItem(itemID);
        } catch (IOException ioe) {
            _log.error("Failed to get WIR '{}' from engine: {}", itemID, ioe.getMessage());
            return null;
        }
    }

    /** returns the spec id for the specified case id */
    public YSpecificationID getSpecIDForCaseID(String caseID) {
        return _wService.getEngineClient().getSpecificationIDForCase(caseID);
    }

    /** retrieves a complete list of external exception triggers from the ruleset
     *  for the specified case
     * @param caseID - the id of the case to get the triggers for
     * @return the (String) list of triggers
     */
    public List getExternalTriggersForCase(String caseID) {
        YSpecificationID specID = getSpecIDForCaseID(caseID);
        if (specID != null) {
            RdrTree tree = _wService.getRdrEvaluator().getTree(specID, null, CaseExternalTrigger);
            return _wService.getRdrEvaluator().getExternalTriggers(tree);
        }
        return null;
    }

    /**
     * Raise an externally triggered exception
     * @param level - the level of the exception (case/item)
     * @param id - the id of the case or item on which the exception is being raised
     * @param trigger - the identifier of (or reason for) the external exception
     */
    public void raiseExternalException(String level, String id, String trigger) {

        synchronized (_mutex) {
            _log.info("EXTERNAL EXCEPTION EVENT"); // note to log

            String caseID, taskID;
            YSpecificationID specID;
            WorkItemRecord wir = null;
            RuleType xType;

            if (level.equalsIgnoreCase("case")) { // if case level
                caseID = id;
                specID = getSpecIDForCaseID(caseID);
                taskID = null;
                xType = CaseExternalTrigger;
            } else { // else item level
                wir = getWorkItemRecord(id);
                caseID = wir.getRootCaseID();
                specID = new YSpecificationID(wir);
                taskID = wir.getTaskID();
                xType = ItemExternalTrigger;
            }

            // add trigger value to case data
            Element eData = augmentExternalData(getCaseData(caseID), trigger);

            // get the exception handler for this trigger
            RdrPair pair = getExceptionHandler(specID, taskID, eData, xType);

            // if pair is null there's no rules defined for this type of constraint
            if (pair == null) {
                _log.error(
                        "No external exception rules defined for spec: {}" + ". Unable to raise exception for '{}'",
                        specID, trigger);
            } else {
                ExletRunner runner = (wir == null) ? new ExletRunner(caseID, pair.getConclusion(), xType)
                        : new ExletRunner(wir, pair.getConclusion(), xType);
                runner.setTrigger(trigger);
                runner.setData(eData);
                raiseException(runner, pair, eData);
            }
        }
    }

    private Element getCaseData(String caseID) {
        String data;
        try {
            data = _wService.getEngineClient().getCaseData(caseID);
        } catch (IOException ioe) {
            data = "<caseData/>";
        }
        return JDOMUtil.stringToElement(data);
    }

    /**
     * Raises an exception (triggered via the API) using the conclusion passed in
     * @param wir the wir to raise the exception for
     * @param ruleType the exception rule type
     * @param conclusion a populated conclusion object
     */
    public String raiseException(WorkItemRecord wir, RuleType ruleType, RdrConclusion conclusion) {
        RdrNode dummyNode = new RdrNode();
        dummyNode.setConclusion(conclusion);
        RdrPair dummyPair = new RdrPair(dummyNode, dummyNode);
        raiseException(wir, dummyPair, null, ruleType);
        return StringUtil.wrap(ruleType.toLongString() + " exception raised for work item: " + wir.getID(),
                "result");
    }

    /**
     *  Replaces a running worklet case with another worklet case after an
     *  amendment to the ruleset for this exception.
     *  Called by WorkletGateway after a call from the Editor that the ruleset
     *  has been updated.
     *  Overrides the WorkletService equivalent - This one looks after exceptions,
     *  that one looks after selections
     *
     *  @param xType - the type of exception that launched the worklet
     *  @param caseID - the id of the original checked out case
     *  @param itemID - the id of the original checked out workitem
     *  @return a string of messages describing the success or otherwise of
     *          the process
     */
    public String replaceWorklet(RuleType xType, String caseID, String itemID) throws IOException {

        synchronized (_mutex) {
            _log.info("REPLACE EXLET EVENT");

            caseID = getIntegralID(caseID);

            // get the ExletRunner for the exception
            ExletRunner runner = _runners.getRunner(xType, caseID, itemID);
            if (runner == null) {
                raise("No exlets running for case: " + caseID);
            }

            // cancel any compensatory worklets running for the case/workitem
            cancelWorkletsForRunner(runner);

            // remove runner from running exlet handlers
            _runners.remove(runner);

            // go through the process again, depending on the exception type
            reevaluate(runner, xType);

            // get the new ExletRunner for the new exception
            runner = _runners.getRunner(xType, caseID, itemID);
            if (runner == null) {
                raise("Failed to start new exlet runner for case: " + caseID);
            }

            return _wService.getRunnerCaseIdList(runner.getWorkletRunners());
        }
    }

    private void reevaluate(ExletRunner runner, RuleType xType) {
        _log.debug("Reevaluating exception for revised ruleset");
        String caseID = runner.getCaseID();
        String trigger = runner.getTrigger();
        Element data = getCaseData(caseID);
        WorkItemRecord wir = xType.isItemLevelType() ? runner.getWir() : null;
        YSpecificationID specID = getSpecIDForCaseID(caseID);
        switch (xType) {
        case CasePreconstraint:
            checkConstraints(specID, caseID, data, true);
            break;
        case CasePostconstraint:
            checkConstraints(specID, caseID, data, false);
            break;
        case ItemPreconstraint:
            checkConstraints(wir, data, true);
            break;
        case ItemPostconstraint:
            checkConstraints(wir, data, false);
            break;
        case ItemAbort:
            if (wir != null)
                handleWorkItemAbortException(wir, wir.getDataListString());
            break;
        case ItemTimeout:
            if (wir != null)
                handleTimeoutEvent(wir, wir.getTaskID());
            break;
        case ItemResourceUnavailable:
            break; // todo
        case ItemConstraintViolation:
            if (wir != null)
                handleConstraintViolationException(wir, wir.getDataListString());
            break;
        case CaseExternalTrigger:
            raiseExternalException("case", caseID, trigger);
            break;
        case ItemExternalTrigger:
            raiseExternalException("item", caseID, trigger);
            break;
        }
    }

    /** returns true if case specified is a worklet instance */
    public boolean isWorkletCase(String caseID) {
        return (_runners.isCompensationWorklet(caseID));
    }

    /** stub method called from RdrConditionFunctions class */
    public String getStatus(String taskID) {
        return null;
    }

    /** restores the contents of the running datasets after a web server restart */
    private void restoreDataSets() {
        _runners.restore();
    }

    /**
     * Adds the contents of the workitem record to the case data for this case - thus
     * providing information about the workitem to the ruleset
     * @param wir - the wir being tested for an exception
     */
    private Element augmentItemData(WorkItemRecord wir, String dataStr) {
        Element data = dataStr != null ? JDOMUtil.stringToElement(dataStr) : new Element("data");

        // blend in any case level data not extant in the work item's data
        //   data = blendInCaseData(wir, data);

        //convert the wir contents to an Element
        Element eWir = JDOMUtil.stringToElement(wir.toXML()).detach();

        Element eInfo = new Element("process_info"); // new Element for info
        eInfo.addContent(eWir); // add the wir
        data.addContent(eInfo); // add element to case data
        return data;
    }

    private Element blendInCaseData(WorkItemRecord wir, Element itemData) {
        if (itemData == null)
            return null;
        Element caseData = getCaseData(wir.getRootCaseID());
        if (caseData != null) {
            Iterator<Element> itr = caseData.getChildren().iterator(); // avoid ConModEx
            while (itr.hasNext()) {
                Element child = itr.next();
                String name = child.getName();

                // assumption: if itemData contains an element of the same name as
                // caseData, then the itemData element's value is the same as, or newer
                // than, the caseData element's value. Therefore, only missing elements
                // should be added
                if (itemData.getChild(name) == null) {
                    itemData.addContent(child.clone());
                }
            }
        }
        return itemData;
    }

    /**
     *  Adds an external exception trigger to the case data so that the correct
     *  RDR can be found for it
     * @param triggerValue - the string value of the external trigger
     */
    private Element augmentExternalData(Element data, String triggerValue) {

        // all external triggers must be delimited with "'s
        if (!triggerValue.startsWith("\""))
            triggerValue = "\"" + triggerValue + "\"";

        Element eTrigger = new Element("trigger"); // new Element for trigger
        eTrigger.addContent(triggerValue); // add the text
        data.addContent(eTrigger); // add element to case data
        return data;
    }

    protected void raise(String msg) throws IOException {
        _log.error(msg);
        throw new IOException(msg);
    }

} // end of ExceptionService class