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.io.StringWriter;
020 import java.util.HashSet;
021 import java.util.IdentityHashMap;
022 import java.util.Iterator;
023 import java.util.List;
024 import java.util.Map;
025 import java.util.Set;
026
027 import org.apache.commons.logging.Log;
028 import org.apache.commons.logging.LogFactory;
029 import org.apache.commons.scxml.model.Data;
030 import org.apache.commons.scxml.model.Datamodel;
031 import org.apache.commons.scxml.model.Parallel;
032 import org.apache.commons.scxml.model.Path;
033 import org.apache.commons.scxml.model.State;
034 import org.apache.commons.scxml.model.Transition;
035 import org.apache.commons.scxml.model.TransitionTarget;
036 import org.apache.commons.scxml.semantics.ErrorConstants;
037 import org.w3c.dom.CharacterData;
038 import org.w3c.dom.Node;
039 import org.w3c.dom.Text;
040
041 /**
042 * Helper class, all methods static final.
043 *
044 */
045 public final class SCXMLHelper {
046
047 /**
048 * Return true if the string is empty.
049 *
050 * @param attr The String to test
051 * @return Is string empty
052 */
053 public static boolean isStringEmpty(final String attr) {
054 if (attr == null || attr.trim().length() == 0) {
055 return true;
056 }
057 return false;
058 }
059
060 /**
061 * Checks whether a transition target tt (State or Parallel) is a
062 * descendant of the transition target context.
063 *
064 * @param tt
065 * TransitionTarget to check - a potential descendant
066 * @param ctx
067 * TransitionTarget context - a potential ancestor
068 * @return true iff tt is a descendant of ctx, false otherwise
069 */
070 public static boolean isDescendant(final TransitionTarget tt,
071 final TransitionTarget ctx) {
072 TransitionTarget parent = tt.getParent();
073 while (parent != null) {
074 if (parent == ctx) {
075 return true;
076 }
077 parent = parent.getParent();
078 }
079 return false;
080 }
081
082 /**
083 * Creates a set which contains given states and all their ancestors
084 * recursively up to the upper bound. Null upperBound means root
085 * of the state machine.
086 *
087 * @param states The Set of States
088 * @param upperBounds The Set of upper bound States
089 * @return transitive closure of a given state set
090 */
091 public static Set getAncestorClosure(final Set states,
092 final Set upperBounds) {
093 Set closure = new HashSet(states.size() * 2);
094 for (Iterator i = states.iterator(); i.hasNext();) {
095 TransitionTarget tt = (TransitionTarget) i.next();
096 while (tt != null) {
097 if (!closure.add(tt)) {
098 //tt is already a part of the closure
099 break;
100 }
101 if (upperBounds != null && upperBounds.contains(tt)) {
102 break;
103 }
104 tt = tt.getParent();
105 }
106 }
107 return closure;
108 }
109
110 /**
111 * Checks whether a given set of states is a legal Harel State Table
112 * configuration (with the respect to the definition of the OR and AND
113 * states).
114 *
115 * @param states
116 * a set of states
117 * @param errRep
118 * ErrorReporter to report detailed error info if needed
119 * @return true if a given state configuration is legal, false otherwise
120 */
121 public static boolean isLegalConfig(final Set states,
122 final ErrorReporter errRep) {
123 /*
124 * For every active state we add 1 to the count of its parent. Each
125 * Parallel should reach count equal to the number of its children and
126 * contribute by 1 to its parent. Each State should reach count exactly
127 * 1. SCXML elemnt (top) should reach count exactly 1. We essentially
128 * summarize up the hierarchy tree starting with a given set of
129 * states = active configuration.
130 */
131 boolean legalConfig = true; // let's be optimists
132 Map counts = new IdentityHashMap();
133 Set scxmlCount = new HashSet();
134 for (Iterator i = states.iterator(); i.hasNext();) {
135 TransitionTarget tt = (TransitionTarget) i.next();
136 TransitionTarget parent = null;
137 while ((parent = tt.getParent()) != null) {
138 HashSet cnt = (HashSet) counts.get(parent);
139 if (cnt == null) {
140 cnt = new HashSet();
141 counts.put(parent, cnt);
142 }
143 cnt.add(tt);
144 tt = parent;
145 }
146 //top-level contribution
147 scxmlCount.add(tt);
148 }
149 //Validate counts:
150 for (Iterator i = counts.entrySet().iterator(); i.hasNext();) {
151 Map.Entry entry = (Map.Entry) i.next();
152 TransitionTarget tt = (TransitionTarget) entry.getKey();
153 Set count = (Set) entry.getValue();
154 if (tt instanceof Parallel) {
155 Parallel p = (Parallel) tt;
156 if (count.size() < p.getChildren().size()) {
157 errRep.onError(ErrorConstants.ILLEGAL_CONFIG,
158 "Not all AND states active for parallel "
159 + p.getId(), entry);
160 legalConfig = false;
161 }
162 } else {
163 if (count.size() > 1) {
164 errRep.onError(ErrorConstants.ILLEGAL_CONFIG,
165 "Multiple OR states active for state "
166 + tt.getId(), entry);
167 legalConfig = false;
168 }
169 }
170 count.clear(); //cleanup
171 }
172 if (scxmlCount.size() > 1) {
173 errRep.onError(ErrorConstants.ILLEGAL_CONFIG,
174 "Multiple top-level OR states active!", scxmlCount);
175 }
176 //cleanup
177 scxmlCount.clear();
178 counts.clear();
179 return legalConfig;
180 }
181
182 /**
183 * Finds the least common ancestor of transition targets tt1 and tt2 if
184 * one exists.
185 *
186 * @param tt1 First TransitionTarget
187 * @param tt2 Second TransitionTarget
188 * @return closest common ancestor of tt1 and tt2 or null
189 */
190 public static TransitionTarget getLCA(final TransitionTarget tt1,
191 final TransitionTarget tt2) {
192 if (tt1 == tt2) {
193 return tt1; //self-transition
194 } else if (isDescendant(tt1, tt2)) {
195 return tt2;
196 } else if (isDescendant(tt2, tt1)) {
197 return tt1;
198 }
199 Set parents = new HashSet();
200 TransitionTarget tmp = tt1;
201 while ((tmp = tmp.getParent()) != null) {
202 parents.add(tmp);
203 }
204 tmp = tt2;
205 while ((tmp = tmp.getParent()) != null) {
206 //test redundant add = common ancestor
207 if (!parents.add(tmp)) {
208 parents.clear();
209 return tmp;
210 }
211 }
212 return null;
213 }
214
215 /**
216 * Returns the set of all states (and parallels) which are exited if a
217 * given transition t is going to be taken.
218 * Current states are necessary to be taken into account
219 * due to orthogonal states and cross-region transitions -
220 * see UML specs for more details.
221 *
222 * @param t
223 * transition to be taken
224 * @param currentStates
225 * the set of current states (simple states only)
226 * @return a set of all states (including composite) which are exited if a
227 * given transition is taken
228 */
229 public static Set getStatesExited(final Transition t,
230 final Set currentStates) {
231 Set allStates = new HashSet();
232 if (t.getTargets().size() == 0) {
233 return allStates;
234 }
235 Path p = (Path) t.getPaths().get(0); // all paths have same upseg
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.getChildren().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.getId(), 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.getId(), value);
466 } else {
467 ctx.setLocal(datum.getId(), valueNode);
468 }
469 }
470 }
471
472 /**
473 * Escape XML strings for serialization.
474 * The basic algorithm is taken from Commons Lang (see oacl.Entities.java)
475 *
476 * @param str A string to be escaped
477 * @return The escaped string
478 */
479 public static String escapeXML(final String str) {
480 if (str == null) {
481 return null;
482 }
483
484 // Make the writer an arbitrary bit larger than the source string
485 int len = str.length();
486 StringWriter stringWriter = new StringWriter(len + 8);
487
488 for (int i = 0; i < len; i++) {
489 char c = str.charAt(i);
490 String entityName = null; // Look for XML 1.0 predefined entities
491 switch (c) {
492 case '"':
493 entityName = "quot";
494 break;
495 case '&':
496 entityName = "amp";
497 break;
498 case '\'':
499 entityName = "apos";
500 break;
501 case '<':
502 entityName = "lt";
503 break;
504 case '>':
505 entityName = "gt";
506 break;
507 default:
508 }
509 if (entityName == null) {
510 if (c > 0x7F) {
511 stringWriter.write("");
512 stringWriter.write(Integer.toString(c));
513 stringWriter.write(';');
514 } else {
515 stringWriter.write(c);
516 }
517 } else {
518 stringWriter.write('&');
519 stringWriter.write(entityName);
520 stringWriter.write(';');
521 }
522 }
523
524 return stringWriter.toString();
525 }
526
527 /**
528 * Discourage instantiation since this is a utility class.
529 */
530 private SCXMLHelper() {
531 super();
532 }
533
534 /**
535 * Current document namespaces are saved under this key in the parent
536 * state's context.
537 */
538 private static final String NAMESPACES_KEY = "_ALL_NAMESPACES";
539
540 }
541