Stripes

ActionBean y Vista

Introducción

Como mencionamos en la página de "Frameworks basados en Acciones" la metáfora de construcción de Stripes va a ser orientada a codificar un objeto que va a responder ante un evento del cliente.

A éste objeto en stripes se le llama ActionBean. Acá un pequeño diagrama de muy alto nivel:

Como ven el flujo es parecido al Model 2:

  • El pedido es manejado por el ActionBean, quien eventualmente debería realizar la lógica de negocio (o delegarla)
  • Luego se hace un forward a una vista que mostrará el resultado a través de mostrar el objeto ActionBean.

Sin embargo, los ActionBeans tienen algunas cosas mucho más simpáticas que los Servlets. Y el framework provee varias herramientitas para resolver problemáticas comunes. Veamos

Contrato del ActionBean

Un ActionBean es en realidad un objeto cualquiera que implemente la interface ActionBean. Esta interface es muy simple a nivel de métodos:

public interface ActionBean {
    /**
     * Called by the Stripes dispatcher to provide context to the ActionBean before invoking the
     * handler method.  Implementations should store a reference to the context for use during
     * event handling.
     */
    public void setContext(ActionBeanContext context);
    /**
     * Implementations must implement this method to return a reference to the context object
     * provided to the ActionBean during the call to setContext(ActionBeanContext).
     */
    public ActionBeanContext getContext();
}

Sin embargo el contrato es mucho más grande que el solo hecho de implementar esos métodos.

Un ActionBean es un objeto que:

  • Responde ante eventos del lado del cliente: mediante métodos públicos.
  • Tiene acceso a dos tipos de contextos de información:
    • Parámetros:
      • en la forma de propiedades del ActionBean.
      • El framework se encagará automáticamente de convertir los parámetros y llamar al setter.
    • Request/Sesión/Aplicación:
      • Como definen los métodos de ActionBean arriba, todo actionBean tendrá seteado automáticamente un ActionBeanContext
      • ActionBeanContext contiene a todos los objetos de más bajo nivel de servlets (request, response, etc)
      • En general uno intentaría no utilizar este contexto
  • Decide qué vista utilizar para mostrarse a sí mismo

Ejemplo HoraActionBean

Veamos un ejemplo muy simple, variante de HolaMundo que muestra la hora actual. Para eso creamos nuestro ActionBean

@UrlBinding("/Hora.htm")
public class HoraActionBean extends BaseActionBean {
    private String horaActual;
    @DefaultHandler
    public Resolution queHoraEs() {
        this.horaActual = DateFormat.getTimeInstance().format(new Date());
        return new ForwardResolution("/hora.jsp");
    }
   
    public String getHoraActual() {
        return horaActual;
    }
   
    public void setHoraActual(String horaActual) {
        this.horaActual = horaActual;
    }
   
}

Y luego en el browser escribimos:

http://localhost:8080/conversor-ui-stripes/Hora.htm

Veremos algo así:

Algunos detalles del código:

  • La clase extiende de BaseActionBean que hicimos nosotros para no tener que implementar los dos métodos de ActionBean en cada ActionBean (igual que los servlets vamos a tener muchos de estos en nuestras aplicaciones. No tiene nada de magia BaseActionBean
  • La clase está anotada con @UrlBinding: esto es lo que en servlets hacíamos declarando en el web.xml el patron de la URL con el cual asociábamos un servlet. Acá es símplemente una annotation en la clase.
  • No hace falta registrar nuestra clase en ningún lado.
  • El método queHoraEs():
    • Será el responsable de:
      • realizar la lógica de "negocio": en nuestro caso calcular la hora actual
      • indicar qué vista utilizar: para esto el método retorna un objeto del framework de tipo Resolution. El framework ya trae varias implementaciones de estos Resolution para los casos más comunes. Encapsulan la lógica para determinar e ir hacia donde corresponda.
    • está anotado con @ÐefaultHandler, indicando que cuando se pida este ActionBean sin especificar una acción/evento/método en particular, se deberá ejecutar este método.
    • este método no retorna la hora actual sino que como nuestros applicationModels de Arena, se la acuerda como una property.
    • Luego la vista sabrá mostrar el ActionBean.

La Vista (jsp + tags)

Definimos entonces la vista para nuestro ejemplo de "Qué hora es?".

<h1>La Hora</h1>
    <p>
        La Hora Actual es: <span class="kilometros">${actionBean.horaActual}</span>
    </p>
   
    <stripes:link href="/Hora.htm">Actualizar</stripes:link>  

Acá estamos mostrando la parte más importante. Luego veremos el soporte para layouts de stripes.

Lo interesante acá es:

  • usamos expresiones de EL (las de la forma ${....})
  • existe la variable ya definida actionBean que referencia al objeto ActionBean dado, que se está mostrando. Algo así como "el modelo detrás de la vista". (Pero ojo porque no es un modelo de negocio)
  • Luego, para generar un link al mismo actionbean podemos usar un tag especial de stripes <stripes:link>

Veremos más adelante que hay varios de estos tags especiales, que terminan generando HTML normal, pero simplifican un poco el trabajo.

Parámetros y formularios

Pasemos a un ejemplo de interacción un poco más compleja (aunque no demasiado), donde el usuario ingresa parámetros.

Para eso vamos a insistir con el viejo y (no tan) querido ejemplo del conversor.

Según la metáfora de Stripes y de los action-framewors podríamos pensar el ActionBean como sigue:

@UrlBinding("/Conversor.htm")
public class ConversorActionBean extends BaseActionBean {
    private double millas;
    private double kilometros;
    @DefaultHandler
    public Resolution convertir() {
        this.kilometros = this.millas * 1.60934;
        return new ForwardResolution("/conversor.jsp");
    }
}

Y sus respectivos getters y setters.

Ahora la vista de esto será:

<h1>Conversor Stripes</h1>
    <stripes:form beanclass="uqbar.examples.conversor.ui.stripes.stripes.action.ConversorActionBean" focus="millas">
        <table>
            <tr>
                <td>Millas:</td>
                <td><stripes:text name="millas"/>
                </td>
            </tr>
            <tr>
                <td colspan="2">
                    <stripes:submit name="convertir" value="Convertir"/>                   
                </td>
            </tr>
            <tr>
                <td>Kilometros:</td>
                <td><span class="kilometros">${actionBean.kilometros}</span></td>
            </tr>
        </table>
    </stripes:form>

Acá vemos varias cosas:

  • Para mostrar el valor de los kilómetros, simplemente utilizamos una expresión EL, donde está disponible el bean como actionBean, Igual al ejemplo anterior.
  • Además, para generar el formulario que el usuario submiteará no usamos HTML puro, sino que utilizamos unos tags especiales de stripes:
    • stripes:form generará el form html pero nos permite especifica algo de más alto nivel que una URL, es la clase del ActionBean que queremos ejecutar al submitear el form.
      • Otra cosa interesante del form es que tiene un atributo focus al cual se le especifica el nombre del control del form que queremos que arranque con foco. Hacer esto en html "regular" requiere escribir javascript
    • stripes:text: generará un <input type=text> pero de nuevo nosotros solo especificamos un nombre, que deberá ser el nombre de la property del ActionBean. Stripes solito se va a encargar de mantener el valor en varias "idas y vueltas" al cliente (que antes hacíamos como <input ... value="${bean.millas}"> en jsp comunacho.
    • Así como este stripes:text hay varios otros controles, se pueden consultar acá
    • stripes:submit generará el botón de submit del form.

Como vemos acá, Stripes se encarga automáticamente de

  • tomar los parámetros del form que vienen en el request
  • convertirlos según el tipo de dato
  • invocar al setter del action bean para actualizar los valores
  • Luego invocar al método del actionbean
  • En caso de mantenerse en al propia vista, se encarga de mostrar los valores actuales de los campos.

Ciclo de Vida de un ActionBean

Bien, hasta ahora todo parece felicidad, y podríamos pensar que el ActionBean se parece bastante al ApplicationModel, o hasta, salvo por el hecho de tener que implementar una interfaz podría ser un objeto de dominio.

Sin embargo no es tan así. Veamos un ejemplo.

Qué pasa si por algún motivo queremos mantener una lista de los valores convertidos:

@UrlBinding("/ConversorConHistorial.htm")
public class ConversorConHistorialActionBean extends BaseActionBean {
    private double millas;
    private double kilometros;
    private List<Double> resultadosAnteriores = new ArrayList<Double>();
    @DefaultHandler
    public Resolution convertir() {
        this.resultadosAnteriores.add(this.kilometros);
        this.kilometros = this.millas * 1.60934;
        return new ForwardResolution("/conversorConHistorial.jsp");
    }

Y la vista:

<h1>Conversor Stripes Con Historial</h1>
    <stripes:form beanclass="uqbar.examples.conversor.ui.stripes.stripes.action.ConversorConHistorialActionBean"  focus="millas">
        <stripes:errors globalErrorsOnly="true" />
        <table>
            <tr>
                <td>Millas:</td>
                <td><stripes:text name="millas"/>
                <stripes:errors field="millas"/>
                </td>
            </tr>
            <tr>
                <td colspan="2">
                    <stripes:submit name="convertiraso" value="Convertir"/>                   
                </td>
            </tr>
            <tr>
                <td>Kilometros:</td>
                <td><span class="kilometros">${actionBean.kilometros}</span></td>
            </tr>
            <tr>
                <td>Historial:</td>
                <td>
                 <ol>
                  <c:forEach items="${actionBean.resultadosAnteriores}" var="resultado">
                    <li class="kilometros">${resultado}</li>
                  </c:forEach>
                 </ol>
                </td>
            </tr>
        </table>
    </stripes:form>

Veremos que ante las reiteradas conversiones que hagamos, nunca se verá la lista de historial. En realidad siempre se verá un único elemento 0

Esto es porque la lista es siempre una nueva, vacía.

No solo la lista, todo el ActionBean es un objeto nuevo.

Stripes por default deshecha los ActionBeans luego de que se le retornó la vista al usuario.

Lo que sucede es que ante un nuevo request, vuelve a instanciarlo y le popula todos los datos que vienen de request, entonces parecería ser el mismo objeto. Pero no lo es. Si necesitamos mantener estado en el actionbean, entre diferentes request, tendremos problemas.

Igual que con JSP, esto es una limitación.

Acá podemos ver una idea del ciclo de vida por el que pasa un request en Stripes:

  • Resolver ActionBean según el form (URL)
  • Crear una nueva instancia del ActionBean
  • Parsear parámetros del request
  • Realizar conversiones de los parámetros
  • Validar parámetros
  • Setear properties
  • Invocar método
  • Ejecutar Resolution (vista)

Algo interesante de Stripes es que si bien es un framework que no intenta cambiar radicalmente la forma de construir aplicaciones web introduciendo grandes nuevos conceptos, está diseñado en forma bastante "objetosa". Con lo cual este ciclo de ejecución está modelado es configurable y extensible. Uno puede meterse entre medio de alguno de esos pasos y extender el framework para hacer algo.

De hecho, sería posible hacerlo trabajar como Wicket, para que guarde los ActionBeans en la session, pero igualmente tener soporte para multitab.

SessionScope'd ActionBeans

Sin embargo, al igual que en Model2, existe una forma de mantener el estado, es decir las instancias de los ActionBean's. Esto es, a través de la session.

A diferencia de servlets, en Stripes esto se hace de forma "declarativa" con una annotation.

Si a nuestro ejemplo le agregamos la annotation @SessionScope:

@SessionScope
@UrlBinding("/ConversorConHistorial.htm")
public class ConversorConHistorialActionBean extends BaseActionBean {

Y ahora ejecutamos nuevamente:

Ahora sí mantiene el estado.

Sin embargo tenemos los mismos problemas que con servlets+jsp:

  • Un mismo usuario no podrá abrir dos tabs de la aplicación (salvo en diferentes browsers que serán contemplados como dos sessions distintas)
  • Estamos metiendo cosas en la session, incrementando el consumo de memoria.

Validaciones

Vamos a ver cómo le agregamos validaciones a nuestro conversor, que de paso nos va a introducir algunas cosas interesantes.

Para arrancar vamos a querar agregarle dos validaciones muy simples al conversor:

  • Las millas son requeridas.
  • Las millas no pueden ser números negativos.

Existen dos tipos de estrategias de validación en Stripes (y varios "colores" dentro de la mismas)

  • En forma declarativa: donde no escribimos lógica imperativa que realiza la validación, sino que solo especificamos la regla de negocio. Esto se hace a través de annotations que ya vienen en Stripes
  • En forma imperativa: para casos no contemplados uno puede escribir código imperativo en java para validar.

Vamos a usar ambas formas para expresar las reglas del conversor:

Validaciones Imperativas y Declarativas

Para la primera regla, stripes ya trae una annotation para esto:

@UrlBinding("/ConversorConValidaciones.htm")
public class ConversorConValidacionesActionBean extends BaseActionBean {
    @Validate(required = true)
    private double millas;

Con @Validate(required = true), solito stripes va a realizar la validación.

Por otro lado, para checkear el valor de millas podemos escribir código que evalúe la condición en el método "convertir"

public Resolution convertir() {
        if (this.millas <= 0) {
            ValidationErrors errors = new ValidationErrors();
            errors.add("millas", new SimpleError("No se pueden convertir millas negativas!"));
            this.getContext().setValidationErrors(errors);
            return new ForwardResolution("/conversorConValidaciones.jsp");
        }
        this.kilometros = this.millas * 1.60934;
        return new ForwardResolution("/conversorConValidaciones.jsp");
    }

Acá vemos que hay toda una pequeña burocracia y código procedural que ata a este objeto con stripes. No es tan feliz como símplemente lanzar una UserException. Igualmente, se podría hacer eso, customizando la forma en que Stripes maneja las excepciones. Pero requiere un trabajo adicional. Ojo, si fuera a codificar una webapp en stripes y no solo un ejemplo, definitívamente haría eso. Porque me reduce la cantidad de código basura y repetido.

Como ven ahí estamos agregando el error indicando el nombre del field. Eso va a servir para que stripes pueda mostrarlo cerca del field.

Veamos entonces cómo se modifica la vista:

    <stripes:form beanclass="uqbar.examples.conversor.ui.stripes.stripes.action.ConversorConValidacionesActionBean"  focus="millas">
        <stripes:errors globalErrorsOnly="true" />
        <table>
            <tr>
                <td>Millas:</td>
                <td><stripes:text name="millas"/>
                <stripes:errors field="millas"/>
                </td>
            </tr>
            <tr>
                <td colspan="2">
                    <stripes:submit name="convertir" value="Convertir"/>                   
                </td>
            </tr>
            <tr>
                <td>Kilometros:</td>
                <td><span class="kilometros">${actionBean.kilometros}</span></td>
            </tr>
        </table>
    </stripes:form>

Ahí vemos dos nuevos tags <stripes:errors>.

Éste tag sirve para presentar la lista de errores.

Se puede usar en tres formas (en general):

  • Todos los errores: dentro de un form un único tag error que mostrará todos los errores, no importa de qué componente es cada uno.
  • Errores por field: podríamos tener un tag <stripes:error> para cada field, y así mostrar ahí solo los errores asociados a ese field.
  • Combinados (como lo estamos haciendo nosotros):
    • Se usan errores para cada field
    • Igualmente se mantiene un tag <stripes:error> para cualquier otro error global o general que se produzca. En este caso se limita con globalErrorsOnly="true".

Ahora se verá así