Tags: seam

JBoss Seam: render an external xhtml

02 Dic 2011

Por fin un estándar con el que entretejer nuestras aplicaciones.

25 Oct 2009

Recientemente se ha liberado la primera versión estable de Weld, la implementación de referencia de Web Beans (Java Context and Dependenty Injection JSR 299). Y aunque puede parecer una liberación cualquiera, en este caso estamos ante un evento realmente importante para el mundillo Java.

¿Por qué es importante esta liberación?

  • Por fin tenemos un estándar Java para la inyección, publicación, localización y búsqueda de componentes.
  • Promete una mejor orquestación entre componentes JEE, evitando los típicos problemas de interoperabilidad entre Beans Spring, componentes Seam, Wicket, JSF, EL, etc..)
  • Un remplazo estándar a las diferentes soluciones de inyección e IoC como Spring IoC, el prometedor Google Guice o el muy utilizado por nosotros Jboss Seam annotations.
  • Fin del infierno XML, definiendo un mecanismo estándar independiente de ficheros de definición XML, que junto con JSF 2.0, Servlet 3.0, JPA, Jax-ws, etc… simplifican el desarrollo JEE.

¿Consecuencias para la industria a medio plazo?

  • Es demasiado pronto para saberlo, pero…
    ¿Quizás el fin de la guerra de frameworks se inyección?…. (Google Guice vs Spring vs Jboss Seam). Consiguiendo un efecto similar al que consiguió JPA/JDO estandarizando el acceso a datos.
  • Por fin una especificación JEE con la que construir aplicaciones complejas de forma sencilla.
  • En un momento en el que muchas organizaciones están intentando estandarizar sus desarrollos, llegando incluso a restringir o imponer las tecnologías utilizadas, la preselección de implementaciones como Spring IoC o Seam carecerá de sentido. Siendo mucho más coherente la recomendación de estándares en lugar de restringir la evolución natural de la tecnología y la sana competición entre sus diferentes implementaciones.

¿Consecuencia para nosotros?

  • Ninguna…. Gracias a nuestra apuesta por Jboss Seam (desde hace ya unos años), esta especificación se encontraba ya en nuestra hoja de ruta antes incluso de que fuese un Draft, por lo que para nosotros será suficiente con una mínima adaptación al estándar.
  • La implementación es estable, la especificación muy bien pensada, y en las pruebas que hemos realizado con la beta, ha demostrado que consigue simplificar los desarrollos y hacer más comprensible el “oscuro arte de la inyección de componentes”.
  • En breve empezaremos a usarla en algún proyecto piloto (que pueda asumir el riesgo de I+D asociado), analizaremos en detalle los posibles problemas que puedan surgir, y si todo se comporta como promete a mediados de 2010 empezaremos a usar la nueva especificación en nuestros proyectos Java.

¿Defectos?

  • Llega demasiado tarde. Otros frameworks llevan, aunque de una forma mucho menos limpia,  ofreciendo algo similar desde hace ya mucho tiempo.
  • Todavía no conocemos sus defectos. Y eso es un gran problema ya que hasta que no conozcamos sus problemáticas asociadas no será seguro su uso en proyectos de envergadura.
  • Esperemos que en la huida del infierno de los ficheros de configuración XML, no nos metamos en  las tinieblas del uso de inyección e IoC para todo. Ya que una aplicación con una gran cantidad de componentes, comportamientos o atributos que dependen de la inyección hace muy complicada la comprensión de su funcionamiento.

En definitiva, todo indica que estamos antes una de las piezas clave del próximo JEE 6.

Implementando un flujo de firmas en nuestro workflow

03 Nov 2008

A continuación vamos a explicar cómo aprovechar las bondades de tres componentes habituales en nuestros desarrollos para implementar un sencillo flujo de firma de formularios.

Los jugadores son:

  1. Jbpm 3.2 como motor de workflow.
  2. Formula 2 como motor de formularios.
  3. Viafirma 1.3.5 como motor de firma digital.

La aplicación que los implementa está desarrollada con Seam + JSF + JPA.

La Descripción

Básicamente el requisito era el siguiente: un formulario completado por un usuario entraba en un flujo de transiciones en el que distintos departamentos y/o personas tenían que ir aprobándolo con su firma digital.

La solución tradicional con la que contábamos se basaba en recuperar los datos del primer formulario, y en la transición adecuada los mostrábamos para que el firmante pudiera comprobar los datos (tareas).

Esto implicaba un esfuerzo en la redendirización nuevamente de los datos facilitados por el usuario, por lo que decidimos aprovechar las características de los componentes usados para simplicarlo todo.

Solución

Formula2 permite la identificación de los campos creados en el formulario. De esta manera nuestra solución consisitirá en identificar un mismo nombre de campo dentro de un mismo ProcessId.

Una vez asegurado en nuestro proceso esta identificación de campos, ahora toca el turno de Formula2; duplicaremos el formulario original, y modificaremos todos sus campos al tipo ReadOnly, de manera que el firmante no pueda modificar los datos mostrados en el formulario inicial.


Copiar un formulario en Formula2

Opcionalmente, a esta copia del formulario se le podrán añadir campos adicionales, como por ejemplo, un campo de observaciones o bien otros campos para informar valores necesarios en la siguiente transición.

Esta tarea de modificar cada campo de un formulario podría resultar algo engorrosa, pero solo habrá que hacerlo una vez, ya que la primera copia con todos sus campos “ReadOnly” nos serviría  de base para las sucesivas copias que necesitemos, y para estas copias ya no será necesario modificar la propiedad de sus campos.

Modificar propiedad campo

Resultado

A medida que el diseño del flujo (process-definition) va ejecutando las tareas e invocando los distintos formularios, los actores propietarios de dichas tareas van firmando con su certificado digital los formularios.

El proceso de firma es delegado a nuestro al motor de firma VIAFIRMA. Cuando éste completa la transacción de firma, devuelve todos los datos necesarios a nuestro sistema. En este caso, Jbpm interpreta estos datos de firma como variables del proceso, añadiéndolos como un dato más de la transición.

Como valor agregado, estos datos de firma y los contenidos en el formulario son indexados mediante Lucene (implementando openSearch), y de esta forma ofrecemos al usuario la posibilidad de buscar su proceso por el código de firma que se le informó.

Histórico del Proceso

Tras el proceso de firma, se le muestra al usuario una pantalla de recibo, donde se le accede a toda la información del proceso con las distintas opciones:

  • Descarga del formulario que fue firmado.
  • Descarga del comprobante de firma, con los datos del firmante y de la transacción (fecha, hora y certificado usado).
  • Id. de las tarea y proceso.
  • Link permanente a su proceso.
  • Todo ello apoyado con imágenes bidimensionales como el Qr-Code con el resumen de la firma y el BarCode con el código de la firma.

Justificante de Firma

Posibles Conflictos

Seguro que a alguno ya se le ha pasado por la cabeza un posible conflicto:

¿Qué ocurre si para un mismo ProcessId se ven involucrados 2 formularios que no son copias el uno del otro, pero el identificador que le pusimos al campo es el mismo, por ejemplo ID_PROFESOR?

  • si el segundo formulario NO es una copia del primero, pero tiene un campo con el mismo Id., efectivamente nuestro proceso hará que este segundo formulario se renderice con el valor introducido para ese campo en el primer formulario. Pero, al no tratarse de una copia, el campo no será del tipo ReadOnly (o no debiera), por lo que el valor podría ser modificado por el usuario.
  • La solución a este tipo de conflictos en nuestro caso: anteponer un identificador del formulario a modo de prefijo en el identificador del campo. Por ejemplo: FRM_9_ID_PROFESOR.

Resumen

Gracias a la funcionalidad de Formula2 para identificar los campos y duplicar formularios conseguiremos una sencilla implementación de un ESCRITORIO DE FIRMA.

  • Nos ahorramos construir N formularios.
  • Nos ahorramos la lógica para recuperar y renderizar los datos asociados al formulario original y que necesitamos ir mostrando a cada firmante.

Arroz con Mango: Seam + Logger & Finder Interceptor + Lucene…

05 Sep 2008

Bueno, tuve unas necesidades en un proyecto, así que comencé por hacer un GenericDAO, luego le agregué un FinderInterceptor el cual permite que cuando hagas findByWhatever te busque un NamedQuery que esté definido en la Entidad de JPA, como tip le agregué un logger el cual loguea cada método conjunto con los parámetros pasados, aqui comienzo a explicar uno por uno:

GenericDAO; interfase que define el contrato del JPA DAO el cual implementa de forma genérica las funcionalidades del mismo; esta interfase está interceptada por un Finder del cual hablaremos más tarde; tuve que usar Lucene ya que el proyecto corre sobre MSSQL Server y el bendito no soporta translate:


package com.viavansi.fdu.persistencia.DAO;
import java.io.Serializable;
import java.util.List;

import org.apache.lucene.queryParser.ParseException;

import com.viavansi.framework.core.persistencia.servicios.excepciones.ExcepcionPersistencia;/**
 * Interface for data access objects.
 *
 * <p>
 * Generic Interface DAO which provides the basic contracted operations for
 * every DAO; an implementation is also provided.
 *
 * @param <T>
 * The persistent class.
 * @param <PK>
 * The class of the primary key of the persistent class.
 */
@FinderExecutor
public interface GenericDao<T, PK extends Serializable> { /**
  * Merge.
  *
  * @param persistentObject
  */
 void update(T persistentObject) throws ExcepcionPersistencia; /**
  * Make the instance persistent.
  *
  * @throws ExcepcionPersistencia
  */
 void create(T newInstance) throws ExcepcionPersistencia; /**
  * Make the object transient.
  *
  * @param persistentObject
  * @throws ExcepcionPersistencia
  */
 void delete(T persistentObject) throws ExcepcionPersistencia; /**
  * Returns a persistent object specified by its key.
  *
  * @throws ExcepcionPersistencia
  */
 T read(PK id) throws ExcepcionPersistencia; /**
  * Returns all persistent entities.
  */
 List<T> findAll() throws ExcepcionPersistencia; /**
  * Resolves and executes a finder. <p/>
  * <p>
  * This implementation uses the short name of the type class, appending a .
  * and the method name so the name of the query to look up becomes
  * Pet1.findByName if the method is findByName and the type Pet1. <p/>
  * <p>
  * An other implementation would be useful as well that does not return a
  * list but a single object.
  */
 List<T> executeFinder(String method, Object[] queryArguments)
  throws ExcepcionPersistencia; List<T> searchByText(String expresion) throws ExcepcionPersistencia,
  ParseException;

Ahora definimos el DAO que contiene injectado el EntityManager:


package com.viavansi.fdu.persistencia.DAO;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.Query;

import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.Search;
import org.jboss.seam.annotations.In;

import com.viavansi.framework.core.excepciones.CodigoError;
import com.viavansi.framework.core.persistencia.servicios.excepciones.ExcepcionDatosNoEncontrados;
import com.viavansi.framework.core.persistencia.servicios.excepciones.ExcepcionPersistencia;

/**
 * Generic data access object.
 *
 * @param <T>
 * The persistent class.
 * @param <PK>
 * The class of the primary key of the persistent class.
 */
public abstract class GenericJpaDaoFDU<T, PK extends Serializable> implements
  GenericDao<T, PK> {

protected Class<T> type;

@In("fduPersistenceContext")
 protected EntityManager entityManager;

public EntityManager getEntityManager() {
  return entityManager;
 }

public void setEntityManager(EntityManager entityManager) {
  this.entityManager = entityManager;
 }

public void create(T newInstance) throws ExcepcionPersistencia {
  try {
  this.entityManager.persist(newInstance);
  } catch (Exception e) {
  throw new ExcepcionPersistencia(CodigoError.ERROR_NO_DEFINIDO, e);
  }
 }

public void delete(T persistentObject) throws ExcepcionPersistencia {
  try {
  this.entityManager.remove(persistentObject);
  } catch (Exception e) {
  throw new ExcepcionPersistencia(CodigoError.ERROR_NO_DEFINIDO, e);
  }
 }

public T read(PK id) throws ExcepcionDatosNoEncontrados {
  T t = (T) this.entityManager.find(type, id);
  if (t == null) {
  throw new ExcepcionDatosNoEncontrados(
  CodigoError.ERROR_DATOS_NO_ENCONTRADOS,
  "Datos no encontrados");
  }
  return t;
 }

public void update(T transientObject) throws ExcepcionPersistencia {
  try {
  this.entityManager.merge(transientObject);
  } catch (Exception e) {
  throw new ExcepcionPersistencia(CodigoError.ERROR_NO_DEFINIDO, e);
  }
 }

@SuppressWarnings("unchecked")
 public List<T> findAll() throws ExcepcionPersistencia {
  try {
  return entityManager.createQuery(
  "select obj from " + this.type.getName() + " obj")
  .getResultList();
  } catch (Exception e) {
  throw new ExcepcionPersistencia(CodigoError.ERROR_NO_DEFINIDO, e);
  }
 }

/**
  * Resolves and executes a finder. <p/>
  * <p>
  * This implementation uses the short name of the type class, appending a .
  * and the method name so the name of the query to look up becomes
  * Pet1.findByName if the method is findByName and the type Pet1. <p/>
  * <p>
  * An other implementation would be useful as well that does not return a
  * list but a single object.
  *
  * @throws ExcepcionPersistencia
  */
 @SuppressWarnings( { "unchecked" })
 public List<T> executeFinder(String method, Object[] queryArguments)
  throws ExcepcionPersistencia {
  try {
  String queryName = queryNameFromMethod(method);
  Query query = entityManager.createNamedQuery(queryName);
  for (int i = 0; i < queryArguments.length; i++) {
  query.setParameter(i, queryArguments[i]);
  }
  return query.getResultList();
  } catch (Exception e) {
  throw new ExcepcionPersistencia(CodigoError.ERROR_NO_DEFINIDO, e);
  }
 }

/**
  * Resolves the name of the named query.
  *
  * @param finderMethod
  * "findPerson, etc."
  * @return
  */
 protected String queryNameFromMethod(String finderMethod) {
  return type.getSimpleName() + "." + finderMethod;
 }

/*
  * (non-Javadoc)
  *
  * @see
  * com.viavansi.fdu.persistencia.DAO.GenericDao#findWhere(java.lang.String)
  */
 @SuppressWarnings("unchecked")
 public List<T> searchByText(String expression)
  throws ExcepcionPersistencia, ParseException {
  PropertyUtilsBean propertyUtils = BeanUtilsBean.getInstance()
  .getPropertyUtils();
  StringBuilder builder = new StringBuilder();
  boolean firstField = true;
  for (PropertyDescriptor descriptor : propertyUtils
  .getPropertyDescriptors(type)) {
  if (firstField) {
  firstField = false;
  } else {
  builder.append(" OR ");
  }
  builder.append(descriptor.getName() + ":" + expression);
  }
  FullTextEntityManager fullTextEntityManager = Search
  .createFullTextEntityManager(entityManager);
  QueryParser parser = new QueryParser("id", new ISOLatin1Analyzer());
  org.apache.lucene.search.Query luceneQuery = parser.parse(builder
  .toString());
  Query query = fullTextEntityManager.createFullTextQuery(luceneQuery,
  type);
  List result = query.getResultList();
  if (result.size() == 0) {
  throw new ExcepcionDatosNoEncontrados();
  }
  return result;
 }

/**
  *
  * @param <T>
  * @param expression
  * @param entityManager
  * @param type
  * @return
  * @throws ExcepcionPersistencia
  * @throws ParseException
  */
 @SuppressWarnings("unchecked")
 public static <T> List<T> searchByText(String expression,
  EntityManager entityManager, Class<T> type)
  throws ExcepcionPersistencia, ParseException {
  PropertyUtilsBean propertyUtils = BeanUtilsBean.getInstance()
  .getPropertyUtils();
  StringBuilder builder = new StringBuilder();
  boolean firstField = true;
  for (PropertyDescriptor descriptor : propertyUtils
  .getPropertyDescriptors(type)) {
  if (firstField) {
  firstField = false;
  } else {
  builder.append(" OR ");
  }
  builder.append(descriptor.getName() + ":" + expression);
  }
  FullTextEntityManager fullTextEntityManager = Search
  .createFullTextEntityManager(entityManager);
  QueryParser parser = new QueryParser("id", new ISOLatin1Analyzer());
  org.apache.lucene.search.Query luceneQuery = parser.parse(builder
  .toString());
  Query query = fullTextEntityManager.createFullTextQuery(luceneQuery,
  type);
  return query.getResultList();
 }

}

Nuestro 1er DAO para una Entidad; como verán, se apoya en el GenericDAO, y en su implementación:


package com.viavansi.fdu.persistencia.DAO;
import java.util.List;

import org.jboss.seam.annotations.Name;

import com.viavansi.fdu.persistencia.VO.ProcessInfoVO;

/**
 * @author gmedina
 *
 */
@Name("processInfoDAO")
public class ProcessInfoDAO extends GenericJpaDaoFDU<ProcessInfoVO, Long> {

public ProcessInfoDAO() {
  this.type = ProcessInfoVO.class;
 }

public List<ProcessInfoVO> findByProcessName(String name) {
  return null;
 }

public List<ProcessInfoVO> findByJbpmName(String jbpmName) {
  return null;
 }

public List<ProcessInfoVO> findByArea(String area) {
  return null;
 }

}

Vamos a mostrar nuestro Finder interceptor el cual sigue el Interceptor pattern y está soportado por Seam; muy sencillo, si el método se llama findByWhatever el busca un named query llamado findByWhatever, en tu implementación del DAO solo debes retornar null, si retornas algo entonces el finder se anula automáticamente:


package com.viavansi.fdu.interceptor;
import org.jboss.seam.annotations.intercept.AroundInvoke;
import org.jboss.seam.intercept.InvocationContext;

import com.viavansi.fdu.persistencia.DAO.GenericDao;

/**
 * @author gmedina
 *
 */
public class FinderInterceptor { /**
  *
  * @param invocation
  * @return
  * @throws Throwable
  */
 @AroundInvoke
 @SuppressWarnings("unchecked")
 public Object executeFinder(InvocationContext invocation) throws Throwable {
  String methodName = invocation.getMethod().getName();
  if (methodName.startsWith("findBy")) {
  GenericDao dao = (GenericDao) invocation.getTarget();
  Object result = invocation.proceed();
  return result == null ? dao.executeFinder(methodName, invocation
  .getParameters()) : result;
  } else
  return invocation.proceed();
 }

Nuestro persistence.xml con la configuración de Lucene que necesita:


<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0"
 xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
 <!--
  Conexión por defecto de la aplicación utilizando POOL de conexiones
 -->
 <persistence-unit name="fduPersistenceUnit"
  transaction-type="RESOURCE_LOCAL">
  <provider>org.hibernate.ejb.HibernatePersistence</provider>
  <jta-data-source>java:comp/env/jdbc/fdu</jta-data-source>
  <class>com.viavansi.fdu.persistencia.VO.UsuarioVO</class>
  <class>com.viavansi.fdu.persistencia.VO.MiembroVO</class>
  <class>com.viavansi.fdu.persistencia.VO.RolVO</class>
  <class>com.viavansi.fdu.persistencia.VO.ProcessInfoVO</class>
  <properties>
  <property name="hibernate.show_sql" value="true" />
  <property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />
  <property name="hibernate.cache.provider_class" value="org.hibernate.cache.EhCacheProvider" />
  <!--
  Configuración para el soporte de prefijos en Hibernate. Estrategia
  para generación de nombres de tablas asociadas a anotaciones Table
  EJB3.0.
  -->
  <property name="hibernate.ejb.naming_strategy"
  value="com.viavansi.framework.persistencia.jpa.NamingStrategy" />
  <property name="hibernate.search.default.directory_provider"
  value="org.hibernate.search.store.FSDirectoryProvider" />
  <property name="hibernate.search.default.indexBase"
  value="/Java/lucene/fdu/app" />
  </properties>
 </persistence-unit> </persistence>

Finalmente nuestro DAO el cual tiene anotaciones de JPA y Hibernate Search/Lucene


package com.viavansi.fdu.persistencia.VO;
import java.io.Serializable;
import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import org.hibernate.search.annotations.Analyzer;
import org.hibernate.search.annotations.DateBridge;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.Resolution;
import org.hibernate.search.annotations.Store;

import com.viavansi.fdu.persistencia.DAO.ISOLatin1Analyzer;

/**
 * @author gmedina
 *
 */
@NamedQueries( {
  @NamedQuery(name = "ProcessInfoVO.findByProcessName", query = "from ProcessInfoVO processInfo where processInfo.processName = ?"),
  @NamedQuery(name = "ProcessInfoVO.findByJbpmName", query = "from ProcessInfoVO processInfo where processInfo.jbpmName = ?"),
  @NamedQuery(name = "ProcessInfoVO.findByArea", query = "from ProcessInfoVO processInfo where processInfo.area = ?") })
@Indexed
@Analyzer(impl = ISOLatin1Analyzer.class)
@Entity
@Table(name = "`${PREFIX_FDU}PROCESS_INFO`")
public class ProcessInfoVO implements Serializable {

private static final long serialVersionUID = 1570433106072090149L;

private Long id;
 private String processName;
 private String jbpmName;
 private Date startDate;
 private Date dueDate;
 private boolean active;
 private String area;

/**
  * @return the id
  */
 @Id
 @DocumentId
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 public Long getId() {
  return id;
 }

/**
  * @param id
  * the id to set
  */
 public void setId(Long id) {
  this.id = id;
 }

/**
  * @return the processName
  */
 @Field(index = Index.TOKENIZED, store = Store.NO)
 @Column(name = "PROCESS_NAME", unique = true, nullable = false, insertable = true, updatable = true, length = 255)
 public String getProcessName() {
  return processName;
 }

/**
  * @param processName
  * the processName to set
  */
 public void setProcessName(String processName) {
  this.processName = processName;
 }

/**
  * @return the jbpmName
  */
 @Field(index = Index.TOKENIZED, store = Store.NO)
 @Column(name = "JBPM_NAME", unique = true, nullable = false, insertable = true, updatable = true, length = 255)
 public String getJbpmName() {
  return jbpmName;
 }

/**
  * @param jbpmName
  * the jbpmName to set
  */
 public void setJbpmName(String jbpmName) {
  this.jbpmName = jbpmName;
 }

/**
  * @return the startDate
  */
 @Field(index = Index.UN_TOKENIZED)
 @DateBridge(resolution = Resolution.DAY)
 @Temporal(TemporalType.DATE)
 @Column(name = "START_DATE", unique = false, nullable = true, insertable = true, updatable = true)
 public Date getStartDate() {
  return startDate;
 }

/**
  * @param startDate
  * the startDate to set
  */
 public void setStartDate(Date startDate) {
  this.startDate = startDate;
 }

/**
  * @return the dueDate
  */
 @Field(index = Index.UN_TOKENIZED)
 @DateBridge(resolution = Resolution.DAY)
 @Temporal(TemporalType.DATE)
 @Column(name = "DUE_DATE", unique = false, nullable = true, insertable = true, updatable = true)
 public Date getDueDate() {
  return dueDate;
 }

/**
  * @param dueDate
  * the dueDate to set
  */
 public void setDueDate(Date dueDate) {
  this.dueDate = dueDate;
 }

/**
  * @return the active
  */
 @Column(name = "ACTIVE", unique = false, nullable = false, insertable = true, updatable = true)
 public boolean isActive() {
  return active;
 }

/**
  * @param active
  * the active to set
  */
 public void setActive(boolean active) {
  this.active = active;
 }

/**
  * @return the area
  */
 @Field(index = Index.TOKENIZED, store = Store.NO)
 @Column(name = "AREA", unique = false, nullable = false, insertable = true, updatable = true, length = 100)
 public String getArea() {
  return area;
 }

/**
  * @param area
  * the area to set
  */
 public void setArea(String area) {
  this.area = area;
 }

/*
  * (non-Javadoc)
  *
  * @see java.lang.Object#hashCode()
  */
 @Override
 public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result + ((id == null) ? 0 : id.hashCode());
  return result;
 }

/*
  * (non-Javadoc)
  *
  * @see java.lang.Object#equals(java.lang.Object)
  */
 @Override
 public boolean equals(Object obj) {
  if (this == obj) {
  return true;
  }
  if (obj == null || !(obj instanceof ProcessInfoVO)) {
  return false;
  }
  ProcessInfoVO other = (ProcessInfoVO) obj;
  if (id == null) {
  if (other.id != null) {
  return false;
  }
  } else if (!id.equals(other.id)) {
  return false;
  }
  return true;
 }

}

Nota: Lucene necesita construir un indice inicial, les pegaré un pedazo de código de como hacer esto:


@SuppressWarnings("unchecked")
 public void reIndexLuceneDB() {
  FullTextEntityManager fullTextEntityManager = Search
  .createFullTextEntityManager(entityManager);
  Class[] entityClasses = { UsuarioVO.class, RolVO.class,
  ProcessInfoVO.class };
  for (Class entityClass : entityClasses) {
  for (Object object : entityManager.createQuery(
  "select obj from " + entityClass.getName() + " obj")
  .getResultList()) {
  fullTextEntityManager.index(object);
  }
  }
 }

Bueno, he pegado tanto código que explicarlo todo en un solo post es difícil, asi que manden sus preguntas.

Que lo disfruten.

Guido.

Spring vs Seam (La guerra ha empezado)

10 Abr 2008

Como primer contrincante tenemos Spring, un framework muy extendido, al que le empezaban a pesar las arrugas, y que tras su última actualización ha vuelto a rejuvenecer.

Como segundo contrincante tenemos Seam, un framework joven, y con una nueva filosofía.

He de reconocer que cada vez me gusta mas Spring, cada vez se parece más a Jboss Seam :p. Os dejo algunos ejemplos en los que el nuevo Spring ha seguido el camino marcado por Seam, abandonando los “desagradables” xmls de mapeo declarativo, para ofrecer la misma funcionalidad con anotaciones.

Gestión de transacciones con Spring:

@Transactional(readOnly = false)
void postComment(long articleId, Comment comment);

Gestión de transacciones con Seam:

@Transactional
void postComment(long articleId, Comment comment);

Publicación de Manager JSF Beans con Spring 2.5:

@Controller(”PersonaController”)
@Scope(”session”)
public class PersonaController{
..
}

Publicación de Manager JSF Beans con Seam:

@Name("personaController")
@Scope(SESSION)
public class PersonaController{
..
}

La verdad es que revisando una a una las nuevas anotaciones y funcionalidades que proporciona Spring, solo me queda alegrarme por el parecido. Estoy seguro que con esta competencia ganamos todos, por ahora estoy del lado de Seam ya que su enfoque resulta menos forzado y mucho mas alineado con los futuros Web Beans, pero quien sabe lo que nos depara el futuro.

Los que quieran profundizar en las diferencias,  pueden ver Framework Deachmatch: Spring vs Seam[pps]

También se han publicado una serie de artículos muy interesantes sobre como integrar Spring con Seam.

Framework Seam: autorización en aplicaciones

04 Mar 2008

Como todos ya sabemos uno de los puntos importantes de una aplicación web es el control de la seguridad en cuanto a la autenticación y la autorización de usuarios.

Básicamente a la Autenticación le corresponde la tarea de ver quién es el usuario y a la Autorización le corresponde permitir o no el acceso a las distintas zonas de una aplicación web a partir, por ejemplo, de su perfil en la aplicación.

En este post me centraré en la Autorización, ya que Jboss Seam tiene mucho que decir al respecto, y os muestro una posible implementación para controlar el acceso de un usuario a una “zona” determinada de una aplicación.

Supongamos una aplicación con dos zonas, una zona de administración y una zona de acceso a usuarios. Para la primera zona (http://<aplicacion>/admin) se necesita que el usuario posea el rol “admin”, mientras que para la segunda(http://<aplicacion>/usuario) el rol “usuario”, nos centraremos en la primera, la segunda os la dejo como ejercicio…
También supongamos que tenemos un objeto “authenticator” que implementa los distintos métodos que requiere Seam respectos a la autenticación.
Normalmente, cuando un usuario entra en la aplicación sin haberse logado Seam lanza la excepción “NotLoggedInException” y posteriormente se redirige al usuario a la correspondiente pantalla para logarse. Este comportamiento se controla mediante el archivo “pages.xml”:
<exception class=”org.jboss.seam.security.NotLoggedInException“>
<redirect view-id=”/index.xhtml“>
<message>Usted debe estar logado para acceder a esta página.</message>
</redirect>
</exception>

Tras introducir los datos para el login (usuario y contraseña, certificado digital, etc.), el método “returnTo” del “autenticator” nos redirige a la zona adecuada, según observemos el rol que tiene asignado el usuario, esto lo podemos indicar en el fichero “components.xml”:
<event type=”org.jboss.seam.postAuthenticate“>
<action execute=”#{authenticator.returnTo}“/>
</event>

Una primera propuesta (adelanto que no es la definitiva), para controlar el acceso a la zona “admin” podríamos definir en el pages.xml lo siguiente:

<page  view-id=”/admin/*” login-required=”true”>
<restrict>#{s:hasRole(‘admin’)}</restrict>
</page>

<exception class=”org.jboss.seam.security.AuthorizationException“>
<end-conversation/>
<redirect view-id=”/index.xhtml“>
<message>Usted no posee los privilegios suficientes para acceder a esta zona.</message>
</redirect>
</exception>

Que en Castellano significa: para las peticiones a “/admin/*” comprueba que el usuario tiene el rol “admin”. Cuando no lo tenga se lanzará la correpondiente excepción “AuthorizationException” y se le redirigirá a “/index.xhtml” mostrándose el mensaje “Usted no posee los privilegios suficientes para acceder a esta zona.”

Quizás si no vamos más allá podamos pensar que tenemos todo controlado, pero ahora imaginemos la siguiente situación:

1) Un usuario entra en la aplicación, se va a la página de Login (por el NotLoggedInException capturado) y se loga correctamente, sin rol “admin” por lo que no debería tener acceso a /admin, de hecho el “return True” le redirige a la zona pública.
2) El usuario que se cree un piratilla escribe a fuego en la barra de direcciones del navegador “/admin” y le da al botón de Enter…

¿Qué pasará?
Lo esperado es que si el usuario pide “manualmente” en /admin tras logarse y obtener únicamente el rol “usuario”, la aplicación genera un AuthorizationException y se le redirigirá a /index.xhtml (pantalla inicial de bienvenida)…
Hazlo y comprobarás que no… el usuario tiene acceso a la zona de administración.

¿Y porqué? Pues una de las causas que he leído en el foro de jboss es que “esa excepción no se trata en la fase de render del JSF” y entonces no tiene el comportamiento que se indica en el pages.xml. Quizás no haya que buscar más porqués y proponer otra solución.
Una segunda propuesta, bastante atractiva aunque después propondré otra ligeramente más refinada, trata de que la aplicación capture correctamente la excepción AuthorizationException, nos redirija al index.xhtml y no suponga tener que redefinir arquitectura de ningún tipo… allá va:
Deberemos crear un nuevo “Controller” como este, especialmente dedicado a controlar el acceso a la zona de administración, quizás se podría usar cualquier controller ya existente, pero así queda más limpito nuestro código (vease “asignación de responsabilidades”):
@Name(“authorizationAdminController”)
@Scope(CONVERSATION)
@Restrict(“#{s:hasRole(‘admin’)}”)
public class AuthorizationAdminController extends ControllerJsfApplication {
public void forceCheckAdminAuthorization(){}
}

Vemos que queda un controller práctiamente vacío.
Debido a @Restrict sólo podrán “invocar” a este controller los Identities(usuarios logados) con rol “admin”.

Ahora modificamos nuestro pages.xml de la siguiente forma:
Donde antes teníamos:
<page  view-id=”/admin/*” login-required=”true”>
<restrict>#{s:hasRole(‘admin’)}</restrict>
</page>

Ahora tenemos:
<page  view-id=”/admin/*” login-required=”true” action=”#authorizationAdminController.forceCheckAdminAuthorization}“>
</page>
Con lo que al hacer una petición a “/admin” se ejecutará el action ( authorizationAdminController.forceCheckAdminAuthorization ), el cual no podrá estar accesible si no se posee el rol “admin”, debido a la anotación @Restrict del controller. Si el identity no posee este rol se lanza AuthorizationException, igual que pasaba antes sin usar el controller, sólo que ahora esta excepción si se captura correctamente y se realiza la redirección indicada en el pages.xml.
También deberemos mantener:
<exception class=”org.jboss.seam.security.AuthorizationException”>
<end-conversation/>
<redirect view-id=”/index.xhtml”>
<message>Usted no posee los privilegios suficientes para acceder a esta zona.</message>
</redirect>
</exception>
Con esta modificación conseguimos proteger correctamente la zona “/admin”, un usuario que intente logarse y posteriormente entrar en “/admin”, esta vez se le mostrará el index.xhtml con el mensaje: “Usted no posee los privilegios suficientes para acceder a esta zona.”
Una tercera solución es simplemente un refinamiento de la segunda, y llegamos a ella a través de la pregunta:
Si tenemos 50 zonas de protegidas, ¿tendremos que implementar 50 Controller semejantes a “AuthorizationAdminController” pero con un @Restrict distinto?
Pues no, la solución está en no anotar el Controller con @Restrict, sino anotar el propio método, así podríamos tener un único controller y N métodos para comprobar distintas autorizaciones:

@Name(“authorizationAdminController”)
@Scope(CONVERSATION)
public class AuthorizationAdminController extends ControllerJsfApplication {

@Restrict(“#{s:hasRole(‘admin’)}”)
public void forceCheckAdminAuthorization(){}

@Restrict(“#{s:hasRole(‘usuario’)}”)
public void forceCheckUsuarioAuthorization(){}

………

@Restrict(“#{s:hasRole(‘XXXX’)}”)
public void forceCheckXXXXXXXAuthorization(){}

}

Indicando en el pages.xml el método “forceCheck X Authorization” que corresponde a cada zona las protegeremos convenientemente.

Pues con esto ha acabado lo que ha intentado ser una explicación.

Nunca olvidaré esta “primera vez” en que escribí en xnoccio, espero que hallais disfrutado tanto como yo.

Que la suerte os acompañe !!

Seam en aplicaciones Standalone

20 Feb 2008

Jboss Seam es un magnífico framework para el desarrollo de aplicaciones Web, pero plantea algunos problemas, cuando deseamos reutilizar nuestro código Seam  desde una aplicación que no se esta ejecutando dentro de un servidor de aplicaciones o desde un Thread independiente. Esto es debido a que sin la envoltura del servidor de aplicaciones, ninguno de los contextos Seam estarán disponibles.

El error típico con el que nos vamos a encontrar es:


java.lang.IllegalStateException: No application context active
at org.jboss.seam.Component.forName(Component.java:1807)

Código para inicializar Seam antes de utilizarlo:


     /**
     * Inicializa el contexto Seam.
     * Necesario para ejecutar código cuando el contexto Seam no esta inicializado y necesitamos acceder a elementos del contexto.
     * Con esto evitamos la excepción "No application context active"
     * @author Félix García Borrego
     */
    public static void initContextApplication(){
        if(!Contexts.isApplicationContextActive()){
        // Si el contexto no esta inicializado lo creamos
       if(!Lifecycle.isApplicationInitialized()){
            // La aplicación no esta activa, la activamos
            MockServletContext servletcontextMock = new MockServletContext();
            ServletLifecycle.beginApplication(servletcontextMock);
            new Initialization(servletcontextMock).create().init();
     }
     // Inicializamos el contexto
     ServletLifecycle.beginInitialization();
 }