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é?

No hay comentarios: