/*
* This software is subject to the terms of the Eclipse Public License v1.0
* Agreement, available at the following URL:
* http://www.eclipse.org/legal/epl-v10.html.
* You must accept the terms of that agreement to use this software.
*
* Copyright (c) 2002-2013 Pentaho Corporation..  All rights reserved.
*/

package mondrian.tui;

import mondrian.olap.Util;

import org.apache.xerces.dom.DocumentImpl;
import org.apache.xerces.impl.Constants;
import org.apache.xerces.parsers.DOMParser;
import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;
import org.apache.xpath.domapi.XPathEvaluatorImpl;

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

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;

/**
 * Some XML parsing, validation and transform utility methods used
 * to valiate XMLA responses.
 *
 * @author Richard M. Emberson
 */
public class XmlUtil {

    public static final String LINE_SEP =
        System.getProperty("line.separator", "\n");

    public static final String SOAP_PREFIX = "SOAP-ENV";
    public static final String XSD_PREFIX = "xsd";

    public static final String XMLNS = "xmlns";

    public static final String NAMESPACES_FEATURE_ID =
        "http://xml.org/sax/features/namespaces";
    public static final String VALIDATION_FEATURE_ID =
        "http://xml.org/sax/features/validation";

    public static final String SCHEMA_VALIDATION_FEATURE_ID =
        "http://apache.org/xml/features/validation/schema";
    public static final String FULL_SCHEMA_VALIDATION_FEATURE_ID =
        "http://apache.org/xml/features/validation/schema-full-checking";

    public static final String DEFER_NODE_EXPANSION =
        "http://apache.org/xml/features/dom/defer-node-expansion";

    public static final String SCHEMA_LOCATION =
        Constants.XERCES_PROPERTY_PREFIX + Constants.SCHEMA_LOCATION;

    /**
     * This is the xslt that can extract the "data" part of a SOAP
     * XMLA response.
     */
    public static final String getSoapXmlaXds2xd(String xmlaPrefix) {
        return
            "<?xml version='1.0'?>" + LINE_SEP
            + "<xsl:stylesheet " + LINE_SEP
            + "xmlns:xsl='http://www.w3.org/1999/XSL/Transform' " + LINE_SEP
            + "xmlns:xalan='http://xml.apache.org/xslt' " + LINE_SEP
            + "xmlns:xsd='http://www.w3.org/2001/XMLSchema' " + LINE_SEP
            + "xmlns:ROW='urn:schemas-microsoft-com:xml-analysis:rowset' "
            + LINE_SEP
            + "xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/' "
            + LINE_SEP
            + "xmlns:xmla='urn:schemas-microsoft-com:xml-analysis' " + LINE_SEP
            + "version='1.0' " + LINE_SEP
            + "> " + LINE_SEP
            + "<xsl:output method='xml'  " + LINE_SEP
            + "encoding='UTF-8' " + LINE_SEP
            + "indent='yes'  " + LINE_SEP
            + "xalan:indent-amount='2'/> " + LINE_SEP
            + "  " + LINE_SEP
            + "<!-- consume '/' and apply --> " + LINE_SEP
            + "<xsl:template match='/'> " + LINE_SEP
            + "<xsl:apply-templates/> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'Envelope' and apply --> " + LINE_SEP
            + "<xsl:template match='SOAP-ENV:Envelope'> " + LINE_SEP
            + "<xsl:apply-templates/> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'Header' --> " + LINE_SEP
            + "<xsl:template match='SOAP-ENV:Header'> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'Body' and apply --> " + LINE_SEP
            + "<xsl:template match='SOAP-ENV:Body'> " + LINE_SEP
                + "<xsl:apply-templates/> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'DiscoverResponse' and apply --> " + LINE_SEP
            + "<xsl:template match='" + xmlaPrefix + ":DiscoverResponse'> "
            + LINE_SEP
            + "<xsl:apply-templates/> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'return' and apply --> " + LINE_SEP
            + "<xsl:template match='" + xmlaPrefix + ":return'> " + LINE_SEP
            + "<xsl:apply-templates/> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'xsd:schema' --> " + LINE_SEP
            + "<xsl:template match='xsd:schema'> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- copy everything else --> " + LINE_SEP
            + "<xsl:template match='*|@*'> " + LINE_SEP
            + "<xsl:copy> " + LINE_SEP
            + "<xsl:apply-templates select='@*|node()'/> " + LINE_SEP
            + "</xsl:copy> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "</xsl:stylesheet>";
    }

    /**
     * This is the xslt that can extract the "schema" part of a SOAP
     * XMLA response.
     */
    public static final String getSoapXmlaXds2xs(String xmlaPrefix) {
        return
            "<?xml version='1.0'?> " + LINE_SEP
            + "<xsl:stylesheet  " + LINE_SEP
            + "xmlns:xsl='http://www.w3.org/1999/XSL/Transform'  " + LINE_SEP
            + "xmlns:xalan='http://xml.apache.org/xslt' " + LINE_SEP
            + "xmlns:xsd='http://www.w3.org/2001/XMLSchema' " + LINE_SEP
            + "xmlns:ROW='urn:schemas-microsoft-com:xml-analysis:rowset' "
            + LINE_SEP
            + "xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/'  "
            + LINE_SEP
            + "xmlns:xmla='urn:schemas-microsoft-com:xml-analysis' " + LINE_SEP
            + "version='1.0' " + LINE_SEP
            + "> " + LINE_SEP
            + "<xsl:output method='xml'  " + LINE_SEP
            + "encoding='UTF-8' " + LINE_SEP
            + "indent='yes'  " + LINE_SEP
            + "xalan:indent-amount='2'/> " + LINE_SEP
            + "<!-- consume '/' and apply --> " + LINE_SEP
            + "<xsl:template match='/'> " + LINE_SEP
            + "<xsl:apply-templates/> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'Envelope' and apply --> " + LINE_SEP
            + "<xsl:template match='SOAP-ENV:Envelope'> " + LINE_SEP
            + "<xsl:apply-templates/> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'Header' --> " + LINE_SEP
            + "<xsl:template match='SOAP-ENV:Header'> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'Body' and apply --> " + LINE_SEP
            + "<xsl:template match='SOAP-ENV:Body'> " + LINE_SEP
            + "<xsl:apply-templates/> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'DiscoverResponse' and apply --> " + LINE_SEP
            + "<xsl:template match='" + xmlaPrefix + ":DiscoverResponse'> "
            + LINE_SEP
            + "<xsl:apply-templates/> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'return' and apply --> " + LINE_SEP
            + "<xsl:template match='" + xmlaPrefix + ":return'> " + LINE_SEP
            + "<xsl:apply-templates/> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'root' and apply --> " + LINE_SEP
            + "<xsl:template match='ROW:root'> " + LINE_SEP
            + "<xsl:apply-templates/> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'row' --> " + LINE_SEP
            + "<xsl:template match='ROW:row'> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- copy everything else --> " + LINE_SEP
            + "<xsl:template match='*|@*'> " + LINE_SEP
            + "<xsl:copy> " + LINE_SEP
            + "<xsl:apply-templates select='@*|node()'/> " + LINE_SEP
            + "</xsl:copy> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "</xsl:stylesheet>";
    }

    /**
     * This is the xslt that can extract the "data" part of a XMLA response.
     */
    public static final String getXmlaXds2xd(String ns) {
        String xmlaPrefix = (ns == null) ? "" : (ns + ":");
        return
            "<?xml version='1.0'?>" + LINE_SEP
            + "<xsl:stylesheet " + LINE_SEP
            + "xmlns:xsl='http://www.w3.org/1999/XSL/Transform' " + LINE_SEP
            + "xmlns:xalan='http://xml.apache.org/xslt' " + LINE_SEP
            + "xmlns:xsd='http://www.w3.org/2001/XMLSchema' " + LINE_SEP
            + "xmlns:ROW='urn:schemas-microsoft-com:xml-analysis:rowset' "
            + LINE_SEP
            + "xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/' "
            + LINE_SEP
            + "xmlns:xmla='urn:schemas-microsoft-com:xml-analysis' " + LINE_SEP
            + "version='1.0' " + LINE_SEP
            + "> " + LINE_SEP
            + "<xsl:output method='xml'  " + LINE_SEP
            + "encoding='UTF-8' " + LINE_SEP
            + "indent='yes'  " + LINE_SEP
            + "xalan:indent-amount='2'/> " + LINE_SEP
            + "  " + LINE_SEP
            + "<!-- consume '/' and apply --> " + LINE_SEP
            + "<xsl:template match='/'> " + LINE_SEP
            + "<xsl:apply-templates/> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'DiscoverResponse' and apply --> " + LINE_SEP
            + "<xsl:template match='" + xmlaPrefix + "DiscoverResponse'> "
            + LINE_SEP
            + "<xsl:apply-templates/> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'return' and apply --> " + LINE_SEP
            + "<xsl:template match='" + xmlaPrefix + "return'> " + LINE_SEP
            + "<xsl:apply-templates/> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'xsd:schema' --> " + LINE_SEP
            + "<xsl:template match='xsd:schema'> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- copy everything else --> " + LINE_SEP
            + "<xsl:template match='*|@*'> " + LINE_SEP
            + "<xsl:copy> " + LINE_SEP
            + "<xsl:apply-templates select='@*|node()'/> " + LINE_SEP
            + "</xsl:copy> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "</xsl:stylesheet>";
    }

    /**
     * This is the xslt that can extract the "schema" part of a XMLA response.
     */
    public static final String getXmlaXds2xs(String ns) {
        String xmlaPrefix = (ns == null) ? "" : (ns + ":");
        return
            "<?xml version='1.0'?> " + LINE_SEP
            + "<xsl:stylesheet  " + LINE_SEP
            + "xmlns:xsl='http://www.w3.org/1999/XSL/Transform'  " + LINE_SEP
            + "xmlns:xalan='http://xml.apache.org/xslt' " + LINE_SEP
            + "xmlns:xsd='http://www.w3.org/2001/XMLSchema' " + LINE_SEP
            + "xmlns:ROW='urn:schemas-microsoft-com:xml-analysis:rowset' "
            + LINE_SEP
            + "xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/'  "
            + LINE_SEP
            + "xmlns:xmla='urn:schemas-microsoft-com:xml-analysis' " + LINE_SEP
            + "version='1.0' " + LINE_SEP
            + "> " + LINE_SEP
            + "<xsl:output method='xml'  " + LINE_SEP
            + "encoding='UTF-8' " + LINE_SEP
            + "indent='yes'  " + LINE_SEP
            + "xalan:indent-amount='2'/> " + LINE_SEP
            + "<!-- consume '/' and apply --> " + LINE_SEP
            + "<xsl:template match='/'> " + LINE_SEP
            + "<xsl:apply-templates/> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'DiscoverResponse' and apply --> " + LINE_SEP
            + "<xsl:template match='" + xmlaPrefix + "DiscoverResponse'> "
            + LINE_SEP
            + "<xsl:apply-templates/> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'return' and apply --> " + LINE_SEP
            + "<xsl:template match='" + xmlaPrefix + "return'> " + LINE_SEP
            + "<xsl:apply-templates/> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'root' and apply --> " + LINE_SEP
            + "<xsl:template match='ROW:root'> " + LINE_SEP
            + "<xsl:apply-templates/> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- consume 'row' --> " + LINE_SEP
            + "<xsl:template match='ROW:row'> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "<!-- copy everything else --> " + LINE_SEP
            + "<xsl:template match='*|@*'> " + LINE_SEP
            + "<xsl:copy> " + LINE_SEP
            + "<xsl:apply-templates select='@*|node()'/> " + LINE_SEP
            + "</xsl:copy> " + LINE_SEP
            + "</xsl:template> " + LINE_SEP
            + "</xsl:stylesheet>";
    }

    /**
     * Error handler plus helper methods.
     */
    public static class SaxErrorHandler implements ErrorHandler {
        public static final String WARNING_STRING        = "WARNING";
        public static final String ERROR_STRING          = "ERROR";
        public static final String FATAL_ERROR_STRING    = "FATAL";

        // DOMError values
        public static final short SEVERITY_WARNING      = 1;
        public static final short SEVERITY_ERROR        = 2;
        public static final short SEVERITY_FATAL_ERROR  = 3;

        public void printErrorInfos(PrintStream out) {
            if (errors != null) {
                for (ErrorInfo error : errors) {
                    out.println(formatErrorInfo(error));
                }
            }
        }

        public static String formatErrorInfos(SaxErrorHandler saxEH) {
            if (! saxEH.hasErrors()) {
                return "";
            }
            StringBuilder buf = new StringBuilder(512);
            for (ErrorInfo error : saxEH.getErrors()) {
                buf.append(formatErrorInfo(error));
                buf.append(LINE_SEP);
            }
            return buf.toString();
        }

        public static String formatErrorInfo(ErrorInfo ei) {
            StringBuilder buf = new StringBuilder(128);
            buf.append("[");
            switch (ei.severity) {
            case SEVERITY_WARNING:
                buf.append(WARNING_STRING);
                break;
            case SEVERITY_ERROR:
                buf.append(ERROR_STRING);
                break;
            case SEVERITY_FATAL_ERROR:
                buf.append(FATAL_ERROR_STRING);
                break;
            }
            buf.append(']');
            String systemId = ei.exception.getSystemId();
            if (systemId != null) {
                int index = systemId.lastIndexOf('/');
                if (index != -1) {
                    systemId = systemId.substring(index + 1);
                }
                buf.append(systemId);
            }
            buf.append(':');
            buf.append(ei.exception.getLineNumber());
            buf.append(':');
            buf.append(ei.exception.getColumnNumber());
            buf.append(": ");
            buf.append(ei.exception.getMessage());
            return buf.toString();
        }

        public static class ErrorInfo {
            public SAXParseException exception;
            public short severity;
            ErrorInfo(short severity, SAXParseException exception) {
                this.severity = severity;
                this.exception = exception;
            }
        }

        private List<ErrorInfo> errors;

        public SaxErrorHandler() {
        }

        public List<ErrorInfo> getErrors() {
            return this.errors;
        }

        public boolean hasErrors() {
            return (this.errors != null);
        }

        public void warning(SAXParseException exception) throws SAXException {
            addError(new ErrorInfo(SEVERITY_WARNING, exception));
        }

        public void error(SAXParseException exception) throws SAXException {
            addError(new ErrorInfo(SEVERITY_ERROR, exception));
        }

        public void fatalError(SAXParseException exception)
            throws SAXException
        {
            addError(new ErrorInfo(SEVERITY_FATAL_ERROR, exception));
        }

        protected void addError(ErrorInfo ei) {
            if (this.errors == null) {
                this.errors = new ArrayList<ErrorInfo>();
            }
            this.errors.add(ei);
        }

        public String getFirstError() {
            return (hasErrors())
                ? formatErrorInfo(errors.get(0))
                : "";
        }
    }

    public static Document newDocument(Node firstElement, boolean deepcopy) {
        Document newDoc = new DocumentImpl();
        newDoc.appendChild(newDoc.importNode(firstElement, deepcopy));
        return newDoc;
    }


    //////////////////////////////////////////////////////////////////////////
    // parse
    //////////////////////////////////////////////////////////////////////////

    /**
     * Get your non-cached DOM parser which can be configured to do schema
     * based validation of the instance Document.
     *
     */
    public static DOMParser getParser(
        String schemaLocationPropertyValue,
        EntityResolver entityResolver,
        boolean validate)
        throws SAXNotRecognizedException, SAXNotSupportedException
    {
        boolean doingValidation =
            (validate || (schemaLocationPropertyValue != null));

        DOMParser parser = new DOMParser();

        parser.setEntityResolver(entityResolver);
        parser.setErrorHandler(new SaxErrorHandler());
        parser.setFeature(DEFER_NODE_EXPANSION, false);
        parser.setFeature(NAMESPACES_FEATURE_ID, true);
        parser.setFeature(SCHEMA_VALIDATION_FEATURE_ID, doingValidation);
        parser.setFeature(VALIDATION_FEATURE_ID, doingValidation);

        if (schemaLocationPropertyValue != null) {
            parser.setProperty(
                SCHEMA_LOCATION,
                schemaLocationPropertyValue.replace('\\', '/'));
        }

        return parser;
    }

    /**
     * See if the DOMParser after parsing a Document has any errors and,
     * if so, throw a RuntimeException exception containing the errors.
     *
     */
    private static void checkForParseError(
        final DOMParser parser,
        Throwable t)
    {
        if (Util.IBM_JVM) {
            // IBM JDK returns lots of errors. Not sure whether they are
            // real errors, but ignore for now.
            return;
        }

        final ErrorHandler errorHandler = parser.getErrorHandler();
        if (errorHandler instanceof SaxErrorHandler) {
            final SaxErrorHandler saxEH = (SaxErrorHandler) errorHandler;
            final List<SaxErrorHandler.ErrorInfo> errors = saxEH.getErrors();

            if (errors != null && errors.size() > 0) {
                String errorStr = SaxErrorHandler.formatErrorInfos(saxEH);
                throw new RuntimeException(errorStr, t);
            }
        } else {
            System.out.println("errorHandler=" + errorHandler);
        }
    }

    private static void checkForParseError(final DOMParser parser) {
        checkForParseError(parser, null);
    }


    /**
     * Parse a String into a Document (no validation).
     *
     */
    public static Document parseString(String s)
        throws SAXException, IOException
    {
        // Hack to workaround bug #622 until 1.4.2_08
        final int length = s.length();

        if (length > 16777216 && length % 4 == 1) {
            final StringBuilder buf = new StringBuilder(length + 1);

            buf.append(s);
            buf.append('\n');
            s = buf.toString();
        }

        return XmlUtil.parse(s.getBytes());
    }

    /**
     * Parse a byte array into a Document (no validation).
     *
     */
    public static Document parse(byte[] bytes)
        throws SAXException, IOException
    {
        return XmlUtil.parse(new ByteArrayInputStream(bytes));
    }

    public static Document parse(File file)
        throws SAXException, IOException
    {
        return parse(new FileInputStream(file));
    }

    /**
     * Parse a stream into a Document (no validation).
     *
     */
    public static Document parse(InputStream in)
        throws SAXException, IOException
    {
        InputSource source = new InputSource(in);

        DOMParser parser = XmlUtil.getParser(null, null, false);
        try {
            parser.parse(source);
            checkForParseError(parser);
        } catch (SAXParseException ex) {
            checkForParseError(parser, ex);
        }

        Document document = parser.getDocument();
        return document;
    }


    //////////////////////////////////////////////////////////////////////////
    // xpath
    //////////////////////////////////////////////////////////////////////////
    /**
     * Create a context document for use in performing XPath operations.
     * An array of prefix/namespace-urls are provided as input. These
     * namespace-urls should be all of those that will appear in the
     * document against which an xpath is to be applied.
     * Importantly, it is, in fact, each element of the Document that
     * has a default namespace these namespaces MUST have
     * prefix/namespace-urls pairs in the context document and the prefix
     * provided MUST also be used in the xpath.
     * Elements with explicit namespaces don't have to have pairs in the
     * context Document as long as the xpath uses the same prefixes
     * that appear in the target Document.
     *
     */
    public static Document createContextDocument(String[][] nsArray)
        throws SAXException, IOException
    {
        StringBuilder buf = new StringBuilder(256);
        buf.append("<?xml version='1.0' encoding='utf-8'?>");
        buf.append("<DOES_NOT_MATTER");
        for (int i = 0; i < nsArray.length; i++) {
            String prefix = nsArray[i][0];
            String nsURI = nsArray[i][1];

            buf.append(" xmlns:");
            buf.append(prefix);
            buf.append("=\"");
            buf.append(nsURI);
            buf.append("\"");
        }
        buf.append(" />");

        String docStr = buf.toString();
        return parseString(docStr);
    }

    public static String makeSoapPath() {
        return XmlUtil.makeSoapPath(SOAP_PREFIX);
    }

    // '/soapX:Envelope/soapX:Body/*'
    public static String makeSoapPath(String prefix) {
        StringBuilder buf = new StringBuilder(20);
        buf.append('/');
        if (prefix != null) {
            buf.append(prefix);
            buf.append(':');
        }
        buf.append("Envelope");
        buf.append('/');
        if (prefix != null) {
            buf.append(prefix);
            buf.append(':');
        }
        buf.append("Body");
        buf.append('/');
        buf.append('*');

        return buf.toString();
    }

    public static String makeRootPathInSoapBody() {
        return makeRootPathInSoapBody("xmla", XSD_PREFIX);
    }

    // '/xmla:DiscoverResponse/xmla:return/ROW/root/*'
    public static String makeRootPathInSoapBody(
        String xmlaPrefix,
        String xsdPrefix)
    {
        StringBuilder buf = new StringBuilder(20);
        buf.append("/").append(xmlaPrefix).append(":DiscoverResponse");
        buf.append("/").append(xmlaPrefix).append(":return");
        buf.append("/ROW:root");
        buf.append('/');
        buf.append('*');
/*
        if (xsdPrefix != null) {
            buf.append(xsdPrefix);
            buf.append(':');
        }
        buf.append("schema");
*/

        return buf.toString();
    }

    public static String selectAsString(
        Node node,
        String xpath)
        throws XPathException
    {
        return XmlUtil.selectAsString(node, xpath, node);
    }

    public static String selectAsString(
        Node node,
        String xpath,
        Node namespaceNode)
        throws XPathException
    {
        XPathResult xpathResult = XmlUtil.select(node, xpath, namespaceNode);
        return XmlUtil.convertToString(xpathResult, false);
    }

    public static Node[] selectAsNodes(
        Node node,
        String xpath)
        throws XPathException
    {
        return XmlUtil.selectAsNodes(node, xpath, node);
    }

    public static Node[] selectAsNodes(
        Node node,
        String xpath,
        Node namespaceNode)
        throws XPathException
    {
        XPathResult xpathResult = XmlUtil.select(node, xpath, namespaceNode);
        return XmlUtil.convertToNodes(xpathResult);
    }

    public static XPathResult select(
        Node contextNode,
        String xpath,
        Node namespaceNode)
        throws XPathException
    {
        XPathEvaluator evaluator = new XPathEvaluatorImpl();
        XPathNSResolver resolver = evaluator.createNSResolver(namespaceNode);

        return (XPathResult) evaluator.evaluate(
            xpath, contextNode, resolver,
            XPathResult.ANY_TYPE, null);
    }

    /**
     * Convert an XPathResult object to String.
     *
     */
    public static String convertToString(
        XPathResult xpathResult,
        boolean prettyPrint)
    {
        switch (xpathResult.getResultType()) {
        case XPathResult.NUMBER_TYPE:
            double d = xpathResult.getNumberValue();
            return Double.toString(d);

        case XPathResult.STRING_TYPE:
            String s = xpathResult.getStringValue();
            return s;

        case XPathResult.BOOLEAN_TYPE:
            boolean b = xpathResult.getBooleanValue();
            return String.valueOf(b);

        case XPathResult.FIRST_ORDERED_NODE_TYPE:
        case XPathResult.ANY_UNORDERED_NODE_TYPE:
        {
            Node node = xpathResult.getSingleNodeValue();
            return XmlUtil.toString(node, prettyPrint);
        }
        case XPathResult.ORDERED_NODE_ITERATOR_TYPE:
        case XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
        {
            StringBuilder buf = new StringBuilder(512);
            Node node = xpathResult.iterateNext();
            while (node != null) {
                buf.append(XmlUtil.toString(node, prettyPrint));
                node = xpathResult.iterateNext();
            }
            return buf.toString();
        }
        case XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE:
        case XPathResult.ORDERED_NODE_SNAPSHOT_TYPE:
        {
            StringBuilder buf = new StringBuilder(512);
            int len = xpathResult.getSnapshotLength();
            for (int i = 0; i < len; i++) {
                Node node = xpathResult.snapshotItem(i);
                buf.append(XmlUtil.toString(node, prettyPrint));
            }
            return buf.toString();
        }
        default:
            String msg = "Unknown xpathResult.type = "
                + xpathResult.getResultType();
            throw new XPathException(XPathException.TYPE_ERR, msg);
        }
    }

    private static final Node[] NULL_NODE_ARRAY = new Node[0];

    /**
     * Convert an XPathResult to an array of Nodes.
     *
     */
    public static Node[] convertToNodes(XPathResult xpathResult) {
        switch (xpathResult.getResultType()) {
        case XPathResult.NUMBER_TYPE:
            return NULL_NODE_ARRAY;

        case XPathResult.STRING_TYPE:
            return NULL_NODE_ARRAY;

        case XPathResult.BOOLEAN_TYPE:
            return NULL_NODE_ARRAY;

        case XPathResult.FIRST_ORDERED_NODE_TYPE:
        case XPathResult.ANY_UNORDERED_NODE_TYPE:
        {
            Node node = xpathResult.getSingleNodeValue();
            return new Node[] { node };
        }
        case XPathResult.ORDERED_NODE_ITERATOR_TYPE:
        case XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
        {
            List<Node> list = new ArrayList<Node>();
            Node node = xpathResult.iterateNext();
            while (node != null) {
                list.add(node);
                node = xpathResult.iterateNext();
            }
            return (Node[]) list.toArray(NULL_NODE_ARRAY);
        }
        case XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE:
        case XPathResult.ORDERED_NODE_SNAPSHOT_TYPE:
        {
            int len = xpathResult.getSnapshotLength();
            Node[] nodes = new Node[len];
            for (int i = 0; i < len; i++) {
                Node node = xpathResult.snapshotItem(i);
                nodes[i] = node;
            }
            return nodes;
        }
        default:
            String msg = "Unknown xpathResult.type = "
                + xpathResult.getResultType();
            throw new XPathException(XPathException.TYPE_ERR, msg);
        }
    }

    /**
     * Convert a Node to a String.
     *
     */
    public static String toString(Node node, boolean prettyPrint) {
        if (node == null) {
            return null;
        }
        try {
            Document doc = node.getOwnerDocument();
            OutputFormat format;
            if (doc != null) {
                format = new OutputFormat(doc, null, prettyPrint);
            } else {
                format = new OutputFormat("xml", null, prettyPrint);
                format.setLineWidth(0); // don't wrap lines
            }
            if (prettyPrint) {
                format.setLineSeparator(LINE_SEP);
            } else {
                format.setLineSeparator("");
            }
            StringWriter writer = new StringWriter(1000);
            XMLSerializer serial = new XMLSerializer(writer, format);
            serial.asDOMSerializer();
            if (node instanceof Document) {
                serial.serialize((Document) node);
            } else if (node instanceof Element) {
                format.setOmitXMLDeclaration(true);
                serial.serialize((Element) node);
            } else if (node instanceof DocumentFragment) {
                format.setOmitXMLDeclaration(true);
                serial.serialize((DocumentFragment) node);
            } else if (node instanceof Text) {
                Text text = (Text) node;
                return text.getData();
            } else if (node instanceof Attr) {
                Attr attr = (Attr) node;
                String name = attr.getName();
                String value = attr.getValue();
                writer.write(name);
                writer.write("=\"");
                writer.write(value);
                writer.write("\"");
                if (prettyPrint) {
                    writer.write(LINE_SEP);
                }
            } else {
                writer.write("node class = " + node.getClass().getName());
                if (prettyPrint) {
                    writer.write(LINE_SEP);
                } else {
                    writer.write(' ');
                }
                writer.write("XmlUtil.toString: fix me: ");
                writer.write(node.toString());
                if (prettyPrint) {
                    writer.write(LINE_SEP);
                }
            }
            return writer.toString();
        } catch (Exception ex) {
            // ignore
            return null;
        }
    }

    //////////////////////////////////////////////////////////////////////////
    // validate
    //////////////////////////////////////////////////////////////////////////

    /**
     * This can be extened to have a map from publicId/systemId
     * to InputSource.
     */
    public static class Resolver implements EntityResolver {
        private InputSource source;

        protected Resolver() {
            this((InputSource) null);
        }
        public Resolver(Document doc) {
            this(XmlUtil.toString(doc, false));
        }
        public Resolver(String str) {
            this(new InputSource(new StringReader(str)));
        }
        public Resolver(InputSource source) {
            this.source = source;
        }
        public InputSource resolveEntity(
            String publicId,
            String systemId)
            throws SAXException, IOException
        {
            return source;
        }
    }

    /**
     * Get the Xerces version being used.
     *
     * @return Xerces version being used
     */
    public static String getXercesVersion() {
        try {
            return org.apache.xerces.impl.Version.getVersion();
        } catch (java.lang.NoClassDefFoundError ex) {
            return "Xerces-J 2.2.0";
        }
    }

    /**
     * Get the number part of the Xerces Version string.
     *
     * @return number part of the Xerces Version string
     */
    public static String getXercesVersionNumberString() {
        String version = getXercesVersion();
        int index = version.indexOf(' ');
        return (index == -1)
            ? "0.0.0" : version.substring(index + 1);
    }

    private static int[] versionNumbers = null;

    /**
     * Gets the Xerces version numbers as a three part array of ints where
     * the first element is the major release number, the second is the
     * minor release number, and the third is the patch number.
     *
     * @return Xerces version number as int array
     */
    public static synchronized int[] getXercesVersionNumbers() {
        if (versionNumbers == null) {
            int[] verNums = new int[3];
            String verNumStr = getXercesVersionNumberString();
            int index = verNumStr.indexOf('.');
            verNums[0] = Integer.parseInt(verNumStr.substring(0, index));

            verNumStr = verNumStr.substring(index + 1);
            index = verNumStr.indexOf('.');
            verNums[1] = Integer.parseInt(verNumStr.substring(0, index));

            verNumStr = verNumStr.substring(index + 1);
            verNums[2] = Integer.parseInt(verNumStr);

            versionNumbers = verNums;
        }

        return versionNumbers;
    }

    /**
     * I could not get Xerces 2.2 to validate. So, I hard coded allowing
     * Xerces 2.6 and above to validate and by setting the following
     * System property one can test validating with earlier versions
     * of Xerces.
     */
    private static final String ALWAYS_ATTEMPT_VALIDATION =
        "mondrian.xml.always.attempt.validation";

    private static final boolean alwaysAttemptValidation;
    static {
        alwaysAttemptValidation =
            Boolean.getBoolean(ALWAYS_ATTEMPT_VALIDATION);
    }

    /**
     * Returns whether the XML parser supports validation.
     *
     * <p>I could not get validation to work with Xerces 2.2 so I put in
     * this check. If you want to test on an earlier version of Xerces
     * simply define the above property:
     *   "mondrian.xml.always.attempt.validation",
     * to true.
     *
     * @return whether the XML parser supports validation
     */
    public static boolean supportsValidation() {
        if (alwaysAttemptValidation) {
            return true;
        } else {
            int[] verNums = getXercesVersionNumbers();
            return (verNums[0] >= 3)
                || ((verNums[0] == 2) && (verNums[1] >= 6));
        }
    }

    public static void validate(
        Document doc,
        String schemaLocationPropertyValue,
        EntityResolver resolver)
        throws IOException,
        SAXException
    {
        OutputFormat format  = new OutputFormat(doc, null, true);
        StringWriter writer = new StringWriter(1000);
        XMLSerializer serial = new XMLSerializer(writer, format);
        serial.asDOMSerializer();
        serial.serialize(doc);
        String docString = writer.toString();

        validate(docString, schemaLocationPropertyValue, resolver);
    }

    public static void validate(
        String docStr,
        String schemaLocationPropertyValue,
        EntityResolver resolver)
        throws IOException,
        SAXException
    {
        if (resolver == null) {
            resolver = new Resolver();
        }
        DOMParser parser =
            getParser(schemaLocationPropertyValue, resolver, true);

        try {
            parser.parse(new InputSource(new StringReader(docStr)));
            checkForParseError(parser);
        } catch (SAXParseException ex) {
            checkForParseError(parser, ex);
        }
    }

    /**
     * This is used to get a Document's namespace attribute value.
     *
     */
    public static String getNamespaceAttributeValue(Document doc) {
        Element el = doc.getDocumentElement();
        String prefix = el.getPrefix();
        Attr attr = (prefix == null)
                    ? el.getAttributeNode(XMLNS)
                    : el.getAttributeNode(XMLNS + ':' + prefix);
        return (attr == null) ? null : attr.getValue();
    }

    //////////////////////////////////////////////////////////////////////////
    // transform
    //////////////////////////////////////////////////////////////////////////

    private static TransformerFactory tfactory = null;

    public static TransformerFactory getTransformerFactory()
        throws TransformerFactoryConfigurationError
    {
        if (tfactory == null) {
            tfactory = TransformerFactory.newInstance();
        }
        return tfactory;
    }

    /**
     * Transform a Document and return the transformed Node.
     *
     */
    public static Node transform(
        Document inDoc,
        String xslFileName,
        String[][] namevalueParameters)
        throws ParserConfigurationException,
               SAXException,
               IOException,
               TransformerConfigurationException,
               TransformerException
    {
        DocumentBuilderFactory dfactory = DocumentBuilderFactory.newInstance();
        dfactory.setNamespaceAware(true);

        DocumentBuilder docBuilder = dfactory.newDocumentBuilder();
        Node xslDOM = docBuilder.parse(new InputSource(xslFileName));

        TransformerFactory tfactory = getTransformerFactory();
        Templates stylesheet = tfactory.newTemplates(
            new DOMSource(xslDOM, xslFileName));
        Transformer transformer = stylesheet.newTransformer();
        if (namevalueParameters != null) {
            for (int i = 0; i < namevalueParameters.length; i++) {
                String name = namevalueParameters[i][0];
                String value = namevalueParameters[i][1];

                transformer.setParameter(name, value);
            }
        }
        DOMResult domResult = new DOMResult();
        transformer.transform(new DOMSource(inDoc, null), domResult);

        return domResult.getNode();
    }

    public static Node transform(
        Document inDoc,
        String xslFileName)
        throws ParserConfigurationException,
               SAXException,
               IOException,
               TransformerConfigurationException,
               TransformerException
    {
        return transform(inDoc, xslFileName, null);
    }

    /**
     * Transform a Document and return the transformed Node.
     *
     */
    public static Node transform(
        Document inDoc,
        Reader xslReader,
        String[][] namevalueParameters)
        throws ParserConfigurationException,
               SAXException,
               IOException,
               TransformerConfigurationException,
               TransformerException
    {
        DocumentBuilderFactory dfactory = DocumentBuilderFactory.newInstance();
        dfactory.setNamespaceAware(true);

        DocumentBuilder docBuilder = dfactory.newDocumentBuilder();
        Node xslDOM = docBuilder.parse(new InputSource(xslReader));

        TransformerFactory tfactory = getTransformerFactory();
        Templates stylesheet = tfactory.newTemplates(new DOMSource(xslDOM));
        Transformer transformer = stylesheet.newTransformer();
        if (namevalueParameters != null) {
            for (int i = 0; i < namevalueParameters.length; i++) {
                String name = namevalueParameters[i][0];
                String value = namevalueParameters[i][1];

                transformer.setParameter(name, value);
            }
        }

        DOMResult domResult = new DOMResult();
        transformer.transform(new DOMSource(inDoc, null), domResult);

        return domResult.getNode();
    }

    public static Node transform(
        Document inDoc,
        Reader xslReader)
        throws ParserConfigurationException,
               SAXException,
               IOException,
               TransformerConfigurationException,
               TransformerException
    {
        return transform(inDoc, xslReader, null);
    }


    private XmlUtil() {
    }
}

// End XmlUtil.java
