View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.scxml;
18  
19  import java.util.HashSet;
20  import java.util.IdentityHashMap;
21  import java.util.Iterator;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.Set;
25  
26  import org.apache.commons.logging.Log;
27  import org.apache.commons.logging.LogFactory;
28  import org.apache.commons.scxml.model.Data;
29  import org.apache.commons.scxml.model.Datamodel;
30  import org.apache.commons.scxml.model.Parallel;
31  import org.apache.commons.scxml.model.Path;
32  import org.apache.commons.scxml.model.State;
33  import org.apache.commons.scxml.model.Transition;
34  import org.apache.commons.scxml.model.TransitionTarget;
35  import org.apache.commons.scxml.semantics.ErrorConstants;
36  import org.w3c.dom.CharacterData;
37  import org.w3c.dom.Node;
38  import org.w3c.dom.Text;
39  
40  /***
41   * Helper class, all methods static final.
42   *
43   */
44  public final class SCXMLHelper {
45  
46      /***
47       * Return true if the string is empty.
48       *
49       * @param attr The String to test
50       * @return Is string empty
51       */
52      public static boolean isStringEmpty(final String attr) {
53          if (attr == null || attr.trim().length() == 0) {
54              return true;
55          }
56          return false;
57      }
58  
59      /***
60       * Checks whether a transition target tt (State or Parallel) is a
61       * descendant of the transition target context.
62       *
63       * @param tt
64       *            TransitionTarget to check - a potential descendant
65       * @param ctx
66       *            TransitionTarget context - a potential ancestor
67       * @return true iff tt is a descendant of ctx, false otherwise
68       */
69      public static boolean isDescendant(final TransitionTarget tt,
70              final TransitionTarget ctx) {
71          TransitionTarget parent = tt.getParent();
72          while (parent != null) {
73              if (parent == ctx) {
74                  return true;
75              }
76              parent = parent.getParent();
77          }
78          return false;
79      }
80  
81      /***
82       * Creates a set which contains given states and all their ancestors
83       * recursively up to the upper bound. Null upperBound means root
84       * of the state machine.
85       *
86       * @param states The Set of States
87       * @param upperBounds The Set of upper bound States
88       * @return transitive closure of a given state set
89       */
90      public static Set getAncestorClosure(final Set states,
91              final Set upperBounds) {
92          Set closure = new HashSet(states.size() * 2);
93          for (Iterator i = states.iterator(); i.hasNext();) {
94              TransitionTarget tt = (TransitionTarget) i.next();
95              while (tt != null) {
96                  if (!closure.add(tt)) {
97                      //tt is already a part of the closure
98                      break;
99                  }
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();
252                     regions.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();
257                         siblings.hasNext();) {
258                     State s = (State) siblings.next();
259                     for (Iterator act = currentStates.iterator();
260                             act.hasNext();) {
261                         TransitionTarget a = (TransitionTarget) act.next();
262                         if (isDescendant(a, s)) {
263                             //a is affected
264                             boolean added = false;
265                             added = allStates.add(a);
266                             while (added && a != s) {
267                                 a = a.getParent();
268                                 added = allStates.add(a);
269                             }
270                         }
271                     }
272                 }
273             }
274         }
275         return allStates;
276     }
277 
278     /***
279      * According to the UML definition, two transitions
280      * are conflicting if the sets of states they exit overlap.
281      *
282      * @param t1 a transition to check against t2
283      * @param t2 a transition to check against t1
284      * @param currentStates the set of current states (simple states only)
285      * @return true if the t1 and t2 are conflicting transitions
286      * @see #getStatesExited(Transition, Set)
287      */
288     public static boolean inConflict(final Transition t1,
289             final Transition t2, final Set currentStates) {
290         Set ts1 = getStatesExited(t1, currentStates);
291         Set ts2 = getStatesExited(t2, currentStates);
292         ts1.retainAll(ts2);
293         if (ts1.isEmpty()) {
294             return false;
295         }
296         return true;
297     }
298 
299     /***
300      * Whether the first argument is a subtype of the second.
301      *
302      * @param child The candidate subtype
303      * @param parent The supertype
304      * @return true if child is subtype of parent, otherwise false
305      */
306     public static boolean subtypeOf(final Class child, final Class parent) {
307         if (child == null || parent == null) {
308             return false;
309         }
310         for (Class current = child; current != Object.class;
311                 current = current.getSuperclass()) {
312             if (current == parent) {
313                 return true;
314             }
315         }
316         return false;
317     }
318 
319     /***
320      * Whether the class implements the interface.
321      *
322      * @param clas The candidate class
323      * @param interfayce The interface
324      * @return true if clas implements interfayce, otherwise false
325      */
326     public static boolean implementationOf(final Class clas,
327             final Class interfayce) {
328         if (clas == null || interfayce == null || !interfayce.isInterface()) {
329             return false;
330         }
331         for (Class current = clas; current != Object.class;
332                 current = current.getSuperclass()) {
333             Class[] implementedInterfaces = current.getInterfaces();
334             for (int i = 0; i < implementedInterfaces.length; i++) {
335                 if (implementedInterfaces[i] == interfayce) {
336                     return true;
337                 }
338             }
339         }
340         return false;
341     }
342 
343     /***
344      * Set node value, depending on its type, from a String.
345      *
346      * @param node A Node whose value is to be set
347      * @param value The new value
348      */
349     public static void setNodeValue(final Node node, final String value) {
350         switch(node.getNodeType()) {
351             case Node.ATTRIBUTE_NODE:
352                 node.setNodeValue(value);
353                 break;
354             case Node.ELEMENT_NODE:
355                 //remove all text children
356                 if (node.hasChildNodes()) {
357                     Node child = node.getFirstChild();
358                     while (child != null) {
359                         if (child.getNodeType() == Node.TEXT_NODE) {
360                             node.removeChild(child);
361                         }
362                         child = child.getNextSibling();
363                     }
364                 }
365                 //create a new text node and append
366                 Text txt = node.getOwnerDocument().createTextNode(value);
367                 node.appendChild(txt);
368                 break;
369             case Node.TEXT_NODE:
370             case Node.CDATA_SECTION_NODE:
371                 ((CharacterData) node).setData(value);
372                 break;
373             default:
374                 String err = "Trying to set value of a strange Node type: "
375                     + node.getNodeType();
376                 //Logger.logln(Logger.E, err);
377                 throw new IllegalArgumentException(err);
378         }
379     }
380 
381     /***
382      * Retrieve a DOM node value as a string depending on its type.
383      *
384      * @param node A node to be retreived
385      * @return The value as a string
386      */
387     public static String getNodeValue(final Node node) {
388         String result = "";
389         if (node == null) {
390             return result;
391         }
392         switch(node.getNodeType()) {
393             case Node.ATTRIBUTE_NODE:
394                 result = node.getNodeValue();
395                 break;
396             case Node.ELEMENT_NODE:
397                 if (node.hasChildNodes()) {
398                     Node child = node.getFirstChild();
399                     StringBuffer buf = new StringBuffer();
400                     while (child != null) {
401                         if (child.getNodeType() == Node.TEXT_NODE) {
402                             buf.append(((CharacterData) child).getData());
403                         }
404                         child = child.getNextSibling();
405                     }
406                     result = buf.toString();
407                 }
408                 break;
409             case Node.TEXT_NODE:
410             case Node.CDATA_SECTION_NODE:
411                 result = ((CharacterData) node).getData();
412                 break;
413             default:
414                 String err = "Trying to get value of a strange Node type: "
415                     + node.getNodeType();
416                 //Logger.logln(Logger.W, err );
417                 throw new IllegalArgumentException(err);
418         }
419         return result.trim();
420     }
421 
422     /***
423      * Clone data model.
424      *
425      * @param ctx The context to clone to.
426      * @param datamodel The datamodel to clone.
427      * @param evaluator The expression evaluator.
428      * @param log The error log.
429      */
430     public static void cloneDatamodel(final Datamodel datamodel,
431             final Context ctx, final Evaluator evaluator,
432             final Log log) {
433         if (datamodel == null) {
434             return;
435         }
436         List data = datamodel.getData();
437         if (data == null) {
438             return;
439         }
440         for (Iterator iter = data.iterator(); iter.hasNext();) {
441             Data datum = (Data) iter.next();
442             Node datumNode = datum.getNode();
443             Node valueNode = null;
444             if (datumNode != null) {
445                 valueNode = datumNode.cloneNode(true);
446             }
447             // prefer "src" over "expr" over "inline"
448             if (!SCXMLHelper.isStringEmpty(datum.getSrc())) {
449                 ctx.setLocal(datum.getName(), valueNode);
450             } else if (!SCXMLHelper.isStringEmpty(datum.
451                     getExpr())) {
452                 Object value = null;
453                 try {
454                     ctx.setLocal(NAMESPACES_KEY, datum.getNamespaces());
455                     value = evaluator.eval(ctx, datum.getExpr());
456                     ctx.setLocal(NAMESPACES_KEY, null);
457                 } catch (SCXMLExpressionException see) {
458                     if (log != null) {
459                         log.error(see.getMessage(), see);
460                     } else {
461                         Log defaultLog = LogFactory.getLog(SCXMLHelper.class);
462                         defaultLog.error(see.getMessage(), see);
463                     }
464                 }
465                 ctx.setLocal(datum.getName(), value);
466             } else {
467                 ctx.setLocal(datum.getName(), valueNode);
468             }
469         }
470     }
471 
472     /***
473      * Discourage instantiation since this is a utility class.
474      */
475     private SCXMLHelper() {
476         super();
477     }
478 
479     /***
480      * Current document namespaces are saved under this key in the parent
481      * state's context.
482      */
483     private static final String NAMESPACES_KEY = "_ALL_NAMESPACES";
484 
485 }
486