View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    * 
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   * 
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */ 
17  package org.apache.commons.betwixt.io;
18  
19  import java.beans.IntrospectionException;
20  import java.io.BufferedWriter;
21  import java.io.IOException;
22  import java.io.OutputStream;
23  import java.io.OutputStreamWriter;
24  import java.io.UnsupportedEncodingException;
25  import java.io.Writer;
26  
27  import org.apache.commons.betwixt.XMLUtils;
28  import org.apache.commons.betwixt.strategy.MixedContentEncodingStrategy;
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  import org.xml.sax.Attributes;
32  import org.xml.sax.SAXException;
33  
34  /** <p><code>BeanWriter</code> outputs beans as XML to an io stream.</p>
35    *
36    * <p>The output for each bean is an xml fragment
37    * (rather than a well-formed xml-document).
38    * This allows bean representations to be appended to a document 
39    * by writing each in turn to the stream.
40    * So to create a well formed xml document, 
41    * you'll need to write the prolog to the stream first.
42    * If you append more than one bean to the stream, 
43    * then you'll need to add a wrapping root element as well.
44    *
45    * <p> The line ending to be used is set by {@link #setEndOfLine}. 
46    * 
47    * <p> The output can be formatted (with whitespace) for easy reading 
48    * by calling {@link #enablePrettyPrint}. 
49    * The output will be indented. 
50    * The indent string used is set by {@link #setIndent}.
51    *
52    * <p> Bean graphs can sometimes contain cycles. 
53    * Care must be taken when serializing cyclic bean graphs
54    * since this can lead to infinite recursion. 
55    * The approach taken by <code>BeanWriter</code> is to automatically
56    * assign an <code>ID</code> attribute value to beans.
57    * When a cycle is encountered, 
58    * an element is written that has the <code>IDREF</code> attribute set to the 
59    * id assigned earlier.
60    *
61    * <p> The names of the <code>ID</code> and <code>IDREF</code> attributes used 
62    * can be customized by the <code>XMLBeanInfo</code>.
63    * The id's used can also be customized by the user 
64    * via <code>IDGenerator</code> subclasses.
65    * The implementation used can be set by the <code>IdGenerator</code> property.
66    * BeanWriter defaults to using <code>SequentialIDGenerator</code> 
67    * which supplies id values in numeric sequence.
68    * 
69    * <p>If generated <code>ID</code> attribute values are not acceptable in the output,
70    * then this can be disabled by setting the <code>WriteIDs</code> property to false.
71    * If a cyclic reference is encountered in this case then a
72    * <code>CyclicReferenceException</code> will be thrown. 
73    * When the <code>WriteIDs</code> property is set to false,
74    * it is recommended that this exception is caught by the caller.
75    * 
76    * 
77    * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
78    * @author <a href="mailto:martin@mvdb.net">Martin van den Bemt</a>
79    */
80  public class BeanWriter extends AbstractBeanWriter {
81  
82      /**
83       * Gets the default EOL string. 
84       * @return EOL string, not null
85       */
86      private static final String getEOL() {
87          // just wraps call in an exception check for access restricted environments
88          String result = "\n";
89          try {
90              result = System.getProperty( "line.separator", "\n" );
91          } catch (SecurityException se) {
92              Log log = LogFactory.getLog( BeanWriter.class );
93              log.warn("Cannot load line separator property: " + se.getMessage());
94              log.trace("Caused by: ", se);
95          }
96          return result;
97      }
98      
99      
100     /** Where the output goes */
101     private Writer writer;    
102     /** text used for end of lines. Defaults to <code>\n</code>*/
103     private static final String EOL = getEOL();
104     /** text used for end of lines. Defaults to <code>\n</code>*/
105     private String endOfLine = EOL;
106     /** Initial level of indentation (starts at 1 with the first element by default) */
107     private int initialIndentLevel = 1;
108     /** indentation text */
109     private String indent;
110 
111     /** should we flush after writing bean */
112     private boolean autoFlush;
113     /** Log used for logging (Doh!) */
114     private Log log = LogFactory.getLog( BeanWriter.class );
115     /** Has any content (excluding attributes) been written to the current element */
116     private boolean currentElementIsEmpty = false;
117     /** Has the current element written any body text */
118     private boolean currentElementHasBodyText = false;
119     /** Has the last start tag been closed */
120     private boolean closedStartTag = true;
121     /** Should an end tag be added for empty elements? */
122     private boolean addEndTagForEmptyElement = false;
123     /** Current level of indentation */
124     private int indentLevel;
125     /** USed to determine how body content should be encoded before being output*/
126     private MixedContentEncodingStrategy mixedContentEncodingStrategy 
127         = MixedContentEncodingStrategy.DEFAULT;
128     
129     /**
130      * <p> Constructor uses <code>System.out</code> for output.</p>
131      */
132     public BeanWriter() {
133         this( System.out );
134     }
135     
136     /**
137      * <p> Constuctor uses given <code>OutputStream</code> for output.</p>
138      *
139      * @param out write out representations to this stream
140      */
141     public BeanWriter(OutputStream out) {
142         this.writer = new BufferedWriter( new OutputStreamWriter( out ) );
143         this.autoFlush = true;
144     }
145 
146     /**
147      * <p>Constuctor uses given <code>OutputStream</code> for output 
148      * and allows encoding to be set.</p>
149      *
150      * @param out write out representations to this stream
151      * @param enc the name of the encoding to be used. This should be compatible
152      * with the encoding types described in <code>java.io</code>
153      * @throws UnsupportedEncodingException if the given encoding is not supported
154      */
155     public BeanWriter(OutputStream out, String enc) throws UnsupportedEncodingException {
156         this.writer = new BufferedWriter( new OutputStreamWriter( out, enc ) );
157         this.autoFlush = true;
158     }
159 
160     /**
161      * <p> Constructor sets writer used for output.</p>
162      *
163      * @param writer write out representations to this writer
164      */
165     public BeanWriter(Writer writer) {
166         this.writer = writer;
167     }
168 
169     /**
170      * A helper method that allows you to write the XML Declaration.
171      * This should only be called once before you output any beans.
172      * 
173      * @param xmlDeclaration is the XML declaration string typically of
174      *  the form "&lt;xml version='1.0' encoding='UTF-8' ?&gt;
175      *
176      * @throws IOException when declaration cannot be written
177      */
178     public void writeXmlDeclaration(String xmlDeclaration) throws IOException {
179         writer.write( xmlDeclaration );
180         printLine();
181     }
182     
183     /**
184      * Allows output to be flushed on the underlying output stream
185      * 
186      * @throws IOException when the flush cannot be completed
187      */
188     public void flush() throws IOException {
189         writer.flush();
190     }
191     
192     /**
193      * Closes the underlying output stream
194      *
195      * @throws IOException when writer cannot be closed
196      */
197     public void close() throws IOException {
198         writer.close();
199     }
200     
201     /**
202      * Write the given object to the stream (and then flush).
203      * 
204      * @param bean write this <code>Object</code> to the stream
205      * @throws IOException if an IO problem causes failure
206      * @throws SAXException if a SAX problem causes failure
207      * @throws IntrospectionException if bean cannot be introspected
208      */
209     public void write(Object bean) throws IOException, SAXException, IntrospectionException  {
210 
211         super.write(bean);
212 
213         if ( autoFlush ) {
214             writer.flush();
215         }
216     }
217     
218  
219     /**
220      * <p> Switch on formatted output.
221      * This sets the end of line and the indent.
222      * The default is adding 2 spaces and a newline
223      */
224     public void enablePrettyPrint() {
225         endOfLine = EOL;
226         indent = "  ";
227     }
228 
229     /** 
230      * Gets the string used to mark end of lines.
231      *
232      * @return the string used for end of lines 
233      */
234     public String getEndOfLine() {
235         return endOfLine;
236     }
237     
238     /** 
239      * Sets the string used for end of lines 
240      * Produces a warning the specified value contains an invalid whitespace character
241      *
242      * @param endOfLine the <code>String</code to use 
243      */
244     public void setEndOfLine(String endOfLine) {
245         this.endOfLine = endOfLine;
246         for (int i = 0; i < endOfLine.length(); i++) {
247             if (!Character.isWhitespace(endOfLine.charAt(i))) {
248                 log.warn("Invalid EndOfLine character(s)");
249                 break;
250             }
251         }
252         
253     }
254 
255     /** 
256      * Gets the initial indent level 
257      *
258      * @return the initial level for indentation 
259      * @since 0.8
260      */
261     public int getInitialIndentLevel() {
262         return initialIndentLevel;
263     }
264     
265     /** 
266      * Sets the initial indent level used for pretty print indents  
267      * @param initialIndentLevel use this <code>int</code> to start with
268      * @since 0.8
269      */
270     public void setInitialIndentLevel(int initialIndentLevel) {
271         this.initialIndentLevel = initialIndentLevel;
272     }
273 
274 
275     /** 
276      * Gets the indent string 
277      *
278      * @return the string used for indentation 
279      */
280     public String getIndent() {
281         return indent;
282     }
283     
284     /** 
285      * Sets the string used for pretty print indents  
286      * @param indent use this <code>string</code> for indents
287      */
288     public void setIndent(String indent) {
289         this.indent = indent;
290     }
291 
292     /**
293      * <p> Set the log implementation used. </p>
294      *
295      * @return a <code>org.apache.commons.logging.Log</code> level constant
296      */ 
297     public Log getLog() {
298         return log;
299     }
300 
301     /**
302      * <p> Set the log implementation used. </p>
303      *
304      * @param log <code>Log</code> implementation to use
305      */ 
306     public void setLog( Log log ) {
307         this.log = log;
308     }
309     
310     /**
311      * Gets the encoding strategy for mixed content.
312      * This is used to process body content 
313      * before it is written to the textual output.
314      * @return the <code>MixedContentEncodingStrategy</code>, not null
315      * @since 0.5
316      */
317     public MixedContentEncodingStrategy getMixedContentEncodingStrategy() {
318         return mixedContentEncodingStrategy;
319     }
320 
321     /**
322      * Sets the encoding strategy for mixed content.
323      * This is used to process body content 
324      * before it is written to the textual output.
325      * @param strategy the <code>MixedContentEncodingStrategy</code>
326      * used to process body content, not null
327      * @since 0.5
328      */
329     public void setMixedContentEncodingStrategy(MixedContentEncodingStrategy strategy) {
330         mixedContentEncodingStrategy = strategy;
331     }
332     
333     /**
334      * <p>Should an end tag be added for each empty element?
335      * </p><p>
336      * When this property is false then empty elements will
337      * be written as <code>&lt;<em>element-name</em>/gt;</code>.
338      * When this property is true then empty elements will
339      * be written as <code>&lt;<em>element-name</em>gt;
340      * &lt;/<em>element-name</em>gt;</code>.
341      * </p>
342      * @return true if an end tag should be added
343      */
344     public boolean isEndTagForEmptyElement() {
345         return addEndTagForEmptyElement;
346     }
347     
348     /**
349      * Sets when an an end tag be added for each empty element.
350      * When this property is false then empty elements will
351      * be written as <code>&lt;<em>element-name</em>/gt;</code>.
352      * When this property is true then empty elements will
353      * be written as <code>&lt;<em>element-name</em>gt;
354      * &lt;/<em>element-name</em>gt;</code>.
355      * @param addEndTagForEmptyElement true if an end tag should be 
356      * written for each empty element, false otherwise
357      */
358     public void setEndTagForEmptyElement(boolean addEndTagForEmptyElement) {
359         this.addEndTagForEmptyElement = addEndTagForEmptyElement;
360     }
361     
362     
363     
364     // New API
365     //------------------------------------------------------------------------------
366 
367     
368     /**
369      * Writes the start 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      * @param attr the element's attributes
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 startElement(
380                                 WriteContext context,
381                                 String uri, 
382                                 String localName, 
383                                 String qualifiedName, 
384                                 Attributes attr)
385                                     throws
386                                         IOException,
387                                         SAXException {
388         if ( !closedStartTag ) {
389             writer.write( '>' );
390             printLine();
391         }
392         
393         indentLevel++;
394         
395         indent();
396         writer.write( '<' );
397         writer.write( qualifiedName );
398         
399         for ( int i=0; i< attr.getLength(); i++ ) {
400             writer.write( ' ' );
401             writer.write( attr.getQName(i) );
402             writer.write( "=\"" );
403             writer.write( XMLUtils.escapeAttributeValue( attr.getValue(i) ) );
404             writer.write( '\"' );
405         }
406         closedStartTag = false;
407         currentElementIsEmpty = true;
408         currentElementHasBodyText = false;
409     }
410     
411     /**
412      * Writes the end tag for an element
413      *
414      * @param uri the element's namespace uri
415      * @param localName the element's local name 
416      * @param qualifiedName the element's qualified name
417      *
418      * @throws IOException if an IO problem occurs during writing 
419      * @throws SAXException if an SAX problem occurs during writing 
420      * @since 0.5
421      */
422     protected void endElement(
423                                 WriteContext context,
424                                 String uri, 
425                                 String localName, 
426                                 String qualifiedName)
427                                     throws
428                                         IOException,
429                                         SAXException {
430         if ( 
431             !addEndTagForEmptyElement
432             && !closedStartTag 
433             && currentElementIsEmpty ) {
434         
435             writer.write( "/>" );
436             closedStartTag = true;
437             
438         } else {
439 
440             if (
441                     addEndTagForEmptyElement
442                     && !closedStartTag ) {
443                  writer.write( ">" );
444                  closedStartTag = true;                 
445             }
446             else if (!currentElementHasBodyText) {
447                 indent();
448             }
449             writer.write( "</" );
450             writer.write( qualifiedName );
451             writer.write( '>' );
452             
453         }
454         
455         indentLevel--;
456         printLine();
457         
458         currentElementHasBodyText = false;
459     }
460 
461     /** 
462      * Write element body text 
463      *
464      * @param text write out this body text
465      * @throws IOException when the stream write fails
466      * @since 0.5
467      */
468     protected void bodyText(WriteContext context, String text) throws IOException {
469         if ( text == null ) {
470             // XXX This is probably a programming error
471             log.error( "[expressBodyText]Body text is null" );
472             
473         } else {
474             if ( !closedStartTag ) {
475                 writer.write( '>' );
476                 closedStartTag = true;
477             }
478             writer.write( 
479                 mixedContentEncodingStrategy.encode(
480                     text, 
481                     context.getCurrentDescriptor()) );
482             currentElementIsEmpty = false;
483             currentElementHasBodyText = true;
484         }
485     }
486     
487     /** Writes out an empty line.
488      * Uses current <code>endOfLine</code>.
489      *
490      * @throws IOException when stream write fails
491      */
492     private void printLine() throws IOException {
493         if ( endOfLine != null ) {
494             writer.write( endOfLine );
495         }
496     }
497     
498     /** 
499      * Writes out <code>indent</code>'s to the current <code>indentLevel</code>
500      *
501      * @throws IOException when stream write fails
502      */
503     private void indent() throws IOException {
504         if ( indent != null ) {
505             for ( int i = 1 - initialIndentLevel; i < indentLevel; i++ ) {
506                 writer.write( getIndent() );
507             }
508         }
509     }
510 
511     // OLD API (DEPRECATED)
512     //----------------------------------------------------------------------------
513 
514             
515     /** Writes out an empty line.
516      * Uses current <code>endOfLine</code>.
517      *
518      * @throws IOException when stream write fails
519      * @deprecated 0.5 replaced by new SAX inspired API
520      */
521     protected void writePrintln() throws IOException {
522         if ( endOfLine != null ) {
523             writer.write( endOfLine );
524         }
525     }
526     
527     /** 
528      * Writes out <code>indent</code>'s to the current <code>indentLevel</code>
529      *
530      * @throws IOException when stream write fails
531      * @deprecated 0.5 replaced by new SAX inspired API
532      */
533     protected void writeIndent() throws IOException {
534         if ( indent != null ) {
535             for ( int i = 0; i < indentLevel; i++ ) {
536                 writer.write( getIndent() );
537             }
538         }
539     }
540     
541     /** 
542      * <p>Escape the <code>toString</code> of the given object.
543      * For use as body text.</p>
544      *
545      * @param value escape <code>value.toString()</code>
546      * @return text with escaped delimiters 
547      * @deprecated 0.5 moved into utility class {@link XMLUtils#escapeBodyValue}
548      */
549     protected String escapeBodyValue(Object value) {
550         return XMLUtils.escapeBodyValue(value);
551     }
552 
553     /** 
554      * <p>Escape the <code>toString</code> of the given object.
555      * For use in an attribute value.</p>
556      *
557      * @param value escape <code>value.toString()</code>
558      * @return text with characters restricted (for use in attributes) escaped
559      *
560      * @deprecated 0.5 moved into utility class {@link XMLUtils#escapeAttributeValue}
561      */
562     protected String escapeAttributeValue(Object value) {
563         return XMLUtils.escapeAttributeValue(value);
564     }  
565 
566     /** 
567      * Express an element tag start using given qualified name 
568      *
569      * @param qualifiedName the fully qualified name of the element to write
570      * @throws IOException when stream write fails
571      * @deprecated 0.5 replaced by new SAX inspired API
572      */
573     protected void expressElementStart(String qualifiedName) throws IOException {
574         if ( qualifiedName == null ) {
575             // XXX this indicates a programming error
576             log.fatal( "[expressElementStart]Qualified name is null." );
577             throw new RuntimeException( "Qualified name is null." );
578         }
579         
580         writePrintln();
581         writeIndent();
582         writer.write( '<' );
583         writer.write( qualifiedName );
584     }
585     
586     /** 
587      * Write a tag close to the stream
588      *
589      * @throws IOException when stream write fails
590      * @deprecated 0.5 replaced by new SAX inspired API
591      */
592     protected void expressTagClose() throws IOException {
593         writer.write( '>' );
594     }
595     
596     /** 
597      * Write an element end tag to the stream
598      *
599      * @param qualifiedName the name of the element
600      * @throws IOException when stream write fails
601      * @deprecated 0.5 replaced by new SAX inspired API
602      */
603     protected void expressElementEnd(String qualifiedName) throws IOException {
604         if (qualifiedName == null) {
605             // XXX this indicates a programming error
606             log.fatal( "[expressElementEnd]Qualified name is null." );
607             throw new RuntimeException( "Qualified name is null." );
608         }
609         
610         writer.write( "</" );
611         writer.write( qualifiedName );
612         writer.write( '>' );
613     }    
614     
615     /**  
616      * Write an empty element end to the stream
617      *
618      * @throws IOException when stream write fails
619      * @deprecated 0.5 replaced by new SAX inspired API
620      */
621     protected void expressElementEnd() throws IOException {
622         writer.write( "/>" );
623     }
624 
625     /** 
626      * Write element body text 
627      *
628      * @param text write out this body text
629      * @throws IOException when the stream write fails
630      * @deprecated 0.5 replaced by new SAX inspired API
631      */
632     protected void expressBodyText(String text) throws IOException {
633         if ( text == null ) {
634             // XXX This is probably a programming error
635             log.error( "[expressBodyText]Body text is null" );
636             
637         } else {
638             writer.write( XMLUtils.escapeBodyValue(text) );
639         }
640     }
641     
642     /** 
643      * Writes an attribute to the stream.
644      *
645      * @param qualifiedName fully qualified attribute name
646      * @param value attribute value
647      * @throws IOException when the stream write fails
648      * @deprecated 0.5 replaced by new SAX inspired API
649      */
650     protected void expressAttribute(
651                                 String qualifiedName, 
652                                 String value) 
653                                     throws
654                                         IOException{
655         if ( value == null ) {
656             // XXX probably a programming error
657             log.error( "Null attribute value." );
658             return;
659         }
660         
661         if ( qualifiedName == null ) {
662             // XXX probably a programming error
663             log.error( "Null attribute value." );
664             return;
665         }
666                 
667         writer.write( ' ' );
668         writer.write( qualifiedName );
669         writer.write( "=\"" );
670         writer.write( XMLUtils.escapeAttributeValue(value) );
671         writer.write( '\"' );
672     }
673 
674 
675 }