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