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