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

Classes in this File | Line Coverage | Branch Coverage | Complexity
ModelUpdater |
76% |
93% |
| 5.142857142857143;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 |  |