##### Coverage Report - org.apache.commons.scxml.SCXMLHelper

Classes in this File | Line Coverage | Branch Coverage | Complexity
SCXMLHelper |
74% |
82% |
| 7.076923076923077;7.077

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 | 1546 |
             if (attr == null || attr.trim().length() == 0) {
54 | 762 |
                 return true;
55 |  |
             }
56 | 784 |
             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 | 578 |
             TransitionTarget parent = tt.getParent();
72 | 904 |
             while (parent != null) {
73 | 493 |
                 if (parent == ctx) {
74 | 167 |
                     return true;
75 |  |
                 }
76 | 326 |
                 parent = parent.getParent();
77 |  |
             }
78 | 411 |
             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 | 906 |
             Set closure = new HashSet(states.size() * 2);
93 | 906 |
             for (Iterator i = states.iterator(); i.hasNext();) {
94 | 781 |
                 TransitionTarget tt = (TransitionTarget) i.next();
95 | 1916 |
                 while (tt != null) {
96 | 1270 |
                     if (!closure.add(tt)) {
97 |  |
                         //tt is already a part of the closure
98 | 29 |
                         break;
99 |  |
                     }
100 | 1241 |
                     if (upperBounds != null && upperBounds.contains(tt)) {
101 | 106 |
                         break;
102 |  |
                     }
103 | 1135 |
                     tt = tt.getParent();
104 |  |
                 }
105 |  |
             }
106 | 906 |
             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 | 270 |
             boolean legalConfig = true; // let's be optimists
131 | 270 |
             Map counts = new IdentityHashMap();
132 | 270 |
             Set scxmlCount = new HashSet();
133 | 270 |
             for (Iterator i = states.iterator(); i.hasNext();) {
134 | 283 |
                 TransitionTarget tt = (TransitionTarget) i.next();
135 | 283 |
                 TransitionTarget parent = null;
136 | 501 |
                 while ((parent = tt.getParent()) != null) {
137 | 218 |
                     HashSet cnt = (HashSet) counts.get(parent);
138 | 218 |
                     if (cnt == null) {
139 | 193 |
                         cnt = new HashSet();
140 | 193 |
                         counts.put(parent, cnt);
141 |  |
                     }
142 | 218 |
                     cnt.add(tt);
143 | 218 |
                     tt = parent;
144 |  |
                 }
145 |  |
                 //top-level contribution
146 | 283 |
                 scxmlCount.add(tt);
147 |  |
             }
148 |  |
             //Validate counts:
149 | 270 |
             for (Iterator i = counts.entrySet().iterator(); i.hasNext();) {
150 | 193 |
                 Map.Entry entry = (Map.Entry) i.next();
151 | 193 |
                 TransitionTarget tt = (TransitionTarget) entry.getKey();
152 | 193 |
                 Set count = (Set) entry.getValue();
153 | 193 |
                 if (tt instanceof Parallel) {
154 | 11 |
                     Parallel p = (Parallel) tt;
155 | 11 |
                     if (count.size() < p.getStates().size()) {
156 | 1 |
                         errRep.onError(ErrorConstants.ILLEGAL_CONFIG,
157 |  |
                             "Not all AND states active for parallel "
158 |  |
                             + p.getId(), entry);
159 | 1 |
                         legalConfig = false;
160 |  |
                     }
161 |  |
                 } else {
162 | 182 |
                     if (count.size() > 1) {
163 | 1 |
                         errRep.onError(ErrorConstants.ILLEGAL_CONFIG,
164 |  |
                             "Multiple OR states active for state "
165 |  |
                             + tt.getId(), entry);
166 | 1 |
                         legalConfig = false;
167 |  |
                     }
168 |  |
                 }
169 | 193 |
                 count.clear(); //cleanup
170 |  |
             }
171 | 270 |
             if (scxmlCount.size() > 1) {
172 | 1 |
                 errRep.onError(ErrorConstants.ILLEGAL_CONFIG,
173 |  |
                         "Multiple top-level OR states active!", scxmlCount);
174 |  |
             }
175 |  |
             //cleanup
176 | 270 |
             scxmlCount.clear();
177 | 270 |
             counts.clear();
178 | 270 |
             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 | 109 |
             if (tt1 == tt2) {
192 | 2 |
                 return tt1; //self-transition
193 | 107 |
             } else if (isDescendant(tt1, tt2)) {
194 | 2 |
                 return tt2;
195 | 105 |
             } else if (isDescendant(tt2, tt1)) {
196 | 3 |
                 return tt1;
197 |  |
             }
198 | 102 |
             Set parents = new HashSet();
199 | 102 |
             TransitionTarget tmp = tt1;
200 | 157 |
             while ((tmp = tmp.getParent()) != null) {
201 | 55 |
                 if (tmp instanceof State) {
202 | 47 |
                     parents.add(tmp);
203 |  |
                 }
204 |  |
             }
205 | 102 |
             tmp = tt2;
206 | 109 |
             while ((tmp = tmp.getParent()) != null) {
207 | 42 |
                 if (tmp instanceof State) {
208 |  |
                     //test redundant add = common ancestor
209 | 38 |
                     if (!parents.add(tmp)) {
210 | 35 |
                         parents.clear();
211 | 35 |
                         return tmp;
212 |  |
                     }
213 |  |
                 }
214 |  |
             }
215 | 67 |
             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 | 115 |
             Set allStates = new HashSet();
235 | 115 |
             Path p = t.getPath();
236 |  |
             //the easy part
237 | 115 |
             allStates.addAll(p.getUpwardSegment());
238 | 115 |
             TransitionTarget source = t.getParent();
239 | 115 |
             for (Iterator act = currentStates.iterator(); act.hasNext();) {
240 | 119 |
                 TransitionTarget a = (TransitionTarget) act.next();
241 | 119 |
                 if (isDescendant(a, source)) {
242 | 11 |
                     boolean added = false;
243 | 11 |
                     added = allStates.add(a);
244 | 27 |
                     while (added && a != source) {
245 | 16 |
                         a = a.getParent();
246 | 16 |
                         added = allStates.add(a);
247 |  |
                     }
248 |  |
                 }
249 |  |
             }
250 | 115 |
             if (p.isCrossRegion()) {
251 | 0 |
                 for (Iterator regions = p.getRegionsExited().iterator();
252 | 0 |
                         regions.hasNext();) {
253 | 0 |
                     Parallel par = ((Parallel) ((State) regions.next()).
254 |  |
                         getParent());
255 |  |
                     //let's find affected states in sibling regions
256 | 0 |
                     for (Iterator siblings = par.getStates().iterator();
257 | 0 |
                             siblings.hasNext();) {
258 | 0 |
                         State s = (State) siblings.next();
259 | 0 |
                         for (Iterator act = currentStates.iterator();
260 | 0 |
                                 act.hasNext();) {
261 | 0 |
                             TransitionTarget a = (TransitionTarget) act.next();
262 | 0 |
                             if (isDescendant(a, s)) {
263 |  |
                                 //a is affected
264 | 0 |
                                 boolean added = false;
265 | 0 |
                                 added = allStates.add(a);
266 | 0 |
                                 while (added && a != s) {
267 | 0 |
                                     a = a.getParent();
268 | 0 |
                                     added = allStates.add(a);
269 |  |
                                 }
270 |  |
                             }
271 |  |
                         }
272 |  |
                     }
273 |  |
                 }
274 |  |
             }
275 | 115 |
             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 | 0 |
             Set ts1 = getStatesExited(t1, currentStates);
291 | 0 |
             Set ts2 = getStatesExited(t2, currentStates);
292 | 0 |
             ts1.retainAll(ts2);
293 | 0 |
             if (ts1.isEmpty()) {
294 | 0 |
                 return false;
295 |  |
             }
296 | 0 |
             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 | 0 |
             if (child == null || parent == null) {
308 | 0 |
                 return false;
309 |  |
             }
310 | 1 |
             for (Class current = child; current != Object.class;
311 | 0 |
                     current = current.getSuperclass()) {
312 | 0 |
                 if (current == parent) {
313 | 0 |
                     return true;
314 |  |
                 }
315 |  |
             }
316 | 0 |
             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 | 25 |
             if (clas == null || interfayce == null || !interfayce.isInterface()) {
329 | 0 |
                 return false;
330 |  |
             }
331 | 25 |
             for (Class current = clas; current != Object.class;
332 | 50 |
                     current = current.getSuperclass()) {
333 | 50 |
                 Class[] implementedInterfaces = current.getInterfaces();
334 | 125 |
                 for (int i = 0; i < implementedInterfaces.length; i++) {
335 | 75 |
                     if (implementedInterfaces[i] == interfayce) {
336 | 0 |
                         return true;
337 |  |
                     }
338 |  |
                 }
339 |  |
             }
340 | 25 |
             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 | 8 |
             switch(node.getNodeType()) {
351 |  |
                 case Node.ATTRIBUTE_NODE:
352 | 0 |
                     node.setNodeValue(value);
353 | 0 |
                     break;
354 |  |
                 case Node.ELEMENT_NODE:
355 |  |
                     //remove all text children
356 | 8 |
                     if (node.hasChildNodes()) {
357 | 8 |
                         Node child = node.getFirstChild();
358 | 16 |
                         while (child != null) {
359 | 8 |
                             if (child.getNodeType() == Node.TEXT_NODE) {
360 | 8 |
                                 node.removeChild(child);
361 |  |
                             }
362 | 8 |
                             child = child.getNextSibling();
363 |  |
                         }
364 |  |
                     }
365 |  |
                     //create a new text node and append
366 | 8 |
                     Text txt = node.getOwnerDocument().createTextNode(value);
367 | 8 |
                     node.appendChild(txt);
368 | 8 |
                     break;
369 |  |
                 case Node.TEXT_NODE:
370 |  |
                 case Node.CDATA_SECTION_NODE:
371 | 0 |
                     ((CharacterData) node).setData(value);
372 | 0 |
                     break;
373 |  |
                 default:
374 | 0 |
                     String err = "Trying to set value of a strange Node type: "
375 |  |
                         + node.getNodeType();
376 |  |
                     //Logger.logln(Logger.E, err);
377 | 0 |
                     throw new IllegalArgumentException(err);
378 |  |
             }
379 | 8 |
         }
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 | 58 |
             String result = "";
389 | 58 |
             if (node == null) {
390 | 0 |
                 return result;
391 |  |
             }
392 | 58 |
             switch(node.getNodeType()) {
393 |  |
                 case Node.ATTRIBUTE_NODE:
394 | 17 |
                     result = node.getNodeValue();
395 | 17 |
                     break;
396 |  |
                 case Node.ELEMENT_NODE:
397 | 41 |
                     if (node.hasChildNodes()) {
398 | 41 |
                         Node child = node.getFirstChild();
399 | 41 |
                         StringBuffer buf = new StringBuffer();
400 | 82 |
                         while (child != null) {
401 | 41 |
                             if (child.getNodeType() == Node.TEXT_NODE) {
402 | 41 |
                                 buf.append(((CharacterData) child).getData());
403 |  |
                             }
404 | 41 |
                             child = child.getNextSibling();
405 |  |
                         }
406 | 41 |
                         result = buf.toString();
407 |  |
                     }
408 |  |
                     break;
409 |  |
                 case Node.TEXT_NODE:
410 |  |
                 case Node.CDATA_SECTION_NODE:
411 | 0 |
                     result = ((CharacterData) node).getData();
412 | 0 |
                     break;
413 |  |
                 default:
414 | 0 |
                     String err = "Trying to get value of a strange Node type: "
415 |  |
                         + node.getNodeType();
416 |  |
                     //Logger.logln(Logger.W, err );
417 | 0 |
                     throw new IllegalArgumentException(err);
418 |  |
             }
419 | 58 |
             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 | 242 |
             if (datamodel == null) {
434 | 209 |
                 return;
435 |  |
             }
436 | 33 |
             List data = datamodel.getData();
437 | 33 |
             if (data == null) {
438 | 0 |
                 return;
439 |  |
             }
440 | 33 |
             for (Iterator iter = data.iterator(); iter.hasNext();) {
441 | 48 |
                 Data datum = (Data) iter.next();
442 | 48 |
                 Node datumNode = datum.getNode();
443 | 48 |
                 Node valueNode = null;
444 | 48 |
                 if (datumNode != null) {
445 | 44 |
                     valueNode = datumNode.cloneNode(true);
446 |  |
                 }
447 |  |
                 // prefer "src" over "expr" over "inline"
448 | 48 |
                 if (!SCXMLHelper.isStringEmpty(datum.getSrc())) {
449 | 0 |
                     ctx.setLocal(datum.getName(), valueNode);
450 | 48 |
                 } else if (!SCXMLHelper.isStringEmpty(datum.
451 |  |
                         getExpr())) {
452 | 4 |
                     Object value = null;
453 |  |
                     try {
454 | 4 |
                         ctx.setLocal(NAMESPACES_KEY, datum.getNamespaces());
455 | 4 |
                         value = evaluator.eval(ctx, datum.getExpr());
456 | 4 |
                         ctx.setLocal(NAMESPACES_KEY, null);
457 | 0 |
                     } catch (SCXMLExpressionException see) {
458 | 0 |
                         if (log != null) {
459 | 0 |
                             log.error(see.getMessage(), see);
460 |  |
                         } else {
461 | 0 |
                             Log defaultLog = LogFactory.getLog(SCXMLHelper.class);
462 | 0 |
                             defaultLog.error(see.getMessage(), see);
463 |  |
                         }
464 | 4 |
                     }
465 | 4 |
                     ctx.setLocal(datum.getName(), value);
466 |  |
                 } else {
467 | 44 |
                     ctx.setLocal(datum.getName(), valueNode);
468 |  |
                 }
469 |  |
             }
470 | 33 |
         }
471 |  |
472 |  |
         /**
473 |  |
          * Discourage instantiation since this is a utility class.
474 |  |
          */
475 |  |
         private SCXMLHelper() {
476 | 0 |
             super();
477 | 0 |
         }
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 |  |