KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > java > beans > XMLEncoder


1 /*
2  * @(#)XMLEncoder.java 1.33 03/12/19
3  *
4  * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
5  * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
6  */

7 package java.beans;
8
9 import java.io.*;
10 import java.util.*;
11 import java.lang.reflect.*;
12
13 /**
14  * The <code>XMLEncoder</code> class is a complementary alternative to
15  * the <code>ObjectOutputStream</code> and can used to generate
16  * a textual representation of a <em>JavaBean</em> in the same
17  * way that the <code>ObjectOutputStream</code> can
18  * be used to create binary representation of <code>Serializable</code>
19  * objects. For example, the following fragment can be used to create
20  * a textual representation the supplied <em>JavaBean</em>
21  * and all its properties:
22  * <pre>
23  * XMLEncoder e = new XMLEncoder(
24  * new BufferedOutputStream(
25  * new FileOutputStream("Test.xml")));
26  * e.writeObject(new JButton("Hello, world"));
27  * e.close();
28  * </pre>
29  * Despite the similarity of their APIs, the <code>XMLEncoder</code>
30  * class is exclusively designed for the purpose of archiving graphs
31  * of <em>JavaBean</em>s as textual representations of their public
32  * properties. Like Java source files, documents written this way
33  * have a natural immunity to changes in the implementations of the classes
34  * involved. The <code>ObjectOutputStream</code> continues to be recommended
35  * for interprocess communication and general purpose serialization.
36  * <p>
37  * The <code>XMLEncoder</code> class provides a default denotation for
38  * <em>JavaBean</em>s in which they are represented as XML documents
39  * complying with version 1.0 of the XML specification and the
40  * UTF-8 character encoding of the Unicode/ISO 10646 character set.
41  * The XML documents produced by the <code>XMLEncoder</code> class are:
42  * <ul>
43  * <li>
44  * <em>Portable and version resilient</em>: they have no dependencies
45  * on the private implementation of any class and so, like Java source
46  * files, they may be exchanged between environments which may have
47  * different versions of some of the classes and between VMs from
48  * different vendors.
49  * <li>
50  * <em>Structurally compact</em>: The <code>XMLEncoder</code> class
51  * uses a <em>redundancy elimination</em> algorithm internally so that the
52  * default values of a Bean's properties are not written to the stream.
53  * <li>
54  * <em>Fault tolerant</em>: Non-structural errors in the file,
55  * caused either by damage to the file or by API changes
56  * made to classes in an archive remain localized
57  * so that a reader can report the error and continue to load the parts
58  * of the document which were not affected by the error.
59  * </ul>
60  * <p>
61  * Below is an example of an XML archive containing
62  * some user interface components from the <em>swing</em> toolkit:
63  * <pre>
64  * &lt;?xml version="1.0" encoding="UTF-8"?&gt;
65  * &lt;java version="1.0" class="java.beans.XMLDecoder"&gt;
66  * &lt;object class="javax.swing.JFrame"&gt;
67  * &lt;void property="name"&gt;
68  * &lt;string&gt;frame1&lt;/string&gt;
69  * &lt;/void&gt;
70  * &lt;void property="bounds"&gt;
71  * &lt;object class="java.awt.Rectangle"&gt;
72  * &lt;int&gt;0&lt;/int&gt;
73  * &lt;int&gt;0&lt;/int&gt;
74  * &lt;int&gt;200&lt;/int&gt;
75  * &lt;int&gt;200&lt;/int&gt;
76  * &lt;/object&gt;
77  * &lt;/void&gt;
78  * &lt;void property="contentPane"&gt;
79  * &lt;void method="add"&gt;
80  * &lt;object class="javax.swing.JButton"&gt;
81  * &lt;void property="label"&gt;
82  * &lt;string&gt;Hello&lt;/string&gt;
83  * &lt;/void&gt;
84  * &lt;/object&gt;
85  * &lt;/void&gt;
86  * &lt;/void&gt;
87  * &lt;void property="visible"&gt;
88  * &lt;boolean&gt;true&lt;/boolean&gt;
89  * &lt;/void&gt;
90  * &lt;/object&gt;
91  * &lt;/java&gt;
92  * </pre>
93  * The XML syntax uses the following conventions:
94  * <ul>
95  * <li>
96  * Each element represents a method call.
97  * <li>
98  * The "object" tag denotes an <em>expression</em> whose value is
99  * to be used as the argument to the enclosing element.
100  * <li>
101  * The "void" tag denotes a <em>statement</em> which will
102  * be executed, but whose result will not be used as an
103  * argument to the enclosing method.
104  * <li>
105  * Elements which contain elements use those elements as arguments,
106  * unless they have the tag: "void".
107  * <li>
108  * The name of the method is denoted by the "method" attribute.
109  * <li>
110  * XML's standard "id" and "idref" attributes are used to make
111  * references to previous expressions - so as to deal with
112  * circularities in the object graph.
113  * <li>
114  * The "class" attribute is used to specify the target of a static
115  * method or constructor explicitly; its value being the fully
116  * qualified name of the class.
117  * <li>
118  * Elements with the "void" tag are executed using
119  * the outer context as the target if no target is defined
120  * by a "class" attribute.
121  * <li>
122  * Java's String class is treated specially and is
123  * written &lt;string&gt;Hello, world&lt;/string&gt; where
124  * the characters of the string are converted to bytes
125  * using the UTF-8 character encoding.
126  * </ul>
127  * <p>
128  * Although all object graphs may be written using just these three
129  * tags, the following definitions are included so that common
130  * data structures can be expressed more concisely:
131  * <p>
132  * <ul>
133  * <li>
134  * The default method name is "new".
135  * <li>
136  * A reference to a java class is written in the form
137  * &lt;class&gt;javax.swing.JButton&lt;/class&gt;.
138  * <li>
139  * Instances of the wrapper classes for Java's primitive types are written
140  * using the name of the primitive type as the tag. For example, an
141  * instance of the <code>Integer</code> class could be written:
142  * &lt;int&gt;123&lt;/int&gt;. Note that the <code>XMLEncoder</code> class
143  * uses Java's reflection package in which the conversion between
144  * Java's primitive types and their associated "wrapper classes"
145  * is handled internally. The API for the <code>XMLEncoder</code> class
146  * itself deals only with <code>Object</code>s.
147  * <li>
148  * In an element representing a nullary method whose name
149  * starts with "get", the "method" attribute is replaced
150  * with a "property" attribute whose value is given by removing
151  * the "get" prefix and decapitalizing the result.
152  * <li>
153  * In an element representing a monadic method whose name
154  * starts with "set", the "method" attribute is replaced
155  * with a "property" attribute whose value is given by removing
156  * the "set" prefix and decapitalizing the result.
157  * <li>
158  * In an element representing a method named "get" taking one
159  * integer argument, the "method" attribute is replaced
160  * with an "index" attribute whose value the value of the
161  * first argument.
162  * <li>
163  * In an element representing a method named "set" taking two arguments,
164  * the first of which is an integer, the "method" attribute is replaced
165  * with an "index" attribute whose value the value of the
166  * first argument.
167  * <li>
168  * A reference to an array is written using the "array"
169  * tag. The "class" and "length" attributes specify the
170  * sub-type of the array and its length respectively.
171  * </ul>
172  *
173  *<p>
174  * For more information you might also want to check out
175  * <a
176  href="http://java.sun.com/products/jfc/tsc/articles/persistence4">Using XMLEncoder</a>,
177  * an article in <em>The Swing Connection.</em>
178  * @see XMLDecoder
179  * @see java.io.ObjectOutputStream
180  *
181  * @since 1.4
182  *
183  * @version 1.33 12/19/03
184  * @author Philip Milne
185  */

186 public class XMLEncoder extends Encoder JavaDoc {
187
188     private static String JavaDoc encoding = "UTF-8";
189
190     private OutputStream out;
191     private Object JavaDoc owner;
192     private int indentation = 0;
193     private boolean internal = false;
194     private Map valueToExpression;
195     private Map targetToStatementList;
196     private boolean preambleWritten = false;
197     private NameGenerator JavaDoc nameGenerator;
198
199     private class ValueData {
200         public int refs = 0;
201         public boolean marked = false; // Marked -> refs > 0 unless ref was a target.
202
public String JavaDoc name = null;
203         public Expression JavaDoc exp = null;
204     }
205
206     /**
207      * Creates a new output stream for sending <em>JavaBeans</em>
208      * to the stream <code>out</code> using an XML encoding.
209      *
210      * @param out The stream to which the XML representation of
211      * the objects will be sent.
212      *
213      * @see XMLDecoder#XMLDecoder(InputStream)
214      */

215     public XMLEncoder(OutputStream out) {
216         this.out = out;
217         valueToExpression = new IdentityHashMap();
218         targetToStatementList = new IdentityHashMap();
219     nameGenerator = new NameGenerator JavaDoc();
220     }
221
222     /**
223      * Sets the owner of this encoder to <code>owner</code>.
224      *
225      * @param owner The owner of this encoder.
226      *
227      * @see #getOwner
228      */

229     public void setOwner(Object JavaDoc owner) {
230         this.owner = owner;
231         writeExpression(new Expression JavaDoc(this, "getOwner", new Object JavaDoc[0]));
232     }
233
234     /**
235      * Gets the owner of this encoder.
236      *
237      * @return The owner of this encoder.
238      *
239      * @see #setOwner
240      */

241     public Object JavaDoc getOwner() {
242     return owner;
243     }
244
245     /**
246      * Write an XML representation of the specified object to the output.
247      *
248      * @param o The object to be written to the stream.
249      *
250      * @see XMLDecoder#readObject
251      */

252     public void writeObject(Object JavaDoc o) {
253         if (internal) {
254             super.writeObject(o);
255         }
256         else {
257             writeStatement(new Statement JavaDoc(this, "writeObject", new Object JavaDoc[]{o}));
258         }
259     }
260
261     private Vector statementList(Object JavaDoc target) {
262         Vector list = (Vector)targetToStatementList.get(target);
263         if (list != null) {
264             return list;
265         }
266         list = new Vector();
267         targetToStatementList.put(target, list);
268         return list;
269     }
270
271     
272     private void mark(Object JavaDoc o, boolean isArgument) {
273         if (o == null || o == this) {
274             return;
275         }
276         ValueData d = getValueData(o);
277         Expression JavaDoc exp = d.exp;
278         // Do not mark liternal strings. Other strings, which might,
279
// for example, come from resource bundles should still be marked.
280
if (o.getClass() == String JavaDoc.class && exp == null) {
281             return;
282         }
283         
284         // Bump the reference counts of all arguments
285
if (isArgument) {
286             d.refs++;
287         }
288         if (d.marked) {
289             return;
290         }
291         d.marked = true;
292         Object JavaDoc target = exp.getTarget();
293         if (!(target instanceof Class JavaDoc)) {
294             statementList(target).add(exp);
295         // Pending: Why does the reference count need to
296
// be incremented here?
297
d.refs++;
298         }
299         mark(exp);
300     }
301     
302     private void mark(Statement JavaDoc stm) {
303         Object JavaDoc[] args = stm.getArguments();
304         for (int i = 0; i < args.length; i++) {
305             Object JavaDoc arg = args[i];
306             mark(arg, true);
307         }
308         mark(stm.getTarget(), false);
309     }
310
311
312     /**
313      * Records the Statement so that the Encoder will
314      * produce the actual output when the stream is flushed.
315      * <P>
316      * This method should only be invoked within the context
317      * of initializing a persistence delegate.
318      *
319      * @param oldStm The statement that will be written
320      * to the stream.
321      * @see java.beans.PersistenceDelegate#initialize
322      */

323     public void writeStatement(Statement JavaDoc oldStm) {
324         // System.out.println("XMLEncoder::writeStatement: " + oldStm);
325
boolean internal = this.internal;
326         this.internal = true;
327         try {
328             super.writeStatement(oldStm);
329         /*
330            Note we must do the mark first as we may
331            require the results of previous values in
332            this context for this statement.
333            Test case is:
334                os.setOwner(this);
335                os.writeObject(this);
336             */

337         mark(oldStm);
338             statementList(oldStm.getTarget()).add(oldStm);
339         }
340         catch (Exception JavaDoc e) {
341             getExceptionListener().exceptionThrown(new Exception JavaDoc("XMLEncoder: discarding statement " + oldStm, e));
342         }
343         this.internal = internal;
344     }
345
346
347     /**
348      * Records the Expression so that the Encoder will
349      * produce the actual output when the stream is flushed.
350      * <P>
351      * This method should only be invoked within the context of
352      * initializing a persistence delegate or setting up an encoder to
353      * read from a resource bundle.
354      * <P>
355      * For more information about using resource bundles with the
356      * XMLEncoder, see
357      * http://java.sun.com/products/jfc/tsc/articles/persistence4/#i18n
358      *
359      * @param oldExp The expression that will be written
360      * to the stream.
361      * @see java.beans.PersistenceDelegate#initialize
362      */

363     public void writeExpression(Expression JavaDoc oldExp) {
364         boolean internal = this.internal;
365         this.internal = true;
366         Object JavaDoc oldValue = getValue(oldExp);
367         if (get(oldValue) == null || (oldValue instanceof String JavaDoc && !internal)) {
368             getValueData(oldValue).exp = oldExp;
369             super.writeExpression(oldExp);
370         }
371         this.internal = internal;
372     }
373
374     /**
375      * This method writes out the preamble associated with the
376      * XML encoding if it has not been written already and
377      * then writes out all of the values that been
378      * written to the stream since the last time <code>flush</code>
379      * was called. After flushing, all internal references to the
380      * values that were written to this stream are cleared.
381      */

382     public void flush() {
383     if (!preambleWritten) { // Don't do this in constructor - it throws ... pending.
384
writeln("<?xml version=" + quote("1.0") +
385                         " encoding=" + quote(encoding) + "?>");
386         writeln("<java version=" + quote(System.getProperty("java.version")) +
387                        " class=" + quote(XMLDecoder JavaDoc.class.getName()) + ">");
388         preambleWritten = true;
389     }
390     indentation++;
391     Vector roots = statementList(this);
392     for(int i = 0; i < roots.size(); i++) {
393         Statement JavaDoc s = (Statement JavaDoc)roots.get(i);
394         if ("writeObject".equals(s.getMethodName())) {
395             outputValue(s.getArguments()[0], this, true);
396             }
397         else {
398             outputStatement(s, this, false);
399         }
400     }
401     indentation--;
402
403     try {
404         out.flush();
405     }
406         catch (IOException e) {
407         getExceptionListener().exceptionThrown(e);
408     }
409     clear();
410     }
411
412     void clear() {
413     super.clear();
414     nameGenerator.clear();
415     valueToExpression.clear();
416     targetToStatementList.clear();
417     }
418     
419
420     /**
421      * This method calls <code>flush</code>, writes the closing
422      * postamble and then closes the output stream associated
423      * with this stream.
424      */

425     public void close() {
426         flush();
427         writeln("</java>");
428         try {
429             out.close();
430         }
431         catch (IOException e) {
432             getExceptionListener().exceptionThrown(e);
433         }
434     }
435
436     private String JavaDoc quote(String JavaDoc s) {
437         return "\"" + s + "\"";
438     }
439
440     private ValueData getValueData(Object JavaDoc o) {
441         ValueData d = (ValueData)valueToExpression.get(o);
442         if (d == null) {
443             d = new ValueData();
444             valueToExpression.put(o, d);
445         }
446         return d;
447     }
448
449     private static String JavaDoc quoteCharacters(String JavaDoc s) {
450     StringBuffer JavaDoc result = null;
451         for(int i = 0, max = s.length(), delta = 0; i < max; i++) {
452         char c = s.charAt(i);
453         String JavaDoc replacement = null;
454
455         if (c == '&') {
456         replacement = "&amp;";
457         } else if (c == '<') {
458         replacement = "&lt;";
459         } else if (c == '\r') {
460         replacement = "&#13;";
461         } else if (c == '>') {
462         replacement = "&gt;";
463         } else if (c == '"') {
464         replacement = "&quot;";
465         } else if (c == '\'') {
466         replacement = "&apos;";
467         }
468         
469         if (replacement != null) {
470         if (result == null) {
471             result = new StringBuffer JavaDoc(s);
472         }
473         result.replace(i + delta, i + delta + 1, replacement);
474         delta += (replacement.length() - 1);
475         }
476         }
477         if (result == null) {
478             return s;
479         }
480     return result.toString();
481     }
482
483     private void writeln(String JavaDoc exp) {
484         try {
485             for(int i = 0; i < indentation; i++) {
486                 out.write(' ');
487             }
488             out.write(exp.getBytes(encoding));
489             out.write(" \n".getBytes(encoding));
490         }
491         catch (IOException e) {
492             getExceptionListener().exceptionThrown(e);
493         }
494     }
495
496     private void outputValue(Object JavaDoc value, Object JavaDoc outer, boolean isArgument) {
497         if (value == null) {
498             writeln("<null/>");
499             return;
500         }
501
502         if (value instanceof Class JavaDoc) {
503             writeln("<class>" + ((Class JavaDoc)value).getName() + "</class>");
504             return;
505         }
506
507         ValueData d = getValueData(value);
508         if (d.exp != null) {
509         Object JavaDoc target = d.exp.getTarget();
510         String JavaDoc methodName = d.exp.getMethodName();
511
512         if (target == null || methodName == null) {
513         throw new NullPointerException JavaDoc((target == null ? "target" :
514                         "methodName") + " should not be null");
515         }
516
517         if (target instanceof Field && methodName.equals("get")) {
518         Field f = (Field)target;
519         writeln("<object class=" + quote(f.getDeclaringClass().getName()) +
520             " field=" + quote(f.getName()) + "/>");
521         return;
522         }
523         
524         Class JavaDoc primitiveType = ReflectionUtils.primitiveTypeFor(value.getClass());
525         if (primitiveType != null && target == value.getClass() &&
526         methodName.equals("new")) {
527         String JavaDoc primitiveTypeName = primitiveType.getName();
528         // Make sure that character types are quoted correctly.
529
if (primitiveType == Character.TYPE) {
530             value = quoteCharacters(((Character JavaDoc)value).toString());
531         }
532         writeln("<" + primitiveTypeName + ">" + value + "</" +
533             primitiveTypeName + ">");
534         return;
535         }
536
537     } else if (value instanceof String JavaDoc) {
538             writeln("<string>" + quoteCharacters((String JavaDoc)value) + "</string>");
539             return;
540         }
541
542         if (d.name != null) {
543             writeln("<object idref=" + quote(d.name) + "/>");
544             return;
545         }
546
547         outputStatement(d.exp, outer, isArgument);
548     }
549
550     private void outputStatement(Statement JavaDoc exp, Object JavaDoc outer, boolean isArgument) {
551         Object JavaDoc target = exp.getTarget();
552         String JavaDoc methodName = exp.getMethodName();
553
554     if (target == null || methodName == null) {
555         throw new NullPointerException JavaDoc((target == null ? "target" :
556                         "methodName") + " should not be null");
557     }
558
559         Object JavaDoc[] args = exp.getArguments();
560         boolean expression = exp.getClass() == Expression JavaDoc.class;
561         Object JavaDoc value = (expression) ? getValue((Expression JavaDoc)exp) : null;
562
563         String JavaDoc tag = (expression && isArgument) ? "object" : "void";
564         String JavaDoc attributes = "";
565         ValueData d = getValueData(value);
566         if (expression) {
567             if (d.refs > 1) {
568                 String JavaDoc instanceName = nameGenerator.instanceName(value);
569                 d.name = instanceName;
570                 attributes = attributes + " id=" + quote(instanceName);
571             }
572         }
573
574         // Special cases for targets.
575
if (target == outer) {
576         }
577         else if (target == Array.class && methodName.equals("newInstance")) {
578             tag = "array";
579             attributes = attributes + " class=" + quote(((Class JavaDoc)args[0]).getName());
580             attributes = attributes + " length=" + quote(args[1].toString());
581             args = new Object JavaDoc[]{};
582         }
583         else if (target.getClass() == Class JavaDoc.class) {
584             attributes = attributes + " class=" + quote(((Class JavaDoc)target).getName());
585         }
586         else {
587             d.refs = 2;
588             outputValue(target, outer, false);
589             outputValue(value, outer, false);
590             return;
591         }
592
593
594         // Special cases for methods.
595
if ((!expression && methodName.equals("set") && args.length == 2 &&
596          args[0] instanceof Integer JavaDoc) ||
597              (expression && methodName.equals("get") && args.length == 1 &&
598           args[0] instanceof Integer JavaDoc)) {
599             attributes = attributes + " index=" + quote(args[0].toString());
600             args = (args.length == 1) ? new Object JavaDoc[]{} : new Object JavaDoc[]{args[1]};
601         }
602         else if ((!expression && methodName.startsWith("set") && args.length == 1) ||
603          (expression && methodName.startsWith("get") && args.length == 0)) {
604         attributes = attributes + " property=" +
605         quote(Introspector.decapitalize(methodName.substring(3)));
606         }
607         else if (!methodName.equals("new") && !methodName.equals("newInstance")) {
608             attributes = attributes + " method=" + quote(methodName);
609         }
610
611         Vector statements = statementList(value);
612         // Use XML's short form when there is no body.
613
if (args.length == 0 && statements.size() == 0) {
614             writeln("<" + tag + attributes + "/>");
615             return;
616         }
617
618         writeln("<" + tag + attributes + ">");
619         indentation++;
620
621         for(int i = 0; i < args.length; i++) {
622             outputValue(args[i], null, true);
623         }
624
625         for(int i = 0; i < statements.size(); i++) {
626             Statement JavaDoc s = (Statement JavaDoc)statements.get(i);
627             outputStatement(s, value, false);
628         }
629
630         indentation--;
631         writeln("</" + tag + ">");
632     }
633 }
634
Popular Tags