    001    /*
    002     * Licensed to the Apache Software Foundation (ASF) under one or more
    003     * contributor license agreements.  See the NOTICE file distributed with
    004     * this work for additional information regarding copyright ownership.
    005     * The ASF licenses this file to You under the Apache License, Version 2.0
    006     * (the "License"); you may not use this file except in compliance with
    007     * the License.  You may obtain a copy of the License at
    008     *
    009     *     http://www.apache.org/licenses/LICENSE-2.0
    010     *
    011     * Unless required by applicable law or agreed to in writing, software
    012     * distributed under the License is distributed on an "AS IS" BASIS,
    013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014     * See the License for the specific language governing permissions and
    015     * limitations under the License.
    016     */
    017    package org.apache.commons.scxml;
    018
    019    import java.io.Serializable;
    020    import java.util.ArrayList;
    021    import java.util.Arrays;
    022    import java.util.HashMap;
    023    import java.util.Iterator;
    024    import java.util.List;
    025    import java.util.Map;
    026
    027    import org.apache.commons.logging.Log;
    028    import org.apache.commons.logging.LogFactory;
    029    import org.apache.commons.scxml.model.Datamodel;
    030    import org.apache.commons.scxml.model.History;
    031    import org.apache.commons.scxml.model.ModelException;
    032    import org.apache.commons.scxml.model.SCXML;
    033    import org.apache.commons.scxml.model.State;
    034    import org.apache.commons.scxml.model.Transition;
    035    import org.apache.commons.scxml.model.TransitionTarget;
    036    import org.apache.commons.scxml.semantics.SCXMLSemanticsImpl;
    037
    038    /**
    039     * <p>The SCXML "engine" that executes SCXML documents. The
    040     * particular semantics used by this engine for executing the SCXML are
    041     * encapsulated in the SCXMLSemantics implementation that it uses.</p>
    042     *
    043     * <p>The default implementation is
    044     * <code>org.apache.commons.scxml.semantics.SCXMLSemanticsImpl</code></p>
    045     *
    046     * @see SCXMLSemantics
    047     */
    048    public class SCXMLExecutor implements Serializable {
    049
    050        /**
    051         * Serial version UID.
    052         */
    053        private static final long serialVersionUID = 1L;
    054
    055        /**
    056         * The Logger for the SCXMLExecutor.
    057         */
    058        private Log log = LogFactory.getLog(SCXMLExecutor.class);
    059
    060        /**
    061         * The stateMachine being executed.
    062         */
    063        private SCXML stateMachine;
    064
    065        /**
    066         * The current status of the stateMachine.
    067         */
    068        private Status currentStatus;
    069
    070        /**
    071         * The event dispatcher to interface with external documents etc.
    072         */
    073        private EventDispatcher eventdispatcher;
    074
    075        /**
    076         * The environment specific error reporter.
    077         */
    078        private ErrorReporter errorReporter = null;
    079
    080        /**
    081         * Run-to-completion.
    082         */
    083        private boolean superStep = true;
    084
    085        /**
    086         *  Interpretation semantics.
    087         */
    088        private SCXMLSemantics semantics;
    089
    090        /**
    091         * The SCInstance.
    092         */
    093        private SCInstance scInstance;
    094
    095        /**
    096         * The worker method.
    097         * Re-evaluates current status whenever any events are triggered.
    098         *
    099         * @param evts
    100         *            an array of external events which triggered during the last
    101         *            time quantum
    102         * @throws ModelException in case there is a fatal SCXML object
    103         *            model problem.
    104         */
    105        public synchronized void triggerEvents(final TriggerEvent[] evts)
    106                throws ModelException {
    107            // Set event data, saving old values
    108            Object[] oldData = setEventData(evts);
    109
    110            // Forward events (external only) to any existing invokes,
    111            // and finalize processing
    112            semantics.processInvokes(evts, errorReporter, scInstance);
    113
    114            List evs = new ArrayList(Arrays.asList(evts));
    115            Step step = null;
    116
    117            do {
    118                // CreateStep
    119                step = new Step(evs, currentStatus);
    120                // EnumerateReachableTransitions
    121                semantics.enumerateReachableTransitions(stateMachine, step,
    122                    errorReporter);
    123                // FilterTransitionSet
    124                semantics.filterTransitionsSet(step, eventdispatcher,
    125                    errorReporter, scInstance);
    126                // FollowTransitions
    127                semantics.followTransitions(step, errorReporter, scInstance);
    128                // UpdateHistoryStates
    129                semantics.updateHistoryStates(step, errorReporter, scInstance);
    130                // ExecuteActions
    131                semantics.executeActions(step, stateMachine, eventdispatcher,
    132                    errorReporter, scInstance);
    133                // AssignCurrentStatus
    134                updateStatus(step);
    135                // ***Cleanup external events if superStep
    136                if (superStep) {
    137                    evs.clear();
    138                }
    139            } while (superStep && currentStatus.getEvents().size() > 0);
    140
    141            // InitiateInvokes only after state machine has stabilized
    142            semantics.initiateInvokes(step, errorReporter, scInstance);
    143
    144            // Restore event data
    145            restoreEventData(oldData);
    146            logState();
    147        }
    148
    149        /**
    150         * Convenience method when only one event needs to be triggered.
    151         *
    152         * @param evt
    153         *            the external events which triggered during the last
    154         *            time quantum
    155         * @throws ModelException in case there is a fatal SCXML object
    156         *            model problem.
    157         */
    158        public void triggerEvent(final TriggerEvent evt)
    159                throws ModelException {
    160            triggerEvents(new TriggerEvent[] {evt});
    161        }
    162
    163        /**
    164         * Constructor.
    165         *
    166         * @param expEvaluator The expression evaluator
    167         * @param evtDisp The event dispatcher
    168         * @param errRep The error reporter
    169         */
    170        public SCXMLExecutor(final Evaluator expEvaluator,
    171                final EventDispatcher evtDisp, final ErrorReporter errRep) {
    172            this(expEvaluator, evtDisp, errRep, null);
    173        }
    174
    175        /**
    176         * Convenience constructor.
    177         */
    178        public SCXMLExecutor() {
    179            this(null, null, null, null);
    180        }
    181
    182        /**
    183         * Constructor.
    184         *
    185         * @param expEvaluator The expression evaluator
    186         * @param evtDisp The event dispatcher
    187         * @param errRep The error reporter
    188         * @param semantics The SCXML semantics
    189         */
    190        public SCXMLExecutor(final Evaluator expEvaluator,
    191                final EventDispatcher evtDisp, final ErrorReporter errRep,
    192                final SCXMLSemantics semantics) {
    193            this.eventdispatcher = evtDisp;
    194            this.errorReporter = errRep;
    195            this.currentStatus = new Status();
    196            this.stateMachine = null;
    197            if (semantics == null) {
    198                // Use default semantics, if none provided
    199                this.semantics = new SCXMLSemanticsImpl();
    200            } else {
    201                this.semantics = semantics;
    202            }
    203            this.scInstance = new SCInstance(this);
    204            this.scInstance.setEvaluator(expEvaluator);
    205        }
    206
    207        /**
    208         * Clear all state and begin from "initialstate" indicated
    209         * on root SCXML element.
    210         *
    211         * @throws ModelException in case there is a fatal SCXML object
    212         *         model problem.
    213         */
    214        public synchronized void reset() throws ModelException {
    215            // Reset all variable contexts
    216            Context rootCtx = scInstance.getRootContext();
    217            // Clone root datamodel
    218            if (stateMachine == null) {
    219                log.error(ERR_NO_STATE_MACHINE);
    220                throw new ModelException(ERR_NO_STATE_MACHINE);
    221            } else {
    222                Datamodel rootdm = stateMachine.getDatamodel();
    223                SCXMLHelper.cloneDatamodel(rootdm, rootCtx,
    224                    scInstance.getEvaluator(), log);
    225            }
    226            // all states and parallels, only states have variable contexts
    227            for (Iterator i = stateMachine.getTargets().values().iterator();
    228                    i.hasNext();) {
    229                TransitionTarget tt = (TransitionTarget) i.next();
    230                if (tt instanceof State) {
    231                    Context context = scInstance.lookupContext(tt);
    232                    if (context != null) {
    233                        context.reset();
    234                        Datamodel dm = tt.getDatamodel();
    235                        if (dm != null) {
    236                            SCXMLHelper.cloneDatamodel(dm, context,
    237                                scInstance.getEvaluator(), log);
    238                        }
    239                    }
    240                } else if (tt instanceof History) {
    241                    scInstance.reset((History) tt);
    242                }
    243            }
    244            // CreateEmptyStatus
    245            currentStatus = new Status();
    246            Step step = new Step(null, currentStatus);
    247            // DetermineInitialStates
    248            semantics.determineInitialStates(stateMachine,
    249                    step.getAfterStatus().getStates(),
    250                    step.getEntryList(), errorReporter, scInstance);
    251            // ExecuteActions
    252            semantics.executeActions(step, stateMachine, eventdispatcher,
    253                    errorReporter, scInstance);
    254            // AssignCurrentStatus
    255            updateStatus(step);
    256            // Execute Immediate Transitions
    257            if (superStep && currentStatus.getEvents().size() > 0) {
    258                this.triggerEvents(new TriggerEvent[0]);
    259            } else {
    260                // InitiateInvokes only after state machine has stabilized
    261                semantics.initiateInvokes(step, errorReporter, scInstance);
    262                logState();
    263            }
    264        }
    265
    266        /**
    267         * Get the current status.
    268         *
    269         * @return The current Status
    270         */
    271        public synchronized Status getCurrentStatus() {
    272            return currentStatus;
    273        }
    274
    275        /**
    276         * Set the expression evaluator.
    277         * <b>NOTE:</b> Should only be used before the executor is set in motion.
    278         *
    279         * @param evaluator The evaluator to set.
    280         */
    281        public void setEvaluator(final Evaluator evaluator) {
    282            this.scInstance.setEvaluator(evaluator);
    283        }
    284
    285        /**
    286         * Get the expression evaluator in use.
    287         *
    288         * @return Evaluator The evaluator in use.
    289         */
    290        public Evaluator getEvaluator() {
    291            return scInstance.getEvaluator();
    292        }
    293
    294        /**
    295         * Set the root context for this execution.
    296         * <b>NOTE:</b> Should only be used before the executor is set in motion.
    297         *
    298         * @param rootContext The Context that ties to the host environment.
    299         */
    300        public void setRootContext(final Context rootContext) {
    301            this.scInstance.setRootContext(rootContext);
    302        }
    303
    304        /**
    305         * Get the root context for this execution.
    306         *
    307         * @return Context The root context.
    308         */
    309        public Context getRootContext() {
    310            return scInstance.getRootContext();
    311        }
    312
    313        /**
    314         * Get the state machine that is being executed.
    315         * <b>NOTE:</b> This is the state machine definition or model used by this
    316         * executor instance. It may be shared across multiple executor instances
    317         * and as a best practice, should not be altered. Also note that
    318         * manipulation of instance data for the executor should happen through
    319         * its root context or state contexts only, never through the direct
    320         * manipulation of any {@link Datamodel}s associated with this state
    321         * machine definition.
    322         *
    323         * @return Returns the stateMachine.
    324         */
    325        public SCXML getStateMachine() {
    326            return stateMachine;
    327        }
    328
    329        /**
    330         * Set the state machine to be executed.
    331         * <b>NOTE:</b> Should only be used before the executor is set in motion.
    332         *
    333         * @param stateMachine The stateMachine to set.
    334         */
    335        public void setStateMachine(final SCXML stateMachine) {
    336            // NormalizeStateMachine
    337            SCXML sm = semantics.normalizeStateMachine(stateMachine,
    338                    errorReporter);
    339            // StoreStateMachine
    340            this.stateMachine = sm;
    341        }
    342
    343        /**
    344         * Initiate state machine execution.
    345         *
    346         * @throws ModelException in case there is a fatal SCXML object
    347         *  model problem.
    348         */
    349        public void go() throws ModelException {
    350            // same as reset
    351            this.reset();
    352        }
    353
    354        /**
    355         * Get the environment specific error reporter.
    356         *
    357         * @return Returns the errorReporter.
    358         */
    359        public ErrorReporter getErrorReporter() {
    360            return errorReporter;
    361        }
    362
    363        /**
    364         * Set the environment specific error reporter.
    365         *
    366         * @param errorReporter The errorReporter to set.
    367         */
    368        public void setErrorReporter(final ErrorReporter errorReporter) {
    369            this.errorReporter = errorReporter;
    370        }
    371
    372        /**
    373         * Get the event dispatcher.
    374         *
    375         * @return Returns the eventdispatcher.
    376         */
    377        public EventDispatcher getEventdispatcher() {
    378            return eventdispatcher;
    379        }
    380
    381        /**
    382         * Set the event dispatcher.
    383         *
    384         * @param eventdispatcher The eventdispatcher to set.
    385         */
    386        public void setEventdispatcher(final EventDispatcher eventdispatcher) {
    387            this.eventdispatcher = eventdispatcher;
    388        }
    389
    390        /**
    391         * Use "super-step", default is <code>true</code>
    392         * (that is, run-to-completion is default).
    393         *
    394         * @return Returns the superStep property.
    395         * @see #setSuperStep(boolean)
    396         */
    397        public boolean isSuperStep() {
    398            return superStep;
    399        }
    400
    401        /**
    402         * Set the super step.
    403         *
    404         * @param superStep
    405         * if true, the internal derived events are also processed
    406         *    (run-to-completion);
    407         * if false, the internal derived events are stored in the
    408         * CurrentStatus property and processed within the next
    409         * triggerEvents() invocation, also the immediate (empty event) transitions
    410         * are deferred until the next step
    411          */
    412        public void setSuperStep(final boolean superStep) {
    413            this.superStep = superStep;
    414        }
    415
    416        /**
    417         * Add a listener to the document root.
    418         *
    419         * @param scxml The document root to attach listener to.
    420         * @param listener The SCXMLListener.
    421         */
    422        public void addListener(final SCXML scxml, final SCXMLListener listener) {
    423            Object observable = scxml;
    424            scInstance.getNotificationRegistry().addListener(observable, listener);
    425        }
    426
    427        /**
    428         * Remove this listener from the document root.
    429         *
    430         * @param scxml The document root.
    431         * @param listener The SCXMLListener to be removed.
    432         */
    433        public void removeListener(final SCXML scxml,
    434                final SCXMLListener listener) {
    435            Object observable = scxml;
    436            scInstance.getNotificationRegistry().removeListener(observable,
    437                listener);
    438        }
    439
    440        /**
    441         * Add a listener to this transition target.
    442         *
    443         * @param transitionTarget The <code>TransitionTarget</code> to
    444         *                         attach listener to.
    445         * @param listener The SCXMLListener.
    446         */
    447        public void addListener(final TransitionTarget transitionTarget,
    448                final SCXMLListener listener) {
    449            Object observable = transitionTarget;
    450            scInstance.getNotificationRegistry().addListener(observable, listener);
    451        }
    452
    453        /**
    454         * Remove this listener for this transition target.
    455         *
    456         * @param transitionTarget The <code>TransitionTarget</code>.
    457         * @param listener The SCXMLListener to be removed.
    458         */
    459        public void removeListener(final TransitionTarget transitionTarget,
    460                final SCXMLListener listener) {
    461            Object observable = transitionTarget;
    462            scInstance.getNotificationRegistry().removeListener(observable,
    463                listener);
    464        }
    465
    466        /**
    467         * Add a listener to this transition.
    468         *
    469         * @param transition The <code>Transition</code> to attach listener to.
    470         * @param listener The SCXMLListener.
    471         */
    472        public void addListener(final Transition transition,
    473                final SCXMLListener listener) {
    474            Object observable = transition;
    475            scInstance.getNotificationRegistry().addListener(observable, listener);
    476        }
    477
    478        /**
    479         * Remove this listener for this transition.
    480         *
    481         * @param transition The <code>Transition</code>.
    482         * @param listener The SCXMLListener to be removed.
    483         */
    484        public void removeListener(final Transition transition,
    485                final SCXMLListener listener) {
    486            Object observable = transition;
    487            scInstance.getNotificationRegistry().removeListener(observable,
    488                listener);
    489        }
    490
    491        /**
    492         * Register an <code>Invoker</code> for this target type.
    493         *
    494         * @param targettype The target type (specified by "targettype"
    495         *                   attribute of <invoke> tag).
    496         * @param invokerClass The <code>Invoker</code> <code>Class</code>.
    497         */
    498        public void registerInvokerClass(final String targettype,
    499                final Class invokerClass) {
    500            scInstance.registerInvokerClass(targettype, invokerClass);
    501        }
    502
    503        /**
    504         * Remove the <code>Invoker</code> registered for this target
    505         * type (if there is one registered).
    506         *
    507         * @param targettype The target type (specified by "targettype"
    508         *                   attribute of <invoke> tag).
    509         */
    510        public void unregisterInvokerClass(final String targettype) {
    511            scInstance.unregisterInvokerClass(targettype);
    512        }
    513
    514        /**
    515         * Get the state chart instance for this executor.
    516         *
    517         * @return The SCInstance for this executor.
    518         */
    519        SCInstance getSCInstance() {
    520            return scInstance;
    521        }
    522
    523        /**
    524         * Log the current set of active states.
    525         */
    526        private void logState() {
    527            if (log.isDebugEnabled()) {
    528                Iterator si = currentStatus.getStates().iterator();
    529                StringBuffer sb = new StringBuffer("Current States: [");
    530                while (si.hasNext()) {
    531                    State s = (State) si.next();
    532                    sb.append(s.getId());
    533                    if (si.hasNext()) {
    534                        sb.append(", ");
    535                    }
    536                }
    537                sb.append(']');
    538                log.debug(sb.toString());
    539            }
    540        }
    541
    542        /**
    543         * @param step The most recent Step
    544         */
    545        private void updateStatus(final Step step) {
    546            currentStatus = step.getAfterStatus();
    547            scInstance.getRootContext().setLocal("_ALL_STATES",
    548                SCXMLHelper.getAncestorClosure(currentStatus.getStates(), null));
    549            setEventData((TriggerEvent[]) currentStatus.getEvents().
    550                toArray(new TriggerEvent[0]));
    551        }
    552
    553        /**
    554         * @param evts The events being triggered.
    555         * @return Object[] Previous values.
    556         */
    557        private Object[] setEventData(final TriggerEvent[] evts) {
    558            Context rootCtx = scInstance.getRootContext();
    559            Object[] oldData = {rootCtx.get(EVENT_DATA),
    560                rootCtx.get(EVENT_DATA_MAP)};
    561            int len = evts.length;
    562            if (len > 0) { // 0 has retry semantics (eg: see usage in reset())
    563                Object eventData = null;
    564                Map payloadMap = new HashMap();
    565                for (int i = 0; i < len; i++) {
    566                    TriggerEvent te = evts[i];
    567                    payloadMap.put(te.getName(), te.getPayload());
    568                }
    569                if (len == 1) {
    570                    // we have only one event
    571                    eventData = evts[0].getPayload();
    572                }
    573                rootCtx.setLocal(EVENT_DATA, eventData);
    574                rootCtx.setLocal(EVENT_DATA_MAP, payloadMap);
    575            }
    576            return oldData;
    577        }
    578
    579        /**
    580         * @param oldData The old values to restore to.
    581         */
    582        private void restoreEventData(final Object[] oldData) {
    583            scInstance.getRootContext().setLocal(EVENT_DATA, oldData[0]);
    584            scInstance.getRootContext().setLocal(EVENT_DATA_MAP, oldData[1]);
    585        }
    586
    587        /**
    588         * The special variable for storing single event data / payload.
    589         */
    590        private static final String EVENT_DATA = "_eventdata";
    591
    592        /**
    593         * The special variable for storing event data / payload,
    594         * when multiple events are triggered, keyed by event name.
    595         */
    596        private static final String EVENT_DATA_MAP = "_eventdatamap";
    597
    598        /**
    599         * SCXMLExecutor put into motion without setting a model (state machine).
    600         */
    601        private static final String ERR_NO_STATE_MACHINE =
    602            "SCXMLExecutor: State machine not set";
    603
    604    }
    605