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.io;
    18
    19  import java.text.MessageFormat;
    20  import java.util.HashSet;
    21  import java.util.Iterator;
    22  import java.util.List;
    23  import java.util.Map;
    24  import java.util.Set;
    25  import java.util.StringTokenizer;
    26
    27  import org.apache.commons.logging.LogFactory;
    28  import org.apache.commons.scxml.SCXMLHelper;
    29  import org.apache.commons.scxml.model.History;
    30  import org.apache.commons.scxml.model.Initial;
    31  import org.apache.commons.scxml.model.Invoke;
    32  import org.apache.commons.scxml.model.ModelException;
    33  import org.apache.commons.scxml.model.Parallel;
    34  import org.apache.commons.scxml.model.SCXML;
    35  import org.apache.commons.scxml.model.State;
    36  import org.apache.commons.scxml.model.Transition;
    37  import org.apache.commons.scxml.model.TransitionTarget;
    38
    39  /***
    40   * The ModelUpdater provides the utility methods to check the Commons
    41   * SCXML model for inconsistencies, detect errors, and wire the Commons
    42   * SCXML model appropriately post document parsing by the digester to make
    43   * it executor ready.
    44   */
    45  final class ModelUpdater {
    46
    47      /*
    48       * Post-processing methods to make the SCXML object SCXMLExecutor ready.
    49       */
    50      /***
    51       * <p>Update the SCXML object model and make it SCXMLExecutor ready.
    52       * This is part of post-digester processing, and sets up the necessary
    53       * object references throughtout the SCXML object model for the parsed
    54       * document.</p>
    55       *
    56       * @param scxml The SCXML object (output from Digester)
    57       * @throws ModelException If the object model is flawed
    58       */
    59     static void updateSCXML(final SCXML scxml) throws ModelException {
    60         // Watch case, slightly unfortunate naming ;-)
    61         String initialstate = scxml.getInitialstate();
    62         //we have to use getTargets() here since the initialState can be
    63         //an indirect descendant
    64         TransitionTarget initialTarget = (TransitionTarget) scxml.getTargets().
    65             get(initialstate);
    66         if (initialTarget == null) {
    67             // Where do we, where do we go?
    68             logAndThrowModelError(ERR_SCXML_NO_INIT, new Object[] {
    69                 initialstate });
    70         }
    71         scxml.setInitialTarget(initialTarget);
    72         Map targets = scxml.getTargets();
    73         Map children = scxml.getChildren();
    74         Iterator i = children.keySet().iterator();
    75         while (i.hasNext()) {
    76             TransitionTarget tt = (TransitionTarget) children.get(i.next());
    77             if (tt instanceof State) {
    78                 updateState((State) tt, targets);
    79             } else {
    80                 updateParallel((Parallel) tt, targets);
    81             }
    82         }
    83     }
    84
    85      /***
    86        * Update this State object (part of post-digestion processing).
    87        * Also checks for any errors in the document.
    88        *
    89        * @param s The State object
    90        * @param targets The global Map of all transition targets
    91        * @throws ModelException If the object model is flawed
    92        */
    93      private static void updateState(final State s, final Map targets)
    94      throws ModelException {
    95          //initialize next / inital
    96          Initial ini = s.getInitial();
    97          Map c = s.getChildren();
    98          List initialStates = null;
    99          if (!c.isEmpty()) {
    100             if (ini == null) {
    101                 logAndThrowModelError(ERR_STATE_NO_INIT,
    102                     new Object[] {getStateName(s)});
    103             }
    104             Transition initialTransition = ini.getTransition();
    105             updateTransition(initialTransition, targets);
    106             initialStates = initialTransition.getTargets();
    107             // we have to allow for an indirect descendant initial (targets)
    108             //check that initialState is a descendant of s
    109             if (initialStates.size() == 0) {
    110                 logAndThrowModelError(ERR_STATE_BAD_INIT,
    111                     new Object[] {getStateName(s)});
    112             } else {
    113                 for (int i = 0; i < initialStates.size(); i++) {
    114                     TransitionTarget initialState = (TransitionTarget)
    115                         initialStates.get(i);
    116                     if (!SCXMLHelper.isDescendant(initialState, s)) {
    117                         logAndThrowModelError(ERR_STATE_BAD_INIT,
    118                             new Object[] {getStateName(s)});
    119                     }
    120                 }
    121             }
    122         }
    123         List histories = s.getHistory();
    124         Iterator histIter = histories.iterator();
    125         while (histIter.hasNext()) {
    126             if (s.isSimple()) {
    127                 logAndThrowModelError(ERR_HISTORY_SIMPLE_STATE,
    128                     new Object[] {getStateName(s)});
    129             }
    130             History h = (History) histIter.next();
    131             Transition historyTransition = h.getTransition();
    132             if (historyTransition == null) {
    133                 // try to assign initial as default
    134                 if (initialStates != null && initialStates.size() > 0) {
    135                     for (int i = 0; i < initialStates.size(); i++) {
    136                         if (initialStates.get(i) instanceof History) {
    137                             logAndThrowModelError(ERR_HISTORY_BAD_DEFAULT,
    138                                 new Object[] {h.getId(), getStateName(s)});
    139                         }
    140                     }
    141                     historyTransition = new Transition();
    142                     historyTransition.getTargets().addAll(initialStates);
    143                     h.setTransition(historyTransition);
    144                 } else {
    145                     logAndThrowModelError(ERR_HISTORY_NO_DEFAULT,
    146                         new Object[] {h.getId(), getStateName(s)});
    147                 }
    148             }
    149             updateTransition(historyTransition, targets);
    150             List historyStates = historyTransition.getTargets();
    151             if (historyStates.size() == 0) {
    152                 logAndThrowModelError(ERR_STATE_NO_HIST,
    153                     new Object[] {getStateName(s)});
    154             }
    155             for (int i = 0; i < historyStates.size(); i++) {
    156                 TransitionTarget historyState = (TransitionTarget)
    157                     historyStates.get(i);
    158                 if (!h.isDeep()) {
    159                     if (!c.containsValue(historyState)) {
    160                         logAndThrowModelError(ERR_STATE_BAD_SHALLOW_HIST,
    161                             new Object[] {getStateName(s)});
    162                     }
    163                 } else {
    164                     if (!SCXMLHelper.isDescendant(historyState, s)) {
    165                         logAndThrowModelError(ERR_STATE_BAD_DEEP_HIST,
    166                             new Object[] {getStateName(s)});
    167                     }
    168                 }
    169             }
    170         }
    171         List t = s.getTransitionsList();
    172         for (int i = 0; i < t.size(); i++) {
    173             Transition trn = (Transition) t.get(i);
    174             updateTransition(trn, targets);
    175         }
    176         Parallel p = s.getParallel(); //TODO: Remove in v1.0
    177         Invoke inv = s.getInvoke();
    178         if ((inv != null && p != null)
    179                 || (inv != null && !c.isEmpty())
    180                 || (p != null && !c.isEmpty())) {
    181             logAndThrowModelError(ERR_STATE_BAD_CONTENTS,
    182                 new Object[] {getStateName(s)});
    183         }
    184         if (p != null) {
    185             updateParallel(p, targets);
    186         } else if (inv != null) {
    187             String ttype = inv.getTargettype();
    188             if (ttype == null || ttype.trim().length() == 0) {
    189                 logAndThrowModelError(ERR_INVOKE_NO_TARGETTYPE,
    190                     new Object[] {getStateName(s)});
    191             }
    192             String src = inv.getSrc();
    193             boolean noSrc = (src == null || src.trim().length() == 0);
    194             String srcexpr = inv.getSrcexpr();
    195             boolean noSrcexpr = (srcexpr == null
    196                                  || srcexpr.trim().length() == 0);
    197             if (noSrc && noSrcexpr) {
    198                 logAndThrowModelError(ERR_INVOKE_NO_SRC,
    199                     new Object[] {getStateName(s)});
    200             }
    201             if (!noSrc && !noSrcexpr) {
    202                 logAndThrowModelError(ERR_INVOKE_AMBIGUOUS_SRC,
    203                     new Object[] {getStateName(s)});
    204             }
    205         } else {
    206             Iterator j = c.keySet().iterator();
    207             while (j.hasNext()) {
    208                 updateState((State) c.get(j.next()), targets);
    209             }
    210         }
    211     }
    212
    213     /***
    214       * Update this Parallel object (part of post-digestion processing).
    215       *
    216       * @param p The Parallel object
    217       * @param targets The global Map of all transition targets
    218       * @throws ModelException If the object model is flawed
    219       */
    220     private static void updateParallel(final Parallel p, final Map targets)
    221     throws ModelException {
    222         Iterator i = p.getChildren().iterator();
    223         while (i.hasNext()) {
    224             updateState((State) i.next(), targets);
    225         }
    226     }
    227
    228     /***
    229       * Update this Transition object (part of post-digestion processing).
    230       *
    231       * @param t The Transition object
    232       * @param targets The global Map of all transition targets
    233       * @throws ModelException If the object model is flawed
    234       */
    235     private static void updateTransition(final Transition t,
    236             final Map targets) throws ModelException {
    237         String next = t.getNext();
    238         if (next == null) { // stay transition
    239             return;
    240         }
    241         List tts = t.getTargets();
    242         if (tts.size() == 0) {
    243             // 'next' is a space separated list of transition target IDs
    244             StringTokenizer ids = new StringTokenizer(next);
    245             while (ids.hasMoreTokens()) {
    246                 String id = ids.nextToken();
    247                 TransitionTarget tt = (TransitionTarget) targets.get(id);
    248                 if (tt == null) {
    249                     logAndThrowModelError(ERR_TARGET_NOT_FOUND, new Object[] {
    250                         id });
    251                 }
    252                 tts.add(tt);
    253             }
    254             if (tts.size() > 1) {
    255                 boolean legal = verifyTransitionTargets(tts);
    256                 if (!legal) {
    257                     logAndThrowModelError(ERR_ILLEGAL_TARGETS, new Object[] {
    258                             next });
    259                 }
    260             }
    261         }
    262     }
    263
    264     /***
    265       * Log an error discovered in post-digestion processing.
    266       *
    267       * @param errType The type of error
    268       * @param msgArgs The arguments for formatting the error message
    269       * @throws ModelException The model error, always thrown.
    270       */
    271     private static void logAndThrowModelError(final String errType,
    272             final Object[] msgArgs) throws ModelException {
    273         MessageFormat msgFormat = new MessageFormat(errType);
    274         String errMsg = msgFormat.format(msgArgs);
    275         org.apache.commons.logging.Log log = LogFactory.
    276             getLog(ModelUpdater.class);
    277         log.error(errMsg);
    278         throw new ModelException(errMsg);
    279     }
    280
    281     /***
    282      * Get state identifier for error message. This method is only
    283      * called to produce an appropriate log message in some error
    284      * conditions.
    285      *
    286      * @param state The <code>State</code> object
    287      * @return The state identifier for the error message
    288      */
    289     private static String getStateName(final State state) {
    290         String badState = "anonymous state";
    291         if (!SCXMLHelper.isStringEmpty(state.getId())) {
    292             badState = "state with ID \"" + state.getId() + "\"";
    293         }
    294         return badState;
    295     }
    296
    297     /***
    298      * If a transition has multiple targets, then they satisfy the following
    299      * criteria.
    300      * <ul>
    301      *  <li>They must belong to the regions of the same parallel</li>
    302      *  <li>All regions must be represented with exactly one target</li>
    303      * </ul>
    304      *
    305      * @param tts The transition targets
    306      * @return Whether this is a legal configuration
    307      */
    308     private static boolean verifyTransitionTargets(final List tts) {
    309         if (tts.size() <= 1) { // No contention
    310             return true;
    311         }
    312         TransitionTarget lca = SCXMLHelper.getLCA((TransitionTarget)
    313             tts.get(0), (TransitionTarget) tts.get(1));
    314         if (lca == null || !(lca instanceof Parallel)) {
    315             return false; // Must have a Parallel LCA
    316         }
    317         Parallel p = (Parallel) lca;
    318         Set regions = new HashSet();
    319         for (int i = 0; i < tts.size(); i++) {
    320             TransitionTarget tt = (TransitionTarget) tts.get(i);
    321             while (tt.getParent() != p) {
    322                 tt = tt.getParent();
    323             }
    324             if (!regions.add(tt)) {
    325                 return false; // One per region
    326             }
    327         }
    328         if (regions.size() != p.getChildren().size()) {
    329             return false; // Must represent all regions
    330         }
    331         return true;
    332     }
    333
    334     /***
    335      * Discourage instantiation since this is a utility class.
    336      */
    337     private ModelUpdater() {
    338         super();
    339     }
    340
    341     //// Error messages
    342     /***
    343      * Error message when SCXML document specifies an illegal initial state.
    344      */
    345     private static final String ERR_SCXML_NO_INIT = "No SCXML child state "
    346         + "with ID \"{0}\" found; illegal initialstate for SCXML document";
    347
    348     /***
    349      * Error message when a state element specifies an initial state which
    350      * cannot be found.
    351      */
    352     private static final String ERR_STATE_NO_INIT = "No initial element "
    353         + "available for {0}";
    354
    355     /***
    356      * Error message when a state element specifies an initial state which
    357      * is not a direct descendent.
    358      */
    359     private static final String ERR_STATE_BAD_INIT = "Initial state "
    360         + "null or not a descendant of {0}";
    361
    362     /***
    363      * Error message when a state element contains anything other than
    364      * one &lt;parallel&gt;, one &lt;invoke&gt; or any number of
    365      * &lt;state&gt; children.
    366      */
    367     private static final String ERR_STATE_BAD_CONTENTS = "{0} should "
    368         + "contain either one <parallel>, one <invoke> or any number of "
    369         + "<state> children.";
    370
    371     /***
    372      * Error message when a referenced history state cannot be found.
    373      */
    374     private static final String ERR_STATE_NO_HIST = "Referenced history state"
    375         + " null for {0}";
    376
    377     /***
    378      * Error message when a shallow history state is not a child state.
    379      */
    380     private static final String ERR_STATE_BAD_SHALLOW_HIST = "History state"
    381         + " for shallow history is not child for {0}";
    382
    383     /***
    384      * Error message when a deep history state is not a descendent state.
    385      */
    386     private static final String ERR_STATE_BAD_DEEP_HIST = "History state"
    387         + " for deep history is not descendant for {0}";
    388
    389     /***
    390      * Transition target is not a legal IDREF (not found).
    391      */
    392     private static final String ERR_TARGET_NOT_FOUND =
    393         "Transition target with ID \"{0}\" not found";
    394
    395     /***
    396      * Transition targets do not form a legal configuration.
    397      */
    398     private static final String ERR_ILLEGAL_TARGETS =
    399         "Transition targets \"{0}\" do not satisfy the requirements for"
    400         + " target regions belonging to a <parallel>";
    401
    402     /***
    403      * Simple states should not contain a history.
    404      */
    405     private static final String ERR_HISTORY_SIMPLE_STATE =
    406         "Simple {0} contains history elements";
    407
    408     /***
    409      * History does not specify a default transition target.
    410      */
    411     private static final String ERR_HISTORY_NO_DEFAULT =
    412         "No default target specified for history with ID \"{0}\""
    413         + " belonging to {1}";
    414
    415     /***
    416      * History specifies a bad default transition target.
    417      */
    418     private static final String ERR_HISTORY_BAD_DEFAULT =
    419         "Default target specified for history with ID \"{0}\""
    420         + " belonging to \"{1}\" is also a history";
    421
    422     /***
    423      * Error message when an &lt;invoke&gt; does not specify a "targettype"
    424      * attribute.
    425      */
    426     private static final String ERR_INVOKE_NO_TARGETTYPE = "{0} contains "
    427         + "<invoke> with no \"targettype\" attribute specified.";
    428
    429     /***
    430      * Error message when an &lt;invoke&gt; does not specify a "src"
    431      * or a "srcexpr" attribute.
    432      */
    433     private static final String ERR_INVOKE_NO_SRC = "{0} contains "
    434         + "<invoke> without a \"src\" or \"srcexpr\" attribute specified.";
    435
    436     /***
    437      * Error message when an &lt;invoke&gt; specifies both "src" and "srcexpr"
    438      * attributes.
    439      */
    440     private static final String ERR_INVOKE_AMBIGUOUS_SRC = "{0} contains "
    441         + "<invoke> with both \"src\" and \"srcexpr\" attributes specified,"
    442         + " must specify either one, but not both.";
    443
    444 }
    445