// 
// The Waveset SPML General License 
// 
// Version 0.1, April 2003
// Copyright (C) 2003 Waveset Technologies, Inc..
// 6034 West Courtyard Drive, Suite 210, Austin, Texas 78730
// All rights reserved.
// 
// TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
// 
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
// 
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions, and the disclaimers in Sections 6
//    and 7 below.
// 
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the disclaimers in Sections 6
//    and 7 below in the documentation and/or other materials provided
//    with the distribution.
// 
// 3. The end-user documentation included with the redistribution, if
//    any, must include the following acknowledgment:
// 
//    "This product includes software developed by 
//     Waveset Technologies, Inc. (www.waveset.com)."
// 
//    Alternately, this acknowledgment may appear in the software itself, if
//    and wherever such third-party acknowledgments normally appear.
//  
// 4. The names "Waveset" and "Waveset Technologies, Inc." must not be
//    used to endorse or promote products derived from this software
//    without the prior written permission of Waveset. For written
//    permission, please contact www.waveset.com.
//  
// 5. Products derived from this software may not be called "Waveset",
//    nor may "Waveset" appear in their name, without the prior written
//    permission of Waveset.
//  
// 6. NO WARRANTY 
// 
//    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.
// 
// 7.  LIMITATION OF LIABILITY
// 
//    IN NO EVENT SHALL THE WAVESET OR ITS LICENSORS 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.
// 
// End of Terms and Conditions
// ====================================================================
//
// Component Name: org.openspml.util.XmlParser
//
// Author(s): Jeff Larson, Rafael Sanchez
//
// Description:
//
// XML parsing utilities.  It wraps the Xerces parser and provides
// a parser pool.
//
//

package org.openspml.util;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.ArrayList;

import org.xml.sax.*;
import org.w3c.dom.*;

import org.xml.sax.helpers.XMLReaderFactory;
import javax.xml.parsers.*;

/**
 * A wrapper around the Xerces parser that provides a simpler interface, 
 * and maintains a pool of parser objects for better performance.
 * <p>
 * This implements the Xerces ErrorHandler interface so we can intercept
 * parser errors and mutate them.
 */
public class XmlParser implements ErrorHandler, EntityResolver {

    private static final String code_id = "$Id: XmlParser.java,v 1.5 2003/10/23 17:50:06 jlarson Exp $";

    //////////////////////////////////////////////////////////////////////
    //
    // Static Methods
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * Parses a string with or without validation and returns the Document.
     */
    public static Document parse(String xml, boolean validation)
	throws SpmlException {

	Document d = null;

	XmlParser p = getParser(validation);
	try {
	    d = p.parseString(xml);
	}
	finally {
	    p.pool();
	}

	return d;
    }

    //////////////////////////////////////////////////////////////////////
    //
    // Static Fields
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * The global pool of non-validating parsers.
     */
    private static XmlParser _pool;

    /**
     * The global pool of validating parsers.
     */
    private static XmlParser _validatingPool;

    /**
     * Total number of parsers created (to find leaks).
     */
    private static int _parsersCreated;

    /**
     * Total number of parser we've had to abandon due to errors.
     * Should we leave these on a list?
     */
    private static int _parserCorruptions;

    /**
     * Flag controlling whether we save xml strings that resulted
     * in parsing errors to a log file.
     */
    private static boolean _logFailures = true;

    /**
     * Cached location of the application home directory 
     * for entity resolution.
     */
    private static URL _homeUrl;

    /**
     * Flag indicating that we were unable to obtain a valid
     * _homeUrl once, so stop trying.
     */
    private static boolean _bogusHomeUrl;

    /**
     * Maximum number of times to allow a parser to be used before
     * removing it from the pool.
     * Avoids occasional Xerces memory leaks, at least with older
     * versions of the parser.
     */
    private static final int _maxUsages = 10000;

    //////////////////////////////////////////////////////////////////////
    //
    // Fields
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * The underlying DOM parser.
     */
    private DocumentBuilderFactory _factory = null;
    private DocumentBuilder _parser;

    /**
     * The pool chain.
     */
    private XmlParser _next;

    /**
     * Flag indicating whether or not we're in the pool.
     * This allows us to detect attempts to pool the same object
     * twice without having to search the pool.
     */
    private boolean _inPool;

    /**
     * Keep a count of the number of times this has been used.
     */
    private int _usages;

    /**
     * Default parser to be retrieved by the XMLReader interface
     */
    private static final String _defaultSAX2Parser =
       "org.apache.xerces.parsers.SAXParser";

    //////////////////////////////////////////////////////////////////////
    //
    // Parser Pool
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * Obtain a parser from the pool.
     */
    public static XmlParser getParser() {
        return getParser(false);
    }

    public static synchronized XmlParser getParser(boolean validating) {

	XmlParser p = null;
        
        if (validating) {
            if (_pool != null) {
                p = _pool;
                _pool = p.getNext();
            }
        }
        else {
            if (_validatingPool != null) {
                p = _validatingPool;
                _validatingPool = p.getNext();
            }
        }

        if (p == null) {
            try {
                p = new XmlParser(validating);
                _parsersCreated++;
            }
            catch (ParserConfigurationException e) {
            } 
            catch (FactoryConfigurationError e) {
            }
        }

        if (p != null) {
            p.setPooled(false);
            p.incUsages();
        }

	return p;
    }

    /**
     * Return an object to the pool.
     */
    public static synchronized void poolParser(XmlParser p) {

	// guard against attempts to pool this more than once
	if (!p.isPooled()) {

	    // if this parser has been handled too much,
	    // don't return it to the pool, sometimes they go bad

	    if (_maxUsages <= 0 || p.getUsages() < _maxUsages) {
                if (p._parser.isValidating()) {
                    p.setNext(_validatingPool);
                    _validatingPool = p;
                }
                else {
                    p.setNext(_pool);
                    _pool = p;
                }
                p.setPooled(true);
	    }
	}
    }

    /**
     * Retrieve information about the pool.
     * This assumes that the caller is assembling an XML Report
     * document, we get to contribute one or more <section> elements.
     */
    public static void getStatus(StringBuffer b) {

	b.append("<section><title>Xml Parser</title>\n");

	// first count them
	int count = 0;
	for (XmlParser p = _pool ; p != null ; p = p.getNext())
	    count++;

	int vcount = 0;
	for (XmlParser p = _validatingPool ; p != null ; p = p.getNext())
	    vcount++;

	b.append("<p>" + itoa(_parsersCreated) + " parsers created, " +
		 itoa(count) + " in the non-validating pool, " +
                 itoa(vcount) + " in the validating pool.</p>\n");

	b.append("<p>" + itoa(_parserCorruptions) +
		 " parsers corrupted.");

	// then emit some per-parser usage statistics
	if (count > 0) {
	    b.append("<p>Non-validating Usage counts:</p>\n");
	    int i = 0;
	    for (XmlParser p = _pool ; p != null ; p = p.getNext(), i++) {
		b.append("<p>" + itoa(i) + ": " +
			 itoa(p.getUsages()) + "</p>\n");
	    }
	}

	if (vcount > 0) {
	    b.append("<p>Validating Usage counts:</p>\n");
	    int i = 0;
	    for (XmlParser p = _validatingPool ; p != null ; p = p.getNext(), i++) {
		b.append("<p>" + itoa(i) + ": " +
			 itoa(p.getUsages()) + "</p>\n");
	    }
	}

	b.append("</section>\n");
    }

    static public String itoa(int i) {
	return Integer.toString(i);
    }

    /**
     * Dump information about the pool.
     */
    public static void dumpPool() {

	StringBuffer b = new StringBuffer();
	getStatus(b);
	println(b.toString());
    }

    /** You know it you love it. */
    private static void println(Object o) {
	System.out.println(o);
    }

    //////////////////////////////////////////////////////////////////////
    //
    // Constructor
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * Construct a private utility object.
     */
    public XmlParser(boolean validating)
        throws ParserConfigurationException, FactoryConfigurationError
    {
        getDOMParser(this, this, validating);
    }

    private void getDOMParser(ErrorHandler eh, EntityResolver er,
                              boolean validating)
        throws ParserConfigurationException, FactoryConfigurationError
    {
        if (_factory == null) {
            _factory = DocumentBuilderFactory.newInstance();
        }

        _factory.setNamespaceAware(true);

        // The default for validation is 'false' so only set if the
        // request is 'true'
        if (validating) {
            _factory.setValidating(validating);
        }

        _parser = _factory.newDocumentBuilder();

        if (eh != null) {
            _parser.setErrorHandler(eh);
        }

        if (er != null) {
            _parser.setEntityResolver(er);
        }
    }

    private void getDOMParser(ErrorHandler eh, EntityResolver er)
        throws ParserConfigurationException, FactoryConfigurationError
    {
        getDOMParser(eh, er, false);
    }

    /**
     * Return the parser to the pool.
     */
    public void pool() {
	XmlParser.poolParser(this);
    }

    /** Get the next parser in the chain for pool maintenance. */
    private XmlParser getNext() {
	return _next;
    }

    /** Set the next parser in the chain for pool maintenance. */
    private void setNext(XmlParser p) {
	_next = p;
    }

    /** Get the pooled status flag. */
    private boolean isPooled() {
	return _inPool;
    }

    /** Set the pooled status flag. */
    private void setPooled(boolean b) {
	_inPool = b;
    }

    /** Increment the usage count. */
    private void incUsages() {
	_usages++;
    }

    /** Get the usage count. */
    private int getUsages() {
	return _usages;
    }

    //////////////////////////////////////////////////////////////////////
    //
    // Parsing
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * Parses a string and returns the Document.
     * Validation may be on or off depending on how the parser is created.
     */
    public Document parseString(String xml) throws SpmlException {

        Document doc = null;

	// Hack, if validation is off, remove the DOCTYPE statement so
	// we can avoid DTD processing.  Even with validation off,
	// Xerces still does a lot of expensive DTD processing if
	// if sees a DOCTYPE.

	StringReader reader = null;
	if (_parser.isValidating())
	    reader = new StringReader(xml);
	else {
	    int index = xml.indexOf("DOCTYPE");
	    if (index != -1) {
		for ( ; index < xml.length() ; index++) {
		    if (xml.charAt(index) == '>') {
			index++;
			break;
		    }
		}
		if (index < xml.length())
		    xml = xml.substring(index);
	    }

	    // be smarter and try to reuse a StringBuffer?
	    reader = new StringReader(xml);
	}

	InputSource is = new InputSource(reader);
	try {
	    doc = _parser.parse(is);
	}
	catch (SAXParseException ex) {
	    logFailure(xml);
	    String msg = "XML Error " + getLocationString(ex) + ": " +
		ex.getMessage();
	    throw new SpmlException(msg, ex);
	}
	catch (SAXException ex) {
	    // Assume this indicates a corruption within the parser,
	    // create a new one.
            try {
                getDOMParser(this, this);
            } catch (ParserConfigurationException e) {
                _parser = null;
                throw new SpmlException(e);
            } catch (FactoryConfigurationError er) {
                _parser = null;
                throw new SpmlException(er);
            }

            _parserCorruptions++;

	    logFailure(xml);
	    wrapException(ex);
	}
	catch (IOException ex) {
	    // should never get here during string parsing?
	    throw new SpmlException(ex);
	}

        return doc;
    }

    /**
     * Store a string being parsed that resulted in an error to
     * a log file for diagnosis.
     */
    private void logFailure(String xml) {

	if (_logFailures) {
	    try {
		// Util.writeFile("xmlerror.tmp", xml);
	    }
	    catch (Exception e) {
		// ignore
	    }
	}
    }

    //////////////////////////////////////////////////////////////////////
    //
    // EntityResolver methods
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * Determine the application's "home" directory.
     * This will be performed by <code>Util.getWavesetHomeURL()</code>, 
     * and will be cached in the <code>_homeUrl</code> field.
     * <p>
     * Currently, it is assumed that the home directory is identified
     * by the system property "waveset.home".
     * <p>
     * Note that since we're setting a static, we will have thread
     * synchronization issues, but since we're always setting it
     * to the same value, it won't hurt.
     *
     * UPDATE: Irrelevant for SPML at the moment since we never
     * parse with validation on.
     */
    private URL getAppHome() {

	if ( _homeUrl==null && _bogusHomeUrl==false ) 
	{

	    // _homeUrl = Util.getWavesetHomeURL();

	    //
	    // Couldn't determine the home directory.
	    // We're in resolveEntity right now so we can
	    // only throw SAXException or IOException.
	    //
	    // Set a flag so that we don't keep trying to do this.
	    // (Otherwise, null still looks like first time.)
	    //
	    if ( _homeUrl==null )
		_bogusHomeUrl = true;
	}

	return _homeUrl;
    }

    /**
     * Attempt to resolve an entity reference to an absolute file path.
     */
    public InputSource resolveEntity(String pubid, String sysid)
	throws SAXException, java.io.IOException {

	// System id seems to always come in as a fully qualified
	// pathname, even if it was only a leaf file name in the
	// DOCTYPE statement.

	// Sigh, we should be able to accept only a sysid, strip
	// off the directory, and try to locate that file relative
	// to the applicatio home directory.  For now, since we'll always
	// have a pubid, just use that.

	InputSource is = null;

	// ignore sysid, try to resolve pubid relative to a directory
	if ( pubid!=null ) {

	    // First try the "application home" directory.
	    // Should we try any other (sub)directories?
	    URL homeUrl = getAppHome();
	    if ( _bogusHomeUrl==false ) 
	    {
		URL resoUrl = null;

		/*
		try {
		    // todo: read resources out of jars
		    // resoUrl = Util.getWavesetResource(pubid);
		}
		catch(SpmlException ex) {
		    // Util.debug("Unable to resolve '"+pubid+"': "+ex);
		    // Util.debug(Util.stackToString(ex));
		}
		*/

		if ( resoUrl!=null ) {
		    InputStream ris = resoUrl.openStream();
		    if ( ris!=null ) 
			is = new InputSource(ris);
		}
	    }

	    if ( is==null ) {
		// Couldn't find application home, we could probably
		// just die right now, but lets give it one more try
		// and look in the current working directory as defined
		// by the system property "user.dir"

		String cwd = System.getProperty("user.dir");
		if ( cwd!=null ) {
		    String path = cwd + File.separator + pubid;
		    File f = new File(path);
		    if ( f.exists() ) 
			is = new InputSource(new FileReader(f));
		}
	    }

	    // anything else to try?

	    if ( is==null ) {

		// Couldn't resolve pubid, should try to be smart
		// and prune off the sysid file name and resolve
		// it relative to WSHOME too...
		// Just return null, and let the default resolve die.

		// println("Unable to resolve pubid.");
	    }

	}

	return is;
    }

    //////////////////////////////////////////////////////////////////////
    //
    // ErrorHandler methods
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * Parser warning callback handler.
     * <p>
     * What should we do with warnings, collection them, barf to the console?
     * Might want a flag that treats warnings as errors.
     */
    public void warning(SAXParseException ex) {

        System.err.println("[Warning] "+
                           getLocationString(ex)+": "+
                           ex.getMessage());
    }

    /**
     * Parser error callback handler
     * <p>
     * We get here for things like DTD syntax errors, and DTD
     * validation errors.
     */
    public void error(SAXParseException ex) throws SAXParseException {

	throw ex;
    }

    /**
     * Parser fatal error callback handler.
     * <p>
     * Convert this one to a SpmlException so we can capture
     * the full message text in one place.
     * We get here for things like DTD location failure.
     */
    public void fatalError(SAXParseException ex) throws SAXException {

	// don't print anything, just throw it and let parseString
	// convert it to a SpmlException
	throw ex;
    }

    /**
     * Returns a string describing the location of a parser error.
     */
    private String getLocationString(SAXParseException ex) {
        StringBuffer str = new StringBuffer();

        String systemId = ex.getSystemId();
        if (systemId != null) {
            int index = systemId.lastIndexOf('/');
            if (index != -1)
                systemId = systemId.substring(index + 1);
            str.append(systemId);
        }
        str.append(':');
        str.append(ex.getLineNumber());
        str.append(':');
        str.append(ex.getColumnNumber());

        return str.toString();
    }

    //////////////////////////////////////////////////////////////////////
    //
    // SAX Interface
    //
    //////////////////////////////////////////////////////////////////////

    private XMLReader getXMLReader(ErrorHandler eh, EntityResolver er)
        throws SAXException
    {
        XMLReader p = null;

        try {
            p = XMLReaderFactory.createXMLReader();
        } catch (SAXException e) {
            p = XMLReaderFactory.createXMLReader(_defaultSAX2Parser);
        }

        if (eh != null) {
            p.setErrorHandler(eh);
        }

        if (er != null) {
            p.setEntityResolver(er);
        }

        return p;
    }

    /**
     * Kludge, setValidation above only works on the
     * internal DOMParser
     */
    private boolean setValidation(XMLReader p, boolean validation) {

	boolean success = true;

	try {
	    p.setFeature("http://xml.org/sax/features/validation",
			 validation);
	}
	catch (org.xml.sax.SAXNotSupportedException e) {
	    // hmm, shouldn't happen, but try not to throw here
	    println("Unable to set parser validation:");
	    println(e.toString());
	    success = false;
	}
	catch (org.xml.sax.SAXNotRecognizedException e) {
	    // hmm, shouldn't happen, but try not to throw here
	    println("Unable to set parser validation:");
	    println(e.toString());
	    success = false;
	}

	return success;
    }


    /**
     * Parse a uri using SAX.
     * This is a bit of a kludge, it was glued onto what was formerly
     * a DOM centric parser.  The XmlParser class could be refactored
     * into common/sax/dom classes, but I'm not sure that's worth it.
     * <p>
     * Since we assume we don't do this very often, we'll build
     * a new parser every time.
     */
    public void saxParse(String uri, ContentHandler handler)
	throws SpmlException {

	try {
            XMLReader p = getXMLReader(this, this);

            // Under the assumption that sax parsing is used
            // for bulk loads and import, always enable validation
            setValidation(p, true);

            if (handler != null)
                p.setContentHandler(handler);

	    // These are always path names, and parse(uri) doesn't
	    // always seem to understand it?
	    // p.parse(uri);

	    // this works, and is probably preferred
	    InputSource is = new InputSource(new FileReader(uri));
	    p.parse(is);
	}
	catch (SAXException ex) {
	    wrapException(ex);
	}
	catch (IOException ex) {
	    throw new SpmlException(ex);
	}
    }

    /**
     * Check a SAXException we just caught for a wrapped exception.
     */
    private void wrapException(SAXException e) throws SpmlException {

	if (e instanceof SAXParseException) {

	    SAXParseException spe = (SAXParseException)e;
	    String msg = "XML Error" + getLocationString(spe) + ": " +
		spe.getMessage();

	    throw new SpmlException(msg, e);
	}
	else {
	    // if this is wrapping something, return the wrapped exception
	    Exception wrapped = e.getException();

	    if (wrapped == null)
		throw new SpmlException(e);

	    else if (wrapped instanceof SpmlException)
		throw (SpmlException)wrapped;

	    else if (wrapped instanceof RuntimeException) {
		// unwrap these, as they often don't have messages and
		// we need to see the class name
		throw (RuntimeException)wrapped;
	    }
	    else {
		// wrap whatever it is in our wrapper
		throw new SpmlException(wrapped);
	    }
	}
    }

    /**
     * Parse a stream using SAX.
     */
    public void saxParse(InputStream input, ContentHandler handler)
	throws SpmlException {

	try {
            XMLReader p = getXMLReader(this, this);

            // Under the assumption that sax parsing is used
            // for bulk loads and import, always enable validation
            setValidation(p, true);

            if (handler != null)
                p.setContentHandler(handler);

	    InputSource is = new InputSource(input);
	    p.parse(is);
	}
	catch (SAXParseException ex) {

	    String msg = "XML Error" + getLocationString(ex) + ": " +
		ex.getMessage();

	    throw new SpmlException(msg, ex);
	}
	catch (SAXException ex) {
	    wrapException(ex);
	}
	catch (IOException ex) {
	    throw new SpmlException(ex);
	}
    }

    /**
     * Parse a string using SAX.
     */
    public void saxParseString(String xml, ContentHandler handler)
	throws SpmlException {

	try {
            XMLReader p = getXMLReader(this, this);

            // Under the assumption that sax parsing is used
            // for bulk loads and import, always enable validation
            setValidation(p, true);

            if (handler != null)
                p.setContentHandler(handler);

	    StringReader reader = new StringReader(xml);
	    InputSource is = new InputSource(reader);
	    p.parse(is);
	}
	catch (SAXParseException ex) {

	    String msg = "XML Error" + getLocationString(ex) + ": " +
		ex.getMessage();

	    throw new SpmlException(msg, ex);
	}
	catch (SAXException ex) {
	    wrapException(ex);
	}
	catch (IOException ex) {
	    throw new SpmlException(ex);
	}
    }

     /**
      * Manually clear the pool
      */
     public static void clearPool() {
         _pool = null;
         _validatingPool = null;
     }

    //////////////////////////////////////////////////////////////////////
    //
    // File Utilities
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * Read the contents of a file and return it as a String.
     */
    static public String readFile(String name) throws SpmlException {
	byte[] bytes = readFileBytes(name);
	return new String(bytes);
    }

    static public byte[] readFileBytes(String name) throws SpmlException  {

	String path = getFilePath(name);
	File file = new File(path);
	return readFileBytes(file);
    }

    static public byte[] readFileBytes(File file) throws SpmlException {

	FileInputStream fis = null;
	byte[] bytes = null;
	try {
	    fis = new FileInputStream(file);
	    bytes = readInputStreamBytes(fis, true);
	}
	catch(java.io.FileNotFoundException ex) {
	    throw new SpmlException(ex);
	}

	return bytes;
    }

    /**
     * @return input stream content.
     * @param close - if true, close when finished reading.
     */
    static public byte[] readInputStreamBytes(InputStream is, boolean close)
	throws SpmlException {

        byte[] bytes = null;
	if (is != null) {
            ByteArrayOutputStream bout = new ByteArrayOutputStream(1024);
            try {
        	byte[] buf = new byte[1024];
                int bytesRead = 0;
                while ((bytesRead = is.read(buf)) != -1) {
                    bout.write(buf, 0, bytesRead);
                } // end of while ((read(buf) != -1)
                bytes = bout.toByteArray();
            }
	    catch(java.io.IOException ex) {
		throw new SpmlException(ex);
	    }
            finally {
                if (close && is!=null) {
                    try {
                        is.close();
                    }
                    catch (java.io.IOException ex) {
                        // ignore these
                    }
                } // end of if (is != null)
            }
        }
        return bytes;
    }

    /**
     * Store the contents of a String in a file.
     */
    static public void writeFile(String name, String contents)
	throws SpmlException {
	writeFile(name, contents.getBytes());
    }

    /**
     * Store the contents of a String in a file.
     */
    static public void writeFile(String name, byte[] contents)
	throws SpmlException {

	String path = getFilePath(name);
	FileOutputStream fos = null;
	try {
	    fos = new FileOutputStream(path);
	    fos.write(contents);
	}
	catch(java.io.FileNotFoundException ex) {
	    throw new SpmlException(ex);
	}
	catch(java.io.IOException ex) {
	    throw new SpmlException(ex);
	}
	finally {
	    try {
		if (fos != null) fos.close();
	    }
	    catch (java.io.IOException e) {
		// ignore these
	    }
	}
    }

    /**
     * Build an absolute path name for a file.
     * Typically used when you want to write an output file to
     * the "current working directory".
     *
     * If the name is already an absolute path we use it, otherwise
     * we use the system property "user.dir" as the root directory
     * and add the name.
     */
    public static String getFilePath(String name) {

	String path = null;

	if (name.charAt(0) == '/' || name.indexOf(":") != -1) {

	    // its an absolute path, just return it
	    path = name;
	}
	else {
	    // try relative to user.dir
	    String root = System.getProperty("user.dir");
	    if (root != null)
		path = root + "/" + name;
	    else {
		// no obvious way to position it, have to leave
		// it in the process' current working directory
		path = name;
	    }
	}

	return path;
    }

    //////////////////////////////////////////////////////////////////////
    //
    // main
    //
    //////////////////////////////////////////////////////////////////////

    /**
     * Parses an XML file, and displays any error or warning messages.
     * on the console.
     */
    public static void main(String args[]) {

	if (args.length < 1)
	    println("args: <filename>");
	else {
	    try {
		XmlParser p = new XmlParser(false);
		String xml = readFile(args[0]);
		Document d = p.parseString(xml);

		if (d == null) {
		    String msg = "WARNING: Parser did not return" +
			" a Document object.";
		    println(msg);
		}

		// make sure the sax parser works too
		println("SAX Parsing...\n");
		p.saxParse(args[0], null);

	    }
	    catch (Exception e) {
		println(e.toString());
	    }
	}
    }

}
