You are on page 1of 12

Ejemplo sencillo con JavaCC de un analizador lxico y sintctico

Hay ocasiones en que necesitamos procesar una determinada expresin, por ejemplo para hacer una bsqueda por una serie de criterios obtenidos de la misma. La forma habitual es hacerlo creando un algoritmo especfico ms o menos complejo segn lo sea la expresin con varios splits, expresiones regulares, condiciones, bucles, etc..., que normalmente resulta en cdigo ofuscado difcil de desarrollar, mantener, entender lo que hace y poco flexible ante cambios. Esta es la primera opcin 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 expresin debemos tener en cuenta primeramente dos cosas: cual es su lxico (las palabras que lo forman) y su sintaxis (las reglas que definen el orden del lxico). Ms tarde por otra parte para procesar la expresin necesitaremos de acciones lxicas y sintcticas que es cdigo que se ejecutar para realizar las tareas que necesitemos al procesar la expresin. Para facilitar la tarea existen los compiladores como resultado de la invencin de los primeros lenguajes y aunque parecen algo complejo

de hacer no lo son tanto como desarrollar un algoritmo especfico. JavaCC es una herramienta que nos permite definir el lxico de una expresin o lenguaje, la sintaxis del mismo y las acciones lxicas y sintcticas generando posteriormente con la defincin de estas cosas una serie de archivos .java con el cdigo fuente de un analizador lxico, sintctico y otra serie de archivos .java de utilidad para los mismos. Supongamos que tenemos una aplicacin en la que el usuario tiene una caja de bsqueda en la que puede introducir una serie de palabras separadas por espacios, en la que tambin puede agrupar varias palabas rodendolas con " y tambin puede introducir fechas en varios formatos y con distintos separadores para el da, mes y ao pudiendo especificar da, mes y ao, solo mes y ao, solo el mes o solo el ao, por ejemplo dd.MMM.yyyy, dd/MMMM/yyyy, MMMM-dd-yyyy, MMM.yyyy, MMM, yyyy, ... Para complicarlo an ms los meses pueden estar en diferentes idiomas. Un ejemplo de expresin podra ser: "real madrid" enero.2012 febrero.2012 ftbol en la que intenta buscar elementos relacionados con el real madrid y ftbol y en los meses de enero o febrero de 2012. Veamos el cdigo fuente de nuestro pequeo compilador cuya misin sera interpretar la expresin de la mejor de las formas y devolver un objeto org.hibernate.criterion.Criterion con el que podremos hacer una bsqueda en Hibernate segn los criterios de la expresin. El compilador est dividido en varias partes: PARSER_BEGIN y PARSE_END: define el nombre de nuestro analizador e incluye mtodos de utilidad (en perfecto cdigo Java) que ser incluidos en el analizador sinttico sin modificar y que podremos usar desde las acciones sintcticas. Los mtodos importantes de esta primera parte son los constructores (JavaCC inserta unos pero como vemos podemos definir ms), el mtodo main, buildCriterionTermino y buildCriterionFecha que construirn un Criterion cuando el analizador sintctico detecte un trmino o

fecha respectivamente, la misin principal de nuestro compilador. Estos mtodos no tienen mayor complicacin son puro cdigo Java. (en azul). SKIP y TOKEN: esta parte es la que define el analizador lxico con las palabras de nuestra expresin o lenguaje. Ah estn la forma de las fechas, los trminos, el da, mes y ao, los separadores. Bsicamente son una forma de expresiones regulares para definir cada uno de ellos (en morado). procesarQuery, procesar, termino, fecha: Son propiamente los mtodos del analizador sintctico y van a definir la sintxis de nuestro lenguaje. procesar contiene una de las partes ms importantes ya que es el punto de partida, va cogiendo los tokens proporcionados por el analizador lxico (que genera JavaCC) y determina si es una fecha, trmino o algo desconocido. Segn lo detectado se ejecuta el bloque de cdigo posterior que va entre {} y que constituye una accin sintctica. Como se ve la accin sintctica es perfecto cdigo Java y puede usar las variables definidas en el bloque procesar como ct, ft y r. Despus de procesar todos los trminos de la expresin se ejecuta otra accin sintctica que agrupa todos los Criterion recogidos en ct, cf en uno solo y que ser lo que devuelve el analizador (en verde). En la accin sintctica de termino tenemos que tener en cuenta que el trmino 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 sintctico dara un excepcin al no saber lo que es, teminara y no devolvera nada. De esta forma conseguimos que si una expresin que no se entiende se ignore y se devuelvan al menos el resto de expresiones en el Criterion. Este ser el caso de un

trmino fecha mal expresado como 01.enero/2012 donde mezcla diferentes separadores en la misma fecha. Tal como estn definidos los tokens, el analizador lxico no sabra que es. Y eso es lo principal de nuestro compilador. No es tan complicado hacer uno como podra parecer a priori, sin duda mucho ms fcil que hacer un algoritmo especfico para ello incluso para una expresin tan simple como la tratada, ahora imagnate 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. Adems y dado que lo que genera como resultado son una serie de archivos .java podremos utilizarlos en cualquier entorno, como en alguna administracin pblica cuyo nombre no citar aqu y en la que no esta permitido usar libreras no homologadas por ellos, dado que se trata de cdigo fuente y que no tiene dependencias sobre otras libreras no tendremos ningn problema en usar JavaCC en casos como este. Para compilar el compilador podemos hacerlo con ant con la siguiente tarea (tambin podemos utilizar los propios comandos de JavaCC):
<javacc target="src/main/java/com/blogspot/elblogdepicodev/jj/Buscado r.jj" outputdirectory="src/main/java/com/blogspot/elblogdepicodev/j j/buscador" javacchome="/home/[user]/javacc-5.0"/>

Los archivos generados seran:

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 import import import import import import import import import import import import java.io.InputStream; java.io.Reader; java.io.StringReader; java.text.SimpleDateFormat; java.util.ArrayList; java.util.Calendar; java.util.Date; java.util.List; java.util.Locale; org.hibernate.criterion.Criterion; org.hibernate.criterion.Restrictions; org.hibernate.type.IntegerType; org.hibernate.type.Type; FORMATOS_FECHAS = new String[] "MMM/d/yyyy", "MMMM/d/yyyy", "MMM.d.yyyy", "MMMM.d.yyyy", "MMM-d-yyyy", "MMMM-d-yyyy",

public class Buscador { private static final String[] { "d/MMM/yyyy", "d/MMMM/yyyy", "MMM/yyyy", "MMMM/yyyy", "d.MMM.yyyy", "d.MMMM.yyyy", "MMM.yyyy", "MMMM.yyyy", "d-MMM-yyyy", "d-MMMM-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: // Da, mes, ao 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, ao 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: // Ao 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 TOKEN TOKEN TOKEN TOKEN TOKEN > } TOKEN TOKEN TOKEN TOKEN : : : : : : : : : : { { { { { { { { { { < < < < < < < < < < #NO_SKIP : ~[" ", "\t", "\r", "\n"] > } #SEP1 : "/" > } #SEP2 : "." > } #SEP3 : "-" > } #SEP : (<sep1> | <sep2> | <sep3>) > } #LETRA : ~[" ", "\t", "\r", "\n", "/", ".", "-"] #NUM #MES #DIA #ANO : : : : ["0"-"9"] > } (<letra>)+ > } (<num> | <num><num>) > } (<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); } }

You might also like