/*
* Copyright 2009 Martin Grotzke
*
* 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 de.javakaffee.web.msm;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import org.apache.catalina.Context;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
/**
* This valve is used for tracking requests for that the session must be sent to
* memcached, on host level. This encapsulates/surrounds als container request
* processing like e.g. authentication and ServletRequestListener notification.
*
* @author <a href="mailto:martin.grotzke@javakaffee.de">Martin Grotzke</a>
* @version $Id$
*/
public abstract class RequestTrackingHostValve extends ValveBase {
private static final String REQUEST_IGNORED = "de.javakaffee.msm.request.ignored";
public static final String REQUEST_PROCESS = "de.javakaffee.msm.request.process";
public static final String SESSION_ID_CHANGED = "de.javakaffee.msm.sessionIdChanged";
public static final String REQUEST_PROCESSED = "de.javakaffee.msm.request.processed";
static final String RELOCATE = "session.relocate";
protected static final Log _log = LogFactory.getLog( RequestTrackingHostValve.class );
private final Pattern _ignorePattern;
private final MemcachedSessionService _sessionBackupService;
private final Statistics _statistics;
private final AtomicBoolean _enabled;
protected final String _sessionCookieName;
private final CurrentRequest _currentRequest;
private final Context _msmContext;
private static final String MSM_REQUEST_ID = "msm.requestId";
/**
* Creates a new instance with the given ignore pattern and
* {@link SessionBackupService}.
*
* @param ignorePattern
* the regular expression for request uris to ignore
* @param context
* the catalina context of this valve
* @param sessionBackupService
* the service that actually backups sessions
* @param statistics
* used to store statistics
* @param enabled
* specifies if memcached-session-manager is enabled or not.
* If <code>false</code>, each request is just processed without doing anything further.
*/
public RequestTrackingHostValve( @Nullable final String ignorePattern, @Nonnull final String sessionCookieName,
@Nonnull final MemcachedSessionService sessionBackupService,
@Nonnull final Statistics statistics,
@Nonnull final AtomicBoolean enabled,
@Nonnull final CurrentRequest currentRequest) {
if ( ignorePattern != null ) {
_log.info( "Setting ignorePattern to " + ignorePattern );
_ignorePattern = Pattern.compile( ignorePattern );
} else {
_ignorePattern = null;
}
_sessionCookieName = sessionCookieName;
_sessionBackupService = sessionBackupService;
_statistics = statistics;
_enabled = enabled;
_currentRequest = currentRequest;
_msmContext = _sessionBackupService.getManager().getContext();
}
/**
* Returns the actually used name for the session cookie.
* @return the cookie name, never null.
*/
protected String getSessionCookieName() {
return _sessionCookieName;
}
public boolean isIgnoredRequest() {
final Request request = _currentRequest.get();
return request != null && request.getNote(REQUEST_IGNORED) == Boolean.TRUE;
}
/**
* {@inheritDoc}
*/
@Override
public void invoke( final Request request, final Response response ) throws IOException, ServletException {
final String requestId = getURIWithQueryString( request );
if(!_enabled.get() || !_msmContext.equals(request.getContext())) {
getNext().invoke( request, response );
} else if ( _ignorePattern != null && _ignorePattern.matcher( requestId ).matches() ) {
if(_log.isDebugEnabled()) {
_log.debug( ">>>>>> Ignoring: " + requestId + " (requestedSessionId "+ request.getRequestedSessionId() +") ==================" );
}
try {
storeRequestThreadLocal( request );
request.setNote(REQUEST_IGNORED, Boolean.TRUE);
getNext().invoke( request, response );
} finally {
if(request.getNote(REQUEST_PROCESSED) == Boolean.TRUE) {
final String sessionId = getSessionId(request, response);
if(sessionId != null) {
_sessionBackupService.requestFinished(sessionId, requestId);
}
}
resetRequestThreadLocal();
}
if(_log.isDebugEnabled()) {
_log.debug( "<<<<<< Ignored: " + requestId + " ==================" );
}
} else {
request.setNote(REQUEST_PROCESS, Boolean.TRUE);
if ( _log.isDebugEnabled() ) {
_log.debug( ">>>>>> Request starting: " + requestId + " (requestedSessionId "+ request.getRequestedSessionId() +") ==================" );
}
try {
storeRequestThreadLocal( request );
getNext().invoke( request, response );
} finally {
final Boolean sessionIdChanged = (Boolean) request.getNote(SESSION_ID_CHANGED);
backupSession( request, response, sessionIdChanged == null ? false : sessionIdChanged.booleanValue() );
resetRequestThreadLocal();
}
if ( _log.isDebugEnabled() ) {
logDebugRequestSessionCookie( request );
logDebugResponseCookie( response );
_log.debug( "<<<<<< Request finished: " + requestId + " ==================" );
}
}
}
protected void logDebugRequestSessionCookie( final Request request ) {
final Cookie[] cookies = request.getCookies();
if ( cookies == null ) {
return;
}
for( final javax.servlet.http.Cookie cookie : cookies ) {
if ( cookie.getName().equals( _sessionCookieName ) ) {
_log.debug( "Have request session cookie: domain=" + cookie.getDomain() + ", maxAge=" + cookie.getMaxAge() +
", path=" + cookie.getPath() + ", value=" + cookie.getValue() +
", version=" + cookie.getVersion() + ", secure=" + cookie.getSecure() );
}
}
}
@Nonnull
protected static String getURIWithQueryString( @Nonnull final Request request ) {
final Object note = request.getNote(MSM_REQUEST_ID);
if(note != null) {
// we have a string and want to save cast
return note.toString();
}
final StringBuilder sb = new StringBuilder(30);
sb.append(request.getMethod())
.append(' ')
.append(request.getRequestURI());
if(!isPostMethod(request) && request.getQueryString() != null) {
sb.append('?').append(request.getQueryString());
}
final String result = sb.toString();
request.setNote(MSM_REQUEST_ID, result);
return result;
}
protected static boolean isPostMethod(final Request request) {
final String method = request.getMethod();
if ( method == null && _log.isDebugEnabled() ) {
_log.debug("No method set for request " + request.getRequestURI() +
(request.getQueryString() != null ? "?" + request.getQueryString() : ""));
}
return method != null ? method.toLowerCase().equals( "post" ) : false;
}
void resetRequestThreadLocal() {
_currentRequest.reset();
}
void storeRequestThreadLocal( @Nonnull final Request request ) {
_currentRequest.set( request );
}
private void backupSession( final Request request, final Response response, final boolean sessionIdChanged ) {
/*
* Do we have a session?
*/
final String sessionId = getSessionId(request, response);
if ( sessionId != null ) {
_statistics.requestWithSession();
_sessionBackupService.backupSession( sessionId, sessionIdChanged, getURIWithQueryString( request ) );
}
else {
_statistics.requestWithoutSession();
}
}
private String getSessionId(final Request request, final Response response) {
// If the context is configured with cookie="false" there's no session cookie
// sent so we must rely on data from MemcachedSessionService
String sessionId = (String) request.getNote(MemcachedSessionService.NEW_SESSION_ID);
if(sessionId == null) {
sessionId = getSessionIdFromResponseSessionCookie( response );
}
return sessionId != null ? sessionId : request.getRequestedSessionId();
}
private String getSessionIdFromResponseSessionCookie(final Response response) {
final String[] headers = getSetCookieHeaders(response);
if (headers == null) {
return null;
}
List<String> sessionCookies = new ArrayList<String>(headers.length);
for (final String header : headers) {
if (header != null && header.contains(_sessionCookieName)) {
final String sessionIdPrefix = _sessionCookieName + "=";
final int idxNameStart = header.indexOf(sessionIdPrefix);
final int idxValueStart = idxNameStart + sessionIdPrefix.length();
int idxValueEnd = header.indexOf(';', idxNameStart);
if (idxValueEnd == -1) {
idxValueEnd = header.indexOf(' ', idxValueStart);
}
if (idxValueEnd == -1) {
idxValueEnd = header.length();
}
String result = header.substring(idxValueStart, idxValueEnd);
if(header.substring(idxValueEnd - 1).contains(sessionIdPrefix)) {
_log.warn("Response contains Set-Cookie header with multiple "+ _sessionCookieName+" entries: " + header +
". Session handling might be negatively affected, you should investigate this.");
}
sessionCookies.add(result);
}
}
if (sessionCookies.size() == 1)
return sessionCookies.get(0);
else if (sessionCookies.size() > 1) {
_log.warn("Response contains multiple Set-Cookie headers with a "+ _sessionCookieName+", returning the first one from: " + sessionCookies.toString() +
". Session handling might be negatively affected, you should investigate this.");
return sessionCookies.get(0);
}
return null;
}
/**
* Read the Set-Cookie header from the given response.
*/
protected abstract String[] getSetCookieHeaders(final Response response);
private void logDebugResponseCookie( final Response response ) {
final String[] headers = getSetCookieHeaders(response);
if ( headers != null ) {
for (final String header : headers) {
if (header != null && header.contains(_sessionCookieName)) {
_log.debug( "Request finished, with Set-Cookie header: " + header );
}
}
}
}
}