Proyecto Java EE
 

Caso de estudio

Introducción a Maven

Maven es una herramienta Java de gestión del proceso de desarrollo de proyectos software, que simplifica la complejidad de sus distintas partes: compilación, prueba, empaquetamiento y despliegue. Es una herramienta muy popular en proyectos open source que facilita:

  • la descarga de las librerías (ficheros JAR) externas de las que depende un proyecto
  • la construcción, prueba y despliegue del proyecto desarrollado, produciendo el fichero JAR o WAR final a partir de su código fuente y del fichero POM de descripción del proyecto

Maven se origina de hecho en la comunidad open source, en concreto en la Apache Software Foundation en la que se desarrolló para poder gestionar y minimizar la complejidad de la construcción del proyecto Jakarta Turbine en 2002. El diseñador principal de Maven fue Jason van Zyl, ahora en la empresa Sonatype. En 2003 el proyecto fue aceptado como proyecto de nivel principal de Apache. En octubre de 2005 se lanzó la versión más utilizada en la actualidad de Maven: Maven 2. Desde entonces ha sido adoptado como la herramienta de desarrollo de software de muchas empresas y se ha integrado con muchos otros proyectos y entornos.

Maven es una herramienta de línea de comando, similar a las herramientas habituales en Java como javac, jar o a proyectos como Ant. Aunque es posible utilizar Maven en IDEs como Eclipse o Glassfish, las interfaces de usuario incluyen sus distintos comandos en menús de opciones para que puedan seleccionarse de forma gráfica. Es muy útil conocer la utilización de Maven en línea de comandos porque es la base de cualquier adaptación gráfica.

Una de las características principales de Maven es su enfoque declarativo, frente al enfoque orientado a tareas de herramientas tradicionales como Make o Ant. En Maven, el proceso de compilación de un proyecto se basa en una descripción de su estructura y de su contenido. Maven mantiene el concepto de modelo de un proyecto y obliga a definir un identificador único para cada proyecto que desarrollemos, así como declarar sus características (URL, versión, librerías que usa, tipo y nombre del artefacto generado, etc.). Todas estas características deben estar especificadas en el fichero POM (Project Object Model, fichero pom.xml en el directorio raíz del proyecto). De esta forma es posible publicar el proyecto en un repositorio y ponerlo a disposición de la comunidad para que otros a su vez puedan usarlo como librería.

Instalando Maven

Maven ya viene preinstalado en la máquina virtual del experto. La instalación en Linux es muy sencilla.

En primer lugar debemos descargar la última versión de la página web oficial: http://maven.apache.org/download.html y descomprimirla en algún directorio del sistema. En el caso de la MV, lo hemos instalado en /opt/apache-maven-3.0.3.

Maven es una aplicación Java, y utiliza la variable JAVA_HOME para encontrar el path del JDK. También es necesario añadir el directorio bin de Maven al PATH del sistema. Se pueden definir en el fichero de configuración .bashrc de un usuario o en el fichero del sistema /etc/bashrc para todos los usuarios. En nuestro caso hemos modificado el único usuario de la MV especialista. El código que hemos añadido ha sido este:

export JAVA_HOME=/opt/jdk1.6.0_27
export MAVEN_HOME=/opt/apache-maven-3.0.3
export PATH=${PATH}:${MAVEN_HOME}/bin

Dependencias de librerías en proyectos Java

Una característica del desarrollo de proyectos Java es la gran cantidad de librerías (ficheros JAR) necesarios para compilar y ejecutar un proyecto. Todas las librerías que se importan deben estar físicamente tanto en la máquina en la que se compila el proyecto como en la que posteriormente se ejecuta.

El proceso de mantener estas dependencias es tedioso y muy propenso a errores. Hay que obtener las librerías, cuidar que sean las versiones correctas, obtener las librerías de las que éstas dependen a su vez y distribuirlas todas ellas en todos los ordenadores de los desarrolladores y en los servidores en los que el proyecto se va a desplegar.

Por ejemplo, si nuestro proyecto necesita una implementación de JPA, como Hibernate, es necesario bajarse todos los JAR de Hibernate, junto con los JAR de los que depende, una lista de más de 15 ficheros. Es complicado hacerlo a mano y distribuir los ficheros en todos los ordenadores en los que el proyecto debe compilarse y ejecutarse. Para que Maven automatice el proceso sólo es necesario declarar en el fichero POM las siguientes líneas:

...
   <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>3.5.6-Final</version>
   </dependency>
...

Maven se encarga de descargar todas las librerías cuando ejecutamos el comando mvn install. La siguiente imagen muestra las librerías descargadas en un proyecto Eclipse en el que se utiliza Maven:

El proceso de build de un proyecto

Los que hemos programado en C recordamos los ficheros Makefile en los que se especificaban las dependencias entre los distintos elementos de un proyecto y la secuencia de compilación necesaria para generar una librería o un ejecutable. En Java, el desarrollo de aplicaciones medianamente complejas es más complicado que en C. Estamos obligados a gestionar un gran número de recursos: código fuente, ficheros de configuración, librerías externas, librerías desarrolladas en la empresa, etc. Para gestionar este desarrollo es necesario algo de más nivel que las herramientas que proporciona Java (javac, jar, rmic, java, etc.)

¿En qué consiste el proceso de compilación y empaquetado en Java?. Básicamente en construir lo que Maven llama un artefacto (terminología de Maven que significa fichero) a partir de un proyecto Java definido con una estructura propia de Maven (apartado siguiente). Los posibles artefactos en los que podemos empaquetar un programa Java son:

  • Fichero JAR: librería de clases o aplicación standalone. Contiene clases Java compiladas (.class) organizadas en paquetes, ficheros de recursos y (opcionalmente) otros ficheros JAR con librerías usadas por las clases. En las aplicaciones enterprise, los EJB también se empaquetan en ficheros JAR que se despliegan en servidores de aplicaciones.
  • Fichero WAR: aplicación web lista para desplegarse en un servidor web. Contiene un conjunto de clases Java, librerías, ficheros de configuración y ficheros de distintos formatos que maneja el servidor web (HTML, JPG, etc.)
  • Fichero EAR: aplicación enterprise que se despliega en un servidor de aplicaciones. Contiene librerías, componentes EJB y distintas aplicaciones web (ficheros WAR).

Además, el ciclo de desarrollo de un proyecto es más complejo que esta construcción, ya que es necesario realizar un conjunto de tareas adicionales como gestionar las dependencias con librerías externas, integrar el código en repositorios de control de versiones (CVS, subversion o Git), lanzar tests o desplegar la aplicación en algún servidor de aplicaciones.

Podría pensarse que los entornos de desarrollo (Eclipse o Netbeans) pueden dar una buena solución a la complejidad del proceso de construcción, pero no es así. Son imprescindibles para el desarrollo, pero no ayudan demasiado en la construcción del proyecto. La configuración de las dependencias se realiza mediante asistentes gráficos que no generan ficheros de texto comprensibles que podamos utilizar para comunicarnos con otros compañeros o equipos de desarrolladores y que pueden dar lugar a errores. El hecho de que sean entornos gráficos hacen complicado también usarlos en procesos de automatización y de integración continua.

Estructura de un proyecto Maven

La estructura de directorios de un proyecto Maven genérico es la que aparece en la siguiente figura.

Si observamos el primer nivel, encontramos en la raíz del proyecto el fichero pom.xml, el directorio src y el directorio target. El fichero pom.xml es el fichero principal de Maven en el que se describen todas las características del proyecto: nombre, versión, tipo de artefacto generado, librerías de las que depende, ect. Lo estudiaremos más adelante. El directorio src contiene todo el código fuente original del proyecto. Y el directorio target contiene el resultado de la construcción del proyecto con Maven.

Entrando en más profundidad, nos encontramos los siguientes directorios:

  • src/main/java: el código fuente de las clases Java del proyecto
  • src/main/resources: ficheros de recursos que necesita la aplicación
  • src/main/filters: filtros de recursos, en forma de ficheros de propiedades, que pueden usarse para definir variables que se utilizan en tiempo de ejecución
  • src/main/config: ficheros de configuración
  • src/main/webapp: el directorio de aplicación web de un proyecto WAR
  • src/test/java: código fuente de las pruebas de unidad de las clases que tiene la misma estructura que la estructura del código fuente en el directorio main/java
  • src/tests/resources: recursos utilizados para los tests que no serán desplegados
  • src/tests/filters: filtros de recursos utilizados para los tests
  • src/site: ficheros usados para generar el sitio web Maven del proyecto

Los ficheros de recursos son ficheros leídos desde la aplicación. Tal y como hemos visto en el módulo JHD, estos recursos deben estar contenidos en la raíz del JAR para poder acceder a ellos utilizando el método getResourceAsStream de la clase Class:

InputStream in = 
   getClass().getResourceAsStream("/datos.txt");

Maven se encarga de colocar los recursos en la raíz del JAR al empaquetar el proyecto.

POM: Project Object Model

El elemento más importante de un proyecto Maven, a parte de su estructura, es su fichero POM en el que se define completamente el proyecto. Este fichero define elementos XML preestablecidos que deben ser definidos por el grupo de desarrollo del proyecto. Viendo algunos de ellos podemos entender también más características de Maven.

Vamos a utilizar como ejemplo el primer proyecto de integración jbib-modelo que construiremos más adelante. Veamos su fichero pom.xml. Al comienzo nos encontramojbib-modelos con la cabecera XML y la definición del proyecto:

<project xmlns="http://maven.apache.org/POM/4.0.0" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
  http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>es.ua.jtech.proyint</groupId>
<artifactId>jbib-modelo</artifactId>
<packaging>jar</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>jbib-modelo</name>
<url>http://web.ua.es/expertojee</url>

La primera definición project xmlns es común para todos los ficheros pom.xml. En ella se declara el tipo de esquema XML y la dirección donde se encuentra el fichero de esquema XML. Se utiliza para que los editores de XML puedan validar correctamente el fichero. Esta sintaxis depende de la versión de Maven que se esté utilizando.

Después aparece la identificación del proyecto, en la que hay que definir el grupo que desarrolla el proyecto (groupId), el nombre del artefacto que genera el proyecto (artifactId), el tipo de empaquetamiento (packaging) y su versión (version). Estos campos representan las denominadas coordenadas del proyecto (hablaremos de ello más adelante). En nuestro caso son:

es.ua.jtech.proyint:jbib-modelo:jar:0.0.1-SNAPSHOT

Por último, hay que definir el nombre lógico del proyecto (name) y una URL asociada al mismo url.

A continuación definimos algunas propiedades del proyecto, que se utilizarán en los distintos procesos de Maven. En nuestro caso sólo la codificación de caracteres que estamos utilizando en el código fuente de nuestro proyecto:

<properties>
   <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

A continuación viene la definición de las dependencias del proyecto: librerías de las que dependen el proyecto. En nuestro caso:

  • JUnit: junit:junit:4.8.1:jar
  • Log4j: log4j:log4j:1.2.14:jar
  • Commons Logging: commons-logging:commons-logging:1.1.1:jar
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.8.1</version>
      <type>jar</type>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.14</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>
  </dependencies>

Por último, definimos algunas características de los procesos de Maven que construyen el proyecto, definiendo parámetros para los pluging de Maven que se encargan de ejecutarlos.

En nuestro caso, definimos el nivel de compilación de Java, necesario para que no haya conflicto al importar el proyecto en Eclipse. Es interesante hacer notar que el plugin se identifica de una forma similar al proyecto, utilizando el identificador del grupo, el de proyecto y su número de versión.

<build>
      <plugins>
         <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
               <source>1.6</source>
               <target>1.6</target>
            </configuration>
         </plugin>
      </plugins>
   </build>
</project>

Es posible definir herencia entre los ficheros POM utilizando el identificador parent. Es útil para definir elementos comunes y evitar repetirlos en todos los POM. Por ejemplo, podríamos definir un POM en el que se declaren todas las librerías que se usan habitualmente por nuestros proyectos e incluir este POM en todos los proyectos utilizando herencia.

<project>
   <parent>
      <groupId></groupId>
      <artifactId></artifactId>
      <version></version>
   </parent>
  ...
</project>

Maven define un super POM que por defecto es el padre de todos los POM. Allí se definen elementos comunes como la localización de los repositorios o la estructura de directorios por defecto de Maven. Se puede encontrar este super POM en el fichero llamado pom-4.0.0.xml en el JAR maven-2.2.1-uber.jar en el directorio lib de Maven.

Maven resuelve todas las relaciones de herencia entre POMs y genera internamente un POM efectivo (effective POM) en el que combinan todos los POMs que afectan a un determinado proyecto. Este POM efectivo es el que se utiliza para realizar la construcción del proyecto. Es posible consultar este POM efectivo con el comando:

mvn help:effective-pom

También puede consultarse en la pestaña correspondiente del editor POM del plugin de Ecipse.

Repositorios

Los proyectos software están relacionados. Los proyectos necesitan de clases y librerías definidas en otros proyectos. Esos proyectos pueden ser otros desarrollados por nosotros en la empresa o librerías open source bajadas de Internet.

La tarea de mantener las dependencias de un proyecto es complicada, tanto para las dependencias entre nuestros proyectos como las dependencias con otros proyectos open source disponibles en Internet. Por ejemplo, si queremos utilizar un framework como Spring, tendremos que descargarnos no sólo los JAR desarrollados en el proyecto, sino también un buen número de otras librerías open source que usa. Cada librería es un fichero JAR. ¿Qué pasa si alguna de esas librerías ya las estamos usando y las tenemos ya descargadas? O, peor aún, ¿Qué pasa si estamos usando otras versiones de esas librerías en nuestros proyectos? ¿Podremos detectar los posibles conflictos?. Maven obliga a declarar explícitamente estas dependencias en el fichero POM del proyecto y se encarga de gestionar estos y otros problemas:

  • Descarga las librerías necesarias para construir el proyecto y los ficheros POM asociados a esas librerías
  • Resuelve dependencias transitivas, librerías que dependen de librerías de las que dependen nuestro proyecto
  • Resuelve conflictos entre librerías

Un elemento fundamental para gestionar las dependencias es poder identificar y nombrar un proyecto. En Maven el nombre de un proyecto se define mediante los siguientes elementos (que en Maven se denominan coordenadas):

  • groupId: El grupo, compañía, equipo, organización, etc. Se utiliza una convención similar a la de los paquetes Java, comenzando por el nombre de dominio invertido de la organización que crea el proyecto. Por ejemplo, los groupId de la Apache Software Foundation comienzan con org.apache.
  • artifactId: Identificador único que representa de forma única el proyecto dentro del groupId
  • version: Número de versión del proyecto, por ejemplo 1.3.5 o 1.3.6-beta-01
  • packaging: Tipo de empaquetamiento del proyecto. Por defecto es jar. Un tipo jar genera una librería JAR, un tipo war se refiere a una aplicación web.

En Maven un proyecto genera un artefacto. El artefacto puede ser un fichero JAR, WAR o EAR. El tipo de artefacto viene indicado en el tipo de empaquetamiento del proyecto.

El nombre final del fichero resultante de la construcción del proyecto es por defecto:

<artifactId>-<version>.<packaging>

Por ejemplo, Apache ha desarrollado el proyecto commons-email que proporciona una serie de utilidades para la gestión de correos electrónicos en Java. Sus coordenadas son:

org.apache.commons:commons-email:1.1:jar

El artefacto (fichero JAR) generado por el proyecto tiene como nombre email-1.1.jar

Cuando ejecutamos Maven por primera vez veremos que descarga un número de ficheros del repositorio remoto de Maven. Estos ficheros corresponden a plugins y librerías que necesita para construir el proyecto con el que estamos trabajando. Maven los descarga de un repositorio global a un repositorio local donde están disponibles para su uso. Sólo es necesario hacer esto la primera vez que se necesita la librería o el plugin. Las siguientes ocasiones ya está disponible en el repositorio local.

La direcciones en las que se encuentran los repositorios son las siguientes:

  • Repositorio central: El repositorio central de Maven se encuentra en http://repo1.maven.org/maven2. Se puede acceder a la dirección con un navegador y explorar su estructura.
  • Repositorio local: El repositorio local se encuentra en el directorio ${HOME}/.m2/repository.

La estructura de directorios de los repositorios (tanto el central como el local) está directamente relacionada con las coordenadas de los proyectos. Los proyectos tienen la siguiente ruta, relativa a la raíz del repositorio:

/<groupId>/<artifactId>/<version>/<artifactId>-<version>.<packaging>

Por ejemplo, el artefacto commons-email-1.1.jar, con coordenadas org.apache.commons:commons-email:1.1:jar está disponible en la ruta:

/org/apache/commons/commons-email/1.1/commons-email-1.1.jar

Versiones

El estándar de Maven para los números de versiones es muy importante, porque permite definir reglas para gestionar correctamente las dependencias en caso de conflicto. El número de versión de un proyecto se define por un número principal, un número menor y un número incremental. También es posible definir un calificador, para indicar una versión alfa o beta. Los números se separan por puntos y el calificador por un guión. Por ejemplo, el número 1.3.5-alpha-03 define un número de versión principal 1, la versión menor 3, la versión incremental de 5 y el calificador de "alpha-03".

Maven compara las versiones de una dependencia utilizando este orden. Por ejemplo, la versión 1.3.4 representa un build más reciente que la 1.0.9. Los clasificadores se comparan utilizando comparación de cadenas. Hay que tener cuidado, porque "alpha10" es anterior a "alpha2"; habría que llamar al segundo "alpha02".

Maven permite definir rangos de versiones en las dependencias, utilizando los operadores de rango exclusivos "(", ")" o inclusivos "[", "]". Así, por ejemplo, si queremos indicar que nuestro proyecto necesita una versión de JUnit mayor o igual de 3.8, pero menor que 4.0, lo podemos indicar con el siguiente rango:

<version>[3.8,4.0)</version>

Si una dependencia transitiva necesita la versión 3.8.1, esa es la escoge Maven sin crear ningún conflicto.

Es posible también indicar rangos de mayor que o menor que dejando sin escribir ningún número de versión antes o después de la coma. Por ejemplo, "[4.0,)" representa cualquier número mayor o igual que 4.0, "(,2.0)" representa cualquier versión menor que la 2.0 y "[1.2]" significa sólo la versión 1.2 y ninguna otra.

Cuando dos proyectos necesitan dos versiones distintas de la misma librería, Maven intenta resolver el conflicto, descargándose la que satisface todos los rangos. Si no utilizamos los operadores de rango estamos indicando que preferimos esa versión, pero que podríamos utilizar alguna otra. Por ejemplo, es distinto especificar "3.1" y "[3.1]". En el primer caso preferimos la versión 3.1, pero si otro proyecto necesitara la 3.2 Maven se descargaría esa. En el segundo caso exigimos que la versión descargada sea la 3.1. Si otro proyecto especifica otra versión obligatoria, por ejemplo "3.2", entonces el proyecto no se compilará.

Es posible utilizar la palabra SNAPSHOT en el número de versión para indicar que es una versión en desarrollo y que todavía no está lanzada. Se utiliza internamente en los proyectos en desarrollo. La idea es que antes de que terminemos el desarrollo de la versión 1.0 (o cualquier otro número de versión), utilizaremos el nombre "1.0-SNAPSHOT" para indicar que se trata de "1.0 en desarrollo".

La utilización de la palabra SNAPSHOT en una dependencia hace que Maven descargue al repositorio local la última versión disponible del artefacto. Por ejemplo, si declaramos que necesitamos la librería foo-1.0-SNAPSHOT.jar cuando construyamos el proyecto Maven intentará buscar en el repositorio remoto la última versión de esta librería, incluso aunque ya exista en el repositorio local. Si encuentra en el repositorio remoto la versión foo-1.0.-20110506.110000-1.jar (versión que fue generada el 2011/05/06 a las 11:00:00) la descarga y sustituye la que tiene en el local. De forma inversa, cuando ejecutamos el goal deploy y se despliega el artefacto en el servidor remoto, Maven sustituye el palabra SNAPSHOT por la fecha actual.

Gestión de dependencias

Hemos visto que una de las características principales de Maven es la posibilidad de definir las dependencias de un proyecto. En la sección dependencies del fichero POM se declaran las librerías necesarias para compilar, testear y ejecutar nuestra aplicación. Maven obtiene estas dependencias del repositorio central o de algún repositorio local configurado por nuestra empresa y las guarda en el directorio .$HOME/.m2/repository. Si utilizamos la misma librería en un varios proyectos, sólo se descargará una vez, lo que nos ahorrará espacio de disco y tiempo. Y lo que es más importante, el proyecto será mucho más ligero y portable, porque no llevará incluidas las librerías que necesita para su construcción.

Ya hemos visto en apartados anteriores cómo se declaran las dependencias en el fichero POM. Cada dependencia se define de forma unívoca utilizando sus coordenadas. El mecanismo de declaración de las dependencias es el mismo para las dependencias de librerías externas como para las definidas dentro de la organización.

Para definir una dependencia hay que identificar también el número de versión que se quiere utilizar, utilizando la nomenclatura del apartado anterior. Por ejemplo, la siguiente dependencia especifica una versión 3.0 o posterior de hibernate.

<dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate</artifactId>
   <version>[3.0,)</version>
 </dependency>

Un concepto fundamental en Maven es el de dependencia transitiva. En los repositorios no solo se depositan los artefactos generados por los proyectos, sino también el fichero POM del proyecto. Y en ese fichero se definen las dependencias propias del proyecto. Por ejemplo, junto con el artefacto hibernate-3.0.jar se encuentra el fichero POM hibernate-3.0.pom.xml en el que se definen sus propias dependencias, librerías necesarias para Hibernate-3.0. Estas librerías son dependencias transitivas de nuestro proyecto. Si nuestro proyecto necesita Hibernate, e Hibernate necesita estas otra librería B, nuestro proyecto también necesita (de forma transitiva) la librería B. A su vez esa librería B tendrá también otras dependencias, y así sucesivamente ...

Maven se encarga de resolver todas las dependencias transitivas y de descargar al respositorio local todos los artefactos necesarios para que nuestro proyecto se construya correctamente.

Otro elemento importante es el ámbito (scope) en el que se define la dependencia. El ámbito por defecto es compile y define librerías necesarias para la compilación del proyecto. También es posible especificar otros ámbitos. Por ejemplo test, indicando que la librería es necesaria para realizar pruebas del proyecto:

<dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.8.1</version>
   <type>jar</type>
   <scope>test</scope>
</dependency>

Otros ámbitos posibles son provided y runtime. Una dependencia se define provided cuando es necesaria para compilar la aplicación, pero que no se incluirá en el WAR y no será desplegada. Por ejemplo las APIs de servlets:

<dependency>
   <groupId>javax.servlet</groupId>
   <artifactId>servlet-api</artifactId>
   <version>2.4</version>
   <scope>provided</scope>
</dependency>

Las dependencias runtime son dependencias que no se necesitan para la compilación, sólo para la ejecución. Por ejemplo los drivers de JDBC para conectarse a la base de datos:

<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>3.1.13</version>
   <scope>runtime</scope>
</dependency>

Una herramienta muy útil es el informe de dependencia. Este informe se genera cuando se ejecuta el objetivo site. Maven construye un sitio web con información sobre el proyecto y coloca el informe en el fichero target/dependencies.html:

$ mvn site

El informe muestra una lista de dependencias directas y transitivas y su ámbito.

El ciclo de vida de Maven

El concepto de ciclo de vida es central para Maven. El ciclo de vida de un proyecto Maven es una secuencia de fases que hay que seguir de forma ordenada para construir el artefacto final.

Las fases principales del ciclo de vida por defecto son:

  • validate: valida que el proyecto es correcto y que está disponible toda la información necesaria
  • process-resources: procesar el código fuente, por ejemplo para filtrar algunos valores
  • compile: compila el código fuente del proyecto
  • test: lanza los tests del código fuente compilado del proyecto utilizando el framework de testing disponible. Estos tests no deben necesitar que el proyecto haya sido empaquetado o desplegado
  • package: empaqueta el código compilado del proyecto en un formato distribuible, como un JAR
  • integration-test: procesa y despliega el paquete en un entorno en donde se pueden realizar tests de integración
  • verify: lanza pruebas que verifican que el paquete es válido y satisface ciertos criterios de calidad
  • install: instala el paquete en el repositorio local, para poder ser usado como librería en otros proyectos locales
  • deploy: realizado en un entorno de integración o de lanzamiento, copia el paquete final en el repositorio remoto para ser compartido con otros desarrolladores y otros proyectos.

Todas estas fases se lanzan especificándolas como parámetro en el comando mvn. Si ejecutamos una fase, Maven se asegura que el proyecto pasa por todas las fases anteriores. Por ejemplo:

$ mvn install

Esta llamada realiza la compilación, los tests, el empaquetado los tests de integración y la instalación del paquete resultante en el repositorio local de Maven.

Nota
Para un listado completo de todas las opciones se puede consultar la página de Apache Maven Introduction to the Build Lifecycle

Ejecutando tests

Los tests de unidad son una parte importante de cualquier metodología moderna de desarrollo, y juegan un papel fundamental en el ciclo de vida de desarrollo de Maven. Por defecto, Maven obliga a pasar los tests antes de empaquetar el proyecto. Maven permite utilizar los frameworks de prueba JUnit y TestNG. Las clases de prueba deben colocarse en el directorio src/test.

Para ejecutar los tests se lanza el comando mvn test:

$ mvn test
[INFO] Scanning for projects...
...
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running es.ua.jtech.jbib.model.UsuarioTest
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.082 sec
Running es.ua.jtech.jbib.model.OperacionTest
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.141 sec
Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.018 sec
Running es.ua.jtech.jbib.model.AvisoTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.012 sec

Results :

Tests run: 19, Failures: 0, Errors: 0, Skipped: 0

Maven compilará los tests si es necesario. Por defecto, los tests deben colocarse en el directorio src/test siguiendo una estructura idéntica a la estructura de clases del proyecto. Maven ejecutará todas las clases que comiencen o terminen con Test o que terminen con TestCase.

Los resultados detallados de los tests se producen en texto y en XML y se dejan en el directorio target/surefire-reports. Es posible también generar los resultados en HTML utilizando el comando:

$ mvn surefire-report:report

El informe HTML se generará en el fichero target/site/surefire-report.html.

Plugins y goals

Todo el trabajo que realiza Maven es realizado por módulos independientes que son también descargados del repositorio global. Estos módulos reciben el nombre de plugins. Cada plugin tiene un conjunto de goals que podemos lanzar desde línea de comando. La sintaxis de una ejecución de un goal de un plugin es:

$ mvn <plugin>:goal -Dparam1=valor1 -Dparam2=valor2 ...

Las fases del ciclo de vida vistas en el apartado anterior también se procesan mediante el mecanismo de plugins. Por ejemplo, la llamada a mvn test realmente genera una llamada al objetivo test del plugin surefire:

$ mvn surefire:test

Otro ejemplo es el plugin Jar que define el objetivo jar para realizar la fase package:

$ mvn jar:jar

Existen múltiples plugins de Maven que pueden utilizarse para automatizar distintas tareas complementarias necesarias para la construcción del proyecto. Un ejemplo es el plugin SQL de Codehaus que permite lanzar comandos SQL utilizando su objetivo execute:

$ mvn sql:execute

La configuración del plugin se define en el fichero POM. La siguiente configuración lanza en la fase de construcción process-test-resource dos ficheros de comandos SQL; el primero inicializa la base de datos y el segundo la llena con datos sobre los que se van a realizar las pruebas.

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>sql-maven-plugin</artifactId>
  <version>1.4</version>

  <dependencies>
    <!-- specify the dependent JDBC driver here -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.0.8</version>
    </dependency>
  </dependencies>

  <!-- common configuration shared by all executions -->
  <configuration>
    <driver>com.mysql.jdbc.Driver</driver>
    <url>jdbc:mysql://localhost:3306/</url>
    <username>root</username>
    <password>expertojava</password>
  </configuration>

  <executions>
    <execution>
      <id>create-db</id>
      <phase>process-test-resources</phase>
      <goals>
        <goal>execute</goal>
      </goals>
      <configuration>              
        <srcFiles>
          <srcFile>src/main/sql/biblioteca.sql</srcFile>
        </srcFiles>
      </configuration>
    </execution>

    <execution>
      <id>create-data</id>
      <phase>process-test-resources</phase>
      <goals>
        <goal>execute</goal>
      </goals>
      <configuration>              
        <srcFiles>
          <srcFile>src/test/sql/datos.sql</srcFile>
        </srcFiles>
      </configuration>
    </execution>
  </executions>
</plugin>

Vemos que en el apartado dependency se definen las dependencias del plugin. En este caso el JAR mysql-connector-java-5.0.8 necesario para la conexión a la base de datos. En el apartado executions es donde se definen los ficheros SQL que se ejecutan y la fase del ciclo de vida en la que se lanza.

Cuando se llame a la fase process-test-resoures de Maven (o a cualquiera posterior) se ejecutaran los ficheros SQL:

$ mvn process-test-resources
[INFO] Scanning for projects...
...
[INFO] [sql:execute {execution: create-db}]
[INFO] Executing file: /tmp/biblioteca.2081627011sql
[INFO] 24 of 24 SQL statements executed successfully
[INFO] [sql:execute {execution: create-data}]
[INFO] Executing file: /tmp/datos.601247424sql
[INFO] 27 of 27 SQL statements executed successfully
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3 seconds
[INFO] Finished at: Wed Oct 06 06:16:02 CEST 2010
[INFO] Final Memory: 8M/19M
[INFO] ------------------------------------------------------------------------

La siguiente figura muestra un resumen de los conceptos vistos hasta ahora de fases de build y plugins:

Usando Maven en Eclipse

El plugin de Eclipse m2eclipse permite la utilización de Maven desde el entorno de programación. Ha sido desarrollado por la compañía Sonatype y se distribuye de forma gratuita. El plugin permite importar proyectos Maven en Eclipse, editar y explorar el fichero POM del proyecto, explorar repositorios de Maven o lanzar builds de Maven desde el propio entorno.

El documento Sonatype m2eclipse es un excelente manual de uso del plugin. Os remitimos a él para conocer en profundidad todas sus características. Aquí sólo haremos un rápido repaso.

El plugin permite importar un proyecto Maven en Eclipse y trabajar con él manteniendo su estructura de directorios. El plugin genera automáticamente los ficheros de configuración de Eclipse (los ficheros .project y .classpath) para que utilicen esa estructura de directorios y para que se actualicen las dependencias entre los proyectos y las librerías JAR.

Una vez importado el proyecto el desarrollador puede seguir trabajando en él como lo haría con cualquier proyecto Eclipse. Puede lanzar añadir código, ejecutarlo, lanzar los tests o compartirlo por CVS. En el caso en que fuera un proyecto web, podrá también desplegarlo en el servidor instalado en Eclipse.

El plugin m2eclipse proporciona un editor especializado para trabajar con los ficheros POM. En la parte inferior de la imagen se pueden observar pestañas para acceder a los distintas elementos configurados en el POM: general, dependencias, plugins, informes, jerarquía de dependencias, grafo de dependencias, POM efectivo y código XML.

Por ejemplo, la pestaña de dependencias del POM muestra el siguiente aspecto.

Desde esa pestaña es posible editar las dependencias del POM y añadir nuevas. Una ventana de diálogo permite explorar todos los artefactos del repositorio de Maven.

Es posible activar una vista especial que permite hojear los repositorios. Se activa en la opción Window > Show View > Other... y buscando Maven y seleccionando Maven Repositories.

Es posible también lanzar builds predeterminados de Maven y configurar esas ejecuciones.

Usando Maven con Mercurial

Para crear un repositorio Mercurial a partir de un proyecto Maven, hay que hacer lo habitual: inicializar el repositorio mercurial en la raíz del proyecto y añadir todos sus ficheros. Hay que tener cuidado de añadir en el repositorio sólo los ficheros fuente. Todos los ficheros que crea Maven a partir de los ficheros fuente originales deben ser ignorados.

El siguiente fichero .hgignore contiene las reglas que determinan los ficheros ignorados. Ignoramos los ficheros .jar porque Maven los obtiene automáticamiente.

Fichero .hgignore

syntax: glob

# java deployment assets
*.class
*.jar

# mvn build trees
target/*
cargo/*
bin/*

# hg merge reject files and originals arising for reverts
*.rej
*.orig

# Mac idiosyncrasies
.DS_Store

# Eclipse local workspace configuration
.metadata/*

# Eclipse configuration files are created from Maven
.classpath 
.project   
.settings

Para saber más

Las siguientes referencias son de mucho interés para aprender más sobre Maven o como referencia para consultar dudas:

Caso de estudio

Introducción

A partir de un supuesto básico de la gestión de una biblioteca, vamos a crear un caso de estudio completo que evolucionará conforme estudiemos las diferentes tecnologías de la plataforma Java Enterprise.

El objetivo de esta sesión es introducir el caso de estudio que vamos a desarrollar, obtener una visión global del proyecto, fijando los casos de uso y requisitos principales y definiendo el esqueleto inicial del problema.

Ingeniería de Requisitos

El Instituto de Educación Secundaria "jUA" nos ha encargado que desarrollemos una aplicación para la gestión de los préstamos realizados en la biblioteca del centro, lo que implica tanto una gestión de los libros como de los alumnos y profesores que realizan estos prestamos.

Tras una serie de entrevistas y reuniones con diferente personal del centro, hemos llegado a recopilar las siguientes funcionalidades a informatizar.

  • Un usuario administrador puede realizar acciones relacionadas con la configuración de la aplicación: dar de alta nuevos usuarios, modificar ciertos parámetros, etc.
  • La aplicación será utilizada por bibliotecarios y usuarios de la biblioteca.
  • La biblioteca contiene libros. El sistema debe guardar toda la información necesaria de cada libro: su título, autor, ISBN, etc. Puede existir más de un ejemplar de un mismo libro. Se quiere también guardar la información propia de cada ejemplar: fecha de adquisición, defectos que pueda tener, etc.
  • Tanto bibliotecarios como usuarios podrán realizar con la aplicación acciones sobre los libros y ejemplares: consultar su disponibilidad, reservarlos, prestarlos, etc. Las acciones estarán limitadas al rol.
  • Un bibliotecario se encargará de la gestión de préstamos, libros y ejemplares. La aplicación le permitirá registrar los préstamos y devoluciones de ejemplares que realiza en el mostrador de la biblioteca, así como consultar el estado de un determinado libro, ejemplar o usuario. Podrá también dar de alta libros, ejemplares, modificar su información y darlos de baja.
  • Los profesores y alumnos van a poder realizar las siguientes acciones con la aplicación:
    • Pedir prestado un ejemplar disponible (que el bibliotecario le entregará cuando se pase por el mostrador)
    • Reservar un libro que tiene todos los ejemplares prestados
    • Consultar el estado de los libros y sus ejemplares: un libro puede estar prestado (en sala, en casa o en el departamento) o disponible.
  • Además podrán hacer las siguientes acciones físicas:
    • Recoger un préstamo
    • Devolver un préstamo
  • Para recoger el ejemplar pedido tanto los profesores como los alumnos deberán personarse en la biblioteca, coger el libro y solicitar su préstamo al bibliotecario. El bibliotecario utilizará la aplicación para buscar el ejemplar que ha pedido prestado y se lo dará.
  • La fecha de devolución del ejemplar dependerá de si el usuario es alumno o profesor y empezará a contar a partir del momento en que el libro se toma prestado con la aplicación. El número máximo de libros que puede tener en préstamo un usuario dependerá también de si es profesor o alumno.
  • Los usuarios pueden reservar libros con la aplicación cuando todos los ejemplares estén prestados. En el momento en que uno de los ejemplares se devuelva y quede disponible, la aplicación consultará la lista de reservas del libro y se lo prestará automáticamente al usuario que ha hecho la reserva más antigua. Le enviará una notificación para que sepa lo tiene prestado y que tiene que pasar a recogerlo por la biblioteca.
  • El usuario puede cancelar una reserva en cualquier momento. Cuando la reserva se cancela o se realiza un préstamo asociado a ella, pasa a un histórico de reservas.
  • Existe un número máximo de préstamos y reservas (operaciones) que puede hacer un usuario. La suma del número de préstamos y reservas de un usuario no puede sobrepasar este máximo. El máximo depende de si el usuario es un profesor o un alumno.
  • El estado por defecto de un usuario es activo. Cuando el usuario se retrasa en la devolución de un préstamo pasa a estado moroso. En ese estado no puede pedir prestado ni reservar ningún otro libro. Cuando devuelve el libro se le crea una multa y pasa a estado multado.
  • De la multa nos interesa saber la fecha de inicio y de finalización de la misma. La finalización de la multa dependerá del tipo de usuario. Nos han comunicado que quieren mantener un histórico de las multas que ha tenido un usuario. Cuando pasa la fecha de finalización, el estado del usuario vuelve a activo.

Estas funcionalidades las vamos a convertir más adelante en casos de uso y las vamos a implementar a lo largo del curso, conforme vaya avanzando el proyecto de integración.

Requisitos de Información (IRQ)

Los requisitos de información resumen la información persistente que nos interesa almacenar relacionada con el sistema.

Respecto a un usuario, nos interesa almacenar:

  • Tipo de usuario:profesor, alumno
  • Login y password
  • Nombre y apellidos
  • Correo electrónico
  • Estado de un usuario: activo, moroso o multado
  • Datos referentes a su dirección, como son calle, número, piso, ciudad y código postal.
  • Si el usuario es alumno, necesitaremos guardar quién es su tutor
  • Si el usuario es profesor, necesitaremos su departamento

Existirá un tipo especial de usuario en el sistema, el bibliotecario, que tiene acceso a todas las funciones de gestión de libros, ejemplares y préstamos. Queremos guardar:

  • Login y password
  • NIF: identificador del bibliotecario

Cuando un usuario se retrase en la devolución de un libro, se le creará una multa. Nos han comunicado que quieren mantener un histórico de las multas que ha tenido un usuario. De cada multa nos interesa saber:

  • Usuario que tiene la multa
  • Fecha de inicio
  • Fecha de finalización
  • Estado de la multa: ACTIVA o HISTÓRICA

Respecto a un libro, nos interesa almacenar:

  • ISBN
  • Título y autor
  • Número de páginas
  • Fecha de alta del libro
  • Número de ejemplares disponibles: cambiará conforme se presten y devuelvan ejemplares

En los ejemplares individuales de cada libro guardaremos información propia del ejemplar y además información del préstamo en el que caso en que algún usuario lo haya tomado prestado:

  • Libro del que es ejemplar
  • Identificador del ejemplar: código identificador del ejemplar
  • Fecha de adquisición: fecha en la que el ejemplar se adquirió
  • Usuario que lo ha tomado prestado (vacío si el ejemplar está disponible)
  • Fecha de inicio del préstamo: fecha en la que el usuario ha tomado prestado el ejemplar
  • Fecha de devolución del préstamo: fecha en la que el usuario debe devolver el ejemplar

Respecto a los prestamos históricos vamos a guardar:

  • Fecha de inicio y finalización
  • Usuario del préstamo
  • Ejemplar del préstamo

Por último, en las reservas guardaremos:

  • Usuario que realiza la reserva
  • Libro que se reserva
  • Fecha de reserva: fecha en la que se realiza la reserva
  • Fecha de finalización de la reserva: fecha en la que el usuario cancela la reserva o se activa un préstamo
  • Estado de la reserva: ACTIVA o HISTÓRICA
  • Tipo de finalización de la reserva: cancelada o préstamo

Casos de Uso

En un principio, el número de casos de uso es muy limitado. A lo largo del proyecto nos vamos a centrar en el desarrollo de los siguientes casos de uso generales:

  • El bibliotecario podrá gestionar libros y ejemplares: consultándolos, modificándolos, dándolos de alta y eliminándolos. También podrá consultar y actualizar las operaciones relacionadas con los libros y los usuarios: prestar y devolver ejemplares. Relacionado con las operaciones, podrá consultar el histórico de préstamos y reservas. Y, por último, también podrá consultar los usuarios: sus préstamos, reservas y multas.
  • El usuario podrá consultar los libros y ejemplares de la biblioteca, pedirlos prestados o reservarlos. Podrá también consultar sus propios libros y ejemplares prestados y reservados, así como su histórico de préstamos, reservas y multas.

Destacar que la validación de usuarios para todos los actores se considera una precondición que deben cumplir todos los casos de uso, y por lo tanto no se muestra en el diagrama. Entendemos también que la realización de una reserva por parte de un usuario

A continuación vamos a mostrar en mayor detalle los casos de uso separados por el tipo de usuario, de modo que quede más claro cuales son las operaciones que debe soportar la aplicación.

Un poco de UML...
Recordar que las relaciones entre casos de uso con estereotipo ‹include› representan que el caso de uso incluido se realiza siempre que se realiza el caso base. En cambio, el estereotipo ‹extend› representa que el caso de uso extendido puede realizarse cuando se realiza el caso de uso padre (o puede que no).
Bibliotecario

El tipo de usuario bibliotecario es el más complejo de nuestra aplicación. Como se puede observar, es el que va a poder realizar un mayor número de casos de uso:

Podemos observar tres consultas principales, a partir de las que se derivan posibles acciones adicionales, y una operación de "Alta de libros y ejemplares". El alta permite añadir nuevos libros y/o ejemplares.

Las consultas son las siguientes:

  • Consultar libros y ejemplares: el bibliotecario puede consultar tanto libros como ejemplares y, a partir del listado, seleccionar una serie de posibles acciones:
    • Baja libros y ejemplares: se puede dar de baja libros o ejemplares
    • Modificación libros y ejemplares: modificar los datos de los libros o ejemplares
    • Consultar ejemplares prestados: consultar qué ejemplares se encuentran prestados de un libro; se mostrará la información de los usuarios que han realizado el préstamo
    • Consultar libros reservados: consultar los libros con reservas; se mostrará la información de los usuarios que han realizado la reserva
    • Consultar ejemplares disponibles: consultar ejemplares disponibles de un libro
    • Prestar ejemplar: prestar en el mostrador un ejemplar a un usuario
    • Devolver ejemplar: devolver un ejemplar que se entrega en el mostrador
  • Consultar usuarios: el bibliotecario puede consultar y seleccionar usuarios y hacer las siguientes acciones: devolver un ejemplar que el usuario entrega y tenía prestado, entregar un préstamo que el usuario ha realizado con la aplicación o consultar si el usuario está multado y su histórico de multas
  • Consultar histórico: el bibliotecario puede consultar el histórico de préstamos y reservas realizadas por los usuarios
Alumno o Profesor

En cuanto a un usuario cuyo tipo sea alumno o profesor, y por tanto, sea un usuario registrado en el sistema, las operaciones que puede realizar son las siguientes:

  • Consultar libros y ejemplares: una vez buscados los libros y ejemplares, el usuario podrá tomar prestado un ejemplar disponible o reservar un libro cuyos ejemplares están prestados. Se deberá cumplir el cupo de prestamos y reservas. No podrán tomar prestado ni hacer reservas aquellos usuarios multados.
  • Consultar mis libros y ejemplares: el usuario podrá consultar un listado de los libros y ejemplares que tiene prestados y reservados. Podrá también anular la reserva de un libro.
  • Consultar el histórico de mis préstamos y reservas: el usuario podrá consultar y buscar en un listado del histórico de sus préstamos y reservas.

Más adelante, conforme cambien los requisitos del cliente (que siempre cambian), puede que el sistema permita renovar los prestamos a los usuarios registrados, que éstos puedan modificar su información de usuario, que los bibliotecarios obtengan informes sobre libros más prestados y/o reservados, etc...

Requisitos de Restricción (CRQ)

Respecto a las restricciones que se aplicarán tanto a los casos de uso como a los requisitos de información, y que concretan las reglas de negocio, hemos averiguado:

  • Diferencias a la hora de realizar préstamos y reservas tanto por parte de un profesor como de un alumno:

    Número máximo de operacionesDías de préstamo
    Alumno57
    Profesor830

    Según esta información, el máximo de operaciones (reservas y préstamos) de un alumno es 6. Los libros prestados los tiene que devolver antes de 7 días.

    En el momento que un usuario tenga una demora en la devolución de un préstamo, se considerará al usuario moroso y se le impondrá una penalización del doble de días de desfase durante los cuales no podrá ni reservar ni realizar préstamos de libros.

Análisis y Diseño OO

A partir de esta captura de requisitos inicial, vamos a plantear los elementos que van a formar parte de la aplicación, comenzado por el modelo de clases.

Modelo de Clases Conceptual

A partir de los requisitos y tras unas sesiones de modelado, hemos llegado al siguiente modelo de clases conceptual representado mediante el siguiente diagrama UML:

Utilizaremos un modelo de clases como punto de partida del modelo de datos. En la siguiente sesión construiremos el modelo de datos basándonos en este modelo de clases y utilizando JPA (Java Persistence API). Veremos que este enfoque se denomina ORM (Object Relational Mapping), porque permite definir una relación directa (mapping) entre clases Java y tablas de la base de datos. La relación entre clases Java y tablas se define por medio de anotaciones JPA añadidas en el código fuente de las clases.

¿Por dónde empezamos al hacer el diseño de la aplicación? ¿Por los datos o por las clases? Podemos empezar modelando los datos o las clases, y ambos modelos serán casi semejantes. Normalmente, la elección viene dada por la destreza del analista, si se siente más seguro comenzando por los datos, o con el modelo conceptual de clases. Otra opción es el modelado en paralelo, de modo que al finalizar ambos modelos, podamos compararlos y validar si hemos comprobado todas las restricciones. Daremos más detalles en la siguiente sesión.

Diagramas de Estado

A continuación podemos visualizar un par de diagramas que representan los diferentes estados que puede tomar tanto los ejemplares (por medio de las operaciones) como los usuarios (mediante las acciones que conllevan algunas operaciones).

Estados de un ejemplar

El estado de un ejemplar puede ser DISPONIBLE o PRESTADO. El cambio de un estado a otro cambia cuando se toma prestado y cuando se devuelve. Un caso especial es cuando el ejemplar se devuelve, pero el libro tiene una reserva. El libro continua prestado, ahora al usuario que ha hecho la reserva.

Lo representamos con el siguiente diagrama:

Estados de un usuario

Del mismo modo, los estados de un usuario dependen de si el usuario realiza sus operaciones dentro de las reglas de negocio permitidas. Si la fecha de devolución de algún libro que tiene prestado un usuario vence sin que lo haya devuelto, el usuario pasa a ser MOROSO. Cuando el usuario devuelve el libro, pasa a ser MULTADO. Cuando el usuario ha cumplido todas las multas, vuelve a ser ACTIVO:

Implementación

El objetivo de la sesión de hoy es crear el proyecto jbib-modelo, módulo inicial en el que vamos a incluir todas las clases de dominio, los tipos enumerados y las clases auxiliares. Las clases de dominio implementan las entidades de la aplicación (objetos que vamos a hacer persistentes en la base de datos) y las podemos encontrar en el diagrama de clases presentado en el apartado anterior.

Vamos a construir el proyecto utilizando Maven como herramienta de gestión de builds. Al ser un proyecto formado por múltiples módulos, utilizaremos una estructura Maven de multiproyecto, en el que un directorio padre con un POM define las características comunes y contiene a distintos subdirectorios. Cada subdirectorio define un módulo y contiene su propio fichero POM que describe sus características específicas.

A lo largo de las siguientes sesiones del proyecto iremos construyendo nuevos módulos necesarios para la aplicación.

Paso a paso: Construcción del proyecto

Vamos a crear el espacio de trabajo eclipse llamado proyint-expertojava dentro de /home/expertojava/workspaces. El espacio de trabajo contendrá el proyecto Maven con los distintos módulos que iremos programando a lo largo del curso. También crearemos un repositorio Mercurial en el que guardaremos el workspace y lo subiremos a Bitbucket.

1. Creamos el directorio /home/expertojava/workspaces/proyint-expertojava e inicializamos Mercurial:

$ cd /home/expertojava/workspaces
$ mkdir proyint-expertojava
$ cd proyint-expertojava
$ hg init .

2. Añadimos el fichero .hgignore para trabajar con Maven. Lo podemos copiar del repositorio Bitbucket java_ua/proyint-expertojava (dejaremos allí la solución de esta sesión cuando se cumpla el plazo de entrega).

2. Una vez inicializado el repositorio Mercurial, abrimos un workspace de Eclipse en el directorio creado:

Vamos a utilizar el asistente de Eclipse para crear proyectos Maven. Pulsamos con el botón derecho en el panel de proyectos la opción New > Project... > Maven > Maven Project:

3. Creamos el proyecto padre de todos los módulos que iremos desarrollando más adelante. Seleccionamos la opción skip archetype selection para no basar el proyecto Maven en ningún arquetipo. Eclipse únicamente generará el proyecto con la estructura de directorios Maven adecuada y el POM mínimo con su descripción.

4. Introduce los datos del proyecto necesarios para crear el POM inicial de Maven con los siguientes parámetros:

  • GroupId: es.ua.jtech.proyint
  • ArtifactId: jbib
  • Version: 0.0.1-SNAPSHOT
  • Packaging: pom
  • Name: jbib

Eclipse creará un proyecto nuevo en el que existirá un directorio de fuentes (src) y el fichero POM que descibre este proyecto padre. En el icono del proyecto puedes ver una pequeña "M" que indica que se trata de un proyecto Maven y que el plugin Maven de Eclipse lo ha reconocido como tal.

Podemos abrir el fichero POM para comprobar que el XML se ha creado correctamente:

<project xmlns="http://maven.apache.org/POM/4.0.0" 
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
   http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <groupId>es.ua.jtech.proyint</groupId>
   <artifactId>jbib</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <packaging>pom</packaging>
   <name>jbib</name>
   <description>Proyecto padre de los módulos del proyecto de 
      integración Biblioteca Java</description>
</project>

5. Procedemos a continuación a crear el módulo con el que vamos a trabajar en esta sesión: jbib-modelo. Lo hacemos también utilizando el asistente de Eclipse. Seleccionamos con el botón derecho la opción New > Project... > Maven > Maven Module:

Marcamos también la opción de saltar la selección del arquetipo y escribimos el nombre del módulo y del proyecto padre:

Introducimos a continuación los datos del módulo para crear su POM

  • GroupId: es.ua.jtech.proyint
  • ArtifactId: jbib-modelo
  • Version: 0.0.1-SNAPSHOT
  • Packaging: jar
  • Name: jbib-modelo

Es importante el tipo de empaquetamiento. El módulo se va a empaquetar como un JAR conteniendo todas las clases del dominio y las clases de utilidad. Hay que definir esta opción. Ya veremos más adelante como Maven automatiza el proceso de compilación, prueba y empaquetamiento del JAR.

Podemos comprobar ahora que el proyecto se ha creado correctamente y que su estructura corresponde con la de un proyecto Maven:

Vemos que se ha creado un directorio de fuentes src/main, y otro directorio de tests src/tests. También vemos un directorio target donde Maven colocará los ficheros .class compilados y el JAR resultante del empaquetado del proyecto.

Si revisas los ficheros POM veremos que en el del proyecto padre se ha añadido el nombre del módulo recién creado. El POM del módulo contiene los datos introducidos:

<project xmlns="http://maven.apache.org/POM/4.0.0" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
    http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
      <artifactId>jbib</artifactId>
      <groupId>es.ua.jtech.proyint</groupId>
      <version>0.0.1-SNAPSHOT</version>
      <relativePath>..</relativePath>
   </parent>
   <artifactId>jbib-modelo</artifactId>
   <name>jbib-modelo</name>
   <description>Paquete JAR que implementa el modelo de dominio 
      Biblioteca Java</description>
</project>

6. Hay que añadir algunos elementos a los ficheros POM para configurar correctamente los proyectos.

En primer lugar, podemos ver que el plug-in de Maven interpreta incorrectamente la versión de la máquina virtual Java del proyecto. En el proyecto aparece la J2SE-1.5, en lugar de la que está configurada por defecto en Eclipse, la 1.6. Para solucionar el aviso, indicamos en el POM del proyecto padre explícitamente que el proyecto va a trabajar con la versión 1.6 del compilador Java.

También indicamos que la codificación de los ficheros de código fuente que componen el proyecto es UTF-8 (la versión de Eclipse de la máquina virtual está configurado de esa forma, se puede comprobar en Windows > Preferences > General > Workspace > Text file encoding).

Para ello añadimos el siguiente código al fichero POM del proyecto padre (login-proyint-jbib/pom.xml).

<project> 
  ...

  <properties>
       <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 </properties>


  <build>
   <plugins>
      <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-compiler-plugin</artifactId>
         <configuration>
             <source>1.6</source>
             <target>1.6</target>
         </configuration>
      </plugin>
   </plugins>
</build>

</project>

7. Para que el plugin de Maven actualice los proyectos, hay que pulsar con el botón derecho la opción Maven > Update project configuration. Hazlo en el proyecto hijo jbib-modelo y verás que en el explorador de proyectos se actualiza la versión de Java a la 1.6 y desaparece el aviso que existía.

8. Por último, añadimos en el proyecto padre las dependencias de algunas librerías que vamos a usar en todos los módulos del proyecto:

  • Librería de pruebas junit: junit-4.8.1
  • Librerías de gestión de logs commons-logging y log4j: commons-logging-1.1.1 y log4j-1.2.12
   ...
   </properties>

   <dependencies>
      <dependency>
         <groupId>log4j</groupId>
         <artifactId>log4j</artifactId>
         <version>1.2.12</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>junit</groupId>
         <artifactId>junit</artifactId>
         <version>4.8.1</version>
         <type>jar</type>
         <scope>test</scope>
      </dependency>
   </dependencies>

   <build>
   ...

El proyecto se actualizará automáticamente, y podremos ver las librerías importadas bajo las Maven Dependencies:

Nota
También es posible añadir dependencias utilizando el asistente de Eclipse, que permite ademas buscar por nombre y versión.

Las librerías descargadas se guardan en el directorio /home/expertojava/.m2 (repositorio local de Maven). Puedes explorar el directorio con el navegador de archivos para comprobar que los ficheros JAR se encuentran allí. En el repositorio existen más librerías porque las hemos descargado al montar la máquina virtual para que sea más rápida la construcción de los proyectos.

9. Desde la línea de comandos probamos a compilar con Maven el proyecto con el comando mvn install en el directorio del proyecto padre:

$ cd /home/expertojava/workspaces/proyint-expertojava/jbib
$ mvn install

Maven mostrará el resultado por la salida estándar, indicando el fichero JAR resultante y el directorio en el que se instala. Además de en el repositorio de Maven .m2, también lo deja en el directorio target del proyecto.

10. Añadimos todos los ficheros creados al repositorio Mercurial desde línea de comando o desde Eclipse. Desde línea de comando sería así:

$ cd /home/expertojava/workspaces/proyint-expertojava
$ hg add
$ hg commit -m "Añadido el proyecto padre jbib y el módulo jbib-modelo"

11. Por último, subimos el repositorio a Bitbucket. En Bitbucket creamos en la cuenta de estudiante el repositorio proyint-expertojava, y hacemos un push:

$ hg push https://bitbucket.org/login/proyint-expertojava

También creamos el fichero .hg/hgrc con la URL de push por defecto:

Fichero .hg/hgrc:

[paths]
default-push = ssh://bitbucket.org/login/proyint-expertojava

Primera iteración

Comentamos a continuación los pasos a seguir para desarrollar las clases de dominio del proyecto. Detallaremos una primera iteración en la que explicaremos cómo codificar una de las clases centrales (LibroDomain) y deberás hacer el ejercicio de desarrollar el resto.

Primera clase de dominio

Las clases de dominio representan las entidades que van a hacerse persistentes y con las que va a trabajar la aplicación. En las siguientes sesiones, cuando veamos JPA, veremos cómo se podrán definir la capa de persistencia de la aplicación directamente a partir de estas clases. Serán también objetos que podremos utilizar como Transfer Objects (TOs) dentro del sistema, realizando funciones de meros contenedores y viajando por las capas de la aplicación.

Para asegurarnos que todos nuestros objetos de dominio tienen una estructura común, definimos una clase abstracta, que será la clase padre de todas las clases de dominio. La hacemos serializable para asegurar que los objetos pueden transmitirse entre capas físicas de la aplicación (por ejemplo, entre la capa de presentación y la capa de lógica de negocio) y definimos los métodos equals() y hashCode() para obligar a que las clases hijas implementen y redefinan la igualdad. Estos métodos son muy útiles en el caso en el que los objetos se guarden y queramos buscarlos en colecciones.

package es.ua.jtech.jbib.model;


import java.io.Serializable;

/**
* Padre de todos los objetos de dominio del modelo. Los objetos del
* dominio están relacionados entre si y se hacen persistentes 
* utilizando entidades JPA.
* Pulsando F4 en Eclipse podemos ver la jerarquía. En la clase 
* definimos los métodos abstractos que deben implementar todas las 
* clases del dominio. Los objetos de dominio participan entre ellos 
* en relaciones uno-a-uno, uno-a-muchos y muchos-a-uno y están 
* relacionados mediante referencias. 
* Tendrán una clave única que permite identificarlos en la BD. 
* Una Direccion no es un objeto de dominio.
*/

public abstract class DomainObject implements Serializable {
    private static final long serialVersionUID = 1L;
    public abstract boolean equals(Object object);
    public abstract int hashCode();
}

Todas las entidades las vamos a definir dentro del paquete es.ua.jtech.jbib.model, y utilizaremos el sufijo Domain a modo de nomenclatura para indicar que un objeto de la aplicación es un objeto de dominio.

Cada objeto de dominio se compone de sus atributos, relaciones y de todos los getter/setter que encapsulan al objeto. Así pues, por ejemplo, la representación del Domain Object LibroDomain sería:

package es.ua.jtech.jbib.model;

import java.util.Date;
import java.util.HashSet;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class LibroDomain extends DomainObject {

    private static final long serialVersionUID = 1L;
    private Log logger = LogFactory.getLog(LibroDomain.class);

    private String isbn;
    private String titulo;
    private String autor;
    private Integer numPaginas;
    private Date fechaAlta;
    private Integer numDisponibles;

    private Set<EjemplarDomain> ejemplares = 
        new HashSet<EjemplarDomain>();
    private Set<ReservaDomain> reservas = 
        new HashSet<ReservaDomain>();

    public LibroDomain(String isbn) {
        super();
        this.isbn = isbn;
            logger.debug("Creada una instancia de " + 
                LibroDomain.class.getName());
    }

    public String getIsbn() {
         return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public String getTitulo() {
        return titulo;
    }

    public void setTitulo(String titulo) {
        this.titulo = titulo;
    }

    public String getAutor() {
        return autor;
    }

    public void setAutor(String autor) {
        this.autor = autor;
    }

    public Integer getNumPaginas() {
        return numPaginas;
    }

    public void setNumPaginas(Integer numPaginas) {
        this.numPaginas = numPaginas;
    }

    public Date getFechaAlta() {
        return fechaAlta;
    }

    public void setFechaAlta(Date fechaAlta) {
        this.fechaAlta = fechaAlta;
    }

    public Set<EjemplarDomain> getEjemplares() {
        return ejemplares;
    }

    public void setEjemplares(Set<EjemplarDomain> ejemplares) {
        this.ejemplares = ejemplares;
    }

    public Set<ReservaDomain> getReservas() {
        return reservas;
    }

    public void setReservas(Set<ReservaDomain> reservas) {
        this.reservas = reservas;
    }

    public Integer getNumDisponibles() {
        return numDisponibles;
    }

    public void setNumDisponibles(Integer numDisponibles) {
        this.numDisponibles = numDisponibles;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((isbn == null) ? 0 : isbn.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        LibroDomain other = (LibroDomain) obj;
        if (isbn == null) {
            if (other.isbn != null)
                    return false;
        } else if (!isbn.equals(other.isbn))
            return false;
        return true;
    }

}

Es interesante hacer notar la forma en que están implementados los métodos hashCode y equals. Son métodos muy importantes porque son los que se utilizan para buscar en las colecciones. Los podemos generar con Eclipse usando la opción Source > Generate hashCode() and equals()...

Para elegir los campos a usar en la comparación debemos tener en cuenta que sean claves naturales de los objetos de dominio, tal y como se explica en el esta nota de jBoss e Hibernate. También es importante que garanticemos que los campos siempre existen, utilizándolos también en el constructor. El constructor lo podemos generar con la opción de Eclipse Source > Generate Constructor using fields....

En la próxima sesión, cuando realizemos el mapeado al modelo relacional, a la base de datos, añadiremos un identificador único en cada clase para garantizar la identidad de cada objeto y tener más flexibilidad. Pero es importante que esta identidad será sólo para trabajar con la base de datos. Desde el punto de vista de un objeto de dominio, la identidad debe estar basada en claves naturales propias del objeto.

Eliminando errores

Definimos las clases vacías y los tipos enumerados necesarias para eliminar todos los errores y para completar el módulo (consultar el diagrama de clases UML):

  • Clase EjemplarDomain
  • Clase PrestamoHistoricoDomain
  • Clase abstracta UsuarioDomain y sus clases hijas ProfesorDomain y AlumnoDomain.
  • Clase ReservaDomain
  • Clase MultaDomain
  • Clase Direccion
  • Clase BibliotecarioDomain

Ficheros de recursos

Añadimos los ficheros de recursos necesarios para el funcionamiento de los logs.

Fichero src/main/resources/commons-logging.properties:

org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger

Fichero src/main/resources/log4j.properties:

# Ańade appender A1
log4j.rootLogger=DEBUG, A1
 
# A1 se redirige a la consola
log4j.appender.A1=org.apache.log4j.ConsoleAppender
# Coloca el nivel root del logger en INFO (muestra mensajes de INFO hacia arriba)
log4j.appender.A1.Threshold=INFO
 
# A1 utiliza PatternLayout
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=[%d{dd/MM/yyyy HH:mm:ss}] %p - %m %n

Podemos también añadir un fichero de configuración de logs distinto en el directorio de test, en el que cambiamos el nivel de los tests a DEBUG:

Fichero test/main/resources/log4j.properties:

# Ańade appender A1
log4j.rootLogger=DEBUG, A1
 
# A1 se redirige a la consola
log4j.appender.A1=org.apache.log4j.ConsoleAppender
# Coloca el nivel root del logger en DEBUG (muestra mensajes de DEBUG hacia arriba)
log4j.appender.A1.Threshold=DEBUG
 
# A1 utiliza PatternLayout
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=[%d{dd/MM/yyyy HH:mm:ss}] %p - %m %n

Primer test

Definimos el primer test con la clase es.ua.jtech.jbib.model.LibroDomainTest en el directorio src/test/java, con el que probamos el funcionamiento correcto de la igualdad en la clase LibroDomain:

[package es.ua.jtech.jbib.model;

import static org.junit.Assert.*;
import org.junit.Test;

public class LibroDomainTest {

   /**
    * Dos libros son iguales cuando tienen el mismo ISBN
    */
   @Test
   public void testEquals() {
      LibroDomain libro1 = new LibroDomain("123456789");
      LibroDomain libro2 = new LibroDomain("123456789");
      assertTrue(libro1.equals(libro2));
      libro2.setIsbn(null);
      assertFalse(libro1.equals(libro2));
   }

   /*
    * Prueba del funcionamiento de la pertenencia a 
    * la colección de ejemplares
    */
   @Test
   public void testContainsEjemplares() {
      LibroDomain libro1 = new LibroDomain("123456789");
      EjemplarDomain ejemplar1 = new EjemplarDomain(libro1, "A");
      EjemplarDomain ejemplar2 = new EjemplarDomain(libro1, "B");
      EjemplarDomain ejemplar3 = new EjemplarDomain(libro1, "C");
      libro1.getEjemplares().add(ejemplar1);
      libro1.getEjemplares().add(ejemplar2);
      // Comprueba igualdad de referencia
      assertTrue(libro1.getEjemplares().contains(ejemplar1));
      assertFalse(libro1.getEjemplares().contains(ejemplar3));
      // Comprueba igualdad de valor
      assertTrue(libro1.getEjemplares().contains(
            new EjemplarDomain(libro1, "A")));
   }
}

Ejectuamos el test con el botón derecho sobre la clase o el paquete: Run As > JUnit Test y debe aparecer en verde:

Construcción con Maven

Abrimos un terminal y ejecutamos los comandos Maven para construir el JAR que contiene el proyecto:

$ cd /home/expertojava/workspaces/proyint-expertojava
$ mvn clean
$ mvn install

El comando Maven clean borra todos los ficheros .class y .jar que puedan haber sido creados anteriormente. El comando install realiza secuencialmente las siguientes acciones:

  • Compila todos los módulos del proyecto principal
  • Ejecuta todos los tests
  • Si los tests son correctos, empaqueta el subproyecto bib-modelo en el fichero JAR jbib-modelo-0.0.1-SNAPSHOT.jar que deja en el directorio target del proyecto
  • Copia el fichero JAR en el repositorio local de Maven (directorio .m2) para dejarlo como una librería disponible para otros proyectos.

La última parte de la salida del comando debe ser esta:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running es.ua.jtech.jbib.model.LibroDomainTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.144 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

...

[INFO] Reactor Summary:
[INFO] 
[INFO] jbib ....................................... SUCCESS [0.369s]
[INFO] jbib-modelo ................................ SUCCESS [1.342s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.882s
[INFO] Finished at: Sat Oct 08 08:07:18 CEST 2012
[INFO] Final Memory: 4M/15M
[INFO] ------------------------------------------------------------------------

Todos los ficheros .class y JAR se guardan en el directorio target del proyecto. Puedes explorar el sistema de archivos para comprobarlo.

Esto lo podemos hacer también desde el asistente de Eclipse situándonos en el proyecto principal y pulsando con el botón derecho la opción Run As > Maven install. Puede ser que la salida de la consola sea ligeramente distinta, porque Eclipse usa una versión de Maven instalada en el propio entorno.

Para usar la misma versión que cuando la lanzamos desde línea de comandos, podemos seleccionar la opción Window > Preferences > Maven > Installations y añadir la ruta en la que se encuentra Maven en el sistema operativo: /opt/apache-maven-3.0.3.

Repositorio Bitbucket

Terminamos esta primera iteración haciendo un commit con los cambios y subiéndolos al repositorio Bitubucket.

Configura el repositorio para que el profesor del módulo (usuario: domingogallardo) y el administrador de java_ua (usuario: java_ua) tengan permiso de lectura.

Enumeraciones

En cuanto a las enumeraciones, Java (desde su versión 5.0) permite su creación mediante la clase java.lang.Enum. En nuestro caso, por ejemplo, la enumeración de EstadoUsuario quedaría del siguiente modo:

package es.ua.jtech.jbib.model;

public enum EstadoUsuario {
   ACTIVO, MOROSO, MULTADO
}

Definimos al menos las enumeraciones (consulta el diagrama de clases):

  • EstadoLibro
  • EstadoHistorico
  • EstadoUsuario
  • Localizacion

Implementar y completar las clases de entidad

Para implementar nuestras clases, tendremos que codificar todas las clases y relaciones expuestas en el Modelo de Clases Conceptual.

Hay que tener especial cuidado con las relaciones de herencia. Tenemos la clase UsuarioDomain y las clases hijas ProfesorDomain y AlumnoDomain. La clase padre es abstracta.

Algunas características comunes a todas las clases de entidad:

  • Definimos el constructor y los métodos hashCode y equals usando campos que constituyan claves naturales.
  • Las relaciones X-a-muchos las definimos del tipo Set. De esta forma nos aseguramos que no existen objetos duplicados en las relaciones. La identidad en un conjunto se define con el método equals de sus elementos (definido anteriormente).

Definimos también la clase Direccion que será una clase no entidad y se utilizará en la entidad UsuarioDomain.

Gestión de las Excepciones

Todas las aplicaciones empresariales definen una política de gestión de las excepciones de aplicación.

En nuestro caso, conforme crezca el proyecto, iremos creando nuevas excepciones. Como punto de partida, y como buena norma de programación, vamos a definir una excepción genérica de tipo unchecked (BibliotecaException), que será la excepción padre de todas las excepciones de aplicación de la biblioteca. El hecho de que la excepción se unchecked remarca el carácter de que estamos definiendo excepciones relacionadas con el mal uso del API. En general, un método debe realizar su funcionalidad y terminar correctamente cuando todo ha funcionado bien. Se lanzará una excepción si algo falla. Por ejemplo, cuando definamos un método prestar(libro,usuario) lanzaremos excepciones cuando no se cumplan las condiciones que hacen que el libro pueda ser prestado al usuario. Al lanzar excepciones no chequeadas permitimos que el programador chequee las condiciones antes de llamar al método y no tenga que obligatoriamente capturar una excepción que sabemos que no se va a producir.

Definimos las excepciones en el paquete global es.ua.jtech.jbib

package es.ua.jtech.jbib;

public class BibliotecaException extends RuntimeException {
   private static final long serialVersionUID = 1L;
   
   public BibliotecaException() {
      super();
   }
   
   public BibliotecaException(String message) {
      super(message);
   }
   
   public BibliotecaException(String message, Throwable cause) {
      super(message, cause);
   }
   
}

Podemos observar como, al sobrecargar el constructor con los parámetros {String, Throwable}, nuestra excepción permitirá su uso como Nested Exception.

Implementación de las Reglas de Negocio

Es común agrupar las reglas de negocio de una aplicación en una o más clases (dependiendo de los diferentes subsistemas de la aplicación), para evitar que estén dispersas por la aplicación y acopladas a un gran número de clases.

En nuestro caso, vamos a crear un Singleton, al que llamaremos BibliotecaBR (BR = Business Rules). En principio, los valores estarán escritos directamente sobre la clase, pero en un futuro podríamos querer leer los valores de las reglas de negocio de un fichero de configuración).

El código inicial de nuestras reglas de negocio será el siguiente:

package es.ua.jtech.jbib;

// Imports

/**
 * Reglas de Negocio de la Biblioteca BR = Business Rules
 * 
 * Lo implementamos como un singleton por si algun dia queremos leer las
 * constantes desde un fichero de configuración, lo podemos hacer desde el
 * constructor del singleton
 */
public class BibliotecaBR {
  private int numDiasPrestamoAlumno = 7;
  private int numDiasPrestamoProfesor = 30;
  private int cupoOperacionesAlumno = 5;
  private int cupoOperacionesProfesor = 8;

  private static Log logger = LogFactory.getLog(BibliotecaBR.class);

  private static BibliotecaBR me = new BibliotecaBR();

  private BibliotecaBR() {
    logger.debug("Creada instancia de " + BibliotecaBR.class);
  }

  public static BibliotecaBR getInstance() {
    return me;
  }

  /**
   * Calcula el numero de dias de plazo que tienen un usuario para
   * devolver un prestamo (Alumno = 7 , Profesor = 30)
   * 
   * @param tipo
   *    objeto UsuarioDomain
   * @return numero de dias del prestamo en función de la clase de
   *   UsuarioDomain: AlumnoDomain o ProfesorDomain
   * @throws BibliotecaException
   *     el usuario no es de la clase AlumnoDomain ni ProfesorDomain
   */
  public int calculaNumDiasPrestamo(UsuarioDomain usuario)
      throws BibliotecaException {
    if (usuario instanceof AlumnoDomain) {
      return numDiasPrestamoAlumno;
    } else if (usuario instanceof ProfesorDomain) {
      return numDiasPrestamoProfesor;
    } else {
      String msg = "Solo los alumnos y profesores pueden " + 
         "realizar prestamos";
      logger.error(msg);
      throw new BibliotecaException(msg);
    }
  }

  /**
   * Valida que el número de operaciones realizadas por un determinado
   * tipo de usuario se inferior o igual al cupo definido
   * 
   * @param usuario
   *    objeto UsuarioDomain
   * @param numOp
   *    número de operación que ya tiene realizadas
   * @throws BibliotecaException
   *     el cupo de operacion esta lleno
   * @throws BibliotecaException
   *     el tipo del usuario no es el esperado
   */
  public void compruebaCupoOperaciones(UsuarioDomain usuario, int numOp)
      throws BibliotecaException {
    String msg;
    if (!(usuario instanceof AlumnoDomain)
        && !(usuario instanceof ProfesorDomain)) {
      msg = "Solo los alumnos y profesores pueden tener libros prestados";
      logger.error(msg);
      throw new BibliotecaException(msg);
    }
    if ((usuario instanceof AlumnoDomain && numOp > 
          cupoOperacionesAlumno)
      || (usuario instanceof ProfesorDomain && numOp > 
          cupoOperacionesProfesor)) {
      msg = "El cupo de operaciones posibles esta lleno";
      logger.error(msg);
      throw new BibliotecaException(msg);
    }
  }

  /**
   * Devuelve el número máximo de operaciones (préstamos y reservas) que
   * puede realizar un determinado tipo de usuario
     * 
     * @param tipo
     *      objeto UsuarioDomain
     * @return número máximo de operaciones del tipo de usuario
     * @throws BibliotecaException
     *       el tipo del usuario no es el esperado
     */
    public int cupoOperaciones(UsuarioDomain usuario)
            throws BibliotecaException {
        if (usuario instanceof AlumnoDomain)
            return cupoOperacionesAlumno;
        else if (usuario instanceof ProfesorDomain)
            return cupoOperacionesProfesor;
        else {
            String msg = "Solo los alumnos y profesores pueden tener" + 
                 " libros prestados";
            logger.error(msg);
            throw new BibliotecaException(msg);
        }
    }
}

Podemos observar que vamos a tener un método por cada regla de negocio, y que estos métodos lanzarán excepciones de aplicación en el caso de un comportamiento anómalo.

Tests

Vamos a definir dos tipos de tests, unos relacionados con las clases del modelo y otros con las reglas de negocio.

Los tests se deben colocar dentro de la carpeta test, en el mismo paquete que la clase a probar.

Ejemplo de tests de reglas de negocio:

package es.ua.jtech.jbib;

import static org.junit.Assert.*;

import org.junit.Test;

import es.ua.jtech.jbib.model.AlumnoDomain;
import es.ua.jtech.jbib.model.ProfesorDomain;

/**
 * Pruebas jUnit sobre las reglas de negocio de la biblioteca
 */
public class BibliotecaBRTest {

      @Test
      public void testCalculaNumDiasPrestamoProfesor() {
         int diasProfesor = BibliotecaBR.getInstance()
            .calculaNumDiasPrestamo(
                  new ProfesorDomain("pedro.garcia", "1234"));
            assertEquals(30, diasProfesor);
      }

      @Test
      public void testCalculaNumDiasPrestamoAlumno() {
         // TODO
      }

      @Test
      public void testCupoOperacionesProfesor() {
         // TODO
      }

      @Test
      public void testCupoOperacionesAlumno() {
         // TODO
      }

      @Test
      public void testCompruebaCupoOperacionesProfesorCorrecto() {
         try {
               ProfesorDomain profesor = 
                     new ProfesorDomain("pedro.garcia", "1234");
               BibliotecaBR.getInstance()
                     .compruebaCupoOperaciones(profesor, 8);
               BibliotecaBR.getInstance()
                     .compruebaCupoOperaciones(profesor, 1);
         } catch (BibliotecaException e) {
               fail("No debería fallar - el cupo de operaciones del" + 
                    " PROFESOR es correcto");
         }
      }

      @Test(expected = BibliotecaException.class)
      public void testCompruebaCupoOperacionesProfesorIncorrecto()
                  throws BibliotecaException {
            BibliotecaBR.getInstance()
                .compruebaCupoOperaciones(
                    new ProfesorDomain("pedro.garcia", "1234"), 9);
      }

      @Test
      public void testCompruebaCupoOperacionesAlumnoCorrecto() {
         // TODO
      }

      @Test(expected = BibliotecaException.class)
      public void testCompruebaCupoOperacionesAlumnoIncorrecto()
                  throws BibliotecaException {
         // TODO
      }
}

En los tests de las clases del modelo debemos de comprobar:

  • Relaciones de igualdad.
  • Actualizaciones de las relaciones entre entidades. Por ejemplo, podemos crear una operación, un libro y un usuario, asociar la operación al libro y al usuario y comprobar que los métodos get devuelven correctamente las relaciones.

Resumen

En esta sesión vamos a preparar la base para el resto de sesiones. Por ellos, debemos crear un proyecto padre Maven, al que llamaremos jbib, que contendrá al módulo jbib-modelo con el modelo de dominio de la aplicación:

  1. modelo de objetos dentro del paquete es.ua.jtech.jbib.model, implementando cada entidad con los atributos representados en el diagrama UML, sus relaciones, y teniendo en cuenta la relación de herencia con la clase DomainObject y los constructores necesarios y los métodos de acceso.
  2. clase BibliotecaBR con las reglas de negocio, implementada como un singleton, la cual debe pasar las pruebas JUnit aportadas. Para implementar esta clase, es necesaria la clase BibliotecaException.
  3. tests de las clases de dominio y de las reglas de negocio que realicen algunas comprobaciones de los métodos equals y de las actualizaciones de las relaciones.

Las siguientes imágenes muestran todos los archivos que debe contener el proyecto Eclipse:

Debemos utilizar la etiqueta entrega-proyint-modelo El plazo final de entrega será el jueves 15 de noviembre.