/******************************************************************************* * 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; } }