/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
 *
 * Licensed 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.acegisecurity.context;

import java.io.IOException;
import java.lang.reflect.Method;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;

/**
 * Populates the {@link SecurityContextHolder} with information obtained from
 * the <code>HttpSession</code>.
 * <p/>
 * <p/>
 * The <code>HttpSession</code> will be queried to retrieve the
 * <code>SecurityContext</code> that should be stored against the
 * <code>SecurityContextHolder</code> for the duration of the web request. At
 * the end of the web request, any updates made to the
 * <code>SecurityContextHolder</code> will be persisted back to the
 * <code>HttpSession</code> by this filter.
 * </p>
 * <p/>
 * If a valid <code>SecurityContext</code> cannot be obtained from the
 * <code>HttpSession</code> for whatever reason, a fresh
 * <code>SecurityContext</code> will be created and used instead. The created
 * object will be of the instance defined by the {@link #setContext(Class)}
 * method (which defaults to {@link org.acegisecurity.context.SecurityContextImpl}.
 * </p>
 * <p/>
 * No <code>HttpSession</code> will be created by this filter if one does not
 * already exist. If at the end of the web request the <code>HttpSession</code>
 * does not exist, a <code>HttpSession</code> will <b>only</b> be created if
 * the current contents of the <code>SecurityContextHolder</code> are not
 * {@link java.lang.Object#equals(java.lang.Object)} to a <code>new</code>
 * instance of {@link #setContext(Class)}. This avoids needless
 * <code>HttpSession</code> creation, but automates the storage of changes
 * made to the <code>SecurityContextHolder</code>. There is one exception to
 * this rule, that is if the {@link #forceEagerSessionCreation} property is
 * <code>true</code>, in which case sessions will always be created
 * irrespective of normal session-minimisation logic (the default is
 * <code>false</code>, as this is resource intensive and not recommended).
 * </p>
 * <p/>
 * This filter will only execute once per request, to resolve servlet container
 * (specifically Weblogic) incompatibilities.
 * </p>
 * <p/>
 * If for whatever reason no <code>HttpSession</code> should <b>ever</b> be
 * created (eg this filter is only being used with Basic authentication or
 * similar clients that will never present the same <code>jsessionid</code>
 * etc), the {@link #setAllowSessionCreation(boolean)} should be set to
 * <code>false</code>. Only do this if you really need to conserve server
 * memory and ensure all classes using the <code>SecurityContextHolder</code>
 * are designed to have no persistence of the <code>SecurityContext</code>
 * between web requests. Please note that if {@link #forceEagerSessionCreation}
 * is <code>true</code>, the <code>allowSessionCreation</code> must also be
 * <code>true</code> (setting it to <code>false</code> will cause a startup
 * time error).
 * </p>
 * <p/>
 * This filter MUST be executed BEFORE any authentication processing mechanisms.
 * Authentication processing mechanisms (eg BASIC, CAS processing filters etc)
 * expect the <code>SecurityContextHolder</code> to contain a valid
 * <code>SecurityContext</code> by the time they execute.
 * </p>
 *
 * @author Ben Alex
 * @author Patrick Burleson
 * @author Luke Taylor
 * @author Martin Algesten
 *
 * @version $Id: HttpSessionContextIntegrationFilter.java 2004 2007-09-01 14:43:09Z raykrueger $
 */
public class HttpSessionContextIntegrationFilter implements InitializingBean, Filter {
    //~ Static fields/initializers =====================================================================================

    protected static final Log logger = LogFactory.getLog(HttpSessionContextIntegrationFilter.class);

    static final String FILTER_APPLIED = "__acegi_session_integration_filter_applied";

    public static final String ACEGI_SECURITY_CONTEXT_KEY = "ACEGI_SECURITY_CONTEXT";

    //~ Instance fields ================================================================================================

    private Class context = SecurityContextImpl.class;

    private Object contextObject;

    /**
     * Indicates if this filter can create a <code>HttpSession</code> if
     * needed (sessions are always created sparingly, but setting this value to
     * <code>false</code> will prohibit sessions from ever being created).
     * Defaults to <code>true</code>. Do not set to <code>false</code> if
     * you are have set {@link #forceEagerSessionCreation} to <code>true</code>,
     * as the properties would be in conflict.
     */
    private boolean allowSessionCreation = true;

    /**
     * Indicates if this filter is required to create a <code>HttpSession</code>
     * for every request before proceeding through the filter chain, even if the
     * <code>HttpSession</code> would not ordinarily have been created. By
     * default this is <code>false</code>, which is entirely appropriate for
     * most circumstances as you do not want a <code>HttpSession</code>
     * created unless the filter actually needs one. It is envisaged the main
     * situation in which this property would be set to <code>true</code> is
     * if using other filters that depend on a <code>HttpSession</code>
     * already existing, such as those which need to obtain a session ID. This
     * is only required in specialised cases, so leave it set to
     * <code>false</code> unless you have an actual requirement and are
     * conscious of the session creation overhead.
     */
    private boolean forceEagerSessionCreation = false;

    /**
     * Indicates whether the <code>SecurityContext</code> will be cloned from
     * the <code>HttpSession</code>. The default is to simply reference (ie
     * the default is <code>false</code>). The default may cause issues if
     * concurrent threads need to have a different security identity from other
     * threads being concurrently processed that share the same
     * <code>HttpSession</code>. In most normal environments this does not
     * represent an issue, as changes to the security identity in one thread is
     * allowed to affect the security identitiy in other threads associated with
     * the same <code>HttpSession</code>. For unusual cases where this is not
     * permitted, change this value to <code>true</code> and ensure the
     * {@link #context} is set to a <code>SecurityContext</code> that
     * implements {@link Cloneable} and overrides the <code>clone()</code>
     * method.
     */
    private boolean cloneFromHttpSession = false;

    public boolean isCloneFromHttpSession() {
        return cloneFromHttpSession;
    }

    public void setCloneFromHttpSession(boolean cloneFromHttpSession) {
        this.cloneFromHttpSession = cloneFromHttpSession;
    }

    public HttpSessionContextIntegrationFilter() throws ServletException {
        this.contextObject = generateNewContext();
    }

    //~ Methods ========================================================================================================

    public void afterPropertiesSet() throws Exception {
        if ((this.context == null) || (!SecurityContext.class.isAssignableFrom(this.context))) {
            throw new IllegalArgumentException("context must be defined and implement SecurityContext "
                    + "(typically use org.acegisecurity.context.SecurityContextImpl; existing class is " + this.context
                    + ")");
        }

        if (forceEagerSessionCreation && !allowSessionCreation) {
            throw new IllegalArgumentException(
                    "If using forceEagerSessionCreation, you must set allowSessionCreation to also be true");
        }
    }

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
            ServletException {

        Assert.isInstanceOf(HttpServletRequest.class, req, "ServletRequest must be an instance of HttpServletRequest");
        Assert.isInstanceOf(HttpServletResponse.class, res, "ServletResponse must be an instance of HttpServletResponse");

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        if (request.getAttribute(FILTER_APPLIED) != null) {
            // ensure that filter is only applied once per request
            chain.doFilter(request, response);

            return;
        }

        HttpSession httpSession = null;

        try {
            httpSession = request.getSession(forceEagerSessionCreation);
        }
        catch (IllegalStateException ignored) {
        }

        boolean httpSessionExistedAtStartOfRequest = httpSession != null;

        SecurityContext contextBeforeChainExecution = readSecurityContextFromSession(httpSession);

        // Make the HttpSession null, as we don't want to keep a reference to it lying
        // around in case chain.doFilter() invalidates it.
        httpSession = null;

        if (contextBeforeChainExecution == null) {
            contextBeforeChainExecution = generateNewContext();

            if (logger.isDebugEnabled()) {
                logger.debug("New SecurityContext instance will be associated with SecurityContextHolder");
            }
        } else {
            if (logger.isDebugEnabled()) {
                logger.debug("Obtained a valid SecurityContext from ACEGI_SECURITY_CONTEXT to "
                        + "associate with SecurityContextHolder: '" + contextBeforeChainExecution + "'");
            }
        }

        int contextHashBeforeChainExecution = contextBeforeChainExecution.hashCode();
        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);

        // Create a wrapper that will eagerly update the session with the security context
        // if anything in the chain does a sendError() or sendRedirect().
        // See SEC-398

        OnRedirectUpdateSessionResponseWrapper responseWrapper =
                new OnRedirectUpdateSessionResponseWrapper( response, request,
                    httpSessionExistedAtStartOfRequest, contextHashBeforeChainExecution );

        // Proceed with chain

        try {
            // This is the only place in this class where SecurityContextHolder.setContext() is called
            SecurityContextHolder.setContext(contextBeforeChainExecution);

            chain.doFilter(request, responseWrapper);
        }
        finally {
            // This is the only place in this class where SecurityContextHolder.getContext() is called
            SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();

            // Crucial removal of SecurityContextHolder contents - do this before anything else.
            SecurityContextHolder.clearContext();

            request.removeAttribute(FILTER_APPLIED);

            // storeSecurityContextInSession() might already be called by the response wrapper
            // if something in the chain called sendError() or sendRedirect(). This ensures we only call it
            // once per request.
            if ( !responseWrapper.isSessionUpdateDone() ) {
              storeSecurityContextInSession(contextAfterChainExecution, request,
                      httpSessionExistedAtStartOfRequest, contextHashBeforeChainExecution);
            }

            if (logger.isDebugEnabled()) {
                logger.debug("SecurityContextHolder now cleared, as request processing completed");
            }
        }
    }

    /**
     * Gets the security context from the session (if available) and returns it.
     * <p/>
     * If the session is null, the context object is null or the context object stored in the session
     * is not an instance of SecurityContext it will return null.
     * <p/>
     * If <tt>cloneFromHttpSession</tt> is set to true, it will attempt to clone the context object
     * and return the cloned instance.
     *
     * @param httpSession the session obtained from the request.
     */
    private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
        if (httpSession == null) {
            if (logger.isDebugEnabled()) {
                logger.debug("No HttpSession currently exists");
            }

            return null;
        }

        // Session exists, so try to obtain a context from it.

        Object contextFromSessionObject = httpSession.getAttribute(ACEGI_SECURITY_CONTEXT_KEY);

        if (contextFromSessionObject == null) {
            if (logger.isDebugEnabled()) {
                logger.debug("HttpSession returned null object for ACEGI_SECURITY_CONTEXT");
            }

            return null;
        }

        // We now have the security context object from the session.

        // Clone if required (see SEC-356)
        if (cloneFromHttpSession) {
            Assert.isInstanceOf(Cloneable.class, contextFromSessionObject,
                    "Context must implement Clonable and provide a Object.clone() method");
            try {
                Method m = contextFromSessionObject.getClass().getMethod("clone", new Class[]{});
                if (!m.isAccessible()) {
                    m.setAccessible(true);
                }
                contextFromSessionObject = m.invoke(contextFromSessionObject, new Object[]{});
            }
            catch (Exception ex) {
                ReflectionUtils.handleReflectionException(ex);
            }
        }

        if (!(contextFromSessionObject instanceof SecurityContext)) {
            if (logger.isWarnEnabled()) {
                logger.warn("ACEGI_SECURITY_CONTEXT did not contain a SecurityContext but contained: '"
                        + contextFromSessionObject
                        + "'; are you improperly modifying the HttpSession directly "
                        + "(you should always use SecurityContextHolder) or using the HttpSession attribute "
                        + "reserved for this class?");
            }

            return null;
        }

        // Everything OK. The only non-null return from this method.

        return (SecurityContext) contextFromSessionObject;
    }

    /**
     * Stores the supplied security context in the session (if available) and if it has changed since it was
     * set at the start of the request.
     *
     * @param securityContext the context object obtained from the SecurityContextHolder after the request has
     *        been processed by the filter chain. SecurityContextHolder.getContext() cannot be used to obtain
     *        the context as it has already been cleared by the time this method is called.
     * @param request the request object (used to obtain the session, if one exists).
     * @param httpSessionExistedAtStartOfRequest indicates whether there was a session in place before the
     *        filter chain executed. If this is true, and the session is found to be null, this indicates that it was
     *        invalidated during the request and a new session will now be created.
     * @param contextHashBeforeChainExecution the hashcode of the context before the filter chain executed.
     *        The context will only be stored if it has a different hashcode, indicating that the context changed
     *        during the request.
     *
     */
    private void storeSecurityContextInSession(SecurityContext securityContext,
                                               HttpServletRequest request,
                                               boolean httpSessionExistedAtStartOfRequest,
                                               int contextHashBeforeChainExecution) {
        HttpSession httpSession = null;

        try {
            httpSession = request.getSession(false);
        }
        catch (IllegalStateException ignored) {
        }

        if (httpSession == null) {
            if (httpSessionExistedAtStartOfRequest) {
                if (logger.isDebugEnabled()) {
                    logger.debug("HttpSession is now null, but was not null at start of request; "
                            + "session was invalidated, so do not create a new session");
                }
            } else {
                // Generate a HttpSession only if we need to

                if (!allowSessionCreation) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("The HttpSession is currently null, and the "
                                        + "HttpSessionContextIntegrationFilter is prohibited from creating an HttpSession "
                                        + "(because the allowSessionCreation property is false) - SecurityContext thus not "
                                        + "stored for next request");
                    }
                } else if (!contextObject.equals(securityContext)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("HttpSession being created as SecurityContextHolder contents are non-default");
                    }

                    try {
                        httpSession = request.getSession(true);
                    }
                    catch (IllegalStateException ignored) {
                    }
                } else {
                    if (logger.isDebugEnabled()) {
                        logger.debug("HttpSession is null, but SecurityContextHolder has not changed from default: ' "
                                + securityContext
                                + "'; not creating HttpSession or storing SecurityContextHolder contents");
                    }
                }
            }
        }

        // If HttpSession exists, store current SecurityContextHolder contents but only if
        // the SecurityContext has actually changed (see JIRA SEC-37)
        if (httpSession != null && securityContext.hashCode() != contextHashBeforeChainExecution) {
            httpSession.setAttribute(ACEGI_SECURITY_CONTEXT_KEY, securityContext);

            if (logger.isDebugEnabled()) {
                logger.debug("SecurityContext stored to HttpSession: '" + securityContext + "'");
            }
        }
    }

    public SecurityContext generateNewContext() throws ServletException {
        try {
            return (SecurityContext) this.context.newInstance();
        }
        catch (InstantiationException ie) {
            throw new ServletException(ie);
        }
        catch (IllegalAccessException iae) {
            throw new ServletException(iae);
        }
    }

    /**
     * Does nothing. We use IoC container lifecycle services instead.
     *
     * @param filterConfig ignored
     * @throws ServletException ignored
     */
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    /**
     * Does nothing. We use IoC container lifecycle services instead.
     */
    public void destroy() {
    }

    public boolean isAllowSessionCreation() {
        return allowSessionCreation;
    }

    public void setAllowSessionCreation(boolean allowSessionCreation) {
        this.allowSessionCreation = allowSessionCreation;
    }

    public Class getContext() {
        return context;
    }

    public void setContext(Class secureContext) {
        this.context = secureContext;
    }

    public boolean isForceEagerSessionCreation() {
        return forceEagerSessionCreation;
    }

    public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
        this.forceEagerSessionCreation = forceEagerSessionCreation;
    }


    //~ Inner Classes ==================================================================================================    

    /**
     * Wrapper that is applied to every request to update the <code>HttpSession<code> with
     * the <code>SecurityContext</code> when a <code>sendError()</code> or <code>sendRedirect</code>
     * happens. See SEC-398. The class contains the fields needed to call
     * <code>storeSecurityContextInSession()</code>
     */
    private class OnRedirectUpdateSessionResponseWrapper extends HttpServletResponseWrapper {

        HttpServletRequest request;
        boolean httpSessionExistedAtStartOfRequest;
        int contextHashBeforeChainExecution;

        // Used to ensure storeSecurityContextInSession() is only
        // called once.
        boolean sessionUpdateDone = false;

        /**
         * Takes the parameters required to call <code>storeSecurityContextInSession()</code> in
         * addition to the response object we are wrapping.
         * @see HttpSessionContextIntegrationFilter#storeSecurityContextInSession(SecurityContext, ServletRequest, boolean, int)
         */
        public OnRedirectUpdateSessionResponseWrapper(HttpServletResponse response,
                                                      HttpServletRequest request,
                                                      boolean httpSessionExistedAtStartOfRequest,
                                                      int contextHashBeforeChainExecution) {
            super(response);
            this.request = request;
            this.httpSessionExistedAtStartOfRequest = httpSessionExistedAtStartOfRequest;
            this.contextHashBeforeChainExecution = contextHashBeforeChainExecution;
        }

        /**
         * Makes sure the session is updated before calling the
         * superclass <code>sendError()</code>
         */
        public void sendError(int sc) throws IOException {
            doSessionUpdate();
            super.sendError(sc);
        }

        /**
         * Makes sure the session is updated before calling the
         * superclass <code>sendError()</code>
         */
        public void sendError(int sc, String msg) throws IOException {
            doSessionUpdate();
            super.sendError(sc, msg);
        }

        /**
         * Makes sure the session is updated before calling the
         * superclass <code>sendRedirect()</code>
         */
        public void sendRedirect(String location) throws IOException {
            doSessionUpdate();
            super.sendRedirect(location);
        }

        /**
         * Calls <code>storeSecurityContextInSession()</code>
         */
        private void doSessionUpdate() {
            if (sessionUpdateDone) {
                return;
            }
            SecurityContext securityContext = SecurityContextHolder.getContext();
            storeSecurityContextInSession(securityContext, request,
                    httpSessionExistedAtStartOfRequest, contextHashBeforeChainExecution);
            sessionUpdateDone = true;
        }

        /**
         * Tells if the response wrapper has called
         * <code>storeSecurityContextInSession()</code>.
         */
        public boolean isSessionUpdateDone() {
            return sessionUpdateDone;
        }

    }

}
