/*
* Copyright (C) 2005-2012 BetaCONCEPT Limited
*
* This file is part of Astroboa.
*
* Astroboa is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Astroboa 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 Astroboa. If not, see <http://www.gnu.org/licenses/>.
*/
package org.betaconceptframework.astroboa.portal.filter;
import java.io.IOException;
import java.util.Map;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.servlet.http.HttpSession;
import org.betaconceptframework.astroboa.context.AstroboaClientContext;
import org.betaconceptframework.astroboa.context.AstroboaClientContextHolder;
import org.betaconceptframework.astroboa.context.RepositoryContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* Populates the {@link RepositoryContextHolder} with information obtained from
* the <code>HttpSession</code>.
* <p/>
* <p/>
* The <code>HttpSession</code> will be queried to retrieve the
* <code>RepositoryContext</code> that should be stored against the
* <code>RepositoryContextHolder</code> for the duration of the web request. At
* the end of the web request, any updates made to the
* <code>RepositoryContextHolder</code> will be persisted back to the
* <code>HttpSession</code> by this filter.
* </p>
* <p/>
* No <code>HttpSession</code> will be created by this filter if one does not
* already exist.
* <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 Gregory Chomatas (gchomatas@betaconcept.com)
* @author Savvas Triantafyllou (striantafyllou@betaconcept.com)
*/
public class HttpSessionRepositoryContextIntegrationFilter extends OncePerRequestFilter {
protected static final Logger logger = LoggerFactory.getLogger(HttpSessionRepositoryContextIntegrationFilter.class);
static final String FILTER_APPLIED = "__astroboa_client_context_session_integration_filter_applied";
public static final String ASTROBOA_CLIENT_CONTEXT_MAP_KEY = "ASTROBOA_CLIENT_CONTEXT_MAP";
public static final String ACTIVE_ASTROBOA_CLIENT_CONTEXT_KEY = "ACTIVE_ASTROBOA_CLIENT_CONTEXT";
public HttpSessionRepositoryContextIntegrationFilter() throws ServletException {
}
@Override
protected String getAlreadyFilteredAttributeName() {
return FILTER_APPLIED;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (logger.isDebugEnabled()) {
logger.debug("{} processing request {} queryString {}", new Object[]{this.getClass().getName(), request.getRequestURI(),request.getQueryString()});
}
HttpSession httpSession = null;
try {
httpSession = request.getSession(false);
}
catch (IllegalStateException ignored) {
}
boolean httpSessionExistedAtStartOfRequest = httpSession != null;
Map<String, AstroboaClientContext> clientContextMapBeforeChainExecution = readClientContextMapFromSession(httpSession);
AstroboaClientContext activeClientContextBeforeChainExecution = readActiveClientContextFromSession(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;
int contextHashBeforeChainExecution = 0;
if (clientContextMapBeforeChainExecution != null) {
contextHashBeforeChainExecution = clientContextMapBeforeChainExecution.hashCode();
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
// Create a wrapper that will eagerly update the session with the repository 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 RepositoryContextHolder.setContext() is called
AstroboaClientContextHolder.setClientContextMap(clientContextMapBeforeChainExecution);
AstroboaClientContextHolder.setActiveClientContext(activeClientContextBeforeChainExecution);
filterChain.doFilter(request, responseWrapper);
}
finally {
// This is the only place in this class where RepositoryContextHolder.getContext() is called
Map<String, AstroboaClientContext> clientContextMapAfterChainExecution = AstroboaClientContextHolder.getClientContextMap();
AstroboaClientContext activeClientContextAfterChainExecution = AstroboaClientContextHolder.getActiveClientContext();
// Crucial removal of RepositoryContextHolder contents - do this before anything else.
AstroboaClientContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
// storeRepositoryContextInSession() 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() ) {
storeClientContextInSession(clientContextMapAfterChainExecution, activeClientContextAfterChainExecution, request,
httpSessionExistedAtStartOfRequest, contextHashBeforeChainExecution);
}
logger.debug("RepositoryContextHolder now cleared, as request processing completed");
}
}
private AstroboaClientContext readActiveClientContextFromSession(HttpSession httpSession) {
if (httpSession == null) {
logger.debug("No HttpSession currently exists");
return null;
}
// Session exists, so try to obtain a context from it.
Object activeClientContextFromSessionObject = httpSession.getAttribute(ACTIVE_ASTROBOA_CLIENT_CONTEXT_KEY);
if (activeClientContextFromSessionObject == null) {
logger.debug("HttpSession returned null object for {}",ACTIVE_ASTROBOA_CLIENT_CONTEXT_KEY);
return null;
}
// We now have the repository context object from the session.
if (!(activeClientContextFromSessionObject instanceof AstroboaClientContext)) {
if (logger.isWarnEnabled()) {
logger.warn(ACTIVE_ASTROBOA_CLIENT_CONTEXT_KEY +" did not contain a AstroboaClientContext but contained: '"
+ activeClientContextFromSessionObject
+ "'; are you improperly modifying the HttpSession directly "
+ "(you should always use AstroboaClientContextHolder) or using the HttpSession attribute "
+ "reserved for this class?");
}
return null;
}
// Everything OK. The only non-null return from this method.
return (AstroboaClientContext) activeClientContextFromSessionObject;
}
/**
* Gets the repository 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 RepositoryContext 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 Map<String, AstroboaClientContext> readClientContextMapFromSession(HttpSession httpSession) {
if (httpSession == null) {
logger.debug("No HttpSession currently exists");
return null;
}
// Session exists, so try to obtain a context from it.
Object clientContextMapFromSessionObject = httpSession.getAttribute(ASTROBOA_CLIENT_CONTEXT_MAP_KEY);
if (clientContextMapFromSessionObject == null) {
logger.debug("HttpSession returned null object for {}",ASTROBOA_CLIENT_CONTEXT_MAP_KEY);
return null;
}
// We now have the repository context object from the session.
if (!(clientContextMapFromSessionObject instanceof Map)) {
if (logger.isWarnEnabled()) {
logger.warn(ASTROBOA_CLIENT_CONTEXT_MAP_KEY + " did not contain a Map but contained: '"
+ clientContextMapFromSessionObject
+ "'; are you improperly modifying the HttpSession directly "
+ "(you should always use ClientContextHolder) or using the HttpSession attribute "
+ "reserved for this class?");
}
return null;
}
// Everything OK. The only non-null return from this method.
return (Map<String, AstroboaClientContext>) clientContextMapFromSessionObject;
}
/**
* Stores the supplied repository context in the session (if available) and if it has changed since it was
* set at the start of the request.
*
* @param clientContextMapAfterChainExecution the context object obtained from the RepositoryContextHolder after the request has
* been processed by the filter chain. RepositoryContextHolder.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 storeClientContextInSession(Map<String, AstroboaClientContext> clientContextMapAfterChainExecution,
AstroboaClientContext activeClientContextAfterChainExecution,
HttpServletRequest request,
boolean httpSessionExistedAtStartOfRequest,
int contextHashBeforeChainExecution) {
HttpSession httpSession = null;
try {
httpSession = request.getSession(false);
}
catch (IllegalStateException ignored) {
}
if (httpSession == null) {
if (httpSessionExistedAtStartOfRequest) {
logger.debug("HttpSession is now null, but was not null at start of request; session was invalidated, so do not create a new session");
}
}
// If HttpSession exists, store current RepositoryContextHolder contents but only if
// the RepositoryContext has actually changed (see JIRA SEC-37)
if (httpSession != null && (clientContextMapAfterChainExecution == null || clientContextMapAfterChainExecution.hashCode() != contextHashBeforeChainExecution)) {
httpSession.setAttribute(ASTROBOA_CLIENT_CONTEXT_MAP_KEY, clientContextMapAfterChainExecution);
httpSession.setAttribute(ACTIVE_ASTROBOA_CLIENT_CONTEXT_KEY, activeClientContextAfterChainExecution);
logger.debug("AstroboaClientContext map: '{}', and AstroboaClientContext stored to HttpSession",clientContextMapAfterChainExecution);
}
}
/**
* Does nothing. We use IoC container lifecycle services instead.
*/
public void destroy() {
}
//~ Inner Classes ==================================================================================================
/**
* Wrapper that is applied to every request to update the <code>HttpSession<code> with
* the <code>RepositoryContext</code> when a <code>sendError()</code> or <code>sendRedirect</code>
* happens. See SEC-398. The class contains the fields needed to call
* <code>storeRepositoryContextInSession()</code>
*/
private class OnRedirectUpdateSessionResponseWrapper extends HttpServletResponseWrapper {
HttpServletRequest request;
boolean httpSessionExistedAtStartOfRequest;
int contextHashBeforeChainExecution;
// Used to ensure storeRepositoryContextInSession() is only
// called once.
boolean sessionUpdateDone = false;
/**
* Takes the parameters required to call <code>storeRepositoryContextInSession()</code> in
* addition to the response object we are wrapping.
* @see HttpSessionRepositoryContextIntegrationFilter#storeRepositoryContextInSession(RepositoryContext, 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>storeRepositoryContextInSession()</code>
*/
private void doSessionUpdate() {
if (sessionUpdateDone) {
return;
}
Map<String, AstroboaClientContext> clientContextMap = AstroboaClientContextHolder.getClientContextMap();
AstroboaClientContext activeClientContext = AstroboaClientContextHolder.getActiveClientContext();
storeClientContextInSession(clientContextMap, activeClientContext, request,
httpSessionExistedAtStartOfRequest, contextHashBeforeChainExecution);
sessionUpdateDone = true;
}
/**
* Tells if the response wrapper has called
* <code>storeRepositoryContextInSession()</code>.
*/
public boolean isSessionUpdateDone() {
return sessionUpdateDone;
}
}
}