Coverage Report - org.apache.commons.scxml.io.ModelUpdater

Classes in this File Line Coverage Branch Coverage Complexity
ModelUpdater
76% 
93% 
5.143

 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  51
        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  51
        State initialState = (State) scxml.getTargets().get(initialstate);
 65  51
        if (initialState == null) {
 66  
            // Where do we, where do we go?
 67  0
            logAndThrowModelError(ERR_SCXML_NO_INIT, new Object[] {
 68  
                initialstate });
 69  
        }
 70  51
        scxml.setInitialState(initialState);
 71  51
        Map targets = scxml.getTargets();
 72  51
        Map states = scxml.getStates();
 73  51
        Iterator i = states.keySet().iterator();
 74  187
        while (i.hasNext()) {
 75  136
            updateState((State) states.get(i.next()), targets);
 76  
        }
 77  51
    }
 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  245
         s.getOnEntry().setParent(s);
 92  245
         s.getOnExit().setParent(s);
 93  
         //initialize next / inital
 94  245
         Initial ini = s.getInitial();
 95  245
         Map c = s.getChildren();
 96  245
         TransitionTarget initialState = null;
 97  245
         if (!c.isEmpty()) {
 98  42
             if (ini == null) {
 99  0
                 logAndThrowModelError(ERR_STATE_NO_INIT,
 100  
                     new Object[] {getStateName(s)});
 101  
             }
 102  42
             Transition initialTransition = ini.getTransition();
 103  42
             updateTransition(initialTransition, targets);
 104  42
             initialState = initialTransition.getTarget();
 105  
             // we have to allow for an indirect descendant initial (targets)
 106  
             //check that initialState is a descendant of s
 107  42
             if (initialState == null
 108  
                     || !SCXMLHelper.isDescendant(initialState, s)) {
 109  0
                 logAndThrowModelError(ERR_STATE_BAD_INIT,
 110  
                     new Object[] {getStateName(s)});
 111  
             }
 112  
         }
 113  245
         List histories = s.getHistory();
 114  245
         Iterator histIter = histories.iterator();
 115  250
         while (histIter.hasNext()) {
 116  5
             if (s.isSimple()) {
 117  0
                 logAndThrowModelError(ERR_HISTORY_SIMPLE_STATE,
 118  
                     new Object[] {getStateName(s)});
 119  
             }
 120  5
             History h = (History) histIter.next();
 121  5
             Transition historyTransition = h.getTransition();
 122  5
             if (historyTransition == null) {
 123  
                 // try to assign initial as default
 124  1
                 if (initialState != null
 125  
                         && !(initialState instanceof History)) {
 126  1
                     historyTransition = new Transition();
 127  1
                     historyTransition.setNext(initialState.getId());
 128  1
                     historyTransition.setParent(h);
 129  1
                     h.setTransition(historyTransition);
 130  
                 } else {
 131  0
                     logAndThrowModelError(ERR_HISTORY_NO_DEFAULT,
 132  
                         new Object[] {h.getId(), getStateName(s)});
 133  
                 }
 134  
             }
 135  5
             updateTransition(historyTransition, targets);
 136  5
             State historyState = (State) historyTransition.getTarget();
 137  5
             if (historyState == null) {
 138  0
                 logAndThrowModelError(ERR_STATE_NO_HIST,
 139  
                     new Object[] {getStateName(s)});
 140  
             }
 141  5
             if (!h.isDeep()) {
 142  3
                 if (!c.containsValue(historyState)) {
 143  0
                     logAndThrowModelError(ERR_STATE_BAD_SHALLOW_HIST,
 144  
                         new Object[] {getStateName(s)});
 145  
                 }
 146  
             } else {
 147  2
                 if (!SCXMLHelper.isDescendant(historyState, s)) {
 148  0
                     logAndThrowModelError(ERR_STATE_BAD_DEEP_HIST,
 149  
                         new Object[] {getStateName(s)});
 150  
                 }
 151  
             }
 152  
         }
 153  245
         Map t = s.getTransitions();
 154  245
         Iterator i = t.keySet().iterator();
 155  437
         while (i.hasNext()) {
 156  192
             Iterator j = ((List) t.get(i.next())).iterator();
 157  389
             while (j.hasNext()) {
 158  197
                 Transition trn = (Transition) j.next();
 159  
                 //could add next two lines as a Digester rule for Transition
 160  197
                 trn.setParent(s);
 161  197
                 updateTransition(trn, targets);
 162  
             }
 163  
         }
 164  245
         Parallel p = s.getParallel();
 165  245
         Invoke inv = s.getInvoke();
 166  245
         if ((inv != null && p != null)
 167  
                 || (inv != null && !c.isEmpty())
 168  
                 || (p != null && !c.isEmpty())) {
 169  0
             logAndThrowModelError(ERR_STATE_BAD_CONTENTS,
 170  
                 new Object[] {getStateName(s)});
 171  
         }
 172  245
         if (p != null) {
 173  5
             updateParallel(p, targets);
 174  240
         } else if (inv != null) {
 175  1
             String ttype = inv.getTargettype();
 176  1
             if (ttype == null || ttype.trim().length() == 0) {
 177  0
                 logAndThrowModelError(ERR_INVOKE_NO_TARGETTYPE,
 178  
                     new Object[] {getStateName(s)});
 179  
             }
 180  1
             String src = inv.getSrc();
 181  1
             boolean noSrc = (src == null || src.trim().length() == 0);
 182  1
             String srcexpr = inv.getSrcexpr();
 183  1
             boolean noSrcexpr = (srcexpr == null
 184  
                                  || srcexpr.trim().length() == 0);
 185  1
             if (noSrc && noSrcexpr) {
 186  0
                 logAndThrowModelError(ERR_INVOKE_NO_SRC,
 187  
                     new Object[] {getStateName(s)});
 188  
             }
 189  1
             if (!noSrc && !noSrcexpr) {
 190  0
                 logAndThrowModelError(ERR_INVOKE_AMBIGUOUS_SRC,
 191  
                     new Object[] {getStateName(s)});
 192  
             }
 193  
         } else {
 194  239
             Iterator j = c.keySet().iterator();
 195  336
             while (j.hasNext()) {
 196  97
                 updateState((State) c.get(j.next()), targets);
 197  
             }
 198  
         }
 199  245
     }
 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  5
         Iterator i = p.getStates().iterator();
 211  17
         while (i.hasNext()) {
 212  12
             updateState((State) i.next(), targets);
 213  
         }
 214  5
     }
 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  244
         String next = t.getNext();
 226  244
         if (next == null) { // stay transition
 227  1
             return;
 228  
         }
 229  243
         TransitionTarget tt = t.getTarget();
 230  243
         if (tt == null) {
 231  243
             tt = (TransitionTarget) targets.get(next);
 232  243
             if (tt == null) {
 233  0
                 logAndThrowModelError(ERR_TARGET_NOT_FOUND, new Object[] {
 234  
                     next });
 235  
             }
 236  243
             t.setTarget(tt);
 237  
         }
 238  243
     }
 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  0
         MessageFormat msgFormat = new MessageFormat(errType);
 250  0
         String errMsg = msgFormat.format(msgArgs);
 251  0
         org.apache.commons.logging.Log log = LogFactory.
 252  0
             getLog(ModelUpdater.class);
 253  0
         log.error(errMsg);
 254  0
         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  0
         String badState = "anonymous state";
 267  0
         if (!SCXMLHelper.isStringEmpty(state.getId())) {
 268  0
             badState = "state with ID \"" + state.getId() + "\"";
 269  
         }
 270  0
         return badState;
 271  
     }
 272  
 
 273  
     /**
 274  
      * Discourage instantiation since this is a utility class.
 275  
      */
 276  
     private ModelUpdater() {
 277  0
         super();
 278  0
     }
 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