sábado, 5 de septiembre de 2009

Spring MVC SimpleFormController

Veamos una clase que nos permitirá controlar las acciones que realicemos con los formularios de una manera más simple: SimpleFormController

El caso de uso a desarrollar es uno bien simple de registro de usuarios, los requerimientos son los siguientes:

1. Si el actor o persona que va a registrar al usuario no está autenticada en el sistema entonces redirigirla hacia una página para que se autentique.

Solución a 1. Extensión de la clase HandlerInterceptorAdapter para implementar nuestro propio interceptor de solicitudes hacia un controlador específico, en este caso el controlador es un SimpleFormController, el componente que verificará la existencia del usuario en el contexto de sesion es el Interceptor.

2. Uso de etiquetas de formulario de spring, la libreria a usar será:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

Ahora veamos el código:

Lo primero que tenemos que hacer es implementar nuestro objeto Command, en este caso el objeto command es un javabean simple:

Usuario.java
package pe.com.slcsccy.springmvc.dominio;
import java.util.Date;

public class Usuario {

private String codigo;
private String nombres;
private String apellidos;
private String perfil;
private String multisesion;
private Boolean habilitado;
private String[] aplicaciones;
private String nacionalidad;
private Date fechaRegistro;
private Date fechaCese;

//Continuan setters and getters....
}

Ahora vamos a crear nuestro controlador:

UsuarioFormController.java
package pe.com.slcsccy.springmvc.controllers;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.ArrayList;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.validation.BindException;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.SimpleFormController;
import pe.com.slcsccy.springmvc.dominio.Usuario;
import pe.com.slcsccy.springmvc.facade.Facade;

public class UsuarioFormController extends SimpleFormController {
private Facade facade;

@Override
protected Object formBackingObject(HttpServletRequest request) throws Exception {
Usuario usuario = null;
String coUsuario = request.getParameter("coUsuario");
if (coUsuario != null && !coUsuario.equals("")) {
usuario = facade.getUsuarioService().buscar(coUsuario);
} else {
usuario = new Usuario();
}
return usuario;
}


/**
Este método es llamado antes de que el formulario sea procesado, aquí
es donde colocamos el modelo de datos que el formulario necesita utilizar,
por ejemplo si vas a registrar un usuario necesitas asignarle un perfil de una
lista, la lista la obtenemos del DAO por aquí
*/
@Override
public Map referenceData(HttpServletRequest request) {
Map modelo = new HashMap();
ArrayList perfiles = facade.getPerfilService().listar();
ArrayList aplicaciones = facade.getAplicacionService().listar();
ArrayList nacionalidades = facade.getNacionalidadService().listar();
modelo.put("aplicaciones", aplicaciones);
modelo.put("perfiles", perfiles);
modelo.put("nacionalidades", nacionalidades);
return modelo;
}


/**
* Este método permite registrar editores personalizados para ciertos campos
* de nuestro objeto command, en este caso el objeto command es un usuario
* el cual tiene dos campos de fechas pero de tipo Date, lo que hacemos aquí
* es asociar los valores del request a los tipos Date con una fecha
* personalizada, recuerda que el request.getParameter("campo") te devuelve
* siempre un valor null o un String por lo que la transformación a Date
* lo realiza el editor personalizado declarado aquí(Binding)
*/
@Override
protected void initBinder(HttpServletRequest request,
ServletRequestDataBinder dataBinder) throws ServletException {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dataBinder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}

/**
* hacemos un postprocesamiento de los datos que el usuario a enviado,
* en este caso solo le asignamos a la variable de multisesion un valor N.
* este campo de multisesion está relacionado a un checkbox y en el
* objeto command el campo es detipo String, si el usuario no ha marcado
* el checkbox entonces su valor es null, pero en la base
* el valor solo debe ser ó N ó S. Aqui cambiamos el valor null por un más
* significativo.
*/
@Override
protected void onBindAndValidate(HttpServletRequest request, Object command, BindException errors)
throws Exception {
Usuario usuario = (Usuario)command;
usuario.setMultisesion(usuario.getMultisesion()==null?"N":"S");
}

/**
* Método que se encargará del registro del usuario en la base de datos
* después de que las validaciones fueron exitosas(no hubo errores)
*/
@Override
protected ModelAndView onSubmit(HttpServletRequest request, HttpServletResponse response,
Object command, BindException errors) throws Exception {
Usuario usuario = (Usuario) command;
int coError = facade.getUsuarioService().registrar(usuario);
if (coError == 0) {
return new ModelAndView("forms/vistaOk");
} else {
return new ModelAndView("forms/vistaError");
}
}

/**
* @return the facade
*/
public Facade getFacade() {
return facade;
}

/**
* @param facade the facade to set
*/
public void setFacade(Facade facade) {
this.facade = facade;
}
}


Ahora declaramos la clase que validará los datos que ingrese el usuario:
UsuarioValidator.java
package pe.com.slcsccy.springmvc.validators;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import pe.com.slcsccy.springmvc.dominio.Usuario;

public class UsuarioValidator implements Validator {

public boolean supports(Class clase) {
return Usuario.class.isAssignableFrom(clase);
}

/**
* Método principal que será llamado antes de que pase por el controlador
* si la lista de errores contiene al menos un elemento, regresará al
* formulario
*/
public void validate(Object object, Errors errores) {
Usuario usuario = (Usuario) object;
if(usuario.getAplicaciones().length==0)
errores.rejectValue("aplicaciones", "errores.aplicacionesReq");
ValidationUtils.rejectIfEmpty(errores, "codigo", "errores.codigoReq","Mensaje 1");
ValidationUtils.rejectIfEmpty(errores, "apellidos", "errores.apellidosReq","Mensaje 2");
ValidationUtils.rejectIfEmpty(errores, "nombres", "errores.nombresReq","Mensaje 3");
}
}

Los ultimos componentes a crear serán los interceptores:
LoginInterceptor.java
package pe.com.slcsccy.springmvc.controllers;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

public class LoginInterceptor extends HandlerInterceptorAdapter {


/*
* Este método debe de asegurar que al controller solo le lleguen solicitudes
* que cumplan ciertos datos del negocio, como que el usuario este autenticado por ejemplo,
* recordar que el objeto handler en el argumento es el controller hacia al cual
* se dirige la solicitud.
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
/*Solo retornamos true...*/
return true;
/*Si no queremos ir hacia la vista apropiada entonces:*/
/*if(true){
throw new ModelAndViewDefiningException(new ModelAndView("exception/noAutenticado"));
}else return true;*/
}

/*
* Este meétodo puede hacer un postprocesamiento de la solicitud, por ejemplo
* puede cambiar la vista hacia la cual el controller respondió
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView mav)
throws Exception {
}
}

Otro interceptor para que veas como funcionan en forma encadenada o en chain.
LoginAduanasInterceptor.java
package pe.com.slcsccy.springmvc.controllers;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

public class LoginAduanasInterceptor extends HandlerInterceptorAdapter {


@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("Interceptor de Adunas ejecutandose en el preHandle()");
return true;
//Si no queremos ir hacia la vista apropiada entonces
/*if(true){
throw new ModelAndViewDefiningException(new ModelAndView("exception/noAutenticado"));
}else return true;*/
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView mav)
throws Exception {
System.out.println("Interceptor de Aduanas ejecutandose en el postHandle()");
}
}


Me estaba olvidando lo más importante, el formulario jsp, allí está:
usuarioForm.jsp
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Gestión de Usuarios</title>
</head>
<body>
<form:form commandName="usuario">
Codigo: <form:input path="codigo" /><form:errors path="codigo"/><br/>
Nombres: <form:input path="nombres" /><form:errors path="nombres"/><br/>
Apellidos: <form:input path="apellidos" /><form:errors path="apellidos"/><br/>
Fecha Registro: <form:input path="fechaRegistro" /><form:errors path="fechaRegistro"/><br/>
Fecha Cese: <form:input path="fechaCese" /><form:errors path="fechaCese"/><br/>
Perfiles:<form:select path="perfil">
<form:option value="">SELECCIONE</form:option>
<form:options items="${perfiles}" itemValue="coPerfil"itemLabel="dePerfil"/>
</form:select><form:errors path="perfil"/><br/><br/><br/><br/>

<!-- Ahora vamos a usar un checbox que diga si el usuario está habilitado o no-->
Habilitado:<form:checkbox path="habilitado"/><br><br/>
MultiSesion:<form:checkbox path="multisesion" value="S"/><br><br/>
<!-- Ahora mostramos un conjunto de checkboxes para que seleccione la aplicación a la que tiene acceso-->
Aplicaciones:
<form:checkboxes path="aplicaciones" items="${aplicaciones}" itemValue="coAplicacion" itemLabel="deAplicacion"/>
<form:errors path="aplicaciones"/><br/>
<br/><br/>

Nacionalidad:
<form:radiobuttons path="nacionalidad" items="${nacionalidades}" itemValue="coNacionalidad" itemLabel="deNacionalidad"/>
<form:errors path="nacionalidad"/><br/>
<br/><br/>
<input type="submit" value="Registrar Usuario"/>
</form:form>
</body>
</html>


Y por ultimo la integración de todos los componentes con beans de spring que debes de colocar en el front controller <controller>-servlet.xml

frontController-servlet.xml
...
<!-- Definición de interceptores de los controladores -->
<bean id="loginInterceptor" class="pe.com.slcsccy.springmvc.controllers.LoginInterceptor"/>
<bean id="loginAduanasInterceptor" class="pe.com.slcsccy.springmvc.controllers.LoginAduanasInterceptor"/>

<!-- Con este mapeador asociamos keys a urls y a ciertos controladores, mapping caleta, -->

<!--
Declaramos un mapeador de urls seguro, es decir que pasara por dos
interceptores antes de que llegue al UsuarioFormController
-->
<bean id="handlerMappingSeguro" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="interceptors">
<list>
<l-- recuerda que los inteceptores son llamados en el orden en que son declarados-->
<ref bean="loginInterceptor"/>
<ref bean="loginAduanasInterceptor"/>
</list>
</property>
<property name="urlMap">
<map>
<entry key="/aduanas/usuarioForm.do" value-ref="usuarioFormController"/>
</map>
</property>
</bean>

<!-- Declaramos el validador del formulario -->
<bean name="usuarioValidator" class="pe.com.slcsccy.springmvc.validators.UsuarioValidator"/>

<!--
El bean de formulario que integrará los commands(pojos), jsps(formularios) y
validadores y alguna referencia a otro bean como un facade de servicios por ejemplo.
-->
<bean id="usuarioFormController" class="pe.com.slcsccy.springmvc.controllers.UsuarioFormController">
<property name="facade" ref="facade"/>
<property name="sessionForm" value="true"/>
<property name="commandName" value="usuario"/>
<property name="commandClass" value="pe.com.slcsccy.springmvc.dominio.Usuario"/>
<property name="formView" value="forms/usuarioForm"/>
<property name="successView" value="forms/usuarioRegistrado"/>
<property name="validator" ref="usuarioValidator"/>
</bean>
...


Eso es todo por el momento, animate a hacer tu propia aplicación con todos los componentes, en el próximo post hablaré sobre como podemos crear componentes que permitan descargar hacia Excel y Pdf mediante poi e itext integrado con spring.


6 comentarios:

  1. Muy Bueno el articulo, y el blog tiene buena pinta.
    Respecto al post, veo como describes los metodos a implementar en un formulario, a mi me interesa el metodo formBackingObject(), el problema viene debido a que se me ejecuta, antes de visualizar el formulario y despues. No se si esto es normal pero tengo otros formularios y no me dan problemas.
    Bueno no entro en mas detalles, simplemente me ayudaria saber su funcion y justo en el post veo que no describes ese metodo...Bueno espero que puedas ayudarme.

    Gracias de por adelantado

    ResponderBorrar
  2. Hola, gracias por comentar, el método formBackingObject se encarga de crear el objeto command(objeto que contiene los datos del formulario a procesar). Este método se ejecuta ante cada solicitud que realice el formulario. Normalmente sucede lo siguiente: 1. Desde un link queremos registrar un nuevo usuario(por ejemplo: Registrar Usuario), el link realiza un procesamiento GET hacia el controller que extiende SimpleFormController, entonces el método formBackingObject es llamado y devuelve un usuario con los datos por default que quieran visualizarse en el formulario. 2. Si es que la propiedad es colocada a true al momento de definir el controller entonces cuando terminemos de colocar los datos en el formulario y lo enviemos, el método formBackingObject ya no será llamado, en su lugar spring obtendrá el objeto en forma implicita desde la sesión. 3. Si la propiedad sessioForm no es asignada o tiene el valor false, el método formBackingObject será llamado cada vez que se solicite el controller.

    ResponderBorrar
  3. Muchas gracias por contestar tan rapido, el problema viene que aunque el metodo se ejecute otra vez, si los atributos del objeto command estan situados en el formulario, ya sea como campos input o hidden, estos metodos no se modifican en las 2º ejecucion del metodo formBakingObject. De ay mi error ya que tenia un campo "id" que no añadia en mi formulario, ahora tras incluirlo como "hidden" no da error.

    Respecto a lo que me dices, de configurar la propiedad sessionForm, claro no conozco su funcionalidad, asi que me pongo a ello, ya que yo uso la propiedad requiredSession. No se cual sera la diferencian entre ambas propiedades. No se si tu sabes cual es la diferencia entre ambas?

    Bueno Gracias por todo ahora mismo pruebo, tu configuracion ya que es mucho mas elegante que atributos ocultos.

    ResponderBorrar
  4. Muchas gracias por contestar tan rapido, el problema viene que aunque el metodo se ejecute otra vez, si los atributos del objeto command estan situados en el formulario, ya sea como campos input o hidden, estos metodos no se modifican en las 2º ejecucion del metodo formBakingObject. De ay mi error ya que tenia un campo "id" que no añadia en mi formulario, ahora tras incluirlo como "hidden" no da error.

    Respecto a lo que me dices, de configurar la propiedad sessionForm, claro no conozco su funcionalidad, asi que me pongo a ello, ya que yo uso la propiedad requiredSession. No se cual sera la diferencian entre ambas propiedades. No se si tu sabes cual es la diferencia entre ambas?

    Bueno Gracias por todo ahora mismo pruebo, tu configuracion ya que es mucho mas elegante que atributos ocultos.

    ResponderBorrar
  5. Hola, son muy buenos los posts sobre spring, soy nuevo en esto y estoy aprendiendo, quiero probar este ejemplo, pero faltan las clases Facade, no se si pudieras ponerlos para poder probar y asi aprender mas acerca de spring

    ResponderBorrar
  6. che mucho codigo y pocas imagenes ! poco didactico lo tuyo

    ResponderBorrar

Es bueno comunicarnos, comenta!!.