1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.apache.commons.betwixt.io;
17
18 import java.beans.IntrospectionException;
19 import java.io.BufferedWriter;
20 import java.io.IOException;
21 import java.io.OutputStream;
22 import java.io.OutputStreamWriter;
23 import java.io.UnsupportedEncodingException;
24 import java.io.Writer;
25
26 import org.apache.commons.betwixt.XMLUtils;
27 import org.apache.commons.betwixt.strategy.MixedContentEncodingStrategy;
28 import org.apache.commons.logging.Log;
29 import org.apache.commons.logging.LogFactory;
30 import org.xml.sax.Attributes;
31 import org.xml.sax.SAXException;
32
33 /*** <p><code>BeanWriter</code> outputs beans as XML to an io stream.</p>
34 *
35 * <p>The output for each bean is an xml fragment
36 * (rather than a well-formed xml-document).
37 * This allows bean representations to be appended to a document
38 * by writing each in turn to the stream.
39 * So to create a well formed xml document,
40 * you'll need to write the prolog to the stream first.
41 * If you append more than one bean to the stream,
42 * then you'll need to add a wrapping root element as well.
43 *
44 * <p> The line ending to be used is set by {@link #setEndOfLine}.
45 *
46 * <p> The output can be formatted (with whitespace) for easy reading
47 * by calling {@link #enablePrettyPrint}.
48 * The output will be indented.
49 * The indent string used is set by {@link #setIndent}.
50 *
51 * <p> Bean graphs can sometimes contain cycles.
52 * Care must be taken when serializing cyclic bean graphs
53 * since this can lead to infinite recursion.
54 * The approach taken by <code>BeanWriter</code> is to automatically
55 * assign an <code>ID</code> attribute value to beans.
56 * When a cycle is encountered,
57 * an element is written that has the <code>IDREF</code> attribute set to the
58 * id assigned earlier.
59 *
60 * <p> The names of the <code>ID</code> and <code>IDREF</code> attributes used
61 * can be customized by the <code>XMLBeanInfo</code>.
62 * The id's used can also be customized by the user
63 * via <code>IDGenerator</code> subclasses.
64 * The implementation used can be set by the <code>IdGenerator</code> property.
65 * BeanWriter defaults to using <code>SequentialIDGenerator</code>
66 * which supplies id values in numeric sequence.
67 *
68 * <p>If generated <code>ID</code> attribute values are not acceptable in the output,
69 * then this can be disabled by setting the <code>WriteIDs</code> property to false.
70 * If a cyclic reference is encountered in this case then a
71 * <code>CyclicReferenceException</code> will be thrown.
72 * When the <code>WriteIDs</code> property is set to false,
73 * it is recommended that this exception is caught by the caller.
74 *
75 *
76 * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
77 * @author <a href="mailto:martin@mvdb.net">Martin van den Bemt</a>
78 */
79 public class BeanWriter extends AbstractBeanWriter {
80
81 /*** Where the output goes */
82 private Writer writer;
83 /*** text used for end of lines. Defaults to <code>\n</code>*/
84 private static final String EOL = "\n";
85 /*** text used for end of lines. Defaults to <code>\n</code>*/
86 private String endOfLine = EOL;
87 /*** indentation text */
88 private String indent;
89
90 /*** should we flush after writing bean */
91 private boolean autoFlush;
92 /*** Log used for logging (Doh!) */
93 private Log log = LogFactory.getLog( BeanWriter.class );
94 /*** Has any content (excluding attributes) been written to the current element */
95 private boolean currentElementIsEmpty = false;
96 /*** Has the current element written any body text */
97 private boolean currentElementHasBodyText = false;
98 /*** Has the last start tag been closed */
99 private boolean closedStartTag = true;
100 /*** Should an end tag be added for empty elements? */
101 private boolean addEndTagForEmptyElement = false;
102 /*** Current level of indentation (starts at 1 with the first element) */
103 private int indentLevel;
104 /*** USed to determine how body content should be encoded before being output*/
105 private MixedContentEncodingStrategy mixedContentEncodingStrategy
106 = MixedContentEncodingStrategy.DEFAULT;
107
108 /***
109 * <p> Constructor uses <code>System.out</code> for output.</p>
110 */
111 public BeanWriter() {
112 this( System.out );
113 }
114
115 /***
116 * <p> Constuctor uses given <code>OutputStream</code> for output.</p>
117 *
118 * @param out write out representations to this stream
119 */
120 public BeanWriter(OutputStream out) {
121 this.writer = new BufferedWriter( new OutputStreamWriter( out ) );
122 this.autoFlush = true;
123 }
124
125 /***
126 * <p>Constuctor uses given <code>OutputStream</code> for output
127 * and allows encoding to be set.</p>
128 *
129 * @param out write out representations to this stream
130 * @param enc the name of the encoding to be used. This should be compatible
131 * with the encoding types described in <code>java.io</code>
132 * @throws UnsupportedEncodingException if the given encoding is not supported
133 */
134 public BeanWriter(OutputStream out, String enc) throws UnsupportedEncodingException {
135 this.writer = new BufferedWriter( new OutputStreamWriter( out, enc ) );
136 this.autoFlush = true;
137 }
138
139 /***
140 * <p> Constructor sets writer used for output.</p>
141 *
142 * @param writer write out representations to this writer
143 */
144 public BeanWriter(Writer writer) {
145 this.writer = writer;
146 }
147
148 /***
149 * A helper method that allows you to write the XML Declaration.
150 * This should only be called once before you output any beans.
151 *
152 * @param xmlDeclaration is the XML declaration string typically of
153 * the form "<xml version='1.0' encoding='UTF-8' ?>
154 *
155 * @throws IOException when declaration cannot be written
156 */
157 public void writeXmlDeclaration(String xmlDeclaration) throws IOException {
158 writer.write( xmlDeclaration );
159 printLine();
160 }
161
162 /***
163 * Allows output to be flushed on the underlying output stream
164 *
165 * @throws IOException when the flush cannot be completed
166 */
167 public void flush() throws IOException {
168 writer.flush();
169 }
170
171 /***
172 * Closes the underlying output stream
173 *
174 * @throws IOException when writer cannot be closed
175 */
176 public void close() throws IOException {
177 writer.close();
178 }
179
180 /***
181 * Write the given object to the stream (and then flush).
182 *
183 * @param bean write this <code>Object</code> to the stream
184 * @throws IOException if an IO problem causes failure
185 * @throws SAXException if a SAX problem causes failure
186 * @throws IntrospectionException if bean cannot be introspected
187 */
188 public void write(Object bean) throws IOException, SAXException, IntrospectionException {
189
190 super.write(bean);
191
192 if ( autoFlush ) {
193 writer.flush();
194 }
195 }
196
197
198 /***
199 * <p> Switch on formatted output.
200 * This sets the end of line and the indent.
201 * The default is adding 2 spaces and a newline
202 */
203 public void enablePrettyPrint() {
204 endOfLine = EOL;
205 indent = " ";
206 }
207
208 /***
209 * Gets the string used to mark end of lines.
210 *
211 * @return the string used for end of lines
212 */
213 public String getEndOfLine() {
214 return endOfLine;
215 }
216
217 /***
218 * Sets the string used for end of lines
219 * Produces a warning the specified value contains an invalid whitespace character
220 *
221 * @param endOfLine the <code>String</code to use
222 */
223 public void setEndOfLine(String endOfLine) {
224 this.endOfLine = endOfLine;
225 for (int i = 0; i < endOfLine.length(); i++) {
226 if (!Character.isWhitespace(endOfLine.charAt(i))) {
227 log.warn("Invalid EndOfLine character(s)");
228 break;
229 }
230 }
231
232 }
233
234 /***
235 * Gets the indent string
236 *
237 * @return the string used for indentation
238 */
239 public String getIndent() {
240 return indent;
241 }
242
243 /***
244 * Sets the string used for pretty print indents
245 * @param indent use this <code>string</code> for indents
246 */
247 public void setIndent(String indent) {
248 this.indent = indent;
249 }
250
251 /***
252 * <p> Set the log implementation used. </p>
253 *
254 * @return a <code>org.apache.commons.logging.Log</code> level constant
255 */
256 public Log getLog() {
257 return log;
258 }
259
260 /***
261 * <p> Set the log implementation used. </p>
262 *
263 * @param log <code>Log</code> implementation to use
264 */
265 public void setLog( Log log ) {
266 this.log = log;
267 }
268
269 /***
270 * Gets the encoding strategy for mixed content.
271 * This is used to process body content
272 * before it is written to the textual output.
273 * @return the <code>MixedContentEncodingStrategy</code>, not null
274 * @since 0.5
275 */
276 public MixedContentEncodingStrategy getMixedContentEncodingStrategy() {
277 return mixedContentEncodingStrategy;
278 }
279
280 /***
281 * Sets the encoding strategy for mixed content.
282 * This is used to process body content
283 * before it is written to the textual output.
284 * @param strategy the <code>MixedContentEncodingStrategy</code>
285 * used to process body content, not null
286 * @since 0.5
287 */
288 public void setMixedContentEncodingStrategy(MixedContentEncodingStrategy strategy) {
289 mixedContentEncodingStrategy = strategy;
290 }
291
292 /***
293 * Should an end tag be added for each empty element?
294 * When this property is false then empty elements will
295 * be written as <code><<em>element-name</em>/gt;</code>.
296 * When this property is true then empty elements will
297 * be written as <code><<em>element-name</em>gt;
298 * </<em>element-name</em>gt;</code>.
299 * @return true if an end tag should be added
300 */
301 public boolean isEndTagForEmptyElement() {
302 return addEndTagForEmptyElement;
303 }
304
305 /***
306 * Sets when an an end tag be added for each empty element?
307 * When this property is false then empty elements will
308 * be written as <code><<em>element-name</em>/gt;</code>.
309 * When this property is true then empty elements will
310 * be written as <code><<em>element-name</em>gt;
311 * </<em>element-name</em>gt;</code>.
312 * @param addEndTagForEmptyElement true if an end tag should be
313 * written for each empty element, false otherwise
314 */
315 public void setEndTagForEmptyElement(boolean addEndTagForEmptyElement) {
316 this.addEndTagForEmptyElement = addEndTagForEmptyElement;
317 }
318
319
320
321
322
323
324
325 /***
326 * Writes the start tag for an element.
327 *
328 * @param uri the element's namespace uri
329 * @param localName the element's local name
330 * @param qualifiedName the element's qualified name
331 * @param attr the element's attributes
332 * @throws IOException if an IO problem occurs during writing
333 * @throws SAXException if an SAX problem occurs during writing
334 * @since 0.5
335 */
336 protected void startElement(
337 WriteContext context,
338 String uri,
339 String localName,
340 String qualifiedName,
341 Attributes attr)
342 throws
343 IOException,
344 SAXException {
345 if ( !closedStartTag ) {
346 writer.write( '>' );
347 printLine();
348 }
349
350 indentLevel++;
351
352 indent();
353 writer.write( '<' );
354 writer.write( qualifiedName );
355
356 for ( int i=0; i< attr.getLength(); i++ ) {
357 writer.write( ' ' );
358 writer.write( attr.getQName(i) );
359 writer.write( "=\"" );
360 writer.write( XMLUtils.escapeAttributeValue( attr.getValue(i) ) );
361 writer.write( '\"' );
362 }
363 closedStartTag = false;
364 currentElementIsEmpty = true;
365 currentElementHasBodyText = false;
366 }
367
368 /***
369 * Writes the end tag for an element
370 *
371 * @param uri the element's namespace uri
372 * @param localName the element's local name
373 * @param qualifiedName the element's qualified name
374 *
375 * @throws IOException if an IO problem occurs during writing
376 * @throws SAXException if an SAX problem occurs during writing
377 * @since 0.5
378 */
379 protected void endElement(
380 WriteContext context,
381 String uri,
382 String localName,
383 String qualifiedName)
384 throws
385 IOException,
386 SAXException {
387 if (
388 !addEndTagForEmptyElement
389 && !closedStartTag
390 && currentElementIsEmpty ) {
391
392 writer.write( "/>" );
393 closedStartTag = true;
394
395 } else {
396 if (!currentElementHasBodyText) {
397 indent();
398 }
399 if (
400 addEndTagForEmptyElement
401 && !closedStartTag ) {
402 writer.write( ">" );
403 closedStartTag = true;
404 }
405 writer.write( "</" );
406 writer.write( qualifiedName );
407 writer.write( '>' );
408
409 }
410
411 indentLevel--;
412 printLine();
413
414 currentElementHasBodyText = false;
415 }
416
417 /***
418 * Write element body text
419 *
420 * @param text write out this body text
421 * @throws IOException when the stream write fails
422 * @since 0.5
423 */
424 protected void bodyText(WriteContext context, String text) throws IOException {
425 if ( text == null ) {
426
427 log.error( "[expressBodyText]Body text is null" );
428
429 } else {
430 if ( !closedStartTag ) {
431 writer.write( '>' );
432 closedStartTag = true;
433 }
434 writer.write(
435 mixedContentEncodingStrategy.encode(
436 text,
437 context.getCurrentDescriptor()) );
438 currentElementIsEmpty = false;
439 currentElementHasBodyText = true;
440 }
441 }
442
443 /*** Writes out an empty line.
444 * Uses current <code>endOfLine</code>.
445 *
446 * @throws IOException when stream write fails
447 */
448 private void printLine() throws IOException {
449 if ( endOfLine != null ) {
450 writer.write( endOfLine );
451 }
452 }
453
454 /***
455 * Writes out <code>indent</code>'s to the current <code>indentLevel</code>
456 *
457 * @throws IOException when stream write fails
458 */
459 private void indent() throws IOException {
460 if ( indent != null ) {
461 for ( int i = 0; i < indentLevel; i++ ) {
462 writer.write( getIndent() );
463 }
464 }
465 }
466
467
468
469
470
471 /*** Writes out an empty line.
472 * Uses current <code>endOfLine</code>.
473 *
474 * @throws IOException when stream write fails
475 * @deprecated 0.5 replaced by new SAX inspired API
476 */
477 protected void writePrintln() throws IOException {
478 if ( endOfLine != null ) {
479 writer.write( endOfLine );
480 }
481 }
482
483 /***
484 * Writes out <code>indent</code>'s to the current <code>indentLevel</code>
485 *
486 * @throws IOException when stream write fails
487 * @deprecated 0.5 replaced by new SAX inspired API
488 */
489 protected void writeIndent() throws IOException {
490 if ( indent != null ) {
491 for ( int i = 0; i < indentLevel; i++ ) {
492 writer.write( getIndent() );
493 }
494 }
495 }
496
497 /***
498 * <p>Escape the <code>toString</code> of the given object.
499 * For use as body text.</p>
500 *
501 * @param value escape <code>value.toString()</code>
502 * @return text with escaped delimiters
503 * @deprecated 0.5 moved into utility class {@link XMLUtils#escapeBodyValue}
504 */
505 protected String escapeBodyValue(Object value) {
506 return XMLUtils.escapeBodyValue(value);
507 }
508
509 /***
510 * <p>Escape the <code>toString</code> of the given object.
511 * For use in an attribute value.</p>
512 *
513 * @param value escape <code>value.toString()</code>
514 * @return text with characters restricted (for use in attributes) escaped
515 *
516 * @deprecated 0.5 moved into utility class {@link XMLUtils#escapeAttributeValue}
517 */
518 protected String escapeAttributeValue(Object value) {
519 return XMLUtils.escapeAttributeValue(value);
520 }
521
522 /***
523 * Express an element tag start using given qualified name
524 *
525 * @param qualifiedName the fully qualified name of the element to write
526 * @throws IOException when stream write fails
527 * @deprecated 0.5 replaced by new SAX inspired API
528 */
529 protected void expressElementStart(String qualifiedName) throws IOException {
530 if ( qualifiedName == null ) {
531
532 log.fatal( "[expressElementStart]Qualified name is null." );
533 throw new RuntimeException( "Qualified name is null." );
534 }
535
536 writePrintln();
537 writeIndent();
538 writer.write( '<' );
539 writer.write( qualifiedName );
540 }
541
542 /***
543 * Write a tag close to the stream
544 *
545 * @throws IOException when stream write fails
546 * @deprecated 0.5 replaced by new SAX inspired API
547 */
548 protected void expressTagClose() throws IOException {
549 writer.write( '>' );
550 }
551
552 /***
553 * Write an element end tag to the stream
554 *
555 * @param qualifiedName the name of the element
556 * @throws IOException when stream write fails
557 * @deprecated 0.5 replaced by new SAX inspired API
558 */
559 protected void expressElementEnd(String qualifiedName) throws IOException {
560 if (qualifiedName == null) {
561
562 log.fatal( "[expressElementEnd]Qualified name is null." );
563 throw new RuntimeException( "Qualified name is null." );
564 }
565
566 writer.write( "</" );
567 writer.write( qualifiedName );
568 writer.write( '>' );
569 }
570
571 /***
572 * Write an empty element end to the stream
573 *
574 * @throws IOException when stream write fails
575 * @deprecated 0.5 replaced by new SAX inspired API
576 */
577 protected void expressElementEnd() throws IOException {
578 writer.write( "/>" );
579 }
580
581 /***
582 * Write element body text
583 *
584 * @param text write out this body text
585 * @throws IOException when the stream write fails
586 * @deprecated 0.5 replaced by new SAX inspired API
587 */
588 protected void expressBodyText(String text) throws IOException {
589 if ( text == null ) {
590
591 log.error( "[expressBodyText]Body text is null" );
592
593 } else {
594 writer.write( XMLUtils.escapeBodyValue(text) );
595 }
596 }
597
598 /***
599 * Writes an attribute to the stream.
600 *
601 * @param qualifiedName fully qualified attribute name
602 * @param value attribute value
603 * @throws IOException when the stream write fails
604 * @deprecated 0.5 replaced by new SAX inspired API
605 */
606 protected void expressAttribute(
607 String qualifiedName,
608 String value)
609 throws
610 IOException{
611 if ( value == null ) {
612
613 log.error( "Null attribute value." );
614 return;
615 }
616
617 if ( qualifiedName == null ) {
618
619 log.error( "Null attribute value." );
620 return;
621 }
622
623 writer.write( ' ' );
624 writer.write( qualifiedName );
625 writer.write( "=\"" );
626 writer.write( XMLUtils.escapeAttributeValue(value) );
627 writer.write( '\"' );
628 }
629
630
631 }