blob: c74555f9fa9caae05ff121b6f76a0a54737c8acf [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.scxml2.io;
import java.text.MessageFormat;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.scxml2.model.EnterableState;
import org.apache.commons.scxml2.model.History;
import org.apache.commons.scxml2.model.Initial;
import org.apache.commons.scxml2.model.Invoke;
import org.apache.commons.scxml2.model.ModelException;
import org.apache.commons.scxml2.model.Parallel;
import org.apache.commons.scxml2.model.SCXML;
import org.apache.commons.scxml2.model.SimpleTransition;
import org.apache.commons.scxml2.model.State;
import org.apache.commons.scxml2.model.Transition;
import org.apache.commons.scxml2.model.TransitionTarget;
import org.apache.commons.scxml2.model.TransitionalState;
/**
* The ModelUpdater provides the utility methods to check the Commons
* SCXML model for inconsistencies, detect errors, and wire the Commons
* SCXML model appropriately post document parsing by the SCXMLReader to make
* it executor ready.
*/
final class ModelUpdater {
//// Error messages
/**
* Error message when SCXML document specifies an illegal initial state.
*/
private static final String ERR_SCXML_NO_INIT = "No SCXML child state "
+ "with ID \"{0}\" found; illegal initial state for SCXML document";
/**
* Error message when SCXML document specifies an illegal initial state.
*/
private static final String ERR_UNSUPPORTED_INIT = "Initial attribute or element not supported for "
+ "atomic {0}";
/**
* Error message when a state element specifies an initial state which
* is not a direct descendent.
*/
private static final String ERR_STATE_BAD_INIT = "Initial state "
+ "null or not a descendant of {0}";
/**
* Error message when a referenced history state cannot be found.
*/
private static final String ERR_STATE_NO_HIST = "Referenced history state"
+ " null for {0}";
/**
* Error message when a shallow history state is not a child state.
*/
private static final String ERR_STATE_BAD_SHALLOW_HIST = "History state"
+ " for shallow history is not child for {0}";
/**
* Error message when a deep history state is not a descendent state.
*/
private static final String ERR_STATE_BAD_DEEP_HIST = "History state"
+ " for deep history is not descendant for {0}";
/**
* Transition target is not a legal IDREF (not found).
*/
private static final String ERR_TARGET_NOT_FOUND =
"Transition target with ID \"{0}\" not found";
/**
* Transition targets do not form a legal configuration.
*/
private static final String ERR_ILLEGAL_TARGETS =
"Transition targets \"{0}\" do not satisfy the requirements for"
+ " target regions belonging to a <parallel>";
/**
* Simple states should not contain a history.
*/
private static final String ERR_HISTORY_SIMPLE_STATE =
"Simple {0} contains history elements";
/**
* History does not specify a default transition target.
*/
private static final String ERR_HISTORY_NO_DEFAULT =
"No default target specified for history with ID \"{0}\""
+ " belonging to {1}";
/**
* Error message when an &lt;invoke&gt; specifies both "src" and "srcexpr"
* attributes.
*/
private static final String ERR_INVOKE_AMBIGUOUS_SRC = "{0} contains "
+ "<invoke> with both \"src\" and \"srcexpr\" attributes specified,"
+ " must specify either one, but not both.";
/**
* Discourage instantiation since this is a utility class.
*/
private ModelUpdater() {
}
/*
* Post-processing methods to make the SCXML object SCXMLExecutor ready.
*/
/**
* <p>Update the SCXML object model and make it SCXMLExecutor ready.
* This is part of post-read processing, and sets up the necessary
* object references throughtout the SCXML object model for the parsed
* document.</p>
*
* @param scxml The SCXML object (output from SCXMLReader)
* @throws ModelException If the object model is flawed
*/
static void updateSCXML(final SCXML scxml) throws ModelException {
initDocumentOrder(scxml.getChildren(), 1);
String initial = scxml.getInitial();
SimpleTransition initialTransition = new SimpleTransition();
if (initial != null) {
initialTransition.setNext(scxml.getInitial());
updateTransition(initialTransition, scxml.getTargets());
if (initialTransition.getTargets().size() == 0) {
logAndThrowModelError(ERR_SCXML_NO_INIT, new Object[] {
initial });
}
} else {
// If 'initial' is not specified, the default initial state is
// the first child state in document order.
initialTransition.getTargets().add(scxml.getFirstChild());
}
scxml.setInitialTransition(initialTransition);
Map<String, TransitionTarget> targets = scxml.getTargets();
updateEnterableStates(scxml.getChildren(), targets);
scxml.getInitialTransition().setObservableId(1);
initObservables(scxml.getChildren(), 2);
}
/**
* Initialize all {@link org.apache.commons.scxml2.model.DocumentOrder} instances (EnterableState or Transition)
* by iterating them in document order setting their document order value.
* @param states The list of children states of a parent TransitionalState or the SCXML document itself
* @param nextOrder The next to be used order value
* @return Returns the next to be used order value
*/
private static int initDocumentOrder(final List<EnterableState> states, int nextOrder) {
for (EnterableState state : states) {
state.setOrder(nextOrder++);
if (state instanceof TransitionalState) {
TransitionalState ts = (TransitionalState)state;
for (Transition t : ts.getTransitionsList()) {
t.setOrder(nextOrder++);
}
nextOrder = initDocumentOrder(ts.getChildren(), nextOrder);
}
}
return nextOrder;
}
/**
* Initialize all {@link org.apache.commons.scxml2.model.Observable} instances in the SCXML document
* by iterating them in document order and seeding them with a unique obeservable id.
* @param states The list of children states of a parent TransitionalState or the SCXML document itself
* @param nextObservableId The next observable id sequence value to be used
* @return Returns the next to be used observable id sequence value
*/
private static int initObservables(final List<EnterableState>states, int nextObservableId) {
for (EnterableState es : states) {
es.setObservableId(nextObservableId++);
if (es instanceof TransitionalState) {
TransitionalState ts = (TransitionalState)es;
if (ts instanceof State) {
State s = (State)ts;
if (s.getInitial() != null && s.getInitial().getTransition() != null) {
s.getInitial().getTransition().setObservableId(nextObservableId++);
}
}
for (Transition t : ts.getTransitionsList()) {
t.setObservableId(nextObservableId++);
}
for (History h : ts.getHistory()) {
h.setObservableId(nextObservableId++);
if (h.getTransition() != null) {
h.getTransition().setObservableId(nextObservableId++);
}
}
nextObservableId = initObservables(ts.getChildren(), nextObservableId);
}
}
return nextObservableId;
}
/**
* Update this State object (part of post-read processing).
* Also checks for any errors in the document.
*
* @param state The State object
* @param targets The global Map of all transition targets
* @throws ModelException If the object model is flawed
*/
private static void updateState(final State state, final Map<String, TransitionTarget> targets)
throws ModelException {
List<EnterableState> children = state.getChildren();
if (state.isComposite()) {
//initialize next / initial
Initial ini = state.getInitial();
if (ini == null) {
state.setFirst(children.get(0).getId());
ini = state.getInitial();
}
SimpleTransition initialTransition = ini.getTransition();
updateTransition(initialTransition, targets);
Set<TransitionTarget> initialStates = initialTransition.getTargets();
// we have to allow for an indirect descendant initial (targets)
//check that initialState is a descendant of s
if (initialStates.size() == 0) {
logAndThrowModelError(ERR_STATE_BAD_INIT,
new Object[] {getName(state)});
} else {
for (TransitionTarget initialState : initialStates) {
if (!initialState.isDescendantOf(state)) {
logAndThrowModelError(ERR_STATE_BAD_INIT,
new Object[] {getName(state)});
}
}
}
}
else if (state.getInitial() != null) {
logAndThrowModelError(ERR_UNSUPPORTED_INIT, new Object[] {getName(state)});
}
List<History> histories = state.getHistory();
if (histories.size() > 0 && state.isSimple()) {
logAndThrowModelError(ERR_HISTORY_SIMPLE_STATE,
new Object[] {getName(state)});
}
for (History history : histories) {
updateHistory(history, targets, state);
}
for (Transition transition : state.getTransitionsList()) {
updateTransition(transition, targets);
}
for (Invoke inv : state.getInvokes()) {
if (inv.getSrc() != null && inv.getSrcexpr() != null) {
logAndThrowModelError(ERR_INVOKE_AMBIGUOUS_SRC, new Object[] {getName(state)});
}
}
updateEnterableStates(children, targets);
}
/**
* Update this Parallel object (part of post-read processing).
*
* @param parallel The Parallel object
* @param targets The global Map of all transition targets
* @throws ModelException If the object model is flawed
*/
private static void updateParallel(final Parallel parallel, final Map<String, TransitionTarget> targets)
throws ModelException {
updateEnterableStates(parallel.getChildren(), targets);
for (Transition transition : parallel.getTransitionsList()) {
updateTransition(transition, targets);
}
List<History> histories = parallel.getHistory();
for (History history : histories) {
updateHistory(history, targets, parallel);
}
// TODO: parallel must may have invokes too
}
/**
* Update the EnterableState objects (part of post-read processing).
*
* @param states The EnterableState objects
* @param targets The global Map of all transition targets
* @throws ModelException If the object model is flawed
*/
private static void updateEnterableStates(final List<EnterableState> states,
final Map<String, TransitionTarget> targets)
throws ModelException {
for (EnterableState es : states) {
if (es instanceof State) {
updateState((State) es, targets);
} else if (es instanceof Parallel) {
updateParallel((Parallel) es, targets);
}
}
}
/**
* Update this History object (part of post-read processing).
*
* @param history The History object
* @param targets The global Map of all transition targets
* @param parent The parent TransitionalState for this History
* @throws ModelException If the object model is flawed
*/
private static void updateHistory(final History history,
final Map<String, TransitionTarget> targets,
final TransitionalState parent)
throws ModelException {
SimpleTransition transition = history.getTransition();
if (transition == null || transition.getNext() == null) {
logAndThrowModelError(ERR_HISTORY_NO_DEFAULT,
new Object[] {history.getId(), getName(parent)});
}
else {
updateTransition(transition, targets);
Set<TransitionTarget> historyStates = transition.getTargets();
if (historyStates.size() == 0) {
logAndThrowModelError(ERR_STATE_NO_HIST,
new Object[] {getName(parent)});
}
for (TransitionTarget historyState : historyStates) {
if (!history.isDeep()) {
// Shallow history
if (!parent.getChildren().contains(historyState)) {
logAndThrowModelError(ERR_STATE_BAD_SHALLOW_HIST,
new Object[] {getName(parent)});
}
} else {
// Deep history
if (!historyState.isDescendantOf(parent)) {
logAndThrowModelError(ERR_STATE_BAD_DEEP_HIST,
new Object[] {getName(parent)});
}
}
}
}
}
/**
* Update this Transition object (part of post-read processing).
*
* @param transition The Transition object
* @param targets The global Map of all transition targets
* @throws ModelException If the object model is flawed
*/
private static void updateTransition(final SimpleTransition transition,
final Map<String, TransitionTarget> targets) throws ModelException {
String next = transition.getNext();
if (next == null) { // stay transition
return;
}
Set<TransitionTarget> tts = transition.getTargets();
if (tts.isEmpty()) {
// 'next' is a space separated list of transition target IDs
StringTokenizer ids = new StringTokenizer(next);
while (ids.hasMoreTokens()) {
String id = ids.nextToken();
TransitionTarget tt = targets.get(id);
if (tt == null) {
logAndThrowModelError(ERR_TARGET_NOT_FOUND, new Object[] {
id });
}
tts.add(tt);
}
if (tts.size() > 1) {
boolean legal = verifyTransitionTargets(tts);
if (!legal) {
logAndThrowModelError(ERR_ILLEGAL_TARGETS, new Object[] {
next });
}
}
}
}
/**
* Log an error discovered in post-read processing.
*
* @param errType The type of error
* @param msgArgs The arguments for formatting the error message
* @throws ModelException The model error, always thrown.
*/
private static void logAndThrowModelError(final String errType,
final Object[] msgArgs) throws ModelException {
MessageFormat msgFormat = new MessageFormat(errType);
String errMsg = msgFormat.format(msgArgs);
org.apache.commons.logging.Log log = LogFactory.
getLog(ModelUpdater.class);
log.error(errMsg);
throw new ModelException(errMsg);
}
/**
* Get a transition target identifier for error messages. This method is
* only called to produce an appropriate log message in some error
* conditions.
*
* @param tt The <code>TransitionTarget</code> object
* @return The transition target identifier for the error message
*/
private static String getName(final TransitionTarget tt) {
String name = "anonymous transition target";
if (tt instanceof State) {
name = "anonymous state";
if (tt.getId() != null) {
name = "state with ID \"" + tt.getId() + "\"";
}
} else if (tt instanceof Parallel) {
name = "anonymous parallel";
if (tt.getId() != null) {
name = "parallel with ID \"" + tt.getId() + "\"";
}
} else {
if (tt.getId() != null) {
name = "transition target with ID \"" + tt.getId() + "\"";
}
}
return name;
}
/**
* If a transition has multiple targets, then they satisfy the following
* criteria:
* <ul>
* <li>No target is an ancestor of any other target on the list</li>
* <li>A full legal state configuration results when all ancestors and default initial descendants have been added.
* <br/>This means that they all must share the same least common parallel ancestor.
* </li>
* </ul>
*
* @param tts The transition targets
* @return Whether this is a legal configuration
* @see <a href=http://www.w3.org/TR/scxml/#LegalStateConfigurations">
* http://www.w3.org/TR/scxml/#LegalStateConfigurations</a>
*/
private static boolean verifyTransitionTargets(final Set<TransitionTarget> tts) {
if (tts.size() < 2) { // No contention
return true;
}
TransitionTarget first = null;
int i = 0;
for (TransitionTarget tt : tts) {
if (first == null) {
first = tt;
i = tt.getNumberOfAncestors();
continue;
}
// find least common ancestor
for (i = Math.min(i, tt.getNumberOfAncestors()); i > 0 && first.getAncestor(i-1) != tt.getAncestor(i-1); i--) ;
if (i == 0) {
// no common ancestor
return false;
}
// ensure no target is an ancestor of any other target on the list
for (TransitionTarget other : tts) {
if (other != tt && other.isDescendantOf(tt) || tt.isDescendantOf(other)) {
return false;
}
}
}
// least common ancestor must be a parallel
return first != null && i > 0 && first.getAncestor(i-1) instanceof Parallel;
}
}