/*
 *
 * Derby - Class org.apache.derbyTesting.functionTests.util.XML
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, 
 * software distributed under the License is distributed on an 
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 
 * either express or implied. See the License for the specific 
 * language governing permissions and limitations under the License.
 */
package org.apache.derbyTesting.junit;

import java.io.IOException;
import java.io.InputStreamReader;

import java.lang.reflect.Method;
import java.security.PrivilegedActionException;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

import junit.framework.Assert;

/**
 * <p>
 * XML utility methods for the JUnit tests.
 * </p>
 */
public class XML {

    /**
     * Determine whether or not the classpath with which we're
     * running has the JAXP API classes required for use of
     * the Derby XML operators.
     */
    private static final boolean HAVE_JAXP =
        JDBC.haveClass("org.w3c.dom.Document");

    /**
     * Determine whether or not the classpath with which we're
     * running has a JAXP implementation.
     */
    private static final boolean HAVE_JAXP_IMPL =
            HAVE_JAXP && checkJAXPImplementation();

    /**
     * Determine if we have support evaluating XPath queries.
     */
    private static final boolean HAVE_XPATH
            = HAVE_JAXP_IMPL && checkXPathSupport();

    /**
     * The filepath for the directory that holds the XML "helper" files
     * (i.e. the files to insert and their schema documents).
     */
    private static final String HELPER_FILE_LOCATION =
        "org/apache/derbyTesting/functionTests/tests/lang/xmlTestFiles/";

    /**
     * Return true if the classpath contains JAXP and
     * an implementation of the JAXP interfaces, for example the
     * Xalan classes (this method doesn't care about
     * support for XPath queries).
     */
    public static boolean classpathHasJAXP()
    {
        return HAVE_JAXP_IMPL;
    }

    /**
     * Return true if the classpath meets all of the requirements
     * for use of the SQL/XML operators.
     */
    public static boolean classpathMeetsXMLReqs()
    {
        return HAVE_XPATH;
    }

    /**
     * Insert the contents of a file into the received column of
     * the received table using "setCharacterStream".  Expectation
     * is that the file is in the directory indicated by 
     * HELPER_FILE_LOCATION.
     *
     * @param conn Connection on which to perform the insert.
     * @param tableName Table into which we want to insert.
     * @param colName Column in tableName into which we want to insert.
     * @param fName Name of the file whose content we want to insert.
     * @param numRows Number of times we should insert the received
     *  file's content.
     */
    public static void insertFile(Connection conn, String tableName,
        String colName, String fName, int numRows)
        throws IOException, SQLException, PrivilegedActionException
    {
        // First we have to figure out many chars long the file is.

        fName = HELPER_FILE_LOCATION + fName;
        java.net.URL xFile = BaseTestCase.getTestResource(fName);
        Assert.assertNotNull("XML input file missing: " + fName, xFile);
        
        int charCount = 0;
        char [] cA = new char[1024];
        InputStreamReader reader =
            new InputStreamReader(BaseTestCase.openTestResource(xFile));

        for (int len = reader.read(cA, 0, cA.length); len != -1;
            charCount += len, len = reader.read(cA, 0, cA.length));

        reader.close();

        // Now that we know the number of characters, we can insert
        // using a stream.

        PreparedStatement pSt = conn.prepareStatement(
            "insert into " + tableName + "(" + colName + ") values " +
            "(xmlparse(document cast (? as clob) preserve whitespace))");

        for (int i = 0; i < numRows; i++)
        {
            reader = new InputStreamReader(
                BaseTestCase.openTestResource(xFile));

            pSt.setCharacterStream(1, reader, charCount);
            pSt.execute();
            reader.close();
        }

        pSt.close();
    }

    /**
     * Insert an XML document into the received column of the received
     * test table using setString.  This method parallels "insertFiles"
     * above, except that it should be used for documents that require
     * a Document Type Definition (DTD).  In that case the location of
     * the DTD has to be modified _within_ the document so that it can
     * be found in the running user directory.
     *
     * Expectation is that the file to be inserted is in the directory
     * indicated by HELPER_FILE_LOCATION and that the DTD file has been
     * copied to the user's running directory (via use of the util
     * methods in SupportFilesSetup).
     *
     * @param conn Connection on which to perform the insert.
     * @param tableName Table into which we want to insert.
     * @param colName Column in tableName into which we want to insert.
     * @param fName Name of the file whose content we want to insert.
     * @param dtdName Name of the DTD file that the received file uses.
     * @param numRows Number of times we should insert the received
     *  file's content.
     */
    public static void insertDocWithDTD(Connection conn, String tableName,
        String colName, String fName, String dtdName, int numRows)
        throws IOException, SQLException, PrivilegedActionException
    {
        // Read the file into memory so we can update it.
        fName = HELPER_FILE_LOCATION + fName;
        java.net.URL xFile = BaseTestCase.getTestResource(fName);
        Assert.assertNotNull("XML input file missing: " + fName, xFile);

        int charCount = 0;
        char [] cA = new char[1024];
        StringBuffer sBuf = new StringBuffer();
        InputStreamReader reader =
            new InputStreamReader(BaseTestCase.openTestResource(xFile));

        for (int len = reader.read(cA, 0, cA.length); len != -1;
            charCount += len, len = reader.read(cA, 0, cA.length))
        {
            sBuf.append(cA, 0, len);
        }

        reader.close();

        // Now replace the DTD location.

        java.net.URL dtdURL = SupportFilesSetup.getReadOnlyURL(dtdName);
        Assert.assertNotNull("DTD file missing: " + dtdName, dtdURL);

        String docAsString = sBuf.toString();
        int pos = docAsString.indexOf(dtdName);
        if (pos != -1)
            sBuf.replace(pos, pos+dtdName.length(), dtdURL.toExternalForm());

        // Now (finally) do the insert using the in-memory document with
        // the correct DTD location.
        docAsString = sBuf.toString();
        PreparedStatement pSt = conn.prepareStatement(
            "insert into " + tableName + "(" + colName + ") values " +
            "(xmlparse(document cast (? as clob) preserve whitespace))");

        for (int i = 0; i < numRows; i++)
        {
            pSt.setString(1, docAsString);
            pSt.execute();
        }

        pSt.close();
    }

    /**
     * <p>
     * Determine whether or not the classpath with which we're
     * running contains a JAXP implementation that supports
     * evaluating XPath queries.
     * </p>
     *
     * <p>
     * Assumption is that we only get to this method if we already
     * know that there *is* an implementation of JAXP in the classpath.
     * </p>
     */
    private static boolean checkXPathSupport()
    {
        try {
            Class<?> factoryClass =
                    Class.forName("javax.xml.xpath.XPathFactory");

            Method newFactory =
                    factoryClass.getMethod("newInstance");

            Object factory = newFactory.invoke(null);

            return factory != null;

        } catch (Throwable t) {
            // If something went wrong, assume we don't have the
            // necessary classes.
            return false;
        }
    }

    private static boolean checkJAXPImplementation() {
        try {
            Class<?> factoryClass =
                    Class.forName("javax.xml.parsers.DocumentBuilderFactory");
            Method newFactory =
                    factoryClass.getMethod("newInstance", new Class[0]);
            Object factory = newFactory.invoke(null, new Object[0]);
            return factory != null;
        } catch (Throwable t) {
            return false;
        }
    }

    /**
     * Return the string form of the URL for the jar file that contains
     * whichever JAXP parser implementation is picked up from the user's
     * classpath.  If the JAXP parser is not in the user's classpath,
     * then it must be embedded within the JVM (either implicitly or else
     * through use of "endorsed standards" jars), in which case we return
     * null.
     */
    protected static String getJAXPParserLocation()
    {
        /* If the classpath does not have JAXP then we do not want to
         * instantiate the JAXPFinder class (which happens indirectly
         * if we call its static methods).  This is because JAXPFinder
         * references a JAXP class that does not exist for J2ME, so
         * if we try to call a method on JAXPFinder without a JAXP
         * parser in the classpath, the result for J2ME would be
         * be a NoClassDefFound error (DERBY-2153).
         */
        if (!classpathHasJAXP())
            return null;

        try {
            Class<?> jaxpFinderClass = Class.forName("org.apache.derbyTesting.junit.JAXPFinder");
            Method locatorMethod = jaxpFinderClass.getDeclaredMethod("getJAXPParserLocation");

            return (String) locatorMethod.invoke(null);
        }
        catch (Exception e)
        {
            throw new UnsupportedOperationException( e.getClass().getName() + ": " + e.getMessage() );
        }
    }
}
