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    * @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 "&lt;xml version='1.0' encoding='UTF-8' ?&gt;
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     // New API
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             // XXX This is probably a programming error
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     // OLD API (DEPRECATED)
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             // XXX this indicates a programming error
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             // XXX this indicates a programming error
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             // XXX This is probably a programming error
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             // XXX probably a programming error
575             log.error( "Null attribute value." );
576             return;
577         }
578         
579         if ( qualifiedName == null ) {
580             // XXX probably a programming error
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 }