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.semantics; 018 019import java.util.ArrayList; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.LinkedHashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Set; 027 028import org.apache.commons.scxml2.ActionExecutionContext; 029import org.apache.commons.scxml2.Context; 030import org.apache.commons.scxml2.ErrorReporter; 031import org.apache.commons.scxml2.SCInstance; 032import org.apache.commons.scxml2.SCXMLExecutionContext; 033import org.apache.commons.scxml2.SCXMLExpressionException; 034import org.apache.commons.scxml2.SCXMLSemantics; 035import org.apache.commons.scxml2.SCXMLSystemContext; 036import org.apache.commons.scxml2.StateConfiguration; 037import org.apache.commons.scxml2.TriggerEvent; 038import org.apache.commons.scxml2.invoke.Invoker; 039import org.apache.commons.scxml2.invoke.InvokerException; 040import org.apache.commons.scxml2.model.Action; 041import org.apache.commons.scxml2.model.DocumentOrder; 042import org.apache.commons.scxml2.model.EnterableState; 043import org.apache.commons.scxml2.model.Executable; 044import org.apache.commons.scxml2.model.Final; 045import org.apache.commons.scxml2.model.History; 046import org.apache.commons.scxml2.model.Invoke; 047import org.apache.commons.scxml2.model.OnEntry; 048import org.apache.commons.scxml2.model.OnExit; 049import org.apache.commons.scxml2.model.Script; 050import org.apache.commons.scxml2.model.SimpleTransition; 051import org.apache.commons.scxml2.model.TransitionalState; 052import org.apache.commons.scxml2.model.ModelException; 053import org.apache.commons.scxml2.model.Parallel; 054import org.apache.commons.scxml2.model.SCXML; 055import org.apache.commons.scxml2.model.State; 056import org.apache.commons.scxml2.model.Transition; 057import org.apache.commons.scxml2.model.TransitionTarget; 058import org.apache.commons.scxml2.system.EventVariable; 059 060/** 061 * This class encapsulate and implements the 062 * <a href="http://www.w3.org/TR/2014/CR-scxml-20140313/#AlgorithmforSCXMLInterpretation"> 063 * W3C SCXML Algorithm for SCXML Interpretation</a> 064 * 065 * <p>Custom semantics can be created by sub-classing this implementation.</p> 066 * <p>This implementation is full stateless and all methods are public accessible to make 067 * it easier to extend, reuse and test its behavior.</p> 068 */ 069public class SCXMLSemanticsImpl implements SCXMLSemantics { 070 071 /** 072 * Suffix for error event that are triggered in reaction to invalid data 073 * model locations. 074 */ 075 public static final String ERR_ILLEGAL_ALLOC = ".error.illegalalloc"; 076 077 /** 078 * Optional post processing immediately following SCXMLReader. May be used 079 * for removing pseudo-states etc. 080 * 081 * @param input SCXML state machine 082 * @param errRep ErrorReporter callback 083 * @return normalized SCXML state machine, pseudo states are removed, etc. 084 */ 085 public SCXML normalizeStateMachine(final SCXML input, final ErrorReporter errRep) { 086 //it is a no-op for now 087 return input; 088 } 089 090 /** 091 * First step in the execution of an SCXML state machine. 092 * <p> 093 * This will first (re)initialize the state machine instance, destroying any existing state! 094 * </p> 095 * <p> 096 * The first step is corresponding to the Algorithm for SCXML processing from the interpret() procedure to the 097 * mainLoop() procedure up to the blocking wait for an external event. 098 * </p> 099 * <p> 100 * This step will thus complete the SCXML initial execution and a subsequent macroStep to stabilize the state 101 * machine again before returning. 102 * </p> 103 * <p> 104 * If the state machine no longer is running after all this, first the {@link #finalStep(SCXMLExecutionContext)} 105 * will be called for cleanup before returning. 106 * </p> 107 * @param exctx The execution context for this step 108 * @throws ModelException if the state machine instance failed to initialize or a SCXML model error occurred during 109 * the execution. 110 */ 111 public void firstStep(final SCXMLExecutionContext exctx) throws ModelException { 112 // (re)initialize the execution context and state machine instance 113 exctx.initialize(); 114 // execute global script if defined 115 executeGlobalScript(exctx); 116 // enter initial states 117 HashSet<TransitionalState> statesToInvoke = new HashSet<TransitionalState>(); 118 Step step = new Step(null); 119 step.getTransitList().add(exctx.getStateMachine().getInitialTransition()); 120 microStep(exctx, step, statesToInvoke); 121 // Execute Immediate Transitions 122 123 if (exctx.isRunning()) { 124 macroStep(exctx, statesToInvoke); 125 } 126 127 if (!exctx.isRunning()) { 128 finalStep(exctx); 129 } 130 } 131 132 /** 133 * Next step in the execution of an SCXML state machine. 134 * <p> 135 * The next step is corresponding to the Algorithm for SCXML processing mainEventLoop() procedure after receiving an 136 * external event, up to the blocking wait for another external event. 137 * </p> 138 * <p> 139 * If the state machine isn't {@link SCXMLExecutionContext#isRunning()} (any more), invoking this method will simply 140 * do nothing. 141 * </p> 142 * <p> 143 * If the provided event is a {@link TriggerEvent#CANCEL_EVENT}, the state machine will stop running. 144 * </p> 145 * <p> 146 * Otherwise, the event is set in the {@link SCXMLSystemContext} and processing of the event then is started, and 147 * if the event leads to any transitions a microStep for this event will be performed, followed up by a macroStep 148 * to stabilize the state machine again before returning. 149 * </p> 150 * <p> 151 * If the state machine no longer is running after all this, first the {@link #finalStep(SCXMLExecutionContext)} 152 * will be called for cleanup before returning. 153 * </p> 154 * @param exctx The execution context for this step 155 * @param event The event to process 156 * @throws ModelException if a SCXML model error occurred during the execution. 157 */ 158 public void nextStep(final SCXMLExecutionContext exctx, final TriggerEvent event) throws ModelException { 159 if (!exctx.isRunning()) { 160 return; 161 } 162 if (isCancelEvent(event)) { 163 exctx.stopRunning(); 164 } 165 else { 166 setSystemEventVariable(exctx.getScInstance(), event, false); 167 processInvokes(exctx, event); 168 Step step = new Step(event); 169 selectTransitions(exctx, step); 170 if (!step.getTransitList().isEmpty()) { 171 HashSet<TransitionalState> statesToInvoke = new HashSet<TransitionalState>(); 172 microStep(exctx, step, statesToInvoke); 173 if (exctx.isRunning()) { 174 macroStep(exctx, statesToInvoke); 175 } 176 } 177 } 178 if (!exctx.isRunning()) { 179 finalStep(exctx); 180 } 181 } 182 183 /** 184 * The final step in the execution of an SCXML state machine. 185 * <p> 186 * This final step is corresponding to the Algorithm for SCXML processing exitInterpreter() procedure, after the 187 * state machine stopped running. 188 * </p> 189 * <p> 190 * If the state machine still is {@link SCXMLExecutionContext#isRunning()} invoking this method will simply 191 * do nothing. 192 * </p> 193 * <p> 194 * This final step will exit all remaining active states and cancel any active invokers. 195 * </p> 196 * <p> 197 * <em>TODO: the current implementation does not yet provide final donedata handling.</em> 198 * </p> 199 * @param exctx The execution context for this step 200 * @throws ModelException if a SCXML model error occurred during the execution. 201 */ 202 public void finalStep(SCXMLExecutionContext exctx) throws ModelException { 203 if (exctx.isRunning()) { 204 return; 205 } 206 ArrayList<EnterableState> configuration = new ArrayList<EnterableState>(exctx.getScInstance().getStateConfiguration().getActiveStates()); 207 Collections.sort(configuration, DocumentOrder.reverseDocumentOrderComparator); 208 for (EnterableState es : configuration) { 209 for (OnExit onexit : es.getOnExits()) { 210 executeContent(exctx, onexit); 211 } 212 if (es instanceof TransitionalState) { 213 // check if invokers are active in this state 214 for (Invoke inv : ((TransitionalState)es).getInvokes()) { 215 exctx.cancelInvoker(inv); 216 } 217 } 218 exctx.getNotificationRegistry().fireOnExit(es, es); 219 exctx.getNotificationRegistry().fireOnExit(exctx.getStateMachine(), es); 220 if (!(es instanceof Final && es.getParent() == null)) { 221 exctx.getScInstance().getStateConfiguration().exitState(es); 222 } 223 // else: keep final Final 224 // TODO: returnDoneEvent(s.donedata)? 225 } 226 } 227 228 /** 229 * Perform a micro step in the execution of a state machine. 230 * <p> 231 * This micro step is corresponding to the Algorithm for SCXML processing microstep() procedure. 232 * <p> 233 * @param exctx The execution context for this step 234 * @param step The current micro step 235 * @param statesToInvoke the set of activated states which invokes need to be invoked at the end of the current 236 * macro step 237 * @throws ModelException if a SCXML model error occurred during the execution. 238 */ 239 public void microStep(final SCXMLExecutionContext exctx, final Step step, 240 final Set<TransitionalState> statesToInvoke) 241 throws ModelException { 242 buildStep(exctx, step); 243 exitStates(exctx, step, statesToInvoke); 244 executeTransitionContent(exctx, step); 245 enterStates(exctx, step, statesToInvoke); 246 step.clearIntermediateState(); 247 } 248 249 /** 250 * buildStep builds the exitSet and entrySet for the current configuration given the transitionList on the step. 251 * 252 * @param exctx The SCXML execution context 253 * @param step The step containing the list of transitions to be taken 254 * @throws ModelException if the result of taking the transitions would lead to an illegal configuration 255 */ 256 public void buildStep(final SCXMLExecutionContext exctx, final Step step) throws ModelException { 257 step.clearIntermediateState(); 258 259 // compute exitSet, if there is something to exit and record their History configurations if applicable 260 if (!exctx.getScInstance().getStateConfiguration().getActiveStates().isEmpty()) { 261 computeExitSet(step, exctx.getScInstance().getStateConfiguration()); 262 } 263 // compute entrySet 264 computeEntrySet(exctx, step); 265 266 // default result states to entrySet 267 Set<EnterableState> states = step.getEntrySet(); 268 if (!step.getExitSet().isEmpty()) { 269 // calculate result states by taking current states, subtracting exitSet and adding entrySet 270 states = new HashSet<EnterableState>(exctx.getScInstance().getStateConfiguration().getStates()); 271 states.removeAll(step.getExitSet()); 272 states.addAll(step.getEntrySet()); 273 } 274 // validate the result states represent a legal configuration 275 if (exctx.isCheckLegalConfiguration() && !isLegalConfiguration(states, exctx.getErrorReporter())) { 276 throw new ModelException("Illegal state machine configuration!"); 277 } 278 } 279 280 /** 281 * Perform a macro step in the execution of a state machine. 282 * <p> 283 * This macro step is corresponding to the Algorithm for SCXML processing mainEventLoop() procedure macro step 284 * sub-flow, which are the first <em>3</em> steps of the described <em>4</em>, so everything up to the blocking 285 * wait for an external event. 286 * <p> 287 * @param exctx The execution context for this step 288 * @param statesToInvoke the set of activated states which invokes need to be invoked at the end of the current 289 * macro step 290 * @throws ModelException if a SCXML model error occurred during the execution. 291 */ 292 public void macroStep(final SCXMLExecutionContext exctx, final Set<TransitionalState> statesToInvoke) 293 throws ModelException { 294 do { 295 boolean macroStepDone = false; 296 do { 297 Step step = new Step(null); 298 selectTransitions(exctx, step); 299 if (step.getTransitList().isEmpty()) { 300 TriggerEvent event = exctx.nextInternalEvent(); 301 if (event != null) { 302 if (isCancelEvent(event)) { 303 exctx.stopRunning(); 304 } 305 else { 306 setSystemEventVariable(exctx.getScInstance(), event, true); 307 step = new Step(event); 308 selectTransitions(exctx, step); 309 } 310 } 311 } 312 if (step.getTransitList().isEmpty()) { 313 macroStepDone = true; 314 } 315 else { 316 microStep(exctx, step, statesToInvoke); 317 } 318 319 } while (exctx.isRunning() && !macroStepDone); 320 321 if (exctx.isRunning() && !statesToInvoke.isEmpty()) { 322 initiateInvokes(exctx, statesToInvoke); 323 statesToInvoke.clear(); 324 } 325 } while (exctx.isRunning() && exctx.hasPendingInternalEvent()); 326 } 327 328 /** 329 * Compute and store the set of states to exit for the current list of transitions in the provided step. 330 * <p> 331 * This method corresponds to the Algorithm for SCXML processing computeExitSet() procedure. 332 * <p> 333 * @param step The step containing the list of transitions to be taken 334 * @param stateConfiguration The current configuration of the state machine ({@link SCInstance#getStateConfiguration()}). 335 */ 336 public void computeExitSet(final Step step, final StateConfiguration stateConfiguration) { 337 if (!stateConfiguration.getActiveStates().isEmpty()) { 338 for (SimpleTransition st : step.getTransitList()) { 339 computeExitSet(st, step.getExitSet(), stateConfiguration.getActiveStates()); 340 } 341 recordHistory(step, stateConfiguration.getStates(), stateConfiguration.getActiveStates()); 342 } 343 } 344 345 /** 346 * Compute and store the set of states to exit for one specific transition in the provided step. 347 * <p> 348 * This method corresponds to the Algorithm for SCXML processing computeExitSet() procedure. 349 * <p> 350 * @param transition The transition to compute the states to exit from 351 * @param exitSet The set for adding the states to exit to 352 * @param activeStates The current active states of the state machine ({@link StateConfiguration#getActiveStates()}). 353 */ 354 public void computeExitSet(SimpleTransition transition, Set<EnterableState> exitSet, Set<EnterableState> activeStates) { 355 if (!transition.getTargets().isEmpty()) { 356 TransitionalState transitionDomain = transition.getTransitionDomain(); 357 if (transitionDomain == null) { 358 // root transition: every active state will be exited 359 exitSet.addAll(activeStates); 360 } 361 else { 362 for (EnterableState state : activeStates) { 363 if (state.isDescendantOf(transitionDomain)) { 364 exitSet.add(state); 365 } 366 } 367 } 368 } 369 } 370 371 /** 372 * Record the history configurations for states to exit if applicable and temporarily store this in the step. 373 * <p> 374 * These history configurations must be pre-recorded as they might impact (re)entrance calculation during 375 * {@link #computeEntrySet(SCXMLExecutionContext, Step)}. 376 * </p> 377 * <p> 378 * Only after the new configuration has been validated (see: {@link #isLegalConfiguration(Set, ErrorReporter)}), the 379 * history configurations will be persisted during the actual {@link #exitStates(SCXMLExecutionContext, Step, Set)} 380 * processing. 381 * </p> 382 * @param step The step containing the list of states to exit, and the map to record the new history configurations 383 * @param atomicStates The current set of active atomic states in the state machine 384 * @param activeStates The current set of all active states in the state machine 385 */ 386 public void recordHistory(final Step step, final Set<EnterableState> atomicStates, final Set<EnterableState> activeStates) { 387 for (EnterableState es : step.getExitSet()) { 388 if (es instanceof TransitionalState && ((TransitionalState)es).hasHistory()) { 389 TransitionalState ts = (TransitionalState)es; 390 Set<EnterableState> shallow = null; 391 Set<EnterableState> deep = null; 392 for (History h : ts.getHistory()) { 393 if (h.isDeep()) { 394 if (deep == null) { 395 //calculate deep history for a given state once 396 deep = new HashSet<EnterableState>(); 397 for (EnterableState ott : atomicStates) { 398 if (ott.isDescendantOf(es)) { 399 deep.add(ott); 400 } 401 } 402 } 403 step.getNewHistoryConfigurations().put(h, deep); 404 } else { 405 if (shallow == null) { 406 //calculate shallow history for a given state once 407 shallow = new HashSet<EnterableState>(ts.getChildren()); 408 shallow.retainAll(activeStates); 409 } 410 step.getNewHistoryConfigurations().put(h, shallow); 411 } 412 } 413 } 414 } 415 } 416 417 /** 418 * Compute and store the set of states to enter for the current list of transitions in the provided step. 419 * <p> 420 * This method corresponds to the Algorithm for SCXML processing computeEntrySet() procedure. 421 * <p> 422 * @param exctx The execution context for this step 423 * @param step The step containing the list of transitions to be taken 424 */ 425 public void computeEntrySet(final SCXMLExecutionContext exctx, final Step step) { 426 Set<History> historyTargets = new HashSet<History>(); 427 Set<EnterableState> entrySet = new HashSet<EnterableState>(); 428 for (SimpleTransition st : step.getTransitList()) { 429 for (TransitionTarget tt : st.getTargets()) { 430 if (tt instanceof EnterableState) { 431 entrySet.add((EnterableState) tt); 432 } 433 else { 434 // History 435 historyTargets.add((History)tt); 436 } 437 } 438 } 439 for (EnterableState es : entrySet) { 440 addDescendantStatesToEnter(exctx, step, es); 441 } 442 for (History h : historyTargets) { 443 addDescendantStatesToEnter(exctx, step, h); 444 } 445 for (SimpleTransition st : step.getTransitList()) { 446 TransitionalState ancestor = st.getTransitionDomain(); 447 for (TransitionTarget tt : st.getTargets()) { 448 addAncestorStatesToEnter(exctx, step, tt, ancestor); 449 } 450 } 451 } 452 453 /** 454 * This method corresponds to the Algorithm for SCXML processing addDescendantStatesToEnter() procedure. 455 * 456 * @param exctx The execution context for this step 457 * @param step The step 458 * @param tt The TransitionTarget 459 */ 460 public void addDescendantStatesToEnter(final SCXMLExecutionContext exctx, final Step step, 461 final TransitionTarget tt) { 462 if (tt instanceof History) { 463 History h = (History) tt; 464 Set<EnterableState> lastConfiguration = step.getNewHistoryConfigurations().get(h); 465 if (lastConfiguration == null) { 466 lastConfiguration = exctx.getScInstance().getLastConfiguration(h); 467 } 468 if (lastConfiguration.isEmpty()) { 469 step.getDefaultHistoryTransitions().put(h.getParent(), h.getTransition()); 470 for (TransitionTarget dtt : h.getTransition().getTargets()) { 471 addDescendantStatesToEnter(exctx, step, dtt); 472 } 473 for (TransitionTarget dtt : h.getTransition().getTargets()) { 474 addAncestorStatesToEnter(exctx, step, dtt, tt.getParent()); 475 } 476 } else { 477 for (TransitionTarget dtt : lastConfiguration) { 478 addDescendantStatesToEnter(exctx, step, dtt); 479 } 480 for (TransitionTarget dtt : lastConfiguration) { 481 addAncestorStatesToEnter(exctx, step, dtt, tt.getParent()); 482 } 483 } 484 } 485 else { // tt instanceof EnterableState 486 EnterableState es = (EnterableState)tt; 487 step.getEntrySet().add(es); 488 if (es instanceof Parallel) { 489 for (EnterableState child : ((Parallel)es).getChildren()) { 490 if (!containsDescendant(step.getEntrySet(), child)) { 491 addDescendantStatesToEnter(exctx, step, child); 492 } 493 } 494 } 495 else if (es instanceof State && ((State) es).isComposite()) { 496 step.getDefaultEntrySet().add(es); 497 for (TransitionTarget dtt : ((State)es).getInitial().getTransition().getTargets()) { 498 addDescendantStatesToEnter(exctx, step, dtt); 499 } 500 for (TransitionTarget dtt : ((State)es).getInitial().getTransition().getTargets()) { 501 addAncestorStatesToEnter(exctx, step, dtt, tt); 502 } 503 } 504 } 505 } 506 507 /** 508 * This method corresponds to the Algorithm for SCXML processing addAncestorStatesToEnter() procedure. 509 * 510 * @param exctx The execution context for this step 511 * @param step The step 512 * @param tt The TransitionTarget 513 * @param ancestor The ancestor TransitionTarget 514 */ 515 public void addAncestorStatesToEnter(final SCXMLExecutionContext exctx, final Step step, 516 final TransitionTarget tt, TransitionTarget ancestor) { 517 // for for anc in getProperAncestors(tt,ancestor) 518 for (int i = tt.getNumberOfAncestors()-1; i > -1; i--) { 519 EnterableState anc = tt.getAncestor(i); 520 if (anc == ancestor) { 521 break; 522 } 523 step.getEntrySet().add(anc); 524 if (anc instanceof Parallel) { 525 for (EnterableState child : ((Parallel)anc).getChildren()) { 526 if (!containsDescendant(step.getEntrySet(), child)) { 527 addDescendantStatesToEnter(exctx, step, child); 528 } 529 } 530 531 } 532 } 533 } 534 535 /** 536 * @return Returns true if a member of the provided states set is a descendant of the provided state. 537 * @param states the set of states to check for descendants 538 * @param state the state to check with 539 */ 540 public boolean containsDescendant(Set<EnterableState> states, EnterableState state) { 541 for (EnterableState es : states) { 542 if (es.isDescendantOf(state)) { 543 return true; 544 } 545 } 546 return false; 547 } 548 549 /** 550 * This method corresponds to the Algorithm for SCXML processing selectTransitions() as well as the 551 * selectEventlessTransitions() procedure, depending on the event (or null) in the provided step 552 * <p> 553 * @param exctx The execution context for this step 554 * @param step The step 555 */ 556 public void selectTransitions(final SCXMLExecutionContext exctx, final Step step) throws ModelException { 557 step.getTransitList().clear(); 558 ArrayList<Transition> enabledTransitions = new ArrayList<Transition>(); 559 560 ArrayList<EnterableState> configuration = new ArrayList<EnterableState>(exctx.getScInstance().getStateConfiguration().getActiveStates()); 561 Collections.sort(configuration,DocumentOrder.documentOrderComparator); 562 563 HashSet<EnterableState> visited = new HashSet<EnterableState>(); 564 565 String eventName = step.getEvent() != null ? step.getEvent().getName() : null; 566 for (EnterableState es : configuration) { 567 if (es.isAtomicState()) { 568 if (es instanceof Final) { 569 // Final states don't have transitions, skip to parent 570 if (es.getParent() == null) { 571 // should not happen: a top level active Final state should have stopped the state machine 572 throw new ModelException("Illegal state machine configuration: encountered top level <final> " 573 + "state while processing an event"); 574 } 575 else { 576 es = es.getParent(); 577 } 578 } 579 TransitionalState state = (TransitionalState)es; 580 TransitionalState current = state; 581 int ancestorIndex = state.getNumberOfAncestors()-1; 582 boolean transitionMatched = false; 583 do { 584 for (Transition transition : current.getTransitionsList()) { 585 if (transitionMatched = matchTransition(exctx, transition, eventName)) { 586 enabledTransitions.add(transition); 587 break; 588 } 589 } 590 current = (!transitionMatched && ancestorIndex > -1) ? state.getAncestor(ancestorIndex--) : null; 591 } while (!transitionMatched && current != null && visited.add(current)); 592 } 593 } 594 removeConflictingTransitions(exctx, step, enabledTransitions); 595 } 596 597 /** 598 * This method corresponds to the Algorithm for SCXML processing removeConflictingTransitions() procedure. 599 * 600 * @param exctx The execution context for this step 601 * @param step The step 602 * @param enabledTransitions The list of enabled transitions 603 */ 604 public void removeConflictingTransitions(final SCXMLExecutionContext exctx, final Step step, 605 final List<Transition> enabledTransitions) { 606 LinkedHashSet<Transition> filteredTransitions = new LinkedHashSet<Transition>(); 607 LinkedHashSet<Transition> preemptedTransitions = new LinkedHashSet<Transition>(); 608 Map<Transition, Set<EnterableState>> exitSets = new HashMap<Transition, Set<EnterableState>>(); 609 610 Set<EnterableState> configuration = exctx.getScInstance().getStateConfiguration().getActiveStates(); 611 Collections.sort(enabledTransitions, DocumentOrder.documentOrderComparator); 612 613 for (Transition t1 : enabledTransitions) { 614 boolean t1Preempted = false; 615 Set<EnterableState> t1ExitSet = exitSets.get(t1); 616 for (Transition t2 : filteredTransitions) { 617 if (t1ExitSet == null) { 618 t1ExitSet = new HashSet<EnterableState>(); 619 computeExitSet(t1, t1ExitSet, configuration); 620 exitSets.put(t1, t1ExitSet); 621 } 622 Set<EnterableState> t2ExitSet = exitSets.get(t2); 623 if (t2ExitSet == null) { 624 t2ExitSet = new HashSet<EnterableState>(); 625 computeExitSet(t2, t2ExitSet, configuration); 626 exitSets.put(t2, t2ExitSet); 627 } 628 Set<EnterableState> smaller = t1ExitSet.size() < t2ExitSet.size() ? t1ExitSet : t2ExitSet; 629 Set<EnterableState> larger = smaller == t1ExitSet ? t2ExitSet : t1ExitSet; 630 boolean hasIntersection = false; 631 for (EnterableState s1 : smaller) { 632 hasIntersection = larger.contains(s1); 633 if (hasIntersection) { 634 break; 635 } 636 } 637 if (hasIntersection) { 638 if (t1.getParent().isDescendantOf(t2.getParent())) { 639 preemptedTransitions.add(t2); 640 } 641 else { 642 t1Preempted = true; 643 break; 644 } 645 } 646 } 647 if (t1Preempted) { 648 exitSets.remove(t1); 649 } 650 else { 651 for (Transition preempted : preemptedTransitions) { 652 filteredTransitions.remove(preempted); 653 exitSets.remove(preempted); 654 } 655 filteredTransitions.add(t1); 656 } 657 } 658 step.getTransitList().addAll(filteredTransitions); 659 } 660 661 /** 662 * @param exctx The execution context for this step 663 * @param transition The transition 664 * @param eventName The (optional) event name to match against 665 * @return Returns true if the transition matches against the provided eventName, or is event-less when no eventName 666 * is provided, <em>AND</em> its (optional) condition guard evaluates to true. 667 */ 668 public boolean matchTransition(final SCXMLExecutionContext exctx, final Transition transition, final String eventName) { 669 if (eventName != null) { 670 if (!(transition.isNoEventsTransition() || transition.isAllEventsTransition())) { 671 boolean eventMatch = false; 672 for (String event : transition.getEvents()) { 673 if (eventName.startsWith(event)) { 674 if (eventName.length() == event.length() || eventName.charAt(event.length())=='.') 675 eventMatch = true; 676 break; 677 } 678 } 679 if (!eventMatch) { 680 return false; 681 } 682 } 683 else if (!transition.isAllEventsTransition()) { 684 return false; 685 } 686 } 687 else if (!transition.isNoEventsTransition()) { 688 return false; 689 } 690 if (transition.getCond() != null) { 691 Boolean result = Boolean.FALSE; 692 Context context = exctx.getScInstance().getContext(transition.getParent()); 693 context.setLocal(Context.NAMESPACES_KEY, transition.getNamespaces()); 694 try { 695 if ((result = exctx.getEvaluator().evalCond(context, transition.getCond())) == null) { 696 result = Boolean.FALSE; 697 if (exctx.getAppLog().isDebugEnabled()) { 698 exctx.getAppLog().debug("Treating as false because the cond expression was evaluated as null: '" 699 + transition.getCond() + "'"); 700 } 701 } 702 } 703 catch (SCXMLExpressionException e) { 704 exctx.getInternalIOProcessor().addEvent(new TriggerEvent(TriggerEvent.ERROR_EXECUTION, TriggerEvent.ERROR_EVENT)); 705 exctx.getErrorReporter().onError(ErrorConstants.EXPRESSION_ERROR, "Treating as false due to error: " 706 + e.getMessage(), transition); 707 } 708 finally { 709 context.setLocal(Context.NAMESPACES_KEY, null); 710 } 711 return result; 712 } 713 return true; 714 } 715 716 /** 717 * This method corresponds to the Algorithm for SCXML processing isFinalState() function. 718 * 719 * @param es the enterable state to check 720 * @param configuration the current state machine configuration 721 * @return Return true if s is a compound state and one of its children is an active final state (i.e. is a member 722 * of the current configuration), or if s is a parallel state and isInFinalState is true of all its children. 723 */ 724 public boolean isInFinalState(final EnterableState es, final Set<EnterableState> configuration) { 725 if (es instanceof State) { 726 for (EnterableState child : ((State)es).getChildren()) { 727 if (child instanceof Final && configuration.contains(child)) { 728 return true; 729 } 730 } 731 } 732 else if (es instanceof Parallel) { 733 for (EnterableState child : ((Parallel)es).getChildren()) { 734 if (!isInFinalState(child, configuration)) { 735 return false; 736 } 737 } 738 return true; 739 } 740 return false; 741 } 742 743 /** 744 * Checks if an external event was send (back) by an specific Invoker 745 * 746 * @param invokerId the invokerId 747 * 748 * @param event received external event 749 * @return true if this event was send by the specific Invoker 750 */ 751 public boolean isInvokerEvent(final String invokerId, final TriggerEvent event) { 752 return event.getName().equals("done.invoke."+invokerId) || 753 event.getName().startsWith("done.invoke."+invokerId+"."); 754 } 755 756 /** 757 * Check if an external event indicates the state machine execution must be cancelled. 758 * 759 * @param event received external event 760 * @return true if this event is of type {@link TriggerEvent#CANCEL_EVENT}. 761 */ 762 public boolean isCancelEvent(TriggerEvent event) { 763 return (event.getType() == TriggerEvent.CANCEL_EVENT); 764 } 765 766 /** 767 * Checks whether a given set of states is a legal Harel State Table 768 * configuration (with the respect to the definition of the OR and AND 769 * states). 770 * 771 * @param states a set of states 772 * @param errRep ErrorReporter to report detailed error info if needed 773 * @return true if a given state configuration is legal, false otherwise 774 */ 775 public boolean isLegalConfiguration(final Set<EnterableState> states, final ErrorReporter errRep) { 776 /* 777 * For every active state we add 1 to the count of its parent. Each 778 * Parallel should reach count equal to the number of its children and 779 * contribute by 1 to its parent. Each State should reach count exactly 780 * 1. SCXML elemnt (top) should reach count exactly 1. We essentially 781 * summarize up the hierarchy tree starting with a given set of 782 * states = active configuration. 783 */ 784 boolean legalConfig = true; // let's be optimists 785 Map<EnterableState, Set<EnterableState>> counts = new HashMap<EnterableState, Set<EnterableState>>(); 786 Set<EnterableState> scxmlCount = new HashSet<EnterableState>(); 787 for (EnterableState es : states) { 788 EnterableState parent; 789 while ((parent = es.getParent()) != null) { 790 Set<EnterableState> cnt = counts.get(parent); 791 if (cnt == null) { 792 cnt = new HashSet<EnterableState>(); 793 counts.put(parent, cnt); 794 } 795 cnt.add(es); 796 es = parent; 797 } 798 //top-level contribution 799 scxmlCount.add(es); 800 } 801 if (scxmlCount.size() > 1) { 802 errRep.onError(ErrorConstants.ILLEGAL_CONFIG, "Multiple top-level OR states active!", scxmlCount); 803 legalConfig = false; 804 } 805 else { 806 //Validate child counts: 807 for (Map.Entry<EnterableState, Set<EnterableState>> entry : counts.entrySet()) { 808 EnterableState es = entry.getKey(); 809 Set<EnterableState> count = entry.getValue(); 810 if (es instanceof Parallel) { 811 Parallel p = (Parallel) es; 812 if (count.size() < p.getChildren().size()) { 813 errRep.onError(ErrorConstants.ILLEGAL_CONFIG, "Not all AND states active for parallel " + p.getId(), entry); 814 legalConfig = false; 815 } 816 } else { 817 if (count.size() > 1) { 818 errRep.onError(ErrorConstants.ILLEGAL_CONFIG, "Multiple OR states active for state " + es.getId(), entry); 819 legalConfig = false; 820 } 821 } 822 count.clear(); //cleanup 823 } 824 } 825 //cleanup 826 scxmlCount.clear(); 827 counts.clear(); 828 return legalConfig; 829 } 830 831 /** 832 * Stores the provided event in the system context 833 * <p> 834 * For the event a EventVariable is instantiated and the provided event its type is mapped to the one of the 835 * SCXML specification predefined types. 836 * </p> 837 * @param scInstance the state machine instance holding the system context 838 * @param event The event being stored 839 * @param internal Flag indicating the event was received internally or externally 840 */ 841 public void setSystemEventVariable(final SCInstance scInstance, final TriggerEvent event, boolean internal) { 842 Context systemContext = scInstance.getSystemContext(); 843 EventVariable eventVar = null; 844 if (event != null) { 845 String eventType = internal ? EventVariable.TYPE_INTERNAL : EventVariable.TYPE_EXTERNAL; 846 847 final int triggerEventType = event.getType(); 848 if (triggerEventType == TriggerEvent.ERROR_EVENT || triggerEventType == TriggerEvent.CHANGE_EVENT) { 849 eventType = EventVariable.TYPE_PLATFORM; 850 } 851 852 // TODO: determine sendid, origin, originType and invokeid based on context later. 853 eventVar = new EventVariable(event.getName(), eventType, null, null, null, null, event.getPayload()); 854 } 855 systemContext.setLocal(SCXMLSystemContext.EVENT_KEY, eventVar); 856 } 857 858 /** 859 * Executes the global SCXML script element 860 * 861 * @param exctx The execution context 862 * @throws ModelException if a SCXML model error occurred during the execution. 863 */ 864 public void executeGlobalScript(final SCXMLExecutionContext exctx) throws ModelException { 865 Script globalScript = exctx.getStateMachine().getGlobalScript(); 866 if ( globalScript != null) { 867 try { 868 globalScript.execute(exctx.getActionExecutionContext()); 869 } catch (SCXMLExpressionException e) { 870 exctx.getInternalIOProcessor().addEvent(new TriggerEvent(TriggerEvent.ERROR_EXECUTION, TriggerEvent.ERROR_EVENT)); 871 exctx.getErrorReporter().onError(ErrorConstants.EXPRESSION_ERROR, e.getMessage(), exctx.getStateMachine()); 872 } 873 } 874 } 875 876 /** 877 * This method corresponds to the Algorithm for SCXML processing exitStates() procedure, where the states to exit 878 * already have been pre-computed in {@link #microStep(SCXMLExecutionContext, Step, java.util.Set)}. 879 * 880 * @param exctx The execution context for this micro step 881 * @param step the step 882 * @param statesToInvoke the set of activated states which invokes need to be invoked at the end of the current 883 * macro step 884 * @throws ModelException if a SCXML model error occurred during the execution. 885 */ 886 public void exitStates(final SCXMLExecutionContext exctx, final Step step, 887 final Set<TransitionalState> statesToInvoke) 888 throws ModelException { 889 if (step.getExitSet().isEmpty()) { 890 return; 891 } 892 ArrayList<EnterableState> exitList = new ArrayList<EnterableState>(step.getExitSet()); 893 Collections.sort(exitList, DocumentOrder.reverseDocumentOrderComparator); 894 895 for (EnterableState es : exitList) { 896 897 if (es instanceof TransitionalState && ((TransitionalState)es).hasHistory()) { 898 // persist the new history configurations for this state to exit 899 for (History h : ((TransitionalState)es).getHistory()) { 900 exctx.getScInstance().setLastConfiguration(h, step.getNewHistoryConfigurations().get(h)); 901 } 902 } 903 904 boolean onexitEventRaised = false; 905 for (OnExit onexit : es.getOnExits()) { 906 executeContent(exctx, onexit); 907 if (!onexitEventRaised && onexit.isRaiseEvent()) { 908 onexitEventRaised = true; 909 exctx.getInternalIOProcessor().addEvent(new TriggerEvent("exit.state."+es.getId(), TriggerEvent.CHANGE_EVENT)); 910 } 911 } 912 exctx.getNotificationRegistry().fireOnExit(es, es); 913 exctx.getNotificationRegistry().fireOnExit(exctx.getStateMachine(), es); 914 915 if (es instanceof TransitionalState && !statesToInvoke.remove(es)) { 916 // check if invokers are active in this state 917 for (Invoke inv : ((TransitionalState)es).getInvokes()) { 918 exctx.cancelInvoker(inv); 919 } 920 } 921 exctx.getScInstance().getStateConfiguration().exitState(es); 922 } 923 } 924 925 /** 926 * Executes the executable content for all transitions in the micro step 927 * 928 * @param exctx The execution context for this micro step 929 * @param step the step 930 * @throws ModelException if a SCXML model error occurred during the execution. 931 */ 932 public void executeTransitionContent(final SCXMLExecutionContext exctx, final Step step) throws ModelException { 933 for (SimpleTransition transition : step.getTransitList()) { 934 executeContent(exctx, transition); 935 } 936 } 937 938 /** 939 * Executes the executable content for a specific executable in the micro step 940 * 941 * @param exctx The execution context for this micro step 942 * @param exec the executable providing the execution content 943 * @throws ModelException if a SCXML model error occurred during the execution. 944 */ 945 public void executeContent(SCXMLExecutionContext exctx, Executable exec) throws ModelException { 946 try { 947 for (Action action : exec.getActions()) { 948 action.execute(exctx.getActionExecutionContext()); 949 } 950 } catch (SCXMLExpressionException e) { 951 exctx.getInternalIOProcessor().addEvent(new TriggerEvent(TriggerEvent.ERROR_EXECUTION, TriggerEvent.ERROR_EVENT)); 952 exctx.getErrorReporter().onError(ErrorConstants.EXPRESSION_ERROR, e.getMessage(), exec); 953 } 954 if (exec instanceof Transition) { 955 Transition t = (Transition)exec; 956 if (t.getTargets().isEmpty()) { 957 notifyOnTransition(exctx, t, t.getParent()); 958 } 959 else { 960 for (TransitionTarget tt : t.getTargets()) { 961 notifyOnTransition(exctx, t, tt); 962 } 963 } 964 } 965 } 966 967 /** 968 * Notifies SCXMLListeners on the transition taken 969 * 970 * @param exctx The execution context for this micro step 971 * @param t The Transition taken 972 * @param target The target of the Transition 973 */ 974 public void notifyOnTransition(final SCXMLExecutionContext exctx, final Transition t, 975 final TransitionTarget target) { 976 EventVariable event = (EventVariable)exctx.getScInstance().getSystemContext().getVars().get(SCXMLSystemContext.EVENT_KEY); 977 String eventName = event != null ? event.getName() : null; 978 exctx.getNotificationRegistry().fireOnTransition(t, t.getParent(), target, t, eventName); 979 exctx.getNotificationRegistry().fireOnTransition(exctx.getStateMachine(), t.getParent(), target, t, eventName); 980 } 981 982 /** 983 * This method corresponds to the Algorithm for SCXML processing enterStates() procedure, where the states to enter 984 * already have been pre-computed in {@link #microStep(SCXMLExecutionContext, Step, java.util.Set)}. 985 * 986 * @param exctx The execution context for this micro step 987 * @param step the step 988 * @param statesToInvoke the set of activated states which invokes need to be invoked at the end of the current 989 * macro step 990 * @throws ModelException if a SCXML model error occurred during the execution. 991 */ 992 public void enterStates(final SCXMLExecutionContext exctx, final Step step, 993 final Set<TransitionalState> statesToInvoke) 994 throws ModelException { 995 if (step.getEntrySet().isEmpty()) { 996 return; 997 } 998 ArrayList<EnterableState> entryList = new ArrayList<EnterableState>(step.getEntrySet()); 999 Collections.sort(entryList, DocumentOrder.documentOrderComparator); 1000 for (EnterableState es : entryList) { 1001 exctx.getScInstance().getStateConfiguration().enterState(es); 1002 if (es instanceof TransitionalState && !((TransitionalState)es).getInvokes().isEmpty()) { 1003 statesToInvoke.add((TransitionalState) es); 1004 } 1005 1006 boolean onentryEventRaised = false; 1007 for (OnEntry onentry : es.getOnEntries()) { 1008 executeContent(exctx, onentry); 1009 if (!onentryEventRaised && onentry.isRaiseEvent()) { 1010 onentryEventRaised = true; 1011 exctx.getInternalIOProcessor().addEvent(new TriggerEvent("entry.state."+es.getId(), TriggerEvent.CHANGE_EVENT)); 1012 } 1013 } 1014 exctx.getNotificationRegistry().fireOnEntry(es, es); 1015 exctx.getNotificationRegistry().fireOnEntry(exctx.getStateMachine(), es); 1016 1017 if (es instanceof State && step.getDefaultEntrySet().contains(es) && ((State)es).getInitial() != null) { 1018 executeContent(exctx, ((State)es).getInitial().getTransition()); 1019 } 1020 if (es instanceof TransitionalState) { 1021 SimpleTransition hTransition = step.getDefaultHistoryTransitions().get(es); 1022 if (hTransition != null) { 1023 executeContent(exctx, hTransition); 1024 } 1025 } 1026 1027 if (es instanceof Final) { 1028 State parent = (State)es.getParent(); 1029 if (parent == null) { 1030 exctx.stopRunning(); 1031 } 1032 else { 1033 exctx.getInternalIOProcessor().addEvent(new TriggerEvent("done.state."+parent.getId(),TriggerEvent.CHANGE_EVENT)); 1034 if (parent.isRegion()) { 1035 if (isInFinalState(parent.getParent(), exctx.getScInstance().getStateConfiguration().getActiveStates())) { 1036 exctx.getInternalIOProcessor().addEvent(new TriggerEvent("done.state."+parent.getParent().getId() 1037 , TriggerEvent.CHANGE_EVENT)); 1038 } 1039 } 1040 } 1041 } 1042 } 1043 } 1044 1045 /** 1046 * Initiate any new invoked activities. 1047 * 1048 * @param exctx provides the execution context 1049 * @param statesToInvoke the set of activated states which invokes need to be invoked 1050 */ 1051 public void initiateInvokes(final SCXMLExecutionContext exctx, 1052 final Set<TransitionalState> statesToInvoke) throws ModelException { 1053 ActionExecutionContext aexctx = exctx.getActionExecutionContext(); 1054 for (TransitionalState ts : statesToInvoke) { 1055 for (Invoke invoke : ts.getInvokes()) { 1056 Context ctx = aexctx.getContext(invoke.getParentEnterableState()); 1057 String exctxKey = invoke.getCurrentSCXMLExecutionContextKey(); 1058 ctx.setLocal(exctxKey, exctx); 1059 invoke.execute(aexctx); 1060 ctx.setLocal(exctxKey, null); 1061 } 1062 } 1063 } 1064 1065 /** 1066 * Forward events to invoked activities, execute finalize handlers. 1067 * 1068 * @param exctx provides the execution context 1069 * @param event The events to be forwarded 1070 * 1071 * @throws ModelException in case there is a fatal SCXML object model problem. 1072 */ 1073 public void processInvokes(final SCXMLExecutionContext exctx, final TriggerEvent event) throws ModelException { 1074 for (Map.Entry<Invoke, String> entry : exctx.getInvokeIds().entrySet()) { 1075 if (!isInvokerEvent(entry.getValue(), event)) { 1076 if (entry.getKey().isAutoForward()) { 1077 Invoker inv = exctx.getInvoker(entry.getKey()); 1078 try { 1079 inv.parentEvent(event); 1080 } catch (InvokerException ie) { 1081 exctx.getAppLog().error(ie.getMessage(), ie); 1082 throw new ModelException(ie.getMessage(), ie.getCause()); 1083 } 1084 } 1085 } 1086 /* 1087 else { 1088 // TODO: applyFinalize 1089 } 1090 */ 1091 } 1092 } 1093} 1094