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 */
017package org.apache.commons.scxml2;
018
019import java.util.HashSet;
020import java.util.Queue;
021import java.util.Set;
022import java.util.concurrent.ConcurrentLinkedQueue;
023
024import org.apache.commons.logging.Log;
025import org.apache.commons.logging.LogFactory;
026import org.apache.commons.scxml2.invoke.Invoker;
027import org.apache.commons.scxml2.model.EnterableState;
028import org.apache.commons.scxml2.model.ModelException;
029import org.apache.commons.scxml2.model.Observable;
030import org.apache.commons.scxml2.model.SCXML;
031import org.apache.commons.scxml2.model.TransitionTarget;
032import org.apache.commons.scxml2.semantics.SCXMLSemanticsImpl;
033
034/**
035 * <p>The SCXML &quot;engine&quot; that executes SCXML documents. The
036 * particular semantics used by this engine for executing the SCXML are
037 * encapsulated in the SCXMLSemantics implementation that it uses.</p>
038 *
039 * <p>The default implementation is
040 * <code>org.apache.commons.scxml2.semantics.SCXMLSemanticsImpl</code></p>
041 *
042 * <p>The executor uses SCXMLExecutionContext to manage the state and
043 * provide all the services to the SCXMLSemantics implementation.</p>
044 *
045 * @see SCXMLSemantics
046 */
047public class SCXMLExecutor implements SCXMLIOProcessor {
048
049    /**
050     * The Logger for the SCXMLExecutor.
051     */
052    private Log log = LogFactory.getLog(SCXMLExecutor.class);
053
054    /**
055     * Parent SCXMLExecutor
056     */
057    private SCXMLExecutor parentSCXMLExecutor;
058
059    /**
060     *  Interpretation semantics.
061     */
062    private SCXMLSemantics semantics;
063
064    /**
065     * The state machine execution context
066     */
067    private SCXMLExecutionContext exctx;
068
069    /**
070     * The external event queue
071     */
072    private final Queue<TriggerEvent> externalEventQueue = new ConcurrentLinkedQueue<TriggerEvent>();
073
074    /**
075     * Convenience constructor.
076     */
077    public SCXMLExecutor() {
078        this(null, null, null, null);
079    }
080
081    /**
082     * Constructor.
083     *
084     * @param expEvaluator The expression evaluator
085     * @param evtDisp The event dispatcher
086     * @param errRep The error reporter
087     */
088    public SCXMLExecutor(final Evaluator expEvaluator,
089                         final EventDispatcher evtDisp, final ErrorReporter errRep) {
090        this(expEvaluator, evtDisp, errRep, null);
091    }
092
093    /**
094     * Constructor.
095     *
096     * @param expEvaluator The expression evaluator
097     * @param evtDisp The event dispatcher
098     * @param errRep The error reporter
099     * @param semantics The SCXML semantics
100     */
101    public SCXMLExecutor(final Evaluator expEvaluator,
102                         final EventDispatcher evtDisp, final ErrorReporter errRep,
103                         final SCXMLSemantics semantics) {
104        this.semantics = semantics != null ? semantics : new SCXMLSemanticsImpl();
105        this.exctx = new SCXMLExecutionContext(this, expEvaluator, evtDisp, errRep);
106    }
107
108    /**
109     * Constructor using a parent SCXMLExecutor
110     *
111     * @param parentSCXMLExecutor the parent SCXMLExecutor
112     */
113    public SCXMLExecutor(final SCXMLExecutor parentSCXMLExecutor) {
114        this.parentSCXMLExecutor = parentSCXMLExecutor;
115        this.semantics = parentSCXMLExecutor.semantics;
116        this.exctx = new SCXMLExecutionContext(this, parentSCXMLExecutor.getEvaluator(),
117                parentSCXMLExecutor.getEventdispatcher(), parentSCXMLExecutor.getErrorReporter());
118    }
119
120    /**
121     * @return the parent SCXMLExecutor (if any)
122     */
123    protected SCXMLExecutor getParentSCXMLExecutor() {
124        return parentSCXMLExecutor;
125    }
126
127    /**
128     * Get the current state machine instance status.
129     *
130     * @return The current Status
131     */
132    public synchronized Status getStatus() {
133        return exctx.getScInstance().getCurrentStatus();
134    }
135
136    /**
137     * Initializes the state machine with a specific active configuration
138     * <p>
139     * This will first (re)initialize the current state machine: clearing all variable contexts, histories and current
140     * status, and clones the SCXML root datamodel into the root context.
141     * </p>
142     * @param atomicStateIds The set of atomic state ids for the state machine
143     * @throws ModelException when the state machine hasn't been properly configured yet, when an unknown or illegal
144     * stateId is specified, or when the specified active configuration does not represent a legal configuration.
145     * @see {@link SCInstance#initialize()}
146     * @see {@link SCXMLSemantics#isLegalConfiguration(java.util.Set, ErrorReporter)}
147     */
148    public synchronized void setConfiguration(Set<String> atomicStateIds) throws ModelException {
149        exctx.initialize();
150        Set<EnterableState> states = new HashSet<EnterableState>();
151        for (String stateId : atomicStateIds) {
152            TransitionTarget tt = getStateMachine().getTargets().get(stateId);
153            if (tt instanceof EnterableState && ((EnterableState)tt).isAtomicState()) {
154                EnterableState es = (EnterableState)tt;
155                while (es != null && !states.add(es)) {
156                    es = es.getParent();
157                }
158            }
159            else {
160                throw new ModelException("Illegal atomic stateId "+stateId+": state unknown or not an atomic state");
161            }
162        }
163        if (semantics.isLegalConfiguration(states, getErrorReporter())) {
164            for (EnterableState es : states) {
165                exctx.getScInstance().getStateConfiguration().enterState(es);
166            }
167            logState();
168        }
169        else {
170            throw new ModelException("Illegal state machine configuration!");
171        }
172    }
173
174    /**
175     * Set or replace the expression evaluator
176     * <p>
177     * If the state machine instance has been initialized before, it will be initialized again, destroying all existing
178     * state!
179     * </p>
180     * <p>
181     * Also the external event queue will be cleared.
182     * </p>
183     * @param evaluator The evaluator to set
184     * @throws ModelException if attempting to set a null value or the state machine instance failed to re-initialize
185     */
186    public void setEvaluator(final Evaluator evaluator) throws ModelException {
187        exctx.setEvaluator(evaluator);
188    }
189
190    /**
191     * Get the expression evaluator in use.
192     *
193     * @return Evaluator The evaluator in use.
194     */
195    public Evaluator getEvaluator() {
196        return exctx.getEvaluator();
197    }
198
199    /**
200     * Get the root context for the state machine execution.
201     * <p>
202     * The root context can be used for providing external data to the state machine
203     * </p>
204     *
205     * @return Context The root context.
206     */
207    public Context getRootContext() {
208        return exctx.getScInstance().getRootContext();
209    }
210
211    /**
212     * Get the global context for the state machine execution.
213     * <p>
214     * The global context is the top level context within the state machine itself and should be regarded and treated
215     * "read-only" from external usage.
216     * </p>
217     * @return Context The global context.
218     */
219    public Context getGlobalContext() {
220        return exctx.getScInstance().getGlobalContext();
221    }
222
223    /**
224     * Set the root context for the state machine execution.
225     * <b>NOTE:</b> Should only be used before the executor is set in motion.
226     *
227     * @param rootContext The Context that ties to the host environment.
228     */
229    public void setRootContext(final Context rootContext) {
230        exctx.getScInstance().setRootContext(rootContext);
231    }
232
233    public void setSingleContext(boolean singleContext) throws ModelException {
234        getSCInstance().setSingleContext(singleContext);
235    }
236
237    public boolean isSingleContext() {
238        return getSCInstance().isSingleContext();
239    }
240
241    /**
242     * Get the state machine that is being executed.
243     * <b>NOTE:</b> This is the state machine definition or model used by this
244     * executor instance. It may be shared across multiple executor instances
245     * and should not be altered once in use. Also note that
246     * manipulation of instance data for the executor should happen through
247     * its root context or state contexts only, never through the direct
248     * manipulation of any {@link org.apache.commons.scxml2.model.Datamodel}s associated with this state
249     * machine definition.
250     *
251     * @return Returns the stateMachine.
252     */
253    public SCXML getStateMachine() {
254        return exctx.getStateMachine();
255    }
256
257    /**
258     * Set or replace the state machine to be executed
259     * <p>
260     * If the state machine instance has been initialized before, it will be initialized again, destroying all existing
261     * state!
262     * </p>
263     * <p>
264     * Also the external event queue will be cleared.
265     * </p>
266     * @param stateMachine The state machine to set
267     * @throws ModelException if attempting to set a null value or the state machine instance failed to re-initialize
268     */
269    public void setStateMachine(final SCXML stateMachine) throws ModelException {
270        exctx.setStateMachine(semantics.normalizeStateMachine(stateMachine, exctx.getErrorReporter()));
271        externalEventQueue.clear();
272    }
273
274    /**
275     * Get the environment specific error reporter.
276     *
277     * @return Returns the errorReporter.
278     */
279    public ErrorReporter getErrorReporter() {
280        return exctx.getErrorReporter();
281    }
282
283    /**
284     * Set or replace the error reporter
285     *
286     * @param errorReporter The error reporter to set, if null a SimpleErrorReporter instance will be used instead
287     */
288    public void setErrorReporter(final ErrorReporter errorReporter) {
289        exctx.setErrorReporter(errorReporter);
290    }
291
292    /**
293     * Get the event dispatcher.
294     *
295     * @return Returns the eventdispatcher.
296     */
297    public EventDispatcher getEventdispatcher() {
298        return exctx.getEventDispatcher();
299    }
300
301    /**
302     * Set or replace the event dispatch
303     *
304     * @param eventdispatcher The event dispatcher to set, if null a SimpleDispatcher instance will be used instead
305     */
306    public void setEventdispatcher(final EventDispatcher eventdispatcher) {
307        exctx.setEventdispatcher(eventdispatcher);
308    }
309
310    /**
311     * Set if the SCXML configuration should be checked before execution (default = true)
312     * @param checkLegalConfiguration flag to set
313     */
314    public void setCheckLegalConfiguration(boolean checkLegalConfiguration) {
315        this.exctx.setCheckLegalConfiguration(checkLegalConfiguration);
316    }
317
318    /**
319     * @return if the SCXML configuration will be checked before execution
320     */
321    public boolean isCheckLegalConfiguration() {
322        return exctx.isCheckLegalConfiguration();
323    }
324
325    /**
326     * Get the notification registry.
327     *
328     * @return The notification registry.
329     */
330    public NotificationRegistry getNotificationRegistry() {
331        return exctx.getNotificationRegistry();
332    }
333
334    /**
335     * Add a listener to the {@link Observable}.
336     *
337     * @param observable The {@link Observable} to attach the listener to.
338     * @param listener The SCXMLListener.
339     */
340    public void addListener(final Observable observable, final SCXMLListener listener) {
341        exctx.getNotificationRegistry().addListener(observable, listener);
342    }
343
344    /**
345     * Remove this listener from the {@link Observable}.
346     *
347     * @param observable The {@link Observable}.
348     * @param listener The SCXMLListener to be removed.
349     */
350    public void removeListener(final Observable observable,
351                               final SCXMLListener listener) {
352        exctx.getNotificationRegistry().removeListener(observable, listener);
353    }
354
355    /**
356     * Register an Invoker for this target type.
357     *
358     * @param type The target type (specified by "type" attribute of the invoke element).
359     * @param invokerClass The Invoker class.
360     */
361    public void registerInvokerClass(final String type, final Class<? extends Invoker> invokerClass) {
362        exctx.registerInvokerClass(type, invokerClass);
363    }
364
365    /**
366     * Remove the Invoker registered for this target type (if there is one registered).
367     *
368     * @param type The target type (specified by "type" attribute of the invoke element).
369     */
370    public void unregisterInvokerClass(final String type) {
371        exctx.unregisterInvokerClass(type);
372    }
373
374    /**
375     * Detach the current SCInstance to allow external serialization.
376     * <p>
377     * {@link #attachInstance(SCInstance)} can be used to re-attach a previously detached instance
378     * </p>
379     * <p>
380     * Note: until an instance is re-attached, no operations are allowed (and probably throw exceptions) except
381     * for {@link #addEvent(TriggerEvent)} which might still be used (concurrently) by running Invokers, or
382     * {@link #hasPendingEvents()} to check for possible pending events.
383     * </p>
384     * @return the detached instance
385     */
386    public SCInstance detachInstance() {
387        return exctx.detachInstance();
388    }
389
390    /**
391     * Re-attach a previously detached SCInstance.
392     * <p>
393     * Note: an already attached instance will get overwritten (and thus lost).
394     * </p>
395     * @param instance An previously detached SCInstance
396     */
397    public void attachInstance(SCInstance instance) {
398        exctx.attachInstance(instance);
399    }
400
401    /**
402     * @return Returns true if the state machine is running
403     */
404    public boolean isRunning() {
405        return exctx.isRunning();
406    }
407
408    /**
409     * Initiate state machine execution.
410     *
411     * @throws ModelException in case there is a fatal SCXML object
412     *  model problem.
413     */
414    public void go() throws ModelException {
415        // same as reset
416        this.reset();
417    }
418
419    /**
420     * Clear all state and begin executing the state machine
421     *
422     * @throws ModelException if the state machine instance failed to initialize
423     */
424    public void reset() throws ModelException {
425        // clear any pending external events
426        externalEventQueue.clear();
427
428        // go
429        semantics.firstStep(exctx);
430        logState();
431    }
432
433    /**
434     * Add a new external event, which may be done concurrently, and even when the current SCInstance is detached.
435     * <p>
436     * No processing of the vent will be done, until the next triggerEvent methods is invoked.
437     * </p>
438     * @param evt an external event
439     */
440    public void addEvent(final TriggerEvent evt) {
441        if (evt != null) {
442            externalEventQueue.add(evt);
443        }
444    }
445
446    /**
447     * @return Returns true if there are pending external events to be processed.
448     */
449    public boolean hasPendingEvents() {
450        return !externalEventQueue.isEmpty();
451    }
452
453    /**
454     * @return Returns the current number of pending external events to be processed.
455     */
456    public int getPendingEvents() {
457        return externalEventQueue.size();
458    }
459
460    /**
461     * Convenience method when only one event needs to be triggered.
462     *
463     * @param evt
464     *            the external events which triggered during the last
465     *            time quantum
466     * @throws ModelException in case there is a fatal SCXML object
467     *            model problem.
468     */
469    public void triggerEvent(final TriggerEvent evt)
470            throws ModelException {
471        addEvent(evt);
472        triggerEvents();
473    }
474
475    /**
476     * The worker method.
477     * Re-evaluates current status whenever any events are triggered.
478     *
479     * @param evts
480     *            an array of external events which triggered during the last
481     *            time quantum
482     * @throws ModelException in case there is a fatal SCXML object
483     *            model problem.
484     */
485    public void triggerEvents(final TriggerEvent[] evts)
486            throws ModelException {
487        if (evts != null) {
488            for (TriggerEvent evt : evts) {
489                addEvent(evt);
490            }
491        }
492        triggerEvents();
493    }
494
495    /**
496     * Trigger all pending and incoming events, until there are no more pending events
497     * @throws ModelException in case there is a fatal SCXML object model problem.
498     */
499    public void triggerEvents() throws ModelException {
500        TriggerEvent evt;
501        while (exctx.isRunning() && (evt = externalEventQueue.poll()) != null) {
502            eventStep(evt);
503        }
504    }
505
506    protected void eventStep(TriggerEvent event) throws ModelException {
507        semantics.nextStep(exctx, event);
508        logState();
509    }
510
511    /**
512     * Get the state chart instance for this executor.
513     *
514     * @return The SCInstance for this executor.
515     */
516    protected SCInstance getSCInstance() {
517        return exctx.getScInstance();
518    }
519
520    /**
521     * Log the current set of active states.
522     */
523    protected void logState() {
524        if (log.isDebugEnabled()) {
525            StringBuilder sb = new StringBuilder("Current States: [ ");
526            for (EnterableState es : getStatus().getStates()) {
527                sb.append(es.getId()).append(", ");
528            }
529            int length = sb.length();
530            sb.delete(length - 2, length).append(" ]");
531            log.debug(sb.toString());
532        }
533    }
534}