1 /* 2 * Copyright (c) 2000, 2010, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 package java.beans; 26 27 import java.io.*; 28 import java.util.*; 29 import java.lang.reflect.*; 30 import java.nio.charset.Charset; 31 import java.nio.charset.CharsetEncoder; 32 import java.nio.charset.IllegalCharsetNameException; 33 import java.nio.charset.UnsupportedCharsetException; 34 35 /** 36 * The <code>XMLEncoder</code> class is a complementary alternative to 37 * the <code>ObjectOutputStream</code> and can used to generate 38 * a textual representation of a <em>JavaBean</em> in the same 39 * way that the <code>ObjectOutputStream</code> can 40 * be used to create binary representation of <code>Serializable</code> 41 * objects. For example, the following fragment can be used to create 42 * a textual representation the supplied <em>JavaBean</em> 43 * and all its properties: 44 * <pre> 45 * XMLEncoder e = new XMLEncoder( 46 * new BufferedOutputStream( 47 * new FileOutputStream("Test.xml"))); 48 * e.writeObject(new JButton("Hello, world")); 49 * e.close(); 50 * </pre> 51 * Despite the similarity of their APIs, the <code>XMLEncoder</code> 52 * class is exclusively designed for the purpose of archiving graphs 53 * of <em>JavaBean</em>s as textual representations of their public 54 * properties. Like Java source files, documents written this way 55 * have a natural immunity to changes in the implementations of the classes 56 * involved. The <code>ObjectOutputStream</code> continues to be recommended 57 * for interprocess communication and general purpose serialization. 58 * <p> 59 * The <code>XMLEncoder</code> class provides a default denotation for 60 * <em>JavaBean</em>s in which they are represented as XML documents 61 * complying with version 1.0 of the XML specification and the 62 * UTF-8 character encoding of the Unicode/ISO 10646 character set. 63 * The XML documents produced by the <code>XMLEncoder</code> class are: 64 * <ul> 65 * <li> 66 * <em>Portable and version resilient</em>: they have no dependencies 67 * on the private implementation of any class and so, like Java source 68 * files, they may be exchanged between environments which may have 69 * different versions of some of the classes and between VMs from 70 * different vendors. 71 * <li> 72 * <em>Structurally compact</em>: The <code>XMLEncoder</code> class 73 * uses a <em>redundancy elimination</em> algorithm internally so that the 74 * default values of a Bean's properties are not written to the stream. 75 * <li> 76 * <em>Fault tolerant</em>: Non-structural errors in the file, 77 * caused either by damage to the file or by API changes 78 * made to classes in an archive remain localized 79 * so that a reader can report the error and continue to load the parts 80 * of the document which were not affected by the error. 81 * </ul> 82 * <p> 83 * Below is an example of an XML archive containing 84 * some user interface components from the <em>swing</em> toolkit: 85 * <pre> 86 * <?xml version="1.0" encoding="UTF-8"?> 87 * <java version="1.0" class="java.beans.XMLDecoder"> 88 * <object class="javax.swing.JFrame"> 89 * <void property="name"> 90 * <string>frame1</string> 91 * </void> 92 * <void property="bounds"> 93 * <object class="java.awt.Rectangle"> 94 * <int>0</int> 95 * <int>0</int> 96 * <int>200</int> 97 * <int>200</int> 98 * </object> 99 * </void> 100 * <void property="contentPane"> 101 * <void method="add"> 102 * <object class="javax.swing.JButton"> 103 * <void property="label"> 104 * <string>Hello</string> 105 * </void> 106 * </object> 107 * </void> 108 * </void> 109 * <void property="visible"> 110 * <boolean>true</boolean> 111 * </void> 112 * </object> 113 * </java> 114 * </pre> 115 * The XML syntax uses the following conventions: 116 * <ul> 117 * <li> 118 * Each element represents a method call. 119 * <li> 120 * The "object" tag denotes an <em>expression</em> whose value is 121 * to be used as the argument to the enclosing element. 122 * <li> 123 * The "void" tag denotes a <em>statement</em> which will 124 * be executed, but whose result will not be used as an 125 * argument to the enclosing method. 126 * <li> 127 * Elements which contain elements use those elements as arguments, 128 * unless they have the tag: "void". 129 * <li> 130 * The name of the method is denoted by the "method" attribute. 131 * <li> 132 * XML's standard "id" and "idref" attributes are used to make 133 * references to previous expressions - so as to deal with 134 * circularities in the object graph. 135 * <li> 136 * The "class" attribute is used to specify the target of a static 137 * method or constructor explicitly; its value being the fully 138 * qualified name of the class. 139 * <li> 140 * Elements with the "void" tag are executed using 141 * the outer context as the target if no target is defined 142 * by a "class" attribute. 143 * <li> 144 * Java's String class is treated specially and is 145 * written <string>Hello, world</string> where 146 * the characters of the string are converted to bytes 147 * using the UTF-8 character encoding. 148 * </ul> 149 * <p> 150 * Although all object graphs may be written using just these three 151 * tags, the following definitions are included so that common 152 * data structures can be expressed more concisely: 153 * <p> 154 * <ul> 155 * <li> 156 * The default method name is "new". 157 * <li> 158 * A reference to a java class is written in the form 159 * <class>javax.swing.JButton</class>. 160 * <li> 161 * Instances of the wrapper classes for Java's primitive types are written 162 * using the name of the primitive type as the tag. For example, an 163 * instance of the <code>Integer</code> class could be written: 164 * <int>123</int>. Note that the <code>XMLEncoder</code> class 165 * uses Java's reflection package in which the conversion between 166 * Java's primitive types and their associated "wrapper classes" 167 * is handled internally. The API for the <code>XMLEncoder</code> class 168 * itself deals only with <code>Object</code>s. 169 * <li> 170 * In an element representing a nullary method whose name 171 * starts with "get", the "method" attribute is replaced 172 * with a "property" attribute whose value is given by removing 173 * the "get" prefix and decapitalizing the result. 174 * <li> 175 * In an element representing a monadic method whose name 176 * starts with "set", the "method" attribute is replaced 177 * with a "property" attribute whose value is given by removing 178 * the "set" prefix and decapitalizing the result. 179 * <li> 180 * In an element representing a method named "get" taking one 181 * integer argument, the "method" attribute is replaced 182 * with an "index" attribute whose value the value of the 183 * first argument. 184 * <li> 185 * In an element representing a method named "set" taking two arguments, 186 * the first of which is an integer, the "method" attribute is replaced 187 * with an "index" attribute whose value the value of the 188 * first argument. 189 * <li> 190 * A reference to an array is written using the "array" 191 * tag. The "class" and "length" attributes specify the 192 * sub-type of the array and its length respectively. 193 * </ul> 194 * 195 *<p> 196 * For more information you might also want to check out 197 * <a 198 href="http://java.sun.com/products/jfc/tsc/articles/persistence4">Using XMLEncoder</a>, 199 * an article in <em>The Swing Connection.</em> 200 * @see XMLDecoder 201 * @see java.io.ObjectOutputStream 202 * 203 * @since 1.4 204 * 205 * @author Philip Milne 206 */ 207 public class XMLEncoder extends Encoder implements AutoCloseable { 208 209 private final CharsetEncoder encoder; 210 private final String charset; 211 private final boolean declaration; 212 213 private OutputStreamWriter out; 214 private Object owner; 215 private int indentation = 0; 216 private boolean internal = false; 217 private Map<Object, ValueData> valueToExpression; 218 private Map<Object, List<Statement>> targetToStatementList; 219 private boolean preambleWritten = false; 220 private NameGenerator nameGenerator; 221 222 private class ValueData { 223 public int refs = 0; 224 public boolean marked = false; // Marked -> refs > 0 unless ref was a target. 225 public String name = null; 226 public Expression exp = null; 227 } 228 229 /** 230 * Creates a new XML encoder to write out <em>JavaBeans</em> 231 * to the stream <code>out</code> using an XML encoding. 232 * 233 * @param out the stream to which the XML representation of 234 * the objects will be written 235 * 236 * @throws IllegalArgumentException 237 * if <code>out</code> is <code>null</code> 238 * 239 * @see XMLDecoder#XMLDecoder(InputStream) 240 */ 241 public XMLEncoder(OutputStream out) { 242 this(out, "UTF-8", true, 0); 243 } 244 245 /** 246 * Creates a new XML encoder to write out <em>JavaBeans</em> 247 * to the stream <code>out</code> using the given <code>charset</code> 248 * starting from the given <code>indentation</code>. 249 * 250 * @param out the stream to which the XML representation of 251 * the objects will be written 252 * @param charset the name of the requested charset; 253 * may be either a canonical name or an alias 254 * @param declaration whether the XML declaration should be generated; 255 * set this to <code>false</code> 256 * when embedding the contents in another XML document 257 * @param indentation the number of space characters to indent the entire XML document by 258 * 259 * @throws IllegalArgumentException 260 * if <code>out</code> or <code>charset</code> is <code>null</code>, 261 * or if <code>indentation</code> is less than 0 262 * 263 * @throws IllegalCharsetNameException 264 * if <code>charset</code> name is illegal 265 * 266 * @throws UnsupportedCharsetException 267 * if no support for the named charset is available 268 * in this instance of the Java virtual machine 269 * 270 * @throws UnsupportedOperationException 271 * if loaded charset does not support encoding 272 * 273 * @see Charset#forName(String) 274 * 275 * @since 1.7 276 */ 277 public XMLEncoder(OutputStream out, String charset, boolean declaration, int indentation) { 278 if (out == null) { 279 throw new IllegalArgumentException("the output stream cannot be null"); 280 } 281 if (indentation < 0) { 282 throw new IllegalArgumentException("the indentation must be >= 0"); 283 } 284 Charset cs = Charset.forName(charset); 285 this.encoder = cs.newEncoder(); 286 this.charset = charset; 287 this.declaration = declaration; 288 this.indentation = indentation; 289 this.out = new OutputStreamWriter(out, cs.newEncoder()); 290 valueToExpression = new IdentityHashMap<Object, ValueData>(); 291 targetToStatementList = new IdentityHashMap<Object, List<Statement>>(); 292 nameGenerator = new NameGenerator(); 293 } 294 295 /** 296 * Sets the owner of this encoder to <code>owner</code>. 297 * 298 * @param owner The owner of this encoder. 299 * 300 * @see #getOwner 301 */ 302 public void setOwner(Object owner) { 303 this.owner = owner; 304 writeExpression(new Expression(this, "getOwner", new Object[0])); 305 } 306 307 /** 308 * Gets the owner of this encoder. 309 * 310 * @return The owner of this encoder. 311 * 312 * @see #setOwner 313 */ 314 public Object getOwner() { 315 return owner; 316 } 317 318 /** 319 * Write an XML representation of the specified object to the output. 320 * 321 * @param o The object to be written to the stream. 322 * 323 * @see XMLDecoder#readObject 324 */ 325 public void writeObject(Object o) { 326 if (internal) { 327 super.writeObject(o); 328 } 329 else { 330 writeStatement(new Statement(this, "writeObject", new Object[]{o})); 331 } 332 } 333 334 private List<Statement> statementList(Object target) { 335 List<Statement> list = targetToStatementList.get(target); 336 if (list == null) { 337 list = new ArrayList<Statement>(); 338 targetToStatementList.put(target, list); 339 } 340 return list; 341 } 342 343 344 private void mark(Object o, boolean isArgument) { 345 if (o == null || o == this) { 346 return; 347 } 348 ValueData d = getValueData(o); 349 Expression exp = d.exp; 350 // Do not mark liternal strings. Other strings, which might, 351 // for example, come from resource bundles should still be marked. 352 if (o.getClass() == String.class && exp == null) { 353 return; 354 } 355 356 // Bump the reference counts of all arguments 357 if (isArgument) { 358 d.refs++; 359 } 360 if (d.marked) { 361 return; 362 } 363 d.marked = true; 364 Object target = exp.getTarget(); 365 mark(exp); 366 if (!(target instanceof Class)) { 367 statementList(target).add(exp); 368 // Pending: Why does the reference count need to 369 // be incremented here? 370 d.refs++; 371 } 372 } 373 374 private void mark(Statement stm) { 375 Object[] args = stm.getArguments(); 376 for (int i = 0; i < args.length; i++) { 377 Object arg = args[i]; 378 mark(arg, true); 379 } 380 mark(stm.getTarget(), false); 381 } 382 383 384 /** 385 * Records the Statement so that the Encoder will 386 * produce the actual output when the stream is flushed. 387 * <P> 388 * This method should only be invoked within the context 389 * of initializing a persistence delegate. 390 * 391 * @param oldStm The statement that will be written 392 * to the stream. 393 * @see java.beans.PersistenceDelegate#initialize 394 */ 395 public void writeStatement(Statement oldStm) { 396 // System.out.println("XMLEncoder::writeStatement: " + oldStm); 397 boolean internal = this.internal; 398 this.internal = true; 399 try { 400 super.writeStatement(oldStm); 401 /* 402 Note we must do the mark first as we may 403 require the results of previous values in 404 this context for this statement. 405 Test case is: 406 os.setOwner(this); 407 os.writeObject(this); 408 */ 409 mark(oldStm); 410 Object target = oldStm.getTarget(); 411 if (target instanceof Field) { 412 String method = oldStm.getMethodName(); 413 Object[] args = oldStm.getArguments(); 414 if ((method == null) || (args == null)) { 415 } 416 else if (method.equals("get") && (args.length == 1)) { 417 target = args[0]; 418 } 419 else if (method.equals("set") && (args.length == 2)) { 420 target = args[0]; 421 } 422 } 423 statementList(target).add(oldStm); 424 } 425 catch (Exception e) { 426 getExceptionListener().exceptionThrown(new Exception("XMLEncoder: discarding statement " + oldStm, e)); 427 } 428 this.internal = internal; 429 } 430 431 432 /** 433 * Records the Expression so that the Encoder will 434 * produce the actual output when the stream is flushed. 435 * <P> 436 * This method should only be invoked within the context of 437 * initializing a persistence delegate or setting up an encoder to 438 * read from a resource bundle. 439 * <P> 440 * For more information about using resource bundles with the 441 * XMLEncoder, see 442 * http://java.sun.com/products/jfc/tsc/articles/persistence4/#i18n 443 * 444 * @param oldExp The expression that will be written 445 * to the stream. 446 * @see java.beans.PersistenceDelegate#initialize 447 */ 448 public void writeExpression(Expression oldExp) { 449 boolean internal = this.internal; 450 this.internal = true; 451 Object oldValue = getValue(oldExp); 452 if (get(oldValue) == null || (oldValue instanceof String && !internal)) { 453 getValueData(oldValue).exp = oldExp; 454 super.writeExpression(oldExp); 455 } 456 this.internal = internal; 457 } 458 459 /** 460 * This method writes out the preamble associated with the 461 * XML encoding if it has not been written already and 462 * then writes out all of the values that been 463 * written to the stream since the last time <code>flush</code> 464 * was called. After flushing, all internal references to the 465 * values that were written to this stream are cleared. 466 */ 467 public void flush() { 468 if (!preambleWritten) { // Don't do this in constructor - it throws ... pending. 469 if (this.declaration) { 470 writeln("<?xml version=" + quote("1.0") + 471 " encoding=" + quote(this.charset) + "?>"); 472 } 473 writeln("<java version=" + quote(System.getProperty("java.version")) + 474 " class=" + quote(XMLDecoder.class.getName()) + ">"); 475 preambleWritten = true; 476 } 477 indentation++; 478 List<Statement> statements = statementList(this); 479 while (!statements.isEmpty()) { 480 Statement s = statements.remove(0); 481 if ("writeObject".equals(s.getMethodName())) { 482 outputValue(s.getArguments()[0], this, true); 483 } 484 else { 485 outputStatement(s, this, false); 486 } 487 } 488 indentation--; 489 490 try { 491 out.flush(); 492 } 493 catch (IOException e) { 494 getExceptionListener().exceptionThrown(e); 495 } 496 clear(); 497 } 498 499 void clear() { 500 super.clear(); 501 nameGenerator.clear(); 502 valueToExpression.clear(); 503 targetToStatementList.clear(); 504 } 505 506 507 /** 508 * This method calls <code>flush</code>, writes the closing 509 * postamble and then closes the output stream associated 510 * with this stream. 511 */ 512 public void close() { 513 flush(); 514 writeln("</java>"); 515 try { 516 out.close(); 517 } 518 catch (IOException e) { 519 getExceptionListener().exceptionThrown(e); 520 } 521 } 522 523 private String quote(String s) { 524 return "\"" + s + "\""; 525 } 526 527 private ValueData getValueData(Object o) { 528 ValueData d = valueToExpression.get(o); 529 if (d == null) { 530 d = new ValueData(); 531 valueToExpression.put(o, d); 532 } 533 return d; 534 } 535 536 /** 537 * Returns <code>true</code> if the argument, 538 * a Unicode code point, is valid in XML documents. 539 * Unicode characters fit into the low sixteen bits of a Unicode code point, 540 * and pairs of Unicode <em>surrogate characters</em> can be combined 541 * to encode Unicode code point in documents containing only Unicode. 542 * (The <code>char</code> datatype in the Java Programming Language 543 * represents Unicode characters, including unpaired surrogates.) 544 * <par> 545 * [2] Char ::= #x0009 | #x000A | #x000D 546 * | [#x0020-#xD7FF] 547 * | [#xE000-#xFFFD] 548 * | [#x10000-#x10ffff] 549 * </par> 550 * 551 * @param code the 32-bit Unicode code point being tested 552 * @return <code>true</code> if the Unicode code point is valid, 553 * <code>false</code> otherwise 554 */ 555 private static boolean isValidCharCode(int code) { 556 return (0x0020 <= code && code <= 0xD7FF) 557 || (0x000A == code) 558 || (0x0009 == code) 559 || (0x000D == code) 560 || (0xE000 <= code && code <= 0xFFFD) 561 || (0x10000 <= code && code <= 0x10ffff); 562 } 563 564 private void writeln(String exp) { 565 try { 566 StringBuilder sb = new StringBuilder(); 567 for(int i = 0; i < indentation; i++) { 568 sb.append(' '); 569 } 570 sb.append(exp); 571 sb.append('\n'); 572 this.out.write(sb.toString()); 573 } 574 catch (IOException e) { 575 getExceptionListener().exceptionThrown(e); 576 } 577 } 578 579 private void outputValue(Object value, Object outer, boolean isArgument) { 580 if (value == null) { 581 writeln("<null/>"); 582 return; 583 } 584 585 if (value instanceof Class) { 586 writeln("<class>" + ((Class)value).getName() + "</class>"); 587 return; 588 } 589 590 ValueData d = getValueData(value); 591 if (d.exp != null) { 592 Object target = d.exp.getTarget(); 593 String methodName = d.exp.getMethodName(); 594 595 if (target == null || methodName == null) { 596 throw new NullPointerException((target == null ? "target" : 597 "methodName") + " should not be null"); 598 } 599 600 if (target instanceof Field && methodName.equals("get")) { 601 Field f = (Field)target; 602 writeln("<object class=" + quote(f.getDeclaringClass().getName()) + 603 " field=" + quote(f.getName()) + "/>"); 604 return; 605 } 606 607 Class primitiveType = ReflectionUtils.primitiveTypeFor(value.getClass()); 608 if (primitiveType != null && target == value.getClass() && 609 methodName.equals("new")) { 610 String primitiveTypeName = primitiveType.getName(); 611 // Make sure that character types are quoted correctly. 612 if (primitiveType == Character.TYPE) { 613 char code = ((Character) value).charValue(); 614 if (!isValidCharCode(code)) { 615 writeln(createString(code)); 616 return; 617 } 618 value = quoteCharCode(code); 619 if (value == null) { 620 value = Character.valueOf(code); 621 } 622 } 623 writeln("<" + primitiveTypeName + ">" + value + "</" + 624 primitiveTypeName + ">"); 625 return; 626 } 627 628 } else if (value instanceof String) { 629 writeln(createString((String) value)); 630 return; 631 } 632 633 if (d.name != null) { 634 outputXML(isArgument ? "object" : "void", " idref=" + quote(d.name), value); 635 } 636 else if (d.exp != null) { 637 outputStatement(d.exp, outer, isArgument); 638 } 639 } 640 641 private static String quoteCharCode(int code) { 642 switch(code) { 643 case '&': return "&"; 644 case '<': return "<"; 645 case '>': return ">"; 646 case '"': return """; 647 case '\'': return "'"; 648 case '\r': return " "; 649 default: return null; 650 } 651 } 652 653 private static String createString(int code) { 654 return "<char code=\"#" + Integer.toString(code, 16) + "\"/>"; 655 } 656 657 private String createString(String string) { 658 StringBuilder sb = new StringBuilder(); 659 sb.append("<string>"); 660 int index = 0; 661 while (index < string.length()) { 662 int point = string.codePointAt(index); 663 int count = Character.charCount(point); 664 665 if (isValidCharCode(point) && this.encoder.canEncode(string.substring(index, index + count))) { 666 String value = quoteCharCode(point); 667 if (value != null) { 668 sb.append(value); 669 } else { 670 sb.appendCodePoint(point); 671 } 672 index += count; 673 } else { 674 sb.append(createString(string.charAt(index))); 675 index++; 676 } 677 } 678 sb.append("</string>"); 679 return sb.toString(); 680 } 681 682 private void outputStatement(Statement exp, Object outer, boolean isArgument) { 683 Object target = exp.getTarget(); 684 String methodName = exp.getMethodName(); 685 686 if (target == null || methodName == null) { 687 throw new NullPointerException((target == null ? "target" : 688 "methodName") + " should not be null"); 689 } 690 691 Object[] args = exp.getArguments(); 692 boolean expression = exp.getClass() == Expression.class; 693 Object value = (expression) ? getValue((Expression)exp) : null; 694 695 String tag = (expression && isArgument) ? "object" : "void"; 696 String attributes = ""; 697 ValueData d = getValueData(value); 698 699 // Special cases for targets. 700 if (target == outer) { 701 } 702 else if (target == Array.class && methodName.equals("newInstance")) { 703 tag = "array"; 704 attributes = attributes + " class=" + quote(((Class)args[0]).getName()); 705 attributes = attributes + " length=" + quote(args[1].toString()); 706 args = new Object[]{}; 707 } 708 else if (target.getClass() == Class.class) { 709 attributes = attributes + " class=" + quote(((Class)target).getName()); 710 } 711 else { 712 d.refs = 2; 713 getValueData(target).refs++; 714 List<Statement> statements = statementList(target); 715 if (!statements.contains(exp)) { 716 statements.add(exp); 717 } 718 outputValue(target, outer, false); 719 if (expression) { 720 outputValue(value, outer, isArgument); 721 } 722 return; 723 } 724 if (expression && (d.refs > 1)) { 725 String instanceName = nameGenerator.instanceName(value); 726 d.name = instanceName; 727 attributes = attributes + " id=" + quote(instanceName); 728 } 729 730 // Special cases for methods. 731 if ((!expression && methodName.equals("set") && args.length == 2 && 732 args[0] instanceof Integer) || 733 (expression && methodName.equals("get") && args.length == 1 && 734 args[0] instanceof Integer)) { 735 attributes = attributes + " index=" + quote(args[0].toString()); 736 args = (args.length == 1) ? new Object[]{} : new Object[]{args[1]}; 737 } 738 else if ((!expression && methodName.startsWith("set") && args.length == 1) || 739 (expression && methodName.startsWith("get") && args.length == 0)) { 740 if (3 < methodName.length()) { 741 attributes = attributes + " property=" + 742 quote(Introspector.decapitalize(methodName.substring(3))); 743 } 744 } 745 else if (!methodName.equals("new") && !methodName.equals("newInstance")) { 746 attributes = attributes + " method=" + quote(methodName); 747 } 748 outputXML(tag, attributes, value, args); 749 } 750 751 private void outputXML(String tag, String attributes, Object value, Object... args) { 752 List<Statement> statements = statementList(value); 753 // Use XML's short form when there is no body. 754 if (args.length == 0 && statements.size() == 0) { 755 writeln("<" + tag + attributes + "/>"); 756 return; 757 } 758 759 writeln("<" + tag + attributes + ">"); 760 indentation++; 761 762 for(int i = 0; i < args.length; i++) { 763 outputValue(args[i], null, true); 764 } 765 766 while (!statements.isEmpty()) { 767 Statement s = statements.remove(0); 768 outputStatement(s, value, false); 769 } 770 771 indentation--; 772 writeln("</" + tag + ">"); 773 } 774 }