/*  Copyright (c) 2000 by Oracle Corporation. All Rights Reserved. */

package oracle.portal.utils.xml.v1;

import java.lang.reflect.*;
import java.io.*;
import java.text.*;
import java.util.*;


import org.w3c.dom.*;
import org.w3c.dom.Node;
import org.xml.sax.*;
import oracle.xml.parser.v2.*;
import oracle.webdb.utils.SimpleStringBuffer;

import java.net.*;

/**
 * Class containing utility methods for parsing and manipulating XML documents.
 */
public class XMLUtil
{

    private static final String PI_STYLESHEET = "xml-stylesheet";
    private static final String STYLESHEET_HREF_ATTRIBUTE = "href";
    private static final String STYLESHEET_TYPE_ATTRIBUTE = "type";
    private static final String XSL_STYLESHEET_TYPE = "text/xsl";

    private static final String escapedAmpersand = "&amp;";
    private static final String escapedLessThan = "&lt;";
    private static final String escapedGreaterThan = "&gt;";
    private static final String escapedDoubleQuote = "&quot;";
    private static final String escapedSingleQuote = "&apos;";

    /**
     * Parse an XML document from the given <code>InputStream</code>, reporting
     * errors and warnings to the given <code>OutputStream</code>.
     * @param docInputStream <code>InputStream</code> from which the document
     * should be read
     * @param errOut <code>OutputStream</code> to which parsing errors and
     * warnings should be reported
     * @exception IOException if an I/O error occurs
     * @exception SAXException if a parsing error occurs
     */
    public static XMLDocument parseDocument(InputStream docInputStream, OutputStream errOut)
        throws IOException, SAXException
    {

        // Parse the document.
        DOMParser parser = getParser(errOut);
        parser.parse(docInputStream);

        // Return the document.
        return parser.getDocument();
    }

    /**
     * Apply the given stylesheet to the given XML document, reporting errors
     * and warnings to the given <code>OutputStream</code>.
     * @param xmldoc a parsed XML document
     * @param xsl a parsed XSL stylesheet
     * @param errOut <code>OutputStream</code> to which stylesheet processing
     * errors should be reported
     */
    public static XMLDocument applyXSL(XMLDocument xmldoc, XSLStylesheet xsl,
                                       OutputStream errOut)
    throws IOException, XSLException
    {
        XSLProcessor processor = new XSLProcessor();
        processor.showWarnings(true);
        processor.setErrorStream(errOut);
        XMLDocumentFragment result = processor.processXSL(xsl, xmldoc);
        XMLDocument newdoc = new XMLDocument();
        newdoc.appendChild(result);
        
        return newdoc;
    }

    /**
     * Get an <code>Enumeration</code> of {@link
     * org.w3c.dom.ProcessingInstruction}s from the given XML document which
     * have the specified target (for example, <code>"xml-stylesheet"</code>).
     * @param doc the XML document
     * @param targetName the target of the processing instructions to be returned
     * @return <code>Enumeration</code> of {@link
     * org.w3c.dom.ProcessingInstruction}s from the given XML document which
     * have the specified target.
     */
    public static Enumeration getTargetPIs(XMLDocument doc, String targetName)
    {
        Vector pis = new Vector();

        for (Node n = doc.getFirstChild();
             n != null;
             n = n.getNextSibling())
        {
            if ((n.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) &&
                (((ProcessingInstruction)n).getTarget().equals(targetName)))
            {
                pis.addElement(n);
            }
        }
        return pis.elements();
    }

    /**
     * Get an <code>Enumeration</code> of {@link
     * org.w3c.dom.ProcessingInstruction}s from the given XML document which
     * have a target of <code>"xml-stylesheet"</code>.
     * @param doc the XML document
     * @return <code>Enumeration</code> of {@link
     * org.w3c.dom.ProcessingInstruction}s from the given XML document which
     * have a target of <code>"xml-stylesheet"</code>.
     */
    public static Enumeration getStyleSheetPIs(XMLDocument doc)
    {
        return getTargetPIs(doc, PI_STYLESHEET);
    }

    /**
     * Get an <code>Enumeration</code> of {@link
     * oracle.xml.parser.v2.XSLStylesheet}s referenced by processing
     * instructions in the given XML document which have a target of
     * <code>"xml-stylesheet"</code>.
     * @param doc the XML document
     * @return <code>Enumeration</code> of {@link
     * oracle.xml.parser.v2.XSLStylesheet}s referenced by processing
     * instructions in the given XML document which have a target of
     * <code>"xml-stylesheet"</code>.
     */
    public static Enumeration getXslDocs(XMLDocument doc)
        throws IOException, SAXException, XSLException
    {
        DOMParser parser = getParser();
        Vector xsldocs = new Vector();
        for (Enumeration e = getStyleSheetPIs(doc);
             e.hasMoreElements();)
        {
            ProcessingInstruction pi = (ProcessingInstruction)e.nextElement();

            URL xslURL = getXslURL(parsePI(pi));

            if(xslURL != null)
            {
                parser.parse(xslURL);
            
                xsldocs.addElement(new XSLStylesheet(parser.getDocument(),xslURL));
            }
        }
        return xsldocs.elements();
    }

    /**
     * Given a <code>ProcessingInstruction</code> whose data contains
     * attribute/value pairs, return a <code>Hashtable</code> which contains
     * the attributes as keys and their corresponding values as values.
     * @param pi a processing instruction
     * @return <code>Hashtable</code> mapping processing instruction attributes
     * to values
     */
    public static Hashtable parsePI(ProcessingInstruction pi)
    throws SAXException
    {
        Hashtable hashtable = new Hashtable();
        String data = pi.getNodeValue();

        // First, get rid of an extra '?' character at the end of the
        // instruction, if one is there. There is a bug with the Oracle XSL
        // processor that means it doesn't terminate PIs properly, and means we
        // have to put extra '?'s in ourself.
        if(data.charAt(data.length() - 1) == '?')
        {
            data = data.substring(0, data.length()-1);
        }

        // Now tokenize by whitespace
        StringTokenizer attrValuePairs = new StringTokenizer(pi.getNodeValue(), " \n\r\t");
        while (attrValuePairs.hasMoreTokens())
        {
            String currentAttrValuePair = attrValuePairs.nextToken();

            // deal with whitespace before the end quote of an attribute-value
            // pair
            if (currentAttrValuePair.charAt( currentAttrValuePair.length() - 1) != '"'
                && currentAttrValuePair.charAt( currentAttrValuePair.length() - 1) != '\''  )
            {
                SimpleStringBuffer buff = new SimpleStringBuffer(currentAttrValuePair);
                // Keep appending additional tokens until we get one that ends
                // in a quote
                do
                {
                    if(attrValuePairs.hasMoreTokens())
                    {
                        buff.append(" ");
                        buff.append(attrValuePairs.nextToken());
                    }
                    else
                    {
                        throw new SAXException("Error parsing processing instruction");
                    }
                } while (buff.charAt( buff.length() - 1) != '"'
                         && buff.charAt( buff.length() - 1) != '\'');

                // Copy our buffer back to the value
                currentAttrValuePair = buff.toString();
            } // end deal with whitespace

            // Now split attribute = value pair around the '='
            StringTokenizer st = new StringTokenizer(currentAttrValuePair,"=");
            // Make sure we have two tokens
            if(st.countTokens() < 2)
            {
                throw new SAXException("Error parsing processing instruction");
            }

            String nextToken = st.nextToken().trim();
            String nextValue = st.nextToken().trim();

            // Finally, strip off quotes from attr value
            nextValue = nextValue.substring(1,nextValue.length() - 1 );

            // Put attribute value pair into hashtable
            hashtable.put(nextToken,nextValue);

        }
        return hashtable;
    }

    /**
     * Convert the given String encoding of a boolean value
     * (<code>"true"</code> or <code>"false"</code>) to a <code>boolean</code>.
     * @param value String representation of a boolean value
     * @return <code>true</code> if the string is not <code>null</code> and
     * matches the string <code>"true"</code> in a case-insensitive comparison
     */
    public static boolean getBooleanValue(String value)
    {
        return value != null && value.equalsIgnoreCase("true");
    }

    /**
     * Given a version number string, get the integer corresponding to the
     * 'major version'. For example, calling this method on the string
     * <code>"3.2.1"</code> would return the integer <code>3</code>.
     * @param version a version number string, e.g. "3.2.1"
     * @return the integer 'major version' represented by the digits before the
     * first '.' in the string
     * @exception NumberFormatException if the string does not contain a valid
     * integer major version
     */
    public static int getVersionMajor(String version) {
        int i = version.indexOf('.');
        if (i != -1) {
            version = version.substring(0, i);
        }
        return Integer.parseInt(version);
    }


    /**
     *  Properly escapes text for inclusion in as XML attribute value.
     *  I.e. escapes &, <, >, ', and ".
     *
     *  @param text the String to escape
     *  @return the escaped text.
     */
    public static String escapeXMLAttribute(String text)
    {
        return escapeXML(text, true);
    }

    /**
     *  Properly escapes text for inclusion in as XML text.
     *  I.e. escapes &, <, and >.
     *
     *  @param text the String to escape
     *  @return the escaped text.
     */
    public static String escapeXMLText(String text)
    {
        return escapeXML(text, false);
    }

    /**
     *  General utility to escape text for inclusion in XML.
     *  I.e. escapes &, <,  and > if regular XML text or
     *  escapes &, <, >, ', and " if an XML attribute value.
     *
     *  @param text the String to escape
     *  @param isAttribute indicates if the text should be escaped for inclusion
     *      as an attributes value.  If <code>false</code> then regular text is assumed.
     *  @return the escaped text.
     */
    public static String escapeXML(String text, boolean isAttribute)
    {
        if (text == null) return ""; // return an empty string if no value

        // Build a new string, converting unsafe XML characters to their
        // equivalent entities. These are all defined by the XML specification.
        StringBuffer out = new StringBuffer(text.length());
        char[] in = text.toCharArray();
        String temp = null;
        boolean escapeCharFound = false;
        int charStart = 0;  // start of normal characters

        for (int i = 0; i < in.length; i++)
        {
            switch (in[i])
            {
            case '&':
                // this may be the leading & in a properly escaped XML character
                // check so we don't escape the escaped character.
                if (!alreadyEscaped(text, i))
                {
                    temp = escapedAmpersand;
                    escapeCharFound = true;
                }
                break;
            case '<':
                temp = escapedLessThan;
                escapeCharFound = true;
                break;
            case '>':
                temp = escapedGreaterThan;
                escapeCharFound = true;
                break;
            case '"':
                // only escape in an attribute value
                if (isAttribute)
                {
                    temp = escapedDoubleQuote;
                    escapeCharFound = true;
                }
                break;
            case '\'':
                // only escape in an attribute value
                if (isAttribute)
                {
                    temp = escapedSingleQuote;
                    escapeCharFound = true;
                }
                break;
            default:
                // we write the non-escape characters as ablock for performance
                break;
            }

            if (escapeCharFound)
            {
                // copy out normal characters first
                if (i > charStart)
                {
                    out.append(in, charStart, i - charStart);
                    charStart = i + 1;
                }
                out.append(temp);
                escapeCharFound = false;
            }
        }
        // write out final normal characters
        if (in.length > charStart)
            out.append(in, charStart, in.length - charStart);

        return out.toString();
    }

    private static boolean alreadyEscaped(String in, int pos)
    {
        if (in.startsWith(escapedAmpersand, pos))
            return true;
        else if (in.startsWith(escapedLessThan, pos))
            return true;
        else if (in.startsWith(escapedGreaterThan, pos))
            return true;
        else if (in.startsWith(escapedDoubleQuote, pos))
            return true;
        else if (in.startsWith(escapedSingleQuote, pos))
            return true;

        return false;
    }

    private static DOMParser getParser()
        throws IOException
    {
        return getParser(null);
    }

    private static DOMParser getParser(OutputStream errOut)
        throws IOException
    {
        // Get an instance of the parser
        DOMParser parser = new DOMParser();

        // Set various parser options: validation on,
        // warnings shown
        parser.setValidationMode(false);
        parser.showWarnings(true);

        // Set error output stream, if necessary
        if(errOut != null)
        {
            parser.setErrorStream(errOut);
        }

        return parser;
    }

    private static URL getXslURL(Hashtable pIAttributes) throws MalformedURLException
    {

        String value = (String)pIAttributes.get(STYLESHEET_TYPE_ATTRIBUTE);
        if (value != null && value.equalsIgnoreCase(XSL_STYLESHEET_TYPE))

        {
            value = (String)pIAttributes.get(STYLESHEET_HREF_ATTRIBUTE);
            if (value != null)

            {
                URL xslURL = new URL(value);
                return xslURL;
            }
        }
        return null;
    }

}
