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: */
017: package org.apache.commons.scxml;
018:
019: import java.util.HashSet;
020: import java.util.IdentityHashMap;
021: import java.util.Iterator;
022: import java.util.List;
023: import java.util.Map;
024: import java.util.Set;
025:
026: import org.apache.commons.logging.Log;
027: import org.apache.commons.logging.LogFactory;
028: import org.apache.commons.scxml.model.Data;
029: import org.apache.commons.scxml.model.Datamodel;
030: import org.apache.commons.scxml.model.Parallel;
031: import org.apache.commons.scxml.model.Path;
032: import org.apache.commons.scxml.model.State;
033: import org.apache.commons.scxml.model.Transition;
034: import org.apache.commons.scxml.model.TransitionTarget;
035: import org.apache.commons.scxml.semantics.ErrorConstants;
036: import org.w3c.dom.CharacterData;
037: import org.w3c.dom.Node;
038: import org.w3c.dom.Text;
039:
040: /**
041: * Helper class, all methods static final.
042: *
043: */
044: public final class SCXMLHelper {
045:
046: /**
047: * Return true if the string is empty.
048: *
049: * @param attr The String to test
050: * @return Is string empty
051: */
052: public static boolean isStringEmpty(final String attr) {
053: if (attr == null || attr.trim().length() == 0) {
054: return true;
055: }
056: return false;
057: }
058:
059: /**
060: * Checks whether a transition target tt (State or Parallel) is a
061: * descendant of the transition target context.
062: *
063: * @param tt
064: * TransitionTarget to check - a potential descendant
065: * @param ctx
066: * TransitionTarget context - a potential ancestor
067: * @return true iff tt is a descendant of ctx, false otherwise
068: */
069: public static boolean isDescendant(final TransitionTarget tt,
070: final TransitionTarget ctx) {
071: TransitionTarget parent = tt.getParent();
072: while (parent != null) {
073: if (parent == ctx) {
074: return true;
075: }
076: parent = parent.getParent();
077: }
078: return false;
079: }
080:
081: /**
082: * Creates a set which contains given states and all their ancestors
083: * recursively up to the upper bound. Null upperBound means root
084: * of the state machine.
085: *
086: * @param states The Set of States
087: * @param upperBounds The Set of upper bound States
088: * @return transitive closure of a given state set
089: */
090: public static Set getAncestorClosure(final Set states,
091: final Set upperBounds) {
092: Set closure = new HashSet(states.size() * 2);
093: for (Iterator i = states.iterator(); i.hasNext();) {
094: TransitionTarget tt = (TransitionTarget) i.next();
095: while (tt != null) {
096: if (!closure.add(tt)) {
097: //tt is already a part of the closure
098: break;
099: }
100: if (upperBounds != null && upperBounds.contains(tt)) {
101: break;
102: }
103: tt = tt.getParent();
104: }
105: }
106: return closure;
107: }
108:
109: /**
110: * Checks whether a given set of states is a legal Harel State Table
111: * configuration (with the respect to the definition of the OR and AND
112: * states).
113: *
114: * @param states
115: * a set of states
116: * @param errRep
117: * ErrorReporter to report detailed error info if needed
118: * @return true if a given state configuration is legal, false otherwise
119: */
120: public static boolean isLegalConfig(final Set states,
121: final ErrorReporter errRep) {
122: /*
123: * For every active state we add 1 to the count of its parent. Each
124: * Parallel should reach count equal to the number of its children and
125: * contribute by 1 to its parent. Each State should reach count exactly
126: * 1. SCXML elemnt (top) should reach count exactly 1. We essentially
127: * summarize up the hierarchy tree starting with a given set of
128: * states = active configuration.
129: */
130: boolean legalConfig = true; // let's be optimists
131: Map counts = new IdentityHashMap();
132: Set scxmlCount = new HashSet();
133: for (Iterator i = states.iterator(); i.hasNext();) {
134: TransitionTarget tt = (TransitionTarget) i.next();
135: TransitionTarget parent = null;
136: while ((parent = tt.getParent()) != null) {
137: HashSet cnt = (HashSet) counts.get(parent);
138: if (cnt == null) {
139: cnt = new HashSet();
140: counts.put(parent, cnt);
141: }
142: cnt.add(tt);
143: tt = parent;
144: }
145: //top-level contribution
146: scxmlCount.add(tt);
147: }
148: //Validate counts:
149: for (Iterator i = counts.entrySet().iterator(); i.hasNext();) {
150: Map.Entry entry = (Map.Entry) i.next();
151: TransitionTarget tt = (TransitionTarget) entry.getKey();
152: Set count = (Set) entry.getValue();
153: if (tt instanceof Parallel) {
154: Parallel p = (Parallel) tt;
155: if (count.size() < p.getStates().size()) {
156: errRep.onError(ErrorConstants.ILLEGAL_CONFIG,
157: "Not all AND states active for parallel "
158: + p.getId(), entry);
159: legalConfig = false;
160: }
161: } else {
162: if (count.size() > 1) {
163: errRep.onError(ErrorConstants.ILLEGAL_CONFIG,
164: "Multiple OR states active for state "
165: + tt.getId(), entry);
166: legalConfig = false;
167: }
168: }
169: count.clear(); //cleanup
170: }
171: if (scxmlCount.size() > 1) {
172: errRep.onError(ErrorConstants.ILLEGAL_CONFIG,
173: "Multiple top-level OR states active!", scxmlCount);
174: }
175: //cleanup
176: scxmlCount.clear();
177: counts.clear();
178: return legalConfig;
179: }
180:
181: /**
182: * Finds the least common ancestor of transition targets tt1 and tt2 if
183: * one exists.
184: *
185: * @param tt1 First TransitionTarget
186: * @param tt2 Second TransitionTarget
187: * @return closest common ancestor of tt1 and tt2 or null
188: */
189: public static TransitionTarget getLCA(final TransitionTarget tt1,
190: final TransitionTarget tt2) {
191: if (tt1 == tt2) {
192: return tt1; //self-transition
193: } else if (isDescendant(tt1, tt2)) {
194: return tt2;
195: } else if (isDescendant(tt2, tt1)) {
196: return tt1;
197: }
198: Set parents = new HashSet();
199: TransitionTarget tmp = tt1;
200: while ((tmp = tmp.getParent()) != null) {
201: if (tmp instanceof State) {
202: parents.add(tmp);
203: }
204: }
205: tmp = tt2;
206: while ((tmp = tmp.getParent()) != null) {
207: if (tmp instanceof State) {
208: //test redundant add = common ancestor
209: if (!parents.add(tmp)) {
210: parents.clear();
211: return tmp;
212: }
213: }
214: }
215: return null;
216: }
217:
218: /**
219: * Returns the set of all states (and parallels) which are exited if a
220: * given transition t is going to be taken.
221: * Current states are necessary to be taken into account
222: * due to orthogonal states and cross-region transitions -
223: * see UML specs for more details.
224: *
225: * @param t
226: * transition to be taken
227: * @param currentStates
228: * the set of current states (simple states only)
229: * @return a set of all states (including composite) which are exited if a
230: * given transition is taken
231: */
232: public static Set getStatesExited(final Transition t,
233: final Set currentStates) {
234: Set allStates = new HashSet();
235: Path p = t.getPath();
236: //the easy part
237: allStates.addAll(p.getUpwardSegment());
238: TransitionTarget source = t.getParent();
239: for (Iterator act = currentStates.iterator(); act.hasNext();) {
240: TransitionTarget a = (TransitionTarget) act.next();
241: if (isDescendant(a, source)) {
242: boolean added = false;
243: added = allStates.add(a);
244: while (added && a != source) {
245: a = a.getParent();
246: added = allStates.add(a);
247: }
248: }
249: }
250: if (p.isCrossRegion()) {
251: for (Iterator regions = p.getRegionsExited().iterator(); regions
252: .hasNext();) {
253: Parallel par = ((Parallel) ((State) regions.next())
254: .getParent());
255: //let's find affected states in sibling regions
256: for (Iterator siblings = par.getStates().iterator(); siblings
257: .hasNext();) {
258: State s = (State) siblings.next();
259: for (Iterator act = currentStates.iterator(); act
260: .hasNext();) {
261: TransitionTarget a = (TransitionTarget) act
262: .next();
263: if (isDescendant(a, s)) {
264: //a is affected
265: boolean added = false;
266: added = allStates.add(a);
267: while (added && a != s) {
268: a = a.getParent();
269: added = allStates.add(a);
270: }
271: }
272: }
273: }
274: }
275: }
276: return allStates;
277: }
278:
279: /**
280: * According to the UML definition, two transitions
281: * are conflicting if the sets of states they exit overlap.
282: *
283: * @param t1 a transition to check against t2
284: * @param t2 a transition to check against t1
285: * @param currentStates the set of current states (simple states only)
286: * @return true if the t1 and t2 are conflicting transitions
287: * @see #getStatesExited(Transition, Set)
288: */
289: public static boolean inConflict(final Transition t1,
290: final Transition t2, final Set currentStates) {
291: Set ts1 = getStatesExited(t1, currentStates);
292: Set ts2 = getStatesExited(t2, currentStates);
293: ts1.retainAll(ts2);
294: if (ts1.isEmpty()) {
295: return false;
296: }
297: return true;
298: }
299:
300: /**
301: * Whether the first argument is a subtype of the second.
302: *
303: * @param child The candidate subtype
304: * @param parent The supertype
305: * @return true if child is subtype of parent, otherwise false
306: */
307: public static boolean subtypeOf(final Class child,
308: final Class parent) {
309: if (child == null || parent == null) {
310: return false;
311: }
312: for (Class current = child; current != Object.class; current = current
313: .getSuperclass()) {
314: if (current == parent) {
315: return true;
316: }
317: }
318: return false;
319: }
320:
321: /**
322: * Whether the class implements the interface.
323: *
324: * @param clas The candidate class
325: * @param interfayce The interface
326: * @return true if clas implements interfayce, otherwise false
327: */
328: public static boolean implementationOf(final Class clas,
329: final Class interfayce) {
330: if (clas == null || interfayce == null
331: || !interfayce.isInterface()) {
332: return false;
333: }
334: for (Class current = clas; current != Object.class; current = current
335: .getSuperclass()) {
336: Class[] implementedInterfaces = current.getInterfaces();
337: for (int i = 0; i < implementedInterfaces.length; i++) {
338: if (implementedInterfaces[i] == interfayce) {
339: return true;
340: }
341: }
342: }
343: return false;
344: }
345:
346: /**
347: * Set node value, depending on its type, from a String.
348: *
349: * @param node A Node whose value is to be set
350: * @param value The new value
351: */
352: public static void setNodeValue(final Node node, final String value) {
353: switch (node.getNodeType()) {
354: case Node.ATTRIBUTE_NODE:
355: node.setNodeValue(value);
356: break;
357: case Node.ELEMENT_NODE:
358: //remove all text children
359: if (node.hasChildNodes()) {
360: Node child = node.getFirstChild();
361: while (child != null) {
362: if (child.getNodeType() == Node.TEXT_NODE) {
363: node.removeChild(child);
364: }
365: child = child.getNextSibling();
366: }
367: }
368: //create a new text node and append
369: Text txt = node.getOwnerDocument().createTextNode(value);
370: node.appendChild(txt);
371: break;
372: case Node.TEXT_NODE:
373: case Node.CDATA_SECTION_NODE:
374: ((CharacterData) node).setData(value);
375: break;
376: default:
377: String err = "Trying to set value of a strange Node type: "
378: + node.getNodeType();
379: //Logger.logln(Logger.E, err);
380: throw new IllegalArgumentException(err);
381: }
382: }
383:
384: /**
385: * Retrieve a DOM node value as a string depending on its type.
386: *
387: * @param node A node to be retreived
388: * @return The value as a string
389: */
390: public static String getNodeValue(final Node node) {
391: String result = "";
392: if (node == null) {
393: return result;
394: }
395: switch (node.getNodeType()) {
396: case Node.ATTRIBUTE_NODE:
397: result = node.getNodeValue();
398: break;
399: case Node.ELEMENT_NODE:
400: if (node.hasChildNodes()) {
401: Node child = node.getFirstChild();
402: StringBuffer buf = new StringBuffer();
403: while (child != null) {
404: if (child.getNodeType() == Node.TEXT_NODE) {
405: buf.append(((CharacterData) child).getData());
406: }
407: child = child.getNextSibling();
408: }
409: result = buf.toString();
410: }
411: break;
412: case Node.TEXT_NODE:
413: case Node.CDATA_SECTION_NODE:
414: result = ((CharacterData) node).getData();
415: break;
416: default:
417: String err = "Trying to get value of a strange Node type: "
418: + node.getNodeType();
419: //Logger.logln(Logger.W, err );
420: throw new IllegalArgumentException(err);
421: }
422: return result.trim();
423: }
424:
425: /**
426: * Clone data model.
427: *
428: * @param ctx The context to clone to.
429: * @param datamodel The datamodel to clone.
430: * @param evaluator The expression evaluator.
431: * @param log The error log.
432: */
433: public static void cloneDatamodel(final Datamodel datamodel,
434: final Context ctx, final Evaluator evaluator, final Log log) {
435: if (datamodel == null) {
436: return;
437: }
438: List data = datamodel.getData();
439: if (data == null) {
440: return;
441: }
442: for (Iterator iter = data.iterator(); iter.hasNext();) {
443: Data datum = (Data) iter.next();
444: Node datumNode = datum.getNode();
445: Node valueNode = null;
446: if (datumNode != null) {
447: valueNode = datumNode.cloneNode(true);
448: }
449: // prefer "src" over "expr" over "inline"
450: if (!SCXMLHelper.isStringEmpty(datum.getSrc())) {
451: ctx.setLocal(datum.getName(), valueNode);
452: } else if (!SCXMLHelper.isStringEmpty(datum.getExpr())) {
453: Object value = null;
454: try {
455: ctx.setLocal(NAMESPACES_KEY, datum.getNamespaces());
456: value = evaluator.eval(ctx, datum.getExpr());
457: ctx.setLocal(NAMESPACES_KEY, null);
458: } catch (SCXMLExpressionException see) {
459: if (log != null) {
460: log.error(see.getMessage(), see);
461: } else {
462: Log defaultLog = LogFactory
463: .getLog(SCXMLHelper.class);
464: defaultLog.error(see.getMessage(), see);
465: }
466: }
467: ctx.setLocal(datum.getName(), value);
468: } else {
469: ctx.setLocal(datum.getName(), valueNode);
470: }
471: }
472: }
473:
474: /**
475: * Discourage instantiation since this is a utility class.
476: */
477: private SCXMLHelper() {
478: super ();
479: }
480:
481: /**
482: * Current document namespaces are saved under this key in the parent
483: * state's context.
484: */
485: private static final String NAMESPACES_KEY = "_ALL_NAMESPACES";
486:
487: }
|