Showing the changes to java/pkg/java/beans/XMLEncoder.java (1 of 18352 files). Show all
Unified · Split
| @@ -0,0 +1,774 @@ | ||
| 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 | +} | |