Temario detallado‎ > ‎5: Web Server Side‎ > ‎Java‎ > ‎

Desarrollo Web: Validaciones y Manejo de Errores

Manejo de errores - validaciones cliente vs. servidor

¿Qué pasa si
  • queremos sumar un número con un caracter?
  • la fecha que ingresamos no tiene el formato correcto?
  • la fecha debe ser menor a la fecha de hoy?
  • el socio de una biblioteca no debe ser deudor para retirar el libro?
  • el mismo número de celular no puede ser utilizado por dos clientes distintos?

Claro, hay validaciones. 
¿cómo se agregaron esas validaciones en Arena?
  1. Definiendo el objeto responsable de dicha validación, que se traduce en un if + throw excepción. En algunos casos el error se produce en el controller, cuando se quiere setear un valor incorrecto. En otros casos el error lo tira el dominio (ej: fecha menor a fecha de hoy o socio no deudor). En otros el error lo tira el home (ej: evitar que dos clientes tengan el mismo celular)
  2. El panel lo que tiene que hacer es evitar que el stack trace se propague hasta cancelar la app. Por eso se escribe un bloque try/catch distinguiendo los errores de negocio y los de programa.
En Web, tenemos algunas decisiones más que tomar, además de definir qué objeto es responsable de agregar la validación.
  1. La validación ¿hay que ponerla del lado del cliente o del servidor?
  2. ¿Cómo vamos a manejar la excepción siendo que el "panel" es una página HTML que corre en un browser? O sea, ¿si el negocio tira una excepción, qué resultado produce en nuestra aplicación?
Sobre la pregunta 1), tenemos opiniones encontradas:
a) Colocar las validaciones sólo del lado del servidor tiene algunas ventajas
  • Conservamos el principio Don't Repeat Yourself, o Once and Only Once
  • Si el browser no permite correr código javascript, la aplicación puede funcionar correctamente de todos modos
Pero también tiene desventajas:
  • El formulario es estático hasta que se dispare la petición al servidor
  • Aumenta el tráfico entre cliente y servidor, en algunos casos en forma innecesaria. 
b) Colocar las validaciones sólo en el cliente disminuye la comunicación cliente-servidor, pero por otra parte
  • No todas las validaciones pueden codificarse client-side (ej: dos clientes con el mismo cuit, fecha que no se superponga con un rango de fechas existente, etc.) 
  • En aplicaciones sensibles suele ser un problema que el cliente pueda alterar la página HTML, suprimiendo por ejemplo las validaciones que aplican sobre un formulario. 
c) Una tercera opción es replicar validaciones en cliente y servidor. Es claro que de esta manera estamos duplicando lógica en dos lenguajes diferentes (ya hemos visto que Java y javascript no se parecen tanto como uno supondría), entonces debemos hacer un análisis costo-beneficio entre respetar la unicidad de ideas de diseño/mantenimiento vs. las cualidades de performance y de usabilidad y decidir en consecuencia.
   

Pre-Validaciones 

En la calculadora de javascript vimos que sólo se habilitaba el botón submit si los dos operandos eran números, incluso el botón de división sólo aparecía habilitado si el segundo operando no era cero. En general es una buena idea hacer que la interfaz de usuario sirva de guía: evitar que pueda disparar acciones inválidas lo contiene y aumenta la confianza en la aplicación. Así se puede habilitar/hacer visibles las acciones a medida que se llene la información necesaria para esa acción. 

No obstante, no siempre es posible este approach, sobre todo en formularios donde hay reglas de validación complejas. Tenemos que tener cuidado en este aspecto de no confundir al usuario haciendo desaparecer y aparecer el botón Aceptar en un formulario donde se ingresan 20 campos.
   

Ejemplos donde ver validaciones:

Videoclub

  • Validaciones del formulario de alta (en cliente) del socio 
  • No es posible eliminar un socio que tiene pedidos. ¿Qué opciones posibles tenemos?
    • Server side: el servlet que elimina un socio pregunta si tiene pedidos, en ese caso se debe disparar un mensaje de error
    • Mix (utilizando pre-validaciones): en la búsqueda, el servlet podría enviar junto con la información del cliente un flag booleano que indique si tiene pedidos (o bien guardar en la session la lista de socios del videoclub con los pedidos que fue generando). Entonces
      •  el jsp que lista los socios podría mostrar el link "Eliminar" o no dependiendo de si el socio tiene o no pedidos 
      • O bien dejar habilitado el link Eliminar y mostrar un error si se selecciona esa opción. Esto obviamente produce una sensación frustrante para el usuario, al cual le ofrecen una acción que en realidad no está disponible. 
 Vemos el código del servlet de Eliminación:
 
public void executeTask(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Home<Socio> homeSocios = Videoclub.getInstance().getHome(Socio.class);
String idSocioAEliminar = request.getParameter("idSocioSeleccionado");
if (idSocioAEliminar == null) {
throw new BusinessException("No seleccionó un socio a eliminar");
}
Socio socioAEliminar = homeSocios.searchById(new Integer(idSocioAEliminar));
homeSocios.delete(socioAEliminar);
response.sendRedirect("./BuscarSocioServlet");
}

 La validación la hace el home (delegando la pregunta al objeto de negocio Socio), a través de un hook method validarEliminación que permite insertar validaciones previas a la eliminación. 

protected void validarEliminacion(Socio socio) {
if (!socio.puedeEliminarse()) {
throw new BusinessException("No se puede eliminar al cliente porque tiene pedidos");
}
}

 Ahora, en el executeTask, ¿no hay bloque try/catch? Tranquilos, está en WorkflowVideoclubServlet, la superclase común de todos los servlets del Videoclub:

private void processRequest(HttpServletRequest request, HttpServletResponse response) throws ... {
try {
this.executeTask(request, response);
} catch (BusinessException e) {
this.mostrarError(request, response, e.getMessage());
} catch (UserException e) {
this.mostrarError(request, response, e.getMessage());
}
}

executeTask() es un método abstracto en la superclase, como todo template method y así cada servlet lo resuelve a su manera.
 
En resumen, vemos que el servlet hace un try/catch para recibir el BusinessException. Si todo funciona ok, se redirige al servlet que "refresca" la búsqueda de socios en la session y finalmente forwardea a la JSP que muestra la grilla nuevamente.

Si se fijan bien del servlet de Eliminación al de Nueva Búsqueda hay un sendRedirect, en lugar del forward.
Nosotros recomendamos:
  • de un servlet a una jsp se usa forward: en este caso el browser no interviene, es el web server el que despacha el pedido del servlet al jsp sin que el usuario vea cambios en la URL del navegador: esto tiene sentido porque el requerimiento de buscar se completa cuando la jsp recibe los parámetros que necesita del servlet. El "caso de uso" es el que lleva el nombre del servlet, los componentes que resuelven en ese caso son el servlet + jsp.
  • de un servlet a otro servlet se usa sendRedirect, donde el web server devuelve la respuesta al browser indicándole la URL de destino, entonces el browser simula "hacer" el pedido para comenzar otro caso de uso: es el caso de la eliminación para luego hacer la búsqueda, son dos requerimientos diferentes.

Un artículo interesante que explica esto es el de Jaisankar:

Forward( ) :   javax.Servlet.RequestDispatcher interface. 
  • RequestDispatcher.forward( ) works on the Server.  
  • The forward( ) works inside the WebContainer. 
  • The forward( ) restricts you to redirect only to a resource in the same web-Application.  
  • After executing the forward( ), the control will return back to the same method from where the forward method was called.  
  • The forward( ) will redirect in the application server itself, it doesn't come back to the client.  
  • The forward( ) is faster than Sendredirect( ) . 

To use the forward( ) of the requestDispatcher interface, the first thing to do is to obtain RequestDispatcher Object. The Servlet technology provides in three ways. 

1. By using the getRequestDispatcher( ) of the javax.Servlet.ServletContext interface , passing a String containing the path of the other resources, path is relative to the root of the ServletContext. 
RequestDispatcher rd=request.getRequestDispatcher ("secondServlet"); 
Rd.forward(request, response); 

2.   getRequestDispatcher( ) of the javax.Servlet.Request interface  , the path is relative to current HtpRequest. 
RequestDispatcher rd=getServletContext( ).getRequestDispatcher("servlet/secondServlet");  
Rd.forward(request, response); 

3. By using the getNameDispatcher( ) of the javax.Servlet.ServletContext interface. 
RequestDispatcher rd=getServletContext( ).getNameDispatcher("secondServlet");  
Rd.forward(request, response); 

Sendredirect( ) : javax.Servlet.Http.HttpServletResponce interface 
  • RequestDispatcher.SendRedirect( ) works on the browser.  
  • The SendRedirect( ) allows you to redirect trip to the Client.  
  • The SendRedirect( ) allows you to redirect to any URL.  
  • After executing the SendRedirect( ) the control will not return back to same method.  
  • The Client receives the Http response code 302 indicating that temporarly the client is being redirected to the specified location , if the specified location is relative , this method converts it into an absolute URL before redirecting.  
  • The SendRedirect( ) will come to the Client and go back,.. ie URL appending will happen. 
Response. SendRedirect( "absolute path");  
Absolutepath – other than application ,   relative path - same application.

When you invoke a forward request, the request is sent to another resource on the server, without the client being informed that a different resource is going to process the request. This process occurs completely within the web container. When a sendRedirtect method is invoked, it causes the web container to return to the browser indicating that a new URL should be requested. Because the browser issues a completely new request any object that are stored as request attributes before the redirect occurs will be lost. This extra round trip a redirect is slower than forward. 

Error page

En definitiva estamos utilizando una página que sirve de "message box" para manejar los errores:
  • si es un error de negocio, debería mostrar un mensaje (de warning o del estilo que se quiera, se pueden buscar/generar imágenes representativas de lo que es un error de negocio)
  • si es un error de sistema/programa, debería loguear el trace y mostrar un mensaje del estilo "Ocurrió un error al procesar pedido XXXXX, por favor consulte al administrador del sistema"
En Arena el messageBox era modal, el formulario no permitía cambiar de panel en la misma aplicación. Cuando se cerraba el message box el flujo volvía al panel original donde el error ocurrió. Aquí debemos pasarle esa información a la página de error para poder volver a través de un botón que submitee al caso de uso nuevamente. Para no perder toda la info que se estuvo generando en el formulario tenemos que manejar el estado de una forma conveniente para el usuario y seguramente poco feliz para nosotros (session, request). En nuestro caso cuando ocurre un error redirigimos el control al error.jsp

La idea original era ofrecer a los programadores la posibilidad de hacer una vista custom para mostrar errores en forma elegante...

Links relacionados

Mensajes en la misma página

No obstante su buena intención, las error pages han sido reemplazadas por mensajes de error formateadas con estilos (css) por diferentes cuestiones
  • estéticamente es un poco brusco para el usuario salir de su página hacia otra que muestra un mensaje de error donde es poco probable que vea la información que fue cargando en el mismo orden y con la misma disposición
  • además la vuelta supone un trabajo extra de programación para dejarlo en el estado anterior a la ejecución de la acción, para que el usuario pueda corregir la información cargada
  • si se trata de un error de sistema, al menos permite la posibilidad de copiar la imagen para facilitar la reproducción del error (en lugar de una pantalla en blanco con un mensaje críptico como "Ha ocurrido un error de sistema")
La forma de crear una zona de mensajes es bastante trivial, se suelen usar divs en la parte superior o inferior de la pantalla con un background/font en tonalidades de rojo para errores, amarillo para advertencias o azules/verdes para mensajes de éxito ante operaciones.

Ejemplo práctico

celulares-ui-jsp es el ejemplo de la cátedra que permite echar un vistazo a cómo se manejan validaciones y errores de sistema. En el ejemplo se muestra una pantalla de carga de un cliente para una empresa de celulares pero incorporando la posibilidad de que las validaciones las haga:
  • client-side mostrando todos los errores
  • client-side mostrando de a un error por vez
  • server-side

Algunos apuntes
  • El formulario tiene dos zonas de mensajes
    • Los mensajes de error se muestran en rojo (div class="error")
    • El mensaje por ok se muestra en azul (div class="info")
    • Esto lo define el .css styles.css
  • La validación client-side que muestra de a un error por vez trabaja con exceptions de Javascript. El problema de manejar validaciones con excepciones como hemos venido haciendo es que al cortar el flujo de envío de mensajes no permite saber todos los errores que tiene un formulario. Por otra parte sabemos más fácilmente si la validación pasó bien o no (mediante un bloque try/catch)
  • La validación client-side que muestra todos los errores utiliza la técnica de concatenar los mensajes con viñetas (<ul><li>). Esto permite al usuario  rápidamente darse cuenta de todos los campos incorrectos del formulario, aunque requiere trabajar un poco para saber si la validación pasó ok o no.
  • Por último, la validación server-side trae algunas complicaciones:
    • Del lado cliente nunca generamos el objeto Celular, ni tampoco el ModeloCelular asociado. Pero del lado del servidor tenemos objetos Java, en particular el objeto de dominio Celular que tiene implementado su método validar(). 
    • Entonces el servlet actúa de adapter, recibimos los parámetros y los convertimos en un celular. 
    • Para generar un objeto celular necesitamos hacer la conversión del parámetro "numero" del request a un Integer. Si falla la conversión (NumberFormatException) sabemos que hay un error de validación de número.
    • También tenemos que adaptar un identificador del modelo de celular para llevarlo al objeto ModeloCelular y seteárselo al Celular.
    • Una vez que tenemos al objeto Celular construido, delegamos la validación. 
    • Esto puede arrojar una excepción de negocio, entonces tenemos que atrapar la excepción para que no salte al usuario. Lo que hacemos es pasar un mensaje de error a través del request y forwardear a la misma index.jsp
    • Cuando la jsp index se carga, debemos verificar si recibimos el mensaje dentro del request. Tenemos tres opciones:
      • recibimos un "ok", significa que venimos de validar el formulario en el servidor y la respuesta que vuelve es ok, entonces muestro el mensaje ok en azul
      • recibimos algo distinto de "ok", significa que venimos de validar el formulario en el servidor y hubo errores, entonces lo muestro en rojo
      • recibimos el mensaje vacío, significa que la página está cargando por primera vez: entonces no mostramos ninguno de los divs (ni error ni ok), dejamos que el usuario cargue la información y valide
    • Otra de las cuestiones interesantes para ver es cómo recuperar la información que tenía el formulario index.jsp al ir y volver al servidor
      • el radio button tipo de validación
      • el select (combo) también, para eso estamos guardando en un input oculto (hidden) el id del modelo seleccionado
      • y el mensaje de error lo guardamos también en un input oculto, para poder manipularlo en el evento onload del body: así sabemos si en el request estamos recibiendo un mensaje ok/error. Recordemos que javascript corre en el browser, si no guardamos la info en un hidden no tenemos forma de recuperarla (el request se puede manipular pero cuando estamos del lado del server)