blob: 77be38413fb12da8ad7734cf1df5b2a0decfc5a9 [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.scxml;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.scxml.model.Data;
import org.apache.commons.scxml.model.Datamodel;
import org.apache.commons.scxml.model.Parallel;
import org.apache.commons.scxml.model.Path;
import org.apache.commons.scxml.model.State;
import org.apache.commons.scxml.model.Transition;
import org.apache.commons.scxml.model.TransitionTarget;
import org.apache.commons.scxml.semantics.ErrorConstants;
import org.w3c.dom.CharacterData;
import org.w3c.dom.Node;
import org.w3c.dom.Text;
/**
* Helper class, all methods static final.
*
*/
public final class SCXMLHelper {
/**
* Return true if the string is empty.
*
* @param attr The String to test
* @return Is string empty
*/
public static boolean isStringEmpty(final String attr) {
if (attr == null || attr.trim().length() == 0) {
return true;
}
return false;
}
/**
* Checks whether a transition target tt (State or Parallel) is a
* descendant of the transition target context.
*
* @param tt
* TransitionTarget to check - a potential descendant
* @param ctx
* TransitionTarget context - a potential ancestor
* @return true iff tt is a descendant of ctx, false otherwise
*/
public static boolean isDescendant(final TransitionTarget tt,
final TransitionTarget ctx) {
TransitionTarget parent = tt.getParent();
while (parent != null) {
if (parent == ctx) {
return true;
}
parent = parent.getParent();
}
return false;
}
/**
* Creates a set which contains given states and all their ancestors
* recursively up to the upper bound. Null upperBound means root
* of the state machine.
*
* @param states The Set of States
* @param upperBounds The Set of upper bound States
* @return transitive closure of a given state set
*/
public static Set getAncestorClosure(final Set states,
final Set upperBounds) {
Set closure = new HashSet(states.size() * 2);
for (Iterator i = states.iterator(); i.hasNext();) {
TransitionTarget tt = (TransitionTarget) i.next();
while (tt != null) {
if (!closure.add(tt)) {
//tt is already a part of the closure
break;
}
if (upperBounds != null && upperBounds.contains(tt)) {
break;
}
tt = tt.getParent();
}
}
return closure;
}
/**
* Checks whether a given set of states is a legal Harel State Table
* configuration (with the respect to the definition of the OR and AND
* states).
*
* @param states
* a set of states
* @param errRep
* ErrorReporter to report detailed error info if needed
* @return true if a given state configuration is legal, false otherwise
*/
public static boolean isLegalConfig(final Set states,
final ErrorReporter errRep) {
/*
* For every active state we add 1 to the count of its parent. Each
* Parallel should reach count equal to the number of its children and
* contribute by 1 to its parent. Each State should reach count exactly
* 1. SCXML elemnt (top) should reach count exactly 1. We essentially
* summarize up the hierarchy tree starting with a given set of
* states = active configuration.
*/
boolean legalConfig = true; // let's be optimists
Map counts = new IdentityHashMap();
Set scxmlCount = new HashSet();
for (Iterator i = states.iterator(); i.hasNext();) {
TransitionTarget tt = (TransitionTarget) i.next();
TransitionTarget parent = null;
while ((parent = tt.getParent()) != null) {
HashSet cnt = (HashSet) counts.get(parent);
if (cnt == null) {
cnt = new HashSet();
counts.put(parent, cnt);
}
cnt.add(tt);
tt = parent;
}
//top-level contribution
scxmlCount.add(tt);
}
//Validate counts:
for (Iterator i = counts.entrySet().iterator(); i.hasNext();) {
Map.Entry entry = (Map.Entry) i.next();
TransitionTarget tt = (TransitionTarget) entry.getKey();
Set count = (Set) entry.getValue();
if (tt instanceof Parallel) {
Parallel p = (Parallel) tt;
if (count.size() < p.getChildren().size()) {
errRep.onError(ErrorConstants.ILLEGAL_CONFIG,
"Not all AND states active for parallel "
+ p.getId(), entry);
legalConfig = false;
}
} else {
if (count.size() > 1) {
errRep.onError(ErrorConstants.ILLEGAL_CONFIG,
"Multiple OR states active for state "
+ tt.getId(), entry);
legalConfig = false;
}
}
count.clear(); //cleanup
}
if (scxmlCount.size() > 1) {
errRep.onError(ErrorConstants.ILLEGAL_CONFIG,
"Multiple top-level OR states active!", scxmlCount);
}
//cleanup
scxmlCount.clear();
counts.clear();
return legalConfig;
}
/**
* Finds the least common ancestor of transition targets tt1 and tt2 if
* one exists.
*
* @param tt1 First TransitionTarget
* @param tt2 Second TransitionTarget
* @return closest common ancestor of tt1 and tt2 or null
*/
public static TransitionTarget getLCA(final TransitionTarget tt1,
final TransitionTarget tt2) {
if (tt1 == tt2) {
return tt1; //self-transition
} else if (isDescendant(tt1, tt2)) {
return tt2;
} else if (isDescendant(tt2, tt1)) {
return tt1;
}
Set parents = new HashSet();
TransitionTarget tmp = tt1;
while ((tmp = tmp.getParent()) != null) {
parents.add(tmp);
}
tmp = tt2;
while ((tmp = tmp.getParent()) != null) {
//test redundant add = common ancestor
if (!parents.add(tmp)) {
parents.clear();
return tmp;
}
}
return null;
}
/**
* Returns the set of all states (and parallels) which are exited if a
* given transition t is going to be taken.
* Current states are necessary to be taken into account
* due to orthogonal states and cross-region transitions -
* see UML specs for more details.
*
* @param t
* transition to be taken
* @param currentStates
* the set of current states (simple states only)
* @return a set of all states (including composite) which are exited if a
* given transition is taken
*/
public static Set getStatesExited(final Transition t,
final Set currentStates) {
Set allStates = new HashSet();
if (t.getTargets().size() == 0) {
return allStates;
}
Path p = (Path) t.getPaths().get(0); // all paths have same upseg
//the easy part
allStates.addAll(p.getUpwardSegment());
TransitionTarget source = t.getParent();
for (Iterator act = currentStates.iterator(); act.hasNext();) {
TransitionTarget a = (TransitionTarget) act.next();
if (isDescendant(a, source)) {
boolean added = false;
added = allStates.add(a);
while (added && a != source) {
a = a.getParent();
added = allStates.add(a);
}
}
}
if (p.isCrossRegion()) {
for (Iterator regions = p.getRegionsExited().iterator();
regions.hasNext();) {
Parallel par = ((Parallel) ((State) regions.next()).
getParent());
//let's find affected states in sibling regions
for (Iterator siblings = par.getChildren().iterator();
siblings.hasNext();) {
State s = (State) siblings.next();
for (Iterator act = currentStates.iterator();
act.hasNext();) {
TransitionTarget a = (TransitionTarget) act.next();
if (isDescendant(a, s)) {
//a is affected
boolean added = false;
added = allStates.add(a);
while (added && a != s) {
a = a.getParent();
added = allStates.add(a);
}
}
}
}
}
}
return allStates;
}
/**
* According to the UML definition, two transitions
* are conflicting if the sets of states they exit overlap.
*
* @param t1 a transition to check against t2
* @param t2 a transition to check against t1
* @param currentStates the set of current states (simple states only)
* @return true if the t1 and t2 are conflicting transitions
* @see #getStatesExited(Transition, Set)
*/
public static boolean inConflict(final Transition t1,
final Transition t2, final Set currentStates) {
Set ts1 = getStatesExited(t1, currentStates);
Set ts2 = getStatesExited(t2, currentStates);
ts1.retainAll(ts2);
if (ts1.isEmpty()) {
return false;
}
return true;
}
/**
* Whether the first argument is a subtype of the second.
*
* @param child The candidate subtype
* @param parent The supertype
* @return true if child is subtype of parent, otherwise false
*/
public static boolean subtypeOf(final Class child, final Class parent) {
if (child == null || parent == null) {
return false;
}
for (Class current = child; current != Object.class;
current = current.getSuperclass()) {
if (current == parent) {
return true;
}
}
return false;
}
/**
* Whether the class implements the interface.
*
* @param clas The candidate class
* @param interfayce The interface
* @return true if clas implements interfayce, otherwise false
*/
public static boolean implementationOf(final Class clas,
final Class interfayce) {
if (clas == null || interfayce == null || !interfayce.isInterface()) {
return false;
}
for (Class current = clas; current != Object.class;
current = current.getSuperclass()) {
Class[] implementedInterfaces = current.getInterfaces();
for (int i = 0; i < implementedInterfaces.length; i++) {
if (implementedInterfaces[i] == interfayce) {
return true;
}
}
}
return false;
}
/**
* Set node value, depending on its type, from a String.
*
* @param node A Node whose value is to be set
* @param value The new value
*/
public static void setNodeValue(final Node node, final String value) {
switch(node.getNodeType()) {
case Node.ATTRIBUTE_NODE:
node.setNodeValue(value);
break;
case Node.ELEMENT_NODE:
//remove all text children
if (node.hasChildNodes()) {
Node child = node.getFirstChild();
while (child != null) {
if (child.getNodeType() == Node.TEXT_NODE) {
node.removeChild(child);
}
child = child.getNextSibling();
}
}
//create a new text node and append
Text txt = node.getOwnerDocument().createTextNode(value);
node.appendChild(txt);
break;
case Node.TEXT_NODE:
case Node.CDATA_SECTION_NODE:
((CharacterData) node).setData(value);
break;
default:
String err = "Trying to set value of a strange Node type: "
+ node.getNodeType();
//Logger.logln(Logger.E, err);
throw new IllegalArgumentException(err);
}
}
/**
* Retrieve a DOM node value as a string depending on its type.
*
* @param node A node to be retreived
* @return The value as a string
*/
public static String getNodeValue(final Node node) {
String result = "";
if (node == null) {
return result;
}
switch(node.getNodeType()) {
case Node.ATTRIBUTE_NODE:
result = node.getNodeValue();
break;
case Node.ELEMENT_NODE:
if (node.hasChildNodes()) {
Node child = node.getFirstChild();
StringBuffer buf = new StringBuffer();
while (child != null) {
if (child.getNodeType() == Node.TEXT_NODE) {
buf.append(((CharacterData) child).getData());
}
child = child.getNextSibling();
}
result = buf.toString();
}
break;
case Node.TEXT_NODE:
case Node.CDATA_SECTION_NODE:
result = ((CharacterData) node).getData();
break;
default:
String err = "Trying to get value of a strange Node type: "
+ node.getNodeType();
//Logger.logln(Logger.W, err );
throw new IllegalArgumentException(err);
}
return result.trim();
}
/**
* Clone data model.
*
* @param ctx The context to clone to.
* @param datamodel The datamodel to clone.
* @param evaluator The expression evaluator.
* @param log The error log.
*/
public static void cloneDatamodel(final Datamodel datamodel,
final Context ctx, final Evaluator evaluator,
final Log log) {
if (datamodel == null) {
return;
}
List data = datamodel.getData();
if (data == null) {
return;
}
for (Iterator iter = data.iterator(); iter.hasNext();) {
Data datum = (Data) iter.next();
Node datumNode = datum.getNode();
Node valueNode = null;
if (datumNode != null) {
valueNode = datumNode.cloneNode(true);
}
// prefer "src" over "expr" over "inline"
if (!SCXMLHelper.isStringEmpty(datum.getSrc())) {
ctx.setLocal(datum.getName(), valueNode);
} else if (!SCXMLHelper.isStringEmpty(datum.
getExpr())) {
Object value = null;
try {
ctx.setLocal(NAMESPACES_KEY, datum.getNamespaces());
value = evaluator.eval(ctx, datum.getExpr());
ctx.setLocal(NAMESPACES_KEY, null);
} catch (SCXMLExpressionException see) {
if (log != null) {
log.error(see.getMessage(), see);
} else {
Log defaultLog = LogFactory.getLog(SCXMLHelper.class);
defaultLog.error(see.getMessage(), see);
}
}
ctx.setLocal(datum.getName(), value);
} else {
ctx.setLocal(datum.getName(), valueNode);
}
}
}
/**
* Discourage instantiation since this is a utility class.
*/
private SCXMLHelper() {
super();
}
/**
* Current document namespaces are saved under this key in the parent
* state's context.
*/
private static final String NAMESPACES_KEY = "_ALL_NAMESPACES";
}