/*
 * org.osjava.jardiff.ant.JDTraXLiaison
 *
 * $Id: IOThread.java 1952 2005-08-28 18:03:41Z cybertiger $
 * $URL: https://svn.osjava.org/svn/osjava/trunk/osjava-nio/src/java/org/osjava/nio/IOThread.java $
 * $Rev: 1952 $
 * $Date: 2005-08-28 18:03:41 +0000 (Sun, 28 Aug 2005) $
 * $Author: cybertiger $
 *
 * Copyright (c) 2005, Antony Riley
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * + Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 *
 * + Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * + Neither the name JarDiff nor the names of its contributors may
 *   be used to endorse or promote products derived from this software without
 *   specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package org.osjava.jardiff.ant;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Vector;
import java.util.Enumeration;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.ErrorListener;
import javax.xml.transform.Source;
import javax.xml.transform.SourceLocator;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.URIResolver;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.transform.TransformerConfigurationException;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.XSLTLiaison2;
import org.apache.tools.ant.taskdefs.XSLTProcess;
import org.apache.tools.ant.taskdefs.XSLTLogger;
import org.apache.tools.ant.taskdefs.XSLTLoggerAware;
import org.apache.tools.ant.types.XMLCatalog;
import org.apache.tools.ant.util.JAXPUtils;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

/* JarDiff modifications of Ant class anotated with // JARDIFF */

/**
 * Custom TraX liaison class.
 *
 * This allows loading a stylesheet from a systemid (URL).
 */
public class JDTraXLiaison implements JDXSLTLiaison3, ErrorListener, 
       XSLTLoggerAware 
{

    /**
     * The systemid of the stylesheet (a URL).
     */
    private String systemid; // JARDIFF: Added.

    /**
     * Create a new JDTraXLiaison.
     */
    // JARDIFF: Modified constructor name.
    public JDTraXLiaison() throws Exception {
    }

    /**
     * the name of the factory implementation class to use
     * or null for default JAXP lookup.
     */
    private String factoryName = null;

    /** The trax TransformerFactory */
    private TransformerFactory tfactory = null;

    /** stylesheet to use for transformation */
    private File stylesheet;

    private XSLTLogger logger;

    /** possible resolver for publicIds */
    private EntityResolver entityResolver;

    /** transformer to use for processing files */
    private Transformer transformer;

    /** The In memory version of the stylesheet */
    private Templates templates;

    /**
     * The modification time of the stylesheet from which the templates
     * are read
     */
    private long templatesModTime;

    /** possible resolver for URIs */
    private URIResolver uriResolver;

    /** transformer output properties */
    private Vector outputProperties = new Vector();

    /** stylesheet parameters */
    private Vector params = new Vector();

    /** factory attributes */
    private Vector attributes = new Vector();

    // JARDIFF: Added Method.
    public void setStylesheet(String systemid) {
        if(this.systemid != null) {
            transformer = null;
            // Can't check mod time on URLs
            if(this.systemid.equals(systemid)) {
                templates = null;
            }
        }
        this.systemid = systemid;
        this.stylesheet = null;
    }

    public void setStylesheet(File stylesheet) throws Exception {
        if (this.stylesheet != null) {
            // resetting the stylesheet - reset transformer
            transformer = null;

            // do we need to reset templates as well
            if (!this.stylesheet.equals(stylesheet)
                || (stylesheet.lastModified() != templatesModTime)) {
                templates = null;
            }
        }
        this.stylesheet = stylesheet;
        // JARDIFF: Added next line.
        this.systemid = stylesheet.toURL().toString();
    }

    public void transform(File infile, File outfile) throws Exception {
        if (transformer == null) {
            createTransformer();
        }

        InputStream fis = null;
        OutputStream fos = null;
        try {
            fis = new BufferedInputStream(new FileInputStream(infile));
            fos = new BufferedOutputStream(new FileOutputStream(outfile));
            StreamResult res = new StreamResult(fos);
            // not sure what could be the need of this...
            res.setSystemId(JAXPUtils.getSystemId(outfile));
            Source src = getSource(fis, infile);
            transformer.transform(src, res);
        } finally {
            // make sure to close all handles, otherwise the garbage
            // collector will close them...whenever possible and
            // Windows may complain about not being able to delete files.
            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException ignored) {
                // ignore
            }
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException ignored) {
                // ignore
            }
        }
    }

    /**
     * Get the source instance from the stream and id of the file.
     * @param is the stream containing the stylesheet data.
     * @param infile the file that will be used for the systemid.
     * @return the configured source instance matching the stylesheet.
     * @throws Exception if there is a problem creating the source.
     */
    // JARDIFF: Added for compatability.
    private Source getSource(InputStream is, File infile)
        throws ParserConfigurationException, SAXException {
        return getSource(is, JAXPUtils.getSystemId(infile));
    }

    // JARDIFF: Modified method parameters
    private Source getSource(InputStream is, String systemid)
        throws ParserConfigurationException, SAXException {
        // todo: is this comment still relevant ??
        // FIXME: need to use a SAXSource as the source for the transform
        // so we can plug in our own entity resolver
        Source src = null;
        if (entityResolver != null) {
            if (getFactory().getFeature(SAXSource.FEATURE)) {
                SAXParserFactory spFactory = SAXParserFactory.newInstance();
                spFactory.setNamespaceAware(true);
                XMLReader reader = spFactory.newSAXParser().getXMLReader();
                reader.setEntityResolver(entityResolver);
                src = new SAXSource(reader, new InputSource(is));
            } else {
                throw new IllegalStateException("xcatalog specified, but "
                    + "parser doesn't support SAX");
            }
        } else {
            // WARN: Don't use the StreamSource(File) ctor. It won't work with
            // xalan prior to 2.2 because of systemid bugs.
            src = new StreamSource(is);
        }
        // JARDIFF: Modified (moved to overloaded method variant).
        src.setSystemId(systemid);
        return src;
    }

    /**
     * Read in templates from the stylesheet
     */
    private void readTemplates()
        throws IOException, TransformerConfigurationException,
               ParserConfigurationException, SAXException {

        // Use a stream so that you can close it yourself quickly
        // and avoid keeping the handle until the object is garbaged.
        // (always keep control), otherwise you won't be able to delete
        // the file quickly on windows.
        InputStream xslStream = null;
        try {
            // JARDIFF: Largely modified.
            if(stylesheet != null) {
                xslStream
                    = new BufferedInputStream(new FileInputStream(stylesheet));
                templatesModTime = stylesheet.lastModified();
            } else {
                xslStream = 
                    new URL(systemid).openStream();
                templatesModTime = 0L;
            }
            Source src = getSource(xslStream, systemid);
            templates = getFactory().newTemplates(src);
        } finally {
            if(xslStream != null) {
                xslStream.close();
            }
        }
    }

    /**
     * Create a new transformer based on the liaison settings
     * @return the newly created and configured transformer.
     * @throws Exception thrown if there is an error during creation.
     * @see #setStylesheet(java.io.File)
     * @see #addParam(java.lang.String, java.lang.String)
     * @see #setOutputProperty(java.lang.String, java.lang.String)
     */
    private void createTransformer() throws Exception {
        if (templates == null) {
            readTemplates();
        }

        transformer = templates.newTransformer();

        // configure the transformer...
        transformer.setErrorListener(this);
        if (uriResolver != null) {
            transformer.setURIResolver(uriResolver);
        }
        for (int i = 0; i < params.size(); i++) {
            final String[] pair = (String[]) params.elementAt(i);
            transformer.setParameter(pair[0], pair[1]);
        }
        for (int i = 0; i < outputProperties.size(); i++) {
            final String[] pair = (String[]) outputProperties.elementAt(i);
            transformer.setOutputProperty(pair[0], pair[1]);
        }
    }

    /**
     * return the Transformer factory associated to this liaison.
     * @return the Transformer factory associated to this liaison.
     * @throws BuildException thrown if there is a problem creating
     * the factory.
     * @see #setFactory(String)
     * @since Ant 1.5.2
     */
    private TransformerFactory getFactory() throws BuildException {
        if (tfactory != null) {
            return tfactory;
        }
        // not initialized yet, so create the factory
        if (factoryName == null) {
            tfactory = TransformerFactory.newInstance();
        } else {
            try {
                Class clazz = Class.forName(factoryName);
                tfactory = (TransformerFactory) clazz.newInstance();
            } catch (Exception e) {
                throw new BuildException(e);
            }
        }
        tfactory.setErrorListener(this);

        // specific attributes for the transformer
        for (int i = 0; i < attributes.size(); i++) {
            final Object[] pair = (Object[]) attributes.elementAt(i);
            tfactory.setAttribute((String) pair[0], pair[1]);
        }

        if (uriResolver != null) {
            tfactory.setURIResolver(uriResolver);
        }
        return tfactory;
    }


    /**
     * Set the factory name to use instead of JAXP default lookup.
     * @param name the fully qualified class name of the factory to use
     * or null for the default JAXP look up mechanism.
     * @since Ant 1.6
     */
    public void setFactory(String name) {
        factoryName = name;
    }

    /**
     * Set a custom attribute for the JAXP factory implementation.
     * @param name the attribute name.
     * @param value the value of the attribute, usually a boolean
     * string or object.
     * @since Ant 1.6
     */
    public void setAttribute(String name, Object value) {
        final Object[] pair = new Object[]{name, value};
        attributes.addElement(pair);
    }

    /**
     * Set the output property for the current transformer.
     * Note that the stylesheet must be set prior to calling
     * this method.
     * @param name the output property name.
     * @param value the output property value.
     * @since Ant 1.5
     * @since Ant 1.5
     */
    public void setOutputProperty(String name, String value) {
        final String[] pair = new String[]{name, value};
        outputProperties.addElement(pair);
    }

    /** Set the class to resolve entities during the transformation
     */
    public void setEntityResolver(EntityResolver aResolver) {
        entityResolver = aResolver;
    }

    /** Set the class to resolve URIs during the transformation
     */
    public void setURIResolver(URIResolver aResolver) {
        uriResolver = aResolver;
    }

    public void addParam(String name, String value) {
        final String[] pair = new String[]{name, value};
        params.addElement(pair);
    }

    public void setLogger(XSLTLogger l) {
        logger = l;
    }

    public void error(TransformerException e) {
        logError(e, "Error");
    }

    public void fatalError(TransformerException e) {
        logError(e, "Fatal Error");
        throw new BuildException("Fatal error during transformation", e);
    }

    public void warning(TransformerException e) {
        logError(e, "Warning");
    }

    private void logError(TransformerException e, String type) {
        if (logger == null) {
            return;
        }

        StringBuffer msg = new StringBuffer();
        SourceLocator locator = e.getLocator();
        if (locator != null) {
            String systemid = locator.getSystemId();
            if (systemid != null) {
                String url = systemid;
                if (url.startsWith("file:///")) {
                    url = url.substring(8);
                }
                msg.append(url);
            } else {
                msg.append("Unknown file");
            }
            int line = locator.getLineNumber();
            if (line != -1) {
                msg.append(":" + line);
                int column = locator.getColumnNumber();
                if (column != -1) {
                    msg.append(":" + column);
                }
            }
        }
        msg.append(": " + type + "! ");
        msg.append(e.getMessage());
        if (e.getCause() != null) {
            msg.append(" Cause: " + e.getCause());
        }

        logger.log(msg.toString());
    }

    // kept for backwards compatibility
    /**
     * @deprecated use org.apache.tools.ant.util.JAXPUtils#getSystemId instead
     */
    protected String getSystemId(File file) {
        return JAXPUtils.getSystemId(file);
    }


    /**
     * Specific configuration for the TRaX liaison.
     * @param xsltTask the XSLTProcess task instance from which this liasion
     *        is to be configured.
     */
    public void configure(XSLTProcess xsltTask) {
        XSLTProcess.Factory factory = xsltTask.getFactory();
        if (factory != null) {
            setFactory(factory.getName());

            // configure factory attributes
            for (Enumeration attrs = factory.getAttributes();
                    attrs.hasMoreElements();) {
                XSLTProcess.Factory.Attribute attr =
                        (XSLTProcess.Factory.Attribute) attrs.nextElement();
                setAttribute(attr.getName(), attr.getValue());
            }
        }

        XMLCatalog xmlCatalog = xsltTask.getXMLCatalog();
        // use XMLCatalog as the entity resolver and URI resolver
        if (xmlCatalog != null) {
            setEntityResolver(xmlCatalog);
            setURIResolver(xmlCatalog);
        }


        // configure output properties
        for (Enumeration props = xsltTask.getOutputProperties();
                props.hasMoreElements();) {
            XSLTProcess.OutputProperty prop
                = (XSLTProcess.OutputProperty) props.nextElement();
            setOutputProperty(prop.getName(), prop.getValue());
        }
    }

    /**
     * Specific configuration for the TRaX liaison.
     * @param xsltTask the XSLTProcess task instance from which this liasion
     *        is to be configured.
     */
    // JARDIFF: Added
    public void configure(JDXSLTProcess xsltTask) {
        JDXSLTProcess.Factory factory = xsltTask.getFactory();
        if (factory != null) {
            setFactory(factory.getName());

            // configure factory attributes
            for (Enumeration attrs = factory.getAttributes();
                    attrs.hasMoreElements();) {
                JDXSLTProcess.Factory.Attribute attr =
                        (JDXSLTProcess.Factory.Attribute) attrs.nextElement();
                setAttribute(attr.getName(), attr.getValue());
            }
        }

        XMLCatalog xmlCatalog = xsltTask.getXMLCatalog();
        // use XMLCatalog as the entity resolver and URI resolver
        if (xmlCatalog != null) {
            setEntityResolver(xmlCatalog);
            setURIResolver(xmlCatalog);
        }


        // configure output properties
        for (Enumeration props = xsltTask.getOutputProperties();
                props.hasMoreElements();) {
            JDXSLTProcess.OutputProperty prop
                = (JDXSLTProcess.OutputProperty) props.nextElement();
            setOutputProperty(prop.getName(), prop.getValue());
        }
    }
}
