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

Desarrollo Web: Expression Language

Objetivo

Entender qué termina pasando en un JSP se torna más difícil conforme la aplicación va creciendo si la vista tiene un comportamiento no trivial. Cuesta discriminar el HTML del código java encerrado entre tags <% %> y lleva tiempo acostumbrarse a entender qué código ejecuta en el servidor, qué en el cliente y qué resulta de esa combinación.

Por ese motivo existen para Java unas librerías de tags especiales que permiten aumentar la declaratividad de la vista eliminando los tags <% y %>. En nuestro caso vamos a estudiar las Java Standard Tag Libraries (JSTL) que es un Expression Language (EL), un lenguaje que permite modelar expresiones que luego se traducen en scriptlets (los tags <% y %>).

Búsqueda de libros

El ejemplo que vamos a utilizar es el de la búsqueda de libros (proyecto: libros-ui-jsp-el que pueden encontrar en los ejemplos)

La pantalla de búsqueda de libros de una biblioteca tiene el siguiente formato:
  • un campo texto donde se ingresa parte del título
  • el botón buscar
  • una grilla que muestra: número de libro, título y autor.
Adicionalmente cuando se haga click sobre el título del libro, debería aparecer la información detallada de dicho libro.
  
Pensamos cómo podría ser la página index.jsp
  • un input type text de nombre título. 
  • el botón buscar hace submit, ¿a la misma página? mmmm... eso hace que la index.jsp tenga demasiada responsabilidad. Aplicar MMVC también permite separar responsabilidades y que la vista no haga todo. Entonces llamamos a un servlet de búsqueda.
  • la grilla se muestra si hay resultados, claro. Originalmente debería estar vacía.
Si yo busco Ficciones, estaría bueno que el criterio de búsqueda no se pierda cada vez que yo haga un submit al servlet y éste vuelva al formulario. Entonces vamos a definir una variable de scope request, de manera que la página pueda recordar una búsqueda anterior. Para eso
  • en el servlet vamos a tomar ese request y
  • vamos a volvérselo a pasar al index.jsp de manera que el valor del input type text va a estar dado por el titulo que viene como parámetro (a través del request)
<input type="text" name="titulo" value="${param.titulo}" />

El ${...} funciona como un tag especial: param = viene del request. 

Si omitimos setearle el atributo value al text la búsqueda funciona perfecto, pero cuando vuelvo de buscar desaparece lo que escribí porque el value se blanquea cada vez que entro a la página ==> esto es consecuencia de que las páginas son stateless.

Agregamos en el web.xml el mapeo entre un nombre para el servlet y la clase servlet asociada:
    
        <servlet>
            <servlet-name>searchServlet</servlet-name>
            <servlet-class>uqbar.busquedaLibros.el.controller.SearchServlet</servlet-class>
        </servlet>
        <servlet-mapping>
            <servlet-name>searchServlet</servlet-name>
            <url-pattern>/search</url-pattern>
        </servlet-mapping>
    
Este doble mapeo es un tanto engorroso, ok, pero bueh: permite que yo defina una url y asociarla a un servlet-name y luego el servlet-name asociarlo a una clase servlet que es a la que termina derivando el web server.

Diagrama general

Hacemos un diagrama para repasar cómo funciona una búsqueda:

(hacé click en la imagen para agrandarla)

(1) JSP index.jsp captura el título a buscar en la pantalla inicial
(2) servlet captura en el request el título y llama al home de Libros que resuelve la búsqueda. ¿Dónde los pongo? 
Si bien el servlet y la JSP son clases Java, no puedo hacer que se pasen parámetros como los objetos "normales": no puedo pensar en enviar un mensaje del tipo jspIndex.setLibros(librosEncontrados). Mientras el pedido esté del lado del servidor, puedo usar variables de cualquier scope para que el servlet y la JSP se comuniquen. 

Dónde ponemos el resultado de la búsqueda

Entonces el que puede tener los libros encontrados por la búsqueda es...
  1. el request
  2. la session
  3. la application
      
La opción 3 queda descartada, no quiero compartir los resultados de la búsqueda entre todos los usuarios.

La opción 1 podría ser, pero trabajar a nivel request complica cuando quiero pasar info a una tercera página: hay que acordarse siempre de poner dentro del request próximo lo que no quiero perder (y tener en cuenta cuando la navegación puede ser hacia adelante o hacia atrás). 

La opción 2 es la que vamos a elegir, podemos utilizar el scope session para guardar información relacionada con el caso de uso. Algunas restricciones que aplican: 
  • no puedo poner muuuucha información: no puedo bajar toda la base a la session porque me quedo sin espacio en el application server (la VM). Cada usuario que se conecta necesita ese espacio de objetos para él. 
  • si tenemos un cluster de application servers (o sea, un conjunto de VMs), guardar información en la sesión puede ser problemático. Hay que tener en cuenta que el protocolo http es no-orientado a conexión, entonces cada pedido puede hacerse a cualquier VM. Hay soluciones que se encargan de esto pero es un problema que antes no teníamos. 
Aunque redundante, podemos poner la cantidad de libros en otra variable de session.

Y finalmente... redirigimos el control a index.jsp nuevamente.
      
Vemos el código asociado en el servlet:

      String titulo = request.getParameter("titulo");
      Collection<Libro> libros = Biblioteca.getInstance().buscar(titulo);
              
      request.getSession().setAttribute("libros", libros);
      request.getSession().setAttribute("cantLibros", libros.size());
      
      request.getRequestDispatcher("index.jsp").forward(request, response);

(3) nuevamente en index.jsp recibimos en el request el titulo, y lo asociamos al value del text. Por otra parte, acá tenemos que mostrar la grilla, mientras que cuando arrancamos no estaría bueno que muestre una grilla de resultados vacía puesto que todavía no pedimos buscar nada.  Entonces una opción es insertar un scriptlet
    
        <% if (request.getAttribute("resultado") != null) { // abro%>

Expression Language

Pero vamos a usar EL (Expression Language [1]): en principio porque podemos decir lo mismo con menos palabras, pero además porque EL está asociado al concepto de declaratividad: alguien convierte la expresión que vamos a escribir en el if, lo bueno es que la expresión se acerca más al lenguaje que manejo y menos a lo tecnológico. Por otra parte me concentro más en lo que hay que hacer y tengo menos chance de cometer errores (con los scriptlets es fácil confundirse apertura y cierre y estar escribiendo tags HTML en código Java o viceversa.
    
    <c:if test="${sessionScope.libros != null}" >

Los libros tienen que estar a nivel session. Otra opción es hacer:

    <c:if test="${libros != null}" >

Esto es interesante porque la variable libros puede estar escrita a nivel:
  • request
  • session
  • o application,
y nosotros desde la JSP sólo sabemos que a alguno de esos niveles va a estar definida una variable libros. Entonces cambiar el scope de request a session no es algo tan doloroso (aunque no hay algo así del lado de los servlets).

Esto aumenta la declaratividad (preocuparme por menos cosas que otro hace por mí, ese otro termina resolviendo el algoritmo, la magia)
    
Por último mostramos cómo se completa la grilla. En lugar de tener scriptlets con un for, usamos el tag <c:for> de la siguiente manera:

    <c:forEach items="${sessionScope.libros}" var="libro" varStatus="status">

 Los ítems se sacan de la variable "libros" de la session (podemos omitir el sessionScope). Se definen dos variables por cada iteración:
  •  libro (var="libro"), es la variable que apuntará a cada elemento de la colección de libros.  De esa manera armamos la columna (TD) de la grilla con el tag  
    <td>${libro.autor}</td> 

Esto se traduce en un 

      for (Libro libro : request.getSession().setAttribute("libros")) {
          muestro libro.getAutor();
      }
  • varStatus, contiene el índice para mostrar el # de libro
    <td>${status.count}</td>
    •     count tiene el número de libro (arranca en 1),
    •     index tiene el número de índice de Java (arranca en 0).  

Un último requerimiento

  1. Si la variable libros está en null, significa que nunca buscamos... pero
  2. si la variable libros está empty, significa que deberíamos mostrar algo como "No se encontraron libros con el criterio de búsqueda seleccionado"
  3. otherwise, mostramos la grilla
Esto sería algo como:

            <c:if test="${libros != null}" >
                <h2>Respuestas:</h2>
                <table>
                    <tr>
                        <th>#</th>
                        <th>Nombre</th>
                        <th>Autor</th>
                    </tr>
                    <c:forEach items="${sessionScope.libros}" var="libro" varStatus="status">
                        <tr>
                            <td>${status.count}</td>
                            <td><a href="detalle.jsp?nro=${status.index}">${libro.titulo}</a></td>
                            <td>${libro.autor}</td>
                        </tr>
                    </c:forEach>
                </table>
            </c:if> 

    Y lo reemplazamos por:

        <c:choose><c:when test="${libros == null}" >
        </c:when><c:when test="${empty libros}">
            <h3>No se encontraron libros con ese criterio de búsqueda</h3>
        </c:when><c:otherwise>
            <h2>Respuestas:</h2>
            <table>
                <tr>
                    <th>#</th>
                    <th>Nombre</th>
                    <th>Autor</th>
                </tr>
                <c:forEach items="${sessionScope.libros}" var="libro" varStatus="status">
                    <tr>
                        <td>${status.count}</td>
                        <td><a href="detalle.jsp?nro=${status.index}">${libro.titulo}</a></td>
                        <td>${libro.autor}</td>
                    </tr>
                </c:forEach>
            </table>
        </c:otherwise></c:choose>