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.io.StringWriter;
020 import java.util.Iterator;
021 import java.util.List;
022 import java.util.Map;
023 import java.util.Properties;
024 import java.util.Set;
025
026 import javax.xml.transform.OutputKeys;
027 import javax.xml.transform.Result;
028 import javax.xml.transform.Source;
029 import javax.xml.transform.Transformer;
030 import javax.xml.transform.TransformerException;
031 import javax.xml.transform.TransformerFactory;
032 import javax.xml.transform.dom.DOMSource;
033 import javax.xml.transform.stream.StreamResult;
034
035 import org.apache.commons.logging.LogFactory;
036 import org.apache.commons.scxml.SCXMLHelper;
037 import org.apache.commons.scxml.model.Action;
038 import org.apache.commons.scxml.model.Assign;
039 import org.apache.commons.scxml.model.Cancel;
040 import org.apache.commons.scxml.model.Data;
041 import org.apache.commons.scxml.model.Datamodel;
042 import org.apache.commons.scxml.model.Else;
043 import org.apache.commons.scxml.model.ElseIf;
044 import org.apache.commons.scxml.model.Exit;
045 import org.apache.commons.scxml.model.ExternalContent;
046 import org.apache.commons.scxml.model.Finalize;
047 import org.apache.commons.scxml.model.History;
048 import org.apache.commons.scxml.model.If;
049 import org.apache.commons.scxml.model.Initial;
050 import org.apache.commons.scxml.model.Invoke;
051 import org.apache.commons.scxml.model.Log;
052 import org.apache.commons.scxml.model.NamespacePrefixesHolder;
053 import org.apache.commons.scxml.model.OnEntry;
054 import org.apache.commons.scxml.model.OnExit;
055 import org.apache.commons.scxml.model.Parallel;
056 import org.apache.commons.scxml.model.Param;
057 import org.apache.commons.scxml.model.SCXML;
058 import org.apache.commons.scxml.model.Send;
059 import org.apache.commons.scxml.model.State;
060 import org.apache.commons.scxml.model.Transition;
061 import org.apache.commons.scxml.model.TransitionTarget;
062 import org.apache.commons.scxml.model.Var;
063 import org.w3c.dom.Node;
064
065 /**
066 * <p>Utility class for serializing the Commons SCXML Java object
067 * model. Class uses the visitor pattern to trace through the
068 * object heirarchy. Used primarily for testing, debugging and
069 * visual verification.</p>
070 *
071 * <b>NOTE:</b> This serializer makes the following assumptions about the
072 * original SCXML document(s) parsed to create the object model:
073 * <ul>
074 * <li>The default document namespace is the SCXML namespace:
075 * <i>http://www.w3.org/2005/07/scxml</i></li>
076 * <li>The Commons SCXML namespace
077 * ( <i>http://commons.apache.org/scxml</i> ), if needed, uses the
078 * "<i>cs</i>" prefix</li>
079 * <li>All namespace prefixes needed throughout the document are
080 * declared on the document root element (<scxml>)</li>
081 * </ul>
082 */
083 public class SCXMLSerializer {
084
085 /** The indent to be used while serializing an SCXML object. */
086 private static final String INDENT = " ";
087 /** The JAXP transformer. */
088 private static final Transformer XFORMER = getTransformer();
089 /** The SCXML namespace. */
090 private static final String NAMESPACE_SCXML =
091 "http://www.w3.org/2005/07/scxml";
092 /** The Commons SCXML namespace. */
093 private static final String NAMESPACE_COMMONS_SCXML =
094 "http://commons.apache.org/scxml";
095
096 /**
097 * Serialize this SCXML object (primarily for debugging).
098 *
099 * @param scxml
100 * The SCXML to be serialized
101 * @return String The serialized SCXML
102 */
103 public static String serialize(final SCXML scxml) {
104 StringBuffer b =
105 new StringBuffer("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n").
106 append("<scxml xmlns=\"").append(NAMESPACE_SCXML).
107 append("\"").append(serializeNamespaceDeclarations(scxml)).
108 append(" version=\"").append(scxml.getVersion()).
109 append("\" initial=\"").append(scxml.getInitial()).
110 append("\">\n");
111 if (XFORMER == null) {
112 org.apache.commons.logging.Log log = LogFactory.
113 getLog(SCXMLSerializer.class);
114 log.warn("SCXMLSerializer: DOM serialization pertinent to"
115 + " the document will be skipped since a suitable"
116 + " JAXP Transformer could not be instantiated.");
117 }
118 b.append(INDENT).append("<!-- http://commons.apache.org/scxml -->\n");
119 Datamodel dm = scxml.getDatamodel();
120 if (dm != null) {
121 serializeDatamodel(b, dm, INDENT);
122 }
123 Map c = scxml.getChildren();
124 Iterator i = c.keySet().iterator();
125 while (i.hasNext()) {
126 TransitionTarget tt = (TransitionTarget) c.get(i.next());
127 if (tt instanceof State) {
128 serializeState(b, (State) tt, INDENT);
129 } else {
130 serializeParallel(b, (Parallel) tt, INDENT);
131 }
132 }
133 b.append("</scxml>\n");
134 return b.toString();
135 }
136
137 /**
138 * Serialize this State object.
139 *
140 * @param b The buffer to append the serialization to
141 * @param s The State to serialize
142 * @param indent The indent for this XML element
143 */
144 public static void serializeState(final StringBuffer b,
145 final State s, final String indent) {
146 b.append(indent).append("<state");
147 serializeTransitionTargetAttributes(b, s);
148 boolean f = s.isFinal();
149 if (f) {
150 b.append(" final=\"true\"");
151 }
152 b.append(">\n");
153 Initial ini = s.getInitial();
154 if (ini != null) {
155 serializeInitial(b, ini, indent + INDENT);
156 }
157 List h = s.getHistory();
158 if (h != null) {
159 serializeHistory(b, h, indent + INDENT);
160 }
161 Datamodel dm = s.getDatamodel();
162 if (dm != null) {
163 serializeDatamodel(b, dm, indent + INDENT);
164 }
165 serializeOnEntry(b, s, indent + INDENT);
166 List t = s.getTransitionsList();
167 for (int i = 0; i < t.size(); i++) {
168 serializeTransition(b, (Transition) t.get(i), indent + INDENT);
169 }
170 Parallel p = s.getParallel(); //TODO: Remove in v1.0
171 Invoke inv = s.getInvoke();
172 if (p != null) {
173 serializeParallel(b, p, indent + INDENT);
174 } else if (inv != null) {
175 serializeInvoke(b , inv, indent + INDENT);
176 } else {
177 Map c = s.getChildren();
178 Iterator j = c.keySet().iterator();
179 while (j.hasNext()) {
180 TransitionTarget tt = (TransitionTarget) c.get(j.next());
181 if (tt instanceof State) {
182 serializeState(b, (State) tt, indent + INDENT);
183 } else if (tt instanceof Parallel) {
184 serializeParallel(b, (Parallel) tt, indent + INDENT);
185 }
186 }
187 }
188 serializeOnExit(b, s, indent + INDENT);
189 b.append(indent).append("</state>\n");
190 }
191
192 /**
193 * Serialize this Parallel object.
194 *
195 * @param b The buffer to append the serialization to
196 * @param p The Parallel to serialize
197 * @param indent The indent for this XML element
198 */
199 public static void serializeParallel(final StringBuffer b,
200 final Parallel p, final String indent) {
201 b.append(indent).append("<parallel");
202 serializeTransitionTargetAttributes(b, p);
203 b.append(">\n");
204 serializeOnEntry(b, p, indent + INDENT);
205 Set s = p.getChildren();
206 Iterator i = s.iterator();
207 while (i.hasNext()) {
208 serializeState(b, (State) i.next(), indent + INDENT);
209 }
210 serializeOnExit(b, p, indent + INDENT);
211 b.append(indent).append("</parallel>\n");
212 }
213
214 /**
215 * Serialize this Invoke object.
216 *
217 * @param b The buffer to append the serialization to
218 * @param i The Invoke to serialize
219 * @param indent The indent for this XML element
220 */
221 public static void serializeInvoke(final StringBuffer b,
222 final Invoke i, final String indent) {
223 b.append(indent).append("<invoke");
224 String ttype = i.getTargettype();
225 String src = i.getSrc();
226 String srcexpr = i.getSrcexpr();
227 if (ttype != null) {
228 b.append(" targettype=\"").append(ttype).append("\"");
229 }
230 // Prefer src
231 if (src != null) {
232 b.append(" src=\"").append(src).append("\"");
233 } else if (srcexpr != null) {
234 b.append(" srcexpr=\"").append(srcexpr).append("\"");
235 }
236 b.append(">\n");
237 List params = i.params();
238 for (Iterator iter = params.iterator(); iter.hasNext();) {
239 Param p = (Param) iter.next();
240 b.append(indent).append(INDENT).append("<param name=\"").
241 append(p.getName()).append("\" expr=\"").
242 append(SCXMLHelper.escapeXML(p.getExpr())).append("\"/>\n");
243 }
244 Finalize f = i.getFinalize();
245 if (f != null) {
246 b.append(indent).append(INDENT).append("<finalize>\n");
247 serializeActions(b, f.getActions(), indent + INDENT + INDENT);
248 b.append(indent).append(INDENT).append("</finalize>\n");
249 }
250 b.append(indent).append("</invoke>\n");
251 }
252
253 /**
254 * Serialize this Initial object.
255 *
256 * @param b The buffer to append the serialization to
257 * @param i The Initial to serialize
258 * @param indent The indent for this XML element
259 */
260 public static void serializeInitial(final StringBuffer b, final Initial i,
261 final String indent) {
262 b.append(indent).append("<initial");
263 serializeTransitionTargetAttributes(b, i);
264 b.append(">\n");
265 serializeTransition(b, i.getTransition(), indent + INDENT);
266 b.append(indent).append("</initial>\n");
267 }
268
269 /**
270 * Serialize the History.
271 *
272 * @param b The buffer to append the serialization to
273 * @param l The List of History objects to serialize
274 * @param indent The indent for this XML element
275 */
276 public static void serializeHistory(final StringBuffer b, final List l,
277 final String indent) {
278 if (l.size() > 0) {
279 for (int i = 0; i < l.size(); i++) {
280 History h = (History) l.get(i);
281 b.append(indent).append("<history");
282 serializeTransitionTargetAttributes(b, h);
283 if (h.isDeep()) {
284 b.append(" type=\"deep\"");
285 } else {
286 b.append(" type=\"shallow\"");
287 }
288 b.append(">\n");
289 serializeTransition(b, h.getTransition(), indent + INDENT);
290 b.append(indent).append("</history>\n");
291 }
292 }
293 }
294
295 /**
296 * Serialize this Transition object.
297 *
298 * @param b The buffer to append the serialization to
299 * @param t The Transition to serialize
300 * @param indent The indent for this XML element
301 */
302 public static void serializeTransition(final StringBuffer b,
303 final Transition t, final String indent) {
304 b.append(indent).append("<transition");
305 if (!SCXMLHelper.isStringEmpty(t.getEvent())) {
306 b.append(" event=\"").append(t.getEvent()).append("\"");
307 }
308 if (!SCXMLHelper.isStringEmpty(t.getCond())) {
309 b.append(" cond=\"").append(SCXMLHelper.escapeXML(t.getCond())).
310 append("\"");
311 }
312 boolean next = !SCXMLHelper.isStringEmpty(t.getNext());
313 if (next) {
314 b.append(" target=\"" + t.getNext() + "\"");
315 }
316 b.append(">\n");
317 boolean exit = serializeActions(b, t.getActions(), indent + INDENT);
318 if (!next && !exit) {
319 serializeTarget(b, t, indent + INDENT);
320 }
321 b.append(indent).append("</transition>\n");
322 }
323
324 /**
325 * Serialize this Transition's Target.
326 *
327 *
328 * @param b The buffer to append the serialization to
329 * @param t The Transition whose Target needs to be serialized
330 * @param indent The indent for this XML element
331 *
332 * @deprecated Inline <target> element has been deprecated
333 * in the SCXML WD
334 */
335 public static void serializeTarget(final StringBuffer b,
336 final Transition t, final String indent) {
337 if (t.getTarget() != null) {
338 b.append(indent).append("<target>");
339 // The inline transition target can only be a state
340 serializeState(b, (State) t.getTarget(), indent + INDENT);
341 b.append(indent).append("</target>");
342 }
343 }
344
345 /**
346 * Serialize this Datamodel object.
347 *
348 * @param b The buffer to append the serialization to
349 * @param dm The Datamodel to be serialized
350 * @param indent The indent for this XML element
351 */
352 public static void serializeDatamodel(final StringBuffer b,
353 final Datamodel dm, final String indent) {
354 List data = dm.getData();
355 if (data != null && data.size() > 0) {
356 b.append(indent).append("<datamodel>\n");
357 if (XFORMER == null) {
358 b.append(indent).append(INDENT).
359 append("<!-- Body content was not serialized -->\n");
360 b.append(indent).append("</datamodel>\n");
361 return;
362 }
363 for (Iterator iter = data.iterator(); iter.hasNext();) {
364 Data datum = (Data) iter.next();
365 Node dataNode = datum.getNode();
366 if (dataNode != null) {
367 StringWriter out = new StringWriter();
368 try {
369 Source input = new DOMSource(dataNode);
370 Result output = new StreamResult(out);
371 XFORMER.transform(input, output);
372 } catch (TransformerException te) {
373 org.apache.commons.logging.Log log = LogFactory.
374 getLog(SCXMLSerializer.class);
375 log.error(te.getMessage(), te);
376 b.append(indent).append(INDENT).
377 append("<!-- Data content not serialized -->\n");
378 }
379 b.append(indent).append(INDENT).append(out.toString());
380 } else {
381 b.append(indent).append(INDENT).append("<data id=\"").
382 append(datum.getId()).append("\" expr=\"").
383 append(SCXMLHelper.escapeXML(datum.getExpr())).
384 append("\" />\n");
385 }
386 }
387 b.append(indent).append("</datamodel>\n");
388 }
389 }
390
391 /**
392 * Serialize this OnEntry object.
393 *
394 * @param b The buffer to append the serialization to
395 * @param t The TransitionTarget whose OnEntry is to be serialized
396 * @param indent The indent for this XML element
397 */
398 public static void serializeOnEntry(final StringBuffer b,
399 final TransitionTarget t, final String indent) {
400 OnEntry e = t.getOnEntry();
401 if (e != null && e.getActions().size() > 0) {
402 b.append(indent).append("<onentry>\n");
403 serializeActions(b, e.getActions(), indent + INDENT);
404 b.append(indent).append("</onentry>\n");
405 }
406 }
407
408 /**
409 * Serialize this OnExit object.
410 *
411 * @param b The buffer to append the serialization to
412 * @param t The TransitionTarget whose OnExit is to be serialized
413 * @param indent The indent for this XML element
414 */
415 public static void serializeOnExit(final StringBuffer b,
416 final TransitionTarget t, final String indent) {
417 OnExit x = t.getOnExit();
418 if (x != null && x.getActions().size() > 0) {
419 b.append(indent).append("<onexit>\n");
420 serializeActions(b, x.getActions(), indent + INDENT);
421 b.append(indent).append("</onexit>\n");
422 }
423 }
424
425 /**
426 * Serialize this List of actions.
427 *
428 * @param b The buffer to append the serialization to
429 * @param l The List of actions to serialize
430 * @param indent The indent for this XML element
431 * @return boolean true if the list of actions contains an <exit/>
432 */
433 public static boolean serializeActions(final StringBuffer b, final List l,
434 final String indent) {
435 if (l == null) {
436 return false;
437 }
438 boolean exit = false;
439 Iterator i = l.iterator();
440 while (i.hasNext()) {
441 Action a = (Action) i.next();
442 if (a instanceof Var) {
443 Var v = (Var) a;
444 b.append(indent).append("<cs:var name=\"").append(v.getName())
445 .append("\" expr=\"")
446 .append(SCXMLHelper.escapeXML(v.getExpr()))
447 .append("\"/>\n");
448 } else if (a instanceof Assign) {
449 Assign asn = (Assign) a;
450 b.append(indent).append("<assign");
451 if (!SCXMLHelper.isStringEmpty(asn.getLocation())) {
452 b.append(" location=\"").append(asn.getLocation());
453 if (!SCXMLHelper.isStringEmpty(asn.getSrc())) {
454 b.append("\" src=\"").append(asn.getSrc());
455 } else {
456 b.append("\" expr=\"").
457 append(SCXMLHelper.escapeXML(asn.getExpr()));
458 }
459 } else {
460 b.append(" name=\"").append(asn.getName()).
461 append("\" expr=\"").
462 append(SCXMLHelper.escapeXML(asn.getExpr()));
463 }
464 b.append("\"/>\n");
465 } else if (a instanceof Send) {
466 serializeSend(b, (Send) a, indent);
467 } else if (a instanceof Cancel) {
468 Cancel c = (Cancel) a;
469 b.append(indent).append("<cancel sendid=\"")
470 .append(c.getSendid()).append("\"/>\n");
471 } else if (a instanceof Log) {
472 Log lg = (Log) a;
473 b.append(indent).append("<log expr=\"").
474 append(SCXMLHelper.escapeXML(lg.getExpr())).
475 append("\"/>\n");
476 } else if (a instanceof Exit) {
477 Exit e = (Exit) a;
478 b.append(indent).append("<cs:exit");
479 String expr = SCXMLHelper.escapeXML(e.getExpr());
480 String nl = e.getNamelist();
481 if (expr != null) {
482 b.append(" expr=\"" + expr + "\"");
483 }
484 if (nl != null) {
485 b.append(" namelist=\"" + nl + "\"");
486 }
487 b.append("/>\n");
488 exit = true;
489 } else if (a instanceof If) {
490 If iff = (If) a;
491 serializeIf(b, iff, indent);
492 } else if (a instanceof Else) {
493 b.append(indent).append("<else/>\n");
494 } else if (a instanceof ElseIf) {
495 ElseIf eif = (ElseIf) a;
496 b.append(indent).append("<elseif cond=\"")
497 .append(SCXMLHelper.escapeXML(eif.getCond()))
498 .append("\" />\n");
499 }
500 }
501 return exit;
502 }
503
504 /**
505 * Serialize this Send object.
506 *
507 * @param b The buffer to append the serialization to
508 * @param send The Send object to serialize
509 * @param indent The indent for this XML element
510 */
511 public static void serializeSend(final StringBuffer b,
512 final Send send, final String indent) {
513 b.append(indent).append("<send");
514 if (send.getSendid() != null) {
515 b.append(" sendid=\"").append(send.getSendid()).append("\"");
516 }
517 if (send.getTarget() != null) {
518 b.append(" target=\"").append(send.getTarget()).append("\"");
519 }
520 if (send.getTargettype() != null) {
521 b.append(" targetType=\"").append(send.getTargettype()).append("\"");
522 }
523 if (send.getNamelist() != null) {
524 b.append(" namelist=\"").append(send.getNamelist()).append("\"");
525 }
526 if (send.getDelay() != null) {
527 b.append(" delay=\"").append(send.getDelay()).append("\"");
528 }
529 if (send.getEvent() != null) {
530 b.append(" event=\"").append(send.getEvent()).append("\"");
531 }
532 if (send.getHints() != null) {
533 b.append(" hints=\"").append(send.getHints()).append("\"");
534 }
535 b.append(">\n");
536 b.append(getBodyContent(send));
537 b.append(indent).append("</send>\n");
538 }
539
540 /**
541 * Return serialized body of <code>ExternalContent</code>.
542 *
543 * @param externalContent The model element containing the body content
544 * @return String The serialized body content
545 */
546 public static final String getBodyContent(
547 final ExternalContent externalContent) {
548 StringBuffer buf = new StringBuffer();
549 List externalNodes = externalContent.getExternalNodes();
550 if (externalNodes.size() > 0 && XFORMER == null) {
551 buf.append("<!-- Body content was not serialized -->\n");
552 return buf.toString();
553 }
554 for (int i = 0; i < externalNodes.size(); i++) {
555 Source input = new DOMSource((Node) externalNodes.get(i));
556 StringWriter out = new StringWriter();
557 Result output = new StreamResult(out);
558 try {
559 XFORMER.transform(input, output);
560 } catch (TransformerException te) {
561 org.apache.commons.logging.Log log = LogFactory.
562 getLog(SCXMLSerializer.class);
563 log.error(te.getMessage(), te);
564 buf.append("<!-- Not all body content was serialized -->");
565 }
566 buf.append(out.toString()).append("\n");
567 }
568 return buf.toString();
569 }
570
571 /**
572 * Serialize this If object.
573 *
574 * @param b The buffer to append the serialization to
575 * @param iff The If object to serialize
576 * @param indent The indent for this XML element
577 */
578 public static void serializeIf(final StringBuffer b,
579 final If iff, final String indent) {
580 b.append(indent).append("<if cond=\"").append(SCXMLHelper.
581 escapeXML(iff.getCond())).append("\">\n");
582 serializeActions(b, iff.getActions(), indent + INDENT);
583 b.append(indent).append("</if>\n");
584 }
585
586 /**
587 * Serialize properties of TransitionTarget which are element attributes.
588 *
589 * @param b The buffer to append the serialization to
590 * @param t The TransitionTarget
591 */
592 private static void serializeTransitionTargetAttributes(
593 final StringBuffer b, final TransitionTarget t) {
594 String id = t.getId();
595 if (id != null) {
596 b.append(" id=\"").append(id).append("\"");
597 }
598 }
599
600 /**
601 * Serialize namespace declarations for the root SCXML element.
602 *
603 * @param holder The {@link NamespacePrefixesHolder} object
604 * @return The serialized namespace declarations
605 */
606 private static String serializeNamespaceDeclarations(
607 final NamespacePrefixesHolder holder) {
608 Map ns = holder.getNamespaces();
609 StringBuffer b = new StringBuffer();
610 if (ns != null) {
611 Iterator iter = ns.entrySet().iterator();
612 while (iter.hasNext()) {
613 Map.Entry entry = (Map.Entry) iter.next();
614 String prefix = (String) entry.getKey();
615 String nsURI = (String) entry.getValue();
616 if (prefix.length() == 0 && !nsURI.equals(NAMESPACE_SCXML)) {
617 org.apache.commons.logging.Log log = LogFactory.
618 getLog(SCXMLSerializer.class);
619 log.warn("When using the SCXMLSerializer, the default "
620 + "namespace must be the SCXML namespace:"
621 + NAMESPACE_SCXML);
622 } if (prefix.equals("cs")
623 && !nsURI.equals(NAMESPACE_COMMONS_SCXML)) {
624 org.apache.commons.logging.Log log = LogFactory.
625 getLog(SCXMLSerializer.class);
626 log.warn("When using the SCXMLSerializer, the namespace"
627 + "prefix \"cs\" must bind to the Commons SCXML "
628 + "namespace:" + NAMESPACE_COMMONS_SCXML);
629 } else if (prefix.length() > 0) {
630 b.append(" xmlns:").append(prefix).append("=\"").
631 append(nsURI).append("\"");
632 }
633 }
634 }
635 return b.toString();
636 }
637
638 /**
639 * Get a <code>Transformer</code> instance.
640 *
641 * @return Transformer The <code>Transformer</code> instance.
642 */
643 private static Transformer getTransformer() {
644 Transformer transformer = null;
645 Properties outputProps = new Properties();
646 outputProps.put(OutputKeys.OMIT_XML_DECLARATION, "yes");
647 outputProps.put(OutputKeys.STANDALONE, "no");
648 outputProps.put(OutputKeys.INDENT, "yes");
649 try {
650 TransformerFactory tfFactory = TransformerFactory.newInstance();
651 transformer = tfFactory.newTransformer();
652 transformer.setOutputProperties(outputProps);
653 } catch (Throwable t) {
654 return null;
655 }
656 return transformer;
657 }
658
659 /*
660 * Private methods.
661 */
662 /**
663 * Discourage instantiation since this is a utility class.
664 */
665 private SCXMLSerializer() {
666 super();
667 }
668
669 }
670