View Javadoc

1   /*
2    * Copyright 2001-2004 The Apache Software Foundation.
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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 "&lt;xml version='1.0' encoding='UTF-8' ?&gt;
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>&lt;<em>element-name</em>/gt;</code>.
297      * When this property is true then empty elements will
298      * be written as <code>&lt;<em>element-name</em>gt;
299      * &lt;/<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>&lt;<em>element-name</em>/gt;</code>.
311      * When this property is true then empty elements will
312      * be written as <code>&lt;<em>element-name</em>gt;
313      * &lt;/<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     // New API
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             // XXX This is probably a programming error
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     // OLD API (DEPRECATED)
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             // XXX this indicates a programming error
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             // XXX this indicates a programming error
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             // XXX This is probably a programming error
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             // XXX probably a programming error
615             log.error( "Null attribute value." );
616             return;
617         }
618         
619         if ( qualifiedName == null ) {
620             // XXX probably a programming error
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 }