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.getChildren().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             parents.add(tmp);
    202         }
    203         tmp = tt2;
    204         while ((tmp = tmp.getParent()) != null) {
    205             //test redundant add = common ancestor
    206             if (!parents.add(tmp)) {
    207                 parents.clear();
    208                 return tmp;
    209             }
    210         }
    211         return null;
    212     }
    213
    214     /***
    215      * Returns the set of all states (and parallels) which are exited if a
    216      * given transition t is going to be taken.
    217      * Current states are necessary to be taken into account
    218      * due to orthogonal states and cross-region transitions -
    219      * see UML specs for more details.
    220      *
    221      * @param t
    222      *            transition to be taken
    223      * @param currentStates
    224      *            the set of current states (simple states only)
    225      * @return a set of all states (including composite) which are exited if a
    226      *         given transition is taken
    227      */
    228     public static Set getStatesExited(final Transition t,
    229             final Set currentStates) {
    230         Set allStates = new HashSet();
    231         if (t.getTargets().size() == 0) {
    232             return allStates;
    233         }
    234         Path p = (Path) t.getPaths().get(0); // all paths have same upseg
    235         //the easy part
    236         allStates.addAll(p.getUpwardSegment());
    237         TransitionTarget source = t.getParent();
    238         for (Iterator act = currentStates.iterator(); act.hasNext();) {
    239             TransitionTarget a = (TransitionTarget) act.next();
    240             if (isDescendant(a, source)) {
    241                 boolean added = false;
    242                 added = allStates.add(a);
    243                 while (added && a != source) {
    244                     a = a.getParent();
    245                     added = allStates.add(a);
    246                 }
    247             }
    248         }
    249         if (p.isCrossRegion()) {
    250             for (Iterator regions = p.getRegionsExited().iterator();
    251                     regions.hasNext();) {
    252                 Parallel par = ((Parallel) ((State) regions.next()).
    253                     getParent());
    254                 //let's find affected states in sibling regions
    255                 for (Iterator siblings = par.getChildren().iterator();
    256                         siblings.hasNext();) {
    257                     State s = (State) siblings.next();
    258                     for (Iterator act = currentStates.iterator();
    259                             act.hasNext();) {
    260                         TransitionTarget a = (TransitionTarget) act.next();
    261                         if (isDescendant(a, s)) {
    262                             //a is affected
    263                             boolean added = false;
    264                             added = allStates.add(a);
    265                             while (added && a != s) {
    266                                 a = a.getParent();
    267                                 added = allStates.add(a);
    268                             }
    269                         }
    270                     }
    271                 }
    272             }
    273         }
    274         return allStates;
    275     }
    276
    277     /***
    278      * According to the UML definition, two transitions
    279      * are conflicting if the sets of states they exit overlap.
    280      *
    281      * @param t1 a transition to check against t2
    282      * @param t2 a transition to check against t1
    283      * @param currentStates the set of current states (simple states only)
    284      * @return true if the t1 and t2 are conflicting transitions
    285      * @see #getStatesExited(Transition, Set)
    286      */
    287     public static boolean inConflict(final Transition t1,
    288             final Transition t2, final Set currentStates) {
    289         Set ts1 = getStatesExited(t1, currentStates);
    290         Set ts2 = getStatesExited(t2, currentStates);
    291         ts1.retainAll(ts2);
    292         if (ts1.isEmpty()) {
    293             return false;
    294         }
    295         return true;
    296     }
    297
    298     /***
    299      * Whether the first argument is a subtype of the second.
    300      *
    301      * @param child The candidate subtype
    302      * @param parent The supertype
    303      * @return true if child is subtype of parent, otherwise false
    304      */
    305     public static boolean subtypeOf(final Class child, final Class parent) {
    306         if (child == null || parent == null) {
    307             return false;
    308         }
    309         for (Class current = child; current != Object.class;
    310                 current = current.getSuperclass()) {
    311             if (current == parent) {
    312                 return true;
    313             }
    314         }
    315         return false;
    316     }
    317
    318     /***
    319      * Whether the class implements the interface.
    320      *
    321      * @param clas The candidate class
    322      * @param interfayce The interface
    323      * @return true if clas implements interfayce, otherwise false
    324      */
    325     public static boolean implementationOf(final Class clas,
    326             final Class interfayce) {
    327         if (clas == null || interfayce == null || !interfayce.isInterface()) {
    328             return false;
    329         }
    330         for (Class current = clas; current != Object.class;
    331                 current = current.getSuperclass()) {
    332             Class[] implementedInterfaces = current.getInterfaces();
    333             for (int i = 0; i < implementedInterfaces.length; i++) {
    334                 if (implementedInterfaces[i] == interfayce) {
    335                     return true;
    336                 }
    337             }
    338         }
    339         return false;
    340     }
    341
    342     /***
    343      * Set node value, depending on its type, from a String.
    344      *
    345      * @param node A Node whose value is to be set
    346      * @param value The new value
    347      */
    348     public static void setNodeValue(final Node node, final String value) {
    349         switch(node.getNodeType()) {
    350             case Node.ATTRIBUTE_NODE:
    351                 node.setNodeValue(value);
    352                 break;
    353             case Node.ELEMENT_NODE:
    354                 //remove all text children
    355                 if (node.hasChildNodes()) {
    356                     Node child = node.getFirstChild();
    357                     while (child != null) {
    358                         if (child.getNodeType() == Node.TEXT_NODE) {
    359                             node.removeChild(child);
    360                         }
    361                         child = child.getNextSibling();
    362                     }
    363                 }
    364                 //create a new text node and append
    365                 Text txt = node.getOwnerDocument().createTextNode(value);
    366                 node.appendChild(txt);
    367                 break;
    368             case Node.TEXT_NODE:
    369             case Node.CDATA_SECTION_NODE:
    370                 ((CharacterData) node).setData(value);
    371                 break;
    372             default:
    373                 String err = "Trying to set value of a strange Node type: "
    374                     + node.getNodeType();
    375                 //Logger.logln(Logger.E, err);
    376                 throw new IllegalArgumentException(err);
    377         }
    378     }
    379
    380     /***
    381      * Retrieve a DOM node value as a string depending on its type.
    382      *
    383      * @param node A node to be retreived
    384      * @return The value as a string
    385      */
    386     public static String getNodeValue(final Node node) {
    387         String result = "";
    388         if (node == null) {
    389             return result;
    390         }
    391         switch(node.getNodeType()) {
    392             case Node.ATTRIBUTE_NODE:
    393                 result = node.getNodeValue();
    394                 break;
    395             case Node.ELEMENT_NODE:
    396                 if (node.hasChildNodes()) {
    397                     Node child = node.getFirstChild();
    398                     StringBuffer buf = new StringBuffer();
    399                     while (child != null) {
    400                         if (child.getNodeType() == Node.TEXT_NODE) {
    401                             buf.append(((CharacterData) child).getData());
    402                         }
    403                         child = child.getNextSibling();
    404                     }
    405                     result = buf.toString();
    406                 }
    407                 break;
    408             case Node.TEXT_NODE:
    409             case Node.CDATA_SECTION_NODE:
    410                 result = ((CharacterData) node).getData();
    411                 break;
    412             default:
    413                 String err = "Trying to get value of a strange Node type: "
    414                     + node.getNodeType();
    415                 //Logger.logln(Logger.W, err );
    416                 throw new IllegalArgumentException(err);
    417         }
    418         return result.trim();
    419     }
    420
    421     /***
    422      * Clone data model.
    423      *
    424      * @param ctx The context to clone to.
    425      * @param datamodel The datamodel to clone.
    426      * @param evaluator The expression evaluator.
    427      * @param log The error log.
    428      */
    429     public static void cloneDatamodel(final Datamodel datamodel,
    430             final Context ctx, final Evaluator evaluator,
    431             final Log log) {
    432         if (datamodel == null) {
    433             return;
    434         }
    435         List data = datamodel.getData();
    436         if (data == null) {
    437             return;
    438         }
    439         for (Iterator iter = data.iterator(); iter.hasNext();) {
    440             Data datum = (Data) iter.next();
    441             Node datumNode = datum.getNode();
    442             Node valueNode = null;
    443             if (datumNode != null) {
    444                 valueNode = datumNode.cloneNode(true);
    445             }
    446             // prefer "src" over "expr" over "inline"
    447             if (!SCXMLHelper.isStringEmpty(datum.getSrc())) {
    448                 ctx.setLocal(datum.getName(), valueNode);
    449             } else if (!SCXMLHelper.isStringEmpty(datum.
    450                     getExpr())) {
    451                 Object value = null;
    452                 try {
    453                     ctx.setLocal(NAMESPACES_KEY, datum.getNamespaces());
    454                     value = evaluator.eval(ctx, datum.getExpr());
    455                     ctx.setLocal(NAMESPACES_KEY, null);
    456                 } catch (SCXMLExpressionException see) {
    457                     if (log != null) {
    458                         log.error(see.getMessage(), see);
    459                     } else {
    460                         Log defaultLog = LogFactory.getLog(SCXMLHelper.class);
    461                         defaultLog.error(see.getMessage(), see);
    462                     }
    463                 }
    464                 ctx.setLocal(datum.getName(), value);
    465             } else {
    466                 ctx.setLocal(datum.getName(), valueNode);
    467             }
    468         }
    469     }
    470
    471     /***
    472      * Discourage instantiation since this is a utility class.
    473      */
    474     private SCXMLHelper() {
    475         super();
    476     }
    477
    478     /***
    479      * Current document namespaces are saved under this key in the parent
    480      * state's context.
    481      */
    482     private static final String NAMESPACES_KEY = "_ALL_NAMESPACES";
    483
    484 }
    485