/*
 * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved.
 *
 * The contents of this file are subject to the CCM Public
 * License (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.redhat.com/licenses/ccmpl.html.
 *
 * Software distributed under the License is distributed on an
 * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express
 * or implied. See the License for the specific language
 * governing rights and limitations under the License.
 *
 */
package com.arsdigita.templating;

import com.arsdigita.util.Assert;
import com.arsdigita.util.IO;

import java.io.File;
import java.io.PrintWriter;
import java.io.OutputStream;
import java.io.IOException;

import java.net.URL;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.xml.transform.ErrorListener;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;

import org.w3c.dom.Document;

/**
 * A class for loading, caching and generally managing XSL templates
 * and transformers.
 *
 * @author Dan Berrange
 * @version $Id: //core-platform/dev/src/com/arsdigita/templating/XSLTemplate.java#14 $
 */
public final class XSLTemplate {
    public static final String versionId =
        "$Id: //core-platform/dev/src/com/arsdigita/templating/XSLTemplate.java#14 $" +
        "$Author: dennis $" +
        "$DateTime: 2004/04/07 16:07:11 $";

    private static final Logger s_log = Logger.getLogger(XSLTemplate.class);

    private final URL m_source;
    private final Templates m_templates;
    private final List m_dependents;
    private final Date m_created;

    /**
     * Creates and loads a new template from <code>source</code>,
     * using <code>listener</code> to handle any errors.
     *
     * @param source A <code>URL</code> pointing to the template
     * source text
     * @param listener A <code>ErrorListener</code> to customize
     * behavior on error
     */
    public XSLTemplate(final URL source,
                       final ErrorListener listener) {
        if (Assert.isEnabled()) {
            Assert.exists(source, URL.class);
            Assert.exists(listener, ErrorListener.class);
        }

        m_source = source;

        final SimpleURIResolver resolver = new SimpleURIResolver();

        try {
            s_log.debug("Getting new templates object");

            final TransformerFactory factory =
                TransformerFactory.newInstance();
            factory.setURIResolver(resolver);
            factory.setErrorListener(listener);

            m_templates = factory.newTemplates
                (resolver.resolve(m_source.toString(), null));

            s_log.debug("Done getting new templates");
        } catch (TransformerConfigurationException ex) {
            throw new WrappedTransformerException(ex);
        } catch (TransformerException ex) {
            throw new WrappedTransformerException(ex);
        }

        m_dependents = resolver.getStylesheetURIs();
        m_created = new Date();
    }

    /**
     * Creates and loads a new template from <code>source</code> using
     * the default <code>ErrorListener</code>.
     *
     * @param source A <code>URL</code> pointing to the template
     * source text
     */
    public XSLTemplate(final URL source) {
        this(source, new Log4JErrorListener());
    }

    /**
     * Gets the <code>URL</code> of the template source.
     *
     * @return The <code>URL</code> location of the template source;
     * it cannot be null
     */
    public final URL getSource() {
        return m_source;
    }

    /**
     * Gets a list of all dependent stylesheet files.
     *
     * @return A <code>List</code> of <code>URL</code>s to dependent
     * stylesheet files; it cannot be null
     */
    public final List getDependents() {
        return m_dependents;
    }

    /**
     * Generates a new <code>Transformer</code> from the internal
     * <code>Templates</code> object.
     *
     * @return The new <code>Transformer</code>; it cannot be null
     */
    public final synchronized Transformer newTransformer() {
        s_log.debug("Generating new transformer");

        try {
            return m_templates.newTransformer();
        } catch (TransformerConfigurationException tce) {
            throw new WrappedTransformerException(tce);
        }
    }

    /**
     * Transforms the <code>source</code> document and sends it to
     * <code>result</code>.  If there are errors,
     * <code>listener</code> handles them.  This method internally
     * creates and uses a new <code>Transformer</code>.
     *
     * @param source The <code>Source</code> to be transformed; it
     * cannot be null
     * @param result The <code>Result</code> to capture the
     * transformed product; it cannot be null
     * @param listener A <code>ErrorListener</code> to handle
     * transformation errors; it cannot be null
     */
    public final void transform(final Source source,
                                final Result result,
                                final ErrorListener listener) {
        if (s_log.isDebugEnabled()) {
            s_log.debug("Transforming " + source + " and sending it to " +
                        result + " using error listener " + listener);
        }

        if (Assert.isEnabled()) {
            Assert.exists(source, Source.class);
            Assert.exists(result, Result.class);
            Assert.exists(listener, ErrorListener.class);
        }

        try {
            final Transformer transformer = newTransformer();
            transformer.setErrorListener(listener);

            if (s_log.isDebugEnabled()) {
                s_log.debug("Transforming the XML source document");
            }

            transformer.transform(source, result);

            s_log.debug("Finished transforming");
        } catch (TransformerConfigurationException tce) {
            throw new WrappedTransformerException(tce);
        } catch (TransformerException te) {
            throw new WrappedTransformerException(te);
        }
    }

    /**
     * Transforms the <code>source</code> document and sends it to
     * <code>result</code>.  This method internally creates and uses a
     * new <code>Transformer</code>.
     *
     * @param source The <code>Source</code> to be transformed; it
     * cannot be null
     * @param result The <code>Result</code> to capture the
     * transformed product; it cannot be null
     */
    public final void transform(final Source source,
                                final Result result) {
        transform(source, result, new Log4JErrorListener());
    }

    /**
     * Transforms <code>doc</code> and streams the result to
     * <code>writer</code>.  If there are errors,
     * <code>listener</code> handles them.
     *
     * @param doc The <code>Document</code> to transform; it cannot be
     * null
     * @param writer The <code>PrintWriter</code> to receive the
     * transformed result; it cannot be null
     * @param listener A <code>ErrorListener</code> to handle any
     * errors; it cannot be null
     */
    public final void transform(final Document doc,
                                final PrintWriter writer,
                                final ErrorListener listener) {
        if (Assert.isEnabled()) {
            Assert.exists(doc, Document.class);
            Assert.exists(writer, PrintWriter.class);
            Assert.exists(listener, ErrorListener.class);
        }

        final DOMSource source = new DOMSource(doc);
        final StreamResult result = new StreamResult(writer);

        transform(source, result, listener);
    }

    /**
     * Transforms <code>doc</code> and streams the result to
     * <code>writer</code>.
     *
     * @param doc The <code>Document</code> to transform; it cannot be
     * null
     * @param writer The <code>PrintWriter</code> to receive the
     * transformed result; it cannot be null
     */
    public final void transform(final Document doc,
                                final PrintWriter writer) {
        transform(doc, writer, new Log4JErrorListener());
    }

    /**
     * Checks whether the XSL files associated with the template have
     * been modified.
     *
     * @return <code>true</code> if any dependent files have been
     * modified, otherwise <code>false</code>
     */
    public final boolean isModified() {
        if (s_log.isDebugEnabled()) {
            s_log.debug("Checking if the XSL files for " + this + " " +
                        "have been modified and need to be re-read");
        }

        final Iterator iter = m_dependents.iterator();

        while (iter.hasNext()) {
            final URL url = Templating.transformURL((URL)iter.next());
            Assert.exists(url, URL.class);

            if (url.getProtocol().equals("file")) {
                final File file = new File(url.getPath());

                if (file.lastModified() > m_created.getTime()) {
                    if (s_log.isInfoEnabled()) {
                        s_log.info("File " + file + " was modified " +
                                   file.lastModified());
                    }

                    return true;
                }
            } else {
                if (s_log.isDebugEnabled()) {
                    s_log.debug("The URL is not to a file; assuming " +
                                url + " is not modified");
                }
            }
        }

        s_log.debug("No files were modified");

        return false;
    }

    /**
     * Creates a ZIP file containing this stylesheet and
     * all dependant's. NB, this method assumes that all
     * stylesheets live in the same URL protocol. If the
     * protocol a file is different from the protocol
     * of the top level, then this file will be excluded
     * from the ZIP. In practice this limitation is not
     * critical, because XSL files should always use
     * relative imports, which implies all imported files
     * will be in the same URL space.
     * @param os the output stream to write the ZIP to
     * @param base the base directory in which the files will extract
     */
    public void toZIP(OutputStream os,
                      String base) 
        throws IOException {
        
        final ZipOutputStream zos = new ZipOutputStream(os);
        
        URL src = getSource();
        String srcProto = src.getProtocol();

        if (s_log.isDebugEnabled()) {
            s_log.debug("Outputting files for " + src);
        }

        final Iterator sheets = getDependents().iterator();
        while (sheets.hasNext()) {
            URL xsl = (URL)sheets.next();
            if (xsl.getProtocol().equals(srcProto)) {
                if (s_log.isDebugEnabled()) {
                    s_log.debug("Outputting file " + xsl);
                }
                String path = xsl.getPath();
                if (path.startsWith("/")) {
                    path = path.substring(1);
                }
                
                zos.putNextEntry(new ZipEntry(base + "/" + path));
                
                IO.copy(xsl.openStream(), zos);
            } else {
                s_log.warn("Not outputting file " + xsl + 
                           " because its not under protocol " + srcProto);
            }
        }
        zos.finish();
    }

    private static class Log4JErrorListener implements ErrorListener {
        public void warning(TransformerException e) throws TransformerException {
            log(Level.WARN, e);
        }

        public void error(TransformerException e) throws TransformerException {
            log(Level.ERROR, e);
        }

        public void fatalError(TransformerException e) throws TransformerException {
            log(Level.FATAL, e);
        }

        private static void log(Level level, TransformerException ex) {
            s_log.log(level, "Transformer " + level + ": " +
                      ex.getLocationAsString() + ": " + ex.getMessage(),
                      ex);
        }
    }
}
