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