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 * @version $Revision: 1.24.2.1 $
79 */
80 public class BeanWriter extends AbstractBeanWriter {
81
82 /*** Where the output goes */
83 private Writer writer;
84 /*** text used for end of lines. Defaults to <code>\n</code>*/
85 private static final String EOL = "\n";
86 /*** text used for end of lines. Defaults to <code>\n</code>*/
87 private String endOfLine = EOL;
88 /*** indentation text */
89 private String indent;
90
91 /*** should we flush after writing bean */
92 private boolean autoFlush;
93 /*** Log used for logging (Doh!) */
94 private Log log = LogFactory.getLog( BeanWriter.class );
95 /*** Has any content (excluding attributes) been written to the current element */
96 private boolean currentElementIsEmpty = false;
97 /*** Has the current element written any body text */
98 private boolean currentElementHasBodyText = false;
99 /*** Has the last start tag been closed */
100 private boolean closedStartTag = true;
101 /*** Current level of indentation (starts at 1 with the first element) */
102 private int indentLevel;
103 /*** USed to determine how body content should be encoded before being output*/
104 private MixedContentEncodingStrategy mixedContentEncodingStrategy
105 = MixedContentEncodingStrategy.DEFAULT;
106
107 /***
108 * <p> Constructor uses <code>System.out</code> for output.</p>
109 */
110 public BeanWriter() {
111 this( System.out );
112 }
113
114 /***
115 * <p> Constuctor uses given <code>OutputStream</code> for output.</p>
116 *
117 * @param out write out representations to this stream
118 */
119 public BeanWriter(OutputStream out) {
120 this.writer = new BufferedWriter( new OutputStreamWriter( out ) );
121 this.autoFlush = true;
122 }
123
124 /***
125 * <p>Constuctor uses given <code>OutputStream</code> for output
126 * and allows encoding to be set.</p>
127 *
128 * @param out write out representations to this stream
129 * @param enc the name of the encoding to be used. This should be compatible
130 * with the encoding types described in <code>java.io</code>
131 * @throws UnsupportedEncodingException if the given encoding is not supported
132 */
133 public BeanWriter(OutputStream out, String enc) throws UnsupportedEncodingException {
134 this.writer = new BufferedWriter( new OutputStreamWriter( out, enc ) );
135 this.autoFlush = true;
136 }
137
138 /***
139 * <p> Constructor sets writer used for output.</p>
140 *
141 * @param writer write out representations to this writer
142 */
143 public BeanWriter(Writer writer) {
144 this.writer = writer;
145 }
146
147 /***
148 * A helper method that allows you to write the XML Declaration.
149 * This should only be called once before you output any beans.
150 *
151 * @param xmlDeclaration is the XML declaration string typically of
152 * the form "<xml version='1.0' encoding='UTF-8' ?>
153 *
154 * @throws IOException when declaration cannot be written
155 */
156 public void writeXmlDeclaration(String xmlDeclaration) throws IOException {
157 writer.write( xmlDeclaration );
158 printLine();
159 }
160
161 /***
162 * Allows output to be flushed on the underlying output stream
163 *
164 * @throws IOException when the flush cannot be completed
165 */
166 public void flush() throws IOException {
167 writer.flush();
168 }
169
170 /***
171 * Closes the underlying output stream
172 *
173 * @throws IOException when writer cannot be closed
174 */
175 public void close() throws IOException {
176 writer.close();
177 }
178
179 /***
180 * Write the given object to the stream (and then flush).
181 *
182 * @param bean write this <code>Object</code> to the stream
183 * @throws IOException if an IO problem causes failure
184 * @throws SAXException if a SAX problem causes failure
185 * @throws IntrospectionException if bean cannot be introspected
186 */
187 public void write(Object bean) throws IOException, SAXException, IntrospectionException {
188
189 super.write(bean);
190
191 if ( autoFlush ) {
192 writer.flush();
193 }
194 }
195
196
197 /***
198 * <p> Switch on formatted output.
199 * This sets the end of line and the indent.
200 * The default is adding 2 spaces and a newline
201 */
202 public void enablePrettyPrint() {
203 endOfLine = EOL;
204 indent = " ";
205 }
206
207 /***
208 * Gets the string used to mark end of lines.
209 *
210 * @return the string used for end of lines
211 */
212 public String getEndOfLine() {
213 return endOfLine;
214 }
215
216 /***
217 * Sets the string used for end of lines
218 * Produces a warning the specified value contains an invalid whitespace character
219 *
220 * @param endOfLine the <code>String</code to use
221 */
222 public void setEndOfLine(String endOfLine) {
223 this.endOfLine = endOfLine;
224 for (int i = 0; i < endOfLine.length(); i++) {
225 if (!Character.isWhitespace(endOfLine.charAt(i))) {
226 log.warn("Invalid EndOfLine character(s)");
227 break;
228 }
229 }
230
231 }
232
233 /***
234 * Gets the indent string
235 *
236 * @return the string used for indentation
237 */
238 public String getIndent() {
239 return indent;
240 }
241
242 /***
243 * Sets the string used for pretty print indents
244 * @param indent use this <code>string</code> for indents
245 */
246 public void setIndent(String indent) {
247 this.indent = indent;
248 }
249
250 /***
251 * <p> Set the log implementation used. </p>
252 *
253 * @return a <code>org.apache.commons.logging.Log</code> level constant
254 */
255 public Log getLog() {
256 return log;
257 }
258
259 /***
260 * <p> Set the log implementation used. </p>
261 *
262 * @param log <code>Log</code> implementation to use
263 */
264 public void setLog( Log log ) {
265 this.log = log;
266 }
267
268 /***
269 * Gets the encoding strategy for mixed content.
270 * This is used to process body content
271 * before it is written to the textual output.
272 * @return the <code>MixedContentEncodingStrategy</code>, not null
273 * @since 0.5
274 */
275 public MixedContentEncodingStrategy getMixedContentEncodingStrategy() {
276 return mixedContentEncodingStrategy;
277 }
278
279 /***
280 * Sets the encoding strategy for mixed content.
281 * This is used to process body content
282 * before it is written to the textual output.
283 * @param strategy the <code>MixedContentEncodingStrategy</code>
284 * used to process body content, not null
285 * @since 0.5
286 */
287 public void setMixedContentEncodingStrategy(MixedContentEncodingStrategy strategy) {
288 mixedContentEncodingStrategy = strategy;
289 }
290
291
292
293
294
295
296 /***
297 * Writes the start tag for an element.
298 *
299 * @param uri the element's namespace uri
300 * @param localName the element's local name
301 * @param qualifiedName the element's qualified name
302 * @param attr the element's attributes
303 * @throws IOException if an IO problem occurs during writing
304 * @throws SAXException if an SAX problem occurs during writing
305 * @since 0.5
306 */
307 protected void startElement(
308 WriteContext context,
309 String uri,
310 String localName,
311 String qualifiedName,
312 Attributes attr)
313 throws
314 IOException,
315 SAXException {
316 if ( !closedStartTag ) {
317 writer.write( '>' );
318 printLine();
319 }
320
321 indentLevel++;
322
323 indent();
324 writer.write( '<' );
325 writer.write( qualifiedName );
326
327 for ( int i=0; i< attr.getLength(); i++ ) {
328 writer.write( ' ' );
329 writer.write( attr.getQName(i) );
330 writer.write( "=\"" );
331 writer.write( XMLUtils.escapeAttributeValue( attr.getValue(i) ) );
332 writer.write( '\"' );
333 }
334 closedStartTag = false;
335 currentElementIsEmpty = true;
336 currentElementHasBodyText = false;
337 }
338
339 /***
340 * Writes the end tag for an element
341 *
342 * @param uri the element's namespace uri
343 * @param localName the element's local name
344 * @param qualifiedName the element's qualified name
345 *
346 * @throws IOException if an IO problem occurs during writing
347 * @throws SAXException if an SAX problem occurs during writing
348 * @since 0.5
349 */
350 protected void endElement(
351 WriteContext context,
352 String uri,
353 String localName,
354 String qualifiedName)
355 throws
356 IOException,
357 SAXException {
358 if ( ( !closedStartTag ) && currentElementIsEmpty ) {
359
360 writer.write( "/>" );
361 closedStartTag = true;
362
363 } else {
364 if (!currentElementHasBodyText) {
365 indent();
366 }
367 writer.write( "</" );
368 writer.write( qualifiedName );
369 writer.write( '>' );
370
371 }
372
373 indentLevel--;
374 printLine();
375
376 currentElementHasBodyText = false;
377 }
378
379 /***
380 * Write element body text
381 *
382 * @param text write out this body text
383 * @throws IOException when the stream write fails
384 * @since 0.5
385 */
386 protected void bodyText(WriteContext context, String text) throws IOException {
387 if ( text == null ) {
388
389 log.error( "[expressBodyText]Body text is null" );
390
391 } else {
392 if ( !closedStartTag ) {
393 writer.write( '>' );
394 closedStartTag = true;
395 }
396 writer.write(
397 mixedContentEncodingStrategy.encode(
398 text,
399 context.getCurrentDescriptor()) );
400 currentElementIsEmpty = false;
401 currentElementHasBodyText = true;
402 }
403 }
404
405 /*** Writes out an empty line.
406 * Uses current <code>endOfLine</code>.
407 *
408 * @throws IOException when stream write fails
409 */
410 private void printLine() throws IOException {
411 if ( endOfLine != null ) {
412 writer.write( endOfLine );
413 }
414 }
415
416 /***
417 * Writes out <code>indent</code>'s to the current <code>indentLevel</code>
418 *
419 * @throws IOException when stream write fails
420 */
421 private void indent() throws IOException {
422 if ( indent != null ) {
423 for ( int i = 0; i < indentLevel; i++ ) {
424 writer.write( getIndent() );
425 }
426 }
427 }
428
429
430
431
432
433 /*** Writes out an empty line.
434 * Uses current <code>endOfLine</code>.
435 *
436 * @throws IOException when stream write fails
437 * @deprecated 0.5 replaced by new SAX inspired API
438 */
439 protected void writePrintln() throws IOException {
440 if ( endOfLine != null ) {
441 writer.write( endOfLine );
442 }
443 }
444
445 /***
446 * Writes out <code>indent</code>'s to the current <code>indentLevel</code>
447 *
448 * @throws IOException when stream write fails
449 * @deprecated 0.5 replaced by new SAX inspired API
450 */
451 protected void writeIndent() throws IOException {
452 if ( indent != null ) {
453 for ( int i = 0; i < indentLevel; i++ ) {
454 writer.write( getIndent() );
455 }
456 }
457 }
458
459 /***
460 * <p>Escape the <code>toString</code> of the given object.
461 * For use as body text.</p>
462 *
463 * @param value escape <code>value.toString()</code>
464 * @return text with escaped delimiters
465 * @deprecated 0.5 moved into utility class {@link XMLUtils#escapeBodyValue}
466 */
467 protected String escapeBodyValue(Object value) {
468 return XMLUtils.escapeBodyValue(value);
469 }
470
471 /***
472 * <p>Escape the <code>toString</code> of the given object.
473 * For use in an attribute value.</p>
474 *
475 * @param value escape <code>value.toString()</code>
476 * @return text with characters restricted (for use in attributes) escaped
477 *
478 * @deprecated 0.5 moved into utility class {@link XMLUtils#escapeAttributeValue}
479 */
480 protected String escapeAttributeValue(Object value) {
481 return XMLUtils.escapeAttributeValue(value);
482 }
483
484 /***
485 * Express an element tag start using given qualified name
486 *
487 * @param qualifiedName the fully qualified name of the element to write
488 * @throws IOException when stream write fails
489 * @deprecated 0.5 replaced by new SAX inspired API
490 */
491 protected void expressElementStart(String qualifiedName) throws IOException {
492 if ( qualifiedName == null ) {
493
494 log.fatal( "[expressElementStart]Qualified name is null." );
495 throw new RuntimeException( "Qualified name is null." );
496 }
497
498 writePrintln();
499 writeIndent();
500 writer.write( '<' );
501 writer.write( qualifiedName );
502 }
503
504 /***
505 * Write a tag close to the stream
506 *
507 * @throws IOException when stream write fails
508 * @deprecated 0.5 replaced by new SAX inspired API
509 */
510 protected void expressTagClose() throws IOException {
511 writer.write( '>' );
512 }
513
514 /***
515 * Write an element end tag to the stream
516 *
517 * @param qualifiedName the name of the element
518 * @throws IOException when stream write fails
519 * @deprecated 0.5 replaced by new SAX inspired API
520 */
521 protected void expressElementEnd(String qualifiedName) throws IOException {
522 if (qualifiedName == null) {
523
524 log.fatal( "[expressElementEnd]Qualified name is null." );
525 throw new RuntimeException( "Qualified name is null." );
526 }
527
528 writer.write( "</" );
529 writer.write( qualifiedName );
530 writer.write( '>' );
531 }
532
533 /***
534 * Write an empty element end to the stream
535 *
536 * @throws IOException when stream write fails
537 * @deprecated 0.5 replaced by new SAX inspired API
538 */
539 protected void expressElementEnd() throws IOException {
540 writer.write( "/>" );
541 }
542
543 /***
544 * Write element body text
545 *
546 * @param text write out this body text
547 * @throws IOException when the stream write fails
548 * @deprecated 0.5 replaced by new SAX inspired API
549 */
550 protected void expressBodyText(String text) throws IOException {
551 if ( text == null ) {
552
553 log.error( "[expressBodyText]Body text is null" );
554
555 } else {
556 writer.write( XMLUtils.escapeBodyValue(text) );
557 }
558 }
559
560 /***
561 * Writes an attribute to the stream.
562 *
563 * @param qualifiedName fully qualified attribute name
564 * @param value attribute value
565 * @throws IOException when the stream write fails
566 * @deprecated 0.5 replaced by new SAX inspired API
567 */
568 protected void expressAttribute(
569 String qualifiedName,
570 String value)
571 throws
572 IOException{
573 if ( value == null ) {
574
575 log.error( "Null attribute value." );
576 return;
577 }
578
579 if ( qualifiedName == null ) {
580
581 log.error( "Null attribute value." );
582 return;
583 }
584
585 writer.write( ' ' );
586 writer.write( qualifiedName );
587 writer.write( "=\"" );
588 writer.write( XMLUtils.escapeAttributeValue(value) );
589 writer.write( '\"' );
590 }
591
592
593 }