/**
 * License Agreement.
 *
 * Rich Faces - Natural Ajax for Java Server Faces (JSF)
 *
 * Copyright (C) 2007 Exadel, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1 as published by the Free Software Foundation.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
 */

package org.ajax4jsf.webapp;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.Iterator;
import java.util.Map;

import javax.faces.application.ViewExpiredException;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.ajax4jsf.Messages;
import org.ajax4jsf.application.AjaxViewHandler;
import org.ajax4jsf.context.AjaxContext;
import org.ajax4jsf.context.ContextInitParameters;
import org.ajax4jsf.renderkit.AjaxContainerRenderer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Node;

public abstract class BaseXMLFilter {
	public static final String AJAX_EXPIRED = "Ajax-Expired";

	private static final Log log = LogFactory.getLog(BaseXMLFilter.class);

	public static final String APPLICATION_SCOPE_KEY = BaseXMLFilter.class.getName();
	
	private String mimetype = "text/xml";

	private String publicid = "-//W3C//DTD XHTML 1.0 Transitional//EN";

	private String systemid = "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";

	private String namespace = "http://www.w3.org/1999/xhtml";

	private static final String MIME_TYPE_PARAMETER = "mime-type";

	private static final String PUBLICID_PARAMETER = "publicid";

	private static final String SYSTEMID_PARAMETER = "systemid";

	private static final String NAMESPACE_PARAMETER = "namespace";

	private boolean forcexml = false;

	private boolean forceNotRf = true;
	
	private static final String FORCEXML_PARAMETER = "forceparser";

	private static final String FORCENOTRF_PARAMETER = "forcenotrf";

	private static final String INIT_PARAMETER_PREFIX = "org.ajax4jsf.xmlfilter.";

	public static final String TEXT_HTML = "text/html";

	public static final String APPLICATION_XHTML_XML = "application/xhtml+xml";
	
	private boolean handleViewExpiredOnClient=false;
	
	public BaseFilter filter;

	public void setFilter(BaseFilter filter) {
		this.filter = filter;
	}
	
	public void init(FilterConfig config) throws ServletException {
		if (log.isDebugEnabled()) {
			log.debug("init XML filter service with class "
					+ this.getClass().getName());
		}
		String forceXmlParameter = config.getInitParameter(FORCEXML_PARAMETER);
		if (forceXmlParameter == null) {
			forceXmlParameter = config.getServletContext().getInitParameter(
					INIT_PARAMETER_PREFIX + FORCEXML_PARAMETER);
		}
		setupForceXml(forceXmlParameter);

		String forceNotRfParameter = config.getInitParameter(FORCENOTRF_PARAMETER);
		if (forceNotRfParameter == null) {
			forceNotRfParameter = config.getServletContext().getInitParameter(
					INIT_PARAMETER_PREFIX + FORCENOTRF_PARAMETER);
		}
		setupForcenotrf(forceNotRfParameter);

		setMimetype((String) nz(config.getInitParameter(MIME_TYPE_PARAMETER),
				"text/xml"));
		setPublicid((String) nz(config.getInitParameter(PUBLICID_PARAMETER),
				getPublicid()));
		setSystemid((String) nz(config.getInitParameter(SYSTEMID_PARAMETER),
				getSystemid()));
		setNamespace((String) nz(config.getInitParameter(NAMESPACE_PARAMETER),
				getNamespace()));
		handleViewExpiredOnClient = Boolean.parseBoolean(config.getServletContext().getInitParameter(ContextInitParameters.HANDLE_VIEW_EXPIRED_ON_CLIENT));
		
	}

	private Boolean stringToBoolean(String s) {
		if ("false".equalsIgnoreCase(s)) {
			return Boolean.FALSE;
		} else if ("true".equalsIgnoreCase(s)) {
			return Boolean.TRUE;
		}
		
		return null;
	}
	
	private void setupForcenotrf(String paramValue) {
		Boolean val = stringToBoolean(paramValue);
		if (val != null) {
			this.forceNotRf = val.booleanValue();
		}
	}
	
	/**
	 * @param forceXmlParameter
	 */
	private void setupForceXml(String forceXmlParameter) {
		Boolean val = stringToBoolean(forceXmlParameter);
		if (val != null) {
			this.forcexml = val.booleanValue();
		}
	}

	/**
	 * Perform filter chain with xml parsing and transformation. Subclasses must
	 * implement concrete HTML to XML parsing, nesseasary transformations and
	 * serialization.
	 * 
	 * @param chain
	 * @param httpServletRequest
	 * @param httpServletResponse
	 * @throws ServletException
	 * @throws IOException
	 */
	protected void doXmlFilter(FilterChain chain, HttpServletRequest request,
			final HttpServletResponse response) throws IOException,
			ServletException {
		if (log.isDebugEnabled()) {
			log.debug("XML filter service start processing request");
		}
		FilterServletResponseWrapper servletResponseWrapper = getWrapper(response);
		// HACK - to avoid MyFaces <f:view> incompabilites and bypass
		// intermediaty filters
		// in chain, self-rendered region write directly to wrapper stored in
		// request-scope attribute.
		try {
			request.setAttribute(BaseFilter.RESPONSE_WRAPPER_ATTRIBUTE,
					servletResponseWrapper);
			chain.doFilter(request, servletResponseWrapper);

		} catch (ServletException e) {
			log.error("Exception in the filter chain", e);
			if(handleViewExpiredOnClient && isViewExpired(e) && isAjaxRequest(request)){
				Writer output = resetResponse(response, servletResponseWrapper, "true");
				String message = Messages
				.getMessage(Messages.AJAX_VIEW_EXPIRED);
				response.setHeader(AJAX_EXPIRED, message);
				output.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
						+ "<html xmlns=\"http://www.w3.org/1999/xhtml\"><head>"
						+ "<meta name=\""
						+ AjaxContainerRenderer.AJAX_FLAG_HEADER
						+ "\" content=\"true\" />" + "<meta name=\""
						+ AJAX_EXPIRED
						+ "\" content=\"" + message + "\" />"
						+ "</head></html>");
				output.flush();
				response.flushBuffer();
				return;
			} else {
			throw e;
			}
		} finally {
			request.removeAttribute(BaseFilter.RESPONSE_WRAPPER_ATTRIBUTE);
		}
		String viewId = (String) request
				.getAttribute(AjaxViewHandler.VIEW_ID_KEY);
		Node[] headEvents = (Node[]) request
				.getAttribute(AjaxContext.HEAD_EVENTS_PARAMETER);
	
		HtmlParser parser = null;
		// setup response
		// Redirect in AJAX request - convert to special response recognized by
		// client.
		String redirectLocation = servletResponseWrapper.getRedirectLocation();
		String characterEncoding = servletResponseWrapper
				.getCharacterEncoding();
		Writer output;
		if (null != redirectLocation) {
			if (isAjaxRequest(request)) {
				// Special handling of redirect - client-side script must
				// Check for response and perform redirect by window.location
				if (log.isDebugEnabled()) {
					log.debug("Create AJAX redirect response to url: "
							+ redirectLocation);
				}
				output = resetResponse(response, servletResponseWrapper,
						"redirect");
				response.setHeader(AjaxContainerRenderer.AJAX_LOCATION_HEADER,
						redirectLocation);
				// For buggy XmlHttpRequest realisations repeat headers in
				// <meta>
				output.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
						+ "<html xmlns=\"http://www.w3.org/1999/xhtml\"><head>"
						+ "<meta name=\""
						+ AjaxContainerRenderer.AJAX_FLAG_HEADER
						+ "\" content=\"redirect\" />" + "<meta name=\""
						+ AjaxContainerRenderer.AJAX_LOCATION_HEADER
						+ "\" content=\"" + redirectLocation + "\" />"
						+ "</head></html>");
				output.flush();
				response.flushBuffer();
			} else {
				response.sendRedirect(redirectLocation);
			}
			
		
			return;
		
		} else {
			if ("true".equals(servletResponseWrapper.getHeaders().get(
				AjaxContainerRenderer.AJAX_FLAG_HEADER))) {
				if (log.isDebugEnabled()) {
					log
							.debug("Process response to well-formed XML for AJAX XMLHttpRequest parser");
				}
				// Not caching AJAX request
				response.setHeader("Cache-Control",
						"no-cache, must-revalidate, max_age=0, no-store");
				response.setHeader("Expires", "0");
				response.setHeader("Pragma", "no-cache");
				// response.setCharacterEncoding(servletResponseWrapper
				// .getCharacterEncoding()); //
				// JSContentHandler.DEFAULT_ENCODING);
				// Set the content-type. For AJAX responses default encoding -
				// UTF8.
				// TODO - for null encoding, setup only Output encoding for
				// filter ?
				String outputEncoding = "UTF-8";
				String contentType = getMimetype() + ";charset=" + outputEncoding;
				response.setContentType(contentType);
				parser = getParser(getMimetype(), true, viewId);
				if (null == parser) {
					throw new ServletException(Messages.getMessage(
							Messages.PARSER_NOT_INSTANTIATED_ERROR, contentType));
				}
				output = createOutputWriter(response, outputEncoding);
				parser.setDoctype(getPublicid());
				parser.setInputEncoding(characterEncoding);
				parser.setOutputEncoding(outputEncoding);
				parser.setViewState((String) request
						.getAttribute(AjaxViewHandler.SERIALIZED_STATE_KEY));
			} else {
				// setup conversion reules for output contentType, send directly
				// if content not
				// supported by tidy.
				String contentType = servletResponseWrapper.getContentType();
				String contentTypeCharset = contentType;
				if (log.isDebugEnabled()) {
					log.debug("create HTML/XML parser for content type: "
							+ contentType);
				}
				// if(contentType == null){
				// contentType = request.getContentType();
				// }
				boolean forcenotrf = isForcenotrf();
				
				if (forcenotrf || !servletResponseWrapper.isError()) {
					if (forcenotrf || (headEvents != null && headEvents.length != 0)) {
						if (contentTypeCharset != null) {
							if (contentTypeCharset.indexOf("charset") < 0
									&& null != characterEncoding) {
							    contentTypeCharset += ";charset=" + characterEncoding;
							}
							parser = getParser(contentTypeCharset, false, viewId);
							if (null == parser) {
								if (log.isDebugEnabled()) {
									log
											.debug("Parser not have support for the such content type, send response as-is");
								}
							}
						}
					} else {
						if (log.isDebugEnabled()) {
							log.debug("No resource inclusions detected, send response as-is");
						}
					}
				} else {
					if (log.isDebugEnabled()) {
						log.debug("Servlet error occured, send response as-is");
					}
				}
				
				
				// null or unsupported content type
				if (null == parser) {
					try {
						if (servletResponseWrapper.isUseWriter()) {
							if (contentTypeCharset != null) {
							    response.setContentType(contentTypeCharset);
							}
	
							output = createOutputWriter(response,
									characterEncoding);
							servletResponseWrapper.sendContent(output);
						} else if (servletResponseWrapper.isUseStream()) {
							if (contentType != null) {
							    response.setContentType(contentType);
							}
	
							ServletOutputStream out = response.getOutputStream();
							servletResponseWrapper.sendContent(out);
						}
					} finally {
						// reuseWrapper(servletResponseWrapper);
					}
					
					
					return;
				}
	
				if (contentTypeCharset != null) {
				    response.setContentType(contentTypeCharset);
				}
				
				output = createOutputWriter(response, characterEncoding);
	
				parser.setInputEncoding(characterEncoding);
				parser.setOutputEncoding(characterEncoding);
			}
		}
		
		try {
			// Setup scripts and styles
			parser.setHeadNodes(headEvents);
			// Process parsing.
			long startTimeMills = System.currentTimeMillis();
			servletResponseWrapper.parseContent(output, parser);
			if (log.isDebugEnabled()) {
				startTimeMills = System.currentTimeMillis() - startTimeMills;
				log.debug(Messages.getMessage(Messages.PARSING_TIME_INFO, ""
						+ startTimeMills));
			}
		} catch (Exception e) {
			throw new ServletException(Messages
					.getMessage(Messages.JTIDY_PARSING_ERROR), e);
		} finally {
			reuseParser(parser);
		}
	}

	/**
	 * @param response
	 * @param servletResponseWrapper
	 * @param ajaxResponseType
	 * @return
	 * @throws IOException
	 * @throws UnsupportedEncodingException
	 */
	private Writer resetResponse(final HttpServletResponse response,
			FilterServletResponseWrapper servletResponseWrapper,
			String ajaxResponseType) throws IOException,
			UnsupportedEncodingException {
		Writer output;
		response.reset();
		// Keep cookies.
		for (Iterator<Cookie> iter = servletResponseWrapper.getCookies()
				.iterator(); iter.hasNext();) {
			Cookie cookie = (Cookie) iter.next();
			response.addCookie(cookie);
		}
		// Copy response headers
		Map<String, Object> headers = servletResponseWrapper.getHeaders();
		for (Iterator<Map.Entry<String, Object>> iter = headers.entrySet().iterator(); iter
				.hasNext();) {
			Map.Entry<String, Object> header = iter.next();
			response.setHeader((String) header.getKey(),
					(String) header.getValue());
		}
		response.setHeader(AjaxContainerRenderer.AJAX_FLAG_HEADER,
				ajaxResponseType);
		// Not caching AJAX request
		response.setHeader("Cache-Control",
				"no-cache, must-revalidate, max_age=0, no-store");
		response.setHeader("Expires", "0");
		response.setHeader("Pragma", "no-cache");
		response.setContentType(getMimetype() + ";charset=UTF-8");
		output = createOutputWriter(response, "UTF-8");
		return output;
	}

	/**
	 * Check for a {@link ViewExpiredException} in the exception chain.
	 * @param e exception from filter chain
	 * @return true if any exception in the chain instance of the {@link ViewExpiredException}
	 */
	private boolean isViewExpired(Throwable e) {
		while (null != e) {
			if (e instanceof ViewExpiredException) {
				return true;
			} else {
				e = e.getCause();
			}
		}
		return false;
	}

	/**
	 * @param response
	 * @return
	 * @throws ServletException
	 */
	protected FilterServletResponseWrapper getWrapper(
			HttpServletResponse response) throws ServletException {
		return new FilterServletResponseWrapper(response);
	}

	/**
	 * @param request
	 * @return
	 */
	protected boolean isAjaxRequest(ServletRequest request) {
		try {
			return null != request
					.getParameter(AjaxContainerRenderer.AJAX_PARAMETER_NAME);
		} catch (Exception e) {
			// OCJ 10 - throw exception for static resources.
			return false;
		}
	}

	/**
	 * @param response
	 * @param characterEncoding
	 * @return
	 * @throws IOException
	 * @throws UnsupportedEncodingException
	 */
	private Writer createOutputWriter(final HttpServletResponse response,
			String characterEncoding) throws IOException,
			UnsupportedEncodingException {
		Writer output;
		try {
			output = response.getWriter();
		} catch (IllegalStateException e) {
			if (null != characterEncoding) {
				output = new OutputStreamWriter(response.getOutputStream(),
						characterEncoding);
			} else {
				output = new OutputStreamWriter(response.getOutputStream());
			}
		}
		return output;
	}

	protected abstract void reuseParser(HtmlParser parser);

	protected abstract HtmlParser getParser(String mimetype, boolean isAjax,
			String viewId);

	/**
	 * @param publicid
	 *            The publicid to set.
	 */
	protected void setPublicid(String publicid) {
		this.publicid = publicid;
	}

	/**
	 * @return Returns the publicid.
	 */
	public String getPublicid() {
		return publicid;
	}

	/**
	 * @param systemid
	 *            The systemid to set.
	 */
	protected void setSystemid(String systemid) {
		this.systemid = systemid;
	}

	/**
	 * @return Returns the systemid.
	 */
	public String getSystemid() {
		return systemid;
	}

	/**
	 * @param namespace
	 *            The namespace to set.
	 */
	protected void setNamespace(String namespace) {
		this.namespace = namespace;
	}

	/**
	 * @return Returns the namespace.
	 */
	public String getNamespace() {
		return namespace;
	}

	/**
	 * @param mimetype
	 *            The mimetype to set.
	 */
	protected void setMimetype(String mimetype) {
		this.mimetype = mimetype;
	}

	/**
	 * @return Returns the mimetype.
	 */
	public String getMimetype() {
		return mimetype;
	}
	
	/**
	 * @return Returns the forcexml.
	 */
	public boolean isForcexml() {
		return this.forcexml;
	}

	public boolean isForcenotrf() {
		return this.forceNotRf;
	}
	
	/**
	 * @param forcexml
	 *            The forcexml to set.
	 */
	protected void setForcexml(boolean forcexml) {
		this.forcexml = forcexml;
	}
	
	protected void setForcenotrf(boolean forcenotrf) {
		this.forceNotRf = forcenotrf;
	}

	private Object nz(Object param, Object def) {
		return param != null ? param : def;
	}

}
