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