Páginas

miércoles, 13 de febrero de 2013

Primeros pasos con JSON y Java

Introducción

JSON (JavaScript Object Notation), es un formato estándar de intercambio de datos basado en texto que permite a las aplicaciones intercambiar información.

JSON está estandarizado por la IETF (Internet Engineering Task Force) en el RFC-4627 (http://tools.ietf.org/html/rfc4627). El Mime Type para datos en éste formato es 'application/json' y la extensión oficial de los ficheros con este formato es '.json'.

Los datos intercambiados son independientes tanto de la plataforma como del lenguaje de programación utilizado.

¿Qué ventajas ofrece JSON frente a XML?

JSON va reemplazando a XML poco a poco como medio preferido a la hora de intercambiar datos entre aplicativos de distintas plataformas. Los motivos por los que se argumenta que JSON es mejor son casi siempre los mismos:
  • JSON es más fácil de leer (xml también).
  • JSON tiene una estructura es similar a la de Objetos y Arrays (xml también).
  • JSON es más ligero (bytes) en las transmisiones (desde luego que no hay etiqueta de cierre, pero sí de apertura).
  • JSON se parsea más rápido (supongo que será verdad, pero entiendo que dependerá de la calidad del parseador).

Sintaxis JSON

En este apartado vamos a repasar las principales características de la sintaxis JSON, se puede encontrar fácilmente documentación detallada sobre el tema, por ejemplo en el enlace siguiente: http://www.json.org

Estructuras de datos en JSON

JSON ofrece únicamente tres estructuras de datos:
   
- Pares Nombre/Valor. Un ejemplo sería:
   
            {
                "nombre" : "nombre11"
            }

           
- Objetos. Son colecciones desordenadas de pares Nombre/Valor. Un ejemplo sería:
       
            {
                "contacto" : {
                    "nombre" : "nombre11",
                    "apellido1" : "apellido11",
                    "apellido2" : "apellido21",
                    "email" : "11111111@gmail.com"
                }
            }

           
- Arrays. Son colecciones ordenadas de objetos. Un ejemplo sería:
           
            {
                "agenda" : [
                    {"nombre" : "nombre11", "apellido1" : "apellido11", "apellido2" : "apellido21", "email" : "11111111@gmail.com"},
                    {"nombre" : "nombre12", "apellido1" : "apellido12", "apellido2" : "apellido22", "email" : "22222222@gmail.com"},
                    {"nombre" : "nombre13", "apellido1" : "apellido13", "apellido2" : "apellido23", "email" : "33333333@gmail.com"}
                ]
            }

Tipos de datos en JSON

Hasta el momento hemos visto que en la parte derecha de cada par "Nombre/Valor" aparecía siempre una cadena entrecomillada. JSON admite además de cadenas (String) algunos otros tipos de datos:
  • Object
  • Array
  • String
  • Number
  • Boolean
  • null

Comentarios en JSON

JSON no admite comentarios.

Convenciones en el nombrado de atributos

Los nombres de los atributos en JSON (el elemento izquierdo de cada par Nombre/Valor) se escriben en notación "camel case", que es típica también en Java. La idea es escribir los nombres compuestos metiendo mayúscula al comienzo de cada palabra excepto en la primera. Por ejemplo: "telefFijo", "telefMovil", etc.

Esquemas en JSON

Los documentos JSON, como cabría esperar, están basados en esquemas. Los esquemas definen la estructura de los documentos JSON y permiten validarlos. Los esquemas JSON están evolucionando muy rápidamente (la versión 0.4 está publicada en: http://tools.ietf.org/html/draft-zyp-json-schema-04
   
El sitio de internet donde más esquemas JSON hay publicados es: http://json-schema.org, pero visto el número de esquemas ¿estándares? publicados parece que el tema está aún muy verde. Parece que aún está lejos de sustituir al XML en este sentido.

Herramientas para trabajar con JSON

A la hora de empezar a trabajar con JSON pueden ser interesantes las siguientes herramientas:
  • Validador online JSON. Revisar la sintaxis de un documento JSON grande a ojo es verdaderamente pesado, por tanto puede ser interesante utilizar un validador online como el ofrecido en http://www.jsonlint.com para validar nuestros documentos.
  • Editor JSON. Si lo que necesitamos es un editor JSON donde generar documentos o esquemas con cierto nivel de asistencia, una herramienta muy popular es JSONPad, descargable en: http://www.jsonpad.com/en/Home.html. También hay editores online, siendo uno de los más populares: http://jsoneditoronline.org/

JSON y Java

A la hora de programar aplicaciones que generen o traten datos en formato JSON hay muchos API:
  • Jackson. http://jackson.codehaus.org
  • Google GSON. http://code.google.com/p/google-json/
  • SOJO. http://sojo.sourceforge.net/

Aplicativo Java de pruebas

El siguiente proyecto Java muestra un ejemplo de cómo realizar las siguientes operaciones:
  • Crear un documento JSON a partir de un POJO
  • Crear un POJO a partir de un documento JSON
  • Obtener el esquema de un documento JSON
  • Validar un documento JSON contra un esquema   
Se han utilizado las siguientes librerías:

        <dependency>
            <groupId>org.codehaus.jackson</groupId>
            <artifactId>jackson-mapper-lgpl</artifactId>
            <version>1.9.12</version>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
            <type>jar</type>
        </dependency>

El código son dos clases, la clase Contacto.java y la clase TestJSON.java:
   
package es.ine.sgtic.pojo;

public class Contacto {

    private String nombre;
    private String apellido1;
    private String apellido2;
    private String telefFijo;
    private String telefMovil;
    private String email;

    public Contacto(){
        //Constructor que precisa JSON
    }
    public Contacto(final String nombre,
            final String apellido1,
            final String apellido2,
            final String telefFijo,
            final String telefMovil,
            final String email) {
        this.nombre = nombre;
        this.apellido1 = apellido1;
        this.apellido2 = apellido2;
        this.telefFijo = telefFijo;
        this.telefMovil = telefMovil;
        this.email = email;
    }
    public String getNombre() {
        return nombre;
    }
    public void setNombre(final String nombre) {
        this.nombre = nombre;
    }
    public String getApellido1() {
        return apellido1;
    }
    public void setApellido1(final String apellido1) {
        this.apellido1 = apellido1;
    }
    public String getApellido2() {
        return apellido2;
    }
    public void setApellido2(final String apellido2) {
        this.apellido2 = apellido2;
    }
    public String getTelefFijo() {
        return telefFijo;
    }
    public void setTelefFijo(final String telefFijo) {
        this.telefFijo = telefFijo;
    }
    public String getTelefMovil() {
        return telefMovil;
    }
    public void setTelefMovil(final String telefMovil) {
        this.telefMovil = telefMovil;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(final String email) {
        this.email = email;
    }

    public boolean equals(Contacto cto) {
        return this.nombre.equals(cto.getNombre()) &&
                this.apellido1.equals(cto.getApellido1()) &&
                this.apellido2.equals(cto.getApellido2()) &&
                this.telefFijo.equals(cto.getTelefFijo()) &&
                this.telefMovil.equals(cto.getTelefMovil()) &&
                this.email.equals(cto.getEmail());
    }
}


package es.ine.sgtic.pojo;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.map.SerializationConfig.Feature;
import org.codehaus.jackson.schema.JsonSchema;

public class TestJSON {

    private static Log log = LogFactory.getLog(TestJSON.class);
   
    public static void main (String args[]) 

                       throws JsonGenerationException, 
                       JsonMappingException, IOException {

        //Instanciamos el convertidor Java-JSON (funcionaría en ambos sentidos
        final ObjectMapper mapper = new ObjectMapper();

        //Ajustamos el convertidor para que indente las cadenas JSON genedaras
        mapper.configure(Feature.INDENT_OUTPUT, true);

        //Montamos un pojo de la clase Contacto
        final Contacto cto1 = new Contacto("nombre","apellido1","apellido2",

                                  "123456789","987654321","egdp1970@gmail.com");

        log.trace("Generamos el String cadenaJSON asociado al objeto cto1");
        String cadenaJSON = generaNotacionJSON(mapper, cto1);
        log.trace(cadenaJSON);
        log.trace("----------------------------------------------------");
       
        log.trace("Generamos un objeto cto2 a partir del String cadenaJSON");
        final Contacto cto2 = (Contacto)generaPojojava(mapper, cadenaJSON, cto1.getClass());
        log.trace("Comparamos el objeto cto1 y el objeto cto2");
        log.trace("cto1.equals(cto2)-->"+cto1.equals(cto2));
        log.trace("----------------------------------------------------");

        log.trace("Generamos String cadenaJSON asociado al objeto cto2");
        cadenaJSON = generaNotacionJSON(mapper, cto2);
        log.trace(cadenaJSON);
        log.trace("----------------------------------------------------");

        log.trace("Generamos el String schemaJSON asociado al objeto cto1");
        final String schemaJSON = generaSchemaJSON(mapper, cto1.getClass());
        log.trace(schemaJSON);
        log.trace("----------------------------------------------------");

        log.trace("Validamos el String cadenaJSON contra el esquema schemaJSON");
        log.trace("validaCadenaJSON()-->"+validaCadenaJSON(mapper, cadenaJSON));
        log.trace("----------------------------------------------------");
       
        log.trace("Validamos el String cadenaJSON_ERR contra el esquema schemaJSON");
        final String cadenaJSON_ERR = cadenaJSON + "}";
        log.trace("validaCadenaJSON()-->"+validaCadenaJSON(mapper, cadenaJSON_ERR));
        log.trace("----------------------------------------------------");
    }

    private static String generaNotacionJSON(final ObjectMapper mapper, 

                                             Object obj) 
                                              throws JsonGenerationException, 
                                              JsonMappingException, 
                                              IOException{

        //Convertimos el objeto Java en notación JSON
        final Writer writer = new StringWriter();
        mapper.writeValue(writer, obj);

        //Devolvemos la cadena JSON
        return writer.toString();
    }

    private static Object generaPojojava(final ObjectMapper mapper, 

                                         final String cadenaJSON, 
                                         final Class<?> clazz) 
                                              throws JsonGenerationException, 
                                              JsonMappingException, 
                                              IOException{

        //Convertimos la notación JSON en objeto Java
        return mapper.readValue(cadenaJSON, clazz);
    }

    private static String generaSchemaJSON(final ObjectMapper mapper, 

                                           final Class<?> clazz)                                                                      throws JsonGenerationException, 
                                              JsonMappingException, 
                                              IOException{

        //Generamos el schema JSON para el pojo
        final SerializationConfig cfg = mapper.getSerializationConfig();
        final JsonSchema jsonSchema = mapper.generateJsonSchema(clazz,cfg);
        return jsonSchema.toString();
    }

    private static boolean validaCadenaJSON(final ObjectMapper mapper, 

                                            final String cadenaJSON){

        boolean valido = false;
        try {
            final JsonParser parser = mapper.getJsonFactory().createJsonParser(cadenaJSON);
            while (parser.nextToken() != null) {
            }
            valido = true;
        } catch (JsonParseException e) {
           
//log.error(e.getMessage(),e);
        } catch (IOException e) {
           
//log.error(e.getMessage(),e);
        }

        return valido;
    }
}

lunes, 4 de febrero de 2013

Primeros pasos con Apache Lucene

Introducción

Lucene es un motor de búsqueda de Apache. Está implementado en Java (http://lucene.apache.org) y es usado en multitud de gestores de contenido.

Lucene indexa texto, no trabaja directamente sobre ningún formato de fichero concreto. Por tanto, para indexar "documentos",  previamente deberá extraerse su contenido con las librerías adecuadas.

Dado cualquier texto, Luceneindexa por defecto las primeras 1000 palabras.

Algunas librerías típicas para menejar los documentos más comunes son:

    PDF. JPedal (http://www.jpedal.org/).
    Microsoft Word, Excel, Visio y Power Point. Apache POI (http://poi.apache.org/)
    OpenOffice. LIUS (http://www.bibl.ulaval.ca/lius/).
    RTF. Swing RTFEditorKit class.

Una vez indexado el contenido de un "documento", se podrán efectuar búsquedas sobre los índices generados. Al efectuarse las búsquedas sobre los índices estas son siempre más rápidas que búsquedas sobre los "documentos".

Componentes de Lucene

En Lucene trabaja con tres conceptos esenciales:
  • Documentos. Un documento, para Lucene, es una tupla de N campos. Cada campo tiene un nombre y contiene cierto tipo de información.
  • Campos. Es cada una de las categorías de información que componen el documento.
  • Querys. Para realizar las búsquedas en el índice, Lucene ofrece un lenguaje de consultas, que permite especificar en qué campos buscar, admite comodines, admite AND, OR y NOT, admite búsquedas por proximidad, etc.
Veamos por ejemplo un par de "documentos":
  • Agenda. En una agenda de contactos, un documento sería cada una de las fichas de la agenda. Los campos serían, por ejemplo, el nombre, el primer apellido, el segundo apellido, el teléfono móvil, el teléfono fijo, etc
  • Biblioteca. En una biblioteca, un documento sería cada uno de los libros. Los campos serían, por ejemplo: título, autor, estantería, contenido, ISBN, etc.
Veamos por ejemplo algunas querys en el índice de la biblioteca:
  • Lista de libros donde el autor sea "Phillip"
        autor:Phillip
  • Lista de libros donde el autor sea "Philip K Dick"
        autor:"Philip K Dick"
  • Lista de libros donde el autor sea "Philip K Dick", en el contenido del libro aparezca "ovejas"
        autor:"Philip K Dick" AND contenido:ovejas

Apliación Java de ejemplo

Hemos montado un proyecto Maven2 con las siguientes dependencias:

  <dependencies>
      <dependency>
          <groupId>org.apache.lucene</groupId>
          <artifactId>lucene-core</artifactId>
          <version>4.0.0</version>
      </dependency>
      <dependency>
          <groupId>org.apache.lucene</groupId>
          <artifactId>lucene-queryparser</artifactId>
          <version>4.0.0</version>
      </dependency>
      <dependency>
          <groupId>org.apache.lucene</groupId>
          <artifactId>lucene-analyzers-common</artifactId>
          <version>4.0.0</version>
      </dependency>
      <dependency>
          <groupId>org.apache.commons</groupId>
          <artifactId>commons-io</artifactId>
          <version>1.3.2</version>
      </dependency>
  </dependencies>


Adjuntamos un ejemplo en Java donde se montará el índice de una biblioteca, añadiendo varios documentos y efectuando algunas búsquedas.

import java.io.File;
import java.io.FileReader;
import java.io.IOException;

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.highlight.InvalidTokenOffsetsException;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.util.Version;

public class AppTest {

    public static void main(String[] args) throws IOException, ParseException, InvalidTokenOffsetsException {

        //Fijamos el analizador para tokenizar, indezar y buscar en textos
        final StandardAnalyzer analizador = new StandardAnalyzer(Version.LUCENE_40);

        //Creamos el indice en memoria RAM 

        final Directory indice = new RAMDirectory();
        final IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_40, analizador);

        //Montamos el generador del índice
        final IndexWriter iw = new IndexWriter(indice, config);

        //Nuestros documentos serán libros con tres campos: titulo, autor y contenido.
        //Añadimos un par de libros, los ficheros txt con el contenido de los mismos están en el classpath
        addLibro(iw, "El ingenioso hidalgo Don Quijote de la Mancha", "Miguel de Cervantes y Saavedra", new File("Quijote.txt"));
        addLibro(iw, "El Buscón", "Francisco Gómez de Quevedo y Santibáñez Villegas", new File("Buscon.txt"));
 

        //Cerramos el generador de índice
        iw.close();

        //Lanzamos algunas querys
        lanzaConsulta(analizador, indice, "autor:Cervantes");
        lanzaConsulta(analizador, indice, "autor:\"de Cervantes y\"");
        lanzaConsulta(analizador, indice, "autor:Cervan*");
        lanzaConsulta(analizador, indice, "autor:Cervantes AND titulo:hidalgo");
        lanzaConsulta(analizador, indice, "autor:Cervantes OR autor:Quevedo");
        lanzaConsulta(analizador, indice, "(autor:Cervantes OR autor:Quevedo) AND NOT titulo:Quijote");


        //Algunas querys de búsqueda por proximidad, esto es palabras parecidas a la nuestra 
        //pudiendo diferenciase en N caracteres
        lanzaConsulta(analizador, indice, "contenido:Tocoso~1"); //Buscamos a Dulcinea del "Tocoso" sabiendo que es algo parecido
    }

    private static void addLibro(final IndexWriter iw, 

                                                  final String titulo, 
                                                  final String autor, 
                                                  final File f) throws Exception {

        //Montamos el documento
        final Document libro = new Document();

        //Le añadimos el atributo titulo
        libro.add(new TextField("titulo", titulo, Field.Store.YES));

        //Le añadimos el atributo autor
        libro.add(new TextField("autor", autor, Field.Store.YES));

        //Le añadimos el atributo contenido
        libro.add(new TextField("contenido", new FileReader(f)));

        //Le añadimos la ubicacion del fichero
        libro.add(new TextField("fichero", f.getCanonicalPath(), Field.Store.YES));

        //Damos de alta el documento
        iw.addDocument(libro);
    }


    private static void lanzaConsulta(final StandardAnalyzer analizador, 

                                                           final Directory indice, 
                                                           final String query) throws ParseException, IOException {

        //Montamos el parseador de Querys. El segundo parámetro es el campo por defecto donde
        //se realizarán las búsquedas si una query no indica ningún campo concreto
        final QueryParser queryParser = new QueryParser(Version.LUCENE_40, "titulo", analizador);
        final Query q = queryParser.parse(query);

        //Montamos el buscador
        final IndexReader reader = DirectoryReader.open(indice);
        final IndexSearcher searcher = new IndexSearcher(reader);

        //Lanzamos la query
        final int numMaxResults = 10;
        final TopDocs docsEncontrados = searcher.search(q,numMaxResults);   
        System.out.println("Total hits "+docsEncontrados.totalHits);

        //Recuperamos la lista de coincidencias
        final ScoreDoc[] docs = docsEncontrados.scoreDocs;

        //Mostramos los resultados
        System.out.println("-------------------------------------------------------------------");
        System.out.println("Encontradas " + docsEncontrados.totalHits + " coincidencias para '"+query);
        int idDoc;
        int index;
        float score;
        Document doc;
        for(int i=0;i<docs.length;++i) {
            idDoc = docs[i].doc;
            score = docs[i].score;
            index = docs[i].shardIndex;
            doc = searcher.doc(idDoc);
            System.out.println(String.format("%d. [%f] \t %s \t %s",index,score,doc.get("titulo"),doc.get("autor")));
        }
        System.out.println("-------------------------------------------------------------------");
    }
}


Enlaces de interés

http://wiki.apache.org/lucene-java/LuceneFAQ
http://www.lucenetutorial.com/