Aplicación web con servlets
Introducción
En esta sesión de integración incorporaremos la estructura de clases y los DAOs implementados en las sesiones anteriores, a una aplicación web dinámica que, mediante servlets que utilicen estas clases previas, aportarán todas las funcionalidades de los DAOs a la aplicación web.
Creación del proyecto web
Vamos a crear en primer lugar un nuevo proyecto con Maven, de nombre jbib-web, utilizando para ello el arquetipo webapp-jee5, para tenerlo configurado directamente con la versión de Java EE que vamos a utilizar:
mvn archetype:generate -DgroupId=org.especialistajee.proyint -DartifactId=jbib-web -Dversion=1.0-SNAPSHOT -Dpackage=org.especialistajee.jbib.web
Nos mostrará una lista con todos los posibles arquetipos, y deberemos introducir el número del que queramos utilizar. El arquetipo webapp-jee5 podremos encontrarlo en la posición 208:
208: remote -> webapp-jee5 (JEE 5 web application archetype)
Con esto tendremos configurada la API de Servlets 2.5, de JSP 2.1 y el compilador a nivel 1.5. Ahora pasaremos a preparar Eclipse para poder importar este proyecto. Iremos a Help > About Eclipse > Installation Details para comprobar si tenemos instalado 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 ...
- Deberemos añadir un nuevo site pulsando sobre el botón Add ...
- Pondremos como nombre del nuevo site m2eclipse extras y como dirección http://m2eclipse.sonatype.org/sites/m2e-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 importaremos el proyecto de Maven en Eclipse, como Existing Maven Project, igual que hemos hecho en los proyectos anteriores.
Una vez importado el proyecto, configuraremos el plugin Cargo para automatizar el proceso de despliegue con Maven. Para ello deberemos añadir las siguientes líneas al pom.xml:
<plugin> <groupId>org.codehaus.cargo</groupId> <artifactId>cargo-maven2-plugin</artifactId> <configuration> <container> <containerId>tomcat6x</containerId> <home>/opt/apache-tomcat-6.0.29</home> </container> <configuration> <home>${project.build.directory}/tomcat6x</home> </configuration> </configuration> </plugin>
Con esto podremos poner en marcha el servidor desde línea de comando mediante:
mvn install mvn cargo:start
También podemos ejecutar nuestra aplicación directamente desde Eclipse como hemos hecho a lo largo del curso (Run As > Run on server). En este caso tendremos que configurar Tomcat en Eclipse, si no está configurado ya.
Si probamos a ejecutar la aplicación web de cualquiera de las formas anteriores veremos una página de prueba creada automáticamente por Maven, y que estará desplegada en la siguiente dirección:
http://localhost:8080/jbib-web/
Configuración de las dependencias
Añadiremos las siguientes dependencias al proyecto, a parte de las librerías de Servlets y JSPs ya incluidas por el arquetipo:
<dependency> <groupId>httpunit</groupId> <artifactId>httpunit</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.8.1</version> <type>jar</type> <scope>test</scope> </dependency> <dependency> <groupId>org.especialistajee.proyint</groupId> <artifactId>jbib-persist-dao</artifactId> <version>1.0</version> <type>jar</type> <scope>compile</scope> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.1</version> <type>jar</type> <scope>compile</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.0.8</version> <type>jar</type> <scope>compile</scope> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.14</version> <type>jar</type> <scope>compile</scope> </dependency>
Cabe destacar que no es estrictamente necesario añadir dependencias al driver de MySql y a las librerías de logs, ya que al tener una dependencia con el proyecto jbib-persist-dao, y éste tener dependencias con dichas librerías, automáticamente las tendremos incluidas en el nuevo proyecto. Entonces, ¿conviene indicarlas de forma explícita o no? La respuesta a esta pregunta depende del caso:
- Si nuestro proyecto no utiliza nunca directamente las librerías, sino que sólo son necesarias para ejecutar el código del proyecto del que dependemos, entonces no deberemos incluir las dependencias. Por ejemplo, se da este caso si en el proyecto web no escribimos logs con log4j, y sólo se escriben cuando ejecutamos métodos del DAO. Entonces la dependencia hacia log4j debería estar sólo en el proyecto jbib-persist-dao.
- En caso de utilizar directamente esas librerías en el proyecto, si que deberíamos incluir la dependencia de nuevo. Si no la incluimos seguirá funcionando, ya que está incluida en un proyecto del que dependemos. Sin embargo, si en algún momento ese proyecto cambiase y dejase de utilizarlas, y por lo tando de incluirlas, nuestro proyecto fallará por la falta de dichas dependencias.
Dado que nuestra intención es utilizar log4j en nuestros componentes web, y que necesitamos el driver de MySql para que el servidor pueda realizar la autenticación mediante un realm JDBC, será conveniente volver a incluir estas dependencias de forma explícita en nuestro proyecto.
A parte de estas librerías, incluimos junit y httpunit para poder hacer pruebas de unidad de nuestros componentes web.
Sobre el driver de MySql deberemos tener en cuenta que 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 (a través del DAO en nuestro caso). Sin embargo, dado que vamos a utilizar seguridad declarativa, el servidor Tomcat también deberá acceder a ella para autenticar a los usuarios. Por lo tanto, no bastará con que el driver esté disponible para nuestra aplicación, sino que deberá estar disponible para Tomcat. En Maven podemos hacer esto añadiendo la dependencia en la configuración del plugin Cargo:
<plugin> <groupId>org.codehaus.cargo</groupId> <artifactId>cargo-maven2-plugin</artifactId> <configuration> <container> <containerId>tomcat6x</containerId> <home>/opt/apache-tomcat-6.0.29</home> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> </dependencies> </container> <configuration> <home>${project.build.directory}/tomcat6x</home> </configuration> </configuration> </plugin>
Con esto será suficiente para que la aplicación funcione correctamente cuando arranquemos el servidor desde Maven. Sin embargo, cuando ejecutemos el proyecto desde Eclipse con Run on Server esta configuración no será tenida en cuenta, por lo que Tomcat no será capaz de encontrar el driver adecuado para MySql cuando intente autenticarnos.
También se puede descargar el fichero pom.xml completo.
Configurar el pooling de conexiones
En las sesiones de integración anteriores obteníamos una Connection simple, a través de un DriverManager, en el método createConnection de la clase org.especialistajee.jbib.dao.FactoriaMySqlDaos. Ahora que vamos a pasar a aplicación web, vamos a mejorar el rendimiento, accediendo al pooling de conexiones que nos ofrece Tomcat. Para ello, introduciremos los siguientes cambios:
- Creamos un fichero context.xml
en la carpeta META-INF
de nuestro proyecto web, donde definiremos las características del pooling:
<?xml version="1.0" encoding="ISO-8859-1"?> <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>
-
Modificar 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>
-
Finalmente, tendremos que hacer una modificación en el proyecto jbib-persist-dao.
Deberemos terminar de implementar el método createConnection de la clase
org.especialistajee.jbib.dao.FactoriaMySqlDaos. Recordemos que la obtención
de las conexiones mediante JNDI había quedado sin implementar. Ahora podremos implementar
esta parte haciendo que las obtenga del pool de conexiones que acabamos de configurar
en Tomcat. Para ello, en lugar de lanzar una excepción, cuando se especifique como método
para obtener conexiones JNDI utilizaremos el siguiente código:
case JNDI: try { Context initCtx = new InitialContext(); Context envCtx = (Context) initCtx.lookup("java:comp/env"); DataSource ds = (DataSource)envCtx.lookup("jdbc/biblioteca"); conn = ds.getConnection(); } catch (Exception sqle) { logger.fatal("No se ha podido crear la conexion", sqle); }
Además, especificaremos esta opción como método por defecto para obtener las conexiones:static private TipoFuenteDatos tipoFuenteDatos = TipoFuenteDatos.JNDI;
El problema que surge es que ahora las pruebas de unidad definidas en el proyecto jbib-persist-dao dejarán de funcionar, ya que el pool de conexiones sólo estará disponible cuando el código se ejecute desde dentro de Tomcat. Para evitar estos fallos modificaremos las pruebas de forma que antes de ejecutarse cambien el tipo de fuente de datos a DRIVER. Para ello, en primer lugar debemos definir un método en FactoriaMySqlDaos que nos permita hacer esto:
static public void setTipoFuenteDatos(TipoFuenteDatos tipo) { tipoFuenteDatos = tipo; }
Una vez hecho esto, crearemos un método anotado con @BeforeClass en cada clase de junit (LibroDaoTest y UsuarioDaoTest) que establezca el tipo de fuente de datos a DRIVER:
@BeforeClass public static void inizializaTests() { FactoriaMySqlDaos.setTipoFuenteDatos(TipoFuenteDatos.DRIVER); }
Lo mismo podemos hacer al comienzo del método main de la clase PruebaDao, si queremos que pueda seguir funcionando de forma independiente.
Una vez hechos estos cambios, instalaremos de nuevo este proyecto en el repositorio de Maven.
Montando la web
Acciones a realizar
De la sesión anterior, tenemos unos objetos DAO que realizan una serie de acciones sobre usuarios (IUsuarioDao y su implementación UsuarioMySqlDao), sobre libros (ILibroDao y su implementación LibroMySqlDao), sobre operaciones de usuarios con libros (IOperacionDao y su implementación OperacionMySqlDao), y sobre multas (IMultaDao y su implementación MultaMySqlDao). Estos objetos se apoyan para trabajar en sus respectivos objetos de dominio (domain) para ciertas operaciones, como por ejemplo, dar de alta un nuevo usuario, libro u operación, dado su objeto domain correspondiente (por ejemplo, pasamos un objeto LibroDomain para dar de alta un libro).
Lo que vamos a hacer en esta sesión es incorporar estas clases a la estructura de una aplicación web donde, mediante páginas HTML, indicaremos al servidor la operación que queremos realizar (por ejemplo, dar de alta un nuevo usuario, o listar todos los libros). Estas operaciones irán a parar al servlet encargado de atenderlas (veremos luego qué servlets implementar). Dicho servlet utilizará las clases auxiliares anteriores (los DAOs y los objetos de dominio) para realizar la operación, y luego mostrar un resultado (una página HTML generada por él mismo, con el resultado de la operación).
Es importante tener en cuenta que, para que todo esto funcione de la forma más modular posible, necesitamos tener las clases de manejo de DAOs de la sesión anterior, ya que serán las que nos proporcionen el "puente" para acceder desde los servlets a las operaciones que queramos realizar. Por ejemplo, para sacar un listado de todos los libros de la biblioteca, haríamos, desde el servlet, lo siguiente:
ILibroDao il = FactoriaDaos.getInstance().getLibroDao(); List<LibroDomain> lista = il.getLibros(); ...// y luego recorrer la lista y mostrarla
Como puede verse, el esquema a seguir es que el servlet obtenga en un objeto el DAO (o DAOs) apropiados, y éstos le proporcionen los interfaces DAO que necesite (ILibroDao, en este caso), y con dicho interfaz, llamar al método o métodos necesarios para completar la operación.
En esta sesión vamos a centrarnos en mostrar un listado de libros, y sobre el mismo, poder realizar reservas de los libros que puedan reservarse.
Listado de libros
En esta sesión todo va a partir de una página principal que generará un servlet SeleccionarLibroServlet, con un listado de libros. Dicho servlet lo tenéis parcialmente implementado aquí. Copiadlo en el paquete correspondiente (org.especialistajee.jbib.web):
Deberéis:
- Mapear adecuadamente el servlet en el fichero web.xml.
- Podríais, además de su mapeo, incluir temporalmente el servlet entre las
páginas de bienvenida, para que se cargue como página principal "/":
<welcome-file-list> <welcome-file>SeleccionarLibroServlet</welcome-file> </welcome-file-list>
- Modificar el listado de libros para que, además del título, saque también el autor y el número de páginas, antes de la columna "Operaciones".
- Obtener sólo los libros que estén disponibles para reserva (para hacer esto deberemos obtener el listado de libros a partir de IOperacionDao, en lugar de ILibroDao).
- Implementar la reserva de libros como se comenta a continuación.
Reserva de libros
Para la reserva de libros, definiremos 2 servlets:
-
Un primer servlet PrepararReservaServlet, que será llamado desde los enlaces de "Reservar" del listado de libros anterior (mapear el servlet adecuadamente). Recogerá como parámetro el ISBN del libro, y mostrará una página con un formulario para introducir el login del usuario a nombre del cual se realizará la reserva. Además, en la página se mostrará el título del libro que estamos reservando. En caso de que no reciba el parámetro isbn, o que el ISBN proporcionado no esté en la base de datos, nos redirigirá a la página principal (listado de libros). Para realizar dicha redirección utilizaremos el método sendRedirect.
-
Un segundo servlet RealizarReservaServlet que será llamado desde el formulario del paso anterior, y tomará como parámetros el ISBN del libro y el login del usuario que reserva. Tras realizar la reserva, mostrará una página con el resultado de la operación, y un enlace para poder volver al listado de libros. Para realizar la reserva podemos copiar al servlet el método realizaReserva de la clase PruebaDao implementada anteriormente. Este método debería estar en un objeto de negocio, pero por el momento podemos ponerlo directamente dentro de nuestro serlvet.
Configurar la seguridad
Como último paso, vamos a configurar seguridad declarativa en nuestra aplicación. Para eso seguiremos estos pasos:
-
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 en futuras sesiones de integración veremos cómo, dependiendo de su perfil (rol) le podemos dejar hacer unas u otras cosas.
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" />
-
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, como se vio en el módulo de Servidores Web.
SugerenciaEs 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. -
Finalmente, queda añadir las líneas de configuración de seguridad al fichero web.xml.
El bloque security-constraint:
<security-constraint> <web-resource-collection> <web-resource-name>...</web-resource-name> <url-pattern>...</url-pattern> </web-resource-collection> <auth-constraint> <role-name>...</role-name> <role-name>...</role-name> ... </auth-constraint> </security-constraint>
Hemos dejado en blanco (...) el contenido de cada etiqueta para que pongáis lo que corresponde. Deberemos especificar por separado los 3 roles para los que damos acceso (ALUMNO, PROFESOR y BIBLIOTECARIO). Para ello podemos añadir varias etiquetas role-name dentro del auth-constraint, como puede verse en el código anterior
Bajo el bloque anterior, colocamos el bloque login-config:
<login-config> <auth-method>...</auth-method> ... </login-config>
Pruebas con httpunit
Vamos a utilizar httpunit para probar los componentes web desarrollados. Estas pruebas se harán directamente sobre la clase del servlet, sin necesidad de tener el servidor web en marcha. Introduciremos la clase PreparaReservaServletTest en el paquete org.especialistajee.jbib.web dentro de la carpeta src/test/java.
En la clase hay definidos tres casos de prueba:
- Invocar el método doGet sin pasar el parámetro isbn. El resultado esperado es un código de redirección 302.
- Invocar el método doGet pasando como parámetro isbn un ISBN que no está en la base de datos (1234567890). El resultado esperado es un código de redirección 302.
- Invocar el método doGet pasando como parámetro isbn un ISBN válido (0321180860). El resultado esperado es un código de estado 200.
Hay que destacar que al ejecutar sin poner en marcha el servidor no tendremos disponible la fuente de datos JNDI. Por este motivo, antes de comenzar las pruebas se cambia el tipo de fuente de datos a DRIVER.
Ejecutaremos las pruebas con Maven y comprobaremos que nuestro servlet se comporta según lo especificado.
Resumen
En esta sesión deberemos entregar los proyectos jbib-comun, jbib-persist-dao y jbib-web. Deberemos realizar las siguientes tareas:
|
El plazo final de entrega será el jueves 9 de diciembre. Se realizará una sesión on-line de dudas el martes 7 de 19:00 a 21:00.