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">
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 * <ul>
154 * <li>
155 * The default method name is "new".
156 * <li>
157 * A reference to a java class is written in the form
158 * <class>javax.swing.JButton</class>.
159 * <li>
160 * Instances of the wrapper classes for Java's primitive types are written
161 * using the name of the primitive type as the tag. For example, an
162 * instance of the <code>Integer</code> class could be written:
163 * <int>123</int>. Note that the <code>XMLEncoder</code> class
164 * uses Java's reflection package in which the conversion between
165 * Java's primitive types and their associated "wrapper classes"
166 * is handled internally. The API for the <code>XMLEncoder</code> class
167 * itself deals only with <code>Object</code>s.
168 * <li>
169 * In an element representing a nullary method whose name
170 * starts with "get", the "method" attribute is replaced
171 * with a "property" attribute whose value is given by removing
172 * the "get" prefix and decapitalizing the result.
173 * <li>
174 * In an element representing a monadic method whose name
175 * starts with "set", the "method" attribute is replaced
176 * with a "property" attribute whose value is given by removing
177 * the "set" prefix and decapitalizing the result.
178 * <li>
179 * In an element representing a method named "get" taking one
180 * integer argument, the "method" attribute is replaced
181 * with an "index" attribute whose value the value of the
182 * first argument.
183 * <li>
184 * In an element representing a method named "set" taking two arguments,
185 * the first of which is an integer, the "method" attribute is replaced
186 * with an "index" attribute whose value the value of the
187 * first argument.
210 private final boolean declaration;
211
212 private OutputStreamWriter out;
213 private Object owner;
214 private int indentation = 0;
215 private boolean internal = false;
216 private Map<Object, ValueData> valueToExpression;
217 private Map<Object, List<Statement>> targetToStatementList;
218 private boolean preambleWritten = false;
219 private NameGenerator nameGenerator;
220
221 private class ValueData {
222 public int refs = 0;
223 public boolean marked = false; // Marked -> refs > 0 unless ref was a target.
224 public String name = null;
225 public Expression exp = null;
226 }
227
228 /**
229 * Creates a new XML encoder to write out <em>JavaBeans</em>
230 * to the stream <code>out</code> using an XML encoding.
231 *
232 * @param out the stream to which the XML representation of
233 * the objects will be written
234 *
235 * @throws IllegalArgumentException
236 * if <code>out</code> is <code>null</code>
237 *
238 * @see XMLDecoder#XMLDecoder(InputStream)
239 */
240 public XMLEncoder(OutputStream out) {
241 this(out, "UTF-8", true, 0);
242 }
243
244 /**
245 * Creates a new XML encoder to write out <em>JavaBeans</em>
246 * to the stream <code>out</code> using the given <code>charset</code>
247 * starting from the given <code>indentation</code>.
248 *
249 * @param out the stream to which the XML representation of
250 * the objects will be written
251 * @param charset the name of the requested charset;
252 * may be either a canonical name or an alias
253 * @param declaration whether the XML declaration should be generated;
254 * set this to <code>false</code>
255 * when embedding the contents in another XML document
256 * @param indentation the number of space characters to indent the entire XML document by
257 *
258 * @throws IllegalArgumentException
259 * if <code>out</code> or <code>charset</code> is <code>null</code>,
260 * or if <code>indentation</code> is less than 0
261 *
262 * @throws IllegalCharsetNameException
263 * if <code>charset</code> name is illegal
264 *
265 * @throws UnsupportedCharsetException
266 * if no support for the named charset is available
267 * in this instance of the Java virtual machine
268 *
269 * @throws UnsupportedOperationException
270 * if loaded charset does not support encoding
271 *
272 * @see Charset#forName(String)
273 *
274 * @since 1.7
275 */
276 public XMLEncoder(OutputStream out, String charset, boolean declaration, int indentation) {
277 if (out == null) {
278 throw new IllegalArgumentException("the output stream cannot be null");
279 }
280 if (indentation < 0) {
281 throw new IllegalArgumentException("the indentation must be >= 0");
282 }
283 Charset cs = Charset.forName(charset);
284 this.encoder = cs.newEncoder();
285 this.charset = charset;
286 this.declaration = declaration;
287 this.indentation = indentation;
288 this.out = new OutputStreamWriter(out, cs.newEncoder());
289 valueToExpression = new IdentityHashMap<>();
290 targetToStatementList = new IdentityHashMap<>();
291 nameGenerator = new NameGenerator();
292 }
293
294 /**
295 * Sets the owner of this encoder to <code>owner</code>.
296 *
297 * @param owner The owner of this encoder.
298 *
299 * @see #getOwner
300 */
301 public void setOwner(Object owner) {
302 this.owner = owner;
303 writeExpression(new Expression(this, "getOwner", new Object[0]));
304 }
305
306 /**
307 * Gets the owner of this encoder.
308 *
309 * @return The owner of this encoder.
310 *
311 * @see #setOwner
312 */
313 public Object getOwner() {
314 return owner;
315 }
442 *
443 * @param oldExp The expression that will be written
444 * to the stream.
445 * @see java.beans.PersistenceDelegate#initialize
446 */
447 public void writeExpression(Expression oldExp) {
448 boolean internal = this.internal;
449 this.internal = true;
450 Object oldValue = getValue(oldExp);
451 if (get(oldValue) == null || (oldValue instanceof String && !internal)) {
452 getValueData(oldValue).exp = oldExp;
453 super.writeExpression(oldExp);
454 }
455 this.internal = internal;
456 }
457
458 /**
459 * This method writes out the preamble associated with the
460 * XML encoding if it has not been written already and
461 * then writes out all of the values that been
462 * written to the stream since the last time <code>flush</code>
463 * was called. After flushing, all internal references to the
464 * values that were written to this stream are cleared.
465 */
466 public void flush() {
467 if (!preambleWritten) { // Don't do this in constructor - it throws ... pending.
468 if (this.declaration) {
469 writeln("<?xml version=" + quote("1.0") +
470 " encoding=" + quote(this.charset) + "?>");
471 }
472 writeln("<java version=" + quote(System.getProperty("java.version")) +
473 " class=" + quote(XMLDecoder.class.getName()) + ">");
474 preambleWritten = true;
475 }
476 indentation++;
477 List<Statement> statements = statementList(this);
478 while (!statements.isEmpty()) {
479 Statement s = statements.remove(0);
480 if ("writeObject".equals(s.getMethodName())) {
481 outputValue(s.getArguments()[0], this, true);
482 }
504 void clear() {
505 super.clear();
506 nameGenerator.clear();
507 valueToExpression.clear();
508 targetToStatementList.clear();
509 }
510
511 Statement getMissedStatement() {
512 for (List<Statement> statements : this.targetToStatementList.values()) {
513 for (int i = 0; i < statements.size(); i++) {
514 if (Statement.class == statements.get(i).getClass()) {
515 return statements.remove(i);
516 }
517 }
518 }
519 return null;
520 }
521
522
523 /**
524 * This method calls <code>flush</code>, writes the closing
525 * postamble and then closes the output stream associated
526 * with this stream.
527 */
528 public void close() {
529 flush();
530 writeln("</java>");
531 try {
532 out.close();
533 }
534 catch (IOException e) {
535 getExceptionListener().exceptionThrown(e);
536 }
537 }
538
539 private String quote(String s) {
540 return "\"" + s + "\"";
541 }
542
543 private ValueData getValueData(Object o) {
544 ValueData d = valueToExpression.get(o);
545 if (d == null) {
546 d = new ValueData();
547 valueToExpression.put(o, d);
548 }
549 return d;
550 }
551
552 /**
553 * Returns <code>true</code> if the argument,
554 * a Unicode code point, is valid in XML documents.
555 * Unicode characters fit into the low sixteen bits of a Unicode code point,
556 * and pairs of Unicode <em>surrogate characters</em> can be combined
557 * to encode Unicode code point in documents containing only Unicode.
558 * (The <code>char</code> datatype in the Java Programming Language
559 * represents Unicode characters, including unpaired surrogates.)
560 * <par>
561 * [2] Char ::= #x0009 | #x000A | #x000D
562 * | [#x0020-#xD7FF]
563 * | [#xE000-#xFFFD]
564 * | [#x10000-#x10ffff]
565 * </par>
566 *
567 * @param code the 32-bit Unicode code point being tested
568 * @return <code>true</code> if the Unicode code point is valid,
569 * <code>false</code> otherwise
570 */
571 private static boolean isValidCharCode(int code) {
572 return (0x0020 <= code && code <= 0xD7FF)
573 || (0x000A == code)
574 || (0x0009 == code)
575 || (0x000D == code)
576 || (0xE000 <= code && code <= 0xFFFD)
577 || (0x10000 <= code && code <= 0x10ffff);
578 }
579
580 private void writeln(String exp) {
581 try {
582 StringBuilder sb = new StringBuilder();
583 for(int i = 0; i < indentation; i++) {
584 sb.append(' ');
585 }
586 sb.append(exp);
587 sb.append('\n');
588 this.out.write(sb.toString());
589 }
|
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} class is a complementary alternative to
37 * the {@code ObjectOutputStream} and can used to generate
38 * a textual representation of a <em>JavaBean</em> in the same
39 * way that the {@code ObjectOutputStream} can
40 * be used to create binary representation of {@code Serializable}
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}
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} continues to be recommended
57 * for interprocess communication and general purpose serialization.
58 * <p>
59 * The {@code XMLEncoder} 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} 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} 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">
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 * <ul>
154 * <li>
155 * The default method name is "new".
156 * <li>
157 * A reference to a java class is written in the form
158 * <class>javax.swing.JButton</class>.
159 * <li>
160 * Instances of the wrapper classes for Java's primitive types are written
161 * using the name of the primitive type as the tag. For example, an
162 * instance of the {@code Integer} class could be written:
163 * <int>123</int>. Note that the {@code XMLEncoder} class
164 * uses Java's reflection package in which the conversion between
165 * Java's primitive types and their associated "wrapper classes"
166 * is handled internally. The API for the {@code XMLEncoder} class
167 * itself deals only with {@code Object}s.
168 * <li>
169 * In an element representing a nullary method whose name
170 * starts with "get", the "method" attribute is replaced
171 * with a "property" attribute whose value is given by removing
172 * the "get" prefix and decapitalizing the result.
173 * <li>
174 * In an element representing a monadic method whose name
175 * starts with "set", the "method" attribute is replaced
176 * with a "property" attribute whose value is given by removing
177 * the "set" prefix and decapitalizing the result.
178 * <li>
179 * In an element representing a method named "get" taking one
180 * integer argument, the "method" attribute is replaced
181 * with an "index" attribute whose value the value of the
182 * first argument.
183 * <li>
184 * In an element representing a method named "set" taking two arguments,
185 * the first of which is an integer, the "method" attribute is replaced
186 * with an "index" attribute whose value the value of the
187 * first argument.
210 private final boolean declaration;
211
212 private OutputStreamWriter out;
213 private Object owner;
214 private int indentation = 0;
215 private boolean internal = false;
216 private Map<Object, ValueData> valueToExpression;
217 private Map<Object, List<Statement>> targetToStatementList;
218 private boolean preambleWritten = false;
219 private NameGenerator nameGenerator;
220
221 private class ValueData {
222 public int refs = 0;
223 public boolean marked = false; // Marked -> refs > 0 unless ref was a target.
224 public String name = null;
225 public Expression exp = null;
226 }
227
228 /**
229 * Creates a new XML encoder to write out <em>JavaBeans</em>
230 * to the stream {@code out} using an XML encoding.
231 *
232 * @param out the stream to which the XML representation of
233 * the objects will be written
234 *
235 * @throws IllegalArgumentException
236 * if {@code out} is {@code null}
237 *
238 * @see XMLDecoder#XMLDecoder(InputStream)
239 */
240 public XMLEncoder(OutputStream out) {
241 this(out, "UTF-8", true, 0);
242 }
243
244 /**
245 * Creates a new XML encoder to write out <em>JavaBeans</em>
246 * to the stream {@code out} using the given {@code charset}
247 * starting from the given {@code indentation}.
248 *
249 * @param out the stream to which the XML representation of
250 * the objects will be written
251 * @param charset the name of the requested charset;
252 * may be either a canonical name or an alias
253 * @param declaration whether the XML declaration should be generated;
254 * set this to {@code false}
255 * when embedding the contents in another XML document
256 * @param indentation the number of space characters to indent the entire XML document by
257 *
258 * @throws IllegalArgumentException
259 * if {@code out} or {@code charset} is {@code null},
260 * or if {@code indentation} is less than 0
261 *
262 * @throws IllegalCharsetNameException
263 * if {@code charset} name is illegal
264 *
265 * @throws UnsupportedCharsetException
266 * if no support for the named charset is available
267 * in this instance of the Java virtual machine
268 *
269 * @throws UnsupportedOperationException
270 * if loaded charset does not support encoding
271 *
272 * @see Charset#forName(String)
273 *
274 * @since 1.7
275 */
276 public XMLEncoder(OutputStream out, String charset, boolean declaration, int indentation) {
277 if (out == null) {
278 throw new IllegalArgumentException("the output stream cannot be null");
279 }
280 if (indentation < 0) {
281 throw new IllegalArgumentException("the indentation must be >= 0");
282 }
283 Charset cs = Charset.forName(charset);
284 this.encoder = cs.newEncoder();
285 this.charset = charset;
286 this.declaration = declaration;
287 this.indentation = indentation;
288 this.out = new OutputStreamWriter(out, cs.newEncoder());
289 valueToExpression = new IdentityHashMap<>();
290 targetToStatementList = new IdentityHashMap<>();
291 nameGenerator = new NameGenerator();
292 }
293
294 /**
295 * Sets the owner of this encoder to {@code owner}.
296 *
297 * @param owner The owner of this encoder.
298 *
299 * @see #getOwner
300 */
301 public void setOwner(Object owner) {
302 this.owner = owner;
303 writeExpression(new Expression(this, "getOwner", new Object[0]));
304 }
305
306 /**
307 * Gets the owner of this encoder.
308 *
309 * @return The owner of this encoder.
310 *
311 * @see #setOwner
312 */
313 public Object getOwner() {
314 return owner;
315 }
442 *
443 * @param oldExp The expression that will be written
444 * to the stream.
445 * @see java.beans.PersistenceDelegate#initialize
446 */
447 public void writeExpression(Expression oldExp) {
448 boolean internal = this.internal;
449 this.internal = true;
450 Object oldValue = getValue(oldExp);
451 if (get(oldValue) == null || (oldValue instanceof String && !internal)) {
452 getValueData(oldValue).exp = oldExp;
453 super.writeExpression(oldExp);
454 }
455 this.internal = internal;
456 }
457
458 /**
459 * This method writes out the preamble associated with the
460 * XML encoding if it has not been written already and
461 * then writes out all of the values that been
462 * written to the stream since the last time {@code flush}
463 * was called. After flushing, all internal references to the
464 * values that were written to this stream are cleared.
465 */
466 public void flush() {
467 if (!preambleWritten) { // Don't do this in constructor - it throws ... pending.
468 if (this.declaration) {
469 writeln("<?xml version=" + quote("1.0") +
470 " encoding=" + quote(this.charset) + "?>");
471 }
472 writeln("<java version=" + quote(System.getProperty("java.version")) +
473 " class=" + quote(XMLDecoder.class.getName()) + ">");
474 preambleWritten = true;
475 }
476 indentation++;
477 List<Statement> statements = statementList(this);
478 while (!statements.isEmpty()) {
479 Statement s = statements.remove(0);
480 if ("writeObject".equals(s.getMethodName())) {
481 outputValue(s.getArguments()[0], this, true);
482 }
504 void clear() {
505 super.clear();
506 nameGenerator.clear();
507 valueToExpression.clear();
508 targetToStatementList.clear();
509 }
510
511 Statement getMissedStatement() {
512 for (List<Statement> statements : this.targetToStatementList.values()) {
513 for (int i = 0; i < statements.size(); i++) {
514 if (Statement.class == statements.get(i).getClass()) {
515 return statements.remove(i);
516 }
517 }
518 }
519 return null;
520 }
521
522
523 /**
524 * This method calls {@code flush}, writes the closing
525 * postamble and then closes the output stream associated
526 * with this stream.
527 */
528 public void close() {
529 flush();
530 writeln("</java>");
531 try {
532 out.close();
533 }
534 catch (IOException e) {
535 getExceptionListener().exceptionThrown(e);
536 }
537 }
538
539 private String quote(String s) {
540 return "\"" + s + "\"";
541 }
542
543 private ValueData getValueData(Object o) {
544 ValueData d = valueToExpression.get(o);
545 if (d == null) {
546 d = new ValueData();
547 valueToExpression.put(o, d);
548 }
549 return d;
550 }
551
552 /**
553 * Returns {@code true} if the argument,
554 * a Unicode code point, is valid in XML documents.
555 * Unicode characters fit into the low sixteen bits of a Unicode code point,
556 * and pairs of Unicode <em>surrogate characters</em> can be combined
557 * to encode Unicode code point in documents containing only Unicode.
558 * (The {@code char} datatype in the Java Programming Language
559 * represents Unicode characters, including unpaired surrogates.)
560 * <par>
561 * [2] Char ::= #x0009 | #x000A | #x000D
562 * | [#x0020-#xD7FF]
563 * | [#xE000-#xFFFD]
564 * | [#x10000-#x10ffff]
565 * </par>
566 *
567 * @param code the 32-bit Unicode code point being tested
568 * @return {@code true} if the Unicode code point is valid,
569 * {@code false} otherwise
570 */
571 private static boolean isValidCharCode(int code) {
572 return (0x0020 <= code && code <= 0xD7FF)
573 || (0x000A == code)
574 || (0x0009 == code)
575 || (0x000D == code)
576 || (0xE000 <= code && code <= 0xFFFD)
577 || (0x10000 <= code && code <= 0x10ffff);
578 }
579
580 private void writeln(String exp) {
581 try {
582 StringBuilder sb = new StringBuilder();
583 for(int i = 0; i < indentation; i++) {
584 sb.append(' ');
585 }
586 sb.append(exp);
587 sb.append('\n');
588 this.out.write(sb.toString());
589 }
|