Proyecto Java EE
 

Capa web con servlets y JSPs

Introducción

En esta sesión de integración construiremos sobre la capa de negocio desarrollada en sesiones anteriores una aplicación web dinámica que, mediante servlets y JSPs que utilicen estas clases previas, expondrá una interfaz web que dará acceso a varias de las funcionalidades de la biblioteca.

Proyecto jbib-web

Vamos a crear en primer lugar un nuevo proyecto con Maven, de nombre jbib-web, utilizando para ello el arquetipo webapp-javaee6, para tenerlo configurado directamente con la versión de Java EE que vamos a utilizar.

Para que Eclipse reconozca un proyecto web Maven como proyecto web dinámico de Eclipse WTP será necesario instalar el plugin Maven Integration for Eclipse WTP. De no hacer esto, Eclipse no reconocería el proyecto como proyecto web, y por lo tanto no podríamos ejecutar la aplicación directamente desde este entorno. Podemos ir a a Help > About Eclipse > Installation Details para comprobar si tenemos instalada la extensión Maven Integration for Eclipse WTP (m2e-wtp) del plugin de Maven. Si no es así, procederemos a instalarla como se muestra a continuación:

  • Entramos en Help > Eclipse Marketplace ...
  • Introducimos el texto "maven" en el cuadro de búsqueda.
  • Buscamos entre los resultados de la búsqueda el plugin Maven Integration for Eclipse WTP (Incubation) y pulsamos sobre el botón Install.
  • Pulsamos el botón Next y seguimos las instrucciones del proceso de instalación.

Ahora ya podemos crear el nuevo proyecto jbib-web que va a contener todos los componentes de la capa web.

En el espacio de trabajo del proyecto de integración crea un nuevo proyecto jbib-web. Escoge la opción de Eclipse New > Project... > Maven > Maven Module, sin marcar la opción de Create simple project e introduciendo:

  • Module Name: jbib-web
  • Paquete: es.ua.jtech.jbib.web
  • Parent Project: jbib
  • Archetype: webapp-javaee6 (se indica en la segunda pantalla del asistente)

En el editor del POM, añade la dependencia con el proyecto jbib-negocio y con JSTL 1.2:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>

<dependency>
    <groupId>es.ua.jtech.proyint</groupId>
    <artifactId>jbib-negocio</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>

Podemos eliminar del POM el bloque <build> ... </build> ya que no es necesario. Deberemos también eliminar las etiquetas groupId y version de nuestro proyecto para que coja los valores del proyecto padre. Deberá quedar como se muestra a continuación:

<parent>
    <artifactId>jbib</artifactId>
    <groupId>es.ua.jtech.proyint</groupId>
    <version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>jbib-web</artifactId>
<packaging>war</packaging>

<name>jbib-web</name>

El fichero completo debe quedar tal como se indica en este pom.xml.

Nota
Es posible que en algunas ocasiones Eclipse no reconozca correctamente los módulos de un multiproyecto Maven. Para solucionar este problema eliminaremos todos los proyectos Maven (sin borrarlos del disco), y los volveremos a importar con File > Import ... > Maven > Existing Maven Projects, seleccionando únicamente el proyecto padre. Eclipse importará de forma automática todos los módulos que contiene.

Ahora podemos ejecutar nuestra aplicación web directamente desde Eclipse como hemos hecho a lo largo del curso (Run As > Run on server). Tendremos que configurar Tomcat en Eclipse, si no está configurado ya en este workspace. Veremos una página de prueba creada automáticamente por Maven, que estará desplegada en la siguiente dirección:

http://localhost:8080/jbib-web/

Configuración de la fuente de datos

En las sesiones de integración anteriores se accedía a base de datos a través de una unidad de persistencia JPA en la que se configuraba directamente el driver y la url a la que realizar la conexión. Ahora que vamos a pasar a una aplicación web, vamos a mejorar el rendimiento accediendo al pooling de conexiones que nos ofrece Tomcat.

Para ello creamos un fichero context.xml en la carpeta META-INF de nuestro proyecto web (src/main/webapp/META-INF), donde definiremos las características del pooling:

<Context>
  <Resource
    name="jdbc/biblioteca"
    type="javax.sql.DataSource"
    auth="Container"
    username="root"
    password="expertojava"
    driverClassName="com.mysql.jdbc.Driver"
    url=
      "jdbc:mysql://localhost:3306/biblioteca?autoReconnect=true"/>
 
  <ResourceParams name="jdbc/biblioteca">
   
    <parameter>
      <name>maxActive</name>
      <value>20</value>
    </parameter>
     
    <parameter>
      <name>maxIdle</name>
      <value>5</value>
    </parameter>
 
    <parameter>
      <name>maxWait</name>
      <value>10000</value>
    </parameter>
  </ResourceParams>

</Context>

También modificaremos el fichero web.xml de la aplicación para que referencie al recurso creado en el paso anterior. Para eso, añadimos estas líneas:

<resource-ref>
    <res-ref-name>jdbc/biblioteca</res-ref-name>
    <res-type>javax.sql.DataSource</res-type>
    <res-auth>Container</res-auth>
</resource-ref>

Si en el proyecto no se ha creado el descriptor de despliegue de forma automática, podéis utilizar este web.xml como plantilla.

Finalmente, tendremos que añadir al proyecto web una unidad de persistencia que acceda a la fuente de datos de Tomcat. Para ello:

  • Creamos una carpeta resources en src/main (src/main/resources).
  • Actualizamos en Eclipse la configuración del proyecto Maven, para que el IDE reconozca la carpeta anterior como carpeta de fuentes (pulsamos con el botón derecho sobre el proyecto, y seleccionamos Maven > Update project ...)
  • Creamos dentro de resources una carpeta META-INF.
  • Creamos dentro de la carpeta META-INF anterior un fichero persistence.xml como el siguiente:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0"
  xmlns="http://java.sun.com/xml/ns/persistence" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation=
    "http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
  <persistence-unit name="biblioteca" 
                    transaction-type="RESOURCE_LOCAL">
    <non-jta-data-source>
        java:comp/env/jdbc/biblioteca
    </non-jta-data-source>
    <class>es.ua.jtech.jbib.model.EjemplarDomain</class>
    <class>es.ua.jtech.jbib.model.LibroDomain</class>
    <class>es.ua.jtech.jbib.model.ReservaDomain</class>
    <class>es.ua.jtech.jbib.model.UsuarioDomain</class>
    <class>es.ua.jtech.jbib.model.AlumnoDomain</class>
    <class>es.ua.jtech.jbib.model.ProfesorDomain</class>
    <class>es.ua.jtech.jbib.model.BibliotecarioDomain</class>
    <class>es.ua.jtech.jbib.model.PrestamoHistoricoDomain</class>
    <class>es.ua.jtech.jbib.model.MultaDomain</class>

    <properties>
      <property name="hibernate.dialect" 
                value="org.hibernate.dialect.MySQLInnoDBDialect" />
      <property name="hibernate.hbm2ddl.auto" value="validate" />
      <property name="hibernate.show_sql" value="true" />
    </properties>
  </persistence-unit>
        
</persistence>

Puedes ver aquí el fichero persistence.xml completo. Al crear este fichero en el proyecto web se sobrescribirá el definido en jbib-modelo, de forma que ahora JPA accederá a la base de datos mediante el pool de conexiones de Tomcat, en lugar de conectar directamente.

Cuidado
El directorio META-INF donde debemos poner persistence.xml no es el META-INF general de la aplicación web donde se puso context.xml. El META-INF donde debemos guardarlo debe estar en el classpath, por eso lo ponemos en la carpeta de fuentes resources, para que durante la compilación sea volcado automáticamente a los directorios del classpath de la aplicación.

Una vez hechos estos cambios, instalaremos de nuevo el proyecto padre en el repositorio de Maven para comprobar que las pruebas siguen funcionando correctamente.

Sobre el driver de MySql deberemos tener en cuenta que ya no basta con incluirlo como dependencia del proyecto. Esto bastaría si sólo necesitásemos la base de datos para acceder a ella desde el código de nuestra aplicación. Sin embargo, dado que vamos a utilizar fuentes de datos del servidor, el servidor Tomcat también deberá acceder a ella para obtener conexiones de la fuente de datos. Por lo tanto, no bastará con que el driver esté disponible para nuestra aplicación, sino que deberá estar disponible para Tomcat. Deberemos copiar el driver de MySql al directorio de librerías comunes de Tomcat (/opt/apache-tomcat-7.0.xx/lib).

Configuración de la seguridad

Vamos ahora a definir seguridad declarativa para nuestra aplicación. Utilizaremos seguridad basada en formularios para proteger todo el sitio web, de forma que sólo puedan acceder los usuarios definidos en la base de datos.

Realm de seguridad

Vamos a utilizar la propia tabla usuario para validar quién entra. En principio dejaremos pasar a cualquiera que esté registrado en esa tabla, y de forma programática haremos que según su perfil (rol) pueda realizar unas determinadas operaciones.

Para poder utilizar nuestra base de datos, debemos añadir un bloque Realm en el contexto de nuestra aplicación, que especifique que utilizaremos un JDBCRealm conectado a nuestra base de datos. Para ello, añadimos este bloque dentro de la etiqueta Context de nuestro fichero META-INF/context.xml:

<Realm className="org.apache.catalina.realm.JDBCRealm"
  driverName="com.mysql.jdbc.Driver"
  connectionURL=
      "jdbc:mysql://localhost:3306/biblioteca?autoReconnect=true"
  connectionName="root" connectionPassword="expertojava"
  userTable="usuario" userNameCol="login" userCredCol="password"
  userRoleTable="usuario" roleNameCol="tipo" />

Autentificación basada en formularios

En segundo lugar, debemos definir las páginas login.jsp y errorLogin.jsp en nuestra carpeta WebContent, con el formulario de validación y la página de error para utilizar seguridad basada en formularios.

Nota
Es importante que ambas páginas tengan extensión .jsp, ya que de lo contrario Eclipse no las admitirá como páginas de login y error válidas. Además, en futuras sesiones será mejor tener páginas JSP a las que se pueda incorporar contenido dinámico.

En el fichero web.xml, añadimos el bloque login-config:

<login-config>
    <auth-method>FORM</auth-method>
    <form-login-config>
        <form-login-page>/login.jsp</form-login-page>
        <form-error-page>/errorLogin.jsp</form-error-page>
    </form-login-config>
</login-config>

Protección de recursos

Finalmente, queda añadir las líneas de configuración de seguridad al fichero web.xml para proteger los recursos de toda la web. En primer lugar declararemos los roles que tienen acceso:

    <security-role>
        <role-name>ADMIN</role-name>
    </security-role>
    <security-role>
        <role-name>BIBLIOTECARIO</role-name>
    </security-role>
    <security-role>
        <role-name>PROFESOR</role-name>
    </security-role>
    <security-role>
        <role-name>ALUMNO</role-name>
    </security-role>

Tras esto, protegeremos todos los recursos de la web para que sólo los usuarios con estos roles tengan acceso:

<security-constraint>
    <web-resource-collection>
        <web-resource-name>Sitio web completo</web-resource-name>
        <url-pattern>/</url-pattern>
    </web-resource-collection>
    <auth-constraint>
        <role-name>ADMIN</role-name>
        <role-name>BIBLIOTECARIO</role-name>
        <role-name>PROFESOR</role-name>
        <role-name>ALUMNO</role-name>
    </auth-constraint>
</security-constraint>

Además, vamos a definir una serie de JSPs que sólo deberán ser accesibles de forma interna, ya que normalmente desde el navegador se hará la petición a un servlet, y este servlet delegará en un JSP para generar la respuesta. Por este motivo, deberemos proteger dichos JSPs para que no se pueda acceder a ellos desde el navegador. Para ello crearemos un directorio jsp dentro de la raíz de la web, y lo protegeremos con seguridad declarativa de forma que ningún rol tenga acceso a él:

<security-constraint>
    <web-resource-collection>
        <web-resource-name>JSPs</web-resource-name>
        <url-pattern>/jsp/*</url-pattern>
    </web-resource-collection>
    <auth-constraint></auth-constraint>
</security-constraint>

Haciendo esto, la única forma de acceder a los recursos del directorio jsp será hacer un include o un forward desde dentro de nuestra propia aplicación.

Componentes web

Vamos a crear ahora los componentes necesarios para construir la interfaz web de nuestra aplicación. Tendremos una serie de servlets que recogerán la petición llegada desde el cliente, comprobarán los permisos del usuario, y utilizarán los objetos de negocio definidos en sesiones anteriores para realizar la operación solicitada. Una vez realizada la operación, suministrará los datos obtenidos a un JSP para que se ocupe de generar la presentación y enviársela al cliente.

Vamos a comenzar implementando la funcionalidad de listado de libros.

Servlets

En primer lugar implementaremos el servlet es.ua.jtech.jbib.ListarLibrosServlet que obtendrá el listado de todos los libros mediante el BO correspondiente, y le pasará un atributo listaLibros en el ámbito de la petición al JSP /jsp/biblio/listadoLibros.jsp o /jsp/usuario/listadoLibros.jsp, según si el usuario que ha entrado es de tipo bibliotecario o alumno/profesor, para así mostrar un listado adaptado a cada tipo de usuario. Por el momento el servlet sólo podrá ser accedido por estos tres tipos de usuarios, no se proporciona ningún listado de libros para administradores.

@WebServlet("/ListarLibrosServlet")
public class ListarLibrosServlet extends HttpServlet {
  private static final long serialVersionUID = 1L;

  static Log logger = LogFactory.getLog(ListarLibrosServlet.class);

  protected void doGet(HttpServletRequest request,
                       HttpServletResponse response) 
                           throws ServletException, IOException {
    try {
      ILibroService ils = FactoriaServices.getInstance()
                      .getLibroService();
      List<LibroDomain> lista = ils.listaLibrosPorTitulo();
      request.setAttribute("listaLibros", lista);

      RequestDispatcher rd = null;
      if (request.isUserInRole("BIBLIOTECARIO")) {
              rd = this.getServletContext().getRequestDispatcher(
                              "/jsp/biblio/listadoLibros.jsp");
      } else if (request.isUserInRole("PROFESOR")
                 || request.isUserInRole("ALUMNO")) {
              rd = this.getServletContext().getRequestDispatcher(
                              "/jsp/usuario/listadoLibros.jsp");
      } else {
              rd = this.getServletContext().getRequestDispatcher(
                              "/jsp/error.jsp");
              request.setAttribute("error",
                  "Pagina no disponible para el usuario actual");
      }
      rd.forward(request, response);
    } catch (BibliotecaException ex) {
      request.setAttribute("error",
          "Error obteniendo el listado de libros. " + ex);
      logger.error("Error obteniendo el listado de libros. " + ex);

      RequestDispatcher rd = this.getServletContext()
                      .getRequestDispatcher("/jsp/error.jsp");
      rd.forward(request, response);
    }
  }

  protected void doPost(HttpServletRequest request,
                  HttpServletResponse response) throws ServletException, IOException {
    doGet(request, response);
  }

}

Destacamos también que en todos nuestros servlets en caso de haber un error se hará un forward al JSP error.jsp. Este será un documento común que utilizaremos para mostrar cualquier error que se produzca en nuestra aplicación. En el caso del servlet anterior lo utilizamos para dar un error cuando un administrador intente acceder al listado de libros, o cuando se produzca un error en las operaciones de negocio.

Vamos a utilizar este listado de libros como punto de entrada a la aplicación. Por lo tanto, lo incluiremos entre las páginas de bienvenida, para que se cargue como página principal "/":

<welcome-file-list>
    <welcome-file>ListarLibrosServlet</welcome-file>
</welcome-file-list>
Ayuda
Si aparecen fallos de compilación provenientes del directorio target, normalmente se solucionarán haciendo un mvn clean.

JSPs

Vamos a pasar ahora a crear los JSPs necesarios para presentar los resultados producidos por los servlets anteriores. En primer lugar crearemos el JSP para mostrar el listado de libros, tanto para bibliotecarios (jsp/biblio/listadoLibros.jsp), como para usuarios profesores y alumnos (jsp/usuario/listadoLibros.jsp), cada uno de ellos en el directorio correspondiente a su tipo de usuario.

Tras esto introduciremos la página de error común (jsp/error.jsp) que será mostrada cada vez que ocurra un error en alguna operación.

Estos JSP todavía no se podrán ejecutar, ya que todos ellos utilizan una serie de fragmentos comunes que deberemos crear a continuación.

Fragmentos de JSP

Vamos a introducir una serie de elementos comunes a todas las páginas del sitio web. Estos elementos son la cabecera, el menú de opciones y el pie. Los definiremos en ficheros jspf independientes, y se incluirán en todas las páginas mediante la directiva include.

El fragmento para la cabecera estará dentro del directorio jsp creado anteriormente y se llamará cabecera.jspf.

Para el menú, crearemos el fragmento jsp/biblio/menu.jspf con las opciones para los bibliotecarios, y el fragmento jsp/usuario/menu.jspf con las opciones para los alumnos y profesores. Separaremos de esta forma, en directorios distintos, los JSP destinados a cada tipo de usuario.

Por último, introduciremos el fragmento del pie de página común jsp/pie.jspf.

Hoja de estilo

Podemos observar que los elementos de nuestros JSPs se encuentran organizados dentro de una serie de bloques div que representan los diferentes componentes de nuestros documentos web (cabecera, menú lateral, cuerpo, pie). Esta organización nos permitirá aplicar una hoja de estilo CSS para indicar el aspecto y la ubicación que tendrá cada uno de estos elementos. Podemos crear una hoja de estilo como el siguiente fichero /css/style.css dentro de la carpeta web de nuestra aplicación.

Listado de libros

Ejercicios

Vamos ahora a añadir nuevas funcionalidades a la aplicación, siguiendo el mismo esquema de separación de responsabilidades entre servlets y JSPs que hemos visto para la anterior. Concretamente, añadiremos las siguientes operaciones:

  • Listar los ejemplares de un libro, junto con su disponibilidad, y la posibilidad de pedir un préstamo.
  • Pedir el préstamo de un ejemplar, obteniendo la fecha en la que deberemos devolverlo.
  • Mostrar un listado de los libros que tenemos actualmente prestados (tanto si están en casa como en sala).
  • Cerrar sesión (logout).

Deberemos implementar los siguientes servlets en el paquete es.ua.jtech.jbib.web:

  • EjemplaresLibroServlet: Recibe un parámetro id_libro con el identificador del libro del que queremos consultar los ejemplares. Obtendrá la lista de ejemplares a partir de los métodos de negocio implementados en sesiones anteriores, y se la pasará al JSP /jsp/usuario/ejemplaresLibro.jsp para que la muestre. Esta operación sólo la podrán realizar los usuarios con rol ALUMNO/PROFESOR.
  • PedirPrestamoEjemplarServlet: Recibe un parámetro id_ejemplar con el identificador del ejemplar a pedir prestado. El usuario que pide el préstamo deberá obtenerse a partir del usuario logueado actualmente en la web. Una vez pedido el préstamo, pasará el objeto EjemplarDomain obtenido a la página /jsp/usuario/confirmacionPedirPrestamo.jsp para que muestre la confirmación de la petición del préstamo y los datos de la misma (fecha de préstamo, fecha de devolución, etc). Esta operación sólo la podrán realizar los usuarios con rol ALUMNO/PROFESOR.
  • ListarMisLibrosServlet: Muestra el listado de los ejemplares prestados por el usuario logueado actualmente, tanto si están en sala como en casa. Una vez obtenido este listado de ejemplares, se lo pasa al JSP /jsp/usuario/misLibros.jsp para que lo muestre. Esta operación sólo la podrán realizar los usuarios con rol ALUMNO/PROFESOR.
  • LogoutServlet: Cierra la sesión y vuelve a la página principal de listado de libros (mostrará el formulario de login).

Si se produjese algún error en cualquiera de los servlets anteriores, se realizará un forward a error.jsp para mostrar la información del error producido.

Hemos visto que los servlets anteriores necesitan una serie de JSPs para presentar los datos. Estos JSPs son:

  • /jsp/usuario/ejemplaresLibro.jsp: Muestra una tabla con la lista de ejemplares obtenidos, y de cada ejemplar muestra: número de ejemplar, localización, fecha de adquisición, estado, y operaciones. Si el ejemplar está disponible, en operaciones veremos un enlace que nos permitirá pedirlo prestado, llamando a PedirPrestamoEjemplarServlet pasando como parámetro el identificador del ejemplar. En caso de no estar disponible, si lo tenemos prestado nosotros mostrará el texto "Ya lo tienes", y si lo tiene otro usuario la columna aparecerá vacía.
    Listado de ejemplares (I) Listado de ejemplares (II)
  • /jsp/usuario/confirmacionPedirPrestamo.jsp: Muestra los datos de la petición de préstamo que acabamos de realizar: ISBN, título, número de ejemplar, fecha de préstamo, fecha de devolución.
    Confirmación de pedir préstamo
  • /jsp/usuario/misLibros.jsp: Muestra una tabla con el listado de ejemplares que tenemos prestados actualmente. Para cada ejemplar se muestra: ISBN, número de ejemplar, título, fecha de devolución, y observaciones. En observaciones mostraremos el texto "Pendiente de recoger" si el libro todavía está en SALA.
    Listado de mis libros

Estos JSPs deberán tener todos los elementos comunes de la estructura de las páginas de nuestro sitio web (cabecera, menú y pie).

Resumen

Como ayuda, proporcionamos la siguiente figura con la estructura de directorios resultante de esta sesión.

Para la entrega se deberá etiquetar repositorio bitbucket con el tag entrega-proyint-web.