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.ArrayList;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.LinkedList;
023import java.util.Map;
024import java.util.Queue;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028import org.apache.commons.scxml2.env.SimpleDispatcher;
029import org.apache.commons.scxml2.env.SimpleErrorReporter;
030import org.apache.commons.scxml2.invoke.Invoker;
031import org.apache.commons.scxml2.invoke.InvokerException;
032import org.apache.commons.scxml2.invoke.SimpleSCXMLInvoker;
033import org.apache.commons.scxml2.model.Invoke;
034import org.apache.commons.scxml2.model.ModelException;
035import org.apache.commons.scxml2.model.SCXML;
036
037/**
038 * SCXMLExecutionContext provides all the services and internal data used during the interpretation of an SCXML
039 * statemachine across micro and macro steps
040 */
041public class SCXMLExecutionContext implements SCXMLIOProcessor {
042
043    /**
044     * Default and required supported SCXML Processor Invoker service URI
045     */
046    public static final String SCXML_INVOKER_TYPE_URI = "http://www.w3.org/TR/scxml/";
047    /**
048     * Alias for {@link #SCXML_INVOKER_TYPE_URI}
049     */
050    public static final String SCXML_INVOKER_TYPE = "scxml";
051
052    /**
053     * SCXML Execution Logger for the application.
054     */
055    private Log appLog = LogFactory.getLog(SCXMLExecutionContext.class);
056
057    /**
058     * The action execution context instance, providing restricted access to this execution context
059     */
060    private final ActionExecutionContext actionExecutionContext;
061
062    /**
063     * The SCXMLExecutor of this SCXMLExecutionContext
064     */
065    private final SCXMLExecutor scxmlExecutor;
066
067    /**
068     * The SCInstance.
069     */
070    private SCInstance scInstance;
071
072    /**
073     * The evaluator for expressions.
074     */
075    private Evaluator evaluator;
076
077    /**
078     * The external IOProcessor for Invokers to communicate back on
079     */
080    private SCXMLIOProcessor externalIOProcessor;
081
082    /**
083     * The event dispatcher to interface with external documents etc.
084     */
085    private EventDispatcher eventdispatcher;
086
087    /**
088     * The environment specific error reporter.
089     */
090    private ErrorReporter errorReporter = null;
091
092    /**
093     * The notification registry.
094     */
095    private NotificationRegistry notificationRegistry;
096
097    /**
098     * The internal event queue
099     */
100    private final Queue<TriggerEvent> internalEventQueue = new LinkedList<TriggerEvent>();
101
102    /**
103     * The Invoker classes map, keyed by invoke target types (specified using "type" attribute).
104     */
105    private final Map<String, Class<? extends Invoker>> invokerClasses = new HashMap<String, Class<? extends Invoker>>();
106
107    /**
108     * The map storing the unique invokeId for an Invoke with an active Invoker
109     */
110    private final Map<Invoke, String> invokeIds = new HashMap<Invoke, String>();
111
112    /**
113     * The Map of active Invoker, keyed by their unique invokeId.
114     */
115    private final Map<String, Invoker> invokers = new HashMap<String, Invoker>();
116
117    /**
118     * The Map of the current ioProcessors
119     */
120    private final Map<String, SCXMLIOProcessor> ioProcessors = new HashMap<String, SCXMLIOProcessor>();
121
122    /**
123     * Flag indicating if the SCXML configuration should be checked before execution (default = true)
124     */
125    private boolean checkLegalConfiguration = true;
126
127    /**
128     * Local cache of the SCInstance sessionId, to be able to check against clear/reinitialization
129     */
130    private String sessionId;
131
132    /**
133     * Constructor
134     *
135     * @param scxmlExecutor The SCXMLExecutor of this SCXMLExecutionContext
136     * @param evaluator The evaluator
137     * @param eventDispatcher The event dispatcher, if null a SimpleDispatcher instance will be used
138     * @param errorReporter The error reporter, if null a SimpleErrorReporter instance will be used
139     */
140    protected SCXMLExecutionContext(SCXMLExecutor scxmlExecutor, Evaluator evaluator,
141                                    EventDispatcher eventDispatcher, ErrorReporter errorReporter) {
142        this.scxmlExecutor = scxmlExecutor;
143        this.externalIOProcessor = scxmlExecutor;
144        this.evaluator = evaluator;
145        this.eventdispatcher = eventDispatcher != null ? eventDispatcher : new SimpleDispatcher();
146        this.errorReporter = errorReporter != null ? errorReporter : new SimpleErrorReporter();
147        this.notificationRegistry = new NotificationRegistry();
148
149        this.scInstance = new SCInstance(this, this.evaluator, this.errorReporter);
150        this.actionExecutionContext = new ActionExecutionContext(this);
151
152        ioProcessors.put(SCXMLIOProcessor.DEFAULT_EVENT_PROCESSOR, getExternalIOProcessor());
153        ioProcessors.put(SCXMLIOProcessor.SCXML_EVENT_PROCESSOR, getExternalIOProcessor());
154        ioProcessors.put(SCXMLIOProcessor.INTERNAL_EVENT_PROCESSOR, getInternalIOProcessor());
155        if (scxmlExecutor.getParentSCXMLExecutor() != null) {
156            ioProcessors.put(SCXMLIOProcessor.PARENT_EVENT_PROCESSOR, scxmlExecutor.getParentSCXMLExecutor());
157        }
158        initializeIOProcessors();
159        registerInvokerClass(SCXML_INVOKER_TYPE_URI, SimpleSCXMLInvoker.class);
160        registerInvokerClass(SCXML_INVOKER_TYPE, SimpleSCXMLInvoker.class);
161    }
162
163    public SCXMLExecutor getSCXMLExecutor() {
164        return scxmlExecutor;
165    }
166
167    public SCXMLIOProcessor getExternalIOProcessor() {
168        return externalIOProcessor;
169    }
170
171    public SCXMLIOProcessor getInternalIOProcessor() {
172        return this;
173    }
174
175    /**
176     * @return Returns the restricted execution context for actions
177     */
178    public ActionExecutionContext getActionExecutionContext() {
179        return actionExecutionContext;
180    }
181
182    /**
183     * @return Returns true if this state machine is running
184     */
185    public boolean isRunning() {
186        return scInstance.isRunning();
187    }
188
189    /**
190     * Stop a running state machine
191     */
192    public void stopRunning() {
193        scInstance.setRunning(false);
194    }
195
196    /**
197     * Set if the SCXML configuration should be checked before execution (default = true)
198     * @param checkLegalConfiguration flag to set
199     */
200    public void setCheckLegalConfiguration(boolean checkLegalConfiguration) {
201        this.checkLegalConfiguration = checkLegalConfiguration;
202    }
203
204    /**
205     * @return if the SCXML configuration will be checked before execution
206     */
207    public boolean isCheckLegalConfiguration() {
208        return checkLegalConfiguration;
209    }
210
211    /**
212     * Initialize method which will cancel all current active Invokers, clear the internal event queue and mark the
213     * state machine process as running (again).
214     *
215     * @throws ModelException if the state machine instance failed to initialize.
216     */
217    public void initialize() throws ModelException {
218        if (!invokeIds.isEmpty()) {
219            for (Invoke invoke : new ArrayList<Invoke>(invokeIds.keySet())) {
220                cancelInvoker(invoke);
221            }
222        }
223        internalEventQueue.clear();
224        scInstance.initialize();
225        initializeIOProcessors();
226        scInstance.setRunning(true);
227    }
228
229    /**
230     * @return Returns the SCXML Execution Logger for the application
231     */
232    public Log getAppLog() {
233        return appLog;
234    }
235
236    /**
237     * @return Returns the state machine
238     */
239    public SCXML getStateMachine() {
240        return scInstance.getStateMachine();
241    }
242
243    /**
244     * Set or replace the state machine to be executed
245     * <p>
246     * If the state machine instance has been initialized before, it will be initialized again, destroying all existing
247     * state!
248     * </p>
249     * @param stateMachine The state machine to set
250     * @throws ModelException if attempting to set a null value or the state machine instance failed to re-initialize
251     */
252    protected void setStateMachine(SCXML stateMachine) throws ModelException {
253        scInstance.setStateMachine(stateMachine);
254        // synchronize possible derived evaluator
255        this.evaluator = scInstance.getEvaluator();
256        initializeIOProcessors();
257    }
258
259    /**
260     * The SCXML specification section "C.1.1 _ioprocessors Value" states that the SCXMLEventProcessor <em>must</em>
261     * maintain a 'location' field inside its entry in the _ioprocessors environment variable.
262     * @return the 'location' of the SCXMLEventProcessor
263     */
264    public String getLocation() {
265        return null;
266    }
267
268    /**
269     * @return Returns the SCInstance
270     */
271    public SCInstance getScInstance() {
272        return scInstance;
273    }
274
275    /**
276     * @return Returns The evaluator.
277     */
278    public Evaluator getEvaluator() {
279        return evaluator;
280    }
281
282    /**
283     * Set or replace the evaluator
284     * <p>
285     * If the state machine instance has been initialized before, it will be initialized again, destroying all existing
286     * state!
287     * </p>
288     * @param evaluator The evaluator to set
289     * @throws ModelException if attempting to set a null value or the state machine instance failed to re-initialize
290     */
291    protected void setEvaluator(Evaluator evaluator) throws ModelException {
292        scInstance.setEvaluator(evaluator, false);
293        // synchronize possible derived evaluator
294        this.evaluator = scInstance.getEvaluator();
295        initializeIOProcessors();
296    }
297
298    /**
299     * @return Returns the error reporter
300     */
301    public ErrorReporter getErrorReporter() {
302        return errorReporter;
303    }
304
305    /**
306     * Set or replace the error reporter
307     *
308     * @param errorReporter The error reporter to set, if null a SimpleErrorReporter instance will be used instead
309     */
310    protected void setErrorReporter(ErrorReporter errorReporter) {
311        this.errorReporter = errorReporter != null ? errorReporter : new SimpleErrorReporter();
312        try {
313            scInstance.setErrorReporter(errorReporter);
314        }
315        catch (ModelException me) {
316            // won't happen
317        }
318    }
319
320    /**
321     * @return Returns the event dispatcher
322     */
323    public EventDispatcher getEventDispatcher() {
324        return eventdispatcher;
325    }
326
327    /**
328     * Set or replace the event dispatch
329     *
330     * @param eventdispatcher The event dispatcher to set, if null a SimpleDispatcher instance will be used instead
331     */
332    protected void setEventdispatcher(EventDispatcher eventdispatcher) {
333        this.eventdispatcher = eventdispatcher != null ? eventdispatcher : new SimpleDispatcher();
334    }
335
336    /**
337     * @return Returns the notification registry
338     */
339    public NotificationRegistry getNotificationRegistry() {
340        return notificationRegistry;
341    }
342
343    /**
344     * Initialize the _ioprocessors environment variable, which only can be done when the evaluator is available
345     */
346    protected void initializeIOProcessors() {
347        if (scInstance.getEvaluator() != null) {
348            // lazy register/reset #_scxml_sessionId event target
349            String currentSessionId = (String)getScInstance().getSystemContext().get(SCXMLSystemContext.SESSIONID_KEY);
350            if (sessionId != null && !sessionId.equals(currentSessionId)) {
351                // remove possible old/stale #_scxml_sessionId target
352                ioProcessors.remove(SCXMLIOProcessor.SCXML_SESSION_EVENT_PROCESSOR_PREFIX+sessionId);
353            }
354            sessionId = currentSessionId;
355            if (!ioProcessors.containsKey(SCXMLIOProcessor.SCXML_SESSION_EVENT_PROCESSOR_PREFIX+sessionId)) {
356                ioProcessors.put(SCXMLIOProcessor.SCXML_SESSION_EVENT_PROCESSOR_PREFIX+sessionId, getExternalIOProcessor());
357            }
358            getScInstance().getSystemContext().setLocal(SCXMLSystemContext.IOPROCESSORS_KEY, Collections.unmodifiableMap(ioProcessors));
359        }
360    }
361
362    /**
363     * Detach the current SCInstance to allow external serialization.
364     * <p>
365     * {@link #attachInstance(SCInstance)} can be used to re-attach a previously detached instance
366     * </p>
367     * @return the detached instance
368     */
369    protected SCInstance detachInstance() {
370        SCInstance instance = scInstance;
371        scInstance.detach();
372        Map<String, Object> systemVars = scInstance.getSystemContext().getVars();
373        systemVars.remove(SCXMLSystemContext.IOPROCESSORS_KEY);
374        systemVars.remove(SCXMLSystemContext.EVENT_KEY);
375        scInstance = null;
376        return instance;
377    }
378
379    /**
380     * Re-attach a previously detached SCInstance.
381     * <p>
382     * Note: an already attached instance will get overwritten (and thus lost).
383     * </p>
384     * @param instance An previously detached SCInstance
385     */
386    protected void attachInstance(SCInstance instance) {
387        if (scInstance != null ) {
388            scInstance.detach();
389        }
390        scInstance = instance;
391        if (scInstance != null) {
392            scInstance.detach();
393            try {
394                scInstance.setInternalIOProcessor(this);
395                scInstance.setEvaluator(evaluator, true);
396                scInstance.setErrorReporter(errorReporter);
397                initializeIOProcessors();
398            }
399            catch (ModelException me) {
400                // should not happen
401            }
402        }
403    }
404
405    /**
406     * Register an Invoker for this target type.
407     *
408     * @param type The target type (specified by "type" attribute of the invoke element).
409     * @param invokerClass The Invoker class.
410     */
411    protected void registerInvokerClass(final String type, final Class<? extends Invoker> invokerClass) {
412        invokerClasses.put(type, invokerClass);
413    }
414
415    /**
416     * Remove the Invoker registered for this target type (if there is one registered).
417     *
418     * @param type The target type (specified by "type" attribute of the invoke element).
419     */
420    protected void unregisterInvokerClass(final String type) {
421        invokerClasses.remove(type);
422    }
423
424    /**
425     * Create a new {@link Invoker}
426     *
427     * @param type The type of the target being invoked.
428     * @return An {@link Invoker} for the specified type, if an
429     *         invoker class is registered against that type,
430     *         <code>null</code> otherwise.
431     * @throws InvokerException When a suitable {@link Invoker} cannot be instantiated.
432     */
433    public Invoker newInvoker(final String type) throws InvokerException {
434        Class<? extends Invoker> invokerClass = invokerClasses.get(type);
435        if (invokerClass == null) {
436            throw new InvokerException("No Invoker registered for type \"" + type + "\"");
437        }
438        try {
439            return invokerClass.newInstance();
440        } catch (InstantiationException ie) {
441            throw new InvokerException(ie.getMessage(), ie.getCause());
442        } catch (IllegalAccessException iae) {
443            throw new InvokerException(iae.getMessage(), iae.getCause());
444        }
445    }
446
447    /**
448     * Get the {@link Invoker} for this {@link Invoke}.
449     * May return <code>null</code>. A non-null {@link Invoker} will be
450     * returned if and only if the {@link Invoke} parent TransitionalState is
451     * currently active and contains the &lt;invoke&gt; child.
452     *
453     * @param invoke The <code>Invoke</code>.
454     * @return The Invoker.
455     */
456    public Invoker getInvoker(final Invoke invoke) {
457        return invokers.get(invokeIds.get(invoke));
458    }
459
460    /**
461     * Register the active {@link Invoker} for a {@link Invoke}
462     *
463     * @param invoke The Invoke.
464     * @param invoker The Invoker.
465     * @throws InvokerException when the Invoker doesn't have an invokerId
466     */
467    public void registerInvoker(final Invoke invoke, final Invoker invoker) throws InvokerException {
468        String invokeId = invoker.getInvokeId();
469        if (invokeId == null) {
470            throw new InvokerException("Registering an Invoker without invokerId");
471        }
472        invokeIds.put(invoke, invokeId);
473        invokers.put(invokeId, invoker);
474        ioProcessors.put(SCXMLIOProcessor.EVENT_PROCESSOR_ALIAS_PREFIX+invoke.getId(), invoker.getChildIOProcessor());
475        initializeIOProcessors();
476    }
477
478    /**
479     * Remove a previously active Invoker, which must already have been canceled
480     * @param invoke The Invoke for the Invoker to remove
481     */
482    public void removeInvoker(final Invoke invoke) {
483        invokers.remove(invokeIds.remove(invoke));
484        ioProcessors.remove(SCXMLIOProcessor.EVENT_PROCESSOR_ALIAS_PREFIX+invoke.getId());
485        initializeIOProcessors();
486    }
487
488    /**
489     * @return Returns the map of current active Invokes and their invokeId
490     */
491    public Map<Invoke, String> getInvokeIds() {
492        return invokeIds;
493    }
494
495
496    /**
497     * Cancel and remove an active Invoker
498     *
499     * @param invoke The Invoke for the Invoker to cancel
500     */
501    public void cancelInvoker(Invoke invoke) {
502        String invokeId = invokeIds.get(invoke);
503        if (invokeId != null) {
504            try {
505                invokers.get(invokeId).cancel();
506            } catch (InvokerException ie) {
507                TriggerEvent te = new TriggerEvent("failed.invoke.cancel."+invokeId, TriggerEvent.ERROR_EVENT);
508                addEvent(te);
509            }
510            removeInvoker(invoke);
511        }
512    }
513
514    /**
515     * Add an event to the internal event queue
516     * @param event The event
517     */
518    @Override
519    public void addEvent(TriggerEvent event) {
520        internalEventQueue.add(event);
521    }
522
523    /**
524     * @return Returns the next event from the internal event queue, if available
525     */
526    public TriggerEvent nextInternalEvent() {
527        return internalEventQueue.poll();
528    }
529
530    /**
531     * @return Returns true if the internal event queue isn't empty
532     */
533    public boolean hasPendingInternalEvent() {
534        return !internalEventQueue.isEmpty();
535    }
536}