Proyecto de Integración
 

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 WTP (optional). 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 WTP (optional) del plugin de Maven. Si no es así, procederemos a instalarla como se muestra a continuación:

  • Entramos en Help > Install new software ...
  • Buscamos en el desplegable Work with el site http://m2eclipse.sonatype.org/sites/m2e-extras. Si el site no está, lo añadimos pulsando sobre el botón Add ... e introduciendo como nombre del nuevo site m2eclipse extras.
  • Seleccionamos este site en el desplegable y marcamos el plugin Maven integration for WTP (optional)
  • 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
  • Parent Project: proyint-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>org.especialistajee.proyint</groupId>
    <artifactId>jbib-negocio</artifactId>
    <version>${project.parent.version}</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>

Podemos eliminar del POM el bloque <build> ... </build> ya que no es necesario. Deberemos también coger el groupId y la version del proyecto padre:

<parent>
    <artifactId>proyint-jbib</artifactId>
    <groupId>org.especialistajee.proyint</groupId>
    <version>2011</version>
</parent>

<groupId>${project.parent.groupId}</groupId>
<artifactId>jbib-web</artifactId>
<version>${project.parent.version}</version>
<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, donde definiremos las características del pooling:

<Context>
  <Resource
    name="jdbc/biblioteca"
    type="javax.sql.DataSource"
    auth="Container"
    username="root"
    password="especialista"
    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 modificar el proyecto jbib-modelo para crear una unidad de persistencia que acceda a la fuente de datos de Tomcat. Para ello editaremos el fichero persistence.xml, y añadiremos la siguiente unidad de persistencia, sin eliminar las creadas anteriormente:

<persistence-unit name="proyint" transaction-type="RESOURCE_LOCAL">
   <provider>org.hibernate.ejb.HibernatePersistence</provider>
   <non-jta-data-source>
       java:comp/env/jdbc/biblioteca
   </non-jta-data-source>
   <class>org.especialistajee.jbib.model.UsuarioDomain</class>
   <class>org.especialistajee.jbib.model.AlumnoDomain</class>
   <class>org.especialistajee.jbib.model.BibliotecarioDomain</class>
   <class>org.especialistajee.jbib.model.ProfesorDomain</class>
   <class>org.especialistajee.jbib.model.LibroDomain</class>
   <class>org.especialistajee.jbib.model.MultaDomain</class>
   <class>org.especialistajee.jbib.model.OperacionDomain</class>
   <class>org.especialistajee.jbib.model.ActivaDomain</class>
   <class>org.especialistajee.jbib.model.HistoricaDomain</class>
   <class>org.especialistajee.jbib.model.PrereservaDomain</class>
   <properties>
        <property name="hibernate.dialect" 
                value="org.hibernate.dialect.MySQLInnoDBDialect" />
        <property name="hibernate.hbm2ddl.auto" value="update" />
        <property name="hibernate.show_sql" value="true" />
   </properties>
</persistence-unit>

Para evitar conflictos de nombres, renombraremos las anteriores unidades de persistencia de la siguiente forma:

  • La antigua proyint_test pasará a llamarse proyint_test_entidades.
  • La antigua proyint pasará a llamarse proyint_test_negocio.

Puedes ver aquí como quedaría el fichero persistence.xml completo tras los cambios realizados.

Para que las pruebas de unidad definidas en los proyectos jbib-modelo y jbib-negocio sigan funcionando correctamente, deberemos establecer correctamente la unidad de persistencia a utilizar en cada una de ella. Para ello dentro de EAOTest y EntityTest del proyecto jbib-modelo pondremos:

@BeforeClass
public static void initialize() {
    emf = Persistence
            .createEntityManagerFactory("proyint_test_entidades");
    ....

Por otro lado, en el proyecto jbib-negocio deberemos hacer alguna modificación en el singleton PersistenceManager para permitir que los tests puedan cambiar el nombre de la unidad de persistencia a la que se conectará:

public class PersistenceManager {
   static private String persistenceUnitName = "proyint";
   protected static PersistenceManager me = null;
   private EntityManagerFactory emf = null;

   private PersistenceManager() {
       emf = Persistence
             .createEntityManagerFactory(persistenceUnitName);
       this.setEntityManagerFactory(emf);
   }

   public static void setPersistenceUnitName(String name) {
       if(!persistenceUnitName.equals(name)) {
           if (me != null) {
               throw new IllegalStateException(
                   "No se puede cambiar el nombre de la unidad " 
                   + "de persistencia una vez creada");
           }
           persistenceUnitName = name;			
       }
   }   
   ...

Dentro de este proyecto, en BoTest y PersistenceManagerTest estableceremos el nombre de la unidad de persistencia de la siguiente forma:

@BeforeClass
public static void initialize() {
  PersistenceManager.setPersistenceUnitName("proyint_test_negocio");
}

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.21/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="especialista"
  userTable="usuario" userNameCol="login" userCredCol="password"
  userRoleTable="usuario" roleNameCol="tipoUsuario" />

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 las funcionalidades de listado de libros y de reserva.

Servlets

En primer lugar implementaremos el servlet org.jbib.web.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);

  public ListarLibrosServlet() { }

  protected void doGet(HttpServletRequest request, 
                       HttpServletResponse response) 
                           throws ServletException, IOException {
    try {
      ILibroBo ilb = FactoriaBos.getInstance().getLibroBo();
      List<LibroDomain> lista = ilb.listaLibros();
      request.setAttribute("listaLibros", lista);

      RequestDispatcher rd = null;
      if(request.isUserInRole(TipoUsuario.BIBLIOTECARIO.name())) {
        rd = this.getServletContext()
          .getRequestDispatcher("/jsp/biblio/listadoLibros.jsp");
      } else if(request.isUserInRole(TipoUsuario.PROFESOR.name()) ||
                request.isUserInRole(TipoUsuario.ALUMNO.name())) {
        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>

Para realizar la reserva definiremos el servlet org.jbib.web.RealizarReservaServlet. Este servlet tomará el ISBN del libro a reservar como parámetro, y el usuario para el que se reserva será siempre el usuario autentificado actualmente. Por lo tanto, esta funcionalidad sólo la podrán realizar los alumnos y profesores, ya que el resto de usuarios no pueden hacer reservas. Si la reserva se realiza correctamente este servlet nos llevará de vuelta al listado de libros, donde ya veremos el libro como reservado.

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

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

  public RealizarReservaServlet() {
    super();
  }

  protected void doGet(HttpServletRequest request,
                       HttpServletResponse response) 
                           throws ServletException, IOException {
    try {
      String isbn = request.getParameter("isbn");
      String login = request.getRemoteUser();

      IUsuarioBo iub = FactoriaBos.getInstance().getUsuarioBo();
      UsuarioDomain usuario = iub.recuperaUsuario(login);
			
      if (!request.isUserInRole(TipoUsuario.ALUMNO.name()) && 
          !request.isUserInRole(TipoUsuario.PROFESOR.name())) {
        throw new BibliotecaException("Solo los usuarios "
                               + "pueden realizar una reserva.");
      }

      IOperacionBo iob = FactoriaBos.getInstance().getOperacionBo();
      iob.realizaReserva(usuario.getId(), isbn);
      response.sendRedirect(request.getContextPath() 
                            + "/ListarLibrosServlet");

    } catch (BibliotecaException ex) {
      request.setAttribute("error", "Error realizando la reserva. "
                                    + ex.getMessage());
      logger.error("Error realizando la reserva." + ex.getMessage());

      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);
  }

}
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 las anteriores. Concretamente, añadiremos la posibilidad de dar de alta nuevos libros, o editar o borrar los libros ya existentes. Todas estas operaciones serán exclusivas de los usuarios de tipo bibliotecario.

Mapa de navegación de la web

Deberemos implementar los siguientes servlets en el paquete org.jbib.web:

  • LibroDelServlet: Recibe un parámetro isbn con el ISBN del libro a borrar. Si el borrado se realiza de forma correcta, nos llevará de nuevo al listado de libros, donde veremos que el libro borrado ya no aparece en la lista.
  • LibroPreAddServlet: Nos mostrará el formulario para introducir los datos del libro a dar de alta. Este formulario deberemos crearlo en un JSP de nombre /jsp/biblio/altaLibro.jsp.
  • LibroAddServlet: Recibirá del formulario anterior los datos del libro a insertar, los introducirá en un objeto LibroDomain, y mediante el BO los insertará en la base de datos. Una vez hecha la inserción, nos llevará a la página del listado de libros, donde aparecerá el nuevo libro.
  • LibroPreUpdateServlet: Recibe un parámetro isbn con el ISBN del libro a editar. Obtendrá los datos de este libro de la base de datos y se los pasará mediante un atributo libro en el ámbito de la petición al JSP modificaLibro.jsp que nos mostrará un formulario que tendrá como valores por defecto los datos actuales del libro.
  • LibroUpdateServlet: Este será el servlet invocado por el formulario anterior. Recibirá los nuevos datos del libro de dicho formulario y los actualizará en la base de datos. Una vez hecha la actualización nos llevará al listado principal donde veremos el libro modificado.

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 nuestro servlets necesitan dos JSPs con los formularios para el alta y la modificación de los libros. Estos JSPs son:

  • /jsp/biblio/altaLibro.jsp: Contiene un formulario en el que deberemos introducir los datos del nuevo libro (isbn, titulo, autor, y número de páginas). Este formulario llamará al servlet LibroAddServlet. Alta de libros
  • /jsp/biblio/modificaLibro.jsp: Contiene un formulario para editar los datos del libro. Recibirá en el ámbito de la petición un atributo libro con los datos actuales del libro a editar. Mostrará estos datos como valores por defecto en los campos del formulario (isbn, titulo, autor y número de páginas). En este caso es importante que el campo ISBN sea de sólo lectura, ya que este es el identificador del libro y no debe ser modificado. Respecto a la fecha de alta, no es necesario permitir que se edite este campo. Modificación de libros

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

Por último, nos queda por implementar la funcionalidad de logout. Para ello crearemos un servlet LogoutServlet. Lo único que deberá realizar este servlet es invalidar la sesión actual y llevarnos de nuevo a la página principal para que nos vuelva a pedir los datos de login.

Entrega

Para la entrega se deberá etiquetar el estado del proyecto con el tag entrega-proyint-cw-web. Recordamos que la dirección del proyecto padre proyint-jbib en el servidor SVN debe ser:

svn+ssh://server.jtech.ua.es/home/svn/<login>/proyint/trunk/proyint-jbib

Como ayuda, proporcionamos la siguiente figura con un ejemplo de estructura de paquetes y clases resultantes de esta sesión.