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