/*
 * This file is part of Nuts Framework.
 * Copyright(C) 2009-2012 Nuts Develop Team.
 *
 * Nuts Framework is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License any later version.
 *
 * Nuts Framework is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Nuts Framework. If not, see <http://www.gnu.org/licenses/>.
 */
package nuts.exts.struts2.sitemesh;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import nuts.core.lang.StringUtils;
import nuts.exts.sitemesh.ContentBufferingResponse;
import nuts.exts.sitemesh.HTMLPageContentProcessor;
import nuts.exts.sitemesh.PageResponseWrapper;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts2.views.freemarker.FreemarkerManager;

import com.opensymphony.module.sitemesh.Config;
import com.opensymphony.module.sitemesh.Factory;
import com.opensymphony.sitemesh.Content;
import com.opensymphony.sitemesh.ContentProcessor;
import com.opensymphony.sitemesh.DecoratorSelector;
import com.opensymphony.sitemesh.webapp.ContainerTweaks;
import com.opensymphony.sitemesh.webapp.SiteMeshWebAppContext;
import com.opensymphony.xwork2.inject.Inject;

/**
 * Applies FreeMarker-based sitemesh decorators. Remove query string from
 * request path. <!-- START SNIPPET: javadoc --> The following variables are
 * available to the decorating freemarker page :-
 * <ul>
 * <li>${title} - content of &lt;title&gt; tag in the decorated page</li>
 * <li>${head} - content of &lt;head&gt; tag in the decorated page</li>
 * <li>${body} - content of t&lt;body&gt; tag in the decorated page</li>
 * <li>${page.properties} - content of the page properties</li>
 * <li>${base} - request.getContentPath()</li>
 * <li>${page} - raw com.opensymphony.module.sitemesh.Page</li>
 * </ul>
 * <p/>
 * With the following decorated page :-
 * 
 * <pre>
 *  &lt;html&gt;
 *      &lt;meta name=&quot;author&quot; content=&quot;tm_jee&quot; /&gt;
 *      &lt;head&gt;
 *          &lt;title&gt;My Title&lt;/title&gt;
 *          &lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;mycss.css&quot; /&gt;
 *          &lt;style type=&quot;text/javascript&quot; language=&quot;javascript&quot; src=&quot;myjavascript.js&quot;&gt;&lt;/script&gt;
 *      &lt;/head&gt;
 *      &lt;body&lt;
 *          &lt;h1&gt;Sample&lt;/h1&gt;
 *      &lt;/body&gt;
 *  &lt;/html&gt;
 * </pre>
 * <p/>
 * <table border="1">
 * <tr>
 * <td>Properties</td>
 * <td>Content</td>
 * </tr>
 * <tr>
 * <td>${title}</td>
 * <td>My Title</td>
 * </tr>
 * <tr>
 * <td>${head}</td>
 * <td>&lt;link rel="stylesheet" type="text/css" href="mycss.css" /&gt;
 * &lt;style type="text/javascript" language="javascript"
 * src="myjavascript.js"&gt;&lt;/script&gt;</td>
 * </tr>
 * <tr>
 * <td>${body}</td>
 * <td>&lt;h1&gt;Sample&lt;/h1&gt;</td>
 * </tr>
 * <tr>
 * <td>${page.properties.meta.author}</td>
 * <td>tm_jee</td>
 * </tr>
 * </table>
 * <!-- END SNIPPET: javadoc -->
 */
public class FreeMarkerPageFilter implements Filter {
	private static final Log log = LogFactory.getLog(FreeMarkerPageFilter.class);

	private static final String ALREADY_APPLIED_KEY = FreeMarkerPageFilter.class.getName();

	private static FreemarkerManager freemarkerManager;

	private FilterConfig filterConfig;
	private ContainerTweaks containerTweaks;
	private String passParamName;
	private String passParamValue;
	
	/**
	 * @param mgr freemarkerManager
	 */
	@Inject
	public static void setFreemarkerManager(FreemarkerManager mgr) {
		freemarkerManager = mgr;
	}

	/**
	 * init
	 * 
	 * @param filterConfig filterConfig
	 */
	public void init(FilterConfig filterConfig) {
		this.filterConfig = filterConfig;
		passParamName = filterConfig.getInitParameter("passParamName");
		passParamValue = filterConfig.getInitParameter("passParamValue");
		containerTweaks = new ContainerTweaks();
	}

	/**
	 * destroy
	 */
	public void destroy() {
		filterConfig = null;
		containerTweaks = null;
	}

	/**
	 * Main method of the Filter.
	 * <p>
	 * Checks if the Filter has been applied this request. If not, parses the
	 * page and applies {@link com.opensymphony.module.sitemesh.Decorator} (if
	 * found).
	 */
	public void doFilter(ServletRequest req, ServletResponse res,
			FilterChain chain) throws IOException, ServletException {

		HttpServletRequest request = (HttpServletRequest)req;
		HttpServletResponse response = (HttpServletResponse)res;
		ServletContext servletContext = filterConfig.getServletContext();

		if (StringUtils.isNotEmpty(passParamName)) {
			String val = request.getParameter(passParamName);
			if (val != null 
					&& (passParamValue == null || passParamValue.equals(val))) { 
				if (log.isDebugEnabled()) {
					log.debug(request.getRequestURI() + " - bypass (passParameter: " + passParamName + ")");
				}
				chain.doFilter(request, response);
				return;
			}
		}
		
		SiteMeshWebAppContext webAppContext = new SiteMeshWebAppContext(
				request, response, servletContext);

		ContentProcessor contentProcessor = initContentProcessor(webAppContext);
		DecoratorSelector decoratorSelector = initDecoratorSelector(webAppContext);

		if (filterAlreadyAppliedForRequest(request)) {
			// Prior to Servlet 2.4 spec, it was unspecified whether the filter
			// should be called again upon an include().
			if (log.isDebugEnabled()) {
				log.debug(request.getRequestURI() + " - bypass (filterAlreadyAppliedForRequest)");
			}
			chain.doFilter(request, response);
			return;
		}

		if (!contentProcessor.handles(webAppContext)) {
			// Optimization: If the content doesn't need to be processed, bypass
			// SiteMesh.
			if (log.isDebugEnabled()) {
				log.debug(request.getRequestURI() + " - bypass (no need to process)");
			}
			chain.doFilter(request, response);
			return;
		}

		if (containerTweaks.shouldAutoCreateSession()) {
			// Some containers (such as Tomcat 4) will not allow sessions to be
			// created in the decorator.
			// (i.e after the response has been committed).
			request.getSession(true);
		}

		try {
			if (log.isDebugEnabled()) {
				log.debug(request.getRequestURI() + " - sitemesh...");
			}

			// Continue in filter-chain, writing all content to buffer
			ContentBufferingResponse contentBufferingResponse = new ContentBufferingResponse(
					response, contentProcessor, webAppContext);

			chain.doFilter(request, contentBufferingResponse);

			PageResponseWrapper pageResponseWrapper = contentBufferingResponse.getPageResponseWrapper();
			webAppContext.setUsingStream(pageResponseWrapper.isUsingStream());

			char[] data = pageResponseWrapper.getContents();
			if (data == null) {
				if (log.isDebugEnabled()) {
					log.debug(request.getRequestURI() + " - bypass (null contents)");
				}
				return;
			}

			Content content = contentProcessor.build(data, webAppContext);

			decoratorSelector.selectDecorator(content, webAppContext).render(
					content, webAppContext);

			if (log.isDebugEnabled()) {
				log.debug(request.getRequestURI() + " - sitemesh end.");
			}
		}
		catch (IllegalStateException e) {
			// Some containers (such as WebLogic) throw an IllegalStateException
			// when an error page is served.
			// It may be ok to ignore this. However, for safety it is propegated
			// if possible.
			if (!containerTweaks.shouldIgnoreIllegalStateExceptionOnErrorPage()) {
				throw e;
			}
		}
		catch (RuntimeException e) {
			if (containerTweaks.shouldLogUnhandledExceptions()) {
				// Some containers (such as Tomcat 4) swallow RuntimeExceptions
				// in filters.
				servletContext.log(
						"Unhandled exception occurred whilst decorating page",
						e);
			}
			throw e;
		}
		catch (ServletException e) {
			request.setAttribute(ALREADY_APPLIED_KEY, null);
			throw e;
		}
	}

	protected ContentProcessor initContentProcessor(
			SiteMeshWebAppContext webAppContext) {
		Factory factory = Factory.getInstance(new Config(filterConfig));
		factory.refresh();
		return new HTMLPageContentProcessor(factory);
	}

	protected DecoratorSelector initDecoratorSelector(
			SiteMeshWebAppContext webAppContext) {
		Factory factory = Factory.getInstance(new Config(filterConfig));
		factory.refresh();
		return new FreemarkerDecoratorSelector(freemarkerManager,
				factory.getDecoratorMapper());
	}

	private boolean filterAlreadyAppliedForRequest(HttpServletRequest request) {
		if (request.getAttribute(ALREADY_APPLIED_KEY) == Boolean.TRUE) {
			return true;
		}
		else {
			request.setAttribute(ALREADY_APPLIED_KEY, Boolean.TRUE);
			return false;
		}
	}
}
