blob: 6783a19011809d31af4589a5935f4355b2c46828 [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.myfaces.trinidadinternal.webapp;
import java.io.IOException;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.faces.component.UIViewRoot;
import javax.faces.context.ExternalContext;
import javax.faces.context.ExternalContextWrapper;
import javax.faces.context.FacesContext;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
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.HttpSession;
import org.apache.myfaces.trinidad.context.RequestContext;
import org.apache.myfaces.trinidad.logging.TrinidadLogger;
import org.apache.myfaces.trinidad.util.ClassLoaderUtils;
import org.apache.myfaces.trinidad.util.ExternalContextUtils;
import org.apache.myfaces.trinidad.util.RequestStateMap;
import org.apache.myfaces.trinidadinternal.config.CheckSerializationConfigurator;
import org.apache.myfaces.trinidadinternal.config.GlobalConfiguratorImpl;
import org.apache.myfaces.trinidadinternal.config.dispatch.DispatchResponseConfiguratorImpl;
import org.apache.myfaces.trinidadinternal.config.dispatch.DispatchServletResponse;
import org.apache.myfaces.trinidadinternal.config.upload.FileUploadConfiguratorImpl;
import org.apache.myfaces.trinidadinternal.config.upload.UploadRequestWrapper;
import org.apache.myfaces.trinidadinternal.config.xmlHttp.XmlHttpConfigurator;
import org.apache.myfaces.trinidadinternal.context.DialogServiceImpl;
import org.apache.myfaces.trinidadinternal.context.RequestContextImpl;
import org.apache.myfaces.trinidadinternal.context.external.ServletExternalContext;
import org.apache.myfaces.trinidadinternal.renderkit.core.CoreRenderKit;
import org.apache.myfaces.trinidadinternal.renderkit.core.xhtml.XhtmlConstants;
import org.apache.myfaces.trinidadinternal.share.util.MultipartFormHandler;
import org.apache.myfaces.trinidadinternal.util.QueryParams;
import org.apache.myfaces.trinidadinternal.webapp.wrappers.BasicHTMLBrowserRequestWrapper;
/**
* Actual implementation of the Trinidad servlet filter.
* <p>
* @version $Name: $ ($Revision: adfrt/faces/adf-faces-impl/src/main/java/oracle/adfinternal/view/faces/webapp/AdfFacesFilterImpl.java#0 $) $Date: 10-nov-2005.18:48:59 $
* @todo Allow configuration of the maximum allowed number of bytes in
* an entire request
*/
public class TrinidadFilterImpl implements Filter
{
static public void verifyFilterIsInstalled(FacesContext context)
{
Object isInstalled =
context.getExternalContext().getRequestMap().get(_FILTER_EXECUTED_KEY);
if (!Boolean.TRUE.equals(isInstalled))
{
_LOG.warning("REQUIRED_TRINIDADFILTER_NOT_INSTALLED");
}
}
static public FacesContext getPseudoFacesContext()
{
return _PSEUDO_FACES_CONTEXT.get();
}
/**
* Returns true if the filter is in the middle of executing the
* "return from dialog"
*/
static public boolean isExecutingDialogReturn(FacesContext context)
{
return Boolean.TRUE.equals(
context.getExternalContext().getRequestMap().get(_IS_RETURNING_KEY));
}
public void init(FilterConfig filterConfig) throws ServletException
{
// potentially wrap the FilterConfig to catch Serialization changes
filterConfig = CheckSerializationConfigurator.getFilterConfig(filterConfig);
_servletContext = filterConfig.getServletContext();
//There is some functionality that still might require servlet-only filter services.
_filters = ClassLoaderUtils.getServices(TrinidadFilterImpl.class.getName());
ExternalContext externalContext = new ServletExternalContext(
_servletContext, null, null);
PseudoFacesContext facesContext = new PseudoFacesContext(externalContext);
facesContext.setAsCurrentInstance();
try
{
for(Filter f:_filters)
{
f.init(filterConfig);
}
}
finally
{
facesContext.release();
}
}
public void destroy()
{
//Destroy filter services
for(Filter f:_filters)
{
f.destroy();
}
_filters = null;
}
@SuppressWarnings("unchecked")
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException
{
//Execute the filter services
if (!_filters.isEmpty())
chain = new FilterListChain(_filters, chain);
// Set a flag so that we can detect if the filter has been
// properly installed.
request.setAttribute(_FILTER_EXECUTED_KEY, Boolean.TRUE);
// potentially wrap the request in order to check managed bean HA
if (request instanceof HttpServletRequest)
{
request = CheckSerializationConfigurator.getHttpServletRequest(
new ServletExternalContext(_servletContext, request, response),
(HttpServletRequest)request);
}
// potentially wrap the ServletContext in order to check managed bean HA
ExternalContext externalContext = _createExternalContext(request, response);
// provide a (Pseudo-)FacesContext for configuration tasks
PseudoFacesContext facesContext = new PseudoFacesContext(externalContext);
facesContext.setAsCurrentInstance();
GlobalConfiguratorImpl config;
try
{
config = GlobalConfiguratorImpl.getInstance();
config.beginRequest(externalContext);
}
catch(Throwable t)
{
boolean isPartialRequest = CoreRenderKit.isPartialRequest(externalContext);
facesContext.release();
_handleException(externalContext, isPartialRequest, t);
return;
}
try
{
boolean responseComplete=false;
try
{
// Allow Configurators to wrap response and request
request = (ServletRequest)externalContext.getRequest();
response = (ServletResponse)externalContext.getResponse();
//To maintain backward compatibilty, wrap the request at the filter level
Map<String, String[]> addedParams = FileUploadConfiguratorImpl.getAddedParameters(externalContext);
if(addedParams != null)
{
FileUploadConfiguratorImpl.apply(externalContext);
request = new UploadRequestWrapper(externalContext, addedParams);
}
String noJavaScript = request.getParameter(XhtmlConstants.NON_JS_BROWSER);
// Wrap the request only for Non-javaScript browsers
if(noJavaScript != null &&
XhtmlConstants.NON_JS_BROWSER_TRUE.equals(noJavaScript))
{
request = new BasicHTMLBrowserRequestWrapper((HttpServletRequest)request);
}
responseComplete = facesContext.getResponseComplete();
}
finally
{
// release the PseudoFacesContext, since _doFilterImpl() has its own FacesContext
facesContext.release();
}
// Abort processing if the response has been completed by Configurator
if (!responseComplete)
{
_doFilterImpl(request, response, chain);
}
}
catch (Throwable t)
{
boolean isPartialRequest = CoreRenderKit.isPartialRequest(externalContext);
_handleException(externalContext, isPartialRequest, t);
}
finally
{
try
{
config.endRequest(externalContext);
}
catch(Throwable t)
{
boolean isPartialRequest = CoreRenderKit.isPartialRequest(externalContext);
_handleException(externalContext, isPartialRequest, t);
}
}
}
private ExternalContext _createExternalContext(ServletRequest request, ServletResponse response)
{
// potentially wrap the ServletContext in order to check managed bean HA
ExternalContext externalContext = new ServletExternalContext(
_getPotentiallyWrappedServletContext(request),
request,
response);
if (_isMultipartHttpServletRequest(externalContext))
{
// We need to wrap the ExternalContext for multipart form posts in order to avoid
// premature reads on the request input stream. See MultipartExternalContextWrapper
// class doc for details.
//
// Note: we only appply this fix for HttpServlet use cases, as the fix requires access
// to the query string, and I don't see how to get at the query string for portlet
// requests. It is possible that this means that trinidad file uploads may still be broken
// for portlets when running against JSF 2.2.
externalContext = new MultipartExternalContextWrapper(externalContext);
}
return externalContext;
}
/**
* For PPR errors, handle the request specially
*/
private static void _handleException(ExternalContext externalContext, boolean isPartialRequest, Throwable t)
throws IOException, ServletException
{
if (isPartialRequest)
{
XmlHttpConfigurator.handleError(externalContext, t);
}
else
{
// For non-partial requests, just re-throw. It is not
// our responsibility to catch these
if (t instanceof RuntimeException)
throw ((RuntimeException) t);
if (t instanceof Error)
throw ((Error) t);
if (t instanceof IOException)
throw ((IOException) t);
if (t instanceof ServletException)
throw ((ServletException) t);
// Should always be one of those four types to have
// gotten here.
_LOG.severe(t);
}
}
@SuppressWarnings("unchecked")
private void _doFilterImpl(
ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException
{
// -= Scott O'Bryan =-
// Added for backward compatibility
// potentially wrap the ServletContext to check ManagerBean HA
ExternalContext ec = new ServletExternalContext(_getPotentiallyWrappedServletContext(request),
request,
response);
boolean isHttpReq = ExternalContextUtils.isHttpServletRequest(ec);
if(isHttpReq)
{
response = _getResponse(ec);
ec.setResponse(response);
}
// Set up a PseudoFacesContext with the actual request and response
// so that RequestContext can be more functional in the interval
// between now and when the FacesServlet starts.
PseudoFacesContext pfc = new PseudoFacesContext(ec);
_PSEUDO_FACES_CONTEXT.set(pfc);
try
{
if(isHttpReq)
{
// If there are existing "launchParameters", then that means
// we've returned from a "launch", and we need to re-execute the
// faces lifecycle. ViewHandlerImpl will be responsible for ensuring
// that we re-execute the lifecycle on the correct page.
// -= Simon Lessard =-
// FIXME: Using <String, String[]> for now to accomodate ReplaceParametersRequestWrapper.
// However, the Servlet specification suggest <String, Object> so this
// could lead to some nasty problems one day. Especially if JEE spec includes
// generics for its Servlet API soon.
//
// -= Scott O'Bryan =-
// TODO: The following should be made available to the Portal. This is not trivial
// because this just re-invokes the filter chain with a new set of parameters.
// In the portal environment, this must rerun the portlet without the use of
// filters until Portlet 2.0.
request = _getRequest(ec);
ec.setRequest(request);
}
chain.doFilter(request, response);
if(isHttpReq)
{
_handleDialogReturn(ec);
}
}
finally
{
_PSEUDO_FACES_CONTEXT.remove();
}
}
private String _getKey(String uid)
{
return _LAUNCH_KEY+"_"+uid;
}
private ServletResponse _getResponse(ExternalContext ec)
{
HttpServletResponse dispatch = new DispatchServletResponse(ec);
DispatchResponseConfiguratorImpl.apply(ec);
return dispatch;
}
private ServletRequest _getRequest(ExternalContext ec)
{
String uid = ec.getRequestParameterMap().get(_LAUNCH_KEY);
if(uid != null)
{
/**
* We use pageflow scope so that if something fails on the redirect, we
* have a chance of getting cleaned up early. This will not always happen
* so the object may stick around for a while.
*/
Map<String, Object> sessionMap = ec.getSessionMap();
LaunchData data = (LaunchData)sessionMap.remove(_getKey(uid));
//We are returning from a dialog:
if(data != null)
{
Map<String, Object> requestMap = ec.getRequestMap();
//Setting the flag to properly support isExecutingDialogReturn.
//This is needed for isExecutingDialogReturn.
requestMap.put(_IS_RETURNING_KEY, Boolean.TRUE);
UIViewRoot launchView = data.getLaunchView();
if(launchView != null)
{
requestMap.put(RequestContextImpl.LAUNCH_VIEW, data.getLaunchView());
}
return new ReplaceParametersRequestWrapper(
(HttpServletRequest) ec.getRequest(),
data.getLaunchParam());
}
}
return (ServletRequest)ec.getRequest();
}
private void _handleDialogReturn(ExternalContext ec)
throws IOException
{
Map<String, Object> reqMap = ec.getRequestMap();
if(Boolean.TRUE.equals(reqMap.get(DialogServiceImpl.DIALOG_RETURN)))
{
/**
* We use pageflow scope so that if something fails on the redirect, we
* have a chance of getting cleaned up early. This will not always happen
* so the object may stick around for a while.
*/
Map<String, Object> sessionMap = ec.getSessionMap();
String uid = UUID.randomUUID().toString();
LaunchData data = new LaunchData((UIViewRoot)reqMap.get(RequestContextImpl.LAUNCH_VIEW), (Map<String, String[]>) reqMap.get(RequestContextImpl.LAUNCH_PARAMETERS));
sessionMap.put(_getKey(uid), data);
//Construct URL
//TODO: sobryan I believe some of this can be added to the RequestContextUtils to allow
// this url to be constructed for both portlet and servlet environments. We'll want to research.
HttpServletRequest req = (HttpServletRequest) ec.getRequest();
StringBuffer url = req.getRequestURL().append("?");
String queryStr = req.getQueryString();
if((queryStr != null) && (queryStr.trim().length() >0))
{
url.append(queryStr)
.append("&");
}
url.append(_LAUNCH_KEY)
.append("=")
.append(uid);
//Extensions to Trinidad may have alternatve means of handling PPR. This
//flag allows those extensions to for the <redirect> AJAX message to be returned.
if (RequestContext.getCurrentInstance().isPartialRequest(_PSEUDO_FACES_CONTEXT.get()) ||
Boolean.TRUE.equals(RequestStateMap.getInstance(ec).get(_FORCE_PPR_DIALOG_RETURN)))
{
//Special handling for XmlHttpRequest. Would be cool to handle this much cleaner.
HttpServletResponse resp = (HttpServletResponse) ec.getResponse();
XmlHttpConfigurator.sendXmlRedirect(resp.getWriter(), url.toString());
}
else
{
ec.redirect(url.toString());
}
}
}
private static final class LaunchData implements Serializable
{
private UIViewRoot _launchView;
private Map<String, String[]> _launchParam;
public LaunchData(UIViewRoot launchView, Map<String, String[]> launchParam)
{
_launchView = launchView;
if(launchParam != null)
{
_launchParam = launchParam;
}
else
{
_launchParam = Collections.emptyMap();
}
}
private UIViewRoot getLaunchView()
{
return _launchView;
}
private Map<String, String[]> getLaunchParam()
{
return _launchParam;
}
private static final long serialVersionUID = 1L;
}
private static final class FilterListChain implements FilterChain
{
private final List<Filter> _filters;
private final FilterChain _last;
private final int _index;
public FilterListChain(List<Filter> filters, FilterChain last)
{
this(filters, last, 0);
}
private FilterListChain(List<Filter> filters, FilterChain last, int index)
{
assert index < filters.size();
_filters = filters;
_last = last;
_index = index;
}
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException
{
int nextIndex = _index+1;
final FilterChain next;
// if there are more filters to chain, then keep using
// FilterListChain; otherwise, just use the last chain:
if (nextIndex < _filters.size())
next = new FilterListChain(_filters, _last, nextIndex);
else
next = _last;
_filters.get(_index).doFilter(request, response, next);
}
}
/**
* Returns a potentially wrapped ServletContext for ManagedBean HA
*/
private ServletContext _getPotentiallyWrappedServletContext(ServletRequest request)
{
if (request instanceof HttpServletRequest)
{
HttpSession session = ((HttpServletRequest)request).getSession(false);
if (session != null)
{
return session.getServletContext();
}
}
return _servletContext;
}
private static boolean _isMultipartHttpServletRequest(ExternalContext externalContext)
{
return (MultipartFormHandler.isMultipartRequest(externalContext) &&
(externalContext.getRequest() instanceof HttpServletRequest));
}
/**
* With the addition of a standard multipart form processing solution in Java EE, we now
* have contention between Trinidad and Java EE for who will a) read the input stream for
* multiparm form posts and b) manage uploaded files. This contention was exacerbated by
* the addition of the @MultipartConfig annotation to the FacesServlet in JSF 2.2. As a
* result of this addition, any attempt to get a request parameter (either a query or post
* parameter) will cause the servlet engine to read the input stream. If this happens before
* Trinidad's FileUploadConfiguratorImpl is invoked, FileUploadConfiguratorImpl won't be
* able to read the request input stream and as a result Trinidad file upload processing
* is broken.
*
* As a temporary workaround until we can devise a better integration strategy with Java EE's
* multipart support, we want to avoid request parameter lookups before FileUploadConfiguratorImpl
* gets a crack at the input stream. This ExternalContextWrapper helps with this, by suppressing
* access to the underlying request parameter maps while Configurators are invoked. We do, however,
* need to provide access to query parameters, so these (and not post parameters) are exposed via the
* getRequestParameter* methods.
*
* This simulates the behavior prior to JSF 2.2 (ie. prior to the additional @MultipartConfig):
* query parameter lookups are successful, but post parameter lookups return null until after the
* request input stream is processed.
*/
private static class MultipartExternalContextWrapper extends ExternalContextWrapper
{
private MultipartExternalContextWrapper(ExternalContext wrapped)
{
assert(wrapped.getRequest() instanceof HttpServletRequest);
_wrapped = wrapped;
_queryParams = _parseQueryParameters(wrapped);
}
@Override
public Map<String, String> getRequestParameterMap()
{
return _queryParams.getParameterMap();
}
@Override
public Iterator<String> getRequestParameterNames()
{
return _queryParams.getParameterMap().keySet().iterator();
}
@Override
public Map<String, String[]> getRequestParameterValuesMap()
{
return _queryParams.getParameterValuesMap();
}
@Override
public ExternalContext getWrapped()
{
return _wrapped;
}
private static QueryParams _parseQueryParameters(ExternalContext externalContext)
{
String queryString = _getQueryString(externalContext);
String encoding = _getQueryStringEncoding(externalContext);
try
{
return QueryParams.parseQueryString(queryString, encoding);
}
catch (UnsupportedEncodingException e)
{
_LOG.warning(e);
}
// Retry, forcing encoding to UTF-8
try
{
return QueryParams.parseQueryString(queryString, _UTF8);
}
catch (UnsupportedEncodingException e)
{
_LOG.severe(e);
throw new IllegalStateException(e);
}
}
private static String _getQueryString(ExternalContext externalContext)
{
return ((HttpServletRequest)externalContext.getRequest()).getQueryString();
}
private static String _getQueryStringEncoding(ExternalContext externalContext)
{
String encoding = externalContext.getRequestCharacterEncoding();
// The request character encoding should already have been initialized
// by ServletExternalContext._initHttpServletRequest(). If the request
// character encoding is null, we could:
//
// 1. Try to derive the document's character encoding.
// 2. Default to UTF-8.
// 3. Fail.
//
// I don't know that #1 is possible at this point in the request lifecycle. A null
// encoding here means that the encoding is not specified via the request's content
// type, and is not available via the ViewHandler.CHARACTER_ENCODING_KEY session
// attribute. Not sure where else to look.
//
// I prefer #2 over #3, especially as the javadoc for java.net.URLDecoder states the following:
//
// Note: The World Wide Web Consortium Recommendation states that UTF-8 should be used.
// Not doing so may introduce incompatibilites.
//
// Also note that the encoding that we use to decode the query string does not impct subsequent
// request parameter/payload decoding. This encoding will only be applied to query parameters
// that are retrieved by Configurators during multipart pst processing, which is a very small subset
// of the request paramters.
if (encoding == null)
{
_LOG.info("No request character encoding found. Parsing query string with encoding " +
_UTF8);
encoding = _UTF8;
}
return encoding;
}
private final ExternalContext _wrapped;
private final QueryParams _queryParams;
}
private ServletContext _servletContext;
private List<Filter> _filters = null;
private static final String _UTF8 = "UTF-8";
private static final String _LAUNCH_KEY = "_dlgDta";
private static final String _IS_RETURNING_KEY =
"org.apache.myfaces.trinidadinternal.webapp.AdfacesFilterImpl.IS_RETURNING";
private static final String _FILTER_EXECUTED_KEY =
"org.apache.myfaces.trinidadinternal.webapp.AdfacesFilterImpl.EXECUTED";
//This allows extension which do not use Trinidad's PPR to for an XML dialog return
private static final String _FORCE_PPR_DIALOG_RETURN =
"org.apache.myfaces.trinidad.webapp.FORCE_XML_DIALOG_RETURN";
private static ThreadLocal<PseudoFacesContext> _PSEUDO_FACES_CONTEXT =
new ThreadLocal<PseudoFacesContext>();
private static final TrinidadLogger _LOG =
TrinidadLogger.createTrinidadLogger(TrinidadFilterImpl.class);
}