1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
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
61 String initialstate = scxml.getInitialstate();
62
63
64 TransitionTarget initialTarget = (TransitionTarget) scxml.getTargets().
65 get(initialstate);
66 if (initialTarget == null) {
67
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
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
108
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
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();
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) {
239 return;
240 }
241 List tts = t.getTargets();
242 if (tts.size() == 0) {
243
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) {
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;
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;
326 }
327 }
328 if (regions.size() != p.getChildren().size()) {
329 return false;
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
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 <parallel>, one <invoke> or any number of
365 * <state> 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 <invoke> 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 <invoke> 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 <invoke> 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