viernes, 27 de julio de 2012

Obtener información del entorno de una aplicación web en Java

Java
Hace un tiempo comenté algunas ventajas que obtenemos al tener un sistema de logging en una aplicación y una combinación de librerías que podríamos utilizar para este propósito [1]. Una vez que tenemos un sistema de logging en una aplicación podemos sacarle mucho provecho, como obtener alguna información básica del entorno de ejecución.

La razón de obtener la mayor información del entorno de ejecución es más para el entorno de pruebas o producción que para el entorno en el que desarrollamos, ya que estos primeros pueden llevar alguna configuración especial que no solemos revisar o están fuera de nuestro control aunque en los últimos también puede ser interesante si las hay varias personas que colaboran con posiblemente diferentes sistemas operativos instalados y configuraciones. Tener a nuestra disposición esta información nos puede permitir darnos cuenta de la causa de algun comportamiento extraño. Algún ejemplo de información que nos puede ser de utilidad es la hora del sistema, si trabajamos con fechas la zona horaria en la que está configurado o la codificación de caracteres por defecto.

Vamos a verlo en el caso de una aplicación web. En las aplicaciones web Java tenemos a nuestra disposición dos interfaces que podemos implementar y ser notificados cuando ocurran ciertos eventos en la aplicación como son cuando se inicia, cuando se va a terminar, cuando se crea una sesión y cuando se destruye una sesión. Ahora que estamos interesandos en obtener información del sistema vamos a ver unos ejemplos de clases ServletContextListener y HttpSessionListener que podríamos utilizar para obtener información.

Mediante la interfaz ServletContextListener y sus métodos contextInitialized y contextDestroyed podremos obtener una referencia a un objeto ServletContext y podremos acceder a los métodos que proporciona para por ejemplo conocer los parámetros de inicialización de la aplicación si esta la parametrizamos de esta manera. En el siguiente ejemplo se saca al log la siguiente información: unas trazas de cabecera para saber donde empieza en la salida del log cada inicio de la aplicación, el nombre de contexto de la aplicación, las propiedades del sistema (incluyendo la zona horaria y la codificación de caracteres por defecto), la información del servidor sobre el que se ejecuta la aplicación y la versión de la Java Servlet API, los nodos del arbol JNDI y un enumerado que identificará el servidor de aplicaciones sobre el que se ejecuta la aplicación.

package es.com.blogspot.elblogdepicodev.misc;

import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NameClassPair;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import es.com.blogspot.elblogdepicodev.Entorno.Servidor;

/**
 * Clase que recibe los eventos de inicio y fin de la aplicación web.
 */
public class ContextListener implements ServletContextListener {

 private static Logger logger;

 public static ServletContext SERVLET_CONTEXT;

 @Override
 public void contextInitialized(ServletContextEvent sce) {
  SERVLET_CONTEXT = sce.getServletContext();

  // Configurar el entorno
  try {
   Utilidades.setupEntorno();
  } catch (Exception e) {
   e.printStackTrace();
  }

  logger = LoggerFactory.getLogger(ContextListener.class);

  logger.info("");
  logger.info("--- -.-. . -. ... ..- .-. .- -.. --- -.-. . -. ... ..- .-. .- -.. --- -.-. . -. ... ..- .-. .- -.. ---");
  logger.info("--- -.-. . -. ... ..- .-. .- -.. --- -.-. . -. ... ..- .-. .- -.. --- -.-. . -. ... ..- .-. .- -.. ---");
  logger.info("--- -.-. . -. ... ..- .-. .- -.. --- -.-. . -. ... ..- .-. .- -.. --- -.-. . -. ... ..- .-. .- -.. ---");

  {
   try {
    logger.info("Iniciando aplicación web [" + sce.getServletContext().getServletContextName() + "]...");
    logger.info("Propiedades del sistema:");

    SecurityManager sm = System.getSecurityManager();
    Enumeration e = System.getProperties().keys();
    while (e.hasMoreElements()) {
     String key = (String) e.nextElement();
     try {
      if (sm != null) {
       sm.checkPropertyAccess(key);
      }
      logger.info(key + ": " + System.getProperties().get(key));
     } catch (SecurityException se) {
      logger.info(key + ": [No hay permisos]");
     }
    }
   } catch (Exception e) {
    logger.warn(e.getMessage(), e);
   }
  }

  {
   logger.info("Propiedades del entorno:");
   logger.info("Server info: " + sce.getServletContext().getServerInfo());
   logger.info("Java Servlet API version: " + sce.getServletContext().getMajorVersion() + "." + sce.getServletContext().getMinorVersion());
  }

  {
   // Obtener los nodos del árbol JNDI
   Map enviroment = null;
   try {
    enviroment = new LinkedHashMap();
    buildEnv(new InitialContext(), Constantes.JNDI_EVANDTI, enviroment);
   } catch (Exception e) {
    logger.error(e.getMessage(), e);
   }

   logger.info("Propiedades de entorno para la aplicacion web:");
   Iterator it = enviroment.keySet().iterator();
   while (it.hasNext()) {
    String key = (String) it.next();
    logger.info("{}: {}", new Object[] { key, enviroment.get(key) });
   }
  }

  {
   logger.info("Propiedades dinámicas:");
   Servidor servidor = getServidor(SERVLET_CONTEXT);
   Entorno.setPropiedadDinamica(Entorno.PROP_DINAMICA_SERVIDOR, servidor);

   Iterator it = Entorno.getPropiedadesDinamicas().iterator();
   while (it.hasNext()) {
    String propiedad = (String) it.next();
    Object valor = Entorno.getPropiedadDinamica(propiedad);
    logger.info("{}: {}", propiedad, valor);
   }
  }

  logger.info("Iniciando aplicacion con la configuracion del entorno de «{}»", Entorno.getEntorno().toString().toLowerCase());
 }

 @Override
 public void contextDestroyed(ServletContextEvent sce) {
  logger.info("Finalizando aplicación...");
 }

    /**
     * Añade en un Map los nodos de árbol JNDI configurados en el servidor.
     */
 private void buildEnv(Context c, String name, Map env) throws NamingException {
  NamingEnumeration ne = c.list(name);
  while (ne.hasMoreElements()) {
   NameClassPair n = (NameClassPair) ne.next();
   String x = name + "/" + n.getName();
   Object o = c.lookup(x);
   env.put(x, o);
   if (o instanceof Context) {
    buildEnv(c, x, env);
   }
  }
 }

 /**
  * Devuelve un enum del servidor sobre el que se está ejecutando la aplicación.
     */
 private static Servidor getServidor(ServletContext sc) {
  String si = sc.getServerInfo();

  if (si.contains(Constantes.SERVIDOR_TOMCAT)) {
   return Servidor.TOMCAT;
  } else if (si.contains(Constantes.SERVIDOR_JBOSS)) {
   return Servidor.JBOSS;
  } else {
   return Servidor.DESCONOCIDO;
  }
 }
}

Esto sería un ejemplo de salida de esta clase ContextListener:

2012-05-04 21:03:33,031 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener 
2012-05-04 21:03:33,037 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener --- -.-. . -. ... ..- .-. .- -.. --- -.-. . -. ... ..- .-. .- -.. --- -.-. . -. ... ..- .-. .- -.. ---
2012-05-04 21:03:33,038 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener --- -.-. . -. ... ..- .-. .- -.. --- -.-. . -. ... ..- .-. .- -.. --- -.-. . -. ... ..- .-. .- -.. ---
2012-05-04 21:03:33,038 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener --- -.-. . -. ... ..- .-. .- -.. --- -.-. . -. ... ..- .-. .- -.. --- -.-. . -. ... ..- .-. .- -.. ---
2012-05-04 21:03:33,038 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener Iniciando aplicación web [TapestryCensurado]...
2012-05-04 21:03:33,038 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener Propiedades del sistema:
2012-05-04 21:03:33,038 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.runtime.name: OpenJDK Runtime Environment
2012-05-04 21:03:33,038 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener sun.boot.library.path: /usr/lib/jvm/java-6-openjdk/jre/lib/amd64
2012-05-04 21:03:33,038 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.vm.version: 20.0-b12
2012-05-04 21:03:33,038 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener shared.loader: 
2012-05-04 21:03:33,038 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.vm.vendor: Sun Microsystems Inc.
2012-05-04 21:03:33,038 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.vendor.url: http://java.sun.com/
2012-05-04 21:03:33,038 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener path.separator: :
2012-05-04 21:03:33,038 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener tomcat.util.buf.StringCache.byte.enabled: true
2012-05-04 21:03:33,038 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.util.logging.config.file: /home/picodotdev/archivos/apache-tomcat-7.0.27/conf/logging.properties
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.vm.name: OpenJDK 64-Bit Server VM
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener file.encoding.pkg: sun.io
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener sun.java.launcher: SUN_STANDARD
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener user.country: ES
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener sun.os.patch.level: unknown
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.vm.specification.name: Java Virtual Machine Specification
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener user.dir: /home/picodotdev/archivos/apache-tomcat-7.0.27/bin
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.runtime.version: 1.6.0_24-b24
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.awt.graphicsenv: sun.awt.X11GraphicsEnvironment
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.endorsed.dirs: /home/picodotdev/archivos/apache-tomcat-7.0.27/endorsed
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener os.arch: amd64
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.io.tmpdir: /home/picodotdev/archivos/apache-tomcat-7.0.27/temp
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener line.separator: 

2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.vm.specification.vendor: Sun Microsystems Inc.
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.naming.factory.url.pkgs: org.apache.naming
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.util.logging.manager: org.apache.juli.ClassLoaderLogManager
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener os.name: Linux
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener sun.jnu.encoding: UTF-8
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener tomcat.util.scan.DefaultJarScanner.jarsToSkip: bootstrap.jar,commons-daemon.jar,tomcat-juli.jar,annotations-api.jar,el-api.jar,jsp-api.jar,servlet-api.jar,catalina.jar,catalina-ant.jar,catalina-ha.jar,catalina-tribes.jar,jasper.jar,jasper-el.jar,ecj-*.jar,tomcat-api.jar,tomcat-util.jar,tomcat-coyote.jar,tomcat-dbcp.jar,tomcat-i18n-en.jar,tomcat-i18n-es.jar,tomcat-i18n-fr.jar,tomcat-i18n-ja.jar,tomcat-juli-adapters.jar,catalina-jmx-remote.jar,catalina-ws.jar,tomcat-jdbc.jar,commons-beanutils*.jar,commons-codec*.jar,commons-collections*.jar,commons-dbcp*.jar,commons-digester*.jar,commons-fileupload*.jar,commons-httpclient*.jar,commons-io*.jar,commons-lang*.jar,commons-logging*.jar,commons-math*.jar,commons-pool*.jar,jstl.jar,geronimo-spec-jaxrpc*.jar,wsdl4j*.jar,ant.jar,ant-junit*.jar,aspectj*.jar,jmx.jar,h2*.jar,hibernate*.jar,httpclient*.jar,jmx-tools.jar,jta*.jar,log4j*.jar,mail*.jar,slf4j*.jar,xercesImpl.jar,xmlParserAPIs.jar,xml-apis.jar,dnsns.jar,ldapsec.jar,localedata.jar,sunjce_provider.jar,sunmscapi.jar,sunpkcs11.jar,jhall.jar,tools.jar,sunec.jar,zipfs.jar,apple_provider.jar,AppleScriptEngine.jar,CoreAudio.jar,dns_sd.jar,j3daudio.jar,j3dcore.jar,j3dutils.jar,jai_core.jar,jai_codec.jar,mlibwrapper_jai.jar,MRJToolkit.jar,vecmath.jar,junit.jar,junit-*.jar,ant-launcher.jar
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.library.path: /usr/lib/jvm/java-6-openjdk/jre/lib/amd64/server:/usr/lib/jvm/java-6-openjdk/jre/lib/amd64:/usr/lib/jvm/java-6-openjdk/jre/../lib/amd64:/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib
2012-05-04 21:03:33,039 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.specification.name: Java Platform API Specification
2012-05-04 21:03:33,040 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.class.version: 50.0
2012-05-04 21:03:33,040 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener sun.management.compiler: HotSpot 64-Bit Tiered Compilers
2012-05-04 21:03:33,040 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener os.version: 3.3.4-2-ARCH
2012-05-04 21:03:33,040 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener user.home: /home/picodotdev
2012-05-04 21:03:33,040 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener catalina.useNaming: true
2012-05-04 21:03:33,040 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener user.timezone: Europe/Madrid
2012-05-04 21:03:33,040 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.awt.printerjob: sun.print.PSPrinterJob
2012-05-04 21:03:33,040 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener file.encoding: UTF-8
2012-05-04 21:03:33,040 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.specification.version: 1.6
2012-05-04 21:03:33,040 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener catalina.home: /home/picodotdev/archivos/apache-tomcat-7.0.27
2012-05-04 21:03:33,040 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.class.path: /home/picodotdev/archivos/apache-tomcat-7.0.27/bin/bootstrap.jar:/home/picodotdev/archivos/apache-tomcat-7.0.27/bin/tomcat-juli.jar
2012-05-04 21:03:33,040 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener user.name: picodotdev
2012-05-04 21:03:33,040 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.naming.factory.initial: org.apache.naming.java.javaURLContextFactory
2012-05-04 21:03:33,040 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener package.definition: sun.,java.,org.apache.catalina.,org.apache.coyote.,org.apache.tomcat.,org.apache.jasper.
2012-05-04 21:03:33,040 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.vm.specification.version: 1.0
2012-05-04 21:03:33,040 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener sun.java.command: org.apache.catalina.startup.Bootstrap start
2012-05-04 21:03:33,040 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.home: /usr/lib/jvm/java-6-openjdk/jre
2012-05-04 21:03:33,040 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener sun.arch.data.model: 64
2012-05-04 21:03:33,041 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener user.language: es
2012-05-04 21:03:33,041 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.specification.vendor: Sun Microsystems Inc.
2012-05-04 21:03:33,041 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.vm.info: mixed mode
2012-05-04 21:03:33,041 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.version: 1.6.0_24
2012-05-04 21:03:33,041 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.ext.dirs: /usr/lib/jvm/java-6-openjdk/jre/lib/ext:/usr/java/packages/lib/ext
2012-05-04 21:03:33,041 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener sun.boot.class.path: /usr/lib/jvm/java-6-openjdk/jre/lib/resources.jar:/usr/lib/jvm/java-6-openjdk/jre/lib/rt.jar:/usr/lib/jvm/java-6-openjdk/jre/lib/sunrsasign.jar:/usr/lib/jvm/java-6-openjdk/jre/lib/jsse.jar:/usr/lib/jvm/java-6-openjdk/jre/lib/jce.jar:/usr/lib/jvm/java-6-openjdk/jre/lib/charsets.jar:/usr/lib/jvm/java-6-openjdk/jre/lib/netx.jar:/usr/lib/jvm/java-6-openjdk/jre/lib/plugin.jar:/usr/lib/jvm/java-6-openjdk/jre/lib/rhino.jar:/usr/lib/jvm/java-6-openjdk/jre/lib/modules/jdk.boot.jar:/usr/lib/jvm/java-6-openjdk/jre/classes
2012-05-04 21:03:33,041 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener server.loader: 
2012-05-04 21:03:33,041 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.vendor: Sun Microsystems Inc.
2012-05-04 21:03:33,041 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener catalina.base: /home/picodotdev/archivos/apache-tomcat-7.0.27
2012-05-04 21:03:33,041 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener file.separator: /
2012-05-04 21:03:33,041 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java.vendor.url.bug: http://java.sun.com/cgi-bin/bugreport.cgi
2012-05-04 21:03:33,041 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener common.loader: ${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar
2012-05-04 21:03:33,041 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener sun.io.unicode.encoding: UnicodeLittle
2012-05-04 21:03:33,041 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener sun.cpu.endian: little
2012-05-04 21:03:33,041 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener package.access: sun.,org.apache.catalina.,org.apache.coyote.,org.apache.tomcat.,org.apache.jasper.
2012-05-04 21:03:33,041 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener sun.desktop: gnome
2012-05-04 21:03:33,041 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener sun.cpu.isalist: 
2012-05-04 21:03:33,041 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener Propiedades del entorno:
2012-05-04 21:03:33,042 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener Server info: Apache Tomcat/7.0.27
2012-05-04 21:03:33,042 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener Java Servlet API version: 3.0
2012-05-04 21:03:33,107 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener Propiedades de entorno para la aplicacion web:
2012-05-04 21:03:33,107 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java:/comp/env/com: org.apache.naming.NamingContext@49b9ef36
2012-05-04 21:03:33,108 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java:/comp/env/com/censurado: org.apache.naming.NamingContext@4e07e80a
2012-05-04 21:03:33,108 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java:/comp/env/com/censurado/mail: org.apache.naming.NamingContext@6913108b
2012-05-04 21:03:33,108 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java:/comp/env/com/censurado/mail/censurado: javax.mail.Session@62a34b91
2012-05-04 21:03:33,108 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java:/comp/env/com/censurado/jdbc: org.apache.naming.NamingContext@4eb64f2e
2012-05-04 21:03:33,108 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener java:/comp/env/com/censurado/jdbc/censurado: org.apache.tomcat.dbcp.dbcp.BasicDataSource@23087d61
2012-05-04 21:03:33,108 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener Propiedades dinámicas:
2012-05-04 21:03:33,108 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener dinamica.servidor: TOMCAT
2012-05-04 21:03:33,108 INFO                                        es.com.blogspot.elblogdepicodev.misc.ContextListener Iniciando aplicacion con la configuracion del entorno de «desarrollo»

La siguiente clase HttpSessionListener es muy básica pero nos puede permitir conocer si en algún momento se están creando muchas sesiones de usuario y podríamos acceder a más información a través de la interfaz HttpSession que podemos obtener del objeto HttpSessionEvent que recibimos en el listener.

package es.com.blogspot.elblogdepicodev.misc;

import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Clase que recibe los eventos de creación y destrucción de las sesiones de los
 * usuarios.
 */
public class SessionListener implements HttpSessionListener {

 private static Logger logger = LoggerFactory.getLogger(SessionListener.class);

 @Override
 public void sessionCreated(HttpSessionEvent se) {
  logger.info("Sesion creada");
 }

 @Override
 public void sessionDestroyed(HttpSessionEvent se) {
  logger.info("Sesion destruida");
 }
}

Referencia:
[1] Librerías de logging para Java (slf4j, log4j, java.util.logging, logback, MentaLog)-slf4j.html
Configuración de entorno en Java con ayuda de Groovy

viernes, 20 de julio de 2012

Pruebas unitarias con Spock y Mockito

Java
Si estamos convencidos de que las teorías de Extreme Programming y demás metodologías ágiles son útiles probablemente haremos algún tipo de prueba automatizada. Los inicios sueles ser difíciles ya que al principio hacer pruebas automatizadas parece más trabajo a realizar, sin embargo, una vez empezado se obtienen rápidamente una serie de beneficios en forma de errores detectados de forma temprana de manera que producimos software con menos errores, tenemos mayor seguridad de que entregamos algo que funciona y el hecho de vernos obligagos a desarrollar código testable obtenemos a cambio software de mayor calidad.

Herramientas para hacer pruebas hay muchas en la mayoría de los lenguajes de programación. En el mundo Java una de las primeras herramientas diponibles y que aún sigue usándose ampliameten es JUnit, luego vinieron otras como JUnit 4 o TestNG. Spock es la siguiente herramienta que está empezando a usarse en muchos proyectos. El objetivo de Spock no es distinto del de Junit o TestNG pero la forma de definir las pruebas y el uso de Groovy le hacen tener una serie de características muy interesantes:
  • Fácil de aprender: en sus ideas es muy parecido a JUnit y por tanto si conocemos JUnit podremos enpezar rápicamente con Spock.
  • Groovy: es el lenguaje empleado en los casos de prueba, un lenguaje menos «verboso» que Java que produce código más legible y compacto que este. Algunas de sus características le hacen más adecuado que Java para los casos de prueba.
  • Información: en caso de que alguna prueba falle proporciona información detallada de la comprobación que fallo y sus valores.
  • Soporta varias orientaciones: Se adapta a como deseemos abordar las pruebas, probar primero, probar al final, teses unitarios, teses de integración, desarrollo orientado por pruebas, desarrollo orientado por comportamiento.
  • Compatible: Es compatible con JUnit y por tanto se puede integrar con las principales herramientas e IDEs existentes.
Veamos un ejemplo de como son los teses con Spock:

import spock.lang.Specification

class HelloWorldSpock extends Specification {
    def "length of Spock's and his friends' names"() {
        expect:
        name.size() == length

        where:
        name     | length
        "Spock"  | 5
        "Kirk"   | 4
        "Scotty" | 6
    }
}

En este ejemplo se puede ver el DSL (Domain Specific Language) empleado por Spock. La sección expect es el test que queremos realizar, en este caso que una cadena tenga cierta longitud. La sección where son los datos que proporcionamos al test. Este test es un test parametrizado donde la sección expect se evalua para cada conjunto de valores indicado en la sección where.

El test anterior es muy simple y normalmente necesitaremos hacer mocks para los objetos con los que se relaciona el SUT (Subject Under Test) que queremos probar. En el siguiente ejemplo tenemos la interfaz de un servicio que se encarga de buscar datos en un sistema externo como una base de datos o un web service, si no queremos hacer un test de integración con ese sistema externo tendremos que hacer un mock de la interfaz Servicio. También tenemos la clase Procesador que se encarga de realizar una tarea sobre los datos que obtiene de un objeto que implemente la interfaz Servicio. La clase Procesador y su método procesar forma el SUT en este ejemplo.

Para hacer el mock de la interfaz en el ejemplo se usa la librería Mockito que nos permitirá definir el comportamiento que queramos de los mocks. En la sección setup es lo que hacemos, se crea el mock de Servicio, se define que cuando se llame al método findDatos del Servicio se devuelvan unos datos en concreto y se crea el Procesador pasándole la referencia al mock del Servicio para que le devuelva los datos que deseemos. Dado que conocemos los datos que le va a devolver el Servicio al Procesador y lo que debería hacer el Procesador conocemos el resutlado que debería devover el método procesar y esto es la comprobación que se realiza en la sección then donde se comprueba lo esperado con lo que devuelve. La sección when es el estímulo que proporcionamos al SUT con la tarea que queremos que haga y que nos servirá para comprobar que devuelve los resultados esperados. Este ejemplo es muy simple pero muestra los principales conceptos con los que realizar la mayoría de teses unitarios que queramos.

import spock.lang.Specification
import org.mockito.Mockito

class MockitoSpock extends Specification {
    def "procesado de elementos"() {
        setup:
        def servicio = Mockito.mock(Servicio.class)
        Mockito.when(servicio.findDatos()).thenReturn([[id:1, letra:'A'], [id:2, letra:'B']])        
        def procesador = new Procesador(servicio)
            
        when:
        def resultado = procesador.procesar() 
    
        then:
        resultado == '1,2'
    }
}

interface Servicio {

    List findDatos()
}

class Procesador {

    private Servicio servicio

    Procesador(Servicio servicio) {
        this.servicio = servicio
    }

    String procesar() {
        def datos = servicio.findDatos()
        def ids = datos.collect {
            it.id
        }
        return ids.join(',')
    }
}

En la siguientes capturas se puede ver el informe generado por un proyecto gradle que contenga estos teses tanto pasando de forma correcta como incorrecta.



Refencia:
http://code.google.com/p/spock/
http://code.google.com/p/spock/wiki/GettingStarted
http://code.google.com/p/mockito/

viernes, 13 de julio de 2012

Extender las validaciones de Apache Tapestry

Apache Tapestry
Si en la entrada anterior expliqué como realizar validaciones de formularios en Tapestry en esta explicaré como crear nuevos validadores, como crear macros de validadores y como cambiar el decorador por defecto para mostrar los errores como deseemos. Estas tres cosas ayudará a las aplicaciones tengan menos código duplicado, el comportamiento sea consistente en toda la aplicación y por tanto que el mantenimiento sea más sencillo cuando haya que hacer cambios.

Crear nuevos validadores

Si los validadores por defecto de Tapestry no nos son suficientes necesitaremos crear algunos especificos para nuestras necesidades. Un ejemplo podría ser un campo donde permitamos introducir una cadena que tiene que pertenecer a un grupo de valores. Como es habitual en Tapestry siempre que necesitamos algo nuevo debemos implementar una interfaz o extender una clase. Para este caso podríamos implementar la interfaz Validator pero también es habitual que dispongamos de una clase abstracta de la que podamos extender e implemente la interfaz que necesitamos. Para un nuevo validador podemos utilizar AbstractValidator. Partiendo del código de uno de los validadores que ya disponemos por defecto, podemos hacer uno nuevo para un valor que tiene que estar dentro de un grupo.

public class In extends AbstractValidator<String, String> {
    public In() {
  super(String.class, String.class, "in");
 }

 public void validate(Field field, String constraintValue, MessageFormatter formatter, String value) throws ValidationException {
  List values = Arrays.asList(constraintValue.split("|"));
  if (!values.contains(value))
   throw new ValidationException(buildMessage(formatter, field, constraintValue));
 }

 public void render(Field field, String constraintValue, MessageFormatter formatter, MarkupWriter writer, FormSupport formSupport) {
  formSupport.addValidation(field, "in", buildMessage(formatter, field, constraintValue), constraintValue);
 }

 private String buildMessage(MessageFormatter formatter, Field field, String constraintValue) {
  return formatter.format(constraintValue, field.getLabel());
 }
}

Una vez que tenemos la clase validadora tenemos que dar a conocer a Tapestry de su existencia, para ello debemos hacer una contribución en el contenedor de dependencias:

// ...en la clase del módulo de la aplicación

    public static void contributeFieldValidatorSource(MappedConfiguration<String, Validato> configuration) {
        configuration.add("in", new In());
    }
...

En este momento la podemos usar en los campos de la siguiente forma:

<t:label for="telefono">: <t:textfield t:id="telefonoField" value="producto" validate="in=coche|casa|televisión|libro" label="Producto"/>

Como cambiar el decorador por defecto

Cuando se producen errores de validación, Tapestry se encarga de decorar los campos para marcarlos como que tienen errores para de tal forma que el usuario pueda actuar en consecuencia, esto se hace sin que tengamos que hacer nada por nuestra parte. El decorador por defecto cuando hay errores marca la etiqueta y el campo en rojo y añade una imagen con una equis roja después del campo. Eso puede no ser lo que queramos ya que la imagen de la equis puede hacer que se desmaqueten las cosas si estamos justos de espacio en la pantalla.

Podemos personalizar la forma en que Tapestry decora los campos por defecto añadiendo elementos antes y despues de la etiqueta del campo y del campo mismo, también podemos añadir una clase de error a la etiqueta del campo y al campo de datos. Para ello debemos extender de la clase BaseValidationDecorator que nos proporciona los métodos para realizar las personalizaciones.

En este ejemplo añadimos una clase CSS a la etiqueta y al campo cuando el valor del campo no es válido. Tendremos que modificar el estilo de esa clase CSS que añadimos («t-error») para modificar su aspecto.

...

public final class AppValidationDecorator extends BaseValidationDecorator {

 private final Environment environment;

 private final MarkupWriter markupWriter;

 /**
  * @param environment
  *            used to locate objects and services during the render
  * @param markupWriter
  */
 public AppValidationDecorator(Environment environment, MarkupWriter markupWriter) {
  this.environment = environment;
  this.markupWriter = markupWriter;
 }

 @Override
 public void insideField(Field field) {
  if (inError(field))
   addErrorClassToCurrentElement();
 }

 @Override
 public void insideLabel(Field field, Element element) {
  if (field == null)
   return;

  if (inError(field))
   element.addClassName(CSSClassConstants.ERROR);
 }

 private boolean inError(Field field) {
  ValidationTracker tracker = environment.peekRequired(ValidationTracker.class);

  return tracker.inError(field);
 }

 private void addErrorClassToCurrentElement() {
  markupWriter.getElement().addClassName(CSSClassConstants.ERROR);
 }
}

Modificar el deccorador por defecto que usa Tapestry por defecto por el nuestro nos requerirá que modifiquemos la factoría que se encarga de devolver los decoradores.

public class AppValidationDecoratorFactory implements ValidationDecoratorFactory {
 private final Environment environment;

 public AppValidationDecoratorFactory(Environment environment) {
  this.environment = environment;
 }

 public ValidationDecorator newInstance(MarkupWriter writer) {
  return new AppValidationDecorator(environment, writer);
 }
}

Solo nos quedaría hacer la contribución al contenedor de dependencias para usar nuestra factoría en vez la de por defecto.

// ...en la clase del módulo de la aplicación

public static void bind(ServiceBinder binder) {
    binder.bind(ValidationDecoratorFactory.class, AppValidationDecoratorFactory.class).withId("AppValidationDecoratorFactory");
}

De esta forma podemos cambiar en todos los formularios de la aplicación la forma en que se muestran los mensajes de error de una forma muy simple. Se puede apreciar que toda la lógica de como se muestran los campos con error está en una única clase.

Macros de validaciones

Si nos encontramos en la situación en que un determinado conjunto de validaciones se repite a lo lago de los campos de la aplicación podemos definir una macro con ese conjunto de validaciones, como por ejemplo podría ser la validación de un campo contraseña.

// ...en la clase del módulo de la aplicación

@Contribute(ValidatorMacro.class)
public static void contributeValidatorMacro(MappedConfiguration configuration) {
      configuration.add("password","required,minlength=5,maxlength=15");
}

Ahora podemos usar el validador password, que comprende que el campo sea requerido, de una longitud mínima de 5 caracteres y de 15 como máximo, como si fuera un validador más de forma declarativa:

<input t:type="textField" t:id="password" t:validate="password" />

O mediante anotaciones:

@Validate("password")
private String password;

Referencia:
http://tapestry.apache.org/forms-and-validation.html
http://tapestry.apache.org/bean-validation.html
Validaciones de datos de formularios con Apache Tapestry
Documentación sobre Apache Tapestry

viernes, 6 de julio de 2012

Validaciones de datos de formularios con Apache Tapestry

Apache Tapestry
En una aplicación web todo dato procedente del usuario a través de un formulario debe ser validado para impedir introducir datos erroneos en la base de datos, posiblemente creando inconsistencias, fallos en la aplicación o peor aún creando problemas de seguridad. En una aplicación web las validaciones pueden ser realizadas en el navegador del cliente mendiante javascript y en el el lado del servidor. Las validaciones deberían hacerse siempre en el lado del servidor ya que las validaciones que pongamos en el lado del cliente podrían no realizarse, un ejemplo de esto es si el cliente tiene el javascript desactivado pero no es la única forma de que nos puedan llegar datos erroneos al servidor. Por tanto, las validaciones del cliente son opcionales y pueden ser útiles para no enviar datos al servidor que podemos detectar que son inválidos en el cliente con lo que evitamos tráfico pero nuestra aplicación no debería confiar únicamente en ellas.

Las validaciones en Tapestry en el servidor pueden ser realizadas de varias formas:
  • De forma declarativa
  • Con anotaciones
  • Mediante eventos

Validaciones de forma declarativa

Quizá esta es la forma más sencilla y se aplica en el momento de usar los componentes de formulario en las plantillas de los componentes.

//...(parte de un archivo .tml)

<t:label for="telefono">: <t:textfield t:id="telefonoField" value="producto" validate="regexp=^[a-zA-Z]{3,9}$" label="Producto"/>
<t:label for="cantidad">: <t:textfield t:id="cantidadField" value="cantidad" validate="required,min=0,max=120" label="Cantidad"/>
...
En el campo cantidad se están aplicando varias validaciones: el campo es requerido (required), el valor mínimo aceptado es 0 (min), el valor máximo aceptado es 120 (max). En el campo producto se usa como validación una expresión regular en la que el valor debe tener entre 3 y 9 caracteres. Otros validadores que incorpora Tapestry por defecto y que podemos usar son maxLength, minLength o email. Si estos validadores con los que viene Tapestry no se ajustan a lo que necesitamos podemos definir nuevos y usarlos de la misma manera.

Validaciones con anotaciones

Las validaciones con anotaciones se aplican en las propiedades que los componentes del formulario usan como valor a mostrar y a cambiar cuando se envía el formulario. Siguiendo el ejemplo anterior con anotaciones sería:

//...(parte de un archivo .java)

@Validate("^[a-zA-Z]{3,9}$")
@Property
private String producto;

@Validate("required,min=0,max=120")
@Property
private Integer cantidad;

...

Validaciones con eventos

Los componentes de Tapestry pueden lanzar eventos (cuyos manejadores siguen la forma on[Evento]From[Componente]). En el caso de los componentes de formulario estos lanzan (entre otros) un evento (validate) para que podamos hacer alguna validación personalizada que no esté entre los validadores por defecto. Nuevamente siguiendo el ejemplo anterior si no dispusiesemos los validadores de Tapestry con anotaciones sería:

//...(parte de un archivo .java de un componente)

void onValidateFromProductoField() throws ValidationException {
 if (!Pattern.matches("^[a-zA-Z]{3,9}$", producto)) {
  throw new ValidationException("El producto no es válido.");
 }
}

void onValidateFromCantidadField() throws ValidationException {
 if (cantidad == null || cantidad < 0 || cantidad > 120) {
  throw new ValidationException("La cantidad no es válida.");
 }
}

...

Validaciones dependientes

En las aplicaciones los datos suelen estar relacionados y algunas combinaciones de datos no son posibles. Los validadores nos permiten comprobar los datos individualmente pero si los datos están relacionados pueden no ser suficientes. Los componentes Form lanzan un evento (validate) para que hagamos este tipo de comprobaciones. Suponiendo que un determinado producto tiene una cantidad mínima a solicitar podríamos validar esta dependencia entre estas propiedades con:

//...(parte de un archivo .java de un componente)

@Component
private TextField productoField, cantidadField;

@Component
private Form datosForm;

@Property
private String producto;

@Property
private Integer cantidad;

void onValidateFromDatosForm() throws ValidationException {
 ValidationTracker tracker = datosForm.getDefaultTracker();

 // Comprobar si los datos recogidos de producto y cantidad son correctos
 if (tracker.inError(productoField) || tracker.inError(cantidadField)) {
  return;
 }

 // Realizar las validaciones dependiente entre los campos
 if (producto.equals("pegatina") && cantidad < 10) {
  tracker.recordError("La cantidad mínima de pegatinas es de 10.");
 }
}

...

Tapestry realiza validaciones en el lado del cliente por defecto en cada unos de los campos cuando se lanza el evento blur de los mismos, si no quisieramos realizar las validaciones en el cliente al componente form debemos pasarle al parametro clientValidation el valor «none». También podriamos hacer que las validaciones se realizasen en el lado del cliente en el momento de enviar el formulario y no en cada blur del componente con el valor «submit». El enumerado ClientValidation define los posibles valores que disponemos.

...
<t:form t:id="form" clientValidation="none">
...
</t:form>
...

Como se puede ver las validaciones son sencillas (basta con añadir un parámetro a un componente), flexibles (hay varias formas de hacerlas según prefiramos) y extensibles (se puede personalizar las validaciones y crear nuevas usándolas como las propias de Tapestry). En otra entrada explicaré como definir nuevas validaciones que podemos usar como las propias de Tapestry, como personalizar la decoración de los campos cuando hay errores en ellos y como crear macros de validaciones si hay una combinación de ellas que usamos de forma repetida.

Referencia:
http://tapestry.apache.org/forms-and-validation.html
http://tapestry.apache.org/bean-validation.html
Extender las validaciones de Apache Tapestry
Documentación sobre Apache Tapestry