blob: c53d0cd41a80af5cac051eb228f43310024f0cd2 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.wicket.protocol.http;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
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.annotation.WebFilter;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.wicket.ThreadContext;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.protocol.http.servlet.ResponseIOException;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.http.WebRequest;
import org.apache.wicket.request.http.WebResponse;
import org.apache.wicket.util.file.WebXmlFile;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.string.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Filter for initiating handling of Wicket requests.
* <p>
* The advantage of a filter is that, unlike a servlet, it can choose not to process the request and
* let whatever is next in chain try. So when using a Wicket filter and a request comes in for
* foo.gif the filter can choose not to process it because it knows it is not a wicket-related
* request. Since the filter didn't process it, it falls on to the application server to try, and
* then it works."
*
* @see WicketServlet for documentation
*
* @author Jonathan Locke
* @author Timur Mehrvarz
* @author Juergen Donnerstag
* @author Igor Vaynberg (ivaynberg)
* @author Al Maw
* @author jcompagner
* @author Matej Knopp
*/
public class WicketFilter implements Filter
{
private static final Logger log = LoggerFactory.getLogger(WicketFilter.class);
/** The name of the root path parameter that specifies the root dir of the app. */
public static final String FILTER_MAPPING_PARAM = "filterMappingUrlPattern";
/** The name of the context parameter that specifies application factory class */
public static final String APP_FACT_PARAM = "applicationFactoryClassName";
/**
* Name of parameter used to express a comma separated list of paths that should be ignored
*/
public static final String IGNORE_PATHS_PARAM = "ignorePaths";
// Wicket's Application object
private WebApplication application;
/** the factory used to create the web aplication instance */
private IWebApplicationFactory applicationFactory;
private FilterConfig filterConfig;
private String filterPath;
// filterPath length without trailing "/"
private int filterPathLength = -1;
/** set of paths that should be ignored by the wicket filter */
private final Set<String> ignorePaths = new HashSet<String>();
/**
* A flag indicating whether WicketFilter is used directly or through WicketServlet
*/
private boolean isServlet = false;
/**
* default constructor, usually invoked through the servlet container by the web.xml
* configuration
*/
public WicketFilter()
{
}
/**
* constructor supporting programmatic setup of the filter
* <p/>
* this can be useful for programmatically creating and appending the wicket filter to the
* servlet context using servlet 3 features.
*
* @param application
* web application
*/
public WicketFilter(WebApplication application)
{
this.application = Args.notNull(application, "application");
}
/**
* @return The class loader
*/
protected ClassLoader getClassLoader()
{
return Thread.currentThread().getContextClassLoader();
}
/**
* This is Wicket's main method to execute a request
*
* @param request
* @param response
* @param chain
* @return false, if the request could not be processed
* @throws IOException
* @throws ServletException
*/
boolean processRequest(ServletRequest request, final ServletResponse response,
final FilterChain chain) throws IOException, ServletException
{
final ThreadContext previousThreadContext = ThreadContext.detach();
// Assume we are able to handle the request
boolean res = true;
final ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader();
final ClassLoader newClassLoader = getClassLoader();
HttpServletRequest httpServletRequest = (HttpServletRequest)request;
HttpServletResponse httpServletResponse = (HttpServletResponse)response;
boolean ioExceptionOccurred = false;
try
{
if (previousClassLoader != newClassLoader)
{
Thread.currentThread().setContextClassLoader(newClassLoader);
}
// Make sure getFilterPath() gets called before checkIfRedirectRequired()
String filterPath = getFilterPath(httpServletRequest);
if (filterPath == null)
{
throw new IllegalStateException("filter path was not configured");
}
if (shouldIgnorePath(httpServletRequest))
{
log.debug("Ignoring request {}", httpServletRequest.getRequestURL());
if (chain != null)
{
// invoke next filter from within Wicket context
chain.doFilter(request, response);
}
return false;
}
if ("OPTIONS".equalsIgnoreCase(httpServletRequest.getMethod()))
{
// handle the OPTIONS request outside of normal request processing.
// wicket pages normally only support GET and POST methods, but resources and
// special pages acting like REST clients can also support other methods, so
// we include them all.
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
httpServletResponse.setHeader("Allow",
"GET,POST,OPTIONS,PUT,HEAD,PATCH,DELETE,TRACE");
httpServletResponse.setHeader("Content-Length", "0");
return true;
}
String redirectURL = checkIfRedirectRequired(httpServletRequest);
if (redirectURL == null)
{
// No redirect; process the request
ThreadContext.setApplication(application);
WebRequest webRequest = application.createWebRequest(httpServletRequest, filterPath);
WebResponse webResponse = application.createWebResponse(webRequest,
httpServletResponse);
RequestCycle requestCycle = application.createRequestCycle(webRequest, webResponse);
res = processRequestCycle(requestCycle, webResponse, httpServletRequest,
httpServletResponse, chain);
}
else
{
if (Strings.isEmpty(httpServletRequest.getQueryString()) == false)
{
redirectURL += "?" + httpServletRequest.getQueryString();
}
// send redirect - this will discard POST parameters if the request is POST
// - still better than getting an error because of lacking trailing slash
httpServletResponse.sendRedirect(httpServletResponse.encodeRedirectURL(redirectURL));
}
}
catch (IOException e)
{
ioExceptionOccurred = true;
throw e;
}
catch (ResponseIOException e)
{
ioExceptionOccurred = true;
throw e.getCause();
}
finally
{
ThreadContext.restore(previousThreadContext);
if (newClassLoader != previousClassLoader)
{
Thread.currentThread().setContextClassLoader(previousClassLoader);
}
if (!ioExceptionOccurred && response.isCommitted() &&
!httpServletRequest.isAsyncStarted())
{
try
{
response.flushBuffer();
}
catch (ResponseIOException e)
{
throw e.getCause();
}
}
}
return res;
}
/**
* Process the request cycle
*
* @param requestCycle
* @param webResponse
* @param httpServletRequest
* @param httpServletResponse
* @param chain
* @return false, if the request could not be processed
* @throws IOException
* @throws ServletException
*/
protected boolean processRequestCycle(RequestCycle requestCycle, WebResponse webResponse,
HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
final FilterChain chain) throws IOException, ServletException
{
boolean reqProcessed;
try
{
reqProcessed = requestCycle.processRequest();
if (reqProcessed)
{
webResponse.flush();
}
}
finally
{
requestCycle.detach();
}
if (!reqProcessed)
{
if (chain != null)
{
// invoke next filter from within Wicket context
chain.doFilter(httpServletRequest, httpServletResponse);
}
}
return reqProcessed;
}
/**
* @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
* javax.servlet.ServletResponse, javax.servlet.FilterChain)
*/
@Override
public void doFilter(final ServletRequest request, final ServletResponse response,
final FilterChain chain) throws IOException, ServletException
{
processRequest(request, response, chain);
}
/**
* Creates the web application factory instance.
*
* If no APP_FACT_PARAM is specified in web.xml ContextParamWebApplicationFactory will be used
* by default.
*
* @see ContextParamWebApplicationFactory
*
* @return application factory instance
*/
protected IWebApplicationFactory getApplicationFactory()
{
final String appFactoryClassName = filterConfig.getInitParameter(APP_FACT_PARAM);
if (appFactoryClassName == null)
{
// If no context param was specified we return the default factory
return new ContextParamWebApplicationFactory();
}
else
{
try
{
// Try to find the specified factory class
// see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6500212
// final Class<?> factoryClass = Thread.currentThread()
// .getContextClassLoader()
// .loadClass(appFactoryClassName);
final Class<?> factoryClass = Class.forName(appFactoryClassName, false,
Thread.currentThread().getContextClassLoader());
// Instantiate the factory
return (IWebApplicationFactory)factoryClass.newInstance();
}
catch (ClassCastException e)
{
throw new WicketRuntimeException("Application factory class " +
appFactoryClassName + " must implement IWebApplicationFactory");
}
catch (ClassNotFoundException e)
{
throw new WebApplicationFactoryCreationException(appFactoryClassName, e);
}
catch (InstantiationException e)
{
throw new WebApplicationFactoryCreationException(appFactoryClassName, e);
}
catch (IllegalAccessException e)
{
throw new WebApplicationFactoryCreationException(appFactoryClassName, e);
}
catch (SecurityException e)
{
throw new WebApplicationFactoryCreationException(appFactoryClassName, e);
}
}
}
/**
* If you do have a need to subclass, you may subclass {@link #init(boolean, FilterConfig)}
*
* @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
*/
@Override
public final void init(final FilterConfig filterConfig) throws ServletException
{
init(false, filterConfig);
}
/**
* Servlets and Filters are treated essentially the same with Wicket. This is the entry point
* for both of them.
*
* @see #init(FilterConfig)
*
* @param isServlet
* True if Servlet, false if Filter
* @param filterConfig
* @throws ServletException
*/
public void init(final boolean isServlet, final FilterConfig filterConfig)
throws ServletException
{
this.filterConfig = filterConfig;
this.isServlet = isServlet;
initIgnorePaths(filterConfig);
final ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader();
final ClassLoader newClassLoader = getClassLoader();
try
{
if (previousClassLoader != newClassLoader)
{
Thread.currentThread().setContextClassLoader(newClassLoader);
}
// locate application instance unless it was already specified during construction
if (application == null)
{
applicationFactory = getApplicationFactory();
application = applicationFactory.createApplication(this);
}
if (application.getName() == null)
{
application.setName(filterConfig.getFilterName());
}
application.setWicketFilter(this);
// Allow the filterPath to be preset via setFilterPath()
String configureFilterPath = getFilterPath();
if (configureFilterPath == null)
{
configureFilterPath = getFilterPathFromConfig(filterConfig);
if (configureFilterPath == null)
{
configureFilterPath = getFilterPathFromWebXml(isServlet, filterConfig);
if (configureFilterPath == null)
{
configureFilterPath = getFilterPathFromAnnotation(isServlet);
}
}
if (configureFilterPath != null)
{
setFilterPath(configureFilterPath);
}
}
if (getFilterPath() == null)
{
log.warn("Unable to determine filter path from filter init-param, web.xml, "
+ "or servlet 3.0 annotations. Assuming user will set filter path "
+ "manually by calling setFilterPath(String)");
}
ThreadContext.setApplication(application);
try
{
application.initApplication();
// Give the application the option to log that it is started
application.logStarted();
}
finally
{
ThreadContext.detach();
}
}
catch (Exception e)
{
// #destroy() might not be called by the web container when #init() fails,
// so destroy now
log.error(String.format("The initialization of an application with name '%s' has failed.",
filterConfig.getFilterName()), e);
try
{
destroy();
}
catch (Exception destroyException)
{
log.error("Unable to destroy after initialization failure", destroyException);
}
throw new ServletException(e);
}
finally
{
if (newClassLoader != previousClassLoader)
{
Thread.currentThread().setContextClassLoader(previousClassLoader);
}
}
}
/**
* Stub method that lets subclasses configure filter path from annotations.
*
* @param isServlet
* @return Filter path from annotation
*/
protected String getFilterPathFromAnnotation(boolean isServlet)
{
String[] patterns = null;
if (isServlet)
{
WebServlet servlet = getClass().getAnnotation(WebServlet.class);
if (servlet != null)
{
if (servlet.urlPatterns().length > 0)
{
patterns = servlet.urlPatterns();
}
else
{
patterns = servlet.value();
}
}
}
else
{
WebFilter filter = getClass().getAnnotation(WebFilter.class);
if (filter != null)
{
if (filter.urlPatterns().length > 0)
{
patterns = filter.urlPatterns();
}
else
{
patterns = filter.value();
}
}
}
if (patterns != null && patterns.length > 0)
{
String pattern = patterns[0];
if (patterns.length > 1)
{
log.warn(
"Multiple url patterns defined for Wicket filter/servlet, using the first: {}",
pattern);
}
if ("/*".equals(pattern))
{
pattern = "";
}
if (pattern.endsWith("*"))
{
pattern = pattern.substring(0, pattern.length() - 1);
}
return pattern;
}
return null;
}
/**
*
* @param isServlet
* @param filterConfig
* @return filter path from web.xml
*/
protected String getFilterPathFromWebXml(final boolean isServlet,
final FilterConfig filterConfig)
{
return new WebXmlFile().getUniqueFilterPath(isServlet, filterConfig);
}
/**
* @return filter config
*/
public FilterConfig getFilterConfig()
{
return filterConfig;
}
/**
* Either get the filterPath retrieved from web.xml, or if not found the old (1.3) way via a
* filter mapping param.
*
* @param request
* @return filterPath
*/
protected String getFilterPath(final HttpServletRequest request)
{
return filterPath;
}
/**
* Provide a standard getter for filterPath.
*
* @return The configured filterPath.
*/
public String getFilterPath()
{
return filterPath;
}
/**
*
* @param filterConfig
* @return filter path
*/
protected String getFilterPathFromConfig(FilterConfig filterConfig)
{
String result = filterConfig.getInitParameter(FILTER_MAPPING_PARAM);
if (result != null)
{
if (result.equals("/*"))
{
result = "";
}
else if (!result.startsWith("/") || !result.endsWith("/*"))
{
throw new WicketRuntimeException("Your " + FILTER_MAPPING_PARAM +
" must start with \"/\" and end with \"/*\". It is: " + result);
}
else
{
// remove leading "/" and trailing "*"
result = result.substring(1, result.length() - 1);
}
}
return result;
}
/**
* @see javax.servlet.Filter#destroy()
*/
@Override
public void destroy()
{
if (application != null)
{
try
{
ThreadContext.setApplication(application);
application.internalDestroy();
}
finally
{
ThreadContext.detach();
application = null;
}
}
if (applicationFactory != null)
{
try
{
applicationFactory.destroy(this);
}
finally
{
applicationFactory = null;
}
}
}
/**
* Try to determine as fast as possible if a redirect is necessary
*
* @param request
* @return null, if no redirect is necessary. Else the redirect URL
*/
private String checkIfRedirectRequired(final HttpServletRequest request)
{
return checkIfRedirectRequired(request.getRequestURI(), request.getContextPath());
}
/**
* Try to determine as fast as possible if a redirect is necessary
*
* @param requestURI
* @param contextPath
* @return null, if no redirect is necessary. Else the redirect URL
*/
protected final String checkIfRedirectRequired(final String requestURI, final String contextPath)
{
// length without jsessionid (http://.../abc;jsessionid=...?param)
int uriLength = requestURI.indexOf(';');
if (uriLength == -1)
{
uriLength = requestURI.length();
}
// request.getContextPath() + "/" + filterPath. But without any trailing "/".
int homePathLength = contextPath.length() +
(filterPathLength > 0 ? 1 + filterPathLength : 0);
if (uriLength != homePathLength)
{
// requestURI and homePath are different (in length)
// => continue with standard request processing. No redirect.
return null;
}
// Fail fast failed. Revert to "slow" but exact check
String uri = Strings.stripJSessionId(requestURI);
// home page without trailing slash URI
String homePageUri = contextPath + '/' + getFilterPath();
if (homePageUri.endsWith("/"))
{
homePageUri = homePageUri.substring(0, homePageUri.length() - 1);
}
// If both are equal => redirect
if (uri.equals(homePageUri))
{
uri += "/";
return uri;
}
// no match => standard request processing; no redirect
return null;
}
/**
* Sets the filter path instead of reading it from web.xml.
*
* Please note that you must subclass WicketFilter.init(FilterConfig) and set your filter path
* before you call super.init(filterConfig).
*
* @param filterPath
*/
public final void setFilterPath(String filterPath)
{
// see https://issues.apache.org/jira/browse/WICKET-701
if (this.filterPath != null)
{
throw new IllegalStateException(
"Filter path is write-once. You can not change it. Current value='" + filterPath +
'\'');
}
if (filterPath != null)
{
filterPath = canonicaliseFilterPath(filterPath);
// We only need to determine it once. It'll not change.
if (filterPath.endsWith("/"))
{
filterPathLength = filterPath.length() - 1;
}
else
{
filterPathLength = filterPath.length();
}
}
this.filterPath = filterPath;
}
/**
* Returns a relative path to the filter path and context root from an HttpServletRequest - use
* this to resolve a Wicket request.
*
* @param request
* @return Path requested, minus query string, context path, and filterPath. Relative, no
* leading '/'.
*/
public String getRelativePath(HttpServletRequest request)
{
String path = Strings.stripJSessionId(request.getRequestURI());
String contextPath = request.getContextPath();
path = path.substring(contextPath.length());
if (isServlet)
{
String servletPath = request.getServletPath();
path = path.substring(servletPath.length());
}
if (path.length() > 0)
{
path = path.substring(1);
}
// We should always be under the rootPath, except
// for the special case of someone landing on the
// home page without a trailing slash.
String filterPath = getFilterPath();
if (!path.startsWith(filterPath))
{
if (filterPath.equals(path + "/"))
{
path += "/";
}
}
if (path.startsWith(filterPath))
{
path = path.substring(filterPath.length());
}
return path;
}
protected WebApplication getApplication()
{
return application;
}
/**
* Checks whether this is a request to an ignored path
*
* @param request
* the current http request
* @return {@code true} when the request should be ignored, {@code false} - otherwise
*/
private boolean shouldIgnorePath(final HttpServletRequest request)
{
boolean ignore = false;
if (ignorePaths.size() > 0)
{
String relativePath = getRelativePath(request);
if (Strings.isEmpty(relativePath) == false)
{
for (String path : ignorePaths)
{
if (relativePath.startsWith(path))
{
ignore = true;
break;
}
}
}
}
return ignore;
}
/**
* initializes the ignore paths parameter
*
* @param filterConfig
*/
private void initIgnorePaths(final FilterConfig filterConfig)
{
String paths = filterConfig.getInitParameter(IGNORE_PATHS_PARAM);
if (Strings.isEmpty(paths) == false)
{
String[] parts = Strings.split(paths, ',');
for (String path : parts)
{
path = path.trim();
if (path.startsWith("/"))
{
path = path.substring(1);
}
ignorePaths.add(path);
}
}
}
/**
* A filterPath should have all leading slashes removed and exactly one trailing slash. A
* wildcard asterisk character has no special meaning. If your intention is to mean the top
* level "/" then an empty string should be used instead.
*
* @param filterPath
* @return canonic filter path
*/
static String canonicaliseFilterPath(String filterPath)
{
if (Strings.isEmpty(filterPath))
{
return filterPath;
}
int beginIndex = 0;
int endIndex = filterPath.length();
while (beginIndex < endIndex)
{
char c = filterPath.charAt(beginIndex);
if (c != '/')
{
break;
}
beginIndex++;
}
int o;
int i = o = beginIndex;
while (i < endIndex)
{
char c = filterPath.charAt(i);
i++;
if (c != '/')
{
o = i;
}
}
if (o < endIndex)
{
o++; // include exactly one trailing slash
filterPath = filterPath.substring(beginIndex, o);
}
else
{
// ensure to append trailing slash
filterPath = filterPath.substring(beginIndex) + '/';
}
if (filterPath.equals("/"))
{
return "";
}
return filterPath;
}
}