1/* 2 * Copyright (c) 2000, 2015, 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 */ 25package java.beans; 26 27import java.io.*; 28import java.util.*; 29import java.lang.reflect.*; 30import java.nio.charset.Charset; 31import java.nio.charset.CharsetEncoder; 32import java.nio.charset.IllegalCharsetNameException; 33import java.nio.charset.UnsupportedCharsetException; 34 35/** 36 * The {@code XMLEncoder} class is a complementary alternative to 37 * the {@code ObjectOutputStream} and can used to generate 38 * a textual representation of a <em>JavaBean</em> in the same 39 * way that the {@code ObjectOutputStream} can 40 * be used to create binary representation of {@code Serializable} 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} 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} continues to be recommended 57 * for interprocess communication and general purpose serialization. 58 * <p> 59 * The {@code XMLEncoder} 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} 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} 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 * <ul> 154 * <li> 155 * The default method name is "new". 156 * <li> 157 * A reference to a java class is written in the form 158 * <class>javax.swing.JButton</class>. 159 * <li> 160 * Instances of the wrapper classes for Java's primitive types are written 161 * using the name of the primitive type as the tag. For example, an 162 * instance of the {@code Integer} class could be written: 163 * <int>123</int>. Note that the {@code XMLEncoder} class 164 * uses Java's reflection package in which the conversion between 165 * Java's primitive types and their associated "wrapper classes" 166 * is handled internally. The API for the {@code XMLEncoder} class 167 * itself deals only with {@code Object}s. 168 * <li> 169 * In an element representing a nullary method whose name 170 * starts with "get", the "method" attribute is replaced 171 * with a "property" attribute whose value is given by removing 172 * the "get" prefix and decapitalizing the result. 173 * <li> 174 * In an element representing a monadic method whose name 175 * starts with "set", the "method" attribute is replaced 176 * with a "property" attribute whose value is given by removing 177 * the "set" prefix and decapitalizing the result. 178 * <li> 179 * In an element representing a method named "get" taking one 180 * integer argument, the "method" attribute is replaced 181 * with an "index" attribute whose value the value of the 182 * first argument. 183 * <li> 184 * In an element representing a method named "set" taking two arguments, 185 * the first of which is an integer, the "method" attribute is replaced 186 * with an "index" attribute whose value the value of the 187 * first argument. 188 * <li> 189 * A reference to an array is written using the "array" 190 * tag. The "class" and "length" attributes specify the 191 * sub-type of the array and its length respectively. 192 * </ul> 193 * 194 *<p> 195 * For more information you might also want to check out 196 * <a href="http://www.oracle.com/technetwork/java/persistence4-140124.html"> 197 * Using XMLEncoder</a>, 198 * an article in <em>The Swing Connection.</em> 199 * @see XMLDecoder 200 * @see java.io.ObjectOutputStream 201 * 202 * @since 1.4 203 * 204 * @author Philip Milne 205 */ 206public class XMLEncoder extends Encoder implements AutoCloseable { 207 208 private final CharsetEncoder encoder; 209 private final String charset; 210 private final boolean declaration; 211 212 private OutputStreamWriter out; 213 private Object owner; 214 private int indentation = 0; 215 private boolean internal = false; 216 private Map<Object, ValueData> valueToExpression; 217 private Map<Object, List<Statement>> targetToStatementList; 218 private boolean preambleWritten = false; 219 private NameGenerator nameGenerator; 220 221 private class ValueData { 222 public int refs = 0; 223 public boolean marked = false; // Marked -> refs > 0 unless ref was a target. 224 public String name = null; 225 public Expression exp = null; 226 } 227 228 /** 229 * Creates a new XML encoder to write out <em>JavaBeans</em> 230 * to the stream {@code out} using an XML encoding. 231 * 232 * @param out the stream to which the XML representation of 233 * the objects will be written 234 * 235 * @throws IllegalArgumentException 236 * if {@code out} is {@code null} 237 * 238 * @see XMLDecoder#XMLDecoder(InputStream) 239 */ 240 public XMLEncoder(OutputStream out) { 241 this(out, "UTF-8", true, 0); 242 } 243 244 /** 245 * Creates a new XML encoder to write out <em>JavaBeans</em> 246 * to the stream {@code out} using the given {@code charset} 247 * starting from the given {@code indentation}. 248 * 249 * @param out the stream to which the XML representation of 250 * the objects will be written 251 * @param charset the name of the requested charset; 252 * may be either a canonical name or an alias 253 * @param declaration whether the XML declaration should be generated; 254 * set this to {@code false} 255 * when embedding the contents in another XML document 256 * @param indentation the number of space characters to indent the entire XML document by 257 * 258 * @throws IllegalArgumentException 259 * if {@code out} or {@code charset} is {@code null}, 260 * or if {@code indentation} is less than 0 261 * 262 * @throws IllegalCharsetNameException 263 * if {@code charset} name is illegal 264 * 265 * @throws UnsupportedCharsetException 266 * if no support for the named charset is available 267 * in this instance of the Java virtual machine 268 * 269 * @throws UnsupportedOperationException 270 * if loaded charset does not support encoding 271 * 272 * @see Charset#forName(String) 273 * 274 * @since 1.7 275 */ 276 public XMLEncoder(OutputStream out, String charset, boolean declaration, int indentation) { 277 if (out == null) { 278 throw new IllegalArgumentException("the output stream cannot be null"); 279 } 280 if (indentation < 0) { 281 throw new IllegalArgumentException("the indentation must be >= 0"); 282 } 283 Charset cs = Charset.forName(charset); 284 this.encoder = cs.newEncoder(); 285 this.charset = charset; 286 this.declaration = declaration; 287 this.indentation = indentation; 288 this.out = new OutputStreamWriter(out, cs.newEncoder()); 289 valueToExpression = new IdentityHashMap<>(); 290 targetToStatementList = new IdentityHashMap<>(); 291 nameGenerator = new NameGenerator(); 292 } 293 294 /** 295 * Sets the owner of this encoder to {@code owner}. 296 * 297 * @param owner The owner of this encoder. 298 * 299 * @see #getOwner 300 */ 301 public void setOwner(Object owner) { 302 this.owner = owner; 303 writeExpression(new Expression(this, "getOwner", new Object[0])); 304 } 305 306 /** 307 * Gets the owner of this encoder. 308 * 309 * @return The owner of this encoder. 310 * 311 * @see #setOwner 312 */ 313 public Object getOwner() { 314 return owner; 315 } 316 317 /** 318 * Write an XML representation of the specified object to the output. 319 * 320 * @param o The object to be written to the stream. 321 * 322 * @see XMLDecoder#readObject 323 */ 324 public void writeObject(Object o) { 325 if (internal) { 326 super.writeObject(o); 327 } 328 else { 329 writeStatement(new Statement(this, "writeObject", new Object[]{o})); 330 } 331 } 332 333 private List<Statement> statementList(Object target) { 334 List<Statement> list = targetToStatementList.get(target); 335 if (list == null) { 336 list = new ArrayList<>(); 337 targetToStatementList.put(target, list); 338 } 339 return list; 340 } 341 342 343 private void mark(Object o, boolean isArgument) { 344 if (o == null || o == this) { 345 return; 346 } 347 ValueData d = getValueData(o); 348 Expression exp = d.exp; 349 // Do not mark liternal strings. Other strings, which might, 350 // for example, come from resource bundles should still be marked. 351 if (o.getClass() == String.class && exp == null) { 352 return; 353 } 354 355 // Bump the reference counts of all arguments 356 if (isArgument) { 357 d.refs++; 358 } 359 if (d.marked) { 360 return; 361 } 362 d.marked = true; 363 Object target = exp.getTarget(); 364 mark(exp); 365 if (!(target instanceof Class)) { 366 statementList(target).add(exp); 367 // Pending: Why does the reference count need to 368 // be incremented here? 369 d.refs++; 370 } 371 } 372 373 private void mark(Statement stm) { 374 Object[] args = stm.getArguments(); 375 for (int i = 0; i < args.length; i++) { 376 Object arg = args[i]; 377 mark(arg, true); 378 } 379 mark(stm.getTarget(), stm instanceof Expression); 380 } 381 382 383 /** 384 * Records the Statement so that the Encoder will 385 * produce the actual output when the stream is flushed. 386 * <P> 387 * This method should only be invoked within the context 388 * of initializing a persistence delegate. 389 * 390 * @param oldStm The statement that will be written 391 * to the stream. 392 * @see java.beans.PersistenceDelegate#initialize 393 */ 394 public void writeStatement(Statement oldStm) { 395 // System.out.println("XMLEncoder::writeStatement: " + oldStm); 396 boolean internal = this.internal; 397 this.internal = true; 398 try { 399 super.writeStatement(oldStm); 400 /* 401 Note we must do the mark first as we may 402 require the results of previous values in 403 this context for this statement. 404 Test case is: 405 os.setOwner(this); 406 os.writeObject(this); 407 */ 408 mark(oldStm); 409 Object target = oldStm.getTarget(); 410 if (target instanceof Field) { 411 String method = oldStm.getMethodName(); 412 Object[] args = oldStm.getArguments(); 413 if ((method == null) || (args == null)) { 414 } 415 else if (method.equals("get") && (args.length == 1)) { 416 target = args[0]; 417 } 418 else if (method.equals("set") && (args.length == 2)) { 419 target = args[0]; 420 } 421 } 422 statementList(target).add(oldStm); 423 } 424 catch (Exception e) { 425 getExceptionListener().exceptionThrown(new Exception("XMLEncoder: discarding statement " + oldStm, e)); 426 } 427 this.internal = internal; 428 } 429 430 431 /** 432 * Records the Expression so that the Encoder will 433 * produce the actual output when the stream is flushed. 434 * <P> 435 * This method should only be invoked within the context of 436 * initializing a persistence delegate or setting up an encoder to 437 * read from a resource bundle. 438 * <P> 439 * For more information about using resource bundles with the 440 * XMLEncoder, see 441 * <a href="http://www.oracle.com/technetwork/java/persistence4-140124.html#i18n"> 442 * Creating Internationalized Applications</a>, 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} 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 Statement statement = getMissedStatement(); 491 while (statement != null) { 492 outputStatement(statement, this, false); 493 statement = getMissedStatement(); 494 } 495 496 try { 497 out.flush(); 498 } 499 catch (IOException e) { 500 getExceptionListener().exceptionThrown(e); 501 } 502 clear(); 503 } 504 505 void clear() { 506 super.clear(); 507 nameGenerator.clear(); 508 valueToExpression.clear(); 509 targetToStatementList.clear(); 510 } 511 512 Statement getMissedStatement() { 513 for (List<Statement> statements : this.targetToStatementList.values()) { 514 for (int i = 0; i < statements.size(); i++) { 515 if (Statement.class == statements.get(i).getClass()) { 516 return statements.remove(i); 517 } 518 } 519 } 520 return null; 521 } 522 523 524 /** 525 * This method calls {@code flush}, writes the closing 526 * postamble and then closes the output stream associated 527 * with this stream. 528 */ 529 public void close() { 530 flush(); 531 writeln("</java>"); 532 try { 533 out.close(); 534 } 535 catch (IOException e) { 536 getExceptionListener().exceptionThrown(e); 537 } 538 } 539 540 private String quote(String s) { 541 return "\"" + s + "\""; 542 } 543 544 private ValueData getValueData(Object o) { 545 ValueData d = valueToExpression.get(o); 546 if (d == null) { 547 d = new ValueData(); 548 valueToExpression.put(o, d); 549 } 550 return d; 551 } 552 553 /** 554 * Returns {@code true} if the argument, 555 * a Unicode code point, is valid in XML documents. 556 * Unicode characters fit into the low sixteen bits of a Unicode code point, 557 * and pairs of Unicode <em>surrogate characters</em> can be combined 558 * to encode Unicode code point in documents containing only Unicode. 559 * (The {@code char} datatype in the Java Programming Language 560 * represents Unicode characters, including unpaired surrogates.) 561 * <par> 562 * [2] Char ::= #x0009 | #x000A | #x000D 563 * | [#x0020-#xD7FF] 564 * | [#xE000-#xFFFD] 565 * | [#x10000-#x10ffff] 566 * </par> 567 * 568 * @param code the 32-bit Unicode code point being tested 569 * @return {@code true} if the Unicode code point is valid, 570 * {@code false} otherwise 571 */ 572 private static boolean isValidCharCode(int code) { 573 return (0x0020 <= code && code <= 0xD7FF) 574 || (0x000A == code) 575 || (0x0009 == code) 576 || (0x000D == code) 577 || (0xE000 <= code && code <= 0xFFFD) 578 || (0x10000 <= code && code <= 0x10ffff); 579 } 580 581 private void writeln(String exp) { 582 try { 583 StringBuilder sb = new StringBuilder(); 584 for(int i = 0; i < indentation; i++) { 585 sb.append(' '); 586 } 587 sb.append(exp); 588 sb.append('\n'); 589 this.out.write(sb.toString()); 590 } 591 catch (IOException e) { 592 getExceptionListener().exceptionThrown(e); 593 } 594 } 595 596 private void outputValue(Object value, Object outer, boolean isArgument) { 597 if (value == null) { 598 writeln("<null/>"); 599 return; 600 } 601 602 if (value instanceof Class) { 603 writeln("<class>" + ((Class)value).getName() + "</class>"); 604 return; 605 } 606 607 ValueData d = getValueData(value); 608 if (d.exp != null) { 609 Object target = d.exp.getTarget(); 610 String methodName = d.exp.getMethodName(); 611 612 if (target == null || methodName == null) { 613 throw new NullPointerException((target == null ? "target" : 614 "methodName") + " should not be null"); 615 } 616 617 if (isArgument && target instanceof Field && methodName.equals("get")) { 618 Field f = (Field) target; 619 if (Modifier.isStatic(f.getModifiers())) { 620 writeln("<object class=" + quote(f.getDeclaringClass().getName()) + 621 " field=" + quote(f.getName()) + "/>"); 622 return; 623 } 624 } 625 626 Class<?> primitiveType = primitiveTypeFor(value.getClass()); 627 if (primitiveType != null && target == value.getClass() && 628 methodName.equals("new")) { 629 String primitiveTypeName = primitiveType.getName(); 630 // Make sure that character types are quoted correctly. 631 if (primitiveType == Character.TYPE) { 632 char code = ((Character) value).charValue(); 633 if (!isValidCharCode(code)) { 634 writeln(createString(code)); 635 return; 636 } 637 value = quoteCharCode(code); 638 if (value == null) { 639 value = Character.valueOf(code); 640 } 641 } 642 writeln("<" + primitiveTypeName + ">" + value + "</" + 643 primitiveTypeName + ">"); 644 return; 645 } 646 647 } else if (value instanceof String) { 648 writeln(createString((String) value)); 649 return; 650 } 651 652 if (d.name != null) { 653 if (isArgument) { 654 writeln("<object idref=" + quote(d.name) + "/>"); 655 } 656 else { 657 outputXML("void", " idref=" + quote(d.name), value); 658 } 659 } 660 else if (d.exp != null) { 661 outputStatement(d.exp, outer, isArgument); 662 } 663 } 664 665 private static String quoteCharCode(int code) { 666 switch(code) { 667 case '&': return "&"; 668 case '<': return "<"; 669 case '>': return ">"; 670 case '"': return """; 671 case '\'': return "'"; 672 case '\r': return " "; 673 default: return null; 674 } 675 } 676 677 private static String createString(int code) { 678 return "<char code=\"#" + Integer.toString(code, 16) + "\"/>"; 679 } 680 681 private String createString(String string) { 682 StringBuilder sb = new StringBuilder(); 683 sb.append("<string>"); 684 int index = 0; 685 while (index < string.length()) { 686 int point = string.codePointAt(index); 687 int count = Character.charCount(point); 688 689 if (isValidCharCode(point) && this.encoder.canEncode(string.substring(index, index + count))) { 690 String value = quoteCharCode(point); 691 if (value != null) { 692 sb.append(value); 693 } else { 694 sb.appendCodePoint(point); 695 } 696 index += count; 697 } else { 698 sb.append(createString(string.charAt(index))); 699 index++; 700 } 701 } 702 sb.append("</string>"); 703 return sb.toString(); 704 } 705 706 private void outputStatement(Statement exp, Object outer, boolean isArgument) { 707 Object target = exp.getTarget(); 708 String methodName = exp.getMethodName(); 709 710 if (target == null || methodName == null) { 711 throw new NullPointerException((target == null ? "target" : 712 "methodName") + " should not be null"); 713 } 714 715 Object[] args = exp.getArguments(); 716 boolean expression = exp.getClass() == Expression.class; 717 Object value = (expression) ? getValue((Expression)exp) : null; 718 719 String tag = (expression && isArgument) ? "object" : "void"; 720 String attributes = ""; 721 ValueData d = getValueData(value); 722 723 // Special cases for targets. 724 if (target == outer) { 725 } 726 else if (target == Array.class && methodName.equals("newInstance")) { 727 tag = "array"; 728 attributes = attributes + " class=" + quote(((Class)args[0]).getName()); 729 attributes = attributes + " length=" + quote(args[1].toString()); 730 args = new Object[]{}; 731 } 732 else if (target.getClass() == Class.class) { 733 attributes = attributes + " class=" + quote(((Class)target).getName()); 734 } 735 else { 736 d.refs = 2; 737 if (d.name == null) { 738 getValueData(target).refs++; 739 List<Statement> statements = statementList(target); 740 if (!statements.contains(exp)) { 741 statements.add(exp); 742 } 743 outputValue(target, outer, false); 744 } 745 if (expression) { 746 outputValue(value, outer, isArgument); 747 } 748 return; 749 } 750 if (expression && (d.refs > 1)) { 751 String instanceName = nameGenerator.instanceName(value); 752 d.name = instanceName; 753 attributes = attributes + " id=" + quote(instanceName); 754 } 755 756 // Special cases for methods. 757 if ((!expression && methodName.equals("set") && args.length == 2 && 758 args[0] instanceof Integer) || 759 (expression && methodName.equals("get") && args.length == 1 && 760 args[0] instanceof Integer)) { 761 attributes = attributes + " index=" + quote(args[0].toString()); 762 args = (args.length == 1) ? new Object[]{} : new Object[]{args[1]}; 763 } 764 else if ((!expression && methodName.startsWith("set") && args.length == 1) || 765 (expression && methodName.startsWith("get") && args.length == 0)) { 766 if (3 < methodName.length()) { 767 attributes = attributes + " property=" + 768 quote(Introspector.decapitalize(methodName.substring(3))); 769 } 770 } 771 else if (!methodName.equals("new") && !methodName.equals("newInstance")) { 772 attributes = attributes + " method=" + quote(methodName); 773 } 774 outputXML(tag, attributes, value, args); 775 } 776 777 private void outputXML(String tag, String attributes, Object value, Object... args) { 778 List<Statement> statements = statementList(value); 779 // Use XML's short form when there is no body. 780 if (args.length == 0 && statements.size() == 0) { 781 writeln("<" + tag + attributes + "/>"); 782 return; 783 } 784 785 writeln("<" + tag + attributes + ">"); 786 indentation++; 787 788 for(int i = 0; i < args.length; i++) { 789 outputValue(args[i], null, true); 790 } 791 792 while (!statements.isEmpty()) { 793 Statement s = statements.remove(0); 794 outputStatement(s, value, false); 795 } 796 797 indentation--; 798 writeln("</" + tag + ">"); 799 } 800 801 @SuppressWarnings("rawtypes") 802 static Class primitiveTypeFor(Class wrapper) { 803 if (wrapper == Boolean.class) return Boolean.TYPE; 804 if (wrapper == Byte.class) return Byte.TYPE; 805 if (wrapper == Character.class) return Character.TYPE; 806 if (wrapper == Short.class) return Short.TYPE; 807 if (wrapper == Integer.class) return Integer.TYPE; 808 if (wrapper == Long.class) return Long.TYPE; 809 if (wrapper == Float.class) return Float.TYPE; 810 if (wrapper == Double.class) return Double.TYPE; 811 if (wrapper == Void.class) return Void.TYPE; 812 return null; 813 } 814} 815