Spring WebFlow con JSP

En el articulo anterior http://www.profesor-p.com/2018/10/29/spring-webflow-con-jsp-configuracion/ explicaba como configurar el programa para que Spring WebFlow funcionara. En este articulo explicare como hacer el flujo en si.

La página principal del programa no esta dentro de ningún flujo y sus peticiones son respondidas por Spring MVC, en la clase MyController, la cual podemos encontrar en el paquete profesorp.webflow.controller. Esta clase anotada con la etiqueta @Controller responde en la función indice1 a las peticiones de los recursos “/” e “index”

Aquí podéis ver el código:

    @RequestMapping(value = {"/", "index"})
    public ModelAndView indice1(ModelAndView mod) {
        logger.info("request index");
        mod.addObject("cliente", cliente);
        mod.setViewName("index");
        return mod;
    }

Como veis devuelve un objeto ModelAndView donde guarda el objeto cliente, la cual es una @entity de la tabla Clientes, que ya vimos en la entrada anterior. La vista devuelta es “index.jsp” que esta en el directorio webapp/WEB-INF/jsp/

En “index.jsp” encontramos la etiqueta <sec:authorize access=”isAuthenticated()”> que esta dentro del paquete spring-security-taglibs. Si el usuario no esta autentificado mostrara la pantalla de login, en caso contrario saludara al usuario  y permitirá realizar el traspaso de dinero.

Al usar el  paquete security, y para evitar ataques CSRF,  Spring solo permite realizar peticiones tipo POST,  al recurso /logout. Por ello uso la librería JQuery con la cual creo esa petición en la función de javascript function post(path, parameters)

En todas las peticiones tipo POST debemos incluir la variable _csrf.parameterName  con el valor _csrf.token para evitar estos mismos ataques CSRF. Si no  las ponemos Spring Security no aceptara nuestras peticiones, pues entenderá que es un posible ataque.

 <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

Esta es la pantalla de entrada , solicitando las credenciales.

Pulse sobre la imagen para agrandarla

Y está, la pantalla de entrada , una vez el usuario esta registrado.

Pulse sobre la imagen para agrandarla

 

 

 

 

 

 

Si pulsamos el enlace “Transferencia” iremos al recurso “traspaso” que estará ubicado en la URL http://localhost:8080/webflow/traspaso. Este recurso es tratado por Spring Webflow, por lo cual se cargara el fichero  WEB-INF/flows/traspaso/traspaso.xml pues así lo definimos en la clase WebFlowConfig anteriormente vista.

Todo fichero XML que defina un webflow deberá empezar con las siguientes lineas.

<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/webflow
                          http://www.springframework.org/schema/webflow/spring-webflow.xsd">
    
  

A continuación defino la variable traspasoBean con una nueva instancia del objeto profesorp.webflow.controller.TraspasoBean.

<var name="traspasoBean" class="profesorp.webflow.controller.TraspasoBean"/>

Esta sentencia seria el equivalente al código java traspasoBean  = new profesorp.webflow.controller.TraspasoBean()

Seguidamente. indico que al cargar el flujo de trabajo debe evaluar la expresión logicaService.getCliente() cuyo resultado debe guardar en la variable cliente, la cual sera  de ámbito flow (flowScope.cliente). Una explicación de los ámbitos la tenéis en la pagina oficial de Spring (en ingles me temo). Baste decir, de momento que una variable definida dentro del ámbito flowscope es accesible en todas las paginas que estén definidas en nuestro flujo.

 <on-start>       
        <evaluate expression="logicaService.getCliente()" result="flowScope.cliente" />              
  </on-start>

Tener en cuenta que para poder acceder al objeto logicaService  lo hemos tenido que definir como un Bean en nuestro programa. En este ejemplo lo tenéis en la clase LogicaService, que muestro a continuación.

package profesorp.webflow.services;

@Service
@SessionScope
public class LogicaService {
....
Clientes cliente;
...
 public Clientes getCliente()
  {
         return cliente;
  }
}

A continuación indico la primera vista que se debe cargar, que sera el fichero cuentaOrigen.jsp

  <view-state id="cuentaOrigen" model="traspasoBean">        
        <on-render>
            <evaluate expression="logicaService.getCuentasByCliente()" result="flowScope.cuentas" />
        </on-render>
        <transition on="activate" to="importe"/>       
    </view-state>

Con el parámetro model especifico  las variables que reciba de la vista, deberán ser puestas en el objeto traspasoBean.

Como en la vista  definimos la variable cuentaOrigen (en un campo tipo select),y  el bean profesorp.webflow.controller.TraspasoBean tiene a su vez la variable cuentaOrigen definida, se llamara a la función setCuentaOrigen() de esa clase, con el valor introducido , por el usuario en la vista.

Si tuviéramos más variables en el formulario que coincidieran con sus correspondientes setters, también serian llamadas.

La vista, que detallo a continuación, es un formulario bastante simple donde se pide el numero de cuenta origen:

   <form method="post" action="${flowExecutionUrl}">
                    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
                    <input type="hidden" name="_eventId" value="activate">

                    <div class="form-group">                   
                        <label class="etiqueta" for="Cuenta">Elija Cuenta Origen: </label>
                        <select name="cuentaOrigen">
                            <c:forEach items="${cuentas}" var="item">
                                <option value="${item}">${item}</option>
                            </c:forEach>         
                        </select>
                    </div>
                    <div class="col ">
                        <input class="btn btn-primary" type="submit" value="Siguiente" />
                    </div>
                   <a class="nav-link" href="${flowExecutionUrl}&_eventId=cancel">Cancelar</a>
        </form>

Así se ve la vista en el navegador

 

Lo primero es ver como la URL a llamar por el formulario es el resultado de la variable ${flowExecutionUrl}. Esta variable es puesta automáticamente por Spring WebFlow y apuntara  a la dirección donde continuara el flujo actual.

La variable “_eventId”  es la que Spring WebFlow buscara para decidir que acción llevar a cabo. En este caso le asigno el valor “activate” que coincide con  el valor de la etiqueta transition <transition on="activate" to="importe"/> 

Con esto lo que hacemos es configurar que cuando la variable _eventId tenga el valor “activate” el flujo vaya al ID importe. Por supuesto, el ID deberá existir en nuestro flujo.

Aclarar que “activate” es un literal libre, que igual podria ser ‘MI_SALIDA‘ o ‘SEGUIR‘.

Lo normal, en una vista, es que pueda devolver diferentes valores. Así en  la ultima linea donde declaramos un enlace (<a href…”>) vemos como se llama a ${flowExecutionUrl} pasando el parámetro _eventId=cancel . En el caso de que pulsemos ese enlace lo que se ejecutara sera el siguiente código que esta al final del fichero traspaso.xml

 <global-transitions>
        <transition on="cancel" to="cancel" />
  </global-transitions>

Esto es así porque hemos definido que cualquier evento tipo “cancel” dentro del flujo, llame al ID cancel del que luego hablare.

<evaluate expression="logicaService.getCuentasByCliente()" result="flowScope.cuentas" />

La etiqueta <on-render> hace que lo que haya dentro se ejecute justo antes de renderizar la vista. En este caso llamara a la función logicaService.getCuentasByCliente()y el valor devuelto lo almacenara en flowScope.cuentas

   public Iterable<String> getCuentasByCliente()
   {
       return cuentasClientesRepository.findCuentasByCliente(cliente.getId());
   }

Este objeto cuentas que es un Array de strings que se usa en la vista para crear el desplegable  con las cuentas de origen disponibles, en el código <c:forEach items.....>

Lo siguiente define la vista con ID importe, que es donde ira el usuario cuando pulse el botón siguiente, como hemos definido con las etiquetas <transition on="activate" to="importe"/> anteriores.

  <view-state id="importe" model="traspasoBean">    
        <on-render>
            <evaluate expression="false" result="traspasoBean.puestoPeriodico" />
        </on-render>  
        <transition on="salir" to="comprobarImporte">
            <evaluate expression="logicaService.hasCredit(traspasoBean)" />
        </transition>
    </view-state>

Como en la anterior vista, los valores introducidos en el formulario serán introducidos en traspasoBean, al incluirse la etiqueta model.

Antes de mostrar la vista se llama a la función traspasoBean.puestoPeriodico() con el valor false debido a la etiqueta on-render.

Si el valor de la variable “_eventId” es “salir” el flujo sera dirigido al ID comprobarImporte, pero antes de ir se llamara a la función logicaService.hasCredit(traspasoBean). Si esa función devuelve true se realizara el salto a comprobarImporte en caso contrario se volvera a mostrar la vista actual, es decir importe.

La vista importe esta definida en el fichero importe.jsp del cual pongo un extracto a continuación

           <form method="post" action="${flowExecutionUrl}">
                    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
                    <input type="hidden" name="_eventId" value="salir">
                    <div class="form-group">                 
                        <label class="etiqueta" for="CuentaOrigen">Cuenta Origen </label>
                        <input type="text" name="cuentaOrigen" value="${traspasoBean.cuentaOrigen}" disabled >
                    </div>
                    <div class="form-group">
                        <label class="etiqueta"  for="Importe">Introduzca Importe </label>
                        <input type="text" class="importe" name="importe" value="${traspasoBean.importe}"  placeholder="Importe Traspaso" required>    
                        <c:if test="${traspasoBean.getMsgImporte()!=null}">
                            <div class="alert alert-warning">
                                <strong>Atencion!</strong> ${traspasoBean.getMsgImporte()}
                            </div>
                        </c:if>                    
                    </div>
                    <div class="form-group">
                        <label class="etiqueta" for="CuentaDestino">Cuenta Destino </label>
                        <input type="text" name="cuentaFinal" value="${traspasoBean.cuentaFinal}" >
                    </div>
                    <div class="form-group">
                        <label class="etiqueta"  for="Periodico">Traspaso Periodico</label>
                        <input type="checkbox" name="periodico" value="true"
                               <c:if test="${traspasoBean.periodico}">
                                   checked 
                               </c:if>
                               >
                    </div>
                    <div class="col">
                        <input class="btn btn-primary" type="submit" value="Siguiente" />&nbsp;
                    </div>
               </form>

Resaltar que como la variable periodico esta en un campo checkbox solo sera mandada cuando el usuario seleccione esa opción, por lo cual si no es marcada, la funcion setPeriodico() no sera llamada, pero no se pondrá a false la correspondiente variable en el Bean. Es por ello que antes de mostrar la vista, con la etiqueta <on-render> se pone a false una variable auxiliar que utilizara el programa para saber que no se ha seleccionado la opción “Traspaso Periódico

Una imagen de como se muestra la vista en el navegador.

El id comprobarImporte se define a continuación,

  <decision-state id="comprobarImporte">
        <if test="logicaService.checkImporte(traspasoBean)"
            then="periocidad"
            else="importe"/>
    </decision-state>

En este estado que es tipo decisión, se llama a la función checkImporte(traspasoBean) del objeto logicaService en caso de que devuelva true se ira al ID periocidad, en caso contrario se volverá al ID importe . Esta función comprueba que el cliente tenga suficiente dinero en la cuenta.

El ID periocidad es otro estado tipo decisión que dependiendo del resultado de la función isPeriodico, la cual comprueba si se ha selecionado la opción “Traspaso Periodico” en la vista, ira a periodico o a confirmar

 <decision-state id="periocidad">
        <if test="logicaService.isPeriodico(traspasoBean)"
            then="periodico"
            else="confirmar"/>
    </decision-state>
   <subflow-state id="periodico" subflow="traspaso_time">
        <input name="traspasoBean" />
        <transition on="salir" to="confirmar">            
        </transition>
    </subflow-state>

El ID periodico es una llamada al flujo traspaso_time, al cual se saltara pasándole el objeto traspasoBean debido a la etiqueta input.

Si ese flujo sale con el ID salir se saltara al ID confirmar

A continuación detallo el contenido del flujo traspaso_time que esta definido en el fichero traspaso_time.xml

<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/webflow
                          http://www.springframework.org/schema/webflow/spring-webflow.xsd">
    
    <input name="traspasoBean" required="true" />  
    
    <view-state id="tiempo" model="traspasoBean">         
        <transition on="salir" to="salir"/>       
        <transition on="cancelar" to="cancel" />        
    </view-state>
    <end-state id="salir" />
    <end-state id="cancel" />
</flow>

Lo primero que declaramos es que debemos recibir un objeto traspasoBean . Si no lo recibieramos el programa fallara.

Es decir el flujo lo podríamos llamar con la URL: http://localhost:8080/webflow/traspaso_time pero si lo hacemos al no recibir el objeto  traspasoBean  fallara.

En el flujo mostramos la vista tiempo y si el event_id es salir se saltara al ID salir.

Este ID es tipo end-state por lo cual  se volverá  al anterior  flujo con el ID salir. En el flujo traspaso entonces  se saltara al ID confirmar, como esta definido por la etiqueta: <transition on="salir" to="confirmar"> 

Si el event_id es cancelar también volverá al flujo anterior, pero  a través del ID cancel y por lo tanto se saltara al ID cancel , como se definió con las etiquetas anteriormente mostradas.

   <global-transitions>
        <transition on="cancel" to="cancel" />
    </global-transitions>

La vista tiempo se muestra así en el navegador.

Por último la vista confirmar, muestra todos los datos introducidos y solicita la confirmación. En caso de darla se ira al ID salir con lo cual saldremos del flujo redirigiendonos al recurso index pasando la variable transferencia  con el valor “1”. En caso de que en algún momento hayamos cancelado la transferencia se saldrá del flujo dirigiéndonos a index pero la variable transferencia tendrá el valor “0”

  <view-state id="confirmar">        
        <transition on="salir" to="salir"/>
    </view-state>
    
    <end-state id="salir" view="externalRedirect:./index?transferencia=1" />
    <end-state id="cancel" view="externalRedirect:./index?transferencia=0" />

Esto se nos mostrara en pantalla una vez hayamos confirmado la transferencia

Y con esto termino esta entrada tan larga, pero donde creo que he explicado bastantes conceptos de WebFlow.

Como siempre, espero vuestros comentarios y mejoras, con la esperanza de haberme explicado bien.

¡¡ Hasta otra, alumnos!!

Deja un comentario

English English Spanish Spanish