View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.scxml2;
18  
19  import java.io.Serializable;
20  import java.util.Collections;
21  import java.util.HashMap;
22  import java.util.HashSet;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Set;
26  import java.util.UUID;
27  
28  import javax.xml.parsers.DocumentBuilderFactory;
29  import javax.xml.parsers.ParserConfigurationException;
30  
31  import org.apache.commons.scxml2.env.SimpleContext;
32  import org.apache.commons.scxml2.model.Data;
33  import org.apache.commons.scxml2.model.Datamodel;
34  import org.apache.commons.scxml2.model.EnterableState;
35  import org.apache.commons.scxml2.model.History;
36  import org.apache.commons.scxml2.model.ModelException;
37  import org.apache.commons.scxml2.model.SCXML;
38  import org.apache.commons.scxml2.model.TransitionalState;
39  import org.apache.commons.scxml2.semantics.ErrorConstants;
40  import org.w3c.dom.Document;
41  import org.w3c.dom.Element;
42  import org.w3c.dom.Node;
43  
44  /**
45   * The <code>SCInstance</code> performs book-keeping functions for
46   * a particular execution of a state chart represented by a
47   * <code>SCXML</code> object.
48   */
49  public class SCInstance implements Serializable {
50  
51      /**
52       * Serial version UID.
53       */
54      private static final long serialVersionUID = 2L;
55  
56      /**
57       * SCInstance cannot be initialized without setting a state machine.
58       */
59      private static final String ERR_NO_STATE_MACHINE = "SCInstance: State machine not set";
60  
61      /**
62       * SCInstance cannot be initialized without setting an error reporter.
63       */
64      private static final String ERR_NO_ERROR_REPORTER = "SCInstance: ErrorReporter not set";
65  
66      /**
67       * Flag indicating the state machine instance has been initialized (before).
68       */
69      private boolean initialized;
70  
71      /**
72       * The stateMachine being executed.
73       */
74      private SCXML stateMachine;
75  
76      /**
77       * The current state configuration of the state machine
78       */
79      private final StateConfiguration stateConfiguration;
80  
81      /**
82       * The current status of the stateMachine.
83       */
84      private final Status currentStatus;
85  
86      /**
87       * Running status for this state machine
88       */
89      private boolean running;
90  
91      /**
92       * The SCXML I/O Processor for the internal event queue
93       */
94      private transient SCXMLIOProcessor internalIOProcessor;
95  
96      /**
97       * The Evaluator used for this state machine instance.
98       */
99      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