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 * <p>Should an end tag be added for each empty element?
294 * </p><p>
295 * When this property is false then empty elements will
296 * be written as <code><<em>element-name</em>/gt;</code>.
297 * When this property is true then empty elements will
298 * be written as <code><<em>element-name</em>gt;
299 * </<em>element-name</em>gt;</code>.
300 * </p>
301 * @return true if an end tag should be added
302 */
303 public boolean isEndTagForEmptyElement() {
304 return addEndTagForEmptyElement;
305 }
306
307 /***
308 * Sets when an an end tag be added for each empty element.
309 * When this property is false then empty elements will
310 * be written as <code><<em>element-name</em>/gt;</code>.
311 * When this property is true then empty elements will
312 * be written as <code><<em>element-name</em>gt;
313 * </<em>element-name</em>gt;</code>.
314 * @param addEndTagForEmptyElement true if an end tag should be
315 * written for each empty element, false otherwise
316 */
317 public void setEndTagForEmptyElement(boolean addEndTagForEmptyElement) {
318 this.addEndTagForEmptyElement = addEndTagForEmptyElement;
319 }
320
321
322
323
324
325
326
327 /***
328 * Writes the start tag for an element.
329 *
330 * @param uri the element's namespace uri
331 * @param localName the element's local name
332 * @param qualifiedName the element's qualified name
333 * @param attr the element's attributes
334 * @throws IOException if an IO problem occurs during writing
335 * @throws SAXException if an SAX problem occurs during writing
336 * @since 0.5
337 */
338 protected void startElement(
339 WriteContext context,
340 String uri,
341 String localName,
342 String qualifiedName,
343 Attributes attr)
344 throws
345 IOException,
346 SAXException {
347 if ( !closedStartTag ) {
348 writer.write( '>' );
349 printLine();
350 }
351
352 indentLevel++;
353
354 indent();
355 writer.write( '<' );
356 writer.write( qualifiedName );
357
358 for ( int i=0; i< attr.getLength(); i++ ) {
359 writer.write( ' ' );
360 writer.write( attr.getQName(i) );
361 writer.write( "=\"" );
362 writer.write( XMLUtils.escapeAttributeValue( attr.getValue(i) ) );
363 writer.write( '\"' );
364 }
365 closedStartTag = false;
366 currentElementIsEmpty = true;
367 currentElementHasBodyText = false;
368 }
369
370 /***
371 * Writes the end tag for an element
372 *
373 * @param uri the element's namespace uri
374 * @param localName the element's local name
375 * @param qualifiedName the element's qualified name
376 *
377 * @throws IOException if an IO problem occurs during writing
378 * @throws SAXException if an SAX problem occurs during writing
379 * @since 0.5
380 */
381 protected void endElement(
382 WriteContext context,
383 String uri,
384 String localName,
385 String qualifiedName)
386 throws
387 IOException,
388 SAXException {
389 if (
390 !addEndTagForEmptyElement
391 && !closedStartTag
392 && currentElementIsEmpty ) {
393
394 writer.write( "/>" );
395 closedStartTag = true;
396
397 } else {
398 if (!currentElementHasBodyText) {
399 indent();
400 }
401 if (
402 addEndTagForEmptyElement
403 && !closedStartTag ) {
404 writer.write( ">" );
405 closedStartTag = true;
406 }
407 writer.write( "</" );
408 writer.write( qualifiedName );
409 writer.write( '>' );
410
411 }
412
413 indentLevel--;
414 printLine();
415
416 currentElementHasBodyText = false;
417 }
418
419 /***
420 * Write element body text
421 *
422 * @param text write out this body text
423 * @throws IOException when the stream write fails
424 * @since 0.5
425 */
426 protected void bodyText(WriteContext context, String text) throws IOException {
427 if ( text == null ) {
428
429 log.error( "[expressBodyText]Body text is null" );
430
431 } else {
432 if ( !closedStartTag ) {
433 writer.write( '>' );
434 closedStartTag = true;
435 }
436 writer.write(
437 mixedContentEncodingStrategy.encode(
438 text,
439 context.getCurrentDescriptor()) );
440 currentElementIsEmpty = false;
441 currentElementHasBodyText = true;
442 }
443 }
444
445 /*** Writes out an empty line.
446 * Uses current <code>endOfLine</code>.
447 *
448 * @throws IOException when stream write fails
449 */
450 private void printLine() throws IOException {
451 if ( endOfLine != null ) {
452 writer.write( endOfLine );
453 }
454 }
455
456 /***
457 * Writes out <code>indent</code>'s to the current <code>indentLevel</code>
458 *
459 * @throws IOException when stream write fails
460 */
461 private void indent() throws IOException {
462 if ( indent != null ) {
463 for ( int i = 0; i < indentLevel; i++ ) {
464 writer.write( getIndent() );
465 }
466 }
467 }
468
469
470
471
472
473 /*** Writes out an empty line.
474 * Uses current <code>endOfLine</code>.
475 *
476 * @throws IOException when stream write fails
477 * @deprecated 0.5 replaced by new SAX inspired API
478 */
479 protected void writePrintln() throws IOException {
480 if ( endOfLine != null ) {
481 writer.write( endOfLine );
482 }
483 }
484
485 /***
486 * Writes out <code>indent</code>'s to the current <code>indentLevel</code>
487 *
488 * @throws IOException when stream write fails
489 * @deprecated 0.5 replaced by new SAX inspired API
490 */
491 protected void writeIndent() throws IOException {
492 if ( indent != null ) {
493 for ( int i = 0; i < indentLevel; i++ ) {
494 writer.write( getIndent() );
495 }
496 }
497 }
498
499 /***
500 * <p>Escape the <code>toString</code> of the given object.
501 * For use as body text.</p>
502 *
503 * @param value escape <code>value.toString()</code>
504 * @return text with escaped delimiters
505 * @deprecated 0.5 moved into utility class {@link XMLUtils#escapeBodyValue}
506 */
507 protected String escapeBodyValue(Object value) {
508 return XMLUtils.escapeBodyValue(value);
509 }
510
511 /***
512 * <p>Escape the <code>toString</code> of the given object.
513 * For use in an attribute value.</p>
514 *
515 * @param value escape <code>value.toString()</code>
516 * @return text with characters restricted (for use in attributes) escaped
517 *
518 * @deprecated 0.5 moved into utility class {@link XMLUtils#escapeAttributeValue}
519 */
520 protected String escapeAttributeValue(Object value) {
521 return XMLUtils.escapeAttributeValue(value);
522 }
523
524 /***
525 * Express an element tag start using given qualified name
526 *
527 * @param qualifiedName the fully qualified name of the element to write
528 * @throws IOException when stream write fails
529 * @deprecated 0.5 replaced by new SAX inspired API
530 */
531 protected void expressElementStart(String qualifiedName) throws IOException {
532 if ( qualifiedName == null ) {
533
534 log.fatal( "[expressElementStart]Qualified name is null." );
535 throw new RuntimeException( "Qualified name is null." );
536 }
537
538 writePrintln();
539 writeIndent();
540 writer.write( '<' );
541 writer.write( qualifiedName );
542 }
543
544 /***
545 * Write a tag close to the stream
546 *
547 * @throws IOException when stream write fails
548 * @deprecated 0.5 replaced by new SAX inspired API
549 */
550 protected void expressTagClose() throws IOException {
551 writer.write( '>' );
552 }
553
554 /***
555 * Write an element end tag to the stream
556 *
557 * @param qualifiedName the name of the element
558 * @throws IOException when stream write fails
559 * @deprecated 0.5 replaced by new SAX inspired API
560 */
561 protected void expressElementEnd(String qualifiedName) throws IOException {
562 if (qualifiedName == null) {
563
564 log.fatal( "[expressElementEnd]Qualified name is null." );
565 throw new RuntimeException( "Qualified name is null." );
566 }
567
568 writer.write( "</" );
569 writer.write( qualifiedName );
570 writer.write( '>' );
571 }
572
573 /***
574 * Write an empty element end to the stream
575 *
576 * @throws IOException when stream write fails
577 * @deprecated 0.5 replaced by new SAX inspired API
578 */
579 protected void expressElementEnd() throws IOException {
580 writer.write( "/>" );
581 }
582
583 /***
584 * Write element body text
585 *
586 * @param text write out this body text
587 * @throws IOException when the stream write fails
588 * @deprecated 0.5 replaced by new SAX inspired API
589 */
590 protected void expressBodyText(String text) throws IOException {
591 if ( text == null ) {
592
593 log.error( "[expressBodyText]Body text is null" );
594
595 } else {
596 writer.write( XMLUtils.escapeBodyValue(text) );
597 }
598 }
599
600 /***
601 * Writes an attribute to the stream.
602 *
603 * @param qualifiedName fully qualified attribute name
604 * @param value attribute value
605 * @throws IOException when the stream write fails
606 * @deprecated 0.5 replaced by new SAX inspired API
607 */
608 protected void expressAttribute(
609 String qualifiedName,
610 String value)
611 throws
612 IOException{
613 if ( value == null ) {
614
615 log.error( "Null attribute value." );
616 return;
617 }
618
619 if ( qualifiedName == null ) {
620
621 log.error( "Null attribute value." );
622 return;
623 }
624
625 writer.write( ' ' );
626 writer.write( qualifiedName );
627 writer.write( "=\"" );
628 writer.write( XMLUtils.escapeAttributeValue(value) );
629 writer.write( '\"' );
630 }
631
632
633 }