viernes, 23 de diciembre de 2011

Feliz navidad y próspero 2012

Ha pasado un año completo para El blog de pico.dev y en febrero del 2012 este blog cumplirá el segundo aniversario. A punto de finalizar el 2011 llega el momento de hacer balance de lo que ha dado de si este año. En este 2011 he publicado 43 entradas, unas pocas menos que una media de una a la semana que me he fijado como objetivo desde hace un tiempo. Aunque aún el año no ha acabado este blog ha tenido cerca de 34.271 visitas, 25.251 visitantes y 55.280 páginas vistas, dicho así parecen muchas pero son «solo» cerca de unas 90-150 visitas diarias. Aún así son considerablemente más que el año 2010 (12.970 visitas, 9.758 visitantes y 20.946 páginas vistas) lo que es casi triplicar el tráfico del año anterior, el hecho de ir acumulando contenido en el blog se nota a medida que pasa el tiempo aunque muy lentamente. Seguro que otros blogs que publican cientos de entradas al cabo del año y tienen a varios usuarios publicando en él tienen bastantes más visitas, aunque mi objetivo no es publicar un gran número de entradas del estilo «Como instalar/actualizar el programa X por cada versión menor (X.X.1) en Ubuntu (u otra distribución)» o «Publicada la versión menor X.X.1 del programa X» que creo que aportan más bien poco sino escribir entradas que compartan alguna información más útil.

En cuanto a la temática de las entradas he escrito más sobre programación que lo que hice en el 2010 y eso creo que va a seguir siendo así aunque no dejaré de escribir de vez en cuando alguna más orientada a (Arch) Linux y software libre.

Si habéis llegado o suscrito recientemente a mi blog (y si aún no lo habéis hecho estáis invitados) os pongo aquí la hemeroteca de las entradas que he escrito durante este año:

Programación:
Unir Apache HTTPD y Tomcat mediante un reverse proxy
Enviar correos electrónicos mediante Java Mail
Implementación de un Comparator genérico en Java con ayuda de Groovy
Servidor web ligero Cherokee, instalación e integración con PHP en Arch Linux
Elegir herramientas para un proyecto Java
Instalar Symfony y Cherokee en Arch Linux
Hola mundo con Symfony 2 y Cherokee
Internacionalización a lenguajes con diferentes formas plurales en Java
Seleccionar el lenguaje (locale) según el dominio en Apache Tapestry
Comparar cadenas en Java ignorando acentos
Integración continua con Jenkins
Configuración de entorno en Java con ayuda de Groovy
Ejemplo sencillo con JavaCC de un analizador léxico y sintáctico
Formatear precios con símbolo de moneda en Java
Internacionalización (i18n) de campos con Hibernate
Convenciones para los literales en archivos .properties en Java

Tapestry:
Componente cache para Tapestry 5
Componente lista para Tapestry 5 (paginable y anidable)
Usar Apache Tapestry 5 con Groovy (u otros lenguajes de la JVM)
Motivos para elegir el framework Apache Tapestry
Peticiones Ajax en Tapestry
Peticiones Ajax en Tapestry (II)
Componente AjaxSpinner para Tapestry 5

(Arch) Linux:
Instalar Plymouth como arranque gráfico en Arch Linux
Poner en color pacman y yaourt
Obtener información del sistema en Linux (con inxi, lspci y hwinfo)
Añadir marcas de agua a imágenes con ImageMagick
Motivos para elegir la distribución Arch Linux
Personalizar GNOME (iconos, temas, extensiones, opciones)
Arte usando texto ASCII con FIGlet

Vídeo promocional sobre Arch Linux
El impuesto Windows
Como sería si Microsoft, Apple y Linux...

Otros:
Un día en la Euskal Encounter 19

Dicho lo cual solo me queda daros las gracias por ser parte de las visitas de este blog y desearos...


Referencia:
http://laventanamuerta.net/haz-llegar-la-navidad-a-tu-escritorio-con-los-fondos-navidenos-de-tux/
http://www.klowner.com/wallpaper/
http://www.youtube.com/watch?v=WdyplU0RVk8

sábado, 17 de diciembre de 2011

Arte usando texto ASCII con FIGlet

Seguramente hayas visto en algunos sitios una figura o texto formado por caracteres ASCII, probablemente no hayan sido creadas a mano con la inspiración de un artista, hay herramientas incluso usables desde la web que permiten generarlas. FIGlet es una de ellas que permite crear una figura ASCII a partir de un texto. Su instalción en Arch Linux es tan simple como instalar el paquete figlet con:

$ yaourt -S figlet

En la propia página de figlet podemos encontrar enlaces a varios conjuntos de fuentes a aplicar al texto que queramos generar. También podemos ver algunos ejemplos.

FIGlet tiene más opciones pero la más habitual tal vez sea indicar la fuente a usar y el mensaje del que queremos su representación en arte ASCII.

$ figlet -w 200 "El blog de pico.dev"
 _____ _   _     _                   _              _               _            
| ____| | | |__ | | ___   __ _    __| | ___   _ __ (_) ___ ___   __| | _____   __
|  _| | | | '_ \| |/ _ \ / _` |  / _` |/ _ \ | '_ \| |/ __/ _ \ / _` |/ _ \ \ / /
| |___| | | |_) | | (_) | (_| | | (_| |  __/ | |_) | | (_| (_) | (_| |  __/\ V / 
|_____|_| |_.__/|_|\___/ \__, |  \__,_|\___| | .__/|_|\___\___(_)__,_|\___| \_/  
                         |___/               |_| 

$ figlet -f letters.flf -w 200 "El blog de pico.dev"

EEEEEEE lll  bb      lll                      dd                 iii                        dd                
EE      lll  bb      lll  oooo   gggggg       dd   eee   pp pp         cccc  oooo           dd   eee  vv   vv 
EEEEE   lll  bbbbbb  lll oo  oo gg   gg   dddddd ee   e  ppp  pp iii cc     oo  oo      dddddd ee   e  vv vv  
EE      lll  bb   bb lll oo  oo ggggggg  dd   dd eeeee   pppppp  iii cc     oo  oo ... dd   dd eeeee    vvv   
EEEEEEE lll  bbbbbb  lll  oooo       gg   dddddd  eeeee  pp      iii  ccccc  oooo  ...  dddddd  eeeee    v    
                                 ggggg                   pp

Los textos ASCII generados por FIGlet nos pueden ser de utilidad en los sitios en los que solo podamos usar caracteres de texto como en correos electrónicos que no lleven formato html o en los archivos de log de una aplicación.

Una vez que tenemos nuestro arte ASCII podemos darle color si genera su salida en la terminal y pretendemos usarlo en una aplicación Java.

Referencia:
http://www.figlet.org/
http://patorjk.com/software/taag/
http://www.network-science.de/ascii/

viernes, 9 de diciembre de 2011

Como sería si Microsoft, Apple y Linux...

¿Como sería si Microsoft, Apple y Linux se decicasen a la industria automovilística y vendiesen motores y coches conociendo sus políticas y formas de comercializar su software y productos?

set small_troll_mode=on

Empecemos por Microsoft

Microsoft
Algunos modelos de sus motores son conocidos y recordados por provocar fallos en el coche que son instalados [1], algunos no más molestos como que las ruedas se desinchan de vez en cuando o el aire acondicionado no funciona a veces si a la vez tienes puesta la radio y algunos graves como que el motor se apaga de repente o deja de frenar cuando lo necesitas que provocan pequeños accidentes y pequeñas perdidas de memoria en el conductor a los que se les queda la cara de un tono azulado después de los hechos por el susto.

Aproximádamente cada segundo martes del mes [2] tienes que llevar el coche al taller para hacer una revisión de algunas partes del coche por fallos de seguridad detectados en diversas partes del mismo, no sabes muy bien que van a revisar pero oye si lo dice Microsoft por algo será. Otros problemas de seguridad conocidos y explotados por los ladrones para acceder al interior no son resueltos pasados meses e incluso años siempre a criterio de Microsoft, ellos saben lo que es importante y bueno para sus usuarios.

Los coches vienen con un montón de extras inútiles [3] que no quieres como una alerón hortera, una luces de neon en los bajos o un sistema de alarma que deja de funcionar a los 30 días si no pagas la licencia. Todo ello hace que el coche nuevo y recien salido de fábrica circule a una velocidad máxima de 80 km/h cuando debería alcanzar los 120 km/h y a veces sin ningún motivo aparente se reduzca a 50 km/h pasados unos meses.

No puedes abrir el capó del coche para para ver como es el motor, y por tanto no puedes cambiarle de aceite, la batería o algunas piezas para conseguir una mayor potencia, fiabilidad, seguridad o menor consumo de combustible.

Las administraciones públicas que tienen enormes flotas de automóviles comprados a Microsoft [4] se gastan cada año millones de euros en renovar licencias para usarlos, ya que el coche en realidad no lo compran solo les da un licencia de uso. Pueden comprar coches con motor Linux que algunos no tienen costes de licencias pero los funcionarios están acostumbrados a los coches con motor Windows, es el que usan como particulares.

Por lo visto debe de haber una forma de duplicar un motor rápidamente, las personas lo utilizan para instalarle a sus coches la última versión del motor Windows que siempre dicen trae nuevas prestaciones. Saben que esto no es legal por la licencia de uso del motor y les llaman piratas, pero a la vez Microsoft lo consiente, sabe que cuando vayan a sus trabajos sus empresas tendrán que pagar ya que no estarán acostumbrados a ningún otro tipo de motor. Sino puede obligarles a comprar la licencia como particulares al menos los convierte en adictos a sus motores, piensan que es mejor que usen uno de ellos pirateado que uno de la competencia [5], y así están los funcionarios de muchas las administraciones públicas.

En alguna ocasión el navegador GPS de Microsoft venía ya instalado en el coche junto con el motor. Sin embargo, la Unión Europera a partir de una denuncia del fabricante de navegadores Opera [6] dictaminó que esto era un abuso de posición dominante ya que impedía al resto de fabricantes de GPS colocar sus navegadores y dificultaba la libre competencia. Microsoft argumentaba que el navegador GPS era parte inseparable de su motor y que sin él en no funcionaba. Finalmente, Microsoft se vió obligada a darle al usuario la posibilidad de seleccionar su navegador GPS en una Ballot Screen [7], ya estaba escarmentada de anteriores multas por valor de 500 millones de euros por anteriores litigios [8].

Pasados 5 años, con suerte, tienes que comprar otro coche porque el que que tienes ya no puede cargar con los trastos que llevas en él, no sabes por qué ya que los utilizas para hacer lo mismo que hace 5 años solo son un poco más modernos, te han comentado que puede ser debido a la obsolescencia programada. Tal vez solo quieres la carrocería que ya tienes el motor de tu anterior coche que es antiguo pero que todavía puede funcionar en el nuevo o tal vez te plantes instalar un motor basado en Linux ya que conoces alguna distribución que te lo dá sin ningún coste, ya estas cansado de los defectos de los motores Windows, el comercial sorprendido te dirá que eso no es posible que a ellos los fabricantes de coches no les venden solo carrocerias, ya les dan el motor de Windows preinstalado y así es como lo venden, a esto algunas personas lo conocen como el impuesto Windows.

El turno de Apple

Apple
Al contrario que Microsoft, Apple no solo fabrica motores sino también la carrocería y tampoco podras adquirirlos por separado. La producción se ha deslocalizado a China ya que allí hay mano de obra más barata, abundante y los trabajadores de esas plantas tiene menos derechos laborales que los trabajadores occidentales. Sin embargo, cuando sale algún caso de que algún empleado de esas fábricas llega a suicidarse [1] por las condiciones en que se encuentra, en Apple, dicen que van a cancelar relaciones contractuales si los contratistas no ponen remedio en un intento de que el asunto no afecte a su marca [1].

Apple no fabrica coches de bajo coste, según ellos no saben hacerlo sin que sean una basura [2], (¡tragaros eso fabricantes de utilitarios PC!), asunto arreglado ya tienen una razón para vender sus coches a esos precios, a pesar de utilizar las mismas piezas, son sensiblemente mas caros que los coches PC. A quien no le importa pagar el precio de un coche Mac no le importa sacar de nuevo y a menudo la tarjeta de crédito para pagar el precio que tienen sus accesorios y para pagar los añadidos de la nueva App Store, motivo por el cual esta tiene más éxito que el mismo concepto en otras marcas.

Cada cierto tiempo sacan nuevas actualizaciones para sus motores con un precio de tan solo X con 99, tipica regla comercial para fijar precios y hacer pensar a los clientes que estan pagando menos en un intento por confundirles y demostrando que no les importa contar mentiras piadosas, parece que funciona.

Tampoco puedes abrir el capo para ver como es el motor ni modificarlo ni puedes saber si te han puesto un sistema de segimiento y rastreo CarrierIQ con el que puedan conocer en que sitios has estado o analizar y recopilar información tuya sin tu consentimiento ni saber con que fines [3]. Cuando los alarmados conductores Mac se informan de la existencia de dichos dispositivos instalados en sus vehículos les comentan que en el próximo modelo dejarán de utilizarlos, prometen hacer borrón, cuenta nueva y asunto arreglado, aquí no ha pasado nada ¡estos no son los androides que andáis buscando!. Algunas personas tachadas de poco menos que locos ya advertían de que estas funciones malévolas en motores privativos podían estar presentes [4].

Dicen que sus productos son respetuosos con el medio ambiente, solo utilizan piezas que lo sean, son eléctricos e híbridos nada de utilizar la obsoleta y contaminante tecnología de combustión. Sus motores son lo último y son productos deseados, su maquinaria de marketing se encarga de hacer que asi sea. Tienen una carrocería cuidada pero solo en dos colores, negro y blanco, este último les costó sacarlo al mercado varios meses más tarde que el primero parece ser que por un problema en los focos [5].

Apple es distinta, en los lanzamientos de sus productos algunos clientes pueden pasarse una noche esperando en la puerta del concesionario a que abran al día siguiente para gastarse su dinero y comprar lo último de la marca [6]. Para que no decaiga el ánimo los empleados del concesionario los reciben con aplausos y vitores cuando entran.

Ahora Linux

Linux
Los motores Linux son libres y podrás utilizarlos con cualquier propópsito. Su licencia te da el derecho a obtener sus planos, y si tienes los conocimientos puedes estudiarlos, modificarlos y redistribuir tu mejoras con tus vecinos para ayudarles, estas son las 4 libertades básicas [1]. Algunas personas han logrado proporcionar la mayor potencia, fiabilidad, seguridad o menor consumo de combustible que no se puede hacer con motores Windows o Max OS X. Con los accesorios en su mayoría tambien libres pasa otro tanto de lo mismo.

Han demostrado su fiabilidad y ser muy versátiles y se pueden instalar además de coches, en otro tipo de vehículos como motos, camiones, barcos y aviones e incluso en micromotos como Kutxashares. Pueden utilizar no solo válvulas x86 sino también de las marcas ARM, MIPS, OpenRisc, PowerPC y otras menos conocidas [2].

No existen concesionarios Linux y no lo encontrarás en el resto de concesionarios PC, se piden por internet y en la mayoria de los casos su coste es nulo, a pesar de ello la mayoría de personas PAsan DE FOllones y se quedan con lo que les dán preinstalado aceptando sus defectos. Su negocio no se basa principalmente en vender licencias sino servicios, como RedHat, IBM y Canonical. Dado que cualquier persona puede proponer mejoras construyen comunidades, el avance conseguido por esas comunidades luego es aplicado a sus productos comerciales y el avance en los productos comerciales acaba llegando también a las comunidades de modo que todo el mundo se vea beneficiado. Alrededor de los motores Linux y sus accesorios se crean talleres mecánicos y puestos de trabajo localizados [3] con el objetivo de prestar esos servicios en su ámbito local.

El reciente concepto de App Store y Market para Apple y Microsoft es algo que ya tenian los distribuidores Linux desde hace ya mucho tiempo.

Estos motores tampoco están exentos de fallos y a veces a sus coches también se les desinchan las ruedas pero por lo general una vez conocidos son resueltos más rápidamente que en los coches Microsoft y Apple. Algunos fabricantes de accesorios mecánicos no dan soporte para estos motores y a veces no funcionan correctamente aunque los más comunes ya no suelen presentar problemas, no es como en los años 90 pero la leyenda urbana sigue extendida.

Algunas versiones de los distribuidores requieren que los usuarios monten las piezas ellos mismos como en Arch Linux y Gentoo, otros distribuidores lo ponen tan fácil o más que no tienen nada que envidiar a los coches Microsoft, es el caso de Ubuntu, Linux Mint y Fedora. Tener esta amplia variedad de modelos de motores Linux hace que a alguna gente le cueste decidirse por uno, tratan de buscar «el mejor», pero se equivocan, en Linux no hay «el mejor» quizá sí el mas adecuado para cada tipo de conductor. Para los que tienen el primer motor Linux posiblemente las opciones más adecuadas serían UbuntuLinux MintFedora o openSUSE. Para los usuarios que ya llevan un tiempo usándolos quizá les parezca más adecuada para sus necesidades Arch Linux aunque hay muchos otros modelos [4].

Pasado el tiempo si tratas bien el coche y no lo metes por cualquier carrtera no notarás una degradación de prestaciones y funcionará a los mismos 120 km/h del primer día.

set small_troll_mode=off

Por suerte ni Microsoft, ni Apple, ni Linux se dedican a la fabricación de motores ni coches pero piensa si lo hicieran si aceptarías que instalasen en tu coche un dispositivo de seguimiento sin que te informasen de ello o si no te parecería ridículo que hubiese gente esperando una noche en la puerta de un concesionario para comprar un nuevo modelo de Renault o si tu coche recien comprado te viene con un motón de accesorios que no quieres o si... tal vez tampoco los coches y los ordenadores no sean lo mismo pero... ¿si algunas de estas cosas no las aceptarías en tu coche u otros ámbitos por que las aceptas en tu ordenador? Tú decides.

Artículos relacionados:
El impuesto Windows
De Windows a Arch Linux
Guía instalación Arch Linux (Parte I, instalación base)

Referencia:
Microsoft
[1] Buscar: Windows Millenium y Windows Vista
[2] Buscar: windows martes de parches
[3] Buscar: windows programas preinstaldos
[4] Buscar: windows administracion pública
[5] Buscar: Las versiones pirateadas de Windows frenan el avance de Linux
[6] Buscar: windows explorer denuncia opera
[7] Buscar: windows ballot screen
[8] Buscar: microsoft multa union europea

Apple
[1] Buscar: apple foxconn suicidios
[2] Buscar: apple ordenadores basura 500
[3] Buscar: apple carrieriq
[4] Buscar: richard stallman
[5] Buscar: apple iphone blanco retrasado
[6] Buscar: apple colas tiendas

Linux
[1] Buscar: libertades software libre
[2] Buscar: linux arquitecturas soportadas
[3] Buscar: empresas software libre euskadi
[4] Buscar: linux distribuciones

viernes, 2 de diciembre de 2011

Convenciones para los literales en archivos .properties en Java


Java
La internacionalización (i18n) y la localización (i10n) en las aplicaciones Java ya sean de escritorio o aplicaciones web se suelen hacer a través de archivos .properties obtenidos a partir de la clase ResourceBundle que contienen los literales para cada uno de los idiomas que soporta la aplicación, a cada literal se le asocia una clave que será el medio por el cual se identificará a la hora de recuperarlos.

Un extracto de un archivo típico suele tener el siguiente aspecto:

...
#Combos
comboDepartamentos=Todos los departamentos
comboAreas=Todas las áreas
comboRepositorios=Todos los repositorios
comboAplicaciones=Todas las aplicaciones
comboModulos=Todos los módulos
comboProgramas=Todos los programas
...
#Mantenimiento de Repositorios
mr.aceptar=Aceptar
mr.buscar=Buscar
mr.cancelar=Cancelar
mr.confirmarEliminar=Los repositorios seleccionados pueden tener información relacionada que será eliminada en cascada. ¿Desea eliminar los repositorios seleccionados y la información relacionada?
mr.registroEliminado=El registro ha sido eliminado por otro usuario.
mr.validacion.nombreExiste=Existe otro repositorio con el mismo nombre.
...
#Mantenimiento de Aplicaciones
ma.aceptar=Aceptar
ma.buscar=Buscar
ma.cancelar=Cancelar
...
#Mantenimiento de Módulos
mm.aceptar=Aceptar
mm.buscar=Buscar
mm.confirmarEliminar=Los módulos seleccionados pueden tener información relacionada que será eliminada en cascada. ¿Desea eliminar los módulos seleccionados y la información relacionada?
...

Como se puede observar las claves de los literales de cada pantalla están identificados por un prefijo (mr, ma, mm) con el objetivo de agrupar los de cada pantalla, algunos no tienen prefijo como los de la sección #Combos. A los literales muy largos como mr.confirmarEliminar y mm.confirmarEliminar se les asigna una clave más corta que el propio literal y que tendremos que asignar para identificarlo.

Estas convenciones tienen algunos problemas. Uno de ellos es que hay literales que son muy comunes en la aplicación y estarán varias veces repetidos como Aceptar, Buscar, Cancelar lo que implica que los traductores tendrán que tarducirlo varias veces y a los programadores mantener mayor número de literales, además de ralentizar el trabajo ¿tiene sentido pedir a los traductores traducir el mismo literal varias veces? No. Los literales largos como mr.confirmarEliminar y mm.confirmarEliminar tienen el problema de que sus claves son poco representativas de su literal siendo vistos en el archivo de código fuente donde se use, lo que hace que leerlo sea poco significativo y representativo de los textos que luego se muestran.

Para solventar estos problemas y después de varios años de experiencia trabajando con archivos de literales en Java prefiero seguir otras convenciones. Según estas los archivos tendrían el siguiente aspecto:

...
Aceptar=Aceptar
Buscar=Buscar
Cancelar=Cancelar
...
El_registro_ha_sido_eliminado_por_=El registro ha sido eliminado por otro usuario.
Existe_otro_repositorio_con_=Existe otro repositorio con el mismo nombre.
Los_repositorios_seleccionados_pueden_tener_información_relacionada_=Los repositorios seleccionados pueden tener información relacionada que será eliminada en cascada. ¿Desea eliminar los repositorios seleccionados y la información relacionada?
...
Los_módulos_seleccionados_pueden_tener_información_relacionada_=Los módulos seleccionados pueden tener información relacionada que será eliminada en cascada. ¿Desea eliminar los módulos seleccionados y la información relacionada?
...
#Combos
Todas_las_aplicaciones=Todas las aplicaciones
Todas_las_areas=Todas las áreas
Todos_los_departamentos=Todos los departamentos
Todos_los_modulos=Todos los módulos
Todos_los_programas=Todos los programas
Todos_los_repositorios=Todos los repositorios
...

Como se puede ver para los literales cortos la clave será el propio literal en nuestro idioma nativo sin ñ o tildes, con esto al leer el archivo de código fuente sabremos cual es el texto exacto que se mostrará y no tendremos que estar inventándonos constantemente identificadores para los literales. Los literales cortos pero formados por varias palabras tendrán un «_» para representar el espacio. Las claves de los literales largos son el literal acortado con suficientes palabras para que no se repitan, les añado un «_» al final para saber que se ha acortado, el objetivo de acortarlo es que con unas cuantas palabras ya podemos hacernos una idea del texto que se muestra. Además los literales están ordenados alfabéticamente por la clave para encontrarlos más fácilmente. No digo que estas reglas no puedan tener excepciones para algunos casos pero siguiéndolas para la mayoría evitaremos que según vaya pasando el tiempo estos archivos se conviertan en un problema de mantenimiento, cuando en una aplicación tenemos 2000 o más literales, créeme, puede llegar a serlo.

miércoles, 23 de noviembre de 2011

Apache Tapestry 5.3 publicado (Notas de publicación)

Java
La versión 5.3 de del framework Apache Tapestry para el desarrollo de aplicaciones web dinámicas en Java ha sido publicada y así ha sido anunciada en la lista de correo. Continúa con la línea de las series 5.x e incorpora muchas novedades, mejoras y correcciones de errores, casi 350 peticiones han sido resueltas. Varias de estas nuevas características se suman a algunos Motivos para elegir el framework Apache Tapestry. Las lista de novedades es la siguiente:
Apache Tapestry

Documentación
La ya buena documentación que existía ha sido mejorada y ampliada con nuevas secciones. Así tenemos un tutotial con el que los usuarios que estén empezando les resulte más fácil la tarea, incluye las herramientas a utilizar, las dependencias, como emepezar una aplicación rápidamente utilizando el archetype de maven y un par de ejemplos sencillos para empezar a hacer algo asi como usar Tapestry con Hibernate. También se ha añadido una sección de recetas sobre como hacer cosas habituales en muchas aplicaciones. También se ha añadido una sección de preguntas frecuentes y unas páginas de consulta para características de uso habitual (Application Module Class Cheat SheetComponent Cheat Sheet).


Alertas
Tapestry ahora tiene un mecanismo central para manejar las alertas de usuario. Esto incluye el servicio AlertManager y el componente Alerts. Simplmente añade el componente Alerts a tu layout estandar y Tapestry se encarga del resto. Las alertas se pueden añadir tanto en las peticiones tradicionales como en las peticiones Ajax y pueden ser transitorias (se muestran durante unos segundos), normales o fijas (se mantienen entre peticiones hasta que el usuario las elimina expresamente haciendo clic). Las alertas pueden ser de tres severidades: info, advertencia (warn) y error. La apariencia puede ser personalizada sobreescribiendo las reglas CSS de Tapestry.

Salida de comentarios
Ahora es posible que Tapestry emita comentarios de salida. Esto son comentarios (como <!--BEGIN Index:loop (context:Index.tml, line 15)-->) que ayudan a depurar las etiquetas de salida en la parte del cliente. Esto se habilita para todas las peticiones que usen el símbolo de configuración «tapestry.component-render-tracing-enabled» y pueden ser añadidas a cualquier petición añadiendo el parámetro «t:component-trace=true» a la URL. Esto aumenta significativamente el tamaño de las etiquetas de salida pero puede ser muy útil con layouts compejos para determinar que componente es el responsable de cada porción de la página.

Contribuciones de servicio adaptables
Al hacer contribuciones a un servicio ahora no se está limitado a contribuir un valor que sea asignable al tipo asociado con la configuración. Objetos de cualquier tipo pueden ser contribuidos y el servicio TypeCoercer será usado para transformar el valor al tipo de la configuración (Ver documentación TypeCoercer).

Mejoras en la depuración de componentes
Dada la forma en como Tapestry instrumentaba las páginas y componentes usar un depurador ha sido dificultoso con las clases de páginas y componentes, cualquier campo mutable muestra su valor por defecto en el depurador independientemente de que haya sido escrito en el campo o leído de él. En el modo desarrollo de Tapestry 5.3 los valores ocultos leen de y escriben a esos campos en los propios campos (esto también ha sido corregido en la rama de mantenimiento 5.2.5). Este ocultamiento no ocurre en el modo producción para evitar potenciales pérdidas de memorias.

Recarga deshabilitada en producción
Desde ahora no se buscan cambios en los archivos de clases, plantillas o catálogos de mensajes en el modo producción. Es asumido que las aplicaciones Tapestry son empaquetadas en archivos WAR en producción y que cambiar el archivo WAR causa que el contenedor de servlet redespliegue la aplicación entera. Este cambio mejora el rendimiento y reduce el consumo de memoria en las aplicaciones.

JavaDoc Tapestry
Tapestry ahora incluye una nueva librería , tapestry-javadoc, que reemplaza el antiguo componente de informes basado en Maven. Simplemente poniendo una anotación @tapestrydoc en el JavaDoc de los componentes Tapestry generará la documentación completa como parte del JavaDoc... no mas cambios entre el JavaDoc y el informe del componente generado por Maven y no mas dependencia en Maven para la documentación de los componentes. Los estilos del JavaDoc han sido adaptados a los colores del proyecto.


Template skinning
Tapestry añade la posibilidad de personalizar y/o tematizar («skin and/or theme») las páginas o componentes. Es una extensión de como Tapestry gestiona las páginas por locale pero añade ejes definidos por aplicación además de reglas para encontrar recursos. Ver ComponentResourceSelector y ComponentRequestSelectorAnalyzer.

También hay un nuevo componente Dynamic el cual usa una plantilla externa (no una plantilla de Tapestry) que puede ser seleccionada en tiempo de ejecución.

Mediante esta funcionalidad ahora la plantilla que se aplica para una página o componentes no solo vendrá determinado por el locale de la aplicación sino que podremos introducir más variables en la elección. Una de ellas podría ser el tipo de cliente, como por ejemplo un dispositivo móvil del que hace la petición u otros criterios para asi mostrar contenido más adaptado o personalizado.

En el siguiente enlace de puede ver una demostración de como aplicar la funcionalidad Template Skinning.

Evento de formulario cancelado
Los componentes de formulario ahora reconocen cuando el formulario de la parte cliente fue cancelado. Un nuevo evento «cancelled» es disparado al principio del proceso de envío lo cual permite a la página saltar todas las actualizaciones y validaciones en el servidor cuando sea lo deseado.

Restricciones implicitas en OrderedConfiguration
Al usar OrderedConfiguration.add() sin restricciones Tapestry ahora implicitamente ordenará el elmento añadido después del elemento añadido anteriormente dentro del mismo método. En versiones anteriores esos elementos eran añadidos sin restricciones. Esto hace más fácil contribuir un grupo de elementos relacionados con un orden implicito.

Nuevas validaciones de las clases de componente
Tapestry incluye nuevas validaciones de clases de componente para ayudar a reducir algunos errores comunes. Ahora comprueba que los componentes referenciados por un manejador de eventos concuerde con un componente definido en la plantilla... esto identifica rápidamente errores tipográficos en los nombres de métodos. Esto es, si la firma de un manejador de evento es «onFormularioRegistroSubmit()» se comprueba que exista en la plantilla un componente de id «FormularioRegistro». Esta comprobación puede ser deshabilitada con un símbolo de configuración de tal modo que aplicaciones 5.2 existentes que tengan este tipo de errores todavía puedan funcionar (esto es, tendrá manejadores de eventos muertos que nunca serán invocados).

Carpeta de aplicación
Tapestry ahora puede ser configurado para ejecutarse dentro de una carpeta, lo cual puede ser útil cuando se ejecuta dentro de una aplicación web que contiene otros servlets y filtros como una forma de prevenir conflictos. En vez de ejecutarse en la raiz del contexto de la aplicación.

Mejorado el arquetipo de inicio rápido
El arquetipo de Maven ha sido actualizado y ahora muestra varias nuevas funcionalidades de uso común. Además añade soporte para Gradle (Referencia). La forma de usarlo desde la linea de comandos es:

$MAVEN_HOME/bin/mvn archetype:generate -DarchetypeCatalog=http://tapestry.apache.org/

Páginas blancas
La nueva anotación @WhitelistAccessOnly marca una página como accesible solo por una lista blanca de clientes. Las reglas para la lista son extensibles. La regla por defecto de la lista blanca es que sea accedida desde 127.0.0.1. Usa esta anotación en páginas que pueden exponer datos sensibles como paneles informativos incorporados.

Catálogo de página
Las aplicacicones Tapestry incluyen una página PageCatalog la cual lista todas las páginas cargadas de la aplicación con detalles acerca de su tiempo de construcción y número de componentes. La página requiere acceso en la lista blanca (ver en comentario anterior). Muestra alguna información que solo está disponible en el modo desarrollo. PageCatalog puede ser usada para cargar todas la páginas de la aplicación lo cual es útil para localizar problemas en las páginas... y es especialmente útil al aumentar de una versión anterior de Tapestry.

Otra página de la que podemos obtener información acerca de los servicios de la aplicación es ServiceStatus donde podremos ver el estado de los mismos.





Logging en la parte ciente JavaScript
La consola en la parte cliente Blackbird ha sido eliminada. En su lugar están los mensajes de la consola flotante combinada con el logging a la consola de Firebug o WebKit. Tapestry no captura más las excepciones de JavaScript de inicialización de modo que pueden ser informadas apropiadamente en la consola nativa. Estos cambios deberían hacer la depuración de JavaScript en el lado cliente mucho más fácil.

El informe de error es una de las cosas buenas de Tapestry. Con esta nueva versión esta característica se ha extendido a las peticiones Ajax con lo que cuando se produzca una excepción en el servidor se nos mostrará un informe en una ventana emergente con un contenido similar a las excepciones de las peticiones normales. En este screencast se puede ver en funcionamiento.


Otra lista de novedades no mencionadas en la notas de lanzamiento oficial:

Nuevos componentes
Se han añadido unos cuantos nuevos componentes al los ya presentes en el core. Estos son:
  • KaptchaImage / KaptchaField: Permite evitar el spam en los formularios.
  • Tree: componente ábol que pemite mostrar información jerárquica con nodos expandibles, contraibles y seleccionables. Es personalizable y basado en Ajax.
  • Checklist: componente de selección multiple consistente en diferentes checkbox.




Nueva capa de abstracción JavaScript
En estos momentos Tapestry está atado a Prototype y Scriptaculous por compatibilidad con anteriores versiones (aunque se puede utilizar perfectamente junto con otros frameworks javascript como jQuery). En un futuro la nueva capa capa de abstracción permitirá usar cualquier librería JavaScript. La transición se completará en la versión 5.4.

Soporte Ajax mejorado
La clase MiltiZoneUpdate ha sido marcada como obsoleta en favor del servicio AjaxResponseRenderer. Esta nueva clase permite actualizar una zona con el contenido de Block, Component u otro objeto capaz de ser convertido a RenderCommand. Permite además importar una librería JavaScript como parte de la respuesta Ajax y añadir una llamada a una función en el lado del cliente, útil y a veces necesario para procesar la petición Ajax correctamente.

Integración con JSR 330 (Dependency Injection for Java)
Empezando desde Tapestry 5.3 es posible usar las anotaciones que define la especificación JSR-330 en la capa de servicios de tal modo que no dependan de Tapestry.

Integración JPA2
Integración nativa con JPA2 proporcionando compatibilidad hacia atrás en futuras versiones y posibilidad de usar múltiples unidades de persistencia en la misma aplicación. Permite configurar JPA sin necesidad de XML.

Herramienta construcción, Gradle
Tapestry ha pasado de utilizar Maven a usar Gradle como herramienta de contrucción y compilación para el propio proyecto. Gradle aporta nuevas funcionalidades, simplifica otras y evita algunos de los aspectos menos deseables de Maven.

Actualización Jumpstart
Actualizada la aplicación de referencia Jumpstart a la versión 5.3 con numerosos ejemplos que muestran funcionalidades, y con la posibilidad de ver el código fuente de esos ejemplos.


Otras
  • Incorporación de Underscore.js que añade programación funcional a JavaScript (en modo no conflicto).
  • Coercion de String-a-Enum sin necesidad de hacer una contribución en el contenedor IOC.
  • Compresión JavaScript y CSS a través de de la librería YUICompressor library.
  • Más rápido y más eficiente en uso de memoria.



Cambios disruptivos
  • Tapestry depende ahora del Servlet API versión 2.5 con lo que la versión mínima para ejecutarlo ahora será Tomcat 6.0+.
  • Actualizado Prototype a la versión 1.7.
  • Las plantillas sin son tratadas como si tuvieran el doctype de HTML5 () pudiendose usar entidades como © o &nbsp; sin ver errores de parseo de anteriores versiones.
Algunas interfaces y APIS en Tapstry 5.3 serán eliminadas en Tapestry 5.4 o posteriores. Estas incluyen:
  • Entorno RenderSupport (reemplazada con el entorno JavaScriptSupport).
  • El objeto MultiZoneUpdate, reemplazada con AjaxResponseRenderer.
  • El servicio ClassFactory y la interfaz ClassFab (reemplazada con el servicio PlasticProxyFactory y la interfaz PlasticClass).
  • La funcionalidad "suppress redirects", que permitía a los eventos de los componentes responder directamente con HTML, como en Tapestry 4.
Salvo que se usen características marcadas como obsoletas en Tapestry 5.2 en la nueva versión la actualización no requerirá más que actualizar a las nuevas dependencias.

Y una lista larga de corrección de errores y mejoras. La lista completa está en las notas de publicación de tapestry 5.3 oficiales y aquí una lista de novedades en inglés.

Referencia:
http://tapestry.apache.org/index.html
http://tapestry.apache.org/download.html
Documentación sobre Apache Tapestry
Motivos para elegir el framework Apache Tapestry

viernes, 18 de noviembre de 2011

Personalizar GNOME (iconos, temas, extensiones, opciones)

GNOME
Arch Linux
Una vez que tenemos instalado nuestro sistema una de las primeras cosa que solemos hacer es personalizarlo cambiando el fondo de pantalla, instalando un pack de iconos o cambiando el tema de las ventanas. Aunque GNOME con su versión 3.0 o 3.2 ha perdido algunas funcionalidades y algunas posibilidadesd de configuración la herramienta gnome-tweak-tool permite configurar las opciones más habituales que querrán la mayoría de los usuarios. Para instalarla en arch linux:

# pacman -S gnome-tweak-tool

Una vez instalada veremos su icono en el menu Aplicaciones > Accesorios > Configuración avanzada.

En la pestaña Escritorio podemos configurar algunos comportamientos como mostrar la visibilidad de los iconos Computadora, Papelera, Carpeta Home.


En Extensiones de GNOME-Shell podemos instalar nuevas extensiones que descarguemos aunque es mejor opción hacerlo con el gestor de paquetes de nuestra distribución. Hay una lista de extensiones disponibles para Arch linux que pueden instalarse individualmente y que modifican el comportamiento por defecto del shell de gnome.


En GNOME-Shell podemos personalizar la información que se muestra en el reloj, las acciones a realizar al cerrar la tapa del portátil y los botones que queremos en las ventanas.


En Tema podemos configurar los temas del puntero del ratón, iconos, tema de los componentes GTK+ y de las ventanas. Uno de los temas de iconos más conocidos es el tema faenza disponible en Arch linux en el repositorio AUR con el nombre de paquete faenza-icon-theme. Aunque temas de iconos hay muchos.


En Tipografías se pueden cambiar las fuentes y tamaños de la fuente usada en el sistema, de los documentos, en el título de la ventana y alguna otra opción.


En la opción Ventanas se pueden modificar algunos comportamientos al hacer clic en las ventanas.


La falta de configuración de la versión 3 de gnome es una de las críticas que se le ha hecho pero a medida que vaya evolucionando en versiones lo hará al mismo tiempo en funcionalidades y opciones de configuración, es cuestión de tiempo.

Yo no soy de tener una personalización extrema del escritorio prácticamente tengo con la configuración por defecto exceptuando el tema de iconos faenza.



viernes, 11 de noviembre de 2011

Internacionalización (i18n) de campos con Hibernate

Java
Si estamos desarrollando un sitio web que soporta varios idiomas necesitaremos internacionalizar (i18n) los literales que aparecen en él. Esto incluye también tenerlo en cuenta y solucionarlo en los nombres, descripciones y textos que guardamos en la base de datos de las entidades de dominio y que puedan aparecer en en el html generado.

Hibernate
La primera solución que se nos ocurre para las entidades de dominio es crear un campo por cada idioma y concepto a internacionalizar. Sin embargo, esto tiene el problema de que en la base de datos el número de campos crecerá rápidamente y si tenemos muchos elementos a internacionalizar y muchos idiomas el número de campos puede ser un problema por el tamaño de fila, y aunque los limites en postgresql y los límites de mysql son elevados y más que suficientes para la mayoría de los casos, con la solución anterior podemos llegar a ellos o estar peligrosamente cerca.


Diagrama entidad relación de base de datos
Primera solución (variación número columnas)

Para evitarnos problemas las buenas prácticas de diseño de las bases de datos dicen que las tablas han de estar normalizadas y siguiengo la solución anterior tendríamos variaciones en el número de columnas con lo que inclumpliríamos la primera forma normal (1FN). ¿Pero porque inclumplir esta forma normal puede ser un problema? Porque en el momento que tengamos que soportar un nuevo idioma deberemos modificar el esquema de la base de datos añadiendo un campo por cada concepto de las entidades que haya que internacionalizar. Y hacer esto en una base de datos que está en producción puede ser una fuente de problemas y un peligro. Además, en tablas con muchos registros (de unos cuantos miles o cientos de miles) añadir una columna puede ser muy lento pudiendo llegar a horas o más tiempo cosa que dependiendo del proyecto no es viable al poder tener que realizar paradas largas por mantenimiento.

La solución que voy a explicar a continuación es crear una tabla Diccionario la cual relacionaremos con las tablas de los datos de dominio (Producto) y una tabla Traduccion que relacionaremos con la tabla Diccionario y que contendrá las traducciones para cada idioma. La tabla Traduccion contendrá tres campos la clave primaria del diccionario, el idioma de la traduccion y el literal de la traducción propiamente dicho con lo que ya no tendremos variaciones en el número columnas. En la tabla Traduccion la clave primaria estará formada por la clave del diccionario y el campo locale. El esquema que tendríamos sería el siguiente:

Diagrama entidad relación de base de datos.
Producto-Diccionario 1:1
Diccionario-Traduccion 1:N
 A primera vista la tabla Diccionario no tiene mucho sentido pero nos permitirá crear en Hibernate una clase donde podremos incluir algunos métodos de utilidad de forma que podamos acceder a las traducciones más cómodamente. Para modelar este esquema con hibernate necesitaremos las siguientes entidades de dominio, la entidad Producto, Diccionario, Traduccion y TraduccionPK (necesaria en Hibernate al tener Traduccion una clave compuesta por dos campos o columnas).

// Producto.java (Código resumido)
...

@Entity
@Table(name = "Producto")
public class Producto implements Serializable {
 ...

 @OneToOne(cascade = CascadeType.ALL)
 @JoinColumn(name = "nombre_id", nullable = true)
 private Diccionario nombre;

 @OneToOne(cascade = CascadeType.ALL)
 @JoinColumn(name = "descripcion_id", nullable = true)
 private Diccionario descripcion;

 ...
}

// Diccionario.java
package com.blogspot.elblogdepicodev.domain;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.MapKeyColumn;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import org.apache.commons.lang3.StringUtils;

import com.blogspot.elblogdepicodev.misc.AppThreadLocal;

@Entity
@Table(name = "diccionario")
public class Diccionario implements Serializable {

 private static final long serialVersionUID = -1210174827995726573L;
 
 private static final int LONGITUD_TEXTO_ABREVIADO = 10;
 
 @Id
 @GeneratedValue
 private Long id;

 @OneToMany(cascade = CascadeType.ALL)
 @JoinColumn(name = "diccionario_id")
 @MapKeyColumn(name = "locale")
 private Map<String, Traduccion> traducciones;
 
 public Diccionario() {
 }
 
 public Long getId() {
  return id;
 }

 public void setId(Long id) {
  this.id = id;
 }

 public Map<String, Traduccion> getTraducciones() {
  if (traducciones == null) {
   traducciones = new HashMap<String, Traduccion>();
  }
  return traducciones;
 }

 public void setTraducciones(Map<String, Traduccion> textos) {
  this.traducciones = textos;
 }

 //
 public String getTexto() {
  return getTexto(getLocale());
 }

 public void setTexto(String texto) {
  setTexto(getLocale(), texto);
 }
 
 public String getTexto(Locale locale) {
  Traduccion t = getTraducciones().get(locale.toString());
  if (t == null) {
   return null;
  }
  return t.getTexto();
 }

 public void setTexto(Locale locale, String texto) {
  Traduccion t = getTraducciones().get(locale.toString());
  if (t == null) {
   t = new Traduccion(this, locale.toString());
   getTraducciones().put(locale.toString(), t);
  }
  t.setTexto(texto);
 }

 //
 public boolean equals(Diccionario d) {
  if (d == null) {
   return false;
  }
  return getId().equals(d.getId());
 }

 @Override
 @SuppressWarnings({ "unchecked", "rawtypes" })
 public String toString() {
  Map m = new HashMap();
  m.put("id", getId());
  m.put("texto", StringUtils.abbreviate(getTexto(), LONGITUD_TEXTO_ABREVIADO));
  return m.toString();
 }
    
 private Locale getLocale() {
  return AppThreadLocal.getPreferencias().getLocale();
 }
}

// Traduccion.java
package com.blogspot.elblogdepicodev.domain;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

import org.apache.commons.lang3.StringUtils;

import com.blogspot.elblogdepicodev.misc.Utilidades;

@Entity
@Table(name = "traduccion")
public class Traduccion implements Serializable {

 private static final long serialVersionUID = -1210174827995726573L;
 
 private static final int LONGITUD_TEXTO_ABREVIADO = 10;

 @Id
 private TraduccionPK id;

 @Column(length = 65536)
 private String texto;

 public Traduccion() {  
 }
 
 public Traduccion(Diccionario diccionario, String locale) {
  this.id = new TraduccionPK(diccionario, locale);
 }
 
 public TraduccionPK getId() {
  return id;
 }

 public void setId(TraduccionPK id) {
  this.id = id;
 }

 public String getTexto() {
  return texto;
 }

 public void setTexto(String texto) {
  this.texto = texto;
 }
 
 //
 public Locale getLocale() {
  return Utilidades.getLocale(id.getLocale());
 }

 //
 public boolean equals(Traduccion t) {
  if (t == null) {
   return false;
  }
  return getId().equals(t.getId());
 }

 @Override
 @SuppressWarnings({ "unchecked", "rawtypes" })
 public String toString() {
  Map m = new HashMap();
  m.put("id", getId());
  m.put("locale", getId().getLocale());
  m.put("texto", StringUtils.abbreviate(getTexto(), LONGITUD_TEXTO_ABREVIADO));
  return m.toString();
 }
}

// TraduccionPK.java
package com.blogspot.elblogdepicodev.domain;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

import javax.persistence.Basic;
import javax.persistence.Embeddable;
import javax.persistence.ManyToOne;

@Embeddable
public class TraduccionPK implements Serializable {
 
 private static final long serialVersionUID = 4445136951104395835L;

 @ManyToOne
 private Diccionario diccionario;

 @Basic
 private String locale;

 public TraduccionPK() {  
 }
 
 public TraduccionPK(Diccionario diccionario, String locale) {
  this.diccionario = diccionario;
  this.locale = locale;
 }
 
 public Diccionario getDiccionario() {
  return diccionario;
 }

 public void setDiccionario(Diccionario diccionario) {
  this.diccionario = diccionario;
 }

 public String getLocale() {
  return locale;
 }

 public void setLocale(String locale) {
  this.locale = locale;
 }

 //
 public boolean equals(TraduccionPK t) {
  if (t == null) {
   return false;
  }
  return diccionario.equals(t.getDiccionario()) && getLocale().equals(t.getLocale());
 }

 @Override
 @SuppressWarnings({ "unchecked", "rawtypes" })
 public String toString() {
  Map m = new HashMap();
  m.put("dicionario", getDiccionario().toString());
  m.put("locale", getLocale());
  return m.toString();
 }
}

// Utilidades.java
public class Utilidades {
 ...

 public static Locale getLocale(String locale) {
  String[] s = locale.split("_");
  switch (s.length) {
  case 1: {
   return new Locale(s[0]);
  }
  case 2: {
   return new Locale(s[0], s[1]);
  }
  case 3: {
   return new Locale(s[0], s[1], s[2]);
  }
  default: {
   throw new IllegalArgumentException();
  }
  } 
 }

 ...
}

Esta solución carga las traducciones para todos los idiomas de un diccionario cuando posiblemente solo necesitemos la traducción de un idioma en concreto, tal vez si no quisiésemos que se carguen todos los textos de las traducciones podríamos sacar el campo texto a otra tabla y relacionarla con la Traduccion con un id con lo que los campos de tablas nos quedarían: Traduccion (diccionario_id, locale, texto_id), Texto (id, texto). Aunque con esta última solución necesitamos lanzar una sql más por cada texto que queramos acceder.

Otro problema común que se nos suele presentar en las entidades de dominio es como hacer búsquedas de texto completo («full text seach») en algunas propiedades de esas entidades y con esto no me refiero al débil like de SQL, en el enlace anterior doy varias soluciones (Hibernate Search, SQL, elasticsearch).

Referencia:
http://www.hibernate.org/
http://www.postgresql.org/about/
http://dev.mysql.com/doc/refman/5.0/en/column-count-limit.html
http://es.wikipedia.org/wiki/Normalizaci%C3%B3n_de_bases_de_datos
http://support.microsoft.com/kb/283878/es

viernes, 4 de noviembre de 2011

Formatear precios con símbolo de moneda en Java

Java
Si estamos desarrollando una aplicación que muestre precios y trabaje con diferentes monedas o tengamos que mostrar el precio junto con la moneda de ese precio necesitaremos un NumberFormat configurado para que nos muestre además del precio el símbolo de la moneda. Esto nos será de mucha utilidad en aplicaciones de comercio electrónico, por ejemplo, ya que evitaremos hacer concatenaciones de cadenas String y el código será más legible y fácil de mantener.

Locale locale = ...;
String simbolo = ...;

DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(locale);
dfs.setCurrencySymbol(simbolo);
String pattern = (Constantes.DIVISAS_SIMBOLO_FINAL.indexOf(simbolo) != -1) ? "#,##0.## ¤" : "¤ #,##0.##";
DecimalFormat df = new DecimalFormat(pattern, dfs);

El truco está en hacer uso de un DecimalFormatSymbols y un «¤» en el patrón de formateo. Otra cosa que también tendremos que tener en cuenta si para la moneda que vamos a mostrar el precio el símbolo se pone antes o después de él según la convención de esa moneda. Una vez tenemos el formateador de precio con el símbolo lo usamos de la siguiente forma:

float importe = 13.19;
df.format(importe);

Y obtendremos «13,19 €».

La lógica de obtener el DecimalFormat estaría muy bien tenerlo en una clase de utilidad que lo construya para poder usarlo de diferentes sitios evitando duplicar código.

viernes, 28 de octubre de 2011

¡Hola Planeta Código!

Planeta Código
Últimamente las entradas que estoy escribiendo en el blog están más orientadas a la programación entorno a la plataforma Java que a GNU/Linux aunque siempre relacionadas con el software libre o código abierto. Por ello he incluido mi blog en el agregador de Planeta Código más orientado a temas de programación.

Al igual que cuando incluí mi blog en Planeta Linux el objetivo es llegar a más lectores y si es posible recibir más visitas y comentarios en el blog. Para los que no conozcan mi blog podeís visitarlo en http://elblogdepicodev.blogspot.com. La temática del mismo es el software libre, linux, la programación y cualquier cosa que tenga que ver con la tecnología o la informática.

Algunas de las últimas entradas que he escrito en él son:

Motivos para elegir el framework Apache Tapestry
Peticiones Ajax en Tapestry (II)
Peticiones Ajax en Tapestry
Seleccionar el lenguaje (locale) según el dominio en Apache Tapestry
Usar Apache Tapestry 5 con Groovy (u otros lenguajes de la JVM)
Componente AjaxSpinner para Tapestry 5

Ejemplo sencillo con JavaCC de un analizador léxico y sintáctico
Configuración de entorno en Java con ayuda de Groovy
Comparar cadenas en Java ignorando acentos
Integración continua con Jenkins
Internacionalización a lenguajes con diferentes formas plurales en Java
Hola mundo con Symfony 2 y Cherokee
Elegir herramientas para un proyecto Java

Espero que a alguno os resulten interesantes, si es así estáis invitados a suscribiros al feed de mi blog.

viernes, 21 de octubre de 2011

Peticiones Ajax en Tapestry (II)

Apache Tapestry
Como comentaba en la entrada Peticiones Ajax en Tapestry para hacerlas no es necesaria ni una sola linea de javascript. En la entrada Peticiones Ajax en Tapestry expliqué como hacer una petición Ajax que actualizaba un fragmento de una página y como hacer un petición Ajax que devolviese datos en Json para ser procesados en el cliente.

Pero el soporte para las peticiones Ajax de Tapestry no se queda en esos dos casos. Otro escenario habitual en las páginas es la necesidad de actualizar más de una región de una misma página con una única petición. Para ello Tapestry proporciona la clase MultiZoneUpdate (en la versión 5.3 será desaconsejado su uso en favor del uso de AjaxResponseRenderer) que será el objeto que deberemos devolver en el método del evento que atiende la petición. Este objeto contendrá las referencias de las zonas que queremos actualzar en la página y los nuevos contenidos a mostrar en ellas, Tapestry se encargará en el cliente de actualizarlas automaticamente por nosotros sin que tengamos que hacer nada más.

Veámoslo con un ejemplo.

<t:zone t:id="usuarioZone" update="show">
    <t:if test="service.usuario">
        <span>${service.usuario.login} (<a t:id="cerrarSesion" t:type="actionlink">Cerrar sesion</a>)</span>
        <p:else><span>Invitado (<a t:type="pagelink" page="index">Iniciar sesión/Registrarse</a>)</span></p:else>
 </t:if>
</t:zone>
...
<t:zone t:id="formularioZone" update="show">
 <t:if test="!service.usuario">
  <t:form t:id="usuarioRegistradoForm" zone="datosZone">
   <t:html5textfield type="email" value="urForm.usuario" validate="required" placeholder="Usuario"/>
   <t:html5textfield type="password" value="urForm.contrasena" validate="required" placeholder="Contraseña"/><br/>    
   <t:submit t:id="usuarioRegistradoSubmit" value="Iniciar sesión" class="btn"/>
  </t:form>
 </t:if>

 <t:if test="service.usuario">
  <t:form t:id="cerrarSesionUsuarioRegistradoForm">
   ${service.usuario.login}<br/>
   <t:submit t:id="cerrarSesionUsuarioRegistradoSubmit" value="Cambiar de usuario" class="btn"/>
  </t:form>
 </t:if>
</t:zone>

Object onSubmitFromUsuarioRegistradoForm() throws IOException {
// Registrar al usuario (service.getUsuario() devolverá el objeto que representa al usuario)
...
 if (!service.getRequestGlobals().getRequest().isXHR()) {
  return null;
 }

 MultiZoneUpdate mzu = new MultiZoneUpdate("formularioZone", formularioZone.getBody()).add("usuarioZone", sesionZone.getBody());;

 return mzu;
}

En el ejemplo hay definidas dos zonas (usuarioZone, formularioZone) que son los componentes definidos con <t:zone>. Las zonas representan fragmentos de página cuyo contenido puede ser actualizado por una petición Ajax. Cuando se envía el formuario formularioZone se llama al método onSubmitFromUsuarioRegistradoForm en el cual comprobamos si la petición es una petición Ajax (service.getRequestGlobals().getRequest().isXHR()) si no lo es se actualizará toda la página pero todo seguirá funcionando, si lo es se devuelve un objeto MultiZoneUpdate y se actualizarán las zonas formularioZone y usuarioZone con el contenido de los propios componentes formularioZone y sesionZone aunque podríamos actualizarlas con el contenidos de otros componentes. Los eventos lanzados por Tapestry siguen por defecto una convención que es on[Evento]From[Componente] aunque podrímos darle el nombre que queramos si utilizamos anotaciones.

En Tapestry pasar de una aplicación no Ajax a una Ajax es muy sencillo basta insertar las zonas y en los métodos de los eventos devolver las zonas y contenidos con las que las queremos actualizar. Otro punto importante a destacar y que contribuye a facilitar la adición del soporte Ajax es que el contenido con el que se actualizan las zonas no lo tenemos que saparar en archivos individuales ni supondrá una reestructuración drástica en los archivos del proyecto como tendríamos que hacer en otros frameworks. También a destacar es que esta funcionalidad de actualizar dos fragmentos o zonas de una página no está presente en otros frameworks de por sí donde normalmente cada petición Ajax actualiza un único fragmento. El soporte en Tapestry para trabajar con Ajax es ¡sencillo y excelente!, de lo mejor que hay.

Nota: A partir de la versión 5.3 la clase MultiZoneUpdate ha sido marcada como obsoleta y la nueva clase y método a utilizar es ajaxResponseRenderer.addRender.

Referencia:
Peticiones Ajax en Tapestry
Documentación sobre Apache Tapesty

sábado, 15 de octubre de 2011

Ejemplo sencillo con JavaCC de un analizador léxico y sintáctico

Java
Hay ocasiones en que necesitamos procesar una determinada expresión, por ejemplo para hacer una búsqueda por una serie de criterios obtenidos de la misma. La forma habitual es hacerlo creando un algoritmo específico más o menos complejo según lo sea la expresión con varios splits, expresiones regulares, condiciones, bucles, etc..., que normalmente resulta en código ofuscado difícil de desarrollar, mantener, entender lo que hace y poco flexible ante cambios. Esta es la primera opción que se nos ocurre pero no es la mejor forma de hacerlo como veremos en esta entrada.

Cuando nos enfrentamos a un problema de procesar una expresión debemos tener en cuenta primeramente dos cosas: cual es su léxico (las palabras que lo forman) y su sintaxis (las reglas que definen el orden del léxico). Más tarde por otra parte para procesar la expresión necesitaremos de acciones léxicas y sintácticas que es código que se ejecutará para realizar las tareas que necesitemos al procesar la expresión.

Para facilitar la tarea existen los compiladores como resultado de la invención de los primeros lenguajes y aunque parecen algo complejo de hacer no lo son tanto como desarrollar un algoritmo específico. JavaCC es una herramienta que nos permite definir el léxico de una expresión o lenguaje, la sintaxis del mismo y las acciones léxicas y sintácticas generando posteriormente con la definción de estas cosas una serie de archivos .java con el código fuente de un analizador léxico, sintáctico y otra serie de archivos .java de utilidad para los mismos.

Supongamos que tenemos una aplicación en la que el usuario tiene una caja de búsqueda en la que puede introducir una serie de palabras separadas por espacios, en la que también puede agrupar varias palabas rodeándolas con " y también puede introducir fechas en varios formatos y con distintos separadores para el día, mes y año pudiendo especificar día, mes y año, solo mes y año, solo el mes o solo el año, por ejemplo dd.MMM.yyyy, dd/MMMM/yyyy, MMMM-dd-yyyy, MMM.yyyy, MMM, yyyy, ... Para complicarlo aún más los meses pueden estar en diferentes idiomas. Un ejemplo de expresión podría ser: «"real madrid" enero.2012 febrero.2012 fútbol» en la que intenta buscar elementos relacionados con el real madrid y fútbol y en los meses de enero o febrero de 2012.

Veamos el código fuente de nuestro pequeño compilador cuya misión sera interpretar la expresión de la mejor de las formas y devolver un objeto org.hibernate.criterion.Criterion con el que podremos hacer una búsqueda en Hibernate según los criterios de la expresión. El compilador está dividido en varias partes:

PARSER_BEGIN y PARSE_END: define el nombre de nuestro analizador e incluye métodos de utilidad (en perfecto código Java) que será incluidos en el analizador sintático sin modificar y que podremos usar desde las acciones sintácticas. Los métodos importantes de esta primera parte son los constructores (JavaCC inserta unos pero como vemos podemos definir más), el método main, buildCriterionTermino y buildCriterionFecha que construirán un Criterion cuando el analizador sintáctico detecte un término o fecha respectivamente, la misión principal de nuestro compilador. Estos métodos no tienen mayor complicación son puro código Java. (en azul).

SKIP y TOKEN: esta parte es la que define el analizador léxico con las palabras de nuestra expresión o lenguaje. Ahí están la forma de las fechas, los términos, el día, mes y año, los separadores. Básicamente son una forma de expresiones regulares para definir cada uno de ellos (en morado).

procesarQuery, procesar, termino, fecha: Son propiamente los métodos del analizador sintáctico y van a definir la sintáxis de nuestro lenguaje. procesar contiene una de las partes más importantes ya que es el punto de partida, va cogiendo los tokens proporcionados por el analizador léxico (que genera JavaCC) y determina si es una fecha, término o algo desconocido. Según lo detectado se ejecuta el bloque de código posterior que va entre {} y que constituye una acción sintáctica. Como se ve la acción sintáctica es perfecto código Java y puede usar las variables definidas en el bloque procesar como ct, ft y r. Después de procesar todos los términos de la expresión se ejecuta otra acción sintáctica que agrupa todos los Criterion recogidos en ct, cf en uno solo y que será lo que devuelve el analizador (en verde).

En la acción sintáctica de termino tenemos que tener en cuenta que el término puede ser un mes (enero, ...) por lo que se intenta procesar como una fecha con buildCriterionFecha y si devuelve algo es que se trataba de un mes sino se procesará como un termino con buildCriterionTermino.

¿Por qué se trata un elemento DESCONOCIDO? Porque sino el analizador sintáctico daría un excepción al no saber lo que es, teminaría y no devolvería nada. De esta forma conseguimos que si una expresión que no se entiende se ignore y se devuelvan al menos el resto de expresiones en el Criterion. Este será el caso de un término fecha mal expresado como «01.enero/2012» donde mezcla diferentes separadores en la misma fecha. Tal como están definidos los tokens, el analizador léxico no sabría que es.

Y eso es lo principal de nuestro compilador. No es tan complicado hacer uno como podría parecer a priori, sin duda mucho más fácil que hacer un algoritmo específico para ello incluso para una expresión tan simple como la tratada, ahora imagínate una que pueda ser como «(<expr> or (<expr>and (<expr>or <expr>) or <expr>))».

Esta es una de esas herramientas muy útilies y con la cual sabiendo usarla o al menos tener conocimiento de ella nos puede ahorrar mucho tiempo y conseguir hacer las cosas mejor. Además y dado que lo que genera como resultado son una serie de archivos .java podremos utilizarlos en cualquier entorno, como en alguna administración pública cuyo nombre no citaré aquí y en la que no esta permitido usar librerías no homologadas por ellos, dado que se trata de código fuente y que no tiene dependencias sobre otras librerías no tendremos ningún problema en usar JavaCC en casos como este.

Para compilar el compilador podemos hacerlo con ant con la siguiente tarea (también podemos utilizar los propios comandos de JavaCC):


<javacc target="src/main/java/com/blogspot/elblogdepicodev/jj/Buscador.jj"
 outputdirectory="src/main/java/com/blogspot/elblogdepicodev/jj/buscador"
    javacchome="/home/[user]/javacc-5.0"/>

Los archivos generados serían:

  • Buscador.java
  • BuscadorConstants.java
  • BuscadorTokenManager.java
  • ParseException.java
  • SimpleCharStream.java
  • Token.java
  • TokenMgrError.java


// Buscador.jj
options {
 STATIC = false;
}

PARSER_BEGIN(Buscador)
package com.blogspot.elblogdepicodev.jj.buscador;

import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;

import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Restrictions;
import org.hibernate.type.IntegerType;
import org.hibernate.type.Type;

public class Buscador {
 private static final String[] FORMATOS_FECHAS = new String[] { 
  "d/MMM/yyyy", "d/MMMM/yyyy", "MMM/d/yyyy", "MMMM/d/yyyy", "MMM/yyyy", "MMMM/yyyy",
  "d.MMM.yyyy", "d.MMMM.yyyy", "MMM.d.yyyy", "MMMM.d.yyyy", "MMM.yyyy", "MMMM.yyyy",
  "d-MMM-yyyy", "d-MMMM-yyyy", "MMM-d-yyyy", "MMMM-d-yyyy", "MMM-yyyy", "MMMM-yyyy",
  "MMM", "MMMM", "yyyy" 
 };

 private Locale locale;

 public Buscador(Locale locale) {
  this(new StringReader(""), locale);
 }
 
    public Buscador(InputStream is, String encoding, Locale locale) {
  this(is, encoding);
                this.locale = locale;
 }
 
 public Buscador(Reader r, Locale locale) {
  this(r);
        this.locale = locale;
 }

    public static void main(String[] args) throws ParseException, TokenMgrError {
     StringBuffer sb = new StringBuffer();
     for(int i = 0; i < args.length; ++i) {
   if (i > 0) {
    sb.append(" ");
      }
   sb.append(args[i]);
     }

     Buscador parser = new Buscador(new Locale("es"));
     Criterion criterion = parser.procesarQuery(sb.toString());
     System.out.println(criterion);
 }
 
 // Utilidades
 private Criterion or(List<Criterion> criterions) {
  Criterion lhs = null;
  for (Criterion criterion : criterions) {
   if (lhs == null) {
    lhs = criterion;
   } else {
    lhs = Restrictions.or(lhs, criterion);
   }
  }
  return lhs;
 }

 private Criterion and(List<criterion> criterions) {
  Criterion lhs = null;
  for (Criterion criterion : criterions) {
   if (lhs == null) {
    lhs = criterion;
   } else {
    lhs = Restrictions.and(lhs, criterion);
   }
  }
  return lhs;
 }
 
 private Criterion buildCriterionTermino(String term) {
  List<Criterion> coincidencias = new ArrayList<Criterion>();
  
  String t = "%" + term + "%";

  coincidencias.add(Restrictions.ilike("nombre", t));
  coincidencias.add(Restrictions.ilike("ciudad", t));
  coincidencias.add(Restrictions.ilike("direccion", t));

  Criterion criterio = or(coincidencias);
  
  return criterio; 
 }
 
 private Criterion buildCriterionFecha(String term) {
  Criterion criterio = null;
  
  for (int i = 0; i < FORMATOS_FECHAS.length; ++i) {
   String formatoFecha = FORMATOS_FECHAS[i];
   SimpleDateFormat sdf = new SimpleDateFormat(formatoFecha, locale);

   // Fecha
   try {
    Date fecha = sdf.parse(term);

    Calendar calendario = Calendar.getInstance(locale);
    calendario.setTime(fecha);

    switch (i) {
    case 0:
    case 1:
    case 2:
    case 3:
    case 6:
    case 7:
    case 8:
    case 9:
    case 12:
    case 13:
    case 14:
    case 15:
     // Día, mes, año
     criterio = Restrictions.sqlRestriction("(day({alias}.fecha) = ? and month({alias}.fecha) = ? and year({alias}.fecha) = ?)", new Object[] {
       calendario.get(Calendar.DAY_OF_MONTH), calendario.get(Calendar.MONTH) + 1, calendario.get(Calendar.YEAR) }, new Type[] { IntegerType.INSTANCE,
       IntegerType.INSTANCE, IntegerType.INSTANCE });
     break;
    case 4:
    case 5:
    case 10:
    case 11:
    case 16:
    case 17:
     // Mes, año
     criterio = Restrictions.sqlRestriction("(month({alias}.fecha) = ? and year({alias}.fecha) = ?)", new Object[] {
       calendario.get(Calendar.MONTH) + 1, calendario.get(Calendar.YEAR) }, new Type[] { IntegerType.INSTANCE, IntegerType.INSTANCE });
     break;
    case 18:
    case 19:
     // Mes
     criterio = Restrictions.sqlRestriction("month({alias}.fecha) = ?", calendario.get(Calendar.MONTH) + 1, IntegerType.INSTANCE);
     break;
    case 20:
     // Año
     criterio = Restrictions.sqlRestriction("year({alias}.fecha) = ?", calendario.get(Calendar.YEAR), IntegerType.INSTANCE);
     break;
    default:
     assert (false);
     break;
    }
   } catch (java.text.ParseException e) {
   }
   
   if (criterio != null) {
    break;
   }
  }

  return criterio;
 }
 
 private class CriterionInfo {
  public boolean isFecha;
  public Criterion criterio; 
 }
}
PARSER_END(Buscador)

SKIP : { " " | "\t" | "\n" | "\r" | "\r\n" }

TOKEN : { < #NO_SKIP : ~[" ", "\t", "\r", "\n"] > }
TOKEN : { < #SEP1 : "/" > }
TOKEN : { < #SEP2 : "." > }
TOKEN : { < #SEP3 : "-" > }
TOKEN : { < #SEP : (<sep1> | <sep2> | <sep3>) > }
TOKEN : { < #LETRA : ~[" ", "\t", "\r", "\n", "/", ".", "-"] > }
TOKEN : { < #NUM : ["0"-"9"] > }
TOKEN : { < #MES : (<letra>)+ > }
TOKEN : { < #DIA : (<num> | <num><num>) > }
TOKEN : { < #ANO : (<num><num><num><num>) > }

TOKEN : { < FECHA : (
 <dia><sep1><mes><sep1><ano> | <mes><sep1><dia><sep1><ano> | <mes><sep1><ano> | 
 <dia><sep2><mes><sep2><ano> | <mes><sep2><dia><sep2><ano> | <mes><sep2><ano> |
 <dia><sep3><mes><sep3><ano> | <mes><sep3><dia><sep3><ano> | <mes><sep3><ano> |
 <num><num><num><num>) > }
TOKEN : { < TERMINO : ("\""(~["\""])+"\"" | (<letra>)+) > }
TOKEN : { < DESCONOCIDO : (<no_skip>)+ > }

Criterion procesarQuery(String query) :
{
 Criterion criterio = null;

 ReInit(new StringReader(query));
}
{
 criterio = procesar()
 {
  return criterio;
 }
}

Criterion procesar() :
{
 List<Criterion> ct = new ArrayList<Criterion<();
 List<Criterion> cf = new ArrayList<Criterion<();

 Criterion criterio = null;
 CriterionInfo criterioInfo = null;
 Token t = null;
}
{
 (
  criterio = fecha()
  {
   if (criterio != null) {
    cf.add(criterio);
   }
   //System.out.println(criterio);
  }
 |
  criterioInfo = termino() 
  {
   criterio = criterioInfo.criterio;
   if (criterio != null) {
    if (criterioInfo.isFecha) {
     cf.add(criterio);
    } else {
     ct.add(criterio);
    }
   }
   //System.out.println(criterio);
  }
 |
  t = <desconocido>
  {
   //System.out.println(t.image);
  }
 )*
 <eof>
 {
  List<Criterion> r = new ArrayList<Criterion>();
  if (!ct.isEmpty()) {
   r.addAll(ct);
  }
  if (!cf.isEmpty()) {
   r.add(or(cf));
  }
  return (r.isEmpty())?null:and(r);
 } 
}

CriterionInfo termino() :
{
 Token t = null;
}
{
 t = <termino>
 { 
  //System.out.println(t.image);
  String term = t.image;
  CriterionInfo ci = new CriterionInfo();
   
  // Comprobar si se trata de un mes
  ci.criterio = buildCriterionFecha(term);
  
  if (ci.criterio != null) {
   ci.isFecha = true;
  } else {
   ci.isFecha = false;

   if (term.startsWith("\"") && term.endsWith("\"")) {
    term = term.substring(1, term.length() - 1);
   }
   ci.criterio = buildCriterionTermino(term);
  } 
  return ci;
 }
}

Criterion fecha() :
{
 Token t = null;
}
{
 t = <fecha>
 { 
  String term = t.image;
  return buildCriterionFecha(term);
 }
}

Referencia:
http://javacc.java.net/
http://www.lpsi.eui.upm.es/webcomp/jgperez/java/IntrodJavaCC.pdf

viernes, 7 de octubre de 2011

Configuración de entorno en Java con ayuda de Groovy

JavaGroovy
Una necesidad habitual en prácticamente todas las aplicaciones y proyectos es tener una forma de poder configurar una aplicación en función del entorno en el que se vaya a ejecutar. Cosas habituales que cambian dependiendo del entorno son la configuración de logging, conexión a la base de datos, tal vez la configuración de hibernate si lo usamos, parámetros, etc... Habitualmente necesitaremos un entorno con su configuración para la máquina en la que desarrolla cada programador del proyecto que será el entorno de desarrollo, también es habitual tener un entorno de pruebas independiente del entorno de cada desarrollador y el entorno de producción que es donde se ejecuta la aplicación.

La configuración del multientorno la podemos la podemos hacer de diferentes formas, mediante archivos de propiedades, con xml u otras formas. Aqui vamos a ver como hacerlo con la ayuda de groovy de una forma sencilla sin tener que pelearnos con procesar la forma del archivo, que con xml puede llegar a ser laborioso y con archivos .properties un tanto limitado en cuanto a posibilidades.

Veámos primero lo que sería el archivo de configuración:

[
    entorno: 'desarrollo',
    
 //
 valores: [
  'valor1', 
  'valor2', 
  'valor3'
 ] as String[],

 // Redefinición para configuración para entornos
    entornos: [
        desarrollo: [
      log4j:      '/cfg/log4j-desarrollo.properties',
      hibernate:  '/cfg/hibernate-desarrollo.cfg.xml',
      quartz:     '/cfg/quartz-desarrollo.properties',
  ],
        pruebas: [
   log4j:      '/cfg/log4j-pruebas.properties',
   hibernate:  '/cfg/hibernate-pruebas.cfg.xml',
   quartz:     '/cfg/quartz-pruebas.properties',
  ],
        produccion: [
   log4j:      '/cfg/log4j-produccion.properties',
   hibernate:  '/cfg/hibernate-produccion.cfg.xml',
   quartz:     '/cfg/quartz-produccion.properties',

   valor: [
    'valor-produccion1', 
    'valor-produccion2', 
    'valor-produccion3'
   ] as String[],
  ]
    ]
]

El archivo de configuración es código groovy que define un mapa con una serie de propiedades y valores. La propiedad entorno indica el entorno que será usado en tiempo de ejecución por la aplicación. La propiedad entornos tiene las propiedades específicas de cada entorno. Las propiedad «valores» está definda de forma global independiente de entorno y es común para los entornos de desarrollo y pruebas pero que se redefine para el entorno de producción.

La clase Entorno nos permitirá procesar el archivo de configuración groovy y la clase Utilidades tiene unos métodos de utilidad para facilitar la tarea.

// Entorno.java
package com.blogspot.elblogdepicodev.misc;

import java.io.InputStream;

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

public class Entorno {

 private static Logger logger = LoggerFactory.getLogger(Entorno.class);
 
 public enum Ent {
  DESARROLLO, PRUEBAS, PRODUCCION
 }
 
 public static final String PROP_LOG4J = "log4j";
 public static final String PROP_HIBERNATE = "hibernate";
 public static final String PROP_QUARTZ = "quartz";

 private static Object conf;
 private static Ent entorno;
 private static Object configuracion;
 
 public static void initialize(Object conf) throws Exception {
  initialize(conf, null);
 }
 
 public static void initialize(Object conf, Ent ent) throws Exception {
  Entorno.conf = conf;
  Entorno.entorno = (ent != null)?ent:Ent.valueOf(((String) getPropiedadGlobal("entorno")).toUpperCase());
  Entorno.configuracion = getPropiedadGlobal("entornos." + Entorno.entorno.toString().toLowerCase());
  
  logger.debug("Entorno inicializado (" + ent + ")");
 }
 
 public static Object getPropiedad(String propiedad) {
  try {
   String p = propiedad.replaceAll("\\.", "?.");
   // Ver si es una propiedad del entorno
   Object o = getPropiedadEntorno(p);
   if (o == null) {
    // No es una propiedad del entorno, ver si es una propiedad global
    o = getPropiedadGlobal(p);
   }
   return o;
  } catch (Exception e) {
   throw new RuntimeException(e);
  }
 }
 
 public static String getString(String propiedad) {
  return (String) getPropiedad(propiedad);
 }
 
 public static InputStream getResourceAsStream(String propiedad) {
  return (InputStream) Entorno.class.getResourceAsStream(getString(propiedad));
 }
 
 public static Ent getEntorno() {
  return entorno;
 }
 
 public static boolean isEntorno(Ent e) {
  return entorno.equals(e);
 }
 
 public static boolean isDesarrollo() {
  return isEntorno(Ent.DESARROLLO);
 }
 
 public static boolean isPruebas() {
  return isEntorno(Ent.PRUEBAS);  
 }
 
 public static boolean isProduccion() {
  return isEntorno(Ent.PRODUCCION);
 } 
 
 public static Object getPropiedadGlobal(String propiedad) throws Exception {
  return Utilidades.groovy("conf." + propiedad, Utilidades.map("conf", conf));
 }
 
 public static Object getPropiedadEntorno(String propiedad) throws Exception {
  return Utilidades.groovy("configuracion." + propiedad, Utilidades.map("configuracion", configuracion));
 }
}

// Utilidades.java
package com.blogspot.elblogdepicodev.misc;

import groovy.text.SimpleTemplateEngine;
import groovy.text.TemplateEngine;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Type;
import java.text.MessageFormat;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;

import javax.script.Bindings;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

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

import com.blogspot.elblogdepicodev.misc.Entorno.Ent;

public class Utilidades {

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

 private static ScriptEngine engine;

 static {
  ScriptEngineManager manager = new ScriptEngineManager();
  engine = manager.getEngineByName(Constantes.GROOVY_ENGINE_NAME);
 }

 public static void initializeEntorno() throws Exception {
  initializeEntorno(null);
 }

 public static void initializeEntorno(Ent ent) throws Exception {
  StringBuffer sb = new StringBuffer();

  InputStream is = Utilidades.class.getResourceAsStream("/cfg/Configuracion.groovy");
  Reader r = new InputStreamReader(is);

  char[] b = new char[4096];
  int i = r.read(b);
  while (i != -1) {
   sb.append(b, 0, i);
   i = r.read(b);
  }
  Object configuracion = Utilidades.groovy(sb.toString(), Collections.EMPTY_MAP);
  Entorno.initialize(configuracion, ent);
 }

 public static Object groovy(String script, Map bindings) throws ScriptException {
  Bindings b = engine.createBindings();
  if (bindings != null) {
   b.putAll(bindings);
  }
  return engine.eval(script, b);
 }
}

Los métodos importantes de esta clase son el initializeEntorno de clase Utilidades que se encarga de leer el archivo de configuración, evaluarlo como una expresión groovy y pasárselo al método initialize de la clase Entorno. La llamada a initializeEntorno la haremos cuando se arranque la aplicación posiblemente en un ContextLitener si se trata de una aplicación web. Una vez tengamos en entorno inicializado podremos llamar a getPropiedad de Entorno para obtener el valor de una propiedad, este método primeramente buscará entre la propiedades específicas entorno en ejecución y si no existe en él buscará entre las propiedades globales, devolverá lo primero en lo que encuentre algo.

Con estas dos clases tenemos una solución sencilla pero a la vez muy potente de configuración de entornos. Dado que las propiedades las estamos de definiendo con groovy las propiedades devueltas no será solo datos String que luego tendremos que parsear en nuestra aplicación sino que con la ayuda de groovy las propiedades devueltas podrán ser objetos, Double, Long, Date, listas, mapas, etc .... Y esta no es la unica forma de definir el archivo de configuración podríamos hacer que fuese un objeto en vez de un mapa de propiedades y podríamos definir métodos con alguna lógica y llamarlos al obtener una propiedad. ¡Las posibilidades son muchas para estas pocas líneas de código!

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