001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     * 
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     * 
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */ 
017    package org.apache.commons.betwixt.io;
018    
019    import java.beans.IntrospectionException;
020    import java.io.BufferedWriter;
021    import java.io.IOException;
022    import java.io.OutputStream;
023    import java.io.OutputStreamWriter;
024    import java.io.UnsupportedEncodingException;
025    import java.io.Writer;
026    
027    import org.apache.commons.betwixt.XMLUtils;
028    import org.apache.commons.betwixt.strategy.MixedContentEncodingStrategy;
029    import org.apache.commons.logging.Log;
030    import org.apache.commons.logging.LogFactory;
031    import org.xml.sax.Attributes;
032    import org.xml.sax.SAXException;
033    
034    /** <p><code>BeanWriter</code> outputs beans as XML to an io stream.</p>
035      *
036      * <p>The output for each bean is an xml fragment
037      * (rather than a well-formed xml-document).
038      * This allows bean representations to be appended to a document 
039      * by writing each in turn to the stream.
040      * So to create a well formed xml document, 
041      * you'll need to write the prolog to the stream first.
042      * If you append more than one bean to the stream, 
043      * then you'll need to add a wrapping root element as well.
044      *
045      * <p> The line ending to be used is set by {@link #setEndOfLine}. 
046      * 
047      * <p> The output can be formatted (with whitespace) for easy reading 
048      * by calling {@link #enablePrettyPrint}. 
049      * The output will be indented. 
050      * The indent string used is set by {@link #setIndent}.
051      *
052      * <p> Bean graphs can sometimes contain cycles. 
053      * Care must be taken when serializing cyclic bean graphs
054      * since this can lead to infinite recursion. 
055      * The approach taken by <code>BeanWriter</code> is to automatically
056      * assign an <code>ID</code> attribute value to beans.
057      * When a cycle is encountered, 
058      * an element is written that has the <code>IDREF</code> attribute set to the 
059      * id assigned earlier.
060      *
061      * <p> The names of the <code>ID</code> and <code>IDREF</code> attributes used 
062      * can be customized by the <code>XMLBeanInfo</code>.
063      * The id's used can also be customized by the user 
064      * via <code>IDGenerator</code> subclasses.
065      * The implementation used can be set by the <code>IdGenerator</code> property.
066      * BeanWriter defaults to using <code>SequentialIDGenerator</code> 
067      * which supplies id values in numeric sequence.
068      * 
069      * <p>If generated <code>ID</code> attribute values are not acceptable in the output,
070      * then this can be disabled by setting the <code>WriteIDs</code> property to false.
071      * If a cyclic reference is encountered in this case then a
072      * <code>CyclicReferenceException</code> will be thrown. 
073      * When the <code>WriteIDs</code> property is set to false,
074      * it is recommended that this exception is caught by the caller.
075      * 
076      * 
077      * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
078      * @author <a href="mailto:martin@mvdb.net">Martin van den Bemt</a>
079      */
080    public class BeanWriter extends AbstractBeanWriter {
081    
082        /**
083         * Gets the default EOL string. 
084         * @return EOL string, not null
085         */
086        private static final String getEOL() {
087            // just wraps call in an exception check for access restricted environments
088            String result = "\n";
089            try {
090                result = System.getProperty( "line.separator", "\n" );
091            } catch (SecurityException se) {
092                Log log = LogFactory.getLog( BeanWriter.class );
093                log.warn("Cannot load line separator property: " + se.getMessage());
094                log.trace("Caused by: ", se);
095            }
096            return result;
097        }
098        
099        
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    }