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