/*
 * 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.struts2.sitemesh;

import com.opensymphony.module.sitemesh.HTMLPage;
import com.opensymphony.module.sitemesh.RequestConstants;
import com.opensymphony.xwork2.ActionContext;
import freemarker.core.InvalidReferenceException;
import freemarker.template.Configuration;
import freemarker.template.ObjectWrapper;
import freemarker.template.SimpleHash;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.StrutsException;
import org.apache.struts2.dispatcher.Dispatcher;
import org.apache.struts2.dispatcher.listener.StrutsListener;
import org.apache.struts2.views.freemarker.FreemarkerManager;
import org.apache.struts2.views.freemarker.ScopesHashModel;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Enumeration;
import java.util.Locale;

/**
 * <p>This is a SiteMesh FreeMarker view servlet.</p>
 *
 * <p>It overrides the SiteMesh servlet to rely on the
 * Freemarker Manager in Struts instead of creating it's
 * own manager</p>
 */
public class FreemarkerDecoratorServlet extends freemarker.ext.servlet.FreemarkerServlet {

    private static final Logger LOG = LogManager.getLogger(FreemarkerDecoratorServlet.class);
    public static final long serialVersionUID = -2440216393145762479L;

    protected FreemarkerManager freemarkerManager;
    protected String templatePath;
    protected boolean nocache;
    protected boolean debug;
    protected Configuration config;
    private ObjectWrapper wrapper;
    private String contentType;
    private boolean noCharsetInContentType;

    public void init() throws ServletException {
        try {
            Dispatcher dispatcher = Dispatcher.getInstance(getServletContext());
            if (dispatcher == null) {
                throw new IllegalStateException("Unable to find the Dispatcher in the Servlet Context. Is '" + StrutsListener.class.getName() + "' missing in web.xml?");
            }
            freemarkerManager = dispatcher.getContainer().getInstance(FreemarkerManager.class);
            config = createConfiguration();

            // Process object_wrapper init-param out of order:
            wrapper = config.getObjectWrapper();
            LOG.debug("Using object wrapper of class {}", wrapper.getClass().getName());

            // Process all other init-params:
            Enumeration initpnames = getServletConfig().getInitParameterNames();
            while (initpnames.hasMoreElements()) {
                String name = (String) initpnames.nextElement();
                String value = getInitParameter(name);
                if (name == null) {
                    throw new ServletException("init-param without param-name. Maybe the web.xml is not well-formed?");
                }
                if (value == null) {
                    throw new ServletException("init-param without param-value. Maybe the web.xml is not well-formed?");
                }

                // template path is already handled!
                if (!FreemarkerManager.INITPARAM_TEMPLATE_PATH.equals(name)) freemarkerManager.addSetting(name, value);
            }
            nocache = freemarkerManager.getNocache();
            debug = freemarkerManager.getDebug();
            contentType = freemarkerManager.getContentType();
            noCharsetInContentType = freemarkerManager.getNoCharsetInContentType();
        } catch (ServletException e) {
            throw e;
        } catch (Exception e) {
            throw new ServletException(e);
        }
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        process(request, response);
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        process(request, response);
    }

    private void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // Give chance to subclasses to perform preprocessing
        if (preprocessRequest(request, response)) {
            return;
        }

        String path = requestUrlToTemplatePath(request);

        if (debug) {
            log("Requested template: " + path);
        }

        Template template;
        try {
            template = config.getTemplate(path, deduceLocale(path, request, response));
        } catch (FileNotFoundException e) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        Object attrContentType = template.getCustomAttribute("content_type");
        if (attrContentType != null) {
            response.setContentType(attrContentType.toString());
        } else {
            if (noCharsetInContentType) {
                response.setContentType(contentType + "; charset=" + template.getEncoding());
            } else {
                response.setContentType(contentType);
            }
        }

        // Set cache policy
        setBrowserCachingPolicy(response);

        ServletContext servletContext = getServletContext();
        ScopesHashModel model = (ScopesHashModel) request.getAttribute(FreemarkerManager.ATTR_TEMPLATE_MODEL);
        try {
            if (model == null) {
                ActionContext ctx = ServletActionContext.getActionContext(request);
                if (ctx == null) {
                    throw new StrutsException("ActionContext is null! Freemarker accessed out of action?");
                }
                model = freemarkerManager.buildTemplateModel(ctx.getValueStack(), ctx.getActionInvocation().getAction(), servletContext, request, response, wrapper);
            }

            // Give subclasses a chance to hook into preprocessing
            if (preTemplateProcess(request, response, template, model)) {
                try {
                    // Process the template
                    template.process(model, response.getWriter());
                } finally {
                    // Give subclasses a chance to hook into postprocessing
                    postTemplateProcess(request, response, template, model);
                }
            }
        } catch (InvalidReferenceException x) {
            // this exception is thrown if there is an error processing a reference.  We want to report these!
            HttpServletRequest req = ActionContext.getContext().getServletRequest();
            String resultCode = ActionContext.getContext().getActionInvocation().getResultCode();
            if (req == null) {
                req = request;
            }

            StringBuilder msgBuf = new StringBuilder("Error applying freemarker template to\n       request: ");
            msgBuf.append(req.getRequestURL());
            if (req.getQueryString() != null) {
                msgBuf.append("?").append(req.getQueryString());
            }
            msgBuf.append(" with resultCode: ").append(resultCode).append(".\n\n").append(x.getMessage());
            String msg = msgBuf.toString();
            LOG.error(msg, x);

            ServletException e = new ServletException(msg, x);
            // Attempt to set init cause, but don't freak out if the method
            // is not available (i.e. pre-1.4 JRE). This is required as the
            // constructor-passed throwable won't show up automatically in
            // stack traces.
            try {
                e.getClass().getMethod("initCause", new Class[]{Throwable.class}).invoke(e, x);
            } catch (Exception ex) {
                // Can't set init cause, we're probably running on a pre-1.4
                // JDK, oh well...
            }
            throw e;
        } catch (TemplateException te) {
            if (config.getTemplateExceptionHandler().getClass().getName().contains("Debug")) {
                this.log("Error executing FreeMarker template", te);
            } else {
                ServletException e = new ServletException("Error executing FreeMarker template", te);
                // Attempt to set init cause, but don't freak out if the method
                // is not available (i.e. pre-1.4 JRE). This is required as the
                // constructor-passed throwable won't show up automatically in
                // stack traces.
                try {
                    e.getClass().getMethod("initCause", new Class[]{Throwable.class}).invoke(e, te);
                } catch (Exception ex) {
                    // Can't set init cause, we're probably running on a pre-1.4
                    // JDK, oh well...
                }
                throw e;
            }
        }
    }


    /**
     * Returns the locale used for the
     * {@link Configuration#getTemplate(String, Locale)} call.
     * The base implementation simply returns the locale setting of the
     * configuration. Override this method to provide different behaviour, i.e.
     * to use the locale indicated in the request.
     */
    protected Locale deduceLocale(String templatePath, HttpServletRequest request, HttpServletResponse response) {
        return config.getLocale();
    }


    /**
     * <p>
     * Create the instance of the freemarker Configuration object.
     * </p>
     *
     * <p>
     * this implementation
     * </p>
     *
     * <ul>
     * <li>obtains the default configuration from Configuration.getDefaultConfiguration()
     * <li>sets up template loading from a ClassTemplateLoader and a WebappTemplateLoader
     * <li>sets up the object wrapper to be the BeansWrapper
     * <li>loads settings from the classpath file /freemarker.properties
     * </ul>
     */
    protected freemarker.template.Configuration createConfiguration() {
        return freemarkerManager.getConfiguration(this.getServletContext());
    }

    /**
     * <p>
     * Called before the execution is passed to template.process().
     * This is a generic hook you might use in subclasses to perform a specific
     * action before the template is processed. By default does nothing.
     * A typical action to perform here is to inject application-specific
     * objects into the model root
     * </p>
     *
     * <p>Example: Expose the Serlvet context path as "baseDir" for all templates:
     * </p>
     *
     * <pre>
     *    ((SimpleHash) data).put("baseDir", request.getContextPath() + "/");
     *    return true;
     * </pre>
     *
     * @param request  the actual HTTP request
     * @param response the actual HTTP response
     * @param template the template that will get executed
     * @return true to process the template, false to suppress template processing.
     * @see freemarker.ext.servlet.FreemarkerServlet#preTemplateProcess(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, freemarker.template.Template, freemarker.template.TemplateModel)
     */
    protected boolean preTemplateProcess(HttpServletRequest request, HttpServletResponse response, Template template, TemplateModel templateModel) throws ServletException, IOException {
        boolean result = super.preTemplateProcess(request, response, template, templateModel);

        SimpleHash hash = (SimpleHash) templateModel;

        HTMLPage htmlPage = (HTMLPage) request.getAttribute(RequestConstants.PAGE);

        String title, body, head;

        if (htmlPage == null) {
            title = "No Title";
            body = "No Body";
            head = "<!-- No head -->";
        } else {
            title = htmlPage.getTitle();

            StringWriter buffer = new StringWriter();
            htmlPage.writeBody(buffer);
            body = buffer.toString();

            buffer = new StringWriter();
            htmlPage.writeHead(buffer);
            head = buffer.toString();

            hash.put("page", htmlPage);
        }

        hash.put("title", title);
        hash.put("body", body);
        hash.put("head", head);
        hash.put("base", request.getContextPath());

        /*
          Factory factory = Factory.getInstance(new Config(getServletConfig()));
          Decorator decorator = factory.getDecoratorMapper().getDecorator(request, htmlPage);
          -> decorator.getPage()
          */

        return result;
    }

    /**
     * If the parameter "nocache" was set to true, generate a set of headers
     * that will advise the HTTP client not to cache the returned page.
     */
    private void setBrowserCachingPolicy(HttpServletResponse res) {
        if (nocache) {
            // HTTP/1.1 + IE extensions
            res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, " + "post-check=0, pre-check=0");
            // HTTP/1.0
            res.setHeader("Pragma", "no-cache");
            // Last resort for those that ignore all of the above
            res.setHeader("Expires", FreemarkerManager.EXPIRATION_DATE);
        }
    }
}
