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.HashSet;
21  import java.util.Iterator;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.Set;
25  import java.util.StringTokenizer;
26  
27  import org.apache.commons.logging.LogFactory;
28  import org.apache.commons.scxml.SCXMLHelper;
29  import org.apache.commons.scxml.model.History;
30  import org.apache.commons.scxml.model.Initial;
31  import org.apache.commons.scxml.model.Invoke;
32  import org.apache.commons.scxml.model.ModelException;
33  import org.apache.commons.scxml.model.Parallel;
34  import org.apache.commons.scxml.model.SCXML;
35  import org.apache.commons.scxml.model.State;
36  import org.apache.commons.scxml.model.Transition;
37  import org.apache.commons.scxml.model.TransitionTarget;
38  
39  /**
40   * The ModelUpdater provides the utility methods to check the Commons
41   * SCXML model for inconsistencies, detect errors, and wire the Commons
42   * SCXML model appropriately post document parsing by the digester to make
43   * it executor ready.
44   */
45  final class ModelUpdater {
46  
47      /*
48       * Post-processing methods to make the SCXML object SCXMLExecutor ready.
49       */
50      /**
51       * <p>Update the SCXML object model and make it SCXMLExecutor ready.
52       * This is part of post-digester processing, and sets up the necessary
53       * object references throughtout the SCXML object model for the parsed
54       * document.</p>
55       *
56       * @param scxml The SCXML object (output from Digester)
57       * @throws ModelException If the object model is flawed
58       */
59     static void updateSCXML(final SCXML scxml) throws ModelException {
60         String initial = scxml.getInitial();
61         //we have to use getTargets() here since the initialTarget can be
62         //an indirect descendant
63         TransitionTarget initialTarget = (TransitionTarget) scxml.getTargets().
64             get(initial);
65         if (initialTarget == null) {
66             // Where do we, where do we go?
67             logAndThrowModelError(ERR_SCXML_NO_INIT, new Object[] {
68                 initial });
69         }
70         scxml.setInitialTarget(initialTarget);
71         Map targets = scxml.getTargets();
72         Map children = scxml.getChildren();
73         Iterator i = children.keySet().iterator();
74         while (i.hasNext()) {
75             TransitionTarget tt = (TransitionTarget) children.get(i.next());
76             if (tt instanceof State) {
77                 updateState((State) tt, targets);
78             } else {
79                 updateParallel((Parallel) tt, targets);
80             }
81         }
82     }
83  
84      /**
85        * Update this State object (part of post-digestion processing).
86        * Also checks for any errors in the document.
87        *
88        * @param s The State object
89        * @param targets The global Map of all transition targets
90        * @throws ModelException If the object model is flawed
91        */
92      private static void updateState(final State s, final Map targets)
93      throws ModelException {
94          //initialize next / inital
95          Initial ini = s.getInitial();
96          Map c = s.getChildren();
97          List initialStates = 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             initialStates = initialTransition.getTargets();
106             // we have to allow for an indirect descendant initial (targets)
107             //check that initialState is a descendant of s
108             if (initialStates.size() == 0) {
109                 logAndThrowModelError(ERR_STATE_BAD_INIT,
110                     new Object[] {getStateName(s)});
111             } else {
112                 for (int i = 0; i < initialStates.size(); i++) {
113                     TransitionTarget initialState = (TransitionTarget)
114                         initialStates.get(i);
115                     if (!SCXMLHelper.isDescendant(initialState, s)) {
116                         logAndThrowModelError(ERR_STATE_BAD_INIT,
117                             new Object[] {getStateName(s)});
118                     }
119                 }
120             }
121         }
122         List histories = s.getHistory();
123         Iterator histIter = histories.iterator();
124         while (histIter.hasNext()) {
125             if (s.isSimple()) {
126                 logAndThrowModelError(ERR_HISTORY_SIMPLE_STATE,
127                     new Object[] {getStateName(s)});
128             }
129             History h = (History) histIter.next();
130             Transition historyTransition = h.getTransition();
131             if (historyTransition == null) {
132                 // try to assign initial as default
133                 if (initialStates != null && initialStates.size() > 0) {
134                     for (int i = 0; i < initialStates.size(); i++) {
135                         if (initialStates.get(i) instanceof History) {
136                             logAndThrowModelError(ERR_HISTORY_BAD_DEFAULT,
137                                 new Object[] {h.getId(), getStateName(s)});
138                         }
139                     }
140                     historyTransition = new Transition();
141                     historyTransition.getTargets().addAll(initialStates);
142                     h.setTransition(historyTransition);
143                 } else {
144                     logAndThrowModelError(ERR_HISTORY_NO_DEFAULT,
145                         new Object[] {h.getId(), getStateName(s)});
146                 }
147             }
148             updateTransition(historyTransition, targets);
149             List historyStates = historyTransition.getTargets();
150             if (historyStates.size() == 0) {
151                 logAndThrowModelError(ERR_STATE_NO_HIST,
152                     new Object[] {getStateName(s)});
153             }
154             for (int i = 0; i < historyStates.size(); i++) {
155                 TransitionTarget historyState = (TransitionTarget)
156                     historyStates.get(i);
157                 if (!h.isDeep()) {
158                     if (!c.containsValue(historyState)) {
159                         logAndThrowModelError(ERR_STATE_BAD_SHALLOW_HIST,
160                             new Object[] {getStateName(s)});
161                     }
162                 } else {
163                     if (!SCXMLHelper.isDescendant(historyState, s)) {
164                         logAndThrowModelError(ERR_STATE_BAD_DEEP_HIST,
165                             new Object[] {getStateName(s)});
166                     }
167                 }
168             }
169         }
170         List t = s.getTransitionsList();
171         for (int i = 0; i < t.size(); i++) {
172             Transition trn = (Transition) t.get(i);
173             updateTransition(trn, targets);
174         }
175         Parallel p = s.getParallel(); //TODO: Remove in v1.0
176         Invoke inv = s.getInvoke();
177         if ((inv != null && p != null)
178                 || (inv != null && !c.isEmpty())
179                 || (p != null && !c.isEmpty())) {
180             logAndThrowModelError(ERR_STATE_BAD_CONTENTS,
181                 new Object[] {getStateName(s)});
182         }
183         if (p != null) {
184             updateParallel(p, targets);
185         } else if (inv != null) {
186             String type = inv.getType();
187             if (type == null || type.trim().length() == 0) {
188                 logAndThrowModelError(ERR_INVOKE_NO_TYPE,
189                     new Object[] {getStateName(s)});
190             }
191             String src = inv.getSrc();
192             boolean noSrc = (src == null || src.trim().length() == 0);
193             String srcexpr = inv.getSrcexpr();
194             boolean noSrcexpr = (srcexpr == null
195                                  || srcexpr.trim().length() == 0);
196             if (noSrc && noSrcexpr) {
197                 logAndThrowModelError(ERR_INVOKE_NO_SRC,
198                     new Object[] {getStateName(s)});
199             }
200             if (!noSrc && !noSrcexpr) {
201                 logAndThrowModelError(ERR_INVOKE_AMBIGUOUS_SRC,
202                     new Object[] {getStateName(s)});
203             }
204         } else {
205             Iterator j = c.keySet().iterator();
206             while (j.hasNext()) {
207                 TransitionTarget tt = (TransitionTarget) c.get(j.next());
208                 if (tt instanceof State) {
209                     updateState((State) tt, targets);
210                 } else if (tt instanceof Parallel) {
211                     updateParallel((Parallel) tt, targets);
212                 }
213             }
214         }
215     }
216 
217     /**
218       * Update this Parallel object (part of post-digestion processing).
219       *
220       * @param p The Parallel 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 updateParallel(final Parallel p, final Map targets)
225     throws ModelException {
226         Iterator i = p.getChildren().iterator();
227         while (i.hasNext()) {
228             updateState((State) i.next(), targets);
229         }
230         Iterator j = p.getTransitionsList().iterator();
231         while (j.hasNext()) {
232             updateTransition((Transition) j.next(), targets);
233         }
234     }
235 
236     /**
237       * Update this Transition object (part of post-digestion processing).
238       *
239       * @param t The Transition object
240       * @param targets The global Map of all transition targets
241       * @throws ModelException If the object model is flawed
242       */
243     private static void updateTransition(final Transition t,
244             final Map targets) throws ModelException {
245         String next = t.getNext();
246         if (next == null) { // stay transition
247             return;
248         }
249         List tts = t.getTargets();
250         if (tts.size() == 0) {
251             // 'next' is a space separated list of transition target IDs
252             StringTokenizer ids = new StringTokenizer(next);
253             while (ids.hasMoreTokens()) {
254                 String id = ids.nextToken();
255                 TransitionTarget tt = (TransitionTarget) targets.get(id);
256                 if (tt == null) {
257                     logAndThrowModelError(ERR_TARGET_NOT_FOUND, new Object[] {
258                         id });
259                 }
260                 tts.add(tt);
261             }
262             if (tts.size() > 1) {
263                 boolean legal = verifyTransitionTargets(tts);
264                 if (!legal) {
265                     logAndThrowModelError(ERR_ILLEGAL_TARGETS, new Object[] {
266                             next });
267                 }
268             }
269         }
270         t.getPaths(); // init paths
271     }
272 
273     /**
274       * Log an error discovered in post-digestion processing.
275       *
276       * @param errType The type of error
277       * @param msgArgs The arguments for formatting the error message
278       * @throws ModelException The model error, always thrown.
279       */
280     private static void logAndThrowModelError(final String errType,
281             final Object[] msgArgs) throws ModelException {
282         MessageFormat msgFormat = new MessageFormat(errType);
283         String errMsg = msgFormat.format(msgArgs);
284         org.apache.commons.logging.Log log = LogFactory.
285             getLog(ModelUpdater.class);
286         log.error(errMsg);
287         throw new ModelException(errMsg);
288     }
289 
290     /**
291      * Get state identifier for error message. This method is only
292      * called to produce an appropriate log message in some error
293      * conditions.
294      *
295      * @param state The <code>State</code> object
296      * @return The state identifier for the error message
297      */
298     private static String getStateName(final State state) {
299         String badState = "anonymous state";
300         if (!SCXMLHelper.isStringEmpty(state.getId())) {
301             badState = "state with ID \"" + state.getId() + "\"";
302         }
303         return badState;
304     }
305 
306     /**
307      * If a transition has multiple targets, then they satisfy the following
308      * criteria.
309      * <ul>
310      *  <li>They must belong to the regions of the same parallel</li>
311      *  <li>All regions must be represented with exactly one target</li>
312      * </ul>
313      *
314      * @param tts The transition targets
315      * @return Whether this is a legal configuration
316      */
317     private static boolean verifyTransitionTargets(final List tts) {
318         if (tts.size() <= 1) { // No contention
319             return true;
320         }
321         TransitionTarget lca = SCXMLHelper.getLCA((TransitionTarget)
322             tts.get(0), (TransitionTarget) tts.get(1));
323         if (lca == null || !(lca instanceof Parallel)) {
324             return false; // Must have a Parallel LCA
325         }
326         Parallel p = (Parallel) lca;
327         Set regions = new HashSet();
328         for (int i = 0; i < tts.size(); i++) {
329             TransitionTarget tt = (TransitionTarget) tts.get(i);
330             while (tt.getParent() != p) {
331                 tt = tt.getParent();
332             }
333             if (!regions.add(tt)) {
334                 return false; // One per region
335             }
336         }
337         if (regions.size() != p.getChildren().size()) {
338             return false; // Must represent all regions
339         }
340         return true;
341     }
342 
343     /**
344      * Discourage instantiation since this is a utility class.
345      */
346     private ModelUpdater() {
347         super();
348     }
349 
350     //// Error messages
351     /**
352      * Error message when SCXML document specifies an illegal initial state.
353      */
354     private static final String ERR_SCXML_NO_INIT = "No SCXML child state "
355         + "with ID \"{0}\" found; illegal initialstate for SCXML document";
356 
357     /**
358      * Error message when a state element specifies an initial state which
359      * cannot be found.
360      */
361     private static final String ERR_STATE_NO_INIT = "No initial element "
362         + "available for {0}";
363 
364     /**
365      * Error message when a state element specifies an initial state which
366      * is not a direct descendent.
367      */
368     private static final String ERR_STATE_BAD_INIT = "Initial state "
369         + "null or not a descendant of {0}";
370 
371     /**
372      * Error message when a state element contains anything other than
373      * one &lt;parallel&gt;, one &lt;invoke&gt; or any number of
374      * &lt;state&gt; children.
375      */
376     private static final String ERR_STATE_BAD_CONTENTS = "{0} should "
377         + "contain either one <parallel>, one <invoke> or any number of "
378         + "<state> children.";
379 
380     /**
381      * Error message when a referenced history state cannot be found.
382      */
383     private static final String ERR_STATE_NO_HIST = "Referenced history state"
384         + " null for {0}";
385 
386     /**
387      * Error message when a shallow history state is not a child state.
388      */
389     private static final String ERR_STATE_BAD_SHALLOW_HIST = "History state"
390         + " for shallow history is not child for {0}";
391 
392     /**
393      * Error message when a deep history state is not a descendent state.
394      */
395     private static final String ERR_STATE_BAD_DEEP_HIST = "History state"
396         + " for deep history is not descendant for {0}";
397 
398     /**
399      * Transition target is not a legal IDREF (not found).
400      */
401     private static final String ERR_TARGET_NOT_FOUND =
402         "Transition target with ID \"{0}\" not found";
403 
404     /**
405      * Transition targets do not form a legal configuration.
406      */
407     private static final String ERR_ILLEGAL_TARGETS =
408         "Transition targets \"{0}\" do not satisfy the requirements for"
409         + " target regions belonging to a <parallel>";
410 
411     /**
412      * Simple states should not contain a history.
413      */
414     private static final String ERR_HISTORY_SIMPLE_STATE =
415         "Simple {0} contains history elements";
416 
417     /**
418      * History does not specify a default transition target.
419      */
420     private static final String ERR_HISTORY_NO_DEFAULT =
421         "No default target specified for history with ID \"{0}\""
422         + " belonging to {1}";
423 
424     /**
425      * History specifies a bad default transition target.
426      */
427     private static final String ERR_HISTORY_BAD_DEFAULT =
428         "Default target specified for history with ID \"{0}\""
429         + " belonging to \"{1}\" is also a history";
430 
431     /**
432      * Error message when an &lt;invoke&gt; does not specify a "type"
433      * attribute.
434      */
435     private static final String ERR_INVOKE_NO_TYPE = "{0} contains "
436         + "<invoke> with no \"type\" attribute specified.";
437 
438     /**
439      * Error message when an &lt;invoke&gt; does not specify a "src"
440      * or a "srcexpr" attribute.
441      */
442     private static final String ERR_INVOKE_NO_SRC = "{0} contains "
443         + "<invoke> without a \"src\" or \"srcexpr\" attribute specified.";
444 
445     /**
446      * Error message when an &lt;invoke&gt; specifies both "src" and "srcexpr"
447      * attributes.
448      */
449     private static final String ERR_INVOKE_AMBIGUOUS_SRC = "{0} contains "
450         + "<invoke> with both \"src\" and \"srcexpr\" attributes specified,"
451         + " must specify either one, but not both.";
452 
453 }