Esta fase incluye varias iteraciones sobre el sistema antes de ser entregado. El Plan de Entrega está compuesto por iteraciones de no más de tres semanas. En la primera iteración se puede intentar establecer una arquitectura del sistema que pueda ser utilizada durante el resto del proyecto. Esto se logra escogiendo las historias que fuercen la creación de esta arquitectura, sin embargo, se puede variar con el fin de maximizar el valor de negocio. Al final de la última iteración el sistema estará listo para entrar en producción.
Los elementos que deben tomarse en cuenta durante la elaboración del Plan de la Iteración son: historias de usuario no abordadas, velocidad del proyecto, pruebas de aceptación no superadas en la iteración anterior y tareas no terminadas en la iteración anterior.
En esta iteración se contempla la realización del prototipo que además de servir para evaluar la tecnología también establecerá la arquitectura base del sistema.
Las historias de usuario a abordar se pueden ver en la Tabla 8.1, “Historias primera iteración”.
Tabla 8.1. Historias primera iteración
Funcionalidad común | 2 |
Auditoría | 2 |
Creación de contactos | 1 |
Visualización, modificación, eliminación y búsqueda de contactos | 2 |
ESTIMACIÓN INICIAL | 7 |
REAL | 9 |
Para comenzar el desarrollo es necesario configurar una estructura de directorios adecuada que facilite las tareas. Utilizando Maven esto no supone un gran problema ya que para cada proyecto se tiene una estructura bien definida, con lo que nos debemos centrar en la correspondencia entre nuestro proyecto y sus módulos en proyectos Maven, teniendo en cuenta que una de las principales filosofías de Maven es la de que cada proyecto genere un único artefacto, que es así como llama a los ficheros jar, war,...
Con el objeto de fomentar la reusabilidad el código se separará en módulos basándose en funcionalidad, y dentro de cada módulo se creará un subproyecto para la capa modelo y otro para el interfaz web, que agrupará las capas controlador y vista.
Para auditar los cambios que se realizan a los datos del sistema es necesario saber una serie de datos según el tipo de operación que se realice:
creación
Cuando un nuevo objeto es creado, se debe saber quién lo ha creado y cuándo.
modificación
Cuando se modifica un objeto es necesario saber quién lo ha modificado y cuándo, y además guardar la versión anterior por si fuera necesario restaurarlo.
borrado
Al borrar un objeto se necesita saber quién lo ha borrado y cuándo, y también se debe guardar por si se necesitara restaurar.
De todo lo anterior se deduce que una buena forma de permitir la auditoría es guardar la fecha de creación de un objeto cuando se crea, la de eliminación cuando se borra y el usuario que lo ha creado y borrado respectivamente. A partir de la fecha de borrado o usuario que lo ha borrado, se podrá determinar si un objeto está eliminado o no.
El problema surge con la modificación de objetos, con lo que la aproximación anterior se completa de la siguiente manera: cuando un objeto es modificado se marcará como borrado con fecha de eliminación y autor, y se creará un nuevo objeto que tendrá las mismas propiedades que el objeto modificado, con fecha de creación y autor.
Con esta aproximación lo que se consideraría identificador del
objeto pasaría a representar una única versión del objeto, siendo
necesaria otra propiedad que nos permita relacionar entre sí todas las
versiones del objeto, lo que se llamará código
(code
). Todas las versiones de un mismo objeto
tendrán el mismo código pero cada una con su identificador único, y se
podrá seguir la evolución desde su creación hasta su borrado mediante
este código y las fechas de creación y eliminación.
Al tratar con información temporal deben tratarse dos dimensiones [Fowler]:
momento en el que la información cambia (transaction time o record time)
momento en el que la información es efectiva (valid time o actual time)
En el caso de la auditoría la definición aplicable es la primera, y se utilizará el nombre transaction time para referirse al rango de fechas que comienza en el momento en el que un dato es introducido en el sistema y finaliza cuando este dato deja de ser relevante y es borrado. Este rango de fechas será limitado cuando el objeto esté eliminado o esa versión haya sido modificada, y tendrá su final en el infinito mientras el objeto siga siendo válido.
En el módulo common se irán añadiendo las funcionalidades que se prevé serán utilizadas por más de un módulo para facilitar su reusabilidad.
Dado que la url en la que se encuentra el sitio web es http://oness.sourceforge.net (o también http://oness.sf.net), el paquete base de Java será net.sf.oness, escogiéndose la segunda url por ser más breve y más común entre otros proyectos alojados en Sourceforge utilizar sf en lugar de sourceforge para la nomenclatura de los paquetes. A partir de este paquete base se crearán uno por cada módulo, en este caso, el paquete base para la funcionalidad común será net.sf.oness.common.
En una primera aproximación se diferencian claramente tres tipos de funcionalidades comunes según a la capa de la aplicación que afectarán:
all
Funcionalidad que afectará a la totalidad del sistema.
model
Funcionalidad que afectará tan sólo a la capa modelo del sistema.
webapp
Funcionalidad que afectará sólo a las capas vista y controlador de las aplicaciones web.
Cada submódulo de los anteriores se corresponderá con un paquete, net.sf.oness.common.all, net.sf.oness.common.model y net.sf.oness.common.webapp respectivamente.
Es necesario extraer ciertas tareas repetitivas que serán
comunes a gran parte de las clases Java. Se diferencian dos objetivos
claros: facilitar el depurado (logging y
tracing) e implementación de métodos definidos en
el contrato Java (toString
,
equals
, hashCode
y
clone
).
Este es uno de los campos típicos donde se aplica la Programación Orientada a Aspectos [Laddad03], que nos proporciona los mecanismos para implementar esta funcionalidad de una manera sencilla. El objetivo es mostrar las llamadas a los métodos con sus parámetros, la finalización de estas llamadas y las excepciones que ocurren durante la ejecución.
La tecnología que mejor se adapta es AspectJ, desarrollando un aspecto que se aplique a todas las clases y que pueda deshabilitarse tras la fase de desarrollo de una forma sencilla. También debe permitirse el filtrado de los mensajes de logging a nivel de clase, paquete o tipo de mensaje (entrada de método, salida de método o lanzamiento de excepciones). Para ello este aspecto deberá utilizar un framework como Apache Commons Logging que proporciona una capa de abstracción sobre otros sistemas de logging que pueden ser configurados declarativamente, como Log4J, que será el usado normalmente por su sencillez de configuración y potencia de uso
El aspecto desarrollado LoggingAspect
es totalmente genérico y aplicable a cualquier sistema. Algunas
consideraciones que ha sido necesario tener en cuenta son:
precedencia
el aspecto de logging debe tener mayor precedencia que otros aspectos para que los mensajes de entrada en un método se ejecuten antes que cualquier otro aspecto que se ejecute antes de la ejecución de un método, y los mensajes de salida después de los otros aspectos que se ejecuten después de la ejecución de un método.
evitar la recursividad infinita
no se debe hacer logging del propio aspecto así como
tampoco de las ejecuciones que tienen lugar dentro de los
métodos llamados por él, principalmente el método
toString
que puede ser llamado
implícitamente por la máquina virtual al hacer operaciones con
strings y objetos.
La implementación de estos métodos aunque no es siempre
imprescindible suele ser deseable, y supone una tarea tediosa que
hay que realizar en cada clase que se implementa. Para facilitar o
incluso evitar esta tarea se crea una clase que hará de raíz de la
jerarquía de clases siempre que sea posible, implementando de manera
genérica los métodos toString
,
equals
, hashCode
y
clone
haciendo uso extensivo de los
metadatos que proporciona Java a través del API
reflection. Para facilitar este trabajo ya
existen proyectos de la ASF, principalmente los proyectos
commons-lang y
commons-beanutils.
toString
,
equals
y
hashCode
son implementados utilizando
commons-lang de manera que se utilizan
todas sus propiedades para mostrar de manera homogénea el estado
del objeto, hacer comparaciones entre objetos o calcular su
código hash, respectivamente.
clone
es implementado usando
commons-beanutils, que proporciona una
manera sencilla de clonar superficialmente un objeto copiando
todas sus propiedades.
Como soporte a la auditoría se creará el paquete
auditing, creándose en primer lugar un interfaz
Auditable
que deben implementar
aquellos objetos que pretendan ser auditados. Tal y como se mencionó
en Sección 8.1.2, “Auditoría”, aquellos objetos auditables deben
tener:
identificador (id
)
código (code
)
fechas de transacción
(transactionTime
)
creado-por (createdBy
)
borrado-por (deletedBy
)
Junto con este interfaz se crea una implementación por defecto
AbstractAuditableObject
de la que pueden
heredar otras clases.
Se seguirá en patrón Business Object
[AlurCrupiMalks03] para reflejar el modelo
conceptual del dominio. Estos objetos del dominio serán persistidos
mediante Hibernate, por lo que es necesario afinar los métodos
definidos en BaseObject
, lo que se hará en la
clase AbstractBusinessObject
, debido a que
Hibernate puede utilizar una caché perezosa para cargar las
colecciones, con lo que los métodos
toString
, equals
y
hashCode
definidos en la superclase se
sobrecargarán para ignorar colecciones. El método
clone
debe ser también sobrecargado para
evitar que una colección sea referenciada por dos objetos distintos,
restricción que impone Hibernate y que se soluciona creando una
nueva colección pero con los mismos objetos.
Para facilitar la implementación de los tests, principalmente en cuestiones relacionadas con la integración del framework Spring, se han realizado unas clases utilidad.
SpringApplicationContext
Una clase que proporciona métodos estáticos para acceder al contexto de aplicación de Spring desde los tests.
SpringDatabaseExport
Una utilidad para exportar una base de datos a partir de una fuente de datos definida en el contexto de aplicación de Spring.
SpringDatabaseTestCase
Una clase base para los tests que integra DBUnit con Spring, permitiendo que DBUnit utilice una fuente de datos definida en el contexto de aplicación de Spring.
Para acceder a los datos se utilizará el patrón DAO (Data Access Object [AlurCrupiMalks03]) que ocultará la implementación utilizada por si se diera la necesidad de cambiarla en el futuro. Dado que la persistencia será gestionada en principio con Hibernate, que permite el acceso a gran cantidad de metadatos, se intentará realizar un DAO genérico que proporcione los servicios de persistencia para cualquier clase sin necesidad de escribir más líneas de código.
Se usará también el soporte que proporciona Spring para la
integración con Hibernate, concretamente la clase
HibernateDaoSupport
. Se implementará un DAO
genérico, HibernateDao
, con las siguientes
operaciones típicas:
findById
obtener un objeto a
partir de su identificador
create
crear un objeto
persistente
update
actualizar un objeto
persistente
delete
no se implementará por no
ser necesario ya que los datos no se borrarán nunca de la base
de datos por cuestiones de auditabilidad.
Dado que el DAO será genérico es necesario pasarle la clase a la que proporcionará persistencia en el momento de su creación.
Para ocultar detalles de implementación se crearán dos interfaces:
FinderDao
Este interfaz contendrá aquellos métodos de sólo lectura
que permitirán realizar búsquedas en las base de datos,
comenzando con find
, un método que
implementa la búsqueda por criterio, es decir a partir de un
objeto con algunas propiedades encuentra todos aquellos con
propiedades coincidentes. Utilizando el patrón Value
List Handler [AlurCrupiMalks03] no
devolverá todos los objetos que cumplan el criterio sino sólo un
número determinado, en forma de
PaginatedList
, que representa un rango de
objetos dentro de una lista.
Dao
Interfaz de conveniencia que extiende
AuditingDao
(ver Sección 8.1.3.2.5, “Auditoría”) y
FinderDao
.
La configuración dentro del contexto de aplicación de Spring
se puede ver en el fichero
applicationContext-oness-common-model.xml
,
donde está centralizada para todo el sistema.
Ejemplo 8.1. Configuración general de Hibernate
<!-- Session Factory --> <bean id="sessionFactory" class="org.springframework.orm.hibernate.LocalSessionFactoryBean"> <property name="dataSource"> <ref bean="dataSource" /> </property> <property name="mappingResources"> <ref bean="mappingResources" /> </property> <property name="hibernateProperties"> <ref bean="hibernateProperties" /> </property> </bean> <!-- Transaction manager for a single Hibernate SessionFactory (alternative to JTA) --> <bean id="transactionManager" class="org.springframework.orm.hibernate.HibernateTransactionManager"> <property name="sessionFactory"> <ref local="sessionFactory" /> </property> </bean>
En este fichero se establece la configuración general de
Hibernate aplicable a todos los módulos, el
sessionFactory
y el gestor de transacciones. Se
delega en los contextos de aplicación de cada módulo para la
configuración concreta, así se centraliza y se evita la replicación
de estos parámetros. En concreto se espera que cada módulo defina un
bean llamado mappingResources
que por ejemplo
podría ser algo como esto:
Ejemplo 8.2. Configuración del fichero de mapeo de Hibernate de Party
<bean id="mappingResources" class="java.util.ArrayList"> <constructor-arg> <list> <value>net/sf/oness/party/model/party/bo/Party.hbm.xml</value> </list> </constructor-arg> </bean>
También se evita la definición de los beans
dataSource
e
hibernateProperties
para permitir utilizar
indistintamente un dataSource local usando por
ejemplo commons-DBCP o uno obtenido mediante JNDI, útil en
servidores de aplicaciones J2EE. Tan sólo es necesario definir en
tiempo de ejecución cuál de los siguientes ficheros se añaden al
contexto de aplicación:
applicationContext-oness-common-model-dataSource-dbcp.xml
Útil para la ejecución de los tests fuera de un servidor
J2EE. Las propiedades concretas, definidas como
${...}
, no están en este fichero, sino en
dataSource.properties
que se obtiene del
classpath, de esta forma no es necesario
modificarlo, sino que simplemente se puede añadir otro fichero
con el mismo nombre en el classpath antes
que él y tendrá mayor precedencia. En caso de que alguna de las
propiedades exista como propiedades del sistema también tendrán
mayor precedencia, lo que es de gran utilidad a la hora de
ejecutar los tests con Maven, ya que sin necesidad de cambiar o
crear ningún fichero se podrán ejecutar los tests en cualquier
sistema gestor de bases de datos en cualquier máquina.
Ejemplo 8.3. Configuración de la fuente de datos DBCP
<!-- Get datasource properties from file --> <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="location"> <value>classpath:dataSource.properties</value> </property> <!-- Override properties in file with system properties --> <property name="systemPropertiesModeName"> <value>SYSTEM_PROPERTIES_MODE_OVERRIDE</value> </property> </bean> <!-- DBCP Basic datasource --> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName"> <value>${dataSource.driverClassName}</value> </property> <property name="url"> <value>${dataSource.url}</value> </property> <property name="username"> <value>${dataSource.username}</value> </property> <property name="password"> <value>${dataSource.password}</value> </property> </bean> <!-- Hibernate properties --> <bean id="hibernateProperties" class="net.sf.oness.common.model.dao.hibernate.HibernateProperties"> <constructor-arg> <props> <prop key="hibernate.dialect">${hibernate.dialect}</prop> <prop key="hibernate.show_sql">${hibernate.show_sql}</prop> <prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop> </props> </constructor-arg> </bean>
dataSource.properties
utilizadas
normalmente:
applicationContext-oness-common-model-dataSource-jndi.xml
Útil para la ejecución en servidores J2EE como Tomcat. Permite ejecutar la aplicación sin necesidad de modificar el fichero war distribuido, ya que la configuración de los parámetros necesarios se realiza en el contenedor.
Ejemplo 8.5. Configuración de la fuente de datos JNDI
<!-- JNDI DataSource for J2EE environments --> <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"> <value>java:comp/env/jdbc/oness</value> </property> </bean> <!-- Hibernate Dialect --> <bean id="hibernateDialect" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"> <value>java:comp/env/net.sf.oness.common.model.hibernateDialect</value> </property> </bean> <!-- Hibernate properties --> <bean id="hibernateProperties" class="net.sf.oness.common.model.dao.hibernate.HibernateProperties"> <constructor-arg> <props> <prop key="hibernate.show_sql">false</prop> <prop key="hibernate.hbm2ddl.auto">create</prop> </props> </constructor-arg> <property name="hibernateDialect"><ref local='hibernateDialect'/></property> </bean>
La configuración concreta para el servidor Tomcat, utilizando el módulo party como ejemplo, se especifica a continuación. Tan sólo algunas propiedades (en negrita) necesitan ser modificadas para utilizar otro sistema gestor de base de datos u otra máquina como servidor.
Ejemplo 8.6. Configuración de la aplicación web party-webapp en Tomcat utilizando JNDI
<!-- To setup this context copy this file to the specified directory and change it to match your needs. You may rename it if you want. o Tomcat 4: TOMCAT_HOME/webapps o Tomcat 5: TOMCAT_HOME/conf/Catalina/localhost --> <Context path="/oness-party-webapp" docBase="${catalina.home}/webapps/oness-party-webapp.war" debug="99" reloadable="true"> <!-- To use a global jndi datasource uncomment the <ResourceLink> tag and move the other entries to your server.xml under <GlobalNamingResources> --> <!-- <ResourceLink name="jdbc/oness" global="jdbc/oness" type="javax.sql.DataSource"/> --> <Environment description="Hibernate dialect" name="net.sf.oness.common.model.hibernateDialect" value="net.sf.hibernate.dialect.MySQLDialect" type="java.lang.String"/> <Resource name="jdbc/oness" auth="Container" type="javax.sql.DataSource"/> <ResourceParams name="jdbc/oness"> <parameter> <name>factory</name> <value>org.apache.commons.dbcp.BasicDataSourceFactory</value> </parameter> <!-- Maximum number of dB connections in pool. Make sure you configure your mysqld max_connections large enough to handle all of your db connections. Set to 0 for no limit. --> <parameter> <name>maxActive</name> <value>100</value> </parameter> <!-- Maximum number of idle dB connections to retain in pool. Set to 0 for no limit. --> <parameter> <name>maxIdle</name> <value>30</value> </parameter> <!-- Maximum time to wait for a dB connection to become available in ms, in this example 10 seconds. An Exception is thrown if this timeout is exceeded. Set to -1 to wait indefinitely. --> <parameter> <name>maxWait</name> <value>10000</value> </parameter> <!-- MySQL dB username and password for dB connections --> <parameter> <name>username</name> <value></value> </parameter> <parameter> <name>password</name> <value></value> </parameter> <!-- Class name for JDBC driver --> <parameter> <name>driverClassName</name> <value>com.mysql.jdbc.Driver</value> </parameter> <!-- Autocommit setting. This setting is required to make Hibernate work. Or you can remove calls to commit(). --> <parameter> <name>defaultAutoCommit</name> <value>false</value> </parameter> <!-- The JDBC connection url for connecting to your MySQL dB. The autoReconnect=true argument to the url makes sure that the mm.mysql JDBC Driver will automatically reconnect if mysqld closed the connection. mysqld by default closes idle connections after 8 hours. --> <parameter> <name>url</name> <value>jdbc:mysql://localhost/test</value> </parameter> <!-- Recover abandoned connections --> <parameter> <name>removeAbandoned</name> <value>true</value> </parameter> <!-- Set the number of seconds a dB connection has been idle before it is considered abandoned. --> <parameter> <name>removeAbandonedTimeout</name> <value>60</value> </parameter> <!-- Log a stack trace of the code which abandoned the dB connection resources. --> <parameter> <name>logAbandoned</name> <value>true</value> </parameter> </ResourceParams> </Context>
En este apartado se implementará lo definido en Sección 8.1.2, “Auditoría”, creando un aspecto que intercepte las llamadas a los DAOs de la siguiente manera:
create
Antes de crear el objeto persistente se establecería el inicio de la fecha de transacción al momento actual y el final a infinito.
delete
Se sustituye la llamada al método
delete
del DAO por una llamada a
update
tras poner el final de la fecha
de transacción al momento actual.
update
Se sustituye la llamada al método
update
del DAO por dos llamadas, una a
update
para marcar como borrado el
objeto anterior y otra a create
para
crear la nueva versión.
En principio este aspecto se ha implementado con AspectJ, pero posteriormente se ha implementado mediante el soporte AOP de Spring dado que la primera opción requería utilizar el compilador de AspectJ y la segunda permite una integración más fácil dado que no modifica el bytecode java, sino que utiliza proxies dinámicos.
AuditableDaoAdvisor
Este es el aspecto que interceptará las llamadas a los
DAOs y las delegará a
AuditingDaoHelper
.
AuditingDaoHelper
En esta clase se implementan las reglas mencionadas anteriormente que se ejecutarán cada vez que se intercepte una llamada.
Para dar soporte se añaden los siguientes interfaces:
AuditableDao
Interfaz con los métodos necesarios que debe implementar
un DAO para permitir la auditoría
(findById
,
create
y
update
).
AuditingDao
Este interfaz extiende AuditableDao
y añade el método delete
.
Para facilitar la utilización de fechas y rangos de fechas se crean las clases:
Date
Encapsula la clase Calendar
de Java
truncando su precisión normalmente al segundo ya que no es
necesario más ni es soportada por muchas bases de datos, y añade
nuevas funcionalidades. Se definen dos constantes
MIN_VALUE
y MAX_VALUE
que
representan respectivamente el infinito negativo y
positivo.
DateRange
Está formada por un Date
inicial y
uno final y añade métodos para trabajar con rangos de fechas
(inclusión, duración,...).
Para poder persistir estas clases con Hibernate como componentes y poder reutilizarlas es necesario implementar unos interfaces:
DateType
Persiste Date
implementando
UserType
.
DateRangeType
Persiste DateRange
implementando
CompositeUserType
. Principalmente se
transforman las constantes MIN_VALUE
y
MAX_VALUE
a valores null
en SQL.
La configuración del aspecto se realiza dentro del contexto de
aplicación de Spring en el fichero
applicationContext-oness-common-model.xml
para
que esté disponible para todos los módulos del sistema.
Ejemplo 8.7. Configuración del aspecto de auditoría en Spring
<!-- AuditingDao AOP --> <bean id="autoProxyCreator" class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"> </bean> <bean id="auditingDaoAdvisor" class="net.sf.oness.common.model.dao.AuditableDaoAdvisor"> </bean>
Se utiliza la funcionalidad de creación automática de proxies
de Spring para que el aspecto se aplique automáticamente a todos
aquellos beans definidos en el contexto de
aplicación que cumplan el contrato establecido por el método
matches
del aspecto.
Detro de este paquete se puede encontrar una implementación
del interfaz List
del api
collections de Java que proporciona una manera
de tratar listas paginadas, aquellas en las que normalmente sólo se
accede a una pequeña porción, tal y como se define en el patrón
ValueListHandler [AlurCrupiMalks03].
También se incluye DatabasePopulator
,
una clase que configurada desde el contexto de aplicación de Spring
permite importar datos de un fichero de DBUnit tanto en formato xml
como xls (Excel) en la base de datos. Se utilizará para insertar los
datos de ejemplo para las demostraciones.
Para no tener que implementar una acción de Struts para cada
acción de la vista se utilizarán
DispatchActions
que permitirán utilizar una
única clase para varias urls de peticiones, agrupando también la
funcionalidad relacionada. Para facilitar la utilización se creará una
acción AutoDispatchAction
que basándose en la
url de la petición escogerá automáticamente el método que debe ser
llamado, por ejemplo urls del tipo create* o update* provocarán
llamadas a los métodos create
o
update
respectivamente.
Para integrar Spring con Struts y poder acceder al contexto de
aplicación de donde se obtendrá la correspondiente fachada de la capa
modelo se añade también la clase
SpringActionSupport
que permite obtener de
manera sencilla cualquier objeto del contexto de aplicación de
Spring.
Los tests de unidad deben comprobar porciones de código lo más
pequeñas posible y de forma aislada, por lo que será necesario
utilizar mock objects que sustituyan la fachada
de la capa modelo para aislarla de esta forma de la capa controlador.
Se utilizará con este fin el framework JMock, que
proporciona una solución basada en mocks
dinámicos mucho más eficiente en el desarrollo que soluciones
estáticas que requieren la generación de código fuente. Por otro lado
también es necesario probar las acciones de Struts aisladamente sin
necesidad de ejecutarlas en un contenedor, para lo que se ajusta el
proyecto StrutsTestCase, una extensión de JUnit. Para poder integrar
ambas aproximaciones, JMock y StrutsTestCase, es necesario introducir
una nueva clase JMockStrutsTestCase
de la que
extenderán los tests concretos.
Posteriormente este módulo ha sido movido a common-webapp-controller al crear los nuevos módulos common-webapp-view y common-webapp-taglib en la segunda iteración.
Comenzamos con los objetos del dominio que surgen en las historias relativas a los contactos y se realiza el diagrama UML que se puede ver en la Figura 8.1, “Creación, visualización, modificación, eliminación y búsqueda de contactos” para el diseño de la capa modelo.
Como se puede ver se tratan los conceptos de
party: representa un contacto
person: aquellos contactos que son personas
title: título (Sr., Sra.)
firstName: nombre
lastName: apellidos
organization: aquellos contactos que son organizaciones
officialName: nombre oficial
Como interfaz de la capa modelo hacia capas superiores dentro de la arquitectura MVC se crea la fachada (façade) o servicio PartyFacade para ocultar los detalles de implementación de la capa modelo, desempeñando el papel de los patrones SessionFacade y BusinessDelegate [AlurCrupiMalks03].
Para gestionar la persistencia de estos objetos se etiquetan los ficheros fuente con atributos XDoclet que serán procesados mediante el plugin XDoclet de Maven para generar los ficheros de configuración de Hibernate que especifican para cada clase cómo se mapearán esos objetos en el sistema gestor de bases de datos.
Por ejemplo para la clase Party
Ejemplo 8.9. Definición de atributos de persistencia en la clase Party
/**
* Value object for Party.
*
* The subclasses MUST override the getType() method
*
* @hibernate.class table="party"
*
* @hibernate.discriminator column="type"
*
* @author Carlos Sanchez
*/
public class Party extends AbstractBusinessObject {
La línea en negrita especifica que los objetos de esta clase serán persistidos en la tabla party en la base de datos.
Ejemplo 8.10. Definición de atributos de persistencia en los métodos de la clase Party
/**
* @hibernate.property
*
* @return String
*/
public String getInternalName() {
return this.internalName;
}
public void setInternalName(String internalName) {
this.internalName = internalName;
}
En este caso la línea en negrita define que la propiedad
internalName
es persistente, con lo que la tabla en
la base de datos tendrá una columna para guardar sus valores.
En cuanto a la fachada será necesaria una implementación del
interfaz PartyFacadeDelegate
que
utilice Spring, PartySpringFacadeDelegate
. Es
una buena práctica crear un interfaz para que los detalles de
implementación queden ocultos a las otras capas. Entre estos detalles
de implementación se encuentra una nueva propiedad,
partyDao
, y los métodos necesarios para acceder a
ella, getPartyDao
y
setPartyDao
. Esta propiedad contendrá el DAO
responsable de la persistencia de la clase
Party
y sus subclases y será inicializado por
Spring a partir del contexto de aplicación.
Cada uno de los métodos de la fachada debe ejecutarse en una transacción, lo que se puede configurar en el contexto de aplicación de Spring, que se puede ver en el Ejemplo 8.11, “Configuración de la fachada y los DAOs de party”.
Ejemplo 8.11. Configuración de la fachada y los DAOs de party
<!-- ======================= PERSISTENCE DEFINITIONS ======================== --> <bean id="mappingResources" class="java.util.ArrayList"> <constructor-arg> <list> <value>net/sf/oness/party/model/party/bo/Party.hbm.xml</value> </list> </constructor-arg> </bean> <!-- DAO objects: Hibernate implementation --> <bean id="partyDao" class="net.sf.oness.common.model.dao.hibernate.HibernateDao"> <constructor-arg> <value>net.sf.oness.party.model.party.bo.Party</value> </constructor-arg> <property name="sessionFactory"> <ref bean="sessionFactory" /> </property> </bean> <!-- =============================== FACADE ================================ --> <!-- Party Facade --> <bean id="partyFacadeDelegate" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager"> <ref bean="transactionManager" /> </property> <property name="target"> <ref local="partyTarget" /> </property> <property name="transactionAttributes"> <props> <prop key="get*">readOnly</prop> <prop key="find*">readOnly</prop> <prop key="edit*">readOnly</prop> <prop key="create*">PROPAGATION_REQUIRED,ISOLATION_SERIALIZABLE</prop> <prop key="update*">PROPAGATION_REQUIRED,ISOLATION_SERIALIZABLE</prop> <prop key="delete*">PROPAGATION_REQUIRED,ISOLATION_SERIALIZABLE</prop> </props> </property> </bean> <!-- PartyTarget primary business object implementation --> <bean id="partyTarget" class="net.sf.oness.party.model.facade.PartySpringFacadeDelegate"> <property name="partyDao"> <ref local="partyDao" /> </property> </bean>
La aplicación web actuará como interfaz del modelo anteriormente creado. Se divide en dos partes: controlador y vista, que serán implementadas utilizando Struts, Tiles, JSP, JSTL y CSS.
Se incluye un fichero de configuración para Tomcat, ya listado anteriormente en el Ejemplo 8.6, “Configuración de la aplicación web party-webapp en Tomcat utilizando JNDI”, que permite configurar la aplicación vía JNDI sin necesidad de modificar el fichero war.
El controlador constará de la clase
PartyAction
, que gestionará todas aquellas
acciones que realice el usuario a través de la vista y las
transformará en llamadas a la fachada de la capa modelo.
create
: crear un contacto
update
: actualizar un
contacto
find
: buscar un contacto por sus
atributos
show
: mostrar un contacto
concreto
edit
: mostrar un contacto para su
posterior posible actualización
delete
: borrar un contacto
La vista estará formada por las páginas JSP, los mensajes de la aplicación y los ficheros de configuración.
Interfaz web
Utilizando Tiles es posible utilizar una aproximación basada en componentes para realizar el interfaz web, separando las páginas JSP en porciones reutilizables que se ensamblarán en los ficheros de configuración de Tiles mediante definiciones.
Menús de la aplicación
Páginas JSP necesarias para proporcionar la funcionalidad de gestión de contactos.
details.jsp
: muestra los
detalles de un contacto.
form.jsp
: que se utiliza
tanto para crear o editar un contacto como para buscar
por sus atributos.
list.jsp
: muestra una lista
de contactos, resultado de una búsqueda.
Páginas de soporte: cabecera, pie de página, menús,...
Mensajes
Para permitir la internacionalización de la aplicación se utiliza el soporte que proporciona JSTL. A cada mensaje se le asigna una clave que es la que se utiliza en las páginas JSP y en ficheros de texto, uno por idioma, se escriben las correspondencias entre clave y mensaje.
Configuración
Descriptor de aplicaciones web
Ubicación de los mensajes para JSTL
Ubicación de los ficheros de configuración de Spring
Acciones y formularios de struts
Validación de los formularios de struts
Definiciones de tiles
Menús de struts-menu
En esta segunda iteración se completará la gestión de contactos y se añadirá la funcionalidad de autenticación y autorización, como se puede ver en la Tabla 8.2, “Historias segunda iteración”.
Tabla 8.2. Historias segunda iteración
Añadir información de contacto a un contacto | 1 |
Visualización, modificación y eliminación de información de contacto | 1 |
Autenticación y autorización | 2 |
ESTIMACIÓN INICIAL | 4 |
REAL | 2 |
En primer lugar se diseñan los objetos del dominio que modelan la información de contacto, dirección postal, número de teléfono, dirección de correo electrónico y dirección web, cuyo diagrama UML se puede ver en la Figura 8.2, “Información de contacto”.
Para exponer esta nueva funcionalidad a las capas superiores se añadirá a la fachada el método necesario para crear esta información para un contacto, quedando con se ve en la Figura 8.3, “Añadir información de contacto a un contacto”.
En cuanto a las capas controlador y vista, en la primera se
añadirá una acción para gestionar la información de contacto, en
principio con el método necesario para crear la información de contacto
(create
), junto con la configuración necesaria,
mientras que en la segunda se añadirán más mensajes de aplicación, así
como dos páginas jsp, una con el formulario necesario para introducir
los datos en el momento de la creación y otra para mostrar una lista de
informaciones de contacto, modificando la página que mostraba los
detalles de contacto para incluir la opción de añadir información de
contacto así como la lista referente al contacto que se está
visualizando.
En los casos de número de teléfono y dirección postal, en los que es necesario especificar el país al que se refieren, es necesario presentar una lista de países para que el usuario pueda seleccionar uno de ellos. La mejor forma de implementar esta funcionalidad es mediante una librería de tags que pueda ser reutilizada, lo que implica que debería estar dentro del módulo common-webapp. Esto, y el hecho de que posteriormente será necesario factorizar los recursos comunes de las aplicaciones web hace que el módulo common-webapp pase a ser common-webapp-controller, y se creará un nuevo módulo common-webapp-taglib donde estará contenida la librería de tags para mostrar los países.
Esta librería debe proporcionar dos funciones:
mostrar una lista de países en un formulario, en forma de lista desplegable donde el usuario pueda seleccionar uno de ellos.
mostrar el nombre de un país en el idioma del usuario.
Los países se identificarán por su código ISO y en principio se añadirán los nombres en inglés, español y francés.
Para facilitar su uso se implementa soporte para el lenguaje de expresiones de JSTL basado en la implementación de referencia de JSTL realizada por el proyecto Jakarta taglibs dentro de la Apache Software Foundation.
Con este fin se añadirán los métodos
updateContactInfo
,
deleteContactInfo
y
findContactInfo
a la fachada del modelo, tal y
como se muestra en la Figura 8.4, “Visualización, modificación y eliminación de información de
contacto”.
En el controlador será necesario añadir los correspondientes
métodos a la acción ContactInfoAction
anteriormente implementada: update
,
show
, edit
y
delete
, para actulizar, mostrar, mostrar para
editar y borrar respectivamente, junto con la configuración necesaria de
Struts.
En la vista tan sólo será necesario añadir la página jsp para mostrar los detalles de una información de contacto, sea del tipo que sea, y la configuración de las definiciones de Tiles correspondientes.
Para gestionar la autenticación y autorización se utilizará Acegi Security System for Spring, que permite integrar de manera sencilla y no intrusiva toda una serie de características de seguridad en una aplicación que utilice Spring.
Acegi proporciona implementaciones con las que la información de
usuario puede estar en memoria, útil en entornos de prueba, en una
base de datos accesible vía JBDC, o en un servicio JAAS. Para obtener
una total independencia del sistema gestor de base de datos será
necesario realizar una implementación que utilice Hibernate para
acceder a la información. Para ello tan sólo será necesario crear las
clases User
y Authority
,
que representan respectivamente usuarios, con nombre, contraseña y
estado (habilitado o deshabilitado), y los roles o grupos a los que
pertenece.
En cuanto a la configuración de Acegi ésta se realiza dentro del
contexto de aplicación de Spring, en un fichero
applicationContext-oness-user-model.xml
que puede
ser incluido o excluido en la configuración, habilitando o no las
características de autenticación y autorización, útil para la
realización de tests.
Ejemplo 8.12. Configuración de autenticación y autorización en el modelo
<beans> <!-- User Dao --> <bean id="userDao" class="net.sf.oness.user.model.dao.UserHibernateDao"> <property name="sessionFactory"> <ref bean="sessionFactory" /> </property> </bean> <bean id="mappingResources-user" class="java.util.ArrayList"> <constructor-arg> <list> <value>net/sf/oness/user/model/bo/Authority.hbm.xml</value> <value>net/sf/oness/user/model/bo/User.hbm.xml</value> </list> </constructor-arg> </bean> <!-- ==================== AUTHENTICATION DEFINITIONS =================== --> <!-- Data access object which stores authentication information --> <!-- Hibernate implementation --> <bean id="authenticationDao" class="net.sf.oness.user.model.dao.UserHibernateDao"> <property name="sessionFactory"> <ref bean="sessionFactory" /> </property> </bean> <!-- ============ SECURITY BEANS YOU WILL RARELY (IF EVER) CHANGE ========== --> <bean id="daoAuthenticationProvider" class="net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider"> <property name="authenticationDao"><ref local="authenticationDao"/></property> <property name="userCache"><ref local="userCache"/></property> <!-- <property name="saltSource"><ref local="saltSource"/></property> <property name="passwordEncoder"><ref local="passwordEncoder"/></property> --> </bean> <bean id="passwordEncoder" class="net.sf.acegisecurity.providers.encoding.Md5PasswordEncoder"/> <bean id="userCache" class="net.sf.acegisecurity.providers.dao.cache.EhCacheBasedUserCache"> <property name="minutesToIdle"><value>5</value></property> </bean> <bean id="authenticationManager" class="net.sf.acegisecurity.providers.ProviderManager"> <property name="providers"> <list> <ref local="daoAuthenticationProvider"/> </list> </property> </bean> <bean id="roleVoter" class="net.sf.acegisecurity.vote.RoleVoter"/> <bean id="accessDecisionManager" class="net.sf.acegisecurity.vote.AffirmativeBased"> <property name="allowIfAllAbstainDecisions"><value>false</value></property> <property name="decisionVoters"> <list> <ref local="roleVoter"/> </list> </property> </bean> </beans>
Será necesario añadir los ficheros de mapeado de Hibernate
correspondientes a la gestión de usuarios a los correspondientes a
cada módulo, para lo que se crea una clase utilidad
ListConcatenator
que se puede utilizar como se
muestra en el Ejemplo 8.13, “Concatenación de listas en Spring”.
Ejemplo 8.13. Concatenación de listas en Spring
<bean id="mappingResources" class="net.sf.oness.common.model.util.spring.ListConcatenator"> <constructor-arg> <list> <ref local="mappingResources-party"/> <ref bean="mappingResources-user"/> </list> </constructor-arg> </bean> <bean id="mappingResources-party" class="java.util.ArrayList"> <constructor-arg> <list> <value>net/sf/oness/party/model/party/bo/Party.hbm.xml</value> <value>net/sf/oness/party/model/contact/bo/ContactInfo.hbm.xml</value> <value>net/sf/oness/party/model/contact/bo/Country.hbm.xml</value> </list> </constructor-arg> </bean>
En este momento se pueden completar las tareas pendientes de la
primera iteración en cuanto a la auditoría se refiere. Para ello se
crea la clase SecurityHelper
con el método
getUserName
dentro del módulo
common-model que delegará en el
ContextHolder
proporcionado por Acegi. Así se
podrá acceder de forma sencilla al nombre de usuario desde
AuditingDaoHelper
para guardarlo como
responsable de los cambios realizados.
Para añadir autenticación y autorización a las aplicaciones web se utilizará tambien el soporte que ofrece Acegi.
Acegi Security permite que la aplicación se pueda integrar de manera sencilla en el servicio central de autenticación CAS desarrollado por la universidad de Yale, teniendo así funcionalidad de Single Sign On, lo que permite que distintas aplicaciones se autentiquen en un único punto y evitando la necesidad de que los usuarios introduzcan su nombre y contraseña en cada una de ellas. La utilización o no de CAS es cuestión de configuración, pudiendo habilitarlo y deshabilitarlo sin que afecte al funcionamiento del sistema.
El módulo user se desarrollará para que pueda funcionar tanto con CAS como sin él, que será el funcionamiento por defecto ya que la instalación de un servidor CAS aún siendo una simple aplicación web requiere la creación de un certificado SSL que debe ser añadido en el directorio donde reside la máquina virtual Java así como la configuración del servidor de aplicaciones para activar el soporte HTTPS, siendo demasiados requisitos como para que la aplicación sea fácilmente probada por aquellas personas que se la descarguen de el sitio web en [Sourceforge]. Para más información sobre la configuración de la integración con CAS se puede consultar la documentación de Acegi.
El sistema utilizado por Acegi para implementar los mecanismos de seguridad en los recursos web está basado en el uso de filtros, que interceptan las peticiones HTTP y realizan algún tipo de procesamiento tomando las medidas oportunas.
Ejemplo 8.14. Configuración de autenticación y autorización en el descriptor de aplicación web
<!-- cas --> <context-param> <param-name>edu.yale.its.tp.cas.authHandler</param-name> <param-value>net.sf.acegisecurity.adapters.cas.CasPasswordHandlerProxy</param-value> </context-param> <!-- Required for CAS ProxyTicketReceptor servlet. This is the URL to CAS' "proxy" actuator, where a PGT and TargetService can be presented to obtain a new proxy ticket. THIS CAN BE REMOVED IF THE APPLICATION DOESN'T NEED TO ACT AS A PROXY --> <context-param> <param-name>edu.yale.its.tp.cas.proxyUrl</param-name> <param-value>http://localhost:8433/cas/proxy</param-value> </context-param> <!-- Acegi Security Filters --> <!-- <filter> <filter-name>Acegi Channel Processing Filter</filter-name> <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class> <init-param> <param-name>targetClass</param-name> <param-value> net.sf.acegisecurity.securechannel.ChannelProcessingFilter </param-value> </init-param> </filter> --> <filter> <filter-name>Acegi Authentication Processing Filter</filter-name> <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class> <init-param> <param-name>targetClass</param-name> <!-- Not using CAS --> <param-value> net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilter </param-value> <!-- Using CAS --> <!-- <param-value> net.sf.acegisecurity.ui.cas.CasProcessingFilter </param-value> --> </init-param> </filter> <!-- <filter> <filter-name>Acegi CAS Processing Filter</filter-name> <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class> <init-param> <param-name>targetClass</param-name> <param-value>net.sf.acegisecurity.ui.cas.CasProcessingFilter</param-value> </init-param> </filter> <filter> <filter-name>Acegi HTTP BASIC Authorization Filter</filter-name> <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class> <init-param> <param-name>targetClass</param-name> <param-value>net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter</param-value> </init-param> </filter> --> <filter> <filter-name>Acegi Security System for Spring Auto Integration Filter</filter-name> <filter-class>net.sf.acegisecurity.ui.AutoIntegrationFilter</filter-class> </filter> <filter> <filter-name>Acegi HTTP Request Security Filter</filter-name> <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class> <init-param> <param-name>targetClass</param-name> <param-value> net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter </param-value> </init-param> </filter> <!-- <filter-mapping> <filter-name>Acegi Channel Processing Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> --> <filter-mapping> <filter-name>Acegi Authentication Processing Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- <filter-mapping> <filter-name>Acegi CAS Processing Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>Acegi HTTP BASIC Authorization Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> --> <filter-mapping> <filter-name>Acegi Security System for Spring Auto Integration Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>Acegi HTTP Request Security Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- CAS servlet which receives a proxy-granting ticket from the CAS server. THIS CAN BE REMOVED IF THE APPLICATION DOESN'T NEED TO ACT AS A PROXY --> <!-- <servlet> <servlet-name>casproxy</servlet-name> <servlet-class>edu.yale.its.tp.cas.proxy.ProxyTicketReceptor</servlet-class> <load-on-startup>3</load-on-startup> </servlet> --> <!-- CAS Proxy --> <!-- <servlet-mapping> <servlet-name>casproxy</servlet-name> <url-pattern>/casProxy/*</url-pattern> </servlet-mapping> -->
La configuración de los distintos filtros en el contexto de aplicación de Spring se puede ver en el Ejemplo 8.15, “Configuración de autenticación y autorización en la aplicación web”.
Ejemplo 8.15. Configuración de autenticación y autorización en la aplicación web
<bean id="authenticationProcessingFilter" class="net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilter"> <property name="authenticationManager"> <ref bean="authenticationManager"/> </property> <property name="authenticationFailureUrl"> <value><![CDATA[/show.do?page=.login&login_error=1]]></value> </property> <property name="defaultTargetUrl"><value>/</value></property> <property name="filterProcessesUrl"><value>/security_check</value></property> </bean> <bean id="securityEnforcementFilter" class="net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter"> <property name="filterSecurityInterceptor"> <ref bean="filterInvocationInterceptor"/> </property> <property name="authenticationEntryPoint"> <ref local="authenticationProcessingFilterEntryPoint"/> </property> </bean> <bean id="authenticationProcessingFilterEntryPoint" class="net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint"> <property name="loginFormUrl"> <value><![CDATA[/show.do?page=.login]]></value> </property> <property name="forceHttps"><value>false</value></property> </bean> <!-- Use basic authentication --> <!-- <bean id="basicProcessingFilter" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter"> <property name="authenticationManager"> <ref bean="authenticationManager"/> </property> <property name="authenticationEntryPoint"> <ref bean="basicProcessingFilterEntryPoint"/> </property> </bean> <bean id="basicProcessingFilterEntryPoint" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint"> <property name="realmName"><value>ONess Realm</value></property> </bean> -->
Un ejemplo de configuración de reglas se puede ver en el Ejemplo 8.16, “Configuración de reglas de autorización”. En él se configura el filtro que permite o restringe el acceso a distintas urls según el grupo al que pertenece el usuario. En este caso se requiere que todas aquellas operaciones que impliquen una modificación de datos sólo puedan ser realizadas por usuarios no anónimos. Los usuarios anónimos podrán acceder a otras urls como búsqueda y consulta. Un usuario dentro del grupo administrador podrá acceder a todas las urls y será el único que podra hacerlo a aquellas que se encuentren bajo /secure/.
Ejemplo 8.16. Configuración de reglas de autorización
<bean id="filterInvocationInterceptor" class="net.sf.acegisecurity.intercept.web.FilterSecurityInterceptor"> <property name="authenticationManager"> <ref bean="authenticationManager"/> </property> <property name="accessDecisionManager"> <ref bean="accessDecisionManager"/> </property> <property name="objectDefinitionSource"> <value> CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON PATTERN_TYPE_APACHE_ANT /secure/**=ROLE_ADMIN /**/*create*=ROLE_USER,ROLE_ADMIN /**/*edit*=ROLE_USER,ROLE_ADMIN /**/*update*=ROLE_USER,ROLE_ADMIN /**/*delete*=ROLE_USER,ROLE_ADMIN </value> </property> </bean>
Dado que gran parte de los recursos anteriormente desarrollados en el módulo party serán compartidos por el módulo user y otras futuras aplicaciones web, surge la necesidad de separarlos para poder ser reutilizados. La situación ideal sería crear otra aplicación web con los recursos comunes y acceder a ellos desde las otras aplicaciones, pero esta aproximación tiene dos inconvenientes:
no todos los contenedores permiten que una aplicación acceda a los recursos de otra ya que no es una funcionalidad estándar, aunque los más extendidos sí, como Tomcat.
Tiles no permite utilizar componentes alojados en otro contexto (aplicación web).
Estos inconvenientes, principalmente el segundo, hacen que sea inviable esta solución, con lo cual será necesario crear aplicaciones web que contengan los recursos comunes. Para ello se descompondrá el módulo common-webapp en common-webapp-controller, que contendrá todo lo que estaba anteriormente en common-webapp, y common-webapp-view, que contendrá los recursos comunes anteriormente mencionados, y que serán incluidos automáticamente en las aplicaciones web en el momento de su construcción.
Más concretamente el módulo common-webapp-view contendrá:
Mensajes comunes a todo el sistema
Interfaz web
Páginas de error
Imágenes
JavaScript
Hojas de estilo CSS
Diseño (cabecera, pie de página,...)
Configuración
Configuración común en el fichero descriptor de
aplicación web web.xml
.
Configuración común en el fichero de configuración de
Struts struts-config.xml
.
Definiciones de tiles comunes
Reglas de validación
Para poder combinar la configuración común en lo ficheros xml del descriptor de aplicación web y de struts con la correspondiente a cada módulo ha sido necesaria la creación de dos hojas de transformación xml que a partir de dos ficheros xml con entradas obtiene uno con las entradas de ambos. Esta aproximación es mucho mejor y más elegante que la utilización de XDoclet que obligaría a crear un gran número de entidades xml, con el consiguiente engorro que supone su manejo por además no ser ficheros xml bien formados.
Se hace imprescindible insertar datos de ejemplo en la base de
datos ya que en caso contrario los usuarios no podrían acceder a la
funcionalidad que se ha configurado para requerir un usuario no
anónimo. Para ello en esta iteración se ha optado por realizar una
acción de Struts DatabasePopulatorAction
en el
módulo common-webapp-controller que utilizará la
clase DatabasePopulator
del módulo
common-model. [1]
En esta tercera iteración se implementará la funcionalidad relativa a la gestión de inventario y la accesibilidad desde dispositivos móviles de todos los módulos.
Tabla 8.3. Historias tercera iteración
Creación de modelos y productos | 1 |
Visualización, modificación, eliminación y búsqueda de modelos | 1 |
Creación y modificación de precios | 1 |
Accesibilidad desde dispositivos móviles | 1 |
ESTIMACIÓN INICIAL | 4 |
REAL | 4 |
En esta iteración han surgido problemas en la implementación de la funcionalidad relativa a la auditoría en el núcleo del sistema, dado que han surgido errores en la integración con Hibernate en las relaciones entre objetos. A pesar de estos problemas la duración de la iteración no ha aumentado, gracias a que añadir nueva funcionalidad ha resultado tener un coste en tiempo realmente bajo.
Para comenzar se realiza el diagrama de clases UML que se puede ver en la Figura 8.6, “Creación de modelos y productos” con los objetos del dominio descritos en la historia de usuario.
Al igual que en el módulo party se creará una fachada InventoryFacade del módulo como interfaz de la capa modelo hacia capas superiores ocultando los detalles de implementación de la capa modelo.
En la capa controlador se añadirá una acción de Struts,
ModelAction
, que hará de intermediario entre la
vista y el modelo, con su correspondiente configuración. En la capa
vista al igual que en el módulo party se añadirán
las páginas jsp para mostrar el formulario de creación y búsqueda de
modelos.
Los métodos getAll*
son necesarios para
construír una caché con la información de tallajes y colores en la capa
vista sin necesidad de acceder a la base de datos en reiteradas
ocasiones. Estos datos no suelen ser modificados por lo que pueden ser
cargados al inicio de la aplicación, para lo que se desarrollará un
listener de la aplicación web,
SpringContextLoaderListener
, que extenderá y
sustituirá al proporcionado por Spring, para además de realizar sus
operaciones inicializar esta caché. En caso de necesitar reconstruir la
caché tan sólo será necesario recargar la aplicación web en el
contenedor.
Una vez creados los objetos del dominio en la historia anterior en cuanto a la capa modelo sólo será necesario añadir a la fachada los métodos que implementarán esta nueva funcionalidad.
En la capa controlador se añadirán a la clase
ModelAction
los métodos que darán soporte a la
nueva funcionalidad (find
,
edit
, update
,
delete
) y en la capa vista la página jsp para
visualizar un modelo, que mostrará todas las tallas y colores en los que
existe, así como sus códigos de barras y las existencias en cada uno de
los almacenes.
Será necesario modelar los precios y tarifas, de forma que cada
producto tenga un precio por tarifa, y añadir a la fachada los métodos
editPrices
y
updatePrices
, así como hacer que
findModelWithDetails
devuelva también los
precios de cada producto en cada tarifa.
En el controlador serán necesarias una nueva acción,
PriceAction
, que permitirá visualizar los precios
de cada producto de un modelo y su posterior actualización, y las
páginas jsp en la vista. Además se añadirá a la visualización de modelos
los precios de cada producto en cada una de las tarifas.
Dado que la aplicación debe ser accesible desde dispositivos móviles tipo PDA se debe buscar una forma sencilla para adaptar el interfaz web a estos dispositivos, lo que implica principalmente considerar el tamaño de pantalla. Para realizar las pruebas se ha utilizado el simulador del sistema operativo PalmOS junto con su navegador web PalmSource.
La primera opción para adaptar el interfaz ha sido utilizar el
soporte que proporciona CSS para estabecer distintas hojas de estilo
para distintos fines a través del atributo media.
Así por ejemplo se ha creado una hoja de estilo para impresión
(media="print"
) que evita que la cabecera, el pie de
página o el menú no se impriman, tan sólo el contenido ocupando todo el
ancho de la página. Para los dispositivos con pantallas reducidas se
puede utilizar el tipo de medio handheld
, que de
igual forma que para el tipo print
no mostrará ni
cabecera ni pie de página, pero que mantendrá el menu en formato
texto.
El problema que ha surgido es que el navegador PalmSource no
utiliza las hojas de estilo definidas como handheld
,
puede que sea debido a que la calidad de la imagen de la Palm es mucho
mejor que la de otros dispositivos portátiles como pueden ser los
móviles y los desarrolladores del navegador hayan optado por no utilizar
ese tipo de hojas de estilo. Por lo tanto la única opcion restante es
utilizar el identificador que el navegador envía con cada petición y
cambiar las hojas de estilo si éste se corresponde con el identificador
de PalmSource. Dado que las hojas de estilo y otras características de
la presentación se definen mediante Tiles en el fichero de configuración
de definiciones se ha optado por crear una acción de Tiles,
SwitchLayoutController
, que procesará todas las
peticiones y tendrá acceso al contexto de configuración de Tiles, donde
se configurará otra disposición de interfaz para PalmSource que
sustituirá al interfaz por defecto en caso de que la petición sea de
este navegador. Es una solución extensible, pudiéndose añadir tantas
definiciones como sea necesario para distintos tipos de navegador o
compartir una única definición entre varios.
El interfaz en Internet Explorer en la Figura 8.11, “Interfaz en Internet Explorer, página principal” y la Figura 8.12, “Interfaz en Internet Explorer, resultado de una búsqueda” se puede comparar con el resultado en el simulador de una Palm de la Figura 8.13, “Interfaz en una Palm”, para la página principal y para una página de resultados de búsqueda. Aunque en la página de búsqueda los resultados ocupan más ancho del que ofrece la pantalla se puede desplazar la pantalla tanto de arriba a abajo como de izquierda a derecha utilizando las barras de desplazamiento.
En esta iteración se ha optado por cambiar el método de carga de datos de ejemplo en la base de datos, utilizando para ello el listener anteriormente realizado en esta iteración, de forma que los datos son insertados en el momento de cargarse la aplicación web sin necesidad de intervención por parte del usuario.
Esta última iteración concluirá el sistema con la implementación de la funcionalidad relacionada con la gestión de pedidos, albaranes y facturas, tanto para compras como ventas. Como ya se ha comentado anteriormente la introducción de nuevas características es un proceso rápido, cuya duración real es inferior a la inicialmente estimada.
Tabla 8.4. Historias cuarta iteración
Creación de pedidos y añadir productos a un pedido | 1 |
Visualización, modificación, eliminación y listado de pedidos | 1 |
Creación de albaranes y facturas | 1 |
ESTIMACIÓN INICIAL | 3 |
REAL | 2 |
El proceso de creación de un pedido comenzaría a partir de la visualización de un contacto. Una vez creado se podrán añadir productos desde la visualización de productos, siendo común añadir varios productos a la vez del mismo modelo.
El diagrama UML para los objetos del dominio y la fachada puede verse en la Figura 8.14, “Pedidos, albaranes y facturas”.
Para poder mostrar los datos del cliente o proveedor a la vez que se muestran los de pedidos, albaranes o facturas es necesario crear transfer objects compuestos, que agrupen ambos objetos. Lo mismo sucede con los productos y las líneas de pedido, albarán o factura. El diagrama UML se puede ver en la Figura 8.15, “Pedidos, albaranes y facturas, transfer objects”.
En cuanto a la vista y controlador será necesario añadir las páginas y acciones para pedidos, albaranes, facturas y las líneas de cada uno de ellos. Además se añadirán las opciones necesarias a visualización de contactos para crear un pedido de ese contacto y visualización de modelos para añadir uno o varios productos al pedido en curso.
[1] En la siguiente iteración se ha cambiado este método, configurando un listener en la aplicación web que inserta los datos de ejemplo cuando ésta es cargada