Implementation as a SAX filter

/*--

 Copyright 2001 Elliotte Rusty Harold.
 All rights reserved.

 I haven't yet decided on a license.
 It will be some form of open source.

 THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
 WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 DISCLAIMED.  IN NO EVENT SHALL ELLIOTTE RUSTY HAROLD OR ANY
 OTHER CONTRIBUTORS TO THIS PACKAGE
 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 SUCH DAMAGE.

 */

 package com.macfaq.xml;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
import org.xml.sax.helpers.XMLFilterImpl;
import org.xml.sax.helpers.NamespaceSupport;

import java.net.URL;
import java.net.MalformedURLException;
import java.io.IOException;
import java.io.InputStream;
import java.io.BufferedInputStream;
import java.io.InputStreamReader;
import java.util.Stack;

/**
 * <p>
 *  This is a SAX filter which resolves all XInclude include elements
 *  before passing them on to the client application. Currently this
 *  class has the following known deviation from the XInclude specification:
 * </p>
 *  <ol>
 *   <li>XPointer is not supported.</li>
 *  </ol>
 *
 *  <p>
 *    Furthermore, I would definitely use a new instance of this class
 *    for each document you want to process. I doubt it can be used
 *    successfully on multiple documents. Furthermore, I can virtually
 *    guarantee that this class is not thread safe. You have been
 *    warned.
 *  </p>
 *
 *  <p>
 *    Since this class is not designed to be subclassed, and since
 *    I have not yet considered how that might affect the methods 
 *    herein or what other protected methods might be needed to support 
 *    subclasses, I have declared this class final. I may remove this 
 *    restriction later, though the use-case for subclassing is weak.
 *    This class is designed to have its functionality extended via a
 *    a horizontal chain of filters, not a 
 *    vertical hierarchy of sub and superclasses.
 *  </p>
 *
 *  <p>
 *    To use this class: 
 *  </p>
 *  <ol>
 *   <li>Construct an XIncludeFilter object with a known base URL</li>
 *   <li>Pass the XMLReader object from which the raw document will 
 *       be read to the <code>setParent()</code> method of this object. </li>
 *   <li>Pass your own <code>ContentHandler</code> object to the 
 *       <code>setContentHandler()</code> method of this object. This is the 
 *       object which will receive events from the parsed and included
 *       document.
 *   </li>
 *   <li>Optional: if you wish to receive comments, set your own 
 *       <code>LexicalHandler</code> object as the value of this object's
 *       http://xml.org/sax/properties/lexical-handler property.
 *       Also make sure your <code>LexicalHandler</code> asks this object 
 *       for the status of each comment using <code>insideIncludeElement</code>
 *       before doing anything with the comment. 
 *   </li>
 *   <li>Pass the URL of the document to read to this object's 
 *       <code>parse()</code> method</li>
 *  </ol>
 * 
 * <p> e.g.</p>
 *  <pre><code>XIncludeFilter includer = new XIncludeFilter(base); 
 *  includer.setParent(parser);
 *  includer.setContentHandler(new SAXXIncluder(System.out));
 *  includer.parse(args[i]);</code>
 *  </pre>
 * </p>               
 *
 * @author Elliotte Rusty Harold
 * @version 1.0d1
 */
public final class XIncludeFilter extends XMLFilterImpl {

    public final static String XINCLUDE_NAMESPACE
     = "http://www.w3.org/2001/XInclude";

    // Does not yet handle xml:base; need to add that
    // private URL base;
    private Stack bases = new Stack();

  /**
    * <p>
    * This constructor creates a new <code>XIncludeFilter</code>
    * which can be used to read a document while merging in all
    * XInclude references. 
    * </p>
    *
    * @param base  <code>URL</code> The base URI against which
    *              relative URLs will be resolved. 
    */    
    public XIncludeFilter(URL base) {
        bases.push(base);
    }
    
  /**
    * <p>
    * This constructor creates a new <code>XIncludeFilter</code>
    * which can be used to read a document while merging in all
    * XInclude references. 
    * </p>
    *
    * @param base  <code>String</code> The base URI against which
    *              relative URLs will be resolved. 
    * @throws  <code>MalformedURLException</code> If the string cannot
    *          be converted to a <code>java.net.URL</code> object. 
    */    
    public XIncludeFilter(String base) throws MalformedURLException {
        this(new URL(base));
    }
    
    // necessary to throw away contents of non-empty XInclude elements
    private int level = 0;

  /**
    * <p>
    * This utility method returns true if and only if this reader is 
    * currently inside a non-empty include element. (This is <strong>
    * not</strong> the same as being inside the node set whihc replaces
    * the include element.) This is primarily needed for comments
    * inside include elements. It must be checked by the actual
    * LexicalHandler to see whether a comment is passed or not.
    * </p>
    *
    * @return boolean  
    */
    public boolean insideIncludeElement() {
      
        return level != 0;
      
    }
    
    
    public void startElement(String uri, String localName,
      String qName, Attributes atts) throws SAXException {
    
        if (level == 0) { // We're not inside an xi:include element

            // Adjust bases stakc by pushing either the new
            // value of xml:base or the base of the parent
            String base = atts.getValue(NamespaceSupport.XMLNS, "base");
            URL parentBase = (URL) bases.peek();
            URL currentBase = parentBase;
            if (base != null) {
                try {
                    currentBase = new URL(parentBase, base); 
                }
                catch (MalformedURLException e) {
                    throw new SAXException("Malformed base URL: " 
                     + currentBase, e);
                }
            }
            bases.push(currentBase);
          
            if (uri.equals(XINCLUDE_NAMESPACE) && localName.equals("include")) {
                // include external document
                String href = atts.getValue("href");
                // Verify that there is an href attribute
                if (href==null) { 
                    throw new SAXException("Missing href attribute");
                }
                
                String parse = atts.getValue("parse");
                if (parse == null) parse = "xml";
                
                if (parse.equals("text")) {
                    includeTextDocument(href); 
                }
                else if (parse.equals("xml")) {
                    includeXMLDocument(href); 
                }
                // Do I check this one in DOM and JDOM????
                else {
                    throw new SAXException(
                      "Illegal value for parse attribute: " + parse);
                }
                level++;
            }
            else {
                super.startElement(uri, localName, qName, atts);
            } 
        
        }  
      
    }

    public void endElement (String uri, String localName, String qName)
      throws SAXException {
        
        if (uri.equals(XINCLUDE_NAMESPACE) 
           && localName.equals("include")) {
            level--;
        }
        else if (level == 0) {
            bases.pop();
            super.endElement(uri, localName, qName);
        }
        
    }

    private int depth = 0;
     
    public void startDocument() throws SAXException {
        level = 0;
        if (depth == 0) super.startDocument(); 
        depth++;        
    }
    
    public void endDocument() throws SAXException {
      
        depth--;
        if (depth == 0) super.endDocument();
                
    }
    
    // how do prefix mappings move across documents????
    public void startPrefixMapping(String prefix, String uri)
      throws SAXException {
        if (level == 0) super.startPrefixMapping(prefix, uri);
    }
    
    public void endPrefixMapping(String prefix)
      throws SAXException {
        if (level == 0) super.endPrefixMapping(prefix);        
    }

    public void characters(char[] ch, int start, int length) 
      throws SAXException {
        
        if (level == 0) super.characters(ch, start, length);
    
    }

    public void ignorableWhitespace(char[] ch, int start, int length)
      throws SAXException {
        if (level == 0) super.ignorableWhitespace(ch, start, length);
    }

    public void processingInstruction(String target, String data)
      throws SAXException {
        if (level == 0) super.processingInstruction(target, data);
    }

    public void skippedEntity(String name) throws SAXException {
        if (level == 0) super.skippedEntity(name);
    }

  /**
    * <p>
    * This utility method reads a document at a specified URL
    * and fires off calls to <code>characters()</code>.
    * It's used to include files with <code>parse="text"</code>
    * </p>
    *
    * @param  url          URL of the document that will be read
    * @return void  
    * @throws SAXException if the requested document cannot
                           be downloaded from the specified URL.
    */
    private void includeTextDocument(String url) 
      throws SAXException {

        URL source;
        try {
            URL base = (URL) bases.peek();
            source = new URL(base, url);
        }
        catch (MalformedURLException e) {
            UnavailableResourceException ex =
              new UnavailableResourceException("Unresolvable URL " + url);
            ex.setRootCause(e);
            throw new SAXException("Unresolvable URL " + url, ex);
        }
        
        try {
            InputStream in = new BufferedInputStream(source.openStream());
            // does XInclude give you anything to specify the character set????
            InputStreamReader reader = new InputStreamReader(in, "8859_1");
            char[] c = new char[1024];
            while (true) {
                int charsRead = reader.read(c, 0, 1024);
                if (charsRead == -1) break;
                if (charsRead > 0) this.characters(c, 0, charsRead);
            }
        }
        catch (IOException e) {
            throw new SAXException("Document not found: " 
             + source.toExternalForm(), e);
        }

    }

  /**
    * <p>
    * This utility method reads a document at a specified URL
    * and fires off calls to various ContentHandler methods.
    * It's used to include files with <code>parse="xml"</code>
    * </p>
    *
    * @param  url          URL of the document that will be read
    * @return void  
    * @throws SAXException if the requested document cannot
                           be downloaded from the specified URL.
    */
    private void includeXMLDocument(String url) 
      throws SAXException {

        URL source;
        try {
            URL base = (URL) bases.peek();
            source = new URL(base, url);
        }
        catch (MalformedURLException e) {
            UnavailableResourceException ex =
              new UnavailableResourceException("Unresolvable URL " + url);
            ex.setRootCause(e);
            throw new SAXException("Unresolvable URL " + url, ex);
        }
        
        try {
            // make this more robust
            XMLReader parser; 
            try {
                parser = XMLReaderFactory.createXMLReader();
            } 
            catch (SAXException e) {
                try {
                    parser = XMLReaderFactory.createXMLReader(
                      "org.apache.xerces.parsers.SAXParser"
                    );
                }
                catch (SAXException e2) {
                    System.err.println("Could not find an XML parser");
                    return;
                }
            }
            parser.setContentHandler(this);
            // save old level and base
            int previousLevel = level;
            this.level = 0;
            if (bases.contains(source)) {
              // need to figure out how to get file and number where
              // bad include occurs????
              Exception e = new CircularIncludeException(
                "Circular XInclude Reference to " + source + " in " 
              );
              throw new SAXException("Circular XInclude Reference", e);
            }
            bases.push(source);
            parser.parse(source.toExternalForm());
            // restore old level and base
            this.level = previousLevel;
            bases.pop();
        }
        catch (IOException e) {
            throw new SAXException("Document not found: " 
             + source.toExternalForm(), e);
        }

    }
        
}

Previous | Next | Top | Cafe con Leche

Copyright 2000, 2001 Elliotte Rusty Harold
elharo@metalab.unc.edu
Last Modified August 21, 2001