001/*
002 * Copyright 2001 (C) MetaStuff, Ltd. All Rights Reserved.
003 * 
004 * This software is open source. 
005 * See the bottom of this file for the licence.
006 * 
007 * $Id: SAXWriter.java,v 1.12 2001/09/13 12:51:30 jstrachan Exp $
008 */
009
010package org.dom4j.io;
011
012import java.io.IOException;
013import java.util.HashMap;
014import java.util.Iterator;
015import java.util.List;
016import java.util.Map;
017
018import org.dom4j.Attribute;
019import org.dom4j.Branch;
020import org.dom4j.CDATA;
021import org.dom4j.CharacterData;
022import org.dom4j.Comment;
023import org.dom4j.DocumentType;
024import org.dom4j.Document;
025import org.dom4j.Element;
026import org.dom4j.Entity;
027import org.dom4j.Namespace;
028import org.dom4j.ProcessingInstruction;
029import org.dom4j.Text;
030
031import org.dom4j.tree.NamespaceStack;
032
033import org.xml.sax.Attributes;
034import org.xml.sax.ContentHandler;
035import org.xml.sax.DTDHandler;
036import org.xml.sax.EntityResolver;
037import org.xml.sax.ErrorHandler;
038import org.xml.sax.InputSource;
039import org.xml.sax.Locator;
040import org.xml.sax.SAXException;
041import org.xml.sax.SAXNotRecognizedException;
042import org.xml.sax.SAXNotSupportedException;
043import org.xml.sax.XMLFilter;
044import org.xml.sax.XMLReader;
045import org.xml.sax.ext.LexicalHandler;
046import org.xml.sax.helpers.AttributesImpl;
047import org.xml.sax.helpers.LocatorImpl;
048
049/** <p><code>SAXWriter</code> writes a DOM4J tree to a SAX ContentHandler.</p>
050  *
051  * @author <a href="mailto:james.strachan@metastuff.com">James Strachan</a>
052  * @version $Revision: 1.12 $
053  */
054public class SAXWriter implements XMLReader {
055
056    protected static final String[] LEXICAL_HANDLER_NAMES = {
057        "http://xml.org/sax/properties/lexical-handler",
058        "http://xml.org/sax/handlers/LexicalHandler"
059    };
060    
061    protected static String FEATURE_NAMESPACE_PREFIXES = "http://xml.org/sax/features/namespace-prefixes";
062    protected static String FEATURE_NAMESPACES = "http://xml.org/sax/features/namespaces";
063
064    /** <code>ContentHandler</code> to which SAX events are raised */
065    private ContentHandler contentHandler;
066    
067    /** code>DTDHandler</code> fired when a document has a DTD */
068    private DTDHandler dtdHandler;
069    
070    /** code>EntityResolver</code> fired when a document has a DTD */
071    private EntityResolver entityResolver;
072
073    private ErrorHandler errorHandler;
074    
075    /** code>LexicalHandler</code> fired on Entity and CDATA sections */
076    private LexicalHandler lexicalHandler;
077
078    /** code>AttributesImpl</code> used when generating the Attributes */
079    private AttributesImpl attributes = new AttributesImpl();
080    
081    /** Stores the features */
082    private Map features = new HashMap();
083    
084    /** Stores the properties */
085    private Map properties = new HashMap();
086
087    /** Whether namespace declarations are exported as attributes or not */
088    private boolean declareNamespaceAttributes;
089    
090    
091    public SAXWriter() {
092        properties.put( FEATURE_NAMESPACE_PREFIXES, Boolean.FALSE );
093        properties.put( FEATURE_NAMESPACE_PREFIXES, Boolean.TRUE );
094    }
095
096    public SAXWriter(ContentHandler contentHandler) {
097        this();
098        this.contentHandler = contentHandler;
099    }
100
101    public SAXWriter(
102        ContentHandler contentHandler, 
103        EntityResolver entityResolver, 
104        LexicalHandler lexicalHandler
105    ) {
106        this();
107        this.contentHandler = contentHandler;
108        this.entityResolver = entityResolver;
109        this.lexicalHandler = lexicalHandler;
110    }
111
112    
113    /** Generates SAX events for the given Document and all its content
114      *
115      * @param document is the Document to parse
116      * @throw SAXException if there is a SAX error processing the events
117      */
118    public void write(Document document) throws SAXException {
119        if (document != null) {       
120            checkForNullHandlers();
121            
122            documentLocator(document);
123            startDocument();            
124            entityResolver(document);
125            dtdHandler(document);
126            
127            writeContent( document, new NamespaceStack() );
128            endDocument();
129        }
130    }
131    
132    
133    
134    /** Generates SAX events for the given Element and all its content
135      *
136      * @param element is the Element to parse
137      * @throw SAXException if there is a SAX error processing the events
138      */
139    public void write( Element element ) throws SAXException {
140        write( element, new NamespaceStack() );
141    }
142    
143    /** Generates SAX events for the given text
144      *
145      * @param text is the text to send to the SAX ContentHandler
146      * @throw SAXException if there is a SAX error processing the events
147      */
148    public void write( String text ) throws SAXException {
149        if ( text != null ) {
150            char[] chars = text.toCharArray();
151            contentHandler.characters( chars, 0, chars.length );
152        }
153    }
154    
155    /** Generates SAX events for the given CDATA
156      *
157      * @param cdata is the CDATA to parse
158      * @throw SAXException if there is a SAX error processing the events
159      */
160    public void write( CDATA cdata ) throws SAXException {
161        String text = cdata.getText();
162        if ( lexicalHandler != null ) {
163            lexicalHandler.startCDATA();
164            write( text );
165            lexicalHandler.endCDATA();
166        }
167        else {
168            write( text );
169        }
170    }
171    
172    /** Generates SAX events for the given Comment
173      *
174      * @param comment is the Comment to parse
175      * @throw SAXException if there is a SAX error processing the events
176      */
177    public void write( Comment comment ) throws SAXException {
178        if ( lexicalHandler != null ) {
179            String text = comment.getText();
180            char[] chars = text.toCharArray();
181            lexicalHandler.comment( chars, 0, chars.length );
182        }
183    }
184    
185    /** Generates SAX events for the given Entity
186      *
187              * @param e is the Entity to parse
188      * @throw SAXException if there is a SAX error processing the events
189      */
190    public void write( Entity entity ) throws SAXException {
191        String text = entity.getText();
192        if ( lexicalHandler != null ) {
193            String name = entity.getName();
194            lexicalHandler.startEntity(name);
195            write( text );
196            lexicalHandler.endEntity(name);
197        }
198        else {
199            write( text );
200        }
201    }
202    
203    /** Generates SAX events for the given ProcessingInstruction
204      *
205      * @param pi is the ProcessingInstruction to parse
206      * @throw SAXException if there is a SAX error processing the events
207      */
208    public void write( ProcessingInstruction pi ) throws SAXException {        
209        String target = pi.getTarget();
210        String text = pi.getText();
211        contentHandler.processingInstruction(target, text);
212    }
213    
214
215    
216    /** Should namespace declarations be converted to "xmlns" attributes. This property
217      * defaults to <code>false</code> as per the SAX specification. 
218      * This property is set via the SAX feature "http://xml.org/sax/features/namespace-prefixes"
219      */ 
220    public boolean isDeclareNamespaceAttributes() {
221        return declareNamespaceAttributes;
222    }
223    
224    /** Sets whether namespace declarations should be exported as "xmlns" attributes or not.
225      * This property is set from the SAX feature "http://xml.org/sax/features/namespace-prefixes"
226      */ 
227    public void setDeclareNamespaceAttributes(boolean declareNamespaceAttributes) {
228        this.declareNamespaceAttributes = declareNamespaceAttributes;
229    }
230    
231
232    
233    // XMLReader methods
234    //-------------------------------------------------------------------------                
235
236    /** @return the <code>ContentHandler</code> called when SAX events 
237      * are raised
238      */
239    public ContentHandler getContentHandler() {
240        return contentHandler;
241    }
242
243    /** Sets the <code>ContentHandler</code> called when SAX events 
244      * are raised
245      *
246      * @param contentHandler is the <code>ContentHandler</code> called when SAX events 
247      * are raised
248      */
249    public void setContentHandler(ContentHandler contentHandler) {
250        this.contentHandler = contentHandler;
251    }
252
253
254    /** @return the <code>DTDHandler</code> 
255      */
256    public DTDHandler getDTDHandler() {
257        return dtdHandler;
258    }
259
260    /** Sets the <code>DTDHandler</code>.
261      */
262    public void setDTDHandler(DTDHandler dtdHandler) {
263        this.dtdHandler = dtdHandler;
264    }
265
266    /** @return the <code>ErrorHandler</code> 
267      */
268    public ErrorHandler getErrorHandler() {
269        return errorHandler;
270    }
271
272    /** Sets the <code>ErrorHandler</code>.
273      */
274    public void setErrorHandler(ErrorHandler errorHandler) {
275        this.errorHandler = errorHandler;
276    }
277
278    /** @return the <code>EntityResolver</code> used when a Document contains 
279      * a DTD
280      */
281    public EntityResolver getEntityResolver() {
282        return entityResolver;
283    }
284
285    /** Sets the <code>EntityResolver</code> .
286      *
287      * @param entityResolver is the <code>EntityResolver</code> 
288      */
289    public void setEntityResolver(EntityResolver entityResolver) {
290        this.entityResolver = entityResolver;
291    }
292
293    /** @return the <code>LexicalHandler</code> used when a Document contains 
294      * a DTD
295      */
296    public LexicalHandler getLexicalHandler() {
297        return lexicalHandler;
298    }
299
300    /** Sets the <code>LexicalHandler</code> .
301      *
302      * @param entityResolver is the <code>LexicalHandler</code> 
303      */
304    public void setLexicalHandler(LexicalHandler lexicalHandler) {
305        this.lexicalHandler = lexicalHandler;
306    }
307
308    
309    /** Sets the <code>XMLReader</code> used to write SAX events to
310      * 
311      * @param xmlReader is the <code>XMLReader</code> 
312      */
313    public void setXMLReader(XMLReader xmlReader) {
314        setContentHandler( xmlReader.getContentHandler() );
315        setDTDHandler( xmlReader.getDTDHandler() );
316        setEntityResolver( xmlReader.getEntityResolver() );
317        setErrorHandler( xmlReader.getErrorHandler() );
318    }
319    
320    /** Looks up the value of a feature.
321      */
322    public boolean getFeature(String name) 
323            throws SAXNotRecognizedException, SAXNotSupportedException {
324        Boolean answer = (Boolean) features.get(name);
325        return answer != null && answer.booleanValue();
326    }
327
328    /** This implementation does actually use any features but just
329      * stores them for later retrieval
330      */
331    public void setFeature(String name, boolean value) throws SAXNotRecognizedException, SAXNotSupportedException {
332        if ( FEATURE_NAMESPACE_PREFIXES.equals( name ) ) {
333            setDeclareNamespaceAttributes( value );
334        }
335        else if ( FEATURE_NAMESPACE_PREFIXES.equals( name ) ) {
336            if ( ! value ) {
337                throw new SAXNotSupportedException(name + ". namespace feature is always supported in dom4j." );
338            }
339        }
340        features.put(name, (value) ? Boolean.TRUE : Boolean.FALSE );        
341    }
342
343    /** Sets the given SAX property
344      */    
345    public void setProperty(String name, Object value) throws SAXNotRecognizedException, SAXNotSupportedException {
346        for (int i = 0; i < LEXICAL_HANDLER_NAMES.length; i++) {
347            if (LEXICAL_HANDLER_NAMES[i].equals(name)) {
348                setLexicalHandler((LexicalHandler) value);
349                return;
350            }
351        }
352        properties.put(name, value);
353    }
354
355    /** Gets the given SAX property
356      */    
357    public Object getProperty(String name) throws SAXNotRecognizedException, SAXNotSupportedException {
358        for (int i = 0; i < LEXICAL_HANDLER_NAMES.length; i++) {
359            if (LEXICAL_HANDLER_NAMES[i].equals(name)) {
360                return getLexicalHandler();
361            }
362        }
363        return properties.get(name);
364    }
365
366    
367
368    
369    /** This method is not supported. 
370      */
371    public void parse(String systemId) throws SAXNotSupportedException {
372        throw new SAXNotSupportedException(
373            "This XMLReader can only accept <dom4j> InputSource objects"
374        );
375    }
376
377    
378    /** Parses an XML document. 
379      * This method can only accept DocumentInputSource inputs
380      * otherwise a {@link SAXNotSupportedException} exception is thrown.
381      *
382      * @throws SAXNotSupportedException 
383      *      if the input source is not wrapping a dom4j document
384      */
385    public void parse(InputSource input) throws SAXException {
386        if (input instanceof DocumentInputSource) {
387            DocumentInputSource documentInput = (DocumentInputSource) input;
388            Document document = documentInput.getDocument();
389            write( document );
390        }
391        else {
392            throw new SAXNotSupportedException(
393                "This XMLReader can only accept <dom4j> InputSource objects"
394            );
395        }
396    }
397
398    
399    
400    // Implementation methods    
401    //-------------------------------------------------------------------------                
402    
403    protected void writeContent( Branch branch, NamespaceStack namespaceStack ) throws SAXException {
404        for ( Iterator iter = branch.nodeIterator(); iter.hasNext(); ) {
405            Object object = iter.next();
406            if ( object instanceof Element ) {
407                write( (Element) object, namespaceStack );
408            }
409            else if ( object instanceof CharacterData ) { 
410                if ( object instanceof Text ) {
411                    Text text = (Text) object;
412                    write( text.getText() );
413                }
414                else if ( object instanceof CDATA ) {
415                    write( (CDATA) object );
416                }
417                else if ( object instanceof Comment ) {
418                    write( (Comment) object );
419                }
420                else {
421                    throw new SAXException( "Invalid Node in DOM4J content: " + object );
422                }
423            }
424            else if ( object instanceof String ) { 
425                write( (String) object );
426            }
427            else if ( object instanceof Entity ) { 
428                write( (Entity) object );
429            }
430            else if ( object instanceof ProcessingInstruction ) { 
431                write( (ProcessingInstruction) object );
432            }
433            else if ( object instanceof Namespace ) { 
434                // namespaceStack are written via write of element
435            }
436            else {
437                throw new SAXException( "Invalid Node in DOM4J content: " + object );
438            }
439        }
440    }
441    
442    /** The {@link Locator} is only really useful when parsing a textual
443      * document as its main purpose is to identify the line and column number.
444      * Since we are processing an in memory tree which will probably have
445      * its line number information removed, we'll just use -1 for the line
446      * and column numbers.
447      */
448    protected void documentLocator(Document document) throws SAXException {
449        LocatorImpl locator = new LocatorImpl();
450        
451        String publicID = null;
452        String systemID = null;
453        DocumentType docType = document.getDocType();
454        if (docType != null) {
455            publicID = docType.getPublicID();
456            systemID = docType.getSystemID();
457        }
458        locator.setPublicId(publicID);
459        locator.setSystemId(systemID);
460        locator.setLineNumber(-1);
461        locator.setColumnNumber(-1);
462        
463        contentHandler.setDocumentLocator( locator );
464    }
465
466    protected void entityResolver(Document document) throws SAXException {
467        if (entityResolver != null) {
468            DocumentType docType = document.getDocType();
469            if (docType != null) {
470                String publicID = docType.getPublicID();
471                String systemID = docType.getSystemID();
472                
473                if ( publicID != null || systemID != null ) {
474                    try {
475                        entityResolver.resolveEntity( publicID, systemID );
476                    }
477                    catch (IOException e) {
478                        throw new SAXException( 
479                            "Could not resolve entity publicID: " 
480                            + publicID + " systemID: " + systemID, e 
481                        );
482                    }
483                }
484            }
485        }
486    }
487
488    
489    /** We do not yet support DTD or XML Schemas so this method does nothing
490      * right now.
491      */
492    protected void dtdHandler(Document document) throws SAXException {
493    }
494
495    protected void startDocument() throws SAXException {
496        contentHandler.startDocument();
497    }
498
499    protected void endDocument() throws SAXException {
500        contentHandler.endDocument();
501    }
502    
503    protected void write( Element element, NamespaceStack namespaceStack ) throws SAXException {
504        int stackSize = namespaceStack.size();
505        AttributesImpl namespaceAttributes = startPrefixMapping(element, namespaceStack);
506        startElement(element, namespaceAttributes);
507        writeContent(element, namespaceStack);
508        endElement(element);
509        endPrefixMapping(namespaceStack, stackSize);
510    }
511    
512    /** Fires a SAX startPrefixMapping event for all the namespaceStack
513      * which have just come into scope
514      */
515    protected AttributesImpl startPrefixMapping( Element element, NamespaceStack namespaceStack ) throws SAXException {
516        AttributesImpl namespaceAttributes = null;
517        List declaredNamespaces = element.declaredNamespaces();
518        for ( int i = 0, size = declaredNamespaces.size(); i < size ; i++ ) {
519            Namespace namespace = (Namespace) declaredNamespaces.get(i);
520            if ( ! isIgnoreableNamespace( namespace, namespaceStack ) ) {
521                namespaceStack.push( namespace );
522                contentHandler.startPrefixMapping(
523                    namespace.getPrefix(), namespace.getURI()
524                );
525                namespaceAttributes = addNamespaceAttribute( namespaceAttributes, namespace );
526            }
527        }
528        return namespaceAttributes;
529    }
530    
531    /** Fires a SAX endPrefixMapping event for all the namespaceStack which 
532      * have gone out of scope
533      */
534    protected void endPrefixMapping( NamespaceStack namespaceStack, int stackSize ) throws SAXException {                       
535        while ( namespaceStack.size() > stackSize ) {
536            Namespace namespace = namespaceStack.pop();
537            if ( namespace != null ) {
538                contentHandler.endPrefixMapping( namespace.getPrefix() );            
539            }
540        }
541    }
542    
543    
544    protected void startElement( Element element, AttributesImpl namespaceAttributes ) throws SAXException {                       
545        contentHandler.startElement( 
546            element.getNamespaceURI(), 
547            element.getName(), 
548            element.getQualifiedName(), 
549            createAttributes( element, namespaceAttributes )
550        );
551    }
552
553    protected void endElement( Element element ) throws SAXException {        
554        contentHandler.endElement( 
555            element.getNamespaceURI(), 
556            element.getName(), 
557            element.getQualifiedName()
558        );
559    }
560
561    protected Attributes createAttributes( Element element, Attributes namespaceAttributes ) throws SAXException {
562        attributes.clear();
563        if ( namespaceAttributes != null ) {
564            attributes.setAttributes( namespaceAttributes );
565        }
566        
567        for ( Iterator iter = element.attributeIterator(); iter.hasNext(); ) {
568            Attribute attribute = (Attribute) iter.next();
569            attributes.addAttribute( 
570                attribute.getNamespaceURI(), 
571                attribute.getName(), 
572                attribute.getQualifiedName(), 
573                "CDATA",
574                attribute.getValue()
575            );
576        }
577        return attributes;
578    }
579    
580    /** If isDelcareNamespaceAttributes() is enabled then this method will add the
581      * given namespace declaration to the supplied attributes object, creating one if
582      * it does not exist.
583      */
584    protected AttributesImpl addNamespaceAttribute( AttributesImpl namespaceAttributes, Namespace namespace ) {
585        if ( declareNamespaceAttributes ) {
586            if ( namespaceAttributes == null ) {
587                namespaceAttributes = new AttributesImpl();
588            }
589            String prefix = namespace.getPrefix();
590            String qualifiedName = "xmlns";
591            if ( prefix != null && prefix.length() > 0 ) {
592                qualifiedName = "xmlns:" + prefix;
593            }
594            String uri = "";
595            String localName = prefix;
596            String type = "CDATA";
597            String value = namespace.getURI();
598            
599            namespaceAttributes.addAttribute( uri, localName, qualifiedName, type, value );
600        }
601        return namespaceAttributes;
602    }
603
604    
605    /** @return true if the given namespace is an ignorable namespace 
606      * (such as Namespace.NO_NAMESPACE or Namespace.XML_NAMESPACE) or if the
607      * namespace has already been declared in the current scope
608      */
609    protected boolean isIgnoreableNamespace( Namespace namespace, NamespaceStack namespaceStack ) {
610        if ( namespace.equals( Namespace.NO_NAMESPACE ) || namespace.equals( Namespace.XML_NAMESPACE ) ) {
611            return true;
612        }
613        String uri = namespace.getURI();
614        if ( uri == null || uri.length() <= 0 ) {
615            return true;
616        }
617        return namespaceStack.contains( namespace );
618    }
619
620    /** Ensures non-null content handlers?
621      */
622    protected void checkForNullHandlers() {
623    }
624
625}
626
627
628
629
630/*
631 * Redistribution and use of this software and associated documentation
632 * ("Software"), with or without modification, are permitted provided
633 * that the following conditions are met:
634 *
635 * 1. Redistributions of source code must retain copyright
636 *    statements and notices.  Redistributions must also contain a
637 *    copy of this document.
638 *
639 * 2. Redistributions in binary form must reproduce the
640 *    above copyright notice, this list of conditions and the
641 *    following disclaimer in the documentation and/or other
642 *    materials provided with the distribution.
643 *
644 * 3. The name "DOM4J" must not be used to endorse or promote
645 *    products derived from this Software without prior written
646 *    permission of MetaStuff, Ltd.  For written permission,
647 *    please contact dom4j-info@metastuff.com.
648 *
649 * 4. Products derived from this Software may not be called "DOM4J"
650 *    nor may "DOM4J" appear in their names without prior written
651 *    permission of MetaStuff, Ltd. DOM4J is a registered
652 *    trademark of MetaStuff, Ltd.
653 *
654 * 5. Due credit should be given to the DOM4J Project
655 *    (http://dom4j.org/).
656 *
657 * THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS
658 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
659 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
660 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
661 * METASTUFF, LTD. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
662 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
663 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
664 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
665 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
666 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
667 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
668 * OF THE POSSIBILITY OF SUCH DAMAGE.
669 *
670 * Copyright 2001 (C) MetaStuff, Ltd. All Rights Reserved.
671 *
672 * $Id: SAXWriter.java,v 1.12 2001/09/13 12:51:30 jstrachan Exp $
673 */