/* * @(#)XMLEncoder.java 1.33 03/12/19 * * Copyright 2004 Sun Microsystems, Inc. All rights reserved. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. */ package java.beans; import java.io.*; import java.util.*; import java.lang.reflect.*; /** * The XMLEncoder class is a complementary alternative to * the ObjectOutputStream and can used to generate * a textual representation of a JavaBean in the same * way that the ObjectOutputStream can * be used to create binary representation of Serializable * objects. For example, the following fragment can be used to create * a textual representation the supplied JavaBean * and all its properties: *
 *       XMLEncoder e = new XMLEncoder(
 *                          new BufferedOutputStream(
 *                              new FileOutputStream("Test.xml")));
 *       e.writeObject(new JButton("Hello, world"));
 *       e.close();
 * 
* Despite the similarity of their APIs, the XMLEncoder * class is exclusively designed for the purpose of archiving graphs * of JavaBeans as textual representations of their public * properties. Like Java source files, documents written this way * have a natural immunity to changes in the implementations of the classes * involved. The ObjectOutputStream continues to be recommended * for interprocess communication and general purpose serialization. *

* The XMLEncoder class provides a default denotation for * JavaBeans in which they are represented as XML documents * complying with version 1.0 of the XML specification and the * UTF-8 character encoding of the Unicode/ISO 10646 character set. * The XML documents produced by the XMLEncoder class are: *

*

* Below is an example of an XML archive containing * some user interface components from the swing toolkit: *

 * <?xml version="1.0" encoding="UTF-8"?>
 * <java version="1.0" class="java.beans.XMLDecoder">
 * <object class="javax.swing.JFrame">
 *   <void property="name">
 *     <string>frame1</string>
 *   </void>
 *   <void property="bounds">
 *     <object class="java.awt.Rectangle">
 *       <int>0</int>
 *       <int>0</int>
 *       <int>200</int>
 *       <int>200</int>
 *     </object>
 *   </void>
 *   <void property="contentPane">
 *     <void method="add">
 *       <object class="javax.swing.JButton">
 *         <void property="label">
 *           <string>Hello</string>
 *         </void>
 *       </object>
 *     </void>
 *   </void>
 *   <void property="visible">
 *     <boolean>true</boolean>
 *   </void>
 * </object>
 * </java>
 * 
* The XML syntax uses the following conventions: * *

* Although all object graphs may be written using just these three * tags, the following definitions are included so that common * data structures can be expressed more concisely: *

*

* *

* For more information you might also want to check out * Using XMLEncoder, * an article in The Swing Connection. * @see XMLDecoder * @see java.io.ObjectOutputStream * * @since 1.4 * * @version 1.33 12/19/03 * @author Philip Milne */ public class XMLEncoder extends Encoder { private static String encoding = "UTF-8"; private OutputStream out; private Object owner; private int indentation = 0; private boolean internal = false; private Map valueToExpression; private Map targetToStatementList; private boolean preambleWritten = false; private NameGenerator nameGenerator; private class ValueData { public int refs = 0; public boolean marked = false; // Marked -> refs > 0 unless ref was a target. public String name = null; public Expression exp = null; } /** * Creates a new output stream for sending JavaBeans * to the stream out using an XML encoding. * * @param out The stream to which the XML representation of * the objects will be sent. * * @see XMLDecoder#XMLDecoder(InputStream) */ public XMLEncoder(OutputStream out) { this.out = out; valueToExpression = new IdentityHashMap(); targetToStatementList = new IdentityHashMap(); nameGenerator = new NameGenerator(); } /** * Sets the owner of this encoder to owner. * * @param owner The owner of this encoder. * * @see #getOwner */ public void setOwner(Object owner) { this.owner = owner; writeExpression(new Expression(this, "getOwner", new Object[0])); } /** * Gets the owner of this encoder. * * @return The owner of this encoder. * * @see #setOwner */ public Object getOwner() { return owner; } /** * Write an XML representation of the specified object to the output. * * @param o The object to be written to the stream. * * @see XMLDecoder#readObject */ public void writeObject(Object o) { if (internal) { super.writeObject(o); } else { writeStatement(new Statement(this, "writeObject", new Object[]{o})); } } private Vector statementList(Object target) { Vector list = (Vector)targetToStatementList.get(target); if (list != null) { return list; } list = new Vector(); targetToStatementList.put(target, list); return list; } private void mark(Object o, boolean isArgument) { if (o == null || o == this) { return; } ValueData d = getValueData(o); Expression exp = d.exp; // Do not mark liternal strings. Other strings, which might, // for example, come from resource bundles should still be marked. if (o.getClass() == String.class && exp == null) { return; } // Bump the reference counts of all arguments if (isArgument) { d.refs++; } if (d.marked) { return; } d.marked = true; Object target = exp.getTarget(); if (!(target instanceof Class)) { statementList(target).add(exp); // Pending: Why does the reference count need to // be incremented here? d.refs++; } mark(exp); } private void mark(Statement stm) { Object[] args = stm.getArguments(); for (int i = 0; i < args.length; i++) { Object arg = args[i]; mark(arg, true); } mark(stm.getTarget(), false); } /** * Records the Statement so that the Encoder will * produce the actual output when the stream is flushed. *

* This method should only be invoked within the context * of initializing a persistence delegate. * * @param oldStm The statement that will be written * to the stream. * @see java.beans.PersistenceDelegate#initialize */ public void writeStatement(Statement oldStm) { // System.out.println("XMLEncoder::writeStatement: " + oldStm); boolean internal = this.internal; this.internal = true; try { super.writeStatement(oldStm); /* Note we must do the mark first as we may require the results of previous values in this context for this statement. Test case is: os.setOwner(this); os.writeObject(this); */ mark(oldStm); statementList(oldStm.getTarget()).add(oldStm); } catch (Exception e) { getExceptionListener().exceptionThrown(new Exception("XMLEncoder: discarding statement " + oldStm, e)); } this.internal = internal; } /** * Records the Expression so that the Encoder will * produce the actual output when the stream is flushed. *

* This method should only be invoked within the context of * initializing a persistence delegate or setting up an encoder to * read from a resource bundle. *

* For more information about using resource bundles with the * XMLEncoder, see * http://java.sun.com/products/jfc/tsc/articles/persistence4/#i18n * * @param oldExp The expression that will be written * to the stream. * @see java.beans.PersistenceDelegate#initialize */ public void writeExpression(Expression oldExp) { boolean internal = this.internal; this.internal = true; Object oldValue = getValue(oldExp); if (get(oldValue) == null || (oldValue instanceof String && !internal)) { getValueData(oldValue).exp = oldExp; super.writeExpression(oldExp); } this.internal = internal; } /** * This method writes out the preamble associated with the * XML encoding if it has not been written already and * then writes out all of the values that been * written to the stream since the last time flush * was called. After flushing, all internal references to the * values that were written to this stream are cleared. */ public void flush() { if (!preambleWritten) { // Don't do this in constructor - it throws ... pending. writeln(""); writeln(""); preambleWritten = true; } indentation++; Vector roots = statementList(this); for(int i = 0; i < roots.size(); i++) { Statement s = (Statement)roots.get(i); if ("writeObject".equals(s.getMethodName())) { outputValue(s.getArguments()[0], this, true); } else { outputStatement(s, this, false); } } indentation--; try { out.flush(); } catch (IOException e) { getExceptionListener().exceptionThrown(e); } clear(); } void clear() { super.clear(); nameGenerator.clear(); valueToExpression.clear(); targetToStatementList.clear(); } /** * This method calls flush, writes the closing * postamble and then closes the output stream associated * with this stream. */ public void close() { flush(); writeln(""); try { out.close(); } catch (IOException e) { getExceptionListener().exceptionThrown(e); } } private String quote(String s) { return "\"" + s + "\""; } private ValueData getValueData(Object o) { ValueData d = (ValueData)valueToExpression.get(o); if (d == null) { d = new ValueData(); valueToExpression.put(o, d); } return d; } private static String quoteCharacters(String s) { StringBuffer result = null; for(int i = 0, max = s.length(), delta = 0; i < max; i++) { char c = s.charAt(i); String replacement = null; if (c == '&') { replacement = "&"; } else if (c == '<') { replacement = "<"; } else if (c == '\r') { replacement = " "; } else if (c == '>') { replacement = ">"; } else if (c == '"') { replacement = """; } else if (c == '\'') { replacement = "'"; } if (replacement != null) { if (result == null) { result = new StringBuffer(s); } result.replace(i + delta, i + delta + 1, replacement); delta += (replacement.length() - 1); } } if (result == null) { return s; } return result.toString(); } private void writeln(String exp) { try { for(int i = 0; i < indentation; i++) { out.write(' '); } out.write(exp.getBytes(encoding)); out.write(" \n".getBytes(encoding)); } catch (IOException e) { getExceptionListener().exceptionThrown(e); } } private void outputValue(Object value, Object outer, boolean isArgument) { if (value == null) { writeln(""); return; } if (value instanceof Class) { writeln("" + ((Class)value).getName() + ""); return; } ValueData d = getValueData(value); if (d.exp != null) { Object target = d.exp.getTarget(); String methodName = d.exp.getMethodName(); if (target == null || methodName == null) { throw new NullPointerException((target == null ? "target" : "methodName") + " should not be null"); } if (target instanceof Field && methodName.equals("get")) { Field f = (Field)target; writeln(""); return; } Class primitiveType = ReflectionUtils.primitiveTypeFor(value.getClass()); if (primitiveType != null && target == value.getClass() && methodName.equals("new")) { String primitiveTypeName = primitiveType.getName(); // Make sure that character types are quoted correctly. if (primitiveType == Character.TYPE) { value = quoteCharacters(((Character)value).toString()); } writeln("<" + primitiveTypeName + ">" + value + ""); return; } } else if (value instanceof String) { writeln("" + quoteCharacters((String)value) + ""); return; } if (d.name != null) { writeln(""); return; } outputStatement(d.exp, outer, isArgument); } private void outputStatement(Statement exp, Object outer, boolean isArgument) { Object target = exp.getTarget(); String methodName = exp.getMethodName(); if (target == null || methodName == null) { throw new NullPointerException((target == null ? "target" : "methodName") + " should not be null"); } Object[] args = exp.getArguments(); boolean expression = exp.getClass() == Expression.class; Object value = (expression) ? getValue((Expression)exp) : null; String tag = (expression && isArgument) ? "object" : "void"; String attributes = ""; ValueData d = getValueData(value); if (expression) { if (d.refs > 1) { String instanceName = nameGenerator.instanceName(value); d.name = instanceName; attributes = attributes + " id=" + quote(instanceName); } } // Special cases for targets. if (target == outer) { } else if (target == Array.class && methodName.equals("newInstance")) { tag = "array"; attributes = attributes + " class=" + quote(((Class)args[0]).getName()); attributes = attributes + " length=" + quote(args[1].toString()); args = new Object[]{}; } else if (target.getClass() == Class.class) { attributes = attributes + " class=" + quote(((Class)target).getName()); } else { d.refs = 2; outputValue(target, outer, false); outputValue(value, outer, false); return; } // Special cases for methods. if ((!expression && methodName.equals("set") && args.length == 2 && args[0] instanceof Integer) || (expression && methodName.equals("get") && args.length == 1 && args[0] instanceof Integer)) { attributes = attributes + " index=" + quote(args[0].toString()); args = (args.length == 1) ? new Object[]{} : new Object[]{args[1]}; } else if ((!expression && methodName.startsWith("set") && args.length == 1) || (expression && methodName.startsWith("get") && args.length == 0)) { attributes = attributes + " property=" + quote(Introspector.decapitalize(methodName.substring(3))); } else if (!methodName.equals("new") && !methodName.equals("newInstance")) { attributes = attributes + " method=" + quote(methodName); } Vector statements = statementList(value); // Use XML's short form when there is no body. if (args.length == 0 && statements.size() == 0) { writeln("<" + tag + attributes + "/>"); return; } writeln("<" + tag + attributes + ">"); indentation++; for(int i = 0; i < args.length; i++) { outputValue(args[i], null, true); } for(int i = 0; i < statements.size(); i++) { Statement s = (Statement)statements.get(i); outputStatement(s, value, false); } indentation--; writeln(""); } }