View Javadoc

    1   /*
    2    *
    3    *   Copyright 2005-2006 The Apache Software Foundation.
    4    *
    5    *  Licensed under the Apache License, Version 2.0 (the "License");
    6    *  you may not use this file except in compliance with the License.
    7    *  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   */
    18  package org.apache.commons.scxml.io;
    19
    20  import java.text.MessageFormat;
    21  import java.util.Iterator;
    22  import java.util.List;
    23  import java.util.Map;
    24
    25  import org.apache.commons.logging.LogFactory;
    26  import org.apache.commons.scxml.SCXMLHelper;
    27  import org.apache.commons.scxml.model.History;
    28  import org.apache.commons.scxml.model.Initial;
    29  import org.apache.commons.scxml.model.Invoke;
    30  import org.apache.commons.scxml.model.ModelException;
    31  import org.apache.commons.scxml.model.Parallel;
    32  import org.apache.commons.scxml.model.SCXML;
    33  import org.apache.commons.scxml.model.State;
    34  import org.apache.commons.scxml.model.Transition;
    35  import org.apache.commons.scxml.model.TransitionTarget;
    36
    37  /***
    38   * The ModelUpdater provides the utility methods to check the Commons
    39   * SCXML model for inconsistencies, detect errors, and wire the Commons
    40   * SCXML model appropriately post document parsing by the digester to make
    41   * it executor ready.
    42   */
    43  final class ModelUpdater {
    44
    45      /*
    46       * Post-processing methods to make the SCXML object SCXMLExecutor ready.
    47       */
    48      /***
    49       * <p>Update the SCXML object model and make it SCXMLExecutor ready.
    50       * This is part of post-digester processing, and sets up the necessary
    51       * object references throughtout the SCXML object model for the parsed
    52       * document.</p>
    53       *
    54       * @param scxml The SCXML object (output from Digester)
    55       * @throws ModelException If the object model is flawed
    56       */
    57     static void updateSCXML(final SCXML scxml) throws ModelException {
    58         // Watch case, slightly unfortunate naming ;-)
    59         String initialstate = scxml.getInitialstate();
    60         //we have to use getTargets() here since the initialState can be
    61         //an indirect descendant
    62         // Concern marked by one of the code reviewers: better type check,
    63         //            now ClassCastException happens for Parallel
    64         // Response: initial should be a State, for Parallel, it is implicit
    65         State initialState = (State) scxml.getTargets().get(initialstate);
    66         if (initialState == null) {
    67             // Where do we, where do we go?
    68             logAndThrowModelError(ERR_SCXML_NO_INIT, new Object[] {
    69                 initialstate });
    70         }
    71         scxml.setInitialState(initialState);
    72         Map targets = scxml.getTargets();
    73         Map states = scxml.getStates();
    74         Iterator i = states.keySet().iterator();
    75         while (i.hasNext()) {
    76             updateState((State) states.get(i.next()), targets);
    77         }
    78     }
    79
    80      /***
    81        * Update this State object (part of post-digestion processing).
    82        * Also checks for any errors in the document.
    83        *
    84        * @param s The State object
    85        * @param targets The global Map of all transition targets
    86        * @throws ModelException If the object model is flawed
    87        */
    88      private static void updateState(final State s, final Map targets)
    89      throws ModelException {
    90          //ensure both onEntry and onExit have parent
    91          //could add next two lines as a Digester rule for OnEntry/OnExit
    92          s.getOnEntry().setParent(s);
    93          s.getOnExit().setParent(s);
    94          //initialize next / inital
    95          Initial ini = s.getInitial();
    96          Map c = s.getChildren();
    97          TransitionTarget initialState = null;
    98          if (!c.isEmpty()) {
    99              if (ini == null) {
    100                 logAndThrowModelError(ERR_STATE_NO_INIT,
    101                     new Object[] {getStateName(s)});
    102             }
    103             Transition initialTransition = ini.getTransition();
    104             updateTransition(initialTransition, targets);
    105             initialState = initialTransition.getTarget();
    106             // we have to allow for an indirect descendant initial (targets)
    107             //check that initialState is a descendant of s
    108             if (initialState == null
    109                     || !SCXMLHelper.isDescendant(initialState, s)) {
    110                 logAndThrowModelError(ERR_STATE_BAD_INIT,
    111                     new Object[] {getStateName(s)});
    112             }
    113         }
    114         List histories = s.getHistory();
    115         Iterator histIter = histories.iterator();
    116         while (histIter.hasNext()) {
    117             if (s.isSimple()) {
    118                 logAndThrowModelError(ERR_HISTORY_SIMPLE_STATE,
    119                     new Object[] {getStateName(s)});
    120             }
    121             History h = (History) histIter.next();
    122             Transition historyTransition = h.getTransition();
    123             if (historyTransition == null) {
    124                 // try to assign initial as default
    125                 if (initialState != null
    126                         && !(initialState instanceof History)) {
    127                     historyTransition = new Transition();
    128                     historyTransition.setNext(initialState.getId());
    129                     historyTransition.setParent(h);
    130                     h.setTransition(historyTransition);
    131                 } else {
    132                     logAndThrowModelError(ERR_HISTORY_NO_DEFAULT,
    133                         new Object[] {h.getId(), getStateName(s)});
    134                 }
    135             }
    136             updateTransition(historyTransition, targets);
    137             State historyState = (State) historyTransition.getTarget();
    138             if (historyState == null) {
    139                 logAndThrowModelError(ERR_STATE_NO_HIST,
    140                     new Object[] {getStateName(s)});
    141             }
    142             if (!h.isDeep()) {
    143                 if (!c.containsValue(historyState)) {
    144                     logAndThrowModelError(ERR_STATE_BAD_SHALLOW_HIST,
    145                         new Object[] {getStateName(s)});
    146                 }
    147             } else {
    148                 if (!SCXMLHelper.isDescendant(historyState, s)) {
    149                     logAndThrowModelError(ERR_STATE_BAD_DEEP_HIST,
    150                         new Object[] {getStateName(s)});
    151                 }
    152             }
    153         }
    154         Map t = s.getTransitions();
    155         Iterator i = t.keySet().iterator();
    156         while (i.hasNext()) {
    157             Iterator j = ((List) t.get(i.next())).iterator();
    158             while (j.hasNext()) {
    159                 Transition trn = (Transition) j.next();
    160                 //could add next two lines as a Digester rule for Transition
    161                 trn.setParent(s);
    162                 updateTransition(trn, targets);
    163             }
    164         }
    165         Parallel p = s.getParallel();
    166         Invoke inv = s.getInvoke();
    167         if ((inv != null && p != null)
    168                 || (inv != null && !c.isEmpty())
    169                 || (p != null && !c.isEmpty())) {
    170             logAndThrowModelError(ERR_STATE_BAD_CONTENTS,
    171                 new Object[] {getStateName(s)});
    172         }
    173         if (p != null) {
    174             updateParallel(p, targets);
    175         } else if (inv != null) {
    176             String ttype = inv.getTargettype();
    177             if (ttype == null || ttype.trim().length() == 0) {
    178                 logAndThrowModelError(ERR_INVOKE_NO_TARGETTYPE,
    179                     new Object[] {getStateName(s)});
    180             }
    181             String src = inv.getSrc();
    182             boolean noSrc = (src == null || src.trim().length() == 0);
    183             String srcexpr = inv.getSrcexpr();
    184             boolean noSrcexpr = (srcexpr == null
    185                                  || srcexpr.trim().length() == 0);
    186             if (noSrc && noSrcexpr) {
    187                 logAndThrowModelError(ERR_INVOKE_NO_SRC,
    188                     new Object[] {getStateName(s)});
    189             }
    190             if (!noSrc && !noSrcexpr) {
    191                 logAndThrowModelError(ERR_INVOKE_AMBIGUOUS_SRC,
    192                     new Object[] {getStateName(s)});
    193             }
    194         } else {
    195             Iterator j = c.keySet().iterator();
    196             while (j.hasNext()) {
    197                 updateState((State) c.get(j.next()), targets);
    198             }
    199         }
    200     }
    201
    202     /***
    203       * Update this Parallel object (part of post-digestion processing).
    204       *
    205       * @param p The Parallel object
    206       * @param targets The global Map of all transition targets
    207       * @throws ModelException If the object model is flawed
    208       */
    209     private static void updateParallel(final Parallel p, final Map targets)
    210     throws ModelException {
    211         Iterator i = p.getStates().iterator();
    212         while (i.hasNext()) {
    213             updateState((State) i.next(), targets);
    214         }
    215     }
    216
    217     /***
    218       * Update this Transition object (part of post-digestion processing).
    219       *
    220       * @param t The Transition object
    221       * @param targets The global Map of all transition targets
    222       * @throws ModelException If the object model is flawed
    223       */
    224     private static void updateTransition(final Transition t,
    225             final Map targets) throws ModelException {
    226         String next = t.getNext();
    227         TransitionTarget tt = t.getTarget();
    228         if (tt == null) {
    229             tt = (TransitionTarget) targets.get(next);
    230             if (tt == null) {
    231                 logAndThrowModelError(ERR_TARGET_NOT_FOUND, new Object[] {
    232                     next });
    233             }
    234             t.setTarget(tt);
    235         }
    236     }
    237
    238     /***
    239       * Log an error discovered in post-digestion processing.
    240       *
    241       * @param errType The type of error
    242       * @param msgArgs The arguments for formatting the error message
    243       * @throws ModelException The model error, always thrown.
    244       */
    245     private static void logAndThrowModelError(final String errType,
    246             final Object[] msgArgs) throws ModelException {
    247         MessageFormat msgFormat = new MessageFormat(errType);
    248         String errMsg = msgFormat.format(msgArgs);
    249         org.apache.commons.logging.Log log = LogFactory.
    250             getLog(ModelUpdater.class);
    251         log.error(errMsg);
    252         throw new ModelException(errMsg);
    253     }
    254
    255     /***
    256      * Get state identifier for error message. This method is only
    257      * called to produce an appropriate log message in some error
    258      * conditions.
    259      *
    260      * @param state The <code>State</code> object
    261      * @return The state identifier for the error message
    262      */
    263     private static String getStateName(final State state) {
    264         String badState = "anonymous state";
    265         if (!SCXMLHelper.isStringEmpty(state.getId())) {
    266             badState = "state with ID \"" + state.getId() + "\"";
    267         }
    268         return badState;
    269     }
    270
    271     /***
    272      * Discourage instantiation since this is a utility class.
    273      */
    274     private ModelUpdater() {
    275         super();
    276     }
    277
    278     //// Error messages
    279     /***
    280      * Error message when SCXML document specifies an illegal initial state.
    281      */
    282     private static final String ERR_SCXML_NO_INIT = "No SCXML child state "
    283         + "with ID \"{0}\" found; illegal initialstate for SCXML document";
    284
    285     /***
    286      * Error message when a state element specifies an initial state which
    287      * cannot be found.
    288      */
    289     private static final String ERR_STATE_NO_INIT = "No initial element "
    290         + "available for {0}";
    291
    292     /***
    293      * Error message when a state element specifies an initial state which
    294      * is not a direct descendent.
    295      */
    296     private static final String ERR_STATE_BAD_INIT = "Initial state "
    297         + "null or not a descendant of {0}";
    298
    299     /***
    300      * Error message when a state element contains anything other than
    301      * one &lt;parallel&gt;, one &lt;invoke&gt; or any number of
    302      * &lt;state&gt; children.
    303      */
    304     private static final String ERR_STATE_BAD_CONTENTS = "{0} should "
    305         + "contain either one <parallel>, one <invoke> or any number of "
    306         + "<state> children.";
    307
    308     /***
    309      * Error message when a referenced history state cannot be found.
    310      */
    311     private static final String ERR_STATE_NO_HIST = "Referenced history state"
    312         + " null for {0}";
    313
    314     /***
    315      * Error message when a shallow history state is not a child state.
    316      */
    317     private static final String ERR_STATE_BAD_SHALLOW_HIST = "History state"
    318         + " for shallow history is not child for {0}";
    319
    320     /***
    321      * Error message when a deep history state is not a descendent state.
    322      */
    323     private static final String ERR_STATE_BAD_DEEP_HIST = "History state"
    324         + " for deep history is not descendant for {0}";
    325
    326     /***
    327      * Transition target is not a legal IDREF (not found).
    328      */
    329     private static final String ERR_TARGET_NOT_FOUND =
    330         "Transition target with ID \"{0}\" not found";
    331
    332     /***
    333      * Simple states should not contain a history.
    334      */
    335     private static final String ERR_HISTORY_SIMPLE_STATE =
    336         "Simple {0} contains history elements";
    337
    338     /***
    339      * History does not specify a default transition target.
    340      */
    341     private static final String ERR_HISTORY_NO_DEFAULT =
    342         "No default target specified for history with ID \"{0}\""
    343         + " belonging to {1}";
    344
    345     /***
    346      * Error message when an &lt;invoke&gt; does not specify a "targettype"
    347      * attribute.
    348      */
    349     private static final String ERR_INVOKE_NO_TARGETTYPE = "{0} contains "
    350         + "<invoke> with no \"targettype\" attribute specified.";
    351
    352     /***
    353      * Error message when an &lt;invoke&gt; does not specify a "src"
    354      * or a "srcexpr" attribute.
    355      */
    356     private static final String ERR_INVOKE_NO_SRC = "{0} contains "
    357         + "<invoke> without a \"src\" or \"srcexpr\" attribute specified.";
    358
    359     /***
    360      * Error message when an &lt;invoke&gt; specifies both "src" and "srcexpr"
    361      * attributes.
    362      */
    363     private static final String ERR_INVOKE_AMBIGUOUS_SRC = "{0} contains "
    364         + "<invoke> with both \"src\" and \"srcexpr\" attributes specified,"
    365         + " must specify either one, but not both.";
    366
    367 }
    368