Durante la ejecución de una aplicación se pueden producir excepciones en el servidor por diversas causas, un problema de acceso a la base de datos, un error de programación, etc. En el caso de JSF 2, durante las peticiones HTTP, se delega en el sistema que ofrece el contenedor de servlets para el tratamiento de las excepciones. Mediante la definición de reglas del tipo <error-page> en el web.xml de la aplicación podemos conseguir que ante determinadas excepciones del servidor se redirija la petición a una página de error concreta.
JSF 2 además nos ofrece la posibilidad de hilar más fino mediante un ExceptionHandler. La extensión de las clases ExceptionHandlerWrapper y ExceptionHandlerFactory nos permite capturar las excepciones que salten durante la ejecución del ciclo de vida de JSF y hacer un tratamiento concreto dependiendo de la excepción producida.
Este mecanismo es muy útil para el tratamiento de excepciones dentro de llamadas AJAX en una aplicación JSF, puesto que tanto Mojarra como MyFaces ignoran por defecto este tipo de excepciones con las configuraciones de producción, con lo que el usuario no es consciente de que ha ocurrido un error grave en la aplicación.
Vamos a ver como podríamos solucionar este problema con un ejemplo sencillo en el que vamos a redirigir a una página determinada en caso de encontrarnos con una excepción durante una petición AJAX.
Por un lado declaramos nuestra factoría de ExceptionHandler:
public class AjaxExceptionHandlerFactory extends ExceptionHandlerFactory {
/**
* wrapped
*/
private ExceptionHandlerFactory wrapped;
/**
* Constructor de una factoria para el manejo de excepciones AJAX.
*
* @param wrapped La factoría que se encapsula.
*/
public AjaxExceptionHandlerFactory(ExceptionHandlerFactory wrapped) {
this.wrapped = wrapped;
}
/**
* Devuelve una nueva instancia de AjaxExceptionHandler que
* envuelve el exception handler original.
* @return ExceptionHandler ExceptionHandler
*/
@Override
public ExceptionHandler getExceptionHandler() {
return new AjaxExceptionHandler(
getWrapped().getExceptionHandler());
}
/**
* Devuelve la factoría encapsulada.
* @return ExceptionHandlerFactory ExceptionHandlerFactory
*/
@Override
public ExceptionHandlerFactory getWrapped() {
return wrapped;
}
}
A continuación nuestro ExceptionHandler:
public
class
AjaxExceptionHandler
extends
ExceptionHandlerWrapper {
/**
* Logger.
*/
public
static
final
Log LOG =
LogFactory.getLog(AjaxExceptionHandler.
class
);
/**
* Exception handler encapsulado
*/
private
ExceptionHandler wrapped;
/**
* Constructos de un nuevo exception handler para peticiones
* ajax encapsulando el exception handler indicado.
*
* @param wrapped El exception handler encapsulado.
*/
public
AjaxExceptionHandler(ExceptionHandler wrapped) {
this
.wrapped = wrapped;
}
/**
* Maneja las excepciones en peticiones ajax de la siguiente manera,
* sólo y sólo si la actual petición es una petición ajax cuya
* respuesta aún no ha sido enviada y existe al menos una excepción
* que no ha sido tratada.
*
* Las demás excepciones pendientes serán ignoradas, primero hay que
* corregir la primera.
*/
@Override
public
void
handle() {
handleAjaxException(getContext());
wrapped.handle();
}
@Override
public
ExceptionHandler getWrapped() {
return
wrapped;
}
/**
* Metodo que devuelve el contexto JSF
* @return Contexto JSF actual
*/
private
static
FacesContext getContext() {
return
FacesContext.getCurrentInstance();
}
/**
* Método que se encarga de tratar las excepciones encontradas
* durante una petición JSF. Sólo se van a tratar las excepciones en
* peticiones ajax. Si la excepción es en una petición HTTP normal ya
* se encarga el web.xml de redirigir a la página de error.
*
* @param context Contexto JSF actual.
*/
private
void
handleAjaxException(FacesContext context) {
if
(context ==
null
|| !context.getPartialViewContext().isAjaxRequest()) {
return
;
}
Iterator<ExceptionQueuedEvent> unhandledExcQueuedEvents =
getUnhandledExceptionQueuedEvents()
.iterator();
if
(!unhandledExcQueuedEvents.hasNext()) {
return
;
}
Throwable exception = unhandledExcQueuedEvents.next()
.getContext().getException();
if
(exception
instanceof
AbortProcessingException) {
return
;
}
exception = findExceptionRootCause(exception);
String errorPageLocation =
"/errorPage.xhtml"
;
unhandledExcQueuedEvents.remove();
ExternalContext externalContext = context.getExternalContext();
LOG.error(String.format(
"Ocurrio un error no esperado, redirigiendo a %s"
,
errorPageLocation), exception);
HttpServletRequest request =
(HttpServletRequest) externalContext.getRequest();
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
(unhandledExcQueuedEvents.hasNext()) {
unhandledExcQueuedEvents.next();
unhandledExcQueuedEvents.remove();
}
}
/**
* Determina la raiz de la causa de una excepción.
*
* @param exception La excepción de la que se quiere encontrar
* la raiz de la causa.
* @return La excepción raiz de la causa de la excepción primera.
*/
private
Throwable findExceptionRootCause(Throwable exception) {
return
unwrap(exception);
}
/**
* Desenvuelve las causas anidadas de una determinada excepción
* mientras no se encuentre una instancia del tipo indicado,
* entonces se devuelve dicha instancia.
*
* @param <T> El tipo genérico throwable.
* @param exception La excepción a desenvolver.
* @param type El tipo de excepción que tiene que ser devuelto.
* @return La raiz de la causa de la excepción inicial.
*/
private
static
<T
extends
Throwable> Throwable unwrap(
Throwable exception, Class<T> type) {
while
(type.isInstance(exception)
&& exception.getCause() !=
null
) {
exception = exception.getCause();
}
return
exception;
}
/**
* Devuelve las causas anidadas de una excepción dada mientras no
* sean instancias de FacesException (Mojarra) o
* ELException (MyFaces).
*
* @param exception La excepción de la que se quiere quitar el
* anidamiento con FacesException y ELException.
* @return La causa de la excepción.
*/
private
static
Throwable unwrap(Throwable exception) {
return
unwrap(
unwrap(exception, FacesException.
class
),
ELException.
class
);
}
/**
* Muestra la página de error indicada.
*
* @param context Contexto JSF actual
* @param request Request de la petición actual
* @param errorPageLocation Localización de la página
* de error a mostrar.
* @throws IOException En caso de que ocurra un error
* mostrando la página de error, y no se
* pueda mostrar la página de error de emergencia.
*/
private
void
renderErrorPageView(FacesContext context,
final
HttpServletRequest request,
String errorPageLocation)
throws
IOException {
String viewId = errorPageLocation;
ViewHandler viewHandler = context
.getApplication().getViewHandler();
UIViewRoot viewRoot = viewHandler.createView(context, viewId);
context.setViewRoot(viewRoot);
context.getPartialViewContext().setRenderAll(
true
);
try
{
ViewDeclarationLanguage vdl =
viewHandler.getViewDeclarationLanguage(context, viewId);
vdl.buildView(context, viewRoot);
context.getApplication().publishEvent(
context,PreRenderViewEvent.
class
, viewRoot);
vdl.renderView(context, viewRoot);
context.responseComplete();
}
catch
(Exception e) {
throw
new
FacesException(e);
}
finally
{
request.removeAttribute(ERROR_EXCEPTION);
}
}
}
Por último declaramos nuestra factoría en el faces-config.xml de nuestra aplicación:
<factory>
<exception-handler-factory>
org.exceptionhandler.AjaxExceptionHandlerFactory
</exception-handler-factory>
</factory>
Esta solución está basada en la propuesta
por la librería Omnifaces. Consutar en las referencias la documentación
del FullAjaxExceptionHandler de Omnifaces para una implementación más
completa y con ajustes para una mejor integración con distintos
frameworks JSF.
Más información:
https://docs.oracle.com/javaee/6/api/javax/faces/context/ExceptionHandler.html
http://showcase.omnifaces.org/exceptionhandlers/FullAjaxExceptionHandler
http://balusc.blogspot.com.es/2012/03/full-ajax-exception-handler.html
https://weblogs.java.net/blog/edburns/archive/2009/09/03/dealing-gracefully-viewexpiredexception-jsf2
http://www.beyondjava.net/blog/jsf-2-0-hides-exceptions-ajax/