001 /*
002 * Copyright 2000-2006 Sun Microsystems, Inc. All Rights Reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation. Sun designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Sun in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
022 * CA 95054 USA or visit www.sun.com if you need additional information or
023 * have any questions.
024 */
025 package java.beans;
026
027 import java.io.*;
028 import java.util.*;
029 import java.lang.reflect.*;
030 import java.nio.charset.Charset;
031 import java.nio.charset.CharsetEncoder;
032 import java.nio.charset.IllegalCharsetNameException;
033 import java.nio.charset.UnsupportedCharsetException;
034
035 /**
036 * The <code>XMLEncoder</code> class is a complementary alternative to
037 * the <code>ObjectOutputStream</code> and can used to generate
038 * a textual representation of a <em>JavaBean</em> in the same
039 * way that the <code>ObjectOutputStream</code> can
040 * be used to create binary representation of <code>Serializable</code>
041 * objects. For example, the following fragment can be used to create
042 * a textual representation the supplied <em>JavaBean</em>
043 * and all its properties:
044 * <pre>
045 * XMLEncoder e = new XMLEncoder(
046 * new BufferedOutputStream(
047 * new FileOutputStream("Test.xml")));
048 * e.writeObject(new JButton("Hello, world"));
049 * e.close();
050 * </pre>
051 * Despite the similarity of their APIs, the <code>XMLEncoder</code>
052 * class is exclusively designed for the purpose of archiving graphs
053 * of <em>JavaBean</em>s as textual representations of their public
054 * properties. Like Java source files, documents written this way
055 * have a natural immunity to changes in the implementations of the classes
056 * involved. The <code>ObjectOutputStream</code> continues to be recommended
057 * for interprocess communication and general purpose serialization.
058 * <p>
059 * The <code>XMLEncoder</code> class provides a default denotation for
060 * <em>JavaBean</em>s in which they are represented as XML documents
061 * complying with version 1.0 of the XML specification and the
062 * UTF-8 character encoding of the Unicode/ISO 10646 character set.
063 * The XML documents produced by the <code>XMLEncoder</code> class are:
064 * <ul>
065 * <li>
066 * <em>Portable and version resilient</em>: they have no dependencies
067 * on the private implementation of any class and so, like Java source
068 * files, they may be exchanged between environments which may have
069 * different versions of some of the classes and between VMs from
070 * different vendors.
071 * <li>
072 * <em>Structurally compact</em>: The <code>XMLEncoder</code> class
073 * uses a <em>redundancy elimination</em> algorithm internally so that the
074 * default values of a Bean's properties are not written to the stream.
075 * <li>
076 * <em>Fault tolerant</em>: Non-structural errors in the file,
077 * caused either by damage to the file or by API changes
078 * made to classes in an archive remain localized
079 * so that a reader can report the error and continue to load the parts
080 * of the document which were not affected by the error.
081 * </ul>
082 * <p>
083 * Below is an example of an XML archive containing
084 * some user interface components from the <em>swing</em> toolkit:
085 * <pre>
086 * <?xml version="1.0" encoding="UTF-8"?>
087 * <java version="1.0" class="java.beans.XMLDecoder">
088 * <object class="javax.swing.JFrame">
089 * <void property="name">
090 * <string>frame1</string>
091 * </void>
092 * <void property="bounds">
093 * <object class="java.awt.Rectangle">
094 * <int>0</int>
095 * <int>0</int>
096 * <int>200</int>
097 * <int>200</int>
098 * </object>
099 * </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 * @version 1.44 05/09/07
206 * @author Philip Milne
207 */
208 public class XMLEncoder extends Encoder {
209
210 private final CharsetEncoder encoder;
211 private final String charset;
212 private final boolean declaration;
213
214 private OutputStreamWriter out;
215 private Object owner;
216 private int indentation = 0;
217 private boolean internal = false;
218 private Map valueToExpression;
219 private Map targetToStatementList;
220 private boolean preambleWritten = false;
221 private NameGenerator nameGenerator;
222
223 private class ValueData {
224 public int refs = 0;
225 public boolean marked = false; // Marked -> refs > 0 unless ref was a target.
226 public String name = null;
227 public Expression exp = null;
228 }
229
230 /**
231 * Creates a new XML encoder to write out <em>JavaBeans</em>
232 * to the stream <code>out</code> using an XML encoding.
233 *
234 * @param out the stream to which the XML representation of
235 * the objects will be written
236 *
237 * @throws IllegalArgumentException
238 * if <code>out</code> is <code>null</code>
239 *
240 * @see XMLDecoder#XMLDecoder(InputStream)
241 */
242 public XMLEncoder(OutputStream out) {
243 this (out, "UTF-8", true, 0);
244 }
245
246 /**
247 * Creates a new XML encoder to write out <em>JavaBeans</em>
248 * to the stream <code>out</code> using the given <code>charset</code>
249 * starting from the given <code>indentation</code>.
250 *
251 * @param out the stream to which the XML representation of
252 * the objects will be written
253 * @param charset the name of the requested charset;
254 * may be either a canonical name or an alias
255 * @param declaration whether the XML declaration should be generated;
256 * set this to <code>false</code>
257 * when embedding the contents in another XML document
258 * @param indentation the number of space characters to indent the entire XML document by
259 *
260 * @throws IllegalArgumentException
261 * if <code>out</code> or <code>charset</code> is <code>null</code>,
262 * or if <code>indentation</code> is less than 0
263 *
264 * @throws IllegalCharsetNameException
265 * if <code>charset</code> name is illegal
266 *
267 * @throws UnsupportedCharsetException
268 * if no support for the named charset is available
269 * in this instance of the Java virtual machine
270 *
271 * @throws UnsupportedOperationException
272 * if loaded charset does not support encoding
273 *
274 * @see Charset#forName(String)
275 *
276 * @since 1.7
277 */
278 public XMLEncoder(OutputStream out, String charset,
279 boolean declaration, int indentation) {
280 if (out == null) {
281 throw new IllegalArgumentException(
282 "the output stream cannot be null");
283 }
284 if (indentation < 0) {
285 throw new IllegalArgumentException(
286 "the indentation must be >= 0");
287 }
288 Charset cs = Charset.forName(charset);
289 this .encoder = cs.newEncoder();
290 this .charset = charset;
291 this .declaration = declaration;
292 this .indentation = indentation;
293 this .out = new OutputStreamWriter(out, cs.newEncoder());
294 valueToExpression = new IdentityHashMap();
295 targetToStatementList = new IdentityHashMap();
296 nameGenerator = new NameGenerator();
297 }
298
299 /**
300 * Sets the owner of this encoder to <code>owner</code>.
301 *
302 * @param owner The owner of this encoder.
303 *
304 * @see #getOwner
305 */
306 public void setOwner(Object owner) {
307 this .owner = owner;
308 writeExpression(new Expression(this , "getOwner", new Object[0]));
309 }
310
311 /**
312 * Gets the owner of this encoder.
313 *
314 * @return The owner of this encoder.
315 *
316 * @see #setOwner
317 */
318 public Object getOwner() {
319 return owner;
320 }
321
322 /**
323 * Write an XML representation of the specified object to the output.
324 *
325 * @param o The object to be written to the stream.
326 *
327 * @see XMLDecoder#readObject
328 */
329 public void writeObject(Object o) {
330 if (internal) {
331 super .writeObject(o);
332 } else {
333 writeStatement(new Statement(this , "writeObject",
334 new Object[] { o }));
335 }
336 }
337
338 private Vector statementList(Object target) {
339 Vector list = (Vector) targetToStatementList.get(target);
340 if (list != null) {
341 return list;
342 }
343 list = new Vector();
344 targetToStatementList.put(target, list);
345 return list;
346 }
347
348 private void mark(Object o, boolean isArgument) {
349 if (o == null || o == this ) {
350 return;
351 }
352 ValueData d = getValueData(o);
353 Expression exp = d.exp;
354 // Do not mark liternal strings. Other strings, which might,
355 // for example, come from resource bundles should still be marked.
356 if (o.getClass() == String.class && exp == null) {
357 return;
358 }
359
360 // Bump the reference counts of all arguments
361 if (isArgument) {
362 d.refs++;
363 }
364 if (d.marked) {
365 return;
366 }
367 d.marked = true;
368 Object target = exp.getTarget();
369 if (!(target instanceof Class)) {
370 statementList(target).add(exp);
371 // Pending: Why does the reference count need to
372 // be incremented here?
373 d.refs++;
374 }
375 mark(exp);
376 }
377
378 private void mark(Statement stm) {
379 Object[] args = stm.getArguments();
380 for (int i = 0; i < args.length; i++) {
381 Object arg = args[i];
382 mark(arg, true);
383 }
384 mark(stm.getTarget(), false);
385 }
386
387 /**
388 * Records the Statement so that the Encoder will
389 * produce the actual output when the stream is flushed.
390 * <P>
391 * This method should only be invoked within the context
392 * of initializing a persistence delegate.
393 *
394 * @param oldStm The statement that will be written
395 * to the stream.
396 * @see java.beans.PersistenceDelegate#initialize
397 */
398 public void writeStatement(Statement oldStm) {
399 // System.out.println("XMLEncoder::writeStatement: " + oldStm);
400 boolean internal = this .internal;
401 this .internal = true;
402 try {
403 super .writeStatement(oldStm);
404 /*
405 Note we must do the mark first as we may
406 require the results of previous values in
407 this context for this statement.
408 Test case is:
409 os.setOwner(this);
410 os.writeObject(this);
411 */
412 mark(oldStm);
413 statementList(oldStm.getTarget()).add(oldStm);
414 } catch (Exception e) {
415 getExceptionListener().exceptionThrown(
416 new Exception("XMLEncoder: discarding statement "
417 + oldStm, e));
418 }
419 this .internal = internal;
420 }
421
422 /**
423 * Records the Expression so that the Encoder will
424 * produce the actual output when the stream is flushed.
425 * <P>
426 * This method should only be invoked within the context of
427 * initializing a persistence delegate or setting up an encoder to
428 * read from a resource bundle.
429 * <P>
430 * For more information about using resource bundles with the
431 * XMLEncoder, see
432 * http://java.sun.com/products/jfc/tsc/articles/persistence4/#i18n
433 *
434 * @param oldExp The expression that will be written
435 * to the stream.
436 * @see java.beans.PersistenceDelegate#initialize
437 */
438 public void writeExpression(Expression oldExp) {
439 boolean internal = this .internal;
440 this .internal = true;
441 Object oldValue = getValue(oldExp);
442 if (get(oldValue) == null
443 || (oldValue instanceof String && !internal)) {
444 getValueData(oldValue).exp = oldExp;
445 super .writeExpression(oldExp);
446 }
447 this .internal = internal;
448 }
449
450 /**
451 * This method writes out the preamble associated with the
452 * XML encoding if it has not been written already and
453 * then writes out all of the values that been
454 * written to the stream since the last time <code>flush</code>
455 * was called. After flushing, all internal references to the
456 * values that were written to this stream are cleared.
457 */
458 public void flush() {
459 if (!preambleWritten) { // Don't do this in constructor - it throws ... pending.
460 if (this .declaration) {
461 writeln("<?xml version=" + quote("1.0") + " encoding="
462 + quote(this .charset) + "?>");
463 }
464 writeln("<java version="
465 + quote(System.getProperty("java.version"))
466 + " class=" + quote(XMLDecoder.class.getName())
467 + ">");
468 preambleWritten = true;
469 }
470 indentation++;
471 Vector roots = statementList(this );
472 for (int i = 0; i < roots.size(); i++) {
473 Statement s = (Statement) roots.get(i);
474 if ("writeObject".equals(s.getMethodName())) {
475 outputValue(s.getArguments()[0], this , true);
476 } else {
477 outputStatement(s, this , false);
478 }
479 }
480 indentation--;
481
482 try {
483 out.flush();
484 } catch (IOException e) {
485 getExceptionListener().exceptionThrown(e);
486 }
487 clear();
488 }
489
490 void clear() {
491 super .clear();
492 nameGenerator.clear();
493 valueToExpression.clear();
494 targetToStatementList.clear();
495 }
496
497 /**
498 * This method calls <code>flush</code>, writes the closing
499 * postamble and then closes the output stream associated
500 * with this stream.
501 */
502 public void close() {
503 flush();
504 writeln("</java>");
505 try {
506 out.close();
507 } catch (IOException e) {
508 getExceptionListener().exceptionThrown(e);
509 }
510 }
511
512 private String quote(String s) {
513 return "\"" + s + "\"";
514 }
515
516 private ValueData getValueData(Object o) {
517 ValueData d = (ValueData) valueToExpression.get(o);
518 if (d == null) {
519 d = new ValueData();
520 valueToExpression.put(o, d);
521 }
522 return d;
523 }
524
525 /**
526 * Returns <code>true</code> if the argument,
527 * a Unicode code point, is valid in XML documents.
528 * Unicode characters fit into the low sixteen bits of a Unicode code point,
529 * and pairs of Unicode <em>surrogate characters</em> can be combined
530 * to encode Unicode code point in documents containing only Unicode.
531 * (The <code>char</code> datatype in the Java Programming Language
532 * represents Unicode characters, including unpaired surrogates.)
533 * <par>
534 * [2] Char ::= #x0009 | #x000A | #x000D
535 * | [#x0020-#xD7FF]
536 * | [#xE000-#xFFFD]
537 * | [#x10000-#x10ffff]
538 * </par>
539 *
540 * @param code the 32-bit Unicode code point being tested
541 * @return <code>true</code> if the Unicode code point is valid,
542 * <code>false</code> otherwise
543 */
544 private static boolean isValidCharCode(int code) {
545 return (0x0020 <= code && code <= 0xD7FF) || (0x000A == code)
546 || (0x0009 == code) || (0x000D == code)
547 || (0xE000 <= code && code <= 0xFFFD)
548 || (0x10000 <= code && code <= 0x10ffff);
549 }
550
551 private void writeln(String exp) {
552 try {
553 StringBuilder sb = new StringBuilder();
554 for (int i = 0; i < indentation; i++) {
555 sb.append(' ');
556 }
557 sb.append(exp);
558 sb.append('\n');
559 this .out.write(sb.toString());
560 } catch (IOException e) {
561 getExceptionListener().exceptionThrown(e);
562 }
563 }
564
565 private void outputValue(Object value, Object outer,
566 boolean isArgument) {
567 if (value == null) {
568 writeln("<null/>");
569 return;
570 }
571
572 if (value instanceof Class) {
573 writeln("<class>" + ((Class) value).getName() + "</class>");
574 return;
575 }
576
577 ValueData d = getValueData(value);
578 if (d.exp != null) {
579 Object target = d.exp.getTarget();
580 String methodName = d.exp.getMethodName();
581
582 if (target == null || methodName == null) {
583 throw new NullPointerException(
584 (target == null ? "target" : "methodName")
585 + " should not be null");
586 }
587
588 if (target instanceof Field && methodName.equals("get")) {
589 Field f = (Field) target;
590 writeln("<object class="
591 + quote(f.getDeclaringClass().getName())
592 + " field=" + quote(f.getName()) + "/>");
593 return;
594 }
595
596 Class primitiveType = ReflectionUtils
597 .primitiveTypeFor(value.getClass());
598 if (primitiveType != null && target == value.getClass()
599 && methodName.equals("new")) {
600 String primitiveTypeName = primitiveType.getName();
601 // Make sure that character types are quoted correctly.
602 if (primitiveType == Character.TYPE) {
603 char code = ((Character) value).charValue();
604 if (!isValidCharCode(code)) {
605 writeln(createString(code));
606 return;
607 }
608 value = quoteCharCode(code);
609 if (value == null) {
610 value = Character.valueOf(code);
611 }
612 }
613 writeln("<" + primitiveTypeName + ">" + value + "</"
614 + primitiveTypeName + ">");
615 return;
616 }
617
618 } else if (value instanceof String) {
619 writeln(createString((String) value));
620 return;
621 }
622
623 if (d.name != null) {
624 writeln("<object idref=" + quote(d.name) + "/>");
625 return;
626 }
627
628 outputStatement(d.exp, outer, isArgument);
629 }
630
631 private static String quoteCharCode(int code) {
632 switch (code) {
633 case '&':
634 return "&";
635 case '<':
636 return "<";
637 case '>':
638 return ">";
639 case '"':
640 return """;
641 case '\'':
642 return "'";
643 case '\r':
644 return " ";
645 default:
646 return null;
647 }
648 }
649
650 private static String createString(int code) {
651 return "<char code=\"#" + Integer.toString(code, 16) + "\"/>";
652 }
653
654 private String createString(String string) {
655 StringBuilder sb = new StringBuilder();
656 sb.append("<string>");
657 int index = 0;
658 while (index < string.length()) {
659 int point = string.codePointAt(index);
660 int count = Character.charCount(point);
661
662 if (isValidCharCode(point)
663 && this .encoder.canEncode(string.substring(index,
664 index + count))) {
665 String value = quoteCharCode(point);
666 if (value != null) {
667 sb.append(value);
668 } else {
669 sb.appendCodePoint(point);
670 }
671 index += count;
672 } else {
673 sb.append(createString(string.charAt(index)));
674 index++;
675 }
676 }
677 sb.append("</string>");
678 return sb.toString();
679 }
680
681 private void outputStatement(Statement exp, Object outer,
682 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")
689 + " should not be null");
690 }
691
692 Object[] args = exp.getArguments();
693 boolean expression = exp.getClass() == Expression.class;
694 Object value = (expression) ? getValue((Expression) exp) : null;
695
696 String tag = (expression && isArgument) ? "object" : "void";
697 String attributes = "";
698 ValueData d = getValueData(value);
699 if (expression) {
700 if (d.refs > 1) {
701 String instanceName = nameGenerator.instanceName(value);
702 d.name = instanceName;
703 attributes = attributes + " id=" + quote(instanceName);
704 }
705 }
706
707 // Special cases for targets.
708 if (target == outer) {
709 } else if (target == Array.class
710 && methodName.equals("newInstance")) {
711 tag = "array";
712 attributes = attributes + " class="
713 + quote(((Class) args[0]).getName());
714 attributes = attributes + " length="
715 + quote(args[1].toString());
716 args = new Object[] {};
717 } else if (target.getClass() == Class.class) {
718 attributes = attributes + " class="
719 + quote(((Class) target).getName());
720 } else {
721 d.refs = 2;
722 getValueData(target).refs++;
723 outputValue(target, outer, false);
724 if (isArgument) {
725 outputValue(value, outer, false);
726 }
727 return;
728 }
729
730 // Special cases for methods.
731 if ((!expression && methodName.equals("set")
732 && args.length == 2 && args[0] instanceof Integer)
733 || (expression && methodName.equals("get")
734 && args.length == 1 && args[0] instanceof Integer)) {
735 attributes = attributes + " index="
736 + quote(args[0].toString());
737 args = (args.length == 1) ? new Object[] {}
738 : new Object[] { args[1] };
739 } else if ((!expression && methodName.startsWith("set") && args.length == 1)
740 || (expression && methodName.startsWith("get") && args.length == 0)) {
741 attributes = attributes
742 + " property="
743 + quote(Introspector.decapitalize(methodName
744 .substring(3)));
745 } else if (!methodName.equals("new")
746 && !methodName.equals("newInstance")) {
747 attributes = attributes + " method=" + quote(methodName);
748 }
749
750 Vector statements = statementList(value);
751 // Use XML's short form when there is no body.
752 if (args.length == 0 && statements.size() == 0) {
753 writeln("<" + tag + attributes + "/>");
754 return;
755 }
756
757 writeln("<" + tag + attributes + ">");
758 indentation++;
759
760 for (int i = 0; i < args.length; i++) {
761 outputValue(args[i], null, true);
762 }
763
764 for (int i = 0; i < statements.size(); i++) {
765 Statement s = (Statement) statements.get(i);
766 outputStatement(s, value, false);
767 }
768
769 indentation--;
770 writeln("</" + tag + ">");
771 }
772 }
|