/*
 * 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.bebop.page;

import com.arsdigita.developersupport.DeveloperSupport;
import com.arsdigita.dispatcher.DispatcherHelper;
import com.arsdigita.globalization.Globalization;
import com.arsdigita.kernel.Kernel;
import com.arsdigita.templating.Templating;
import com.arsdigita.templating.XSLTemplate;
import com.arsdigita.templating.PresentationManager;
import com.arsdigita.util.Assert;
import com.arsdigita.util.UncheckedWrapperException;
import com.arsdigita.web.CachePolicy;
import com.arsdigita.web.Debugger;
import com.arsdigita.web.TransformationDebugger;
import com.arsdigita.web.Web;
import com.arsdigita.xml.Document;
import com.arsdigita.bebop.Bebop;

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

import java.net.URL;

import java.text.SimpleDateFormat;

import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;
import java.util.Collection;
import java.util.Map.Entry;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.PageContext;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import com.arsdigita.templating.XSLParameterGenerator;

import org.apache.log4j.Logger;

/**
 * A class for managing and obtaining a Stylesheet based on the
 * current request's location in the site map.  First, we try to find
 * a stylesheet specific to this site node.  If we can't find one,
 * then we walk up the site map until we find a parent of this site
 * node that has a stylesheet associated with it.
 *
 * If we haven't found one by the time we reach the root, then we'll
 * do the same tree walk except we'll look for the stylesheet
 * associated with the <em>package</em> mounted on each site node.
 *
 * @author Bill Schneider
 * @version $Id: //core-platform/dev/src/com/arsdigita/bebop/page/PageTransformer.java#17 $
 */
public class PageTransformer implements PresentationManager {
    public static final String versionId =
        "$Id: //core-platform/dev/src/com/arsdigita/bebop/page/PageTransformer.java#17 $" +
        "$Author: dennis $" +
        "$DateTime: 2004/04/07 16:07:11 $";

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

    // this keeps track of all of the XSLParameters that can be added to
    // stylesheets
    private static final HashMap s_XSLParameters = new HashMap();


    // load the default xsl parameter generators
    static {
        registerXSLParameterGenerator
            ("contextPath",
             new XSLParameterGenerator() {
                 public String generateValue(HttpServletRequest request) {
                     return request.getContextPath();
                 }
             });

        registerXSLParameterGenerator
            ("root-context-prefix",
             new XSLParameterGenerator() {
                 public String generateValue(HttpServletRequest request) {
                     return Web.getConfig().getDispatcherContextPath();
                 }
             });

        registerXSLParameterGenerator
            ("context-prefix",
             new XSLParameterGenerator() {
                 public String generateValue(HttpServletRequest request) {
                     return Web.getContext().getRequestURL().getContextPath();
                 }
             });

        registerXSLParameterGenerator
            ("static-prefix",
             new XSLParameterGenerator() {
                 public String generateValue(HttpServletRequest request) {
                     return Web.getContext().getRequestURL().getContextPath() +
                         com.arsdigita.web.URL.STATIC_DIR;
                 }
             });

        registerXSLParameterGenerator
            ("dispatcher-prefix",
             new XSLParameterGenerator() {
                 public String generateValue(HttpServletRequest request) {
                     return com.arsdigita.web.URL.getDispatcherPath();
                 }
             });
    }

    // XXX These need to move somewhere else.

    /**
     *  This is used to indicate that all xsl templates used should
     *  be pulled from the disk and not from the cache.  If this
     *  is in the request with a value of Boolean.TRUE then
     *  all XSL Stylesheets are pulled from the disk, not the cache
     */
    public static final String CACHE_XSL_NONE = "cacheXSLNone";

    /**
     *  This is used to indicate that the "fancy errors" should be
     *  used and that the errors should be placed in the request
     *  for later use.  To use this, have the code place an attribute
     *  with this name in the request with the value of Boolean.TRUE
     */
    public static final String FANCY_ERRORS = "fancyErrors";


    /**
     * State flag for preventing caching in every case.
     */
    public static final String CACHE_NONE = "none";

    /**
     * State flag for per-user caching.
     */
    public static final String CACHE_USER = "user";

    /**
     * State flag for enabling caching in every case.
     */
    public static final String CACHE_WORLD = "world";

    /**
     * State flag for disabling HTTP header caching.
     */
    public static final String CACHE_DISABLE = "disable";

    private static String s_defaultCachePolicy = CACHE_DISABLE;

    /**
     * Sets the default cache behavior for the site.
     *
     * @param policy a <code>String</code> policy, one of {@link
     * #CACHE_NONE}, {@link #CACHE_DISABLE}, {@link #CACHE_USER},
     * {@link #CACHE_WORLD}
     */
    public static void setDefaultCachePolicy(final String policy) {
        s_defaultCachePolicy = policy;
    }

    /**
     * Sets the content type of the response and then gets the PrintWriter
     */
    private PrintWriter getWriter(final HttpServletResponse resp,
                                  final String contentType,
                                  final String charset) {
        Assert.exists(contentType);
        Assert.exists(charset);

        resp.setContentType(contentType + "; " + "charset=" + charset);

        try {
            return resp.getWriter();
        } catch (IllegalStateException e) {
            s_log.warn("Using getOutputStream instead of getWriter");

            try {
                return new PrintWriter
                    (new OutputStreamWriter(resp.getOutputStream(),
                                            charset));
            } catch (IOException ex) {
                throw new UncheckedWrapperException(ex);
            }
        } catch (IOException ex) {
            throw new UncheckedWrapperException(ex);
        }
    }

    /**
     * Uses {@link #servePage(Document, HttpServletRequest,
     * HttpServletResponse, Map)} to implement the
     * <code>PresentationManager</code> interface.
     */
    public void servePage(final Document doc,
                          final HttpServletRequest req,
                          final HttpServletResponse resp) {
        servePage(doc, req, resp, null);
    }

    /**
     * Serves an XML Document, getting and applying the appropriate
     * XSLT.  Also allows for parameters to be set for the
     * transformer.  These will become top-level xsl:params in the
     * stylesheet.  The "contextPath" parameter will always be passed
     * to XSLT, which is the value of
     * <code>req.getContextPath()</code>.
     *
     * @param doc the Bebop page to serve
     * @param req the servlet request
     * @param resp the servlet response
     * @param params a set of name-value pairs to pass as parameters
     * to the Transformer
     */
    public void servePage(final Document doc,
                          final HttpServletRequest req,
                          final HttpServletResponse resp,
                          final Map params) {
        if (Assert.isEnabled()) {
            Assert.exists(doc, Document.class);
            Assert.exists(req, HttpServletRequest.class);
            Assert.exists(resp, HttpServletResponse.class);
        }


        final String charset = Globalization.getDefaultCharset
            (Kernel.getContext().getLocale());

        final String output = req.getParameter("output");
        s_log.info("output=" + output);

        if (output == null) {
            DeveloperSupport.startStage("PresMgr get stylesheet");

            boolean fancyErrors = Bebop.getConfig().wantFancyXSLErrors() ||
                Boolean.TRUE.equals(req.getAttribute(FANCY_ERRORS));

            // Get the stylesheet transformer object corresponding to the
            // current request.
            final XSLTemplate template = 
                Templating.getTemplate
                (req, fancyErrors,
                 !Boolean.TRUE.equals(req.getAttribute(CACHE_XSL_NONE)));

            DeveloperSupport.endStage("PresMgr get stylesheet");

            DeveloperSupport.startStage("PresMgr transform");

            final CachePolicy policy = Web.getConfig().getCachePolicy();

            if (policy != null) {
                policy.implement(req, resp);
            }

            final PrintWriter writer = getWriter(resp, "text/html", charset);

            final Transformer xf = template.newTransformer();
            endTransaction(req);

            // Transformers are not thread-safe, so we assume we have exclusive
            // use of xf here.  But we could recycle it.
            xf.clearParameters();

            if (params != null) {
                final Iterator entries = params.entrySet().iterator();

                while (entries.hasNext()) {
                    final Map.Entry entry = (Map.Entry) entries.next();

                    xf.setParameter
                        ((String) entry.getKey(), entry.getValue());
                }
            }

            addXSLParameters(xf, req);

            // This has no effect on the resulting encoding of the
            // output generated by the XSLT transformer. Why?
            // Because we pass the transformer an instance of the
            // Writer class. The Writer class provides no methods
            // for changing the encoding. So, the only thing this
            // does is, it causes the transformer to include a
            // line like <meta http-equiv="Content-Type"
            // content="text/html; charset=foo"> in the output
            // document.
            xf.setOutputProperty("encoding", charset);

            try {
                xf.transform(new DOMSource(doc.getInternalDocument()),
                             new StreamResult(writer));
            } catch (TransformerException ex) {
                throw new UncheckedWrapperException("cannot transform document", ex);
            }

            DeveloperSupport.endStage("PresMgr transform");

            // copy and paste from BasePresentationManager
            if (Kernel.getConfig().isDebugEnabled()) {
                Document origDoc = (Document) req.getAttribute
                    ("com.arsdigita.xml.Document");
                Debugger.addDebugger
                    (new TransformationDebugger(template.getSource(),
                                                template.getDependents()));
                writer.print(Debugger.getDebugging(req));
            }

        } else if (output.equals("xml")) {
            endTransaction(req);

            final PrintWriter writer = getWriter(resp, "text/xml", charset);

            DispatcherHelper.forceCacheDisable(resp);

            writer.println(doc.toString(true));
        } else if (output.equals("xsl")) {
            DeveloperSupport.startStage("PresMgr get stylesheet");
            
            XSLTemplate template = null;
            try {
                // Get the stylesheet transformer object corresponding to the
                // current request.
                template = 
                    Templating.getTemplate
                    (req, Boolean.TRUE.equals
                     (req.getAttribute(PageTransformer.FANCY_ERRORS)),
                     !Boolean.TRUE.equals
                     (req.getAttribute(PageTransformer.CACHE_XSL_NONE)));
                endTransaction(req);
            } finally {
                DeveloperSupport.endStage("PresMgr get stylesheet");
            }

            DeveloperSupport.startStage("PresMgr send XSL ZIP");

            try {
                Date now = new Date();
                SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd-HH-mm");
                String prefix = "waf-xsl-" + fmt.format(now);

                final OutputStream os = resp.getOutputStream();
                resp.reset();
                resp.setContentType("application/zip");
                resp.setHeader
                    ("Content-Disposition",
                     "attachment; filename=\"" + prefix + ".zip\"");
                DispatcherHelper.forceCacheDisable(resp);
                
                template.toZIP(os, prefix);
                
                resp.flushBuffer();
            } catch (IOException ex) {
                throw new UncheckedWrapperException(ex);
            } finally {
                DeveloperSupport.endStage("PresMgr send XSL ZIP");
            }
        } else {
            throw new IllegalStateException(output + " is an unknown output");
        }
    }


    /**
     * Ends the current transaction. Is a performance optimization to end ASAP before
     * serving the page.
     *
     * @param req HTTP request.
     */
    private void endTransaction(final HttpServletRequest req) {
        // There is no longer any need for a database handle.
        if (req.getAttribute(PageContext.EXCEPTION) == null) {
            Kernel.getContext().getTransaction().end();
        } else {
            Kernel.getContext().getTransaction().abort();
        }
    }

    /**
     * This adds a generator to the list of parameters available to
     * XSLStylesheets.  If this is called a second time with the same
     * parameter name then all previous calls are overwritten and
     * only the last registered generator is used.
     */
    public static void registerXSLParameterGenerator
        (String parameterName,
         XSLParameterGenerator parameterGenerator) {
        s_XSLParameters.put(parameterName, parameterGenerator);
    }

    /**
     *  This removes the parameter from the list of parameters that
     *  will be added to stylesheets
     */
    public static void removeXSLParameterGenerator(String parameterName) {
        s_XSLParameters.remove(parameterName);
    }

    /**
     *  This is a Collection of all names of XSL Parameters that have been
     *  registered
     */
    public static Collection getXSLParameterNames() {
        return s_XSLParameters.keySet();
    }


    /**
     *  This takes a name and request and returns the value that should
     *  be used in the XSL for the given name
     */
    public static String getXSLParameterValue(String name, 
                                              HttpServletRequest request) {
        XSLParameterGenerator generator = 
            (XSLParameterGenerator)s_XSLParameters.get(name);
        if (generator != null) {
            return generator.generateValue(request);
        } else {
            return null;
        }
    }

    /**
     *  This takes in a transformer and adds all of the registered
     *  xsl paraemters.  
     */
    public static void addXSLParameters(Transformer transformer, 
                                        HttpServletRequest request) {
        final Iterator entries = s_XSLParameters.entrySet().iterator();
        
        while (entries.hasNext()) {
            final Map.Entry entry = (Map.Entry) entries.next();

            String value = ((XSLParameterGenerator)entry.getValue())
                .generateValue(request);
            if (value == null) {
                // XSL does not like nulls
                value = "";
            }
            transformer.setParameter((String) entry.getKey(), value);
        }
    }    
}
