alma.acs.nc.sm.generic.AcsScxmlEngine.java Source code

Java tutorial

Introduction

Here is the source code for alma.acs.nc.sm.generic.AcsScxmlEngine.java

Source

/*******************************************************************************
 * ALMA - Atacama Large Millimeter Array
 * Copyright (c) ESO - European Southern Observatory, 2011
 * (in the framework of the ALMA collaboration).
 * All rights reserved.
 * 
 * This library 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; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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 this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
 *******************************************************************************/
package alma.acs.nc.sm.generic;

import java.io.IOException;
import java.net.URL;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.scxml.Context;
import org.apache.commons.scxml.Evaluator;
import org.apache.commons.scxml.EventDispatcher;
import org.apache.commons.scxml.SCXMLExecutor;
import org.apache.commons.scxml.TriggerEvent;
import org.apache.commons.scxml.env.SimpleDispatcher;
import org.apache.commons.scxml.env.Tracer;
import org.apache.commons.scxml.env.jexl.JexlContext;
import org.apache.commons.scxml.env.jexl.JexlEvaluator;
import org.apache.commons.scxml.io.SCXMLParser;
import org.apache.commons.scxml.model.CustomAction;
import org.apache.commons.scxml.model.ModelException;
import org.apache.commons.scxml.model.SCXML;
import org.apache.commons.scxml.model.Transition;
import org.apache.commons.scxml.model.TransitionTarget;
import org.xml.sax.SAXException;

import alma.ACSErrTypeCommon.wrappers.AcsJIllegalStateEventEx;
import alma.ACSErrTypeCommon.wrappers.AcsJStateMachineActionEx;
import alma.acs.nc.sm.generic.AcsScxmlActionDispatcher.ActionExceptionHandler;

/**
 * Class that encapsulates the Scxml engine for ACS.
 * It exposes only a subset of the many features offered by SCXML but also 
 * adds functionality. The focus is on compile time checking,
 * connection of action handler code with other pieces of software,
 * and exception handling. We hide dynamic changes to the state model and asynchronous
 * (queued) processing of multiple events.
 * <p>
 * The code has been taken from ESO SM framework class SMEngine.``
 * <p>
 * @TODO: Make the enum parameters distinguishable by introducing interfaces for action and signal enums. 
 *        Also consider making the SM state an enum type instead of String.
 * <p>
 * This class is thread safe with respect to sending events and checking the current state. 
 * The internally called methods {@link SCXMLExecutor#triggerEvent(TriggerEvent)} and 
 * {@link SCXMLExecutor#getCurrentStatus()} are synchronized.
 * In addition to the synchronization done by the underlying SCXMLExecutor we synchronize
 * {@link #fireSignal(Enum)} also in this class, so that the 'isFinal' call that delivers the return value
 * is guaranteed to run right after the signal was processed.
 * 
 * @param <S> The SM-specific signal enum.
 * @param <A> The SM-specific action enum.
 */
public class AcsScxmlEngine<S extends Enum<S>, A extends Enum<A>> {
    private final Logger logger;
    private final AcsScxmlActionDispatcher<A> actionDispatcher;
    private final Class<S> signalType;
    private final Tracer errorTracer; // TODO: Allow user to supply own impl
    private final Evaluator exprEvaluator;
    private final EventDispatcher eventDispatcher;
    private final Context exprContext;
    private volatile SCXMLExecutor exec;
    private SCXML scxml;

    /**
     * @param scxmlFileName The qualified xml file name, e.g. "/alma/acs/nc/sm/EventSubscriberStates.xml",
     *                      in the form that {@link Class#getResource(String)} can use to load the scxml
     *                      definition file from the classpath. 
     * @param logger
     * @param actionDispatcher 
     * @param signalType enum class, needed to convert signal names to enum values.
     * @throws IllegalArgumentException if any of the args are <code>null</code> or if the <code>actionDispatcher</code>
     *                                  is not complete for all possible actions.
     */
    public AcsScxmlEngine(String scxmlFileName, Logger logger, AcsScxmlActionDispatcher<A> actionDispatcher,
            Class<S> signalType) {

        this.logger = logger;
        this.actionDispatcher = actionDispatcher;
        this.signalType = signalType;

        // TODO decide if we want to insist here, or let the user check this beforehand
        if (!actionDispatcher.isActionMappingComplete()) {
            throw new IllegalArgumentException("actionDispatcher is not complete.");
        }

        errorTracer = new Tracer(); // create error tracer
        exprEvaluator = new JexlEvaluator(); // Evaluator evaluator = new ELEvaluator();
        eventDispatcher = new SimpleDispatcher(); // create event dispatcher
        exprContext = new JexlContext(); // set new context

        // Adding AcsScxmlActionDispatcher to the SM root context 
        // so that the generated action classes can get it from there and can delegate action calls.
        exprContext.set(AcsScxmlActionDispatcher.class.getName(), actionDispatcher);

        try {
            // load the scxml model
            loadModel(scxmlFileName);

            startExecution();

        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Failed to load or start the state machine.", ex); // TODO
        }

    }

    /**
     * Loads the SCXML model from an XML file stored inside of a jar file on the classpath.
     * <p>
     * TODO: define and throw exception in case of load/parse failure.
     * 
     * @param scxmlFileName The qualified xml file name, e.g. "/alma/acs/nc/sm/generated/EventSubscriberSCXML.xml"
     */
    public void loadModel(final String scxmlFileName) {

        try {
            // TODO: Pass InputSource instead of String,
            // because the xml file may be inside a component impl jar file
            // which is not visible to the classloader of this generic SMEngine class.
            URL scxmlUrl = getClass().getResource(scxmlFileName);

            if (scxmlUrl == null) {
                logger.severe(
                        "Failed to load the scxml definition file '" + scxmlFileName + "' from the classpath.");
                // TODO ex;
            }

            List<CustomAction> scxmlActions = actionDispatcher.getScxmlActionMap();
            scxml = SCXMLParser.parse(scxmlUrl, errorTracer, scxmlActions);
            logger.fine("Loaded SCXML file " + scxmlUrl.toString() + "...");
        } catch (ModelException e) {
            logger.severe("Could not load model: " + e.getMessage());
        } catch (SAXException e) {
            logger.severe("Could not load model: " + e.getMessage());
        } catch (IOException e) {
            logger.severe("Could not load model: " + e.getMessage());
        }
    }

    /**
     * Starts SCXML execution.
     * <p>
     * TODO: define and throw exception in case of model failure.
     */
    public void startExecution() {

        try {
            exec = new SCXMLExecutor(exprEvaluator, eventDispatcher, errorTracer);

            // make sure scxml is a valid SCXML doc -> ToBeDone

            exec.addListener(scxml, errorTracer);
            exec.setRootContext(exprContext);

            exec.setStateMachine(scxml);
            //         @TODO: When do we need a java invoker?
            //         exec.registerInvokerClass("java", SMJavaInvoker.class);
            exec.go();
        } catch (ModelException e) {
            logger.severe("Could not start SM execution: " + e.getMessage());
        }

        logger.fine("Started SM execution ...");
    }

    /**
     * Retrieves the current state as a string.
     * @return The state name(s). 
     *         Hierarchical states are separated by "::", with outer state first, e.g. "EnvironmentCreated::Connected::Suspended".
     *         Parallel states are separated by " ".
     */
    public String getCurrentState() {
        @SuppressWarnings("unchecked")
        Set<TransitionTarget> activeStates = exec.getCurrentStatus().getStates();

        StringBuilder sb = new StringBuilder();
        Iterator<TransitionTarget> iter = activeStates.iterator();
        while (iter.hasNext()) {
            sb.append(iter.next().getId());
            if (iter.hasNext()) {
                sb.append(' ');
            }
        }
        return sb.toString();
    }

    /**
     * Checks if a given state is active. 
     * The matching against the current state is done via String comparison, so that
     * especially for hierarchical states it makes sense to call this method
     * asking only for the outer state name(s).
     * <p>
     * TODO: Protect against mismatches that can occur if one state name includes another state name as a substring, 
     *       e.g. by splitting names at "::" and comparing those fragements.
     *  
     * @param stateName The state name (fragment). 
     *         Hierarchical states are separated by "::", with outer state first, e.g. "EnvironmentCreated::Connected".
     * @return <code>true</code> if the given state is active.
     */
    public synchronized boolean isStateActive(String stateName) {
        @SuppressWarnings("unchecked")
        Set<TransitionTarget> activeStates = exec.getCurrentStatus().getStates();

        for (TransitionTarget tt : activeStates) {
            if (tt.getId().indexOf(stateName) >= 0) {
                return true;
            }
        }
        return false;
    }

    /**
     * Exposes the underlying SCXMLExecutor engine,
     * for more specialized calls that we don't put in API methods for.
     */
    public SCXMLExecutor getEngine() {
        return exec;
    }

    /**
     * Sends a signal (event) to the state machine.
     * 
     * The call is synchronous and returns only when the state machine has gone through all transitions/actions, 
     * where of course a /do activity would still continue to run asynchronously.
     * Note that the underlying SCXML engine also supports asynchronous sending of multiple events
     * at a time, but we do not expose this feature in ACS.
     * <p>
     * TODO: How can the client find out whether the signal was applicable 
     *       for the current state or was ignored?
     *       
     * @return True - if all the states are final and there are not events
     *         pending from the last step. False - otherwise.
     */
    public synchronized boolean fireSignal(S signal) {
        TriggerEvent evnt = new TriggerEvent(signal.name(), TriggerEvent.SIGNAL_EVENT, null);
        try {
            exec.triggerEvent(evnt);
        } catch (ModelException e) {
            logger.info(e.getMessage());
        }
        return exec.getCurrentStatus().isFinal();
    }

    /**
     * Simply stores the first exception it receives for later use.
     * This means that if we have multiple transitions for an event, 
     * and more than one transition throws an exception, then it is the first exception 
     * that will get thrown to the user. 
     */
    private static class MyActionExceptionHandler implements ActionExceptionHandler {
        volatile AcsJStateMachineActionEx theEx;

        @Override
        public void setActionException(AcsJStateMachineActionEx ex) {
            if (theEx == null) {
                theEx = ex;
            }
        }
    }

    /**
     * Synchronous event handling as in {@link #fireSignal(Enum)}, 
     * but possibly with exceptions for the following cases:
     * <ul>
     *   <li> The <code>signal</code> gets checked if it can be handled by the current state(s);
     *        an <code>AcsJIllegalStateEventEx</code> exception is thrown if not.
     *   <li> If an executed action throws a AcsJStateMachineActionEx exception, that exception gets thrown here.
     *        Depending on the concrete state machine, an additional response to the error may be 
     *        that the SM goes to an error state, due to an internal event triggered by the action.
     *   <li> <code>ModelException</code>, as thrown by {@link SCXMLExecutor#triggerEvent}, unlikely
     *        with our static use of the SCXML engine.
     * </ul>
     * @param signal
     * @return True - if all the states are final and there are not events
     *         pending from the last step. False - otherwise.
     * @throws AcsJIllegalStateEventEx
     * @throws AcsJStateMachineActionEx
     * @throws ModelException
     */
    public synchronized boolean fireSignalWithErrorFeedback(S signal)
            throws AcsJIllegalStateEventEx, AcsJStateMachineActionEx, ModelException {

        // check if signal is OK, throw exception if not.
        Set<S> applicableSignals = getApplicableSignals();
        if (!applicableSignals.contains(signal)) {
            AcsJIllegalStateEventEx ex = new AcsJIllegalStateEventEx();
            ex.setEvent(signal.name());
            ex.setState(getCurrentState());
            throw ex;
        }

        // Register error callback with action dispatcher.
        // This is only thread safe because this method is synchronized and we 
        // execute only one event at a time.
        MyActionExceptionHandler handler = new MyActionExceptionHandler();
        actionDispatcher.setActionExceptionHandler(handler);
        try {
            TriggerEvent evnt = new TriggerEvent(signal.name(), TriggerEvent.SIGNAL_EVENT, null);
            exec.triggerEvent(evnt);

            if (handler.theEx != null) {
                throw handler.theEx;
            } else {
                // either there was no action associated with the event,
                // or all actions executed without exception.
                return exec.getCurrentStatus().isFinal();
            }
        } finally {
            actionDispatcher.setActionExceptionHandler(null);
        }
    }

    /**
     * Gets the signals that would trigger transitions for the current state.
     * <p>
     * When actually sending such signals later on, the SM may have moved to a different state.
     * To prevent this, you can synchronize on this AcsScxmlEngine, which will block concurrent calls to {@link #fireSignal(Enum)}.
     * <p>
     * This method can be useful for displaying applicable signals in a GUI,
     * or to reject signals (with exception etc) that do not "fit" the current state
     * (while normally such signals would be silently ignored). 
     * The latter gets used in {@link #fireSignalWithErrorFeedback(Enum)}.
     * 
     * @see org.apache.commons.scxml.semantics.SCXMLSemanticsImpl#enumerateReachableTransitions(SCXML, Step, ErrorReporter)
     */
    public synchronized Set<S> getApplicableSignals() {
        Set<String> events = new HashSet<String>();

        @SuppressWarnings("unchecked")
        Set<TransitionTarget> stateSet = new HashSet<TransitionTarget>(exec.getCurrentStatus().getStates());
        LinkedList<TransitionTarget> todoList = new LinkedList<TransitionTarget>(stateSet);

        while (!todoList.isEmpty()) {
            TransitionTarget tt = todoList.removeFirst();
            @SuppressWarnings("unchecked")
            List<Transition> transitions = tt.getTransitionsList();
            for (Transition t : transitions) {
                String event = t.getEvent();
                events.add(event);
            }
            TransitionTarget parentTT = tt.getParent();
            if (parentTT != null && !stateSet.contains(parentTT)) {
                stateSet.add(parentTT);
                todoList.addLast(parentTT);
            }
        }

        // convert signal names to enum constants
        Set<S> ret = new HashSet<S>();
        for (String signalName : events) {
            S signal = Enum.valueOf(signalType, signalName);
            ret.add(signal);
        }
        return ret;
    }
}