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.scxml2.io;
18  
19  import java.text.MessageFormat;
20  import java.util.List;
21  import java.util.Map;
22  import java.util.Set;
23  import java.util.StringTokenizer;
24  
25  import org.apache.commons.logging.LogFactory;
26  import org.apache.commons.scxml2.model.EnterableState;
27  import org.apache.commons.scxml2.model.History;
28  import org.apache.commons.scxml2.model.Initial;
29  import org.apache.commons.scxml2.model.Invoke;
30  import org.apache.commons.scxml2.model.ModelException;
31  import org.apache.commons.scxml2.model.Parallel;
32  import org.apache.commons.scxml2.model.SCXML;
33  import org.apache.commons.scxml2.model.SimpleTransition;
34  import org.apache.commons.scxml2.model.State;
35  import org.apache.commons.scxml2.model.Transition;
36  import org.apache.commons.scxml2.model.TransitionTarget;
37  import org.apache.commons.scxml2.model.TransitionalState;
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 SCXMLReader to make
43   * it executor ready.
44   */
45  final class ModelUpdater {
46  
47      //// Error messages
48      /**
49       * Error message when SCXML document specifies an illegal initial state.
50       */
51      private static final String ERR_SCXML_NO_INIT = "No SCXML child state "
52              + "with ID \"{0}\" found; illegal initial state for SCXML document";
53  
54      /**
55       * Error message when SCXML document specifies an illegal initial state.
56       */
57      private static final String ERR_UNSUPPORTED_INIT = "Initial attribute or element not supported for "
58              + "atomic {0}";
59  
60      /**
61       * Error message when a state element specifies an initial state which
62       * is not a direct descendent.
63       */
64      private static final String ERR_STATE_BAD_INIT = "Initial state "
65              + "null or not a descendant of {0}";
66  
67      /**
68       * Error message when a referenced history state cannot be found.
69       */
70      private static final String ERR_STATE_NO_HIST = "Referenced history state"
71              + " null for {0}";
72  
73      /**
74       * Error message when a shallow history state is not a child state.
75       */
76      private static final String ERR_STATE_BAD_SHALLOW_HIST = "History state"
77              + " for shallow history is not child for {0}";
78  
79      /**
80       * Error message when a deep history state is not a descendent state.
81       */
82      private static final String ERR_STATE_BAD_DEEP_HIST = "History state"
83              + " for deep history is not descendant for {0}";
84  
85      /**
86       * Transition target is not a legal IDREF (not found).
87       */
88      private static final String ERR_TARGET_NOT_FOUND =
89              "Transition target with ID \"{0}\" not found";
90  
91      /**
92       * Transition targets do not form a legal configuration.
93       */
94      private static final String ERR_ILLEGAL_TARGETS =
95              "Transition targets \"{0}\" do not satisfy the requirements for"
96                      + " target regions belonging to a <parallel>";
97  
98      /**
99       * Simple states should not contain a history.
100      */
101     private static final String ERR_HISTORY_SIMPLE_STATE =
102             "Simple {0} contains history elements";
103 
104     /**
105      * History does not specify a default transition target.
106      */
107     private static final String ERR_HISTORY_NO_DEFAULT =
108             "No default target specified for history with ID \"{0}\""
109                     + " belonging to {1}";
110 
111     /**
112      * Error message when an &lt;invoke&gt; specifies both "src" and "srcexpr"
113      * attributes.
114      */
115     private static final String ERR_INVOKE_AMBIGUOUS_SRC = "{0} contains "
116             + "<invoke> with both \"src\" and \"srcexpr\" attributes specified,"
117             + " must specify either one, but not both.";
118 
119     /**
120      * Discourage instantiation since this is a utility class.
121      */
122     private ModelUpdater() {
123         super();
124     }
125 
126     /*
127      * Post-processing methods to make the SCXML object SCXMLExecutor ready.
128      */
129     /**
130      * <p>Update the SCXML object model and make it SCXMLExecutor ready.
131      * This is part of post-read processing, and sets up the necessary
132      * object references throughtout the SCXML object model for the parsed
133      * document.</p>
134      *
135      * @param scxml The SCXML object (output from SCXMLReader)
136      * @throws ModelException If the object model is flawed
137      */
138     static void updateSCXML(final SCXML scxml) throws ModelException {
139         initDocumentOrder(scxml.getChildren(), 1);
140 
141         String initial = scxml.getInitial();
142         SimpleTransition initialTransition = new SimpleTransition();
143 
144         if (initial != null) {
145 
146             initialTransition.setNext(scxml.getInitial());
147             updateTransition(initialTransition, scxml.getTargets());
148 
149             if (initialTransition.getTargets().size() == 0) {
150                 logAndThrowModelError(ERR_SCXML_NO_INIT, new Object[] {
151                         initial });
152             }
153         } else {
154             // If 'initial' is not specified, the default initial state is
155             // the first child state in document order.
156             initialTransition.getTargets().add(scxml.getFirstChild());
157         }
158 
159         scxml.setInitialTransition(initialTransition);
160         Map<String, TransitionTarget> targets = scxml.getTargets();
161         for (EnterableState es : scxml.getChildren()) {
162             if (es instanceof State) {
163                 updateState((State) es, targets);
164             } else if (es instanceof Parallel) {
165                 updateParallel((Parallel) es, targets);
166             }
167         }
168 
169         scxml.getInitialTransition().setObservableId(1);
170         initObservables(scxml.getChildren(), 2);
171     }
172 
173     /**
174      * Initialize all {@link org.apache.commons.scxml2.model.DocumentOrder} instances (EnterableState or Transition)
175      * by iterating them in document order setting their document order value.
176      * @param states The list of children states of a parent TransitionalState or the SCXML document itself
177      * @param nextOrder The next to be used order value
178      * @return Returns the next to be used order value
179      */
180     private static int initDocumentOrder(final List<EnterableState> states, int nextOrder) {
181         for (EnterableState state : states) {
182             state.setOrder(nextOrder++);
183             if (state instanceof TransitionalState) {
184                 TransitionalState ts = (TransitionalState)state;
185                 for (Transition t : ts.getTransitionsList()) {
186                     t.setOrder(nextOrder++);
187                 }
188                 nextOrder = initDocumentOrder(ts.getChildren(), nextOrder);
189             }
190         }
191         return nextOrder;
192     }
193 
194     /**
195      * Initialize all {@link org.apache.commons.scxml2.model.Observable} instances in the SCXML document
196      * by iterating them in document order and seeding them with a unique obeservable id.
197      * @param states The list of children states of a parent TransitionalState or the SCXML document itself
198      * @param nextObservableId The next observable id sequence value to be used
199      * @return Returns the next to be used observable id sequence value
200      */
201     private static int initObservables(final List<EnterableState>states, int nextObservableId) {
202         for (EnterableState es : states) {
203             es.setObservableId(nextObservableId++);
204             if (es instanceof TransitionalState) {
205                 TransitionalState ts = (TransitionalState)es;
206                 if (ts instanceof State) {
207                     State s = (State)ts;
208                     if (s.getInitial() != null && s.getInitial().getTransition() != null) {
209                         s.getInitial().getTransition().setObservableId(nextObservableId++);
210                     }
211                 }
212                 for (Transition t : ts.getTransitionsList()) {
213                     t.setObservableId(nextObservableId++);
214                 }
215                 for (History h : ts.getHistory()) {
216                     h.setObservableId(nextObservableId++);
217                     if (h.getTransition() != null) {
218                         h.getTransition().setObservableId(nextObservableId++);
219                     }
220                 }
221                 nextObservableId = initObservables(ts.getChildren(), nextObservableId);
222             }
223         }
224         return nextObservableId;
225     }
226 
227     /**
228      * Update this State object (part of post-read processing).
229      * Also checks for any errors in the document.
230      *
231      * @param state The State 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 updateState(final State state, final Map<String, TransitionTarget> targets)
236             throws ModelException {
237         List<EnterableState> children = state.getChildren();
238         if (state.isComposite()) {
239             //initialize next / initial
240             Initial ini = state.getInitial();
241             if (ini == null) {
242                 state.setFirst(children.get(0).getId());
243                 ini = state.getInitial();
244             }
245             SimpleTransition initialTransition = ini.getTransition();
246             updateTransition(initialTransition, targets);
247             Set<TransitionTarget> initialStates = initialTransition.getTargets();
248             // we have to allow for an indirect descendant initial (targets)
249             //check that initialState is a descendant of s
250             if (initialStates.size() == 0) {
251                 logAndThrowModelError(ERR_STATE_BAD_INIT,
252                         new Object[] {getName(state)});
253             } else {
254                 for (TransitionTarget initialState : initialStates) {
255                     if (!initialState.isDescendantOf(state)) {
256                         logAndThrowModelError(ERR_STATE_BAD_INIT,
257                                 new Object[] {getName(state)});
258                     }
259                 }
260             }
261         }
262         else if (state.getInitial() != null) {
263             logAndThrowModelError(ERR_UNSUPPORTED_INIT, new Object[] {getName(state)});
264         }
265 
266         List<History> histories = state.getHistory();
267         if (histories.size() > 0 && state.isSimple()) {
268             logAndThrowModelError(ERR_HISTORY_SIMPLE_STATE,
269                     new Object[] {getName(state)});
270         }
271         for (History history : histories) {
272             updateHistory(history, targets, state);
273         }
274         for (Transition transition : state.getTransitionsList()) {
275             updateTransition(transition, targets);
276         }
277 
278         for (Invoke inv : state.getInvokes()) {
279             if (inv.getSrc() != null && inv.getSrcexpr() != null) {
280                 logAndThrowModelError(ERR_INVOKE_AMBIGUOUS_SRC, new Object[] {getName(state)});
281             }
282         }
283 
284         for (EnterableState es : children) {
285             if (es instanceof State) {
286                 updateState((State) es, targets);
287             } else if (es instanceof Parallel) {
288                 updateParallel((Parallel) es, targets);
289             }
290         }
291     }
292 
293     /**
294      * Update this Parallel object (part of post-read processing).
295      *
296      * @param parallel The Parallel object
297      * @param targets The global Map of all transition targets
298      * @throws ModelException If the object model is flawed
299      */
300     private static void updateParallel(final Parallel parallel, final Map<String, TransitionTarget> targets)
301             throws ModelException {
302         for (EnterableState es : parallel.getChildren()) {
303             if (es instanceof State) {
304                 updateState((State) es, targets);
305             } else if (es instanceof Parallel) {
306                 updateParallel((Parallel) es, targets);
307             }
308         }
309         for (Transition transition : parallel.getTransitionsList()) {
310             updateTransition(transition, targets);
311         }
312         List<History> histories = parallel.getHistory();
313         for (History history : histories) {
314             updateHistory(history, targets, parallel);
315         }
316         // TODO: parallel must may have invokes too
317     }
318 
319     /**
320      * Update this History object (part of post-read processing).
321      *
322      * @param history The History object
323      * @param targets The global Map of all transition targets
324      * @param parent The parent TransitionalState for this History
325      * @throws ModelException If the object model is flawed
326      */
327     private static void updateHistory(final History history,
328                                       final Map<String, TransitionTarget> targets,
329                                       final TransitionalState parent)
330             throws ModelException {
331         SimpleTransition transition = history.getTransition();
332         if (transition == null || transition.getNext() == null) {
333             logAndThrowModelError(ERR_HISTORY_NO_DEFAULT,
334                     new Object[] {history.getId(), getName(parent)});
335         }
336         else {
337             updateTransition(transition, targets);
338             Set<TransitionTarget> historyStates = transition.getTargets();
339             if (historyStates.size() == 0) {
340                 logAndThrowModelError(ERR_STATE_NO_HIST,
341                         new Object[] {getName(parent)});
342             }
343             for (TransitionTarget historyState : historyStates) {
344                 if (!history.isDeep()) {
345                     // Shallow history
346                     if (!parent.getChildren().contains(historyState)) {
347                         logAndThrowModelError(ERR_STATE_BAD_SHALLOW_HIST,
348                                 new Object[] {getName(parent)});
349                     }
350                 } else {
351                     // Deep history
352                     if (!historyState.isDescendantOf(parent)) {
353                         logAndThrowModelError(ERR_STATE_BAD_DEEP_HIST,
354                                 new Object[] {getName(parent)});
355                     }
356                 }
357             }
358         }
359     }
360 
361     /**
362      * Update this Transition object (part of post-read processing).
363      *
364      * @param transition The Transition object
365      * @param targets The global Map of all transition targets
366      * @throws ModelException If the object model is flawed
367      */
368     private static void updateTransition(final SimpleTransition transition,
369                                          final Map<String, TransitionTarget> targets) throws ModelException {
370         String next = transition.getNext();
371         if (next == null) { // stay transition
372             return;
373         }
374         Set<TransitionTarget> tts = transition.getTargets();
375         if (tts.isEmpty()) {
376             // 'next' is a space separated list of transition target IDs
377             StringTokenizer ids = new StringTokenizer(next);
378             while (ids.hasMoreTokens()) {
379                 String id = ids.nextToken();
380                 TransitionTarget tt = targets.get(id);
381                 if (tt == null) {
382                     logAndThrowModelError(ERR_TARGET_NOT_FOUND, new Object[] {
383                             id });
384                 }
385                 tts.add(tt);
386             }
387             if (tts.size() > 1) {
388                 boolean legal = verifyTransitionTargets(tts);
389                 if (!legal) {
390                     logAndThrowModelError(ERR_ILLEGAL_TARGETS, new Object[] {
391                             next });
392                 }
393             }
394         }
395     }
396 
397     /**
398      * Log an error discovered in post-read processing.
399      *
400      * @param errType The type of error
401      * @param msgArgs The arguments for formatting the error message
402      * @throws ModelException The model error, always thrown.
403      */
404     private static void logAndThrowModelError(final String errType,
405                                               final Object[] msgArgs) throws ModelException {
406         MessageFormat msgFormat = new MessageFormat(errType);
407         String errMsg = msgFormat.format(msgArgs);
408         org.apache.commons.logging.Log log = LogFactory.
409                 getLog(ModelUpdater.class);
410         log.error(errMsg);
411         throw new ModelException(errMsg);
412     }
413 
414     /**
415      * Get a transition target identifier for error messages. This method is
416      * only called to produce an appropriate log message in some error
417      * conditions.
418      *
419      * @param tt The <code>TransitionTarget</code> object
420      * @return The transition target identifier for the error message
421      */
422     private static String getName(final TransitionTarget tt) {
423         String name = "anonymous transition target";
424         if (tt instanceof State) {
425             name = "anonymous state";
426             if (tt.getId() != null) {
427                 name = "state with ID \"" + tt.getId() + "\"";
428             }
429         } else if (tt instanceof Parallel) {
430             name = "anonymous parallel";
431             if (tt.getId() != null) {
432                 name = "parallel with ID \"" + tt.getId() + "\"";
433             }
434         } else {
435             if (tt.getId() != null) {
436                 name = "transition target with ID \"" + tt.getId() + "\"";
437             }
438         }
439         return name;
440     }
441 
442     /**
443      * If a transition has multiple targets, then they satisfy the following
444      * criteria:
445      * <ul>
446      *  <li>No target is an ancestor of any other target on the list</li>
447      *  <li>A full legal state configuration results when all ancestors and default initial descendants have been added.
448      *  <br/>This means that they all must share the same least common parallel ancestor.
449      *  </li>
450      * </ul>
451      *
452      * @param tts The transition targets
453      * @return Whether this is a legal configuration
454      * @see <a href=http://www.w3.org/TR/scxml/#LegalStateConfigurations">
455      *     http://www.w3.org/TR/scxml/#LegalStateConfigurations</a>
456      */
457     private static boolean verifyTransitionTargets(final Set<TransitionTarget> tts) {
458         if (tts.size() < 2) { // No contention
459             return true;
460         }
461         TransitionTarget first = null;
462         int i = 0;
463         for (TransitionTarget tt : tts) {
464             if (first == null) {
465                 first = tt;
466                 i = tt.getNumberOfAncestors();
467                 continue;
468             }
469             // find least common ancestor
470             for (i = Math.min(i, tt.getNumberOfAncestors()); i > 0 && first.getAncestor(i-1) != tt.getAncestor(i-1); i--) ;
471             if (i == 0) {
472                 // no common ancestor
473                 return false;
474             }
475             // ensure no target is an ancestor of any other target on the list
476             for (TransitionTarget other : tts) {
477                 if (other != tt && other.isDescendantOf(tt) || tt.isDescendantOf(other)) {
478                     return false;
479                 }
480             }
481         }
482         // least common ancestor must be a parallel
483         return first != null && i > 0 && first.getAncestor(i-1) instanceof Parallel;
484    }
485 }