viernes, marzo 15, 2013

Griffon: flujo creacion mvc

Cuando se crea un MVC a través de una de las variantes de:

def mvc=buildMVCGroup([arg1:value1,arg2:..],"pintador")

es fundamental entender el proceso interno que lleva a la correcta construcción del MVC. El proceso se ejecuta en 3 fases y en cada de ellas se sigue por defecto el orden lógico (modelo, vista y finalmente controlador *), la secuencia es como sigue:
 


La primera fase invoca al constructor de cada una de las partes, por ejemplo:

PintadorController(){
 ...
 }

La segunda fase invoca a los setters de los objetos según los parámetros que se hayan pasado al invocar el buildMVCGroup, por ejemplo:

class PintadorModel{
 def arg1
 ...
 void setArg1(def cosa){
 this.arg1=cosa
 }

Es importante subrayar que sólo se invoca al setter si los nombres entre el argumento y la propiedad coinciden. Finalmente se hace el paso de 'postproducción', llamandose al mvcGroupInit de cada artefacto.

void mvcGroupInit(Map args) {
 ...
 }

Un truco importante aqui es que el pintado de la pantalla (es decir la ejecución del script PintadorView.groovy en nuestro ejemplo) se realiza en esta última etapa después del mvcGroupInit del modelo (PintadorModel.groovy) lo cual permite hacer uso de bucles limpios en el propio script si fuera necesario algún tipo de calculo añadido sobre el modelo antes de mostrarlo:

//File: PintadorModel.groovy
 class PintadorModel{
 def arg1
 def objetos
 void mvcGroupInit(Map args){
 objetos=server.getObjetos(arg1)
 }
 }
 ...
//File: PintadorView.groovy
 ...
 vbox(){
 model.objetos.each{
 label $it
 }
 }

Listo! A partir de ahora hacer mvcs no debería tener ningún secreto.

*Nota:

En realidad este orden es así por defecto dado que en la declaración del mvc en el fichero Application.groovy se sigue ese orden, si el orden fuera:

mvcGroups {
 'pintador' {  
   model = 'com.mycompany.PintadorModel' 
   controller = 'com.mycompany.PintadorController' 
   view = 'com.mycompany.PintadorView' 
 } 
 .... 
}

el orden de invocación de los artefactos sería ese para cada una de las tres etapas.

martes, marzo 05, 2013

Griffon y el soporte en eclipse

El soporte de griffon desde eclipse es un poco limitado, lo único que hay para mejorar esto es el uso del plugin eclipse-support

griffon install-plugin eclipse-support

Funciona ok, no rompe y solo hay que hacer 2 consideraciones:

Si el eclipse no se entera de las clases y te da avisos entre los markers de compilacion ejecuta desde la línea de comandos el script que refresca el .classpath:


griffon eclipse-update

En ocasiones (en windows y para jar añadidos al lib directamente) la entrada que hace en el classpath no es correcta (eclipse-support 0.6.3), para arreglarlo hay que sustituir la entrada incorrecta que tendrá la pinta siguiente

<classpathentry kind="var" path="C:\\..\\myproject\\lib\\cosa.jar" />

por lo siguiente

<classpathentry kind="lib" path="C:\\..\\myproject\\lib\\cosa.jar" />

Nota:
Tengo pendiente actualizar el projecto de griffon a 1.2.0 para ver si efectivamente la version del plugin de hace unos días corrige este bug, :D.

viernes, febrero 15, 2013

Desmontando Log4j

Buenas, a quién queremos engañar, la mayoría llevamos años haciendo configuraciones de Log4j gracias a Google, copiando y pegando cachos hasta que funciona...técnicamente es lo que se conoce como "programming by coincidence".

Así que la idea de este post es romper eso y explicar de forma semi-humana como se configura un log4j para que haga lo que queramos.

Lo primero es que todo esto esta explicado con detalle y de forma fantástica aquí: http://logging.apache.org/log4j/1.2/manual.html

Preámbulo

Ahora mis explicaciones, en log4j sólo hay 3 conceptos:
  • Logger: Indica la configuracion de logging para una categoria, son jerarquicos y se ponen nombres de paquetes/clases. Se puede/No se puede
  • Appenders: Sitios donde se puede escribir. ¿Dónde?
  • Layouts: Patrón de escritura del log. ¿Cómo?
Y dado el siguiente código de ejemplo:

Class Perro{
  def log=Logger.getLogger(Perro.class) //   ....   log.debug("Aqui estoy")  ... } 

la librería tiene que resolver 2 preguntas:
  • El mensaje está enabled, es decir, ¿esta autorizado para pasar?
  • Donde lo tiro?

1. ¿Esta autorizado este mensaje?

Para responder esta pregunta hay que entender que existen distintos niveles de log, están ordenados y son los siguientes:

fatal > error > warn > info > debug > trace

Al mismo tiempo cada logger puede tener un nivel asignado o heredar el nivel de su padre de forma que el mensaje estará "enabled" si su mensaje es igual o superior al nivel autorizado.

Suena raro pero es lo mismo que se hace en PortAventura cuando se dice que para pasar al Dragon Khan hay que medir más de 1,30m, lo mismo.  





  Ok, y como se calcula el nivel asociado a un logger?

Pues dado que es jerarquico y funciona como los nombres de paquetes de las clases se puede construir una tabla como la siguiente:

LEVEL ESCRITO LEVEL CALCULADO
root error error
nortia warn warn
nortia.Pepe warn
nortia.hijo debug
nortia.hijo.Pepito debug debug

Ahora que sabes el nivel de un logger ya puedes decidir si eso pasa o no pasa, es decir escribe o no escribe.

2. ¿Donde se escribe?

La última parte es ver donde se escribe el mensaje que hayas puesto, en este punto hay que entender que del mismo modo que un logger tiene un único nivel calculado, un logger puede tener N appenders asociados:

log4j.rootLogger=DEBUG, myConsola, myFichero, myOtroFichero

dicho lo cual hay que seguir una regla muy sencilla
Por defecto se escribe en todos los appenders asociados a la jerarquía del logger  
Dicho lo cual te sale otra tabla que completa a la anterior:

ESCRITO ADDITIVITY CALCULADO
root Console Console
nortia Fich1 Fich1, Console
nortia.Pepe Fich1, Console
nortia.hijo Fich2 false Fich2
nortia.hijo.Pepito Fich2

3. Lo quiero en otro fichero solo...

En ocasiones puedes querer que las clases de tal paquete loggen en tal fichero, para evitar que esos mensajes salgan por todos los logger de la jerarquía basta con indicarlo con el flag de additivity

log4j.additivity.nortia.hijo = false

4.Resumen

En resumen:
  • Logger: 1 nivel escrito o calculado por logger
  • El mensaje esta "enabled" si es "más alto" que nivel calculado ese logger
  • Appenders: N por logger
  • El mensaje se escribe en todos los logger de la jerarquia salvo que uses additivity=false.

viernes, diciembre 07, 2012

Grails, elasticSearch, classcircularity y datepicker

Quiero una aplicación en grails que use datepickers molones y tenga un buscador incrustado, sencillo verdad??

En "efesto" pasen, pasen.... Lo cierto es que la cosa empieza bien, creas la aplicación, haces tus chorra dominios y lo primero que te encuentras es que el picker por defecto de grails es .....un poco mierdoso??

 

 Entonces te das cuenta que sin un framework de css y un conjunto de utilidades hechas por gente mucho mejor que tu no eres nadie y no quieres perder el tiempo con esas pijadas por lo que decides que es hora de meter bootstrap en tu vida (y por tanto jquery).

Así que después de explorar un poco consigues que tu app tenga un aspecto muy chulo (ver ejemplos) e incluso encuentras un datepicker que es compatible con tu nuevo look (http://www.eyecon.ro/bootstrap-datepicker/).

La cosa va bien, eres feliz, aunque ingeniuo porque si haces la prueba tal cual veras que tu controlador cuando hace la instrucción:

miDominio.properties=params

no se entera del precioso campo "fechaDeMiCreacion" y pasa de ti, en otras palabras no sabe como convertir de String a Date por ese formato tan chulo que estas usando debido al datepicker.

Así que volvemos a tirar de San Google y leemos que tenemos que hacer un CustomPropertyEditor para decirle como se convierte el agua en vino, quiero decir, de String a Date, fácil, creamos nuestro PropertyEditorRegistrar que dice lo que tiene que hacer, supongamos (src/groovy/mi.paquete/CustomDatePropertyEditorRegistrar.groovy):  

package my.paquete

import java.text.SimpleDateFormat;
import java.util.Date;

import org.springframework.beans.PropertyEditorRegistrar;
import org.springframework.beans.PropertyEditorRegistry;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.context.i18n.LocaleContextHolder;

class CustomDatePropertyEditorRegistrar implements PropertyEditorRegistrar {

 def messageSource;

 public void registerCustomEditors(PropertyEditorRegistry registry) {
   registry.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat(messageSource.getMessage("editor.date.format",null,'dd/MM/yyyy',LocaleContextHolder.locale )),true));
 }

}
Con esto ya sabemos como hacer el paso, fíjate que uso las messages.properties para poder cambiar el formato de la fecha y hacerlo fácilmente i18n messages.properties:

editor.date.format=MM/dd/yyyy

messages_es.properties:

editor.date.format=dd/MM/yyyy

Ahora para que toda la aplicación sepa de tu logro (ejem...), debes registrar el bean que da el cambiazo, ya sabes, resources.groovy:

beans = {

 customDatePropertyEditor(my.paquete.CustomDatePropertyEditorRegistrar) {
      messageSource = ref('messageSource')
 }
................

Ahora si, el binding de las fechas funciona ok, :D. Estamos???NOOOOOOOOO

Deciamos al principio que queriamos la app con buscador, así que muy ufános hacemos la búsqueda y aunque dudamos entre elasticSearch y searchable por el primero, parece más guay y nosotros queremos ser guays, no?

grails install-plugin elasticSearch

Revisamos nuestros dominios (como Saurón) y añadimos :

static searchable=true

Arrancamos la moto y ..........ostión!

Lo más seguro que el mensaje que diga algo obvio como ClassCircularityError y te quedes pensando que tienes alguna milonga de referencias circulares o sabe Dios el qué.

La solución??

ElasticSearch como cualquier bicho que indexa necesita persistir los objetos que maneja, es decir serializar y deserializar, es decir escribir en fichero y leer del fichero, en su caso lo hace usando el formato JSON.

Te vas oliendo la tostada?

El caso es que acabamos de cambiar la forma en que se hace el binding que pasa de String a Date y eso aunque creamos que solo nos afecta al convertir la request en chicha en los controllers es exactamente lo que hace el buscador cuando escribe y sobre todo lee los datos indexados para devolverte resultados.

Así que ahora resulta que la conversión de agua en vino es más bien la conversión de agua en vino o mosto según se pida por lo que el CustomDateEditor que antes usamos se nos queda un poco flojo y tenemos que darle cera por ahi, al tema:  

package my.paquete
import java.beans.PropertyEditorSupport
import java.text.ParseException
import java.text.SimpleDateFormat

import org.apache.commons.lang.time.DateUtils;
import org.springframework.beans.PropertyEditorRegistrar
import org.springframework.beans.PropertyEditorRegistry
import org.springframework.beans.propertyeditors.CustomDateEditor
import org.springframework.context.i18n.LocaleContextHolder

/**
 * Registro de customs
 * @author ivan.arrizabalaga
 */
class CustomDatePropertyEditorRegistrar implements PropertyEditorRegistrar {
 def messageSource;

 /**
  * Registra el customDateEditor
  */
 public void registerCustomEditors(PropertyEditorRegistry registry) {
  def inputFormat=messageSource.getMessage("editor.date.format",null,'dd/MM/yyyy',LocaleContextHolder.locale)
   registry.registerCustomEditor(Date.class,
     new SearchableDateEditor(inputFormat,true)
    );
  }
}

Hasta aquí más  o menos lo mismo de antes pero metiendo mi propia clase en vez del CustomDateEditor por defecto, sigo:

/**
 * Custom property editor que admite 2 formatos para el parseo de fechas uno para el parseo de fechas desde los formularios y otro para el unmarshall del buscador
 * @author ivan.arrizabalaga
 *
 */
class SearchableDateEditor extends PropertyEditorSupport{
 boolean allowEmpty
 def searchableFormat="yyyy-MM-dd'T'HH:mm:ss'Z'"
 def inputFormat="dd/MM/yyyy"
 String[] formats=[]

 SearchableDateEditor(String inputFormat,boolean allowEmpty){
  this.allowEmpty=true
  this.inputFormat=inputFormat
  this.formats=[this.inputFormat,this.searchableFormat]
 }

 /**
  * Crea un 'Date' a partir de un 'String' se usa en:
  *  -binding de los params (Ej: en los controllers: miCosa.properties=params)
  *  -unmarshall de los indices del buscador
  */
 void setAsText(String text) throws IllegalArgumentException {
  //0.Fechas vacias
  if(this.allowEmpty && !text){
   setValue(null)
  }else{
  //1.Intentamos parsear
   try{
    setValue(DateUtils.parseDate(text, this.formats))
   }catch(ParseException ex){
    throw new IllegalArgumentException("Could not parse date: " + ex.getMessage(), ex)
   }
  }
 }

 /**
  * Devuelve el 'String' a partir del 'Date', se usa:
  *  -marshall del buscador
  */
 String getAsText() {
  def val=getValue()
  val?.respondsTo('format')? val.format(this.searchableFormat):''
 }

}

Ahora si, esta es la buena, ahora cuando parsea String a Date lo intenta con ambos formatos el de el buscador y el de el datepicker y cuando pasa de Date a String usa el formato propio del JSON del buscador para hacer la persistencia.

Mola o qué?

Grails N a M: como diablos va esto??

Las relaciones N a M en grails oficialmente se resuelven simplemente con un doble hasMany una parte que manda a través del belongsTo, sin embargo esto sólo es válido para aquellas relaciones N a M sin atributos adicionales. Lo cierto es que esto casi nunca es así y siempre que hay una N a M en realidad hay una entidad nueva pendiente de ser "descubierta". Un ejemplo:
1 Autor -> N Libros
1 Libro -> N Autores
En realidad hay una entidad intermedia que podría indicar si el autor es principal, los capitulos que ha escrito del libro, un contrato de derechos de autor especifico para cada colaboración.......aja! Vaya! Pues si que habia una entidad en medio, supongamos que la llamamos Autoría y que por simplificar indica si el autor es el principal o no (como Ana Rosa....ejem). Con esto vemos que en realidad tendriamos lo siguiente:
1 Autor -> N Autorias
1 Libro -> N Autorias
1 Autoría -> 1 Libro, 1 Autor
Ahora en virtud de las bondades de GORM (no confundir con la Isla de Gorm) hacer ciertas operaciones como añadir autorias, borrar autores o libros puede resultar un infierno de problemas de hibernate, flushes y demás historias. Así que visto lo visto y basándonos en este articulo(http://grails.org/Many-to-Many+Mapping+without+Hibernate+XML) quedaría el siguiente código fuente: Autor:
package com.nortia.simple
class Autor {

 String nombre

 static hasMany=[autorias:Autoria]
 static constraints = {}
 def libros(){
   return autorias.collect{it.libro}
 }

 def addToLibros(Libro libro, def params=null){
   Autoria.link(this, libro,params)
   return libros()
 }

 def removeFromLibros(Libro libro){
   Autoria.unlink(this, libro)
   return libros()
 }

 def clearLibros(){
   libros().each{
     removeFromLibros(it)
   }
 }

 def safeDelete(){
   this.clearLibros()
   this.delete()
 }
}
Libro:
package com.nortia.simple
class Libro {

 String titulo

 static hasMany=[autorias:Autoria]
static constraints = {
 }

 def autores(){
   return autorias.collect{it.autor}
 }

 def addToAutores(Autor autor, def params=null){
   Autoria.link(autor, this,params)
   return autores()
 }

 def removeFromAutores(Autor autor){
   Autoria.unlink(autor, this)
   return autores()
 }

 def clearAutores(){
   autores().each{
     removeFromAutores(it)
   }
 }

 def safeDelete(){
   this.clearAutores()
   this.delete()
 }
}
Autoría:
package com.nortia.simple
class Autoria {
 Autor autor
 Libro libro
 boolean autorPrincipal=true

 static belongsTo=[autor:Autor, libro:Libro]

 static Autoria link(def autor, def libro,def params=null){
   def autoria=Autoria.findByAutorAndLibro(autor,libro)
   if(!autoria){
     autoria=new Autoria(params)
     autor?.addToAutorias(autoria)
     libro?.addToAutorias(autoria)
     autoria.save()
   }
   return autoria
 }

 static void unlink(def autor, def libro){
   def autoria=Autoria.findByAutorAndLibro(autor,libro)
   if(autoria){
     autor?.removeFromAutorias(autoria)
     libro?.removeFromAutorias(autoria)
     autoria.delete()
   }
 }

}
Test (lanzando la consola como grails console):
import com.nortia.simple.*
println "0.Autorias: ${Autoria.list()}"
println "0.Autores: ${Autor.list()}"
println "0.Libros: ${Libro.list()}"
Autoria.executeUpdate("delete from Autoria")
Autor.executeUpdate("delete from Autor")
Libro.executeUpdate("delete from Libro")
def a=new Autor(nombre:"Carlos Ruiz Zafon")
a.save()
def l=new Libro(titulo:"La sombra del viento")
l.save()
def l2=new Libro(titulo:"La sombra del viento 2")
l2.save()
a.libros()
Autoria.link(a,l,[autorPrincipal:false]) //Opcion con params1
// Opción completa a.addToLibros(l2,[autorPrincipal:false]
a.addToLibros(l2) //Opción básica
a.libros()*.titulo
println "1.Autorias: ${Autoria.list()}"
def autorias=Autoria.list()
autorias.each{
  println "Autor: ${it.autor.nombre} Libro:${it.libro.titulo} \\\
          Es principal: ${it.autorPrincipal}"
}
a.safeDelete()
println "2.Autorias: ${Autoria.list()}"
Fijate que no hay ningún flush por ningún lado, esto es buen síntoma, quiere decir que tienes operaciones atómicas que no requieren del volcado de la sesión. No obstante si ejecutas 2 veces el mismo test se ve que los libros existen al principio y efectivamente