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:
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
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:
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:
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:
Veremos más adelante que hay varios de estos tags especiales, que terminan generando HTML normal, pero simplifican un poco el trabajo.
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:
Como vemos acá, Stripes se encarga automáticamente de
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:
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.
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:
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:
Existen dos tipos de estrategias de validación en Stripes (y varios "colores" dentro de la mismas)
Vamos a usar ambas formas para expresar las reglas del conversor:
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):
Ahora se verá así