001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.commons.scxml.io;
018:
019: import java.text.MessageFormat;
020: import java.util.Iterator;
021: import java.util.List;
022: import java.util.Map;
023:
024: import org.apache.commons.logging.LogFactory;
025: import org.apache.commons.scxml.SCXMLHelper;
026: import org.apache.commons.scxml.model.History;
027: import org.apache.commons.scxml.model.Initial;
028: import org.apache.commons.scxml.model.Invoke;
029: import org.apache.commons.scxml.model.ModelException;
030: import org.apache.commons.scxml.model.Parallel;
031: import org.apache.commons.scxml.model.SCXML;
032: import org.apache.commons.scxml.model.State;
033: import org.apache.commons.scxml.model.Transition;
034: import org.apache.commons.scxml.model.TransitionTarget;
035:
036: /**
037: * The ModelUpdater provides the utility methods to check the Commons
038: * SCXML model for inconsistencies, detect errors, and wire the Commons
039: * SCXML model appropriately post document parsing by the digester to make
040: * it executor ready.
041: */
042: final class ModelUpdater {
043:
044: /*
045: * Post-processing methods to make the SCXML object SCXMLExecutor ready.
046: */
047: /**
048: * <p>Update the SCXML object model and make it SCXMLExecutor ready.
049: * This is part of post-digester processing, and sets up the necessary
050: * object references throughtout the SCXML object model for the parsed
051: * document.</p>
052: *
053: * @param scxml The SCXML object (output from Digester)
054: * @throws ModelException If the object model is flawed
055: */
056: static void updateSCXML(final SCXML scxml) throws ModelException {
057: // Watch case, slightly unfortunate naming ;-)
058: String initialstate = scxml.getInitialstate();
059: //we have to use getTargets() here since the initialState can be
060: //an indirect descendant
061: // Concern marked by one of the code reviewers: better type check,
062: // now ClassCastException happens for Parallel
063: // Response: initial should be a State, for Parallel, it is implicit
064: State initialState = (State) scxml.getTargets().get(
065: initialstate);
066: if (initialState == null) {
067: // Where do we, where do we go?
068: logAndThrowModelError(ERR_SCXML_NO_INIT,
069: new Object[] { initialstate });
070: }
071: scxml.setInitialState(initialState);
072: Map targets = scxml.getTargets();
073: Map states = scxml.getStates();
074: Iterator i = states.keySet().iterator();
075: while (i.hasNext()) {
076: updateState((State) states.get(i.next()), targets);
077: }
078: }
079:
080: /**
081: * Update this State object (part of post-digestion processing).
082: * Also checks for any errors in the document.
083: *
084: * @param s The State object
085: * @param targets The global Map of all transition targets
086: * @throws ModelException If the object model is flawed
087: */
088: private static void updateState(final State s, final Map targets)
089: throws ModelException {
090: //ensure both onEntry and onExit have parent
091: //could add next two lines as a Digester rule for OnEntry/OnExit
092: s.getOnEntry().setParent(s);
093: s.getOnExit().setParent(s);
094: //initialize next / inital
095: Initial ini = s.getInitial();
096: Map c = s.getChildren();
097: TransitionTarget initialState = null;
098: if (!c.isEmpty()) {
099: 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: initialState = initialTransition.getTarget();
106: // we have to allow for an indirect descendant initial (targets)
107: //check that initialState is a descendant of s
108: if (initialState == null
109: || !SCXMLHelper.isDescendant(initialState, s)) {
110: logAndThrowModelError(ERR_STATE_BAD_INIT,
111: new Object[] { getStateName(s) });
112: }
113: }
114: List histories = s.getHistory();
115: Iterator histIter = histories.iterator();
116: while (histIter.hasNext()) {
117: if (s.isSimple()) {
118: logAndThrowModelError(ERR_HISTORY_SIMPLE_STATE,
119: new Object[] { getStateName(s) });
120: }
121: History h = (History) histIter.next();
122: Transition historyTransition = h.getTransition();
123: if (historyTransition == null) {
124: // try to assign initial as default
125: if (initialState != null
126: && !(initialState instanceof History)) {
127: historyTransition = new Transition();
128: historyTransition.setNext(initialState.getId());
129: historyTransition.setParent(h);
130: h.setTransition(historyTransition);
131: } else {
132: logAndThrowModelError(ERR_HISTORY_NO_DEFAULT,
133: new Object[] { h.getId(), getStateName(s) });
134: }
135: }
136: updateTransition(historyTransition, targets);
137: State historyState = (State) historyTransition.getTarget();
138: if (historyState == null) {
139: logAndThrowModelError(ERR_STATE_NO_HIST,
140: new Object[] { getStateName(s) });
141: }
142: if (!h.isDeep()) {
143: if (!c.containsValue(historyState)) {
144: logAndThrowModelError(ERR_STATE_BAD_SHALLOW_HIST,
145: new Object[] { getStateName(s) });
146: }
147: } else {
148: if (!SCXMLHelper.isDescendant(historyState, s)) {
149: logAndThrowModelError(ERR_STATE_BAD_DEEP_HIST,
150: new Object[] { getStateName(s) });
151: }
152: }
153: }
154: Map t = s.getTransitions();
155: Iterator i = t.keySet().iterator();
156: while (i.hasNext()) {
157: Iterator j = ((List) t.get(i.next())).iterator();
158: while (j.hasNext()) {
159: Transition trn = (Transition) j.next();
160: //could add next two lines as a Digester rule for Transition
161: trn.setParent(s);
162: updateTransition(trn, targets);
163: }
164: }
165: Parallel p = s.getParallel();
166: Invoke inv = s.getInvoke();
167: if ((inv != null && p != null) || (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 || srcexpr.trim()
184: .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,
209: final Map targets) 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,
234: new Object[] { 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 <parallel>, one <invoke> or any number of
304: * <state> 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 = "Transition target with ID \"{0}\" not found";
332:
333: /**
334: * Simple states should not contain a history.
335: */
336: private static final String ERR_HISTORY_SIMPLE_STATE = "Simple {0} contains history elements";
337:
338: /**
339: * History does not specify a default transition target.
340: */
341: private static final String ERR_HISTORY_NO_DEFAULT = "No default target specified for history with ID \"{0}\""
342: + " belonging to {1}";
343:
344: /**
345: * Error message when an <invoke> does not specify a "targettype"
346: * attribute.
347: */
348: private static final String ERR_INVOKE_NO_TARGETTYPE = "{0} contains "
349: + "<invoke> with no \"targettype\" attribute specified.";
350:
351: /**
352: * Error message when an <invoke> does not specify a "src"
353: * or a "srcexpr" attribute.
354: */
355: private static final String ERR_INVOKE_NO_SRC = "{0} contains "
356: + "<invoke> without a \"src\" or \"srcexpr\" attribute specified.";
357:
358: /**
359: * Error message when an <invoke> specifies both "src" and "srcexpr"
360: * attributes.
361: */
362: private static final String ERR_INVOKE_AMBIGUOUS_SRC = "{0} contains "
363: + "<invoke> with both \"src\" and \"srcexpr\" attributes specified,"
364: + " must specify either one, but not both.";
365:
366: }
|