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