1. /*
  2. * @(#)XMLEncoder.java 1.25 03/01/27
  3. *
  4. * Copyright 2003 Sun Microsystems, Inc. All rights reserved.
  5. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
  6. */
  7. package java.beans;
  8. import java.io.*;
  9. import java.util.*;
  10. import java.lang.reflect.*;
  11. /**
  12. * The <code>XMLEncoder</code> class is a complementary alternative to
  13. * the <code>ObjectOutputStream</code> and can used to generate
  14. * a textual representation of a <em>JavaBean</em> in the same
  15. * way that the <code>ObjectOutputStream</code> can
  16. * be used to create binary representation of <code>Serializable</code>
  17. * objects. For example, the following fragment can be used to create
  18. * a textual representation the supplied <em>JavaBean</em>
  19. * and all its properties:
  20. * <pre>
  21. * XMLEncoder e = new XMLEncoder(
  22. * new BufferedOutputStream(
  23. * new FileOutputStream("Test.xml")));
  24. * e.writeObject(new JButton("Hello, world"));
  25. * e.close();
  26. * </pre>
  27. * Despite the similarity of their APIs, the <code>XMLEncoder</code>
  28. * class is exclusively designed for the purpose of archiving graphs
  29. * of <em>JavaBean</em>s as textual representations of their public
  30. * properties. Like Java source files, documents written this way
  31. * have a natural immunity to changes in the implementations of the classes
  32. * involved. The <code>ObjectOutputStream</code> continues to be recommended
  33. * for interprocess communication and general purpose serialization.
  34. * <p>
  35. * The <code>XMLEncoder</code> class provides a default denotation for
  36. * <em>JavaBean</em>s in which they are represented as XML documents
  37. * complying with version 1.0 of the XML specification and the
  38. * UTF-8 character encoding of the Unicode/ISO 10646 character set.
  39. * The XML documents produced by the <code>XMLEncoder</code> class are:
  40. * <ul>
  41. * <li>
  42. * <em>Portable and version resilient</em>: they have no dependencies
  43. * on the private implementation of any class and so, like Java source
  44. * files, they may be exchanged between environments which may have
  45. * different versions of some of the classes and between VMs from
  46. * different vendors.
  47. * <li>
  48. * <em>Structurally compact</em>: The <code>XMLEncoder</code> class
  49. * uses a <em>redundancy elimination</em> algorithm internally so that the
  50. * default values of a Bean's properties are not written to the stream.
  51. * <li>
  52. * <em>Fault tolerant</em>: Non-structural errors in the file,
  53. * caused either by damage to the file or by API changes
  54. * made to classes in an archive remain localized
  55. * so that a reader can report the error and continue to load the parts
  56. * of the document which were not affected by the error.
  57. * </ul>
  58. * <p>
  59. * Below is an example of an XML archive containing
  60. * some user interface components from the <em>swing</em> toolkit:
  61. * <pre>
  62. * <?xml version="1.0" encoding="UTF-8"?>
  63. * <java version="1.0" class="java.beans.XMLDecoder">
  64. * <object class="javax.swing.JFrame">
  65. * <void property="name">
  66. * <string>frame1</string>
  67. * </void>
  68. * <void property="bounds">
  69. * <object class="java.awt.Rectangle">
  70. * <int>0</int>
  71. * <int>0</int>
  72. * <int>200</int>
  73. * <int>200</int>
  74. * </object>
  75. * </void>
  76. * <void property="contentPane">
  77. * <void method="add">
  78. * <object class="javax.swing.JButton">
  79. * <void property="label">
  80. * <string>Hello</string>
  81. * </void>
  82. * </object>
  83. * </void>
  84. * </void>
  85. * <void property="visible">
  86. * <boolean>true</boolean>
  87. * </void>
  88. * </object>
  89. * </java>
  90. * </pre>
  91. * The XML syntax uses the following conventions:
  92. * <ul>
  93. * <li>
  94. * Each element represents a method call.
  95. * <li>
  96. * The "object" tag denotes an <em>expression</em> whose value is
  97. * to be used as the argument to the enclosing element.
  98. * <li>
  99. * The "void" tag denotes a <em>statement</em> which will
  100. * be executed, but whose result will not be used as an
  101. * argument to the enclosing method.
  102. * <li>
  103. * Elements which contain elements use those elements as arguments,
  104. * unless they have the tag: "void".
  105. * <li>
  106. * The name of the method is denoted by the "method" attribute.
  107. * <li>
  108. * XML's standard "id" and "idref" attributes are used to make
  109. * references to previous expressions - so as to deal with
  110. * circularities in the object graph.
  111. * <li>
  112. * The "class" attribute is used to specify the target of a static
  113. * method or constructor explicitly; its value being the fully
  114. * qualified name of the class.
  115. * <li>
  116. * Elements with the "void" tag are executed using
  117. * the outer context as the target if no target is defined
  118. * by a "class" attribute.
  119. * <li>
  120. * Java's String class is treated specially and is
  121. * written <string>Hello, world</string> where
  122. * the characters of the string are converted to bytes
  123. * using the UTF-8 character encoding.
  124. * </ul>
  125. * <p>
  126. * Although all object graphs may be written using just these three
  127. * tags, the following definitions are included so that common
  128. * data structures can be expressed more concisely:
  129. * <p>
  130. * <ul>
  131. * <li>
  132. * The default method name is "new".
  133. * <li>
  134. * A reference to a java class is written in the form
  135. * <class>javax.swing.JButton</class>.
  136. * <li>
  137. * Instances of the wrapper classes for Java's primitive types are written
  138. * using the name of the primitive type as the tag. For example, an
  139. * instance of the <code>Integer</code> class could be written:
  140. * <int>123</int>. Note that the <code>XMLEncoder</code> class
  141. * uses Java's reflection package in which the conversion between
  142. * Java's primitive types and their associated "wrapper classes"
  143. * is handled internally. The API for the <code>XMLEncoder</code> class
  144. * itself deals only with <code>Object</code>s.
  145. * <li>
  146. * In an element representing a nullary method whose name
  147. * starts with "get", the "method" attribute is replaced
  148. * with a "property" attribute whose value is given by removing
  149. * the "get" prefix and decapitalizing the result.
  150. * <li>
  151. * In an element representing a monadic method whose name
  152. * starts with "set", the "method" attribute is replaced
  153. * with a "property" attribute whose value is given by removing
  154. * the "set" prefix and decapitalizing the result.
  155. * <li>
  156. * In an element representing a method named "get" taking one
  157. * integer argument, the "method" attribute is replaced
  158. * with an "index" attribute whose value the value of the
  159. * first argument.
  160. * <li>
  161. * In an element representing a method named "set" taking two arguments,
  162. * the first of which is an integer, the "method" attribute is replaced
  163. * with an "index" attribute whose value the value of the
  164. * first argument.
  165. * <li>
  166. * A reference to an array is written using the "array"
  167. * tag. The "class" and "length" attributes specify the
  168. * sub-type of the array and its length respectively.
  169. * </ul>
  170. *
  171. * @see XMLDecoder
  172. * @see java.io.ObjectOutputStream
  173. *
  174. * @since 1.4
  175. *
  176. * @version 1.25 01/27/03
  177. * @author Philip Milne
  178. */
  179. public class XMLEncoder extends Encoder {
  180. private static String encoding = "UTF-8";
  181. private OutputStream out;
  182. private Object owner;
  183. private int indentation = 0;
  184. private boolean internal = false;
  185. private HashMap valueToExpression;
  186. private HashMap targetToStatementList;
  187. private boolean preambleWritten = false;
  188. private class ValueData {
  189. public int refs = 0;
  190. public boolean marked = false; // Marked -> refs > 0 unless ref was a target.
  191. public String name = null;
  192. public Expression exp = null;
  193. }
  194. /**
  195. * Creates a new output stream for sending <em>JavaBeans</em>
  196. * to the stream <code>out</code> using an XML encoding.
  197. *
  198. * @param out The stream to which the XML representation of
  199. * the objects will be sent.
  200. *
  201. * @see XMLDecoder#XMLDecoder(InputStream)
  202. */
  203. public XMLEncoder(OutputStream out) {
  204. this.out = out;
  205. valueToExpression = new IdentityHashtable();
  206. targetToStatementList = new IdentityHashtable();
  207. Statement.setCaching(true);
  208. }
  209. /**
  210. * Sets the owner of this encoder to <code>owner</code>.
  211. *
  212. * @param owner The owner of this encoder.
  213. *
  214. * @see #getOwner
  215. */
  216. public void setOwner(Object owner) {
  217. // System.out.println("setOwner: " + instanceName(owner));
  218. this.owner = owner;
  219. writeExpression(new Expression(this, "getOwner", new Object[0]));
  220. }
  221. /**
  222. * Gets the owner of this encoder.
  223. *
  224. * @return The owner of this encoder.
  225. *
  226. * @see #setOwner
  227. */
  228. public Object getOwner() {
  229. return owner;
  230. }
  231. /**
  232. * Write an XML representation of the specified object to the output.
  233. *
  234. * @param o The object to be written to the stream.
  235. *
  236. * @see XMLDecoder#readObject
  237. */
  238. public void writeObject(Object o) {
  239. // System.out.println("XMLEncoder::writeObject: " + instanceName(o));
  240. if (internal) {
  241. super.writeObject(o);
  242. }
  243. else {
  244. writeStatement(new Statement(this, "writeObject", new Object[]{o}));
  245. }
  246. }
  247. private Vector statementList(Object target) {
  248. Vector list = (Vector)targetToStatementList.get(target);
  249. if (list != null) {
  250. return list;
  251. }
  252. list = new Vector();
  253. targetToStatementList.put(target, list);
  254. return list;
  255. }
  256. private void mark(Object o, boolean isArgument) {
  257. // System.out.println("mark: " + instanceName(o));
  258. if (o == null || o == this) {
  259. return;
  260. }
  261. ValueData d = getValueData(o);
  262. Expression exp = d.exp;
  263. // Do not mark liternal strings. Other strings, which might,
  264. // for example, come from resource bundles should still be marked.
  265. if (o.getClass() == String.class && exp == null) {
  266. return;
  267. }
  268. // Bump the reference counts of all arguments
  269. if (isArgument) {
  270. d.refs++;
  271. }
  272. if (d.marked) {
  273. return;
  274. }
  275. d.marked = true;
  276. Object target = exp.getTarget();
  277. if (!(target instanceof Class)) {
  278. statementList(target).add(exp);
  279. // Pending: Why does the reference count need to
  280. // be incremented here?
  281. d.refs++;
  282. }
  283. mark(exp);
  284. }
  285. private void mark(Statement stm) {
  286. Object[] args = stm.getArguments();
  287. for (int i = 0; i < args.length; i++) {
  288. Object arg = args[i];
  289. mark(arg, true);
  290. }
  291. mark(stm.getTarget(), false);
  292. }
  293. /**
  294. * Records the Statement so that the Encoder will
  295. * produce the actual output when the stream is flushed.
  296. * <P>
  297. * This method should only be called within the context of
  298. * initializing a persistence delegate or setting up an encoder to
  299. * read from a resource bundle.
  300. *
  301. * @param oldStm The statement that will be written
  302. * to the stream.
  303. * @see java.beans.PersistenceDelegate#initialize
  304. */
  305. public void writeStatement(Statement oldStm) {
  306. // System.out.println("XMLEncoder::writeStatement: " + oldStm);
  307. boolean internal = this.internal;
  308. this.internal = true;
  309. try {
  310. super.writeStatement(oldStm);
  311. /*
  312. Note we must do the mark first as we may
  313. require the results of previous values in
  314. this context for this statement.
  315. Test case is:
  316. os.setOwner(this);
  317. os.writeObject(this);
  318. */
  319. mark(oldStm);
  320. statementList(oldStm.getTarget()).add(oldStm);
  321. }
  322. catch (Exception e) {
  323. getExceptionListener().exceptionThrown(new Exception("discarding statement " + oldStm, e));
  324. }
  325. this.internal = internal;
  326. }
  327. /**
  328. * Records the Expression so that the Encoder will
  329. * produce the actual output when the stream is flushed.
  330. * <P>
  331. * This method should only be called within the context of
  332. * initializing a persistence delegate or setting up an encoder to
  333. * read from a resource bundle.
  334. *
  335. * @param oldExp The expression that will be written
  336. * to the stream.
  337. * @see java.beans.PersistenceDelegate#initialize
  338. */
  339. public void writeExpression(Expression oldExp) {
  340. boolean internal = this.internal;
  341. this.internal = true;
  342. Object oldValue = getValue(oldExp);
  343. if (get(oldValue) == null || (oldValue instanceof String && !internal)) {
  344. getValueData(oldValue).exp = oldExp;
  345. super.writeExpression(oldExp);
  346. }
  347. this.internal = internal;
  348. }
  349. /**
  350. * This method writes out the preamble associated with the
  351. * XML encoding if it has not been written already and
  352. * then writes out all of the values that been
  353. * written to the stream since the last time <code>flush</code>
  354. * was called. After flushing, all internal references to the
  355. * values that were written to this stream are cleared.
  356. */
  357. public void flush() {
  358. if (!preambleWritten) { // Don't do this in constructor - it throws ... pending.
  359. writeln("<?xml version=" + quote("1.0") +
  360. " encoding=" + quote(encoding) + "?>");
  361. writeln("<java version=" + quote(System.getProperty("java.version")) +
  362. " class=" + quote(XMLDecoder.class.getName()) + ">");
  363. preambleWritten = true;
  364. }
  365. indentation++;
  366. Vector roots = statementList(this);
  367. for(int i = 0; i < roots.size(); i++) {
  368. Statement s = (Statement)roots.get(i);
  369. if (s.getMethodName() == "writeObject") {
  370. outputValue(s.getArguments()[0], this, true);
  371. }
  372. else {
  373. outputStatement(s, this, false);
  374. }
  375. }
  376. indentation--;
  377. try {
  378. out.flush();
  379. }
  380. catch (IOException e) {
  381. getExceptionListener().exceptionThrown(e);
  382. }
  383. NameGenerator.clear();
  384. super.clear();
  385. valueToExpression.clear();
  386. targetToStatementList.clear();
  387. Statement.setCaching(false); // Clears method cache.
  388. }
  389. /**
  390. * This method calls <code>flush</code>, writes the closing
  391. * postamble and then closes the output stream associated
  392. * with this stream.
  393. */
  394. public void close() {
  395. flush();
  396. writeln("</java>");
  397. try {
  398. out.close();
  399. }
  400. catch (IOException e) {
  401. getExceptionListener().exceptionThrown(e);
  402. }
  403. }
  404. private String quote(String s) {
  405. return "\"" + s + "\"";
  406. }
  407. private String instanceName(Object instance) {
  408. return NameGenerator.instanceName(instance);
  409. }
  410. private String propertyName(String methodName) {
  411. return java.beans.Introspector.decapitalize(methodName.substring(3));
  412. }
  413. private ValueData getValueData(Object o) {
  414. ValueData d = (ValueData)valueToExpression.get(o);
  415. if (d == null) {
  416. d = new ValueData();
  417. valueToExpression.put(o, d);
  418. }
  419. return d;
  420. }
  421. private static String quoteCharacters(String s) {
  422. StringBuffer result = null;
  423. for(int i = 0, max = s.length(), delta = 0; i < max; i++) {
  424. char c = s.charAt(i);
  425. String replacement = null;
  426. if (c == '&') {
  427. replacement = "&";
  428. } else if (c == '<') {
  429. replacement = "<";
  430. } else if (c == '\r') {
  431. replacement = " ";
  432. } else if (c == '>') {
  433. replacement = ">";
  434. } else if (c == '"') {
  435. replacement = """;
  436. } else if (c == '\'') {
  437. replacement = "'";
  438. }
  439. if (replacement != null) {
  440. if (result == null) {
  441. result = new StringBuffer(s);
  442. }
  443. result.replace(i + delta, i + delta + 1, replacement);
  444. delta += (replacement.length() - 1);
  445. }
  446. }
  447. if (result == null) {
  448. return s;
  449. }
  450. return result.toString();
  451. }
  452. private void writeln(String exp) {
  453. try {
  454. for(int i = 0; i < indentation; i++) {
  455. out.write(' ');
  456. }
  457. out.write(exp.getBytes(encoding));
  458. out.write(" \n".getBytes());
  459. }
  460. catch (IOException e) {
  461. getExceptionListener().exceptionThrown(e);
  462. }
  463. }
  464. private void outputValue(Object value, Object outer, boolean isArgument) {
  465. // System.out.println("outputValue: " + instanceName(value));
  466. if (value == null) {
  467. writeln("<null/>");
  468. return;
  469. }
  470. if (value instanceof Class) {
  471. writeln("<class>" + ((Class)value).getName() + "</class>");
  472. return;
  473. }
  474. ValueData d = getValueData(value);
  475. if (d.exp != null && d.exp.getTarget() instanceof Field && d.exp.getMethodName() == "get") {
  476. Field f = (Field)d.exp.getTarget();
  477. writeln("<object class=" + quote(f.getDeclaringClass().getName()) + " field=" + quote(f.getName()) + "/>");
  478. return;
  479. }
  480. Class primitiveType = Statement.primitiveTypeFor(value.getClass());
  481. if (primitiveType != null && d.exp.getTarget() == value.getClass() && d.exp.getMethodName() == "new") {
  482. String primitiveTypeName = primitiveType.getName();
  483. // Make sure that character types are quoted correctly.
  484. if (primitiveType == Character.TYPE) {
  485. value = quoteCharacters(((Character)value).toString());
  486. }
  487. writeln("<" + primitiveTypeName + ">" + value + "</" + primitiveTypeName + ">");
  488. return;
  489. }
  490. if (value instanceof String && d.exp == null) {
  491. writeln("<string>" + quoteCharacters((String)value) + "</string>");
  492. return;
  493. }
  494. if (d.name != null) {
  495. writeln("<object idref=" + quote(d.name) + "/>");
  496. return;
  497. }
  498. outputStatement(d.exp, outer, isArgument);
  499. }
  500. private void outputStatement(Statement exp, Object outer, boolean isArgument) {
  501. // System.out.println("outputStatement: " + exp + " outer: " + instanceName(outer) + " isArg: " + isArgument);
  502. Object target = exp.getTarget();
  503. String methodName = exp.getMethodName();
  504. Object[] args = exp.getArguments();
  505. boolean expression = exp.getClass() == Expression.class;
  506. Object value = (expression) ? getValue((Expression)exp) : null;
  507. String tag = (expression && isArgument) ? "object" : "void";
  508. String attributes = "";
  509. ValueData d = getValueData(value);
  510. if (expression) {
  511. if (d.refs > 1) {
  512. String instanceName = instanceName(value);
  513. d.name = instanceName;
  514. attributes = attributes + " id=" + quote(instanceName);
  515. }
  516. }
  517. // Special cases for targets.
  518. if (target == outer) {
  519. }
  520. else if (target == Array.class && methodName == "newInstance") {
  521. tag = "array";
  522. attributes = attributes + " class=" + quote(((Class)args[0]).getName());
  523. attributes = attributes + " length=" + quote(args[1].toString());
  524. args = new Object[]{};
  525. }
  526. else if (target.getClass() == Class.class) {
  527. attributes = attributes + " class=" + quote(((Class)target).getName());
  528. }
  529. else {
  530. d.refs = 2;
  531. outputValue(target, outer, false);
  532. outputValue(value, outer, false);
  533. return;
  534. }
  535. // Special cases for methods.
  536. if ((!expression && methodName == "set" && args.length == 2 && args[0] instanceof Integer) ||
  537. (expression && methodName == "get" && args.length == 1 && args[0] instanceof Integer)) {
  538. attributes = attributes + " index=" + quote(args[0].toString());
  539. args = (args.length == 1) ? new Object[]{} : new Object[]{args[1]};
  540. }
  541. else if ((!expression && methodName.startsWith("set") && args.length == 1) ||
  542. (expression && methodName.startsWith("get") && args.length == 0)) {
  543. attributes = attributes + " property=" + quote(propertyName(methodName));
  544. }
  545. else if (methodName != "new" && methodName != "newInstance") {
  546. attributes = attributes + " method=" + quote(methodName);
  547. }
  548. Vector statements = statementList(value);
  549. // Use XML's short form when there is no body.
  550. if (args.length == 0 && statements.size() == 0) {
  551. writeln("<" + tag + attributes + "/>");
  552. return;
  553. }
  554. writeln("<" + tag + attributes + ">");
  555. indentation++;
  556. for(int i = 0; i < args.length; i++) {
  557. outputValue(args[i], null, true);
  558. }
  559. for(int i = 0; i < statements.size(); i++) {
  560. Statement s = (Statement)statements.get(i);
  561. outputStatement(s, value, false);
  562. }
  563. indentation--;
  564. writeln("</" + tag + ">");
  565. }
  566. }