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.io.Serializable;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026import java.util.UUID;
027
028import javax.xml.parsers.DocumentBuilderFactory;
029import javax.xml.parsers.ParserConfigurationException;
030
031import org.apache.commons.scxml2.env.SimpleContext;
032import org.apache.commons.scxml2.model.Data;
033import org.apache.commons.scxml2.model.Datamodel;
034import org.apache.commons.scxml2.model.EnterableState;
035import org.apache.commons.scxml2.model.History;
036import org.apache.commons.scxml2.model.ModelException;
037import org.apache.commons.scxml2.model.SCXML;
038import org.apache.commons.scxml2.model.TransitionalState;
039import org.apache.commons.scxml2.semantics.ErrorConstants;
040import org.w3c.dom.Document;
041import org.w3c.dom.Element;
042import org.w3c.dom.Node;
043
044/**
045 * The <code>SCInstance</code> performs book-keeping functions for
046 * a particular execution of a state chart represented by a
047 * <code>SCXML</code> object.
048 */
049public class SCInstance implements Serializable {
050
051    /**
052     * Serial version UID.
053     */
054    private static final long serialVersionUID = 2L;
055
056    /**
057     * SCInstance cannot be initialized without setting a state machine.
058     */
059    private static final String ERR_NO_STATE_MACHINE = "SCInstance: State machine not set";
060
061    /**
062     * SCInstance cannot be initialized without setting an error reporter.
063     */
064    private static final String ERR_NO_ERROR_REPORTER = "SCInstance: ErrorReporter not set";
065
066    /**
067     * Flag indicating the state machine instance has been initialized (before).
068     */
069    private boolean initialized;
070
071    /**
072     * The stateMachine being executed.
073     */
074    private SCXML stateMachine;
075
076    /**
077     * The current state configuration of the state machine
078     */
079    private final StateConfiguration stateConfiguration;
080
081    /**
082     * The current status of the stateMachine.
083     */
084    private final Status currentStatus;
085
086    /**
087     * Running status for this state machine
088     */
089    private boolean running;
090
091    /**
092     * The SCXML I/O Processor for the internal event queue
093     */
094    private transient SCXMLIOProcessor internalIOProcessor;
095
096    /**
097     * The Evaluator used for this state machine instance.
098     */
099    private transient Evaluator evaluator;
100
101    /**
102     * The error reporter.
103     */
104    private transient ErrorReporter errorReporter = null;
105
106    /**
107     * The map of contexts per EnterableState.
108     */
109    private final Map<EnterableState, Context> contexts = new HashMap<EnterableState, Context>();
110
111    /**
112     * The map of last known configurations per History.
113     */
114    private final Map<History, Set<EnterableState>> histories = new HashMap<History, Set<EnterableState>>();
115
116    /**
117     * The root context.
118     */
119    private Context rootContext;
120
121    /**
122     * The wrapped system context.
123     */
124    private SCXMLSystemContext systemContext;
125
126    /**
127     * The global context
128     */
129    private Context globalContext;
130
131    /**
132     * Flag indicating if the globalContext is shared between all states (a single flat context, default false)
133     */
134    private boolean singleContext;
135
136    /**
137     * Constructor
138     * @param internalIOProcessor The I/O Processor for the internal event queue
139     * @param evaluator The evaluator
140     * @param errorReporter The error reporter
141     */
142    protected SCInstance(final SCXMLIOProcessor internalIOProcessor, final Evaluator evaluator,
143                         final ErrorReporter errorReporter) {
144        this.internalIOProcessor = internalIOProcessor;
145        this.evaluator = evaluator;
146        this.errorReporter = errorReporter;
147        this.stateConfiguration = new StateConfiguration();
148        this.currentStatus = new Status(stateConfiguration);
149    }
150
151    /**
152     * (re)Initializes the state machine instance, clearing all variable contexts, histories and current status,
153     * and clones the SCXML root datamodel into the root context.
154     * @throws ModelException if the state machine hasn't been setup for this instance
155     */
156    protected void initialize() throws ModelException {
157        running = false;
158        if (stateMachine == null) {
159            throw new ModelException(ERR_NO_STATE_MACHINE);
160        }
161        if (evaluator == null) {
162            evaluator = EvaluatorFactory.getEvaluator(stateMachine);
163        }
164        if (stateMachine.getDatamodelName() != null && !stateMachine.getDatamodelName().equals(evaluator.getSupportedDatamodel())) {
165            throw new ModelException("Incompatible SCXML document datamodel \""+stateMachine.getDatamodelName()+"\""
166                    + " for evaluator "+evaluator.getClass().getName()+" supported datamodel \""+evaluator.getSupportedDatamodel()+"\"");
167        }
168        if (errorReporter == null) {
169            throw new ModelException(ERR_NO_ERROR_REPORTER);
170        }
171        systemContext = null;
172        globalContext = null;
173        contexts.clear();
174        histories.clear();
175        stateConfiguration.clear();
176
177        // Clone root datamodel
178        Datamodel rootdm = stateMachine.getDatamodel();
179        cloneDatamodel(rootdm, getGlobalContext(), evaluator, errorReporter);
180        initialized = true;
181    }
182
183    /**
184     * Detach this state machine instance to allow external serialization.
185     * <p>
186     * This clears the internal I/O processor, evaluator and errorReporter members.
187     * </p>
188     */
189    protected void detach() {
190        this.internalIOProcessor = null;
191        this.evaluator = null;
192        this.errorReporter = null;
193    }
194
195    /**
196     * Sets the I/O Processor for the internal event queue
197     * @param internalIOProcessor the I/O Processor
198     */
199    protected void setInternalIOProcessor(SCXMLIOProcessor internalIOProcessor) {
200        this.internalIOProcessor = internalIOProcessor;
201    }
202
203    /**
204     * Set or re-attach the evaluator
205     * <p>
206     * If not re-attaching and this state machine instance has been initialized before,
207     * it will be initialized again, destroying all existing state!
208     * </p>
209     * @param evaluator The evaluator for this state machine instance.
210     */
211    protected void setEvaluator(Evaluator evaluator, boolean reAttach) throws ModelException {
212        this.evaluator = evaluator;
213        if (initialized) {
214            if (!reAttach) {
215                // change of evaluator after initialization: re-initialize
216                initialize();
217            }
218            else if (evaluator == null) {
219                throw new ModelException("SCInstance: re-attached without Evaluator");
220            }
221        }
222    }
223
224    /**
225     * @return Return the current evaluator
226     */
227    protected Evaluator getEvaluator() {
228        return evaluator;
229    }
230
231    /**
232     * Set or re-attach the error reporter
233     * @param errorReporter The error reporter for this state machine instance.
234     * @throws ModelException if an attempt is made to set a null value for the error reporter
235     */
236    protected void setErrorReporter(ErrorReporter errorReporter) throws ModelException {
237        if (errorReporter == null) {
238            throw new ModelException(ERR_NO_ERROR_REPORTER);
239        }
240        this.errorReporter = errorReporter;
241    }
242
243    /**
244     * @return Return the state machine for this instance
245     */
246    public SCXML getStateMachine() {
247        return stateMachine;
248    }
249
250    /**
251     * Sets the state machine for this instance.
252     * <p>
253     * If this state machine instance has been initialized before, it will be initialized again, destroying all existing
254     * state!
255     * </p>
256     * @param stateMachine The state machine for this instance
257     * @throws ModelException if an attempt is made to set a null value for the state machine
258     */
259    protected void setStateMachine(SCXML stateMachine) throws ModelException {
260        if (stateMachine == null) {
261            throw new ModelException(ERR_NO_STATE_MACHINE);
262        }
263        this.stateMachine = stateMachine;
264        initialize();
265    }
266
267    public void setSingleContext(boolean singleContext) throws ModelException {
268        if (initialized) {
269            throw new ModelException("SCInstance: already initialized");
270        }
271        this.singleContext = singleContext;
272    }
273
274    public boolean isSingleContext() {
275        return singleContext;
276    }
277
278    /**
279     * Clone data model.
280     *
281     * @param ctx The context to clone to.
282     * @param datamodel The datamodel to clone.
283     * @param evaluator The expression evaluator.
284     * @param errorReporter The error reporter
285     */
286    protected void cloneDatamodel(final Datamodel datamodel, final Context ctx, final Evaluator evaluator,
287                                      final ErrorReporter errorReporter) {
288        if (datamodel == null || Evaluator.NULL_DATA_MODEL.equals(evaluator.getSupportedDatamodel())) {
289            return;
290        }
291        List<Data> data = datamodel.getData();
292        if (data == null) {
293            return;
294        }
295        for (Data datum : data) {
296            if (ctx.has(datum.getId())) {
297                // earlier or externally defined 'initial' value found: do not overwrite
298                continue;
299            }
300            Node datumNode = datum.getNode();
301            Node valueNode = null;
302            if (datumNode != null) {
303                valueNode = datumNode.cloneNode(true);
304            }
305            // prefer "src" over "expr" over "inline"
306            if (datum.getSrc() != null) {
307                ctx.setLocal(datum.getId(), valueNode);
308            } else if (datum.getExpr() != null) {
309                Object value;
310                try {
311                    ctx.setLocal(Context.NAMESPACES_KEY, datum.getNamespaces());
312                    value = evaluator.eval(ctx, datum.getExpr());
313                    ctx.setLocal(Context.NAMESPACES_KEY, null);
314                } catch (SCXMLExpressionException see) {
315                    if (internalIOProcessor != null) {
316                        internalIOProcessor.addEvent(new TriggerEvent(TriggerEvent.ERROR_EXECUTION, TriggerEvent.ERROR_EVENT));
317                    }
318                    errorReporter.onError(ErrorConstants.EXPRESSION_ERROR, see.getMessage(), datum);
319                    continue;
320                }
321                if (Evaluator.XPATH_DATA_MODEL.equals(evaluator.getSupportedDatamodel())) {
322                    try {
323                        Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
324                        // TODO: should use SCXML namespace here?
325                        Element dataNode = document.createElement("data");
326                        dataNode.setAttribute("id", datum.getId());
327                        ctx.setLocal(datum.getId(), dataNode);
328                        evaluator.evalAssign(ctx, "$" + datum.getId(), value, Evaluator.AssignType.REPLACE_CHILDREN, null);
329                    }
330                    catch (ParserConfigurationException pce) {
331                        if (internalIOProcessor != null) {
332                            internalIOProcessor.addEvent(new TriggerEvent(TriggerEvent.ERROR_EXECUTION, TriggerEvent.ERROR_EVENT));
333                        }
334                        errorReporter.onError(ErrorConstants.EXECUTION_ERROR, pce.getMessage(), datum);
335                    }
336                    catch (SCXMLExpressionException see) {
337                        if (internalIOProcessor != null) {
338                            internalIOProcessor.addEvent(new TriggerEvent(TriggerEvent.ERROR_EXECUTION, TriggerEvent.ERROR_EVENT));
339                        }
340                        errorReporter.onError(ErrorConstants.EXPRESSION_ERROR, see.getMessage(), datum);
341                    }
342                }
343                else {
344                    ctx.setLocal(datum.getId(), value);
345                }
346            } else {
347                ctx.setLocal(datum.getId(), valueNode);
348            }
349        }
350    }
351
352    /**
353     * @return Returns the state configuration for this instance
354     */
355    public StateConfiguration getStateConfiguration() {
356        return stateConfiguration;
357    }
358
359    /**
360     * @return Returns the current status for this instance
361     */
362    public Status getCurrentStatus() {
363        return currentStatus;
364    }
365
366    /**
367     * @return Returns if the state machine is running
368     */
369    public boolean isRunning() {
370        return running;
371    }
372
373    /**
374     * Sets the running status of the state machine
375     * @param running flag indicating the running status of the state machine
376     * @throws IllegalStateException Exception thrown if trying to set the state machine running when in a Final state
377     */
378    protected void setRunning(final boolean running) throws IllegalStateException {
379        if (!this.running && running && currentStatus.isFinal()) {
380            throw new IllegalStateException("The state machine is in a Final state and cannot be set running again");
381        }
382        this.running = running;
383    }
384
385    /**
386     * Get the root context.
387     *
388     * @return The root context.
389     */
390    public Context getRootContext() {
391        if (rootContext == null && evaluator != null) {
392            rootContext = Evaluator.NULL_DATA_MODEL.equals(evaluator.getSupportedDatamodel())
393                    ? new SimpleContext() : evaluator.newContext(null);
394        }
395        return rootContext;
396    }
397
398    /**
399     * Set or replace the root context.
400     * @param context The new root context.
401     */
402    protected void setRootContext(final Context context) {
403        this.rootContext = context;
404        // force initialization of rootContext
405        getRootContext();
406        if (systemContext != null) {
407            // re-parent the system context
408            systemContext.setSystemContext(evaluator.newContext(rootContext));
409        }
410    }
411
412    /**
413     * Get the unwrapped (modifiable) system context.
414     *
415     * @return The unwrapped system context.
416     */
417    public Context getSystemContext() {
418        if (systemContext == null) {
419            // force initialization of rootContext
420            getRootContext();
421            if (rootContext != null) {
422                Context internalContext = Evaluator.NULL_DATA_MODEL.equals(evaluator.getSupportedDatamodel()) ?
423                        new SimpleContext(systemContext) : evaluator.newContext(rootContext);
424                systemContext = new SCXMLSystemContext(internalContext);
425                systemContext.getContext().set(SCXMLSystemContext.SESSIONID_KEY, UUID.randomUUID().toString());
426                String _name = stateMachine != null && stateMachine.getName() != null ? stateMachine.getName() : "";
427                systemContext.getContext().set(SCXMLSystemContext.SCXML_NAME_KEY, _name);
428                systemContext.getPlatformVariables().put(SCXMLSystemContext.STATUS_KEY, currentStatus);
429            }
430        }
431        return systemContext != null ? systemContext.getContext() : null;
432    }
433
434    /**
435     * @return Returns the global context, which is the top context <em>within</em> the state machine.
436     */
437    public Context getGlobalContext() {
438        if (globalContext == null) {
439            // force initialization of systemContext
440            getSystemContext();
441            if (systemContext != null) {
442                globalContext = evaluator.newContext(systemContext);
443            }
444        }
445        return globalContext;
446    }
447
448    /**
449     * Get the context for an EnterableState or create one if not created before.
450     *
451     * @param state The EnterableState.
452     * @return The context.
453     */
454    public Context getContext(final EnterableState state) {
455        Context context = contexts.get(state);
456        if (context == null) {
457            if (singleContext) {
458                context = getGlobalContext();
459            }
460            else {
461                EnterableState parent = state.getParent();
462                if (parent == null) {
463                    // docroot
464                    context = evaluator.newContext(getGlobalContext());
465                } else {
466                    context = evaluator.newContext(getContext(parent));
467                }
468            }
469            if (state instanceof TransitionalState) {
470                Datamodel datamodel = ((TransitionalState)state).getDatamodel();
471                cloneDatamodel(datamodel, context, evaluator, errorReporter);
472            }
473            contexts.put(state, context);
474        }
475        return context;
476    }
477
478    /**
479     * Get the context for an EnterableState if available.
480     *
481     * <p>Note: used for testing purposes only</p>
482     *
483     * @param state The EnterableState
484     * @return The context or null if not created yet.
485     */
486    Context lookupContext(final EnterableState state) {
487        return contexts.get(state);
488    }
489
490    /**
491     * Set the context for an EnterableState
492     *
493     * <p>Note: used for testing purposes only</p>
494     *
495     * @param state The EnterableState.
496     * @param context The context.
497     */
498    void setContext(final EnterableState state,
499            final Context context) {
500        contexts.put(state, context);
501    }
502
503    /**
504     * Get the last configuration for this history.
505     *
506     * @param history The history.
507     * @return Returns the lastConfiguration.
508     */
509    public Set<EnterableState> getLastConfiguration(final History history) {
510        Set<EnterableState> lastConfiguration = histories.get(history);
511        if (lastConfiguration == null) {
512            lastConfiguration = Collections.emptySet();
513        }
514        return lastConfiguration;
515    }
516
517    /**
518     * Set the last configuration for this history.
519     *
520     * @param history The history.
521     * @param lc The lastConfiguration to set.
522     */
523    public void setLastConfiguration(final History history,
524            final Set<EnterableState> lc) {
525        histories.put(history, new HashSet<EnterableState>(lc));
526    }
527
528    /**
529     * Resets the history state.
530     *
531     * <p>Note: used for testing purposes only</p>
532     *
533     * @param history The history.
534     */
535    public void resetConfiguration(final History history) {
536        histories.remove(history);
537    }
538}
539