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      * 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>&lt;<em>element-name</em>/gt;</code>.
296      * When this property is true then empty elements will
297      * be written as <code>&lt;<em>element-name</em>gt;
298      * &lt;/<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>&lt;<em>element-name</em>/gt;</code>.
309      * When this property is true then empty elements will
310      * be written as <code>&lt;<em>element-name</em>gt;
311      * &lt;/<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     // New API
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             // XXX This is probably a programming error
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     // OLD API (DEPRECATED)
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             // XXX this indicates a programming error
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             // XXX this indicates a programming error
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             // XXX This is probably a programming error
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             // XXX probably a programming error
613             log.error( "Null attribute value." );
614             return;
615         }
616         
617         if ( qualifiedName == null ) {
618             // XXX probably a programming error
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 }