/*
* Copyright 2017 OmniFaces
*
* 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.omnifaces.exceptionhandler;
import static java.lang.String.format;
import static java.util.logging.Level.SEVERE;
import static javax.servlet.RequestDispatcher.ERROR_EXCEPTION;
import static javax.servlet.RequestDispatcher.ERROR_EXCEPTION_TYPE;
import static javax.servlet.RequestDispatcher.ERROR_MESSAGE;
import static javax.servlet.RequestDispatcher.ERROR_REQUEST_URI;
import static javax.servlet.RequestDispatcher.ERROR_STATUS_CODE;
import static org.omnifaces.util.Exceptions.unwrap;
import static org.omnifaces.util.Faces.getContext;
import static org.omnifaces.util.Faces.getServletContext;
import static org.omnifaces.util.FacesLocal.getLifecycle;
import static org.omnifaces.util.FacesLocal.getRequest;
import static org.omnifaces.util.FacesLocal.normalizeViewId;
import static org.omnifaces.util.Utils.isEmpty;
import static org.omnifaces.util.Utils.isOneInstanceOf;
import static org.omnifaces.util.Utils.unmodifiableSet;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Logger;
import javax.el.ELException;
import javax.faces.FacesException;
import javax.faces.application.ViewHandler;
import javax.faces.component.UIViewRoot;
import javax.faces.context.ExceptionHandler;
import javax.faces.context.ExceptionHandlerFactory;
import javax.faces.context.ExceptionHandlerWrapper;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.ExceptionQueuedEvent;
import javax.faces.event.PhaseId;
import javax.faces.lifecycle.Lifecycle;
import javax.faces.webapp.FacesServlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.omnifaces.config.WebXml;
import org.omnifaces.context.OmniPartialViewContext;
import org.omnifaces.context.OmniPartialViewContextFactory;
import org.omnifaces.filter.FacesExceptionFilter;
import org.omnifaces.util.Exceptions;
import org.omnifaces.util.Hacks;
/**
* <p>
* The {@link FullAjaxExceptionHandler} will transparently handle exceptions during ajax requests exactly the same way
* as exceptions during synchronous (non-ajax) requests.
* <p>
* By default, when an exception occurs during a JSF ajax request, the enduser would not get any form of feedback if the
* action was successfully performed or not. In Mojarra, only when the project stage is set to <code>Development</code>,
* the enduser would see a bare JavaScript alert with only the exception type and message. It would make sense if
* exceptions during ajax requests are handled the same way as exceptions during synchronous requests, which is
* utilizing the standard Servlet API <code><error-page></code> mechanisms in <code>web.xml</code>.
*
* <h3>Installation</h3>
* <p>
* This handler must be registered by a factory as follows in <code>faces-config.xml</code> in order to get it to run:
* <pre>
* <factory>
* <exception-handler-factory>org.omnifaces.exceptionhandler.FullAjaxExceptionHandlerFactory</exception-handler-factory>
* </factory>
* </pre>
*
* <h3>Error pages</h3>
* <p>
* This exception handler will parse the <code>web.xml</code> and <code>web-fragment.xml</code> files to find the error
* page locations of the HTTP error code <code>500</code> and all declared specific exception types. Those locations
* need to point to Facelets files (JSP is not supported) and the URL must match the {@link FacesServlet} mapping (just
* mapping it on <code>*.xhtml</code> should eliminate confusion about virtual URLs). E.g.
* <pre>
* <error-page>
* <exception-type>javax.faces.application.ViewExpiredException</exception-type>
* <location>/WEB-INF/errorpages/expired.xhtml</location>
* </error-page>
* </pre>
* <p>
* The location of the HTTP error code <code>500</code> or the exception type <code>java.lang.Throwable</code> is
* <b>required</b> in order to get the {@link FullAjaxExceptionHandler} to work, because there's then at least a fall
* back error page when there's no match with any of the declared specific exceptions types. You can have both, but the
* <code>java.lang.Throwable</code> one will always get precedence over all others. When you have error pages for
* specific exception types, then you'd better use the <code>500</code> one as fallback error page.
* <pre>
* <error-page>
* <error-code>500</error-code>
* <location>/WEB-INF/errorpages/500.xhtml</location>
* </error-page>
* </pre>
* <p>
* The exception detail is available in the request scope by the standard Servlet error request attributes like as in a
* normal synchronous error page response. You could for example show them in the error page as follows:
* <pre>
* <ul>
* <li>Date/time: #{of:formatDate(now, 'yyyy-MM-dd HH:mm:ss')}</li>
* <li>User agent: #{header['user-agent']}</li>
* <li>User IP: #{request.remoteAddr}</li>
* <li>Request URI: #{requestScope['javax.servlet.error.request_uri']}</li>
* <li>Ajax request: #{facesContext.partialViewContext.ajaxRequest ? 'Yes' : 'No'}</li>
* <li>Status code: #{requestScope['javax.servlet.error.status_code']}</li>
* <li>Exception type: #{requestScope['javax.servlet.error.exception_type']}</li>
* <li>Exception message: #{requestScope['javax.servlet.error.message']}</li>
* <li>Stack trace:
* <pre>#{of:printStackTrace(requestScope['javax.servlet.error.exception'])}</pre>
* </li>
* </ul>
* </pre>
* <p>
* Exceptions during render response can only be handled when the <code>javax.faces.FACELETS_BUFFER_SIZE</code> is
* large enough so that the so far rendered response until the occurrence of the exception fits in there and can
* therefore safely be resetted.
*
* <h3>Error in error page itself</h3>
* <p>
* When the rendering of the error page failed due to a bug in the error page itself, and the response can still be
* resetted, then the {@link FullAjaxExceptionHandler} will display a hardcoded error message in "plain text" informing
* the developer about the double mistake.
*
* <h3>Normal requests</h3>
* <p>
* Note that the {@link FullAjaxExceptionHandler} does not deal with normal (non-ajax) requests at all. To properly
* handle JSF and EL exceptions on normal requests as well, you need an additional {@link FacesExceptionFilter}. This
* will extract the root cause from a wrapped {@link FacesException} and {@link ELException} before delegating the
* {@link ServletException} further to the container (the container will namely use the first root cause of
* {@link ServletException} to match an error page by exception in web.xml).
*
* <h3>Configuration</h3>
* <p>
* By default only {@link FacesException} and {@link ELException} are unwrapped. You can supply a context parameter
* {@value org.omnifaces.exceptionhandler.FullAjaxExceptionHandler#PARAM_NAME_EXCEPTION_TYPES_TO_UNWRAP} to specify
* additional exception types to unwrap. The context parameter value must be a commaseparated string of fully qualified
* names of additional exception types. Note that this also covers subclasses of specified exception types.
* <pre>
* <context-param>
* <param-name>org.omnifaces.EXCEPTION_TYPES_TO_UNWRAP</param-name>
* <param-value>javax.ejb.EJBException,javax.persistence.RollbackException</param-value>
* </context-param>
* </pre>
* <p>
* This context parameter will also be read and used by {@link FacesExceptionFilter}.
* <p>
* By default all exceptions are logged. You can supply a context parameter
* {@value org.omnifaces.exceptionhandler.FullAjaxExceptionHandler#PARAM_NAME_EXCEPTION_TYPES_TO_IGNORE_IN_LOGGING} to
* specify exception types to ignore from logging. The context parameter value must be a commaseparated string of fully
* qualified names of exception types. Note that this also covers subclasses of specified exception types.
* <pre>
* <context-param>
* <param-name>org.omnifaces.EXCEPTION_TYPES_TO_IGNORE_IN_LOGGING</param-name>
* <param-value>javax.faces.application.ViewExpiredException</param-value>
* </context-param>
* </pre>
* <p>
* This context parameter will <strong>not</strong> suppress standard JSF and/or container builtin logging. This will
* only suppress <code>org.omnifaces.exceptionhandler.FullAjaxExceptionHandler</code> logging. So chances are that
* standard JSF and/or container will still log it. This may need to be configured separately.
*
* <h3>Customizing <code>FullAjaxExceptionHandler</code></h3>
* <p>
* If more fine grained control is desired for determining the root cause of the caught exception, or whether it should
* be handled, or determining the error page, or logging the exception, then the developer can opt to extend this
* {@link FullAjaxExceptionHandler} and override one or more of the following protected methods:
* <ul>
* <li>{@link #findExceptionRootCause(FacesContext, Throwable)}
* <li>{@link #shouldHandleExceptionRootCause(FacesContext, Throwable)}
* <li>{@link #findErrorPageLocation(FacesContext, Throwable)}
* <li>{@link #logException(FacesContext, Throwable, String, LogReason)}
* <li>{@link #logException(FacesContext, Throwable, String, String, Object...)}
* </ul>
* <p>
* Don't forget to create a custom {@link ExceptionHandlerFactory} for it as well, so that it could be registered
* in <code>faces-config.xml</code>. This does not necessarily need to extend from
* {@link FullAjaxExceptionHandlerFactory}.
*
* @author Bauke Scholtz
* @see FullAjaxExceptionHandlerFactory
* @see DefaultExceptionHandlerFactory
* @see OmniPartialViewContext
* @see OmniPartialViewContextFactory
* @see WebXml
* @see FacesExceptionFilter
*/
public class FullAjaxExceptionHandler extends ExceptionHandlerWrapper {
// Public constants -----------------------------------------------------------------------------------------------
/**
* The context parameter name to specify additional exception types to unwrap by both {@link FullAjaxExceptionHandler}
* and {@link FacesExceptionFilter}. Those will be added to exception types {@link FacesException} and {@link ELException}.
* @since 2.3
*/
public static final String PARAM_NAME_EXCEPTION_TYPES_TO_UNWRAP =
"org.omnifaces.EXCEPTION_TYPES_TO_UNWRAP";
/**
* The context parameter name to specify exception types to ignore in logging by {@link FullAjaxExceptionHandler}.
* @since 2.5
*/
public static final String PARAM_NAME_EXCEPTION_TYPES_TO_IGNORE_IN_LOGGING =
"org.omnifaces.EXCEPTION_TYPES_TO_IGNORE_IN_LOGGING";
/**
* This is used in {@link FullAjaxExceptionHandler#logException(FacesContext, Throwable, String, LogReason)}.
*
* @author Bauke Scholtz
* @since 2.4
*/
protected enum LogReason {
/** An exception occurred during processing JSF ajax request. Error page will be shown. */
EXCEPTION_HANDLED(LOG_EXCEPTION_HANDLED),
/** An exception occurred during rendering JSF ajax request. Error page will be shown. */
RENDER_EXCEPTION_HANDLED(LOG_RENDER_EXCEPTION_HANDLED),
/** An exception occurred during rendering JSF ajax request. Error page CANNOT be shown as response is already committed. */
RENDER_EXCEPTION_UNHANDLED(LOG_RENDER_EXCEPTION_UNHANDLED),
/** Another exception occurred during rendering error page. A hardcoded error page will be shown. */
ERROR_PAGE_ERROR(LOG_ERROR_PAGE_ERROR);
private final String message;
private LogReason(String message) {
this.message = message;
}
/**
* Returns the default message associated with the log reason.
* @return The default message associated with the log reason.
*/
public String getMessage() {
return message;
}
}
// Private constants ----------------------------------------------------------------------------------------------
private static final Logger logger = Logger.getLogger(FullAjaxExceptionHandler.class.getName());
private static final Set<Class<? extends Throwable>> STANDARD_TYPES_TO_UNWRAP =
unmodifiableSet(FacesException.class, ELException.class);
private static final String ERROR_INVALID_EXCEPTION_TYPES_PARAM_CLASS =
"Context parameter '%s' references a class which cannot be found in runtime classpath: '%s'";
private static final String ERROR_DEFAULT_LOCATION_MISSING =
"Either HTTP 500 or java.lang.Throwable error page is required in web.xml or web-fragment.xml."
+ " Neither was found.";
private static final String LOG_EXCEPTION_HANDLED =
"FullAjaxExceptionHandler: An exception occurred during processing JSF ajax request."
+ " Error page '%s' will be shown.";
private static final String LOG_RENDER_EXCEPTION_HANDLED =
"FullAjaxExceptionHandler: An exception occurred during rendering JSF ajax response."
+ " Error page '%s' will be shown.";
private static final String LOG_RENDER_EXCEPTION_UNHANDLED =
"FullAjaxExceptionHandler: An exception occurred during rendering JSF ajax response."
+ " Error page '%s' CANNOT be shown as response is already committed."
+ " Consider increasing 'javax.faces.FACELETS_BUFFER_SIZE' if it really needs to be handled.";
private static final String LOG_ERROR_PAGE_ERROR =
"FullAjaxExceptionHandler: Well, another exception occurred during rendering error page '%s'."
+ " Trying to render a hardcoded error page now.";
private static final String ERROR_PAGE_ERROR =
"<?xml version='1.0' encoding='UTF-8'?><partial-response id='error'><changes><update id='javax.faces.ViewRoot'>"
+ "<![CDATA[<html lang='en'><head><title>Error in error</title></head><body><section><h2>Oops!</h2>"
+ "<p>A problem occurred during processing the ajax request. Subsequently, another problem occurred during"
+ " processing the error page which should inform you about that problem.</p><p>If you are the responsible"
+ " web developer, it's time to read the server logs about the bug in the error page itself.</p></section>"
+ "</body></html>]]></update></changes></partial-response>";
// Variables ------------------------------------------------------------------------------------------------------
private ExceptionHandler wrapped;
private Class<? extends Throwable>[] exceptionTypesToUnwrap;
private Class<? extends Throwable>[] exceptionTypesToIgnoreInLogging;
// Constructors ---------------------------------------------------------------------------------------------------
/**
* Construct a new ajax exception handler around the given wrapped exception handler.
* @param wrapped The wrapped exception handler.
*/
public FullAjaxExceptionHandler(ExceptionHandler wrapped) {
this.wrapped = wrapped;
exceptionTypesToUnwrap = getExceptionTypesToUnwrap(getServletContext());
exceptionTypesToIgnoreInLogging = getExceptionTypesToIgnoreInLogging(getServletContext());
}
/**
* Get the exception types to unwrap. This contains at least the standard types to unwrap {@link FacesException} and
* {@link ELException}. Additional types can be specified via context parameter
* {@value org.omnifaces.exceptionhandler.FullAjaxExceptionHandler#PARAM_NAME_EXCEPTION_TYPES_TO_UNWRAP}, if any.
* @param context The involved servlet context.
* @return Exception types to unwrap.
* @since 2.3
*/
public static Class<? extends Throwable>[] getExceptionTypesToUnwrap(ServletContext context) {
return parseExceptionTypesParam(context, PARAM_NAME_EXCEPTION_TYPES_TO_UNWRAP, STANDARD_TYPES_TO_UNWRAP);
}
/**
* Get the exception types to ignore in logging. This can be specified via context parameter
* {@value org.omnifaces.exceptionhandler.FullAjaxExceptionHandler#PARAM_NAME_EXCEPTION_TYPES_TO_IGNORE_IN_LOGGING}.
* @param context The involved servlet context.
* @return Exception types to ignore in logging.
* @since 2.5
*/
private static Class<? extends Throwable>[] getExceptionTypesToIgnoreInLogging(ServletContext context) {
return parseExceptionTypesParam(context, PARAM_NAME_EXCEPTION_TYPES_TO_IGNORE_IN_LOGGING, null);
}
@SuppressWarnings("unchecked")
private static Class<? extends Throwable>[] parseExceptionTypesParam(ServletContext context, String paramName, Set<Class<? extends Throwable>> defaults) {
Set<Class<? extends Throwable>> types = new HashSet<>();
if (defaults != null) {
types.addAll(defaults);
}
String typesParam = context.getInitParameter(paramName);
if (!isEmpty(typesParam)) {
for (String typeParam : typesParam.split("\\s*,\\s*")) {
try {
types.add((Class<? extends Throwable>) Class.forName(typeParam));
}
catch (ClassNotFoundException e) {
throw new IllegalArgumentException(
format(ERROR_INVALID_EXCEPTION_TYPES_PARAM_CLASS, paramName, typeParam), e);
}
}
}
return types.toArray(new Class[types.size()]);
}
// Actions --------------------------------------------------------------------------------------------------------
/**
* Handle the ajax exception as follows, only and only if the current request is an ajax request with an uncommitted
* response and there is at least one unhandled exception:
* <ul>
* <li>Find the root cause of the exception by {@link #findExceptionRootCause(FacesContext, Throwable)}.
* <li>Find the error page location based on root cause by {@link #findErrorPageLocation(FacesContext, Throwable)}.
* <li>Set the standard servlet error request attributes.
* <li>Force JSF to render the full error page in its entirety.
* </ul>
* Any remaining unhandled exceptions will be swallowed. Only the first one is relevant.
*/
@Override
public void handle() {
handleAjaxException(getContext());
wrapped.handle();
}
private void handleAjaxException(FacesContext context) {
if (context == null || !context.getPartialViewContext().isAjaxRequest()) {
return; // Not an ajax request.
}
Iterator<ExceptionQueuedEvent> unhandledExceptionQueuedEvents = getUnhandledExceptionQueuedEvents().iterator();
if (!unhandledExceptionQueuedEvents.hasNext()) {
return; // There's no unhandled exception.
}
Throwable exception = unhandledExceptionQueuedEvents.next().getContext().getException();
if (exception instanceof AbortProcessingException) {
return; // Let JSF handle it itself.
}
unhandledExceptionQueuedEvents.remove();
exception = findExceptionRootCause(context, exception);
if (!shouldHandleExceptionRootCause(context, exception)) {
return; // A subclass apparently want to do it differently.
}
String errorPageLocation = findErrorPageLocation(context, exception);
if (errorPageLocation == null) {
throw new IllegalArgumentException(ERROR_DEFAULT_LOCATION_MISSING);
}
if (!canRenderErrorPageView(context, exception, errorPageLocation)) {
return; // If error page cannot be rendered, then it's end of story.
}
// Set the necessary servlet request attributes which a bit decent error page may expect.
HttpServletRequest request = getRequest(context);
request.setAttribute(ERROR_EXCEPTION, exception);
request.setAttribute(ERROR_EXCEPTION_TYPE, exception.getClass());
request.setAttribute(ERROR_MESSAGE, exception.getMessage());
request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI());
request.setAttribute(ERROR_STATUS_CODE, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
try {
renderErrorPageView(context, request, errorPageLocation);
}
catch (IOException e) {
throw new FacesException(e);
}
while (unhandledExceptionQueuedEvents.hasNext()) {
// Any remaining unhandled exceptions are not interesting. First fix the first.
unhandledExceptionQueuedEvents.next();
unhandledExceptionQueuedEvents.remove();
}
}
/**
* Determine the root cause based on the caught exception, which will then be used to find the error page location.
* The default implementation delegates to {@link Exceptions#unwrap(Throwable, Class...)} with {@link FacesException},
* {@link ELException} and the types specified in context parameter
* {@value org.omnifaces.exceptionhandler.FullAjaxExceptionHandler#PARAM_NAME_EXCEPTION_TYPES_TO_UNWRAP}, if any.
* @param context The involved faces context.
* @param exception The caught exception to determine the root cause for.
* @return The root cause of the caught exception.
* @since 1.5
*/
protected Throwable findExceptionRootCause(FacesContext context, Throwable exception) {
return unwrap(exception, exceptionTypesToUnwrap);
}
/**
* Returns <code>true</code> if the {@link FullAjaxExceptionHandler} should handle this exception root cause. If
* this returns <code>false</code>, then the {@link FullAjaxExceptionHandler} will skip handling this exception and
* delegate it further to the wrapped exception handler. The default implementation just returns <code>true</code>.
* @param context The involved faces context.
* @param exception The caught exception to determine the root cause for.
* @return <code>true</code> if the given exception should be handled by the {@link FullAjaxExceptionHandler}.
* @since 1.8
*/
protected boolean shouldHandleExceptionRootCause(FacesContext context, Throwable exception) {
return true;
}
/**
* Determine the error page location based on the given exception.
* The default implementation delegates to {@link WebXml#findErrorPageLocation(Throwable)}.
* @param context The involved faces context.
* @param exception The exception to determine the error page for.
* @return The location of the error page. It must start with <code>/</code> and be relative to the context path.
* @since 1.5
*/
protected String findErrorPageLocation(FacesContext context, Throwable exception) {
return WebXml.INSTANCE.findErrorPageLocation(exception);
}
/**
* Log the thrown exception and determined error page location for the given log reason.
* The default implementation delegates to {@link #logException(FacesContext, Throwable, String, String, Object...)}
* with the default message associated with the log reason.
* @param context The involved faces context.
* @param exception The exception to log.
* @param location The error page location.
* @param reason The log reason.
* @since 2.4
*/
protected void logException(FacesContext context, Throwable exception, String location, LogReason reason) {
logException(context, exception, location, reason.getMessage());
}
/**
* Log the thrown exception and determined error page location with the given message, optionally parameterized
* with the given parameters.
* The default implementation logs through <code>java.util.logging</code> as SEVERE when the thrown exception is
* not an instance of any type specified in context parameter
* {@value org.omnifaces.exceptionhandler.FullAjaxExceptionHandler#PARAM_NAME_EXCEPTION_TYPES_TO_IGNORE_IN_LOGGING}.
* @param context The involved faces context.
* @param exception The exception to log.
* @param location The error page location.
* @param message The log message.
* @param parameters The log message parameters, if any.
* @since 1.6
*/
protected void logException(FacesContext context, Throwable exception, String location, String message, Object... parameters) {
if (!isOneInstanceOf(exception.getClass(), exceptionTypesToIgnoreInLogging)) {
logger.log(SEVERE, format(message, location), exception);
}
}
private boolean canRenderErrorPageView(FacesContext context, Throwable exception, String errorPageLocation) {
if (context.getCurrentPhaseId() != PhaseId.RENDER_RESPONSE) {
logException(context, exception, errorPageLocation, LogReason.EXCEPTION_HANDLED);
return true;
}
else if (!context.getExternalContext().isResponseCommitted()) {
logException(context, exception, errorPageLocation, LogReason.RENDER_EXCEPTION_HANDLED);
resetResponse(context); // If the exception was thrown in midst of rendering the JSF response, then reset (partial) response.
return true;
}
else {
logException(context, exception, errorPageLocation, LogReason.RENDER_EXCEPTION_UNHANDLED);
// Mojarra doesn't close the partial response during render exception. Let do it ourselves.
OmniPartialViewContext.getCurrentInstance(context).closePartialResponse();
return false;
}
}
private void resetResponse(FacesContext context) {
ExternalContext externalContext = context.getExternalContext();
String contentType = externalContext.getResponseContentType(); // Remember content type.
String characterEncoding = externalContext.getResponseCharacterEncoding(); // Remember encoding.
externalContext.responseReset();
OmniPartialViewContext.getCurrentInstance(context).resetPartialResponse();
externalContext.setResponseContentType(contentType);
externalContext.setResponseCharacterEncoding(characterEncoding);
}
private void renderErrorPageView(FacesContext context, final HttpServletRequest request, String errorPageLocation)
throws IOException
{
String viewId = getViewIdAndPrepareParamsIfNecessary(context, errorPageLocation);
ViewHandler viewHandler = context.getApplication().getViewHandler();
UIViewRoot viewRoot = viewHandler.createView(context, viewId);
context.setViewRoot(viewRoot);
context.getPartialViewContext().setRenderAll(true);
Hacks.removeResourceDependencyState(context);
try {
context.setCurrentPhaseId(PhaseId.RENDER_RESPONSE);
Lifecycle lifecycle = getLifecycle(context);
lifecycle.execute(context);
lifecycle.render(context);
context.responseComplete();
}
catch (Exception e) {
// Apparently, the error page itself contained an error.
logException(context, e, errorPageLocation, LogReason.ERROR_PAGE_ERROR);
ExternalContext externalContext = context.getExternalContext();
if (!externalContext.isResponseCommitted()) {
// Okay, reset the response and tell that the error page itself contained an error.
resetResponse(context);
externalContext.setResponseContentType("text/xml");
externalContext.getResponseOutputWriter().write(ERROR_PAGE_ERROR);
context.responseComplete();
}
else {
// Well, it's too late to handle. Just let it go.
throw new FacesException(e);
}
}
finally {
// Prevent some servlet containers from handling error page itself afterwards. So far Tomcat/JBoss
// are known to do that. It would only result in IllegalStateException "response already committed"
// or "getOutputStream() has already been called for this response".
request.removeAttribute(ERROR_EXCEPTION);
}
}
private String getViewIdAndPrepareParamsIfNecessary(FacesContext context, String errorPageLocation) {
String[] parts = errorPageLocation.split("\\?", 2);
// TODO: #287: make params available via #{param(Values)}. Request wrapper needed :|
return normalizeViewId(context, parts[0]);
}
@Override
public ExceptionHandler getWrapped() {
return wrapped;
}
}