Ejemplo de jQuery UI: autocompletar, con origen de datos en JSon Struts2

En este ejemplo se van a combinar diferentes tecnologías, tanto de servidor como de cliente, para crear una página de ejemplo en la que se puede buscar una palabra en castellano de un diccionario mediante autocompletar, tal y como hace Google ahora mismo, por ejemplo.

El elemento principal es Autocomplete de jQuery UI. Tiene la apariencia de una caja de texto, y cuando se escribe, muestra posible palabras similares a la que hemos puesto desde un array de datos. Dispone de varias opciones de origen de datos, como un array en Javascript de forma explícita o tomar los valores de una página externa. Esta es la opción que se ha seguido en esta ocasión porque se parte de un conjunto de más de 80.000 palabras, algo complicado de manejar si se emplean arrays de Javascript.

A grandes rasgos la descripción de la arquitectura es la siguiente: se cuenta con una base de datos en PostgreSQL donde, en una tabla están almacenadas más de 80.000 palabras. El elemento Autocompletar, recoge las palabras que corresponden a cada pulsación de esta base de datos a través de una acción de Struts2, que se encarga de filtrar las palabras y devolvérselas a la página web en formato JSON, de modo que sólo las necesarias serán transmitidas. La comunicación entre la acción de Struts2 y la base de datos se realiza utilizando la arquitectura JPA de persistencia de Java.

En primer lugar debemos encontrar un diccionario. Tras una búsqueda en Google se pueden encontrar varios. Para el ejemplo he usado este http://olea.org/proyectos/lemarios/. La tabla que almacenará el diccionario en PostgreSQL es muy sencilla. Sólo tiene un “id” autonumérico y la columna “palabras” de tipo texto.

La carga de los datos en la tabla de PosgreSQL es una tarea sencilla, simplemente creamos los insert, añadiéndolos con un editor de texto. En mi caso usé Geany.

INSERT INTO palabras (palabras) values ('a');
INSERT INTO palabras (palabras) values ('a-');
INSERT INTO palabras (palabras) values ('aarónico');
INSERT INTO palabras (palabras) values ('aaronita');
INSERT INTO palabras (palabras) values ('aba');
INSERT INTO palabras (palabras) values ('ababa');
INSERT INTO palabras (palabras) values ('ababillarse');
INSERT INTO palabras (palabras) values ('ababol');
INSERT INTO palabras (palabras) values ('abacá');
INSERT INTO palabras (palabras) values ('abacal');
INSERT INTO palabras (palabras) values ('abacalero');
... así hasta más de 80.000 palabras

El siguiente paso es crear la infraestructura en Java usando JPA par la comunicación con esta base de datos. Netbeans facilita esta tarea en gran medida. Los pasos básicos descritos levemente son los siguientes:

  • En la ventana Services, crear una conexión de postgresql (habrá que tener el driver incluído en las librerías) a nuestra base de datos. Navegando veremos que funciona, accediendo a la tabla y realizando consultas SQL para comprobar que todo va bien
  • En la ventaja Projects, seleccionando nuestro proyecto Web de Struts2, creamos un elemento nuevo. En el wizard, en la categoría de Persistence (es posible que haya que instalarlo si no viene) se elige “Persistence Unir”. Esto creará o añadirá al archivo persistence.xml la configuración de la conexión con la base de datos.
  • Una vez tenemos la conexión, se tendrá que crear la clase “Palabra” que modela la entidad de la base de datos de la tabla palabras. Vamos al paquete “data” (crearlo para separar clases si no existe) y ahí se elige nuevo elemento,y en Persistence, se elige Entity Clasess from Database. Se seguirá el wizard, eligiendo la conexión a nuestra base de datos y la tabla palabras.
  • Finalmente se añade un grado más de indirección, creando una clase controladora de la clase palabras recién creada.

El uso de JPA y de las facilidades de creación de código automático de Netbeans facilitan bastante todas estas tareas de conexión con la base de datos.

La clase “Palabras” ya dispone de toda la configuración para la comunicación con la base de datos por medio de las anotaciones, así como sus atributos y getters/setter.

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package data;

import java.io.Serializable;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;

/**
 *
 * @author Alberto Moratilla <alberto.moratilla@gmail.com>
 */
@Entity
@Table(name = "palabras")
@NamedQueries({
    @NamedQuery(name = "Palabras.findAll", query = "SELECT p FROM Palabras p"),
    @NamedQuery(name = "Palabras.findById", query = "SELECT p FROM Palabras p WHERE p.id = :id"),
    @NamedQuery(name = "Palabras.findByPalabras", query = "SELECT p FROM Palabras p WHERE p.palabras = :palabras")})
public class Palabras implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @Basic(optional = false)
    @Column(name = "id")
    private Integer id;
    @Basic(optional = false)
    @Column(name = "palabras")
    private String palabras;

    public Palabras() {
    }

    public Palabras(Integer id) {
        this.id = id;
    }

    public Palabras(Integer id, String palabras) {
        this.id = id;
        this.palabras = palabras;
    }

    public Integer getId() {
        return id;
    }

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

    public String getPalabras() {
        return palabras;
    }

    public void setPalabras(String palabras) {
        this.palabras = palabras;
    }

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (id != null ? id.hashCode() : 0);
        return hash;
    }

    @Override
    public boolean equals(Object object) {
        // TODO: Warning - this method won't work in the case the id fields are not set
        if (!(object instanceof Palabras)) {
            return false;
        }
        Palabras other = (Palabras) object;
        if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "data.Palabras[id=" + id + "]";
    }

}

La clase PalabrasJPAController se utilizará para tratar con la base de datos y poner obtener palabras. Incluye de por sí los métodos básicos para tratar con estas tareas, pero debemos incluir en este caso uno especializado para nuestra labor.

El método que necesitamos consiste en que nos devuelva un listado de palabras con el criterio de que estas palabras comienzan con la cadena de texto indicada por parámetro. En SQL sería devolver las palabras con la condición LIKE palabra = texto+”%”. El resultado será una lista de palabras.

Para hacer este método me he basado en el que proporciona por defecto, findPalabrasEntities, modificándolo un poco para incluir el LIKE:

public List<Palabras> findPalabrasStartsWith(String texto) {
	EntityManager em = getEntityManager();
	try {
		CriteriaQuery<Palabras> cq = em.getCriteriaBuilder().createQuery(Palabras.class);
		CriteriaBuilder cb = em.getCriteriaBuilder();
		Metamodel m = em.getMetamodel();

		Root<Palabras> palabra = cq.from(Palabras.class);
		cq.select(palabra);
		cq.where(cb.like(palabra.<String>get("palabras"), texto + "%"));

		Query q = em.createQuery(cq);
		return q.getResultList();
	} finally {
		em.close();
	}
}

El funcionamiento de las consultas en JPA es un poco “curioso”, pero básicamente el código anterior realiza esta tarea.

Una vez tenemos un código Java que nos devuelve una lista de Palabras, es hora de integrarlo en nuestra idea global, que consiste en proporcionar este listado a la aplicación en formato JSON tal y como ella espera.

El problema radica en el formato que requiere el objeto Autocomplete para funcionar correctamente. Si vamos al ejemplo de la página de jQuery UI tenemos que éste funciona mediante tres atributos: “id”, “label”, “value”. Para transformar nuestro objeto “Palabra” que tiene solamente “id” y “palabras” en el formato deseado, se empleará un serializador de gSon personalizado. En este post, https://mysticalpotato.wordpress.com/2010/11/11/json-en-java-utilizando-la-biblioteca-com-google-gson-gson/, describo cómo hacerlo. Información de cómo funciona la librería Gson se puede encontrar en: https://mysticalpotato.wordpress.com/2010/11/11/json-en-java-utilizando-la-biblioteca-com-google-gson-gson/.

También podemos ver en la documentación cómo se realiza la comunicación entre “Autocomplete” y nuestra acción. Cuando el usuario escribre un cierto número de caracteres, se hace la petición a la URL de la acción, adjuntando un parámetro por get. Este parámetro es “term”.

La acción de Struts2 por lo tanto estará preparada para realizar las siguientes acciones:

  • Recibir por GET el texto con el cual comienzan las palabras contenidas en el listado.
  • Cargar el listado utilizando el JPAController.
  • Devolver el listado en formato JSON adecuado

Utilizando la información de los Post anteriores y escribiendo el resultado directamente en el response (ver https://mysticalpotato.wordpress.com/2010/11/09/struts2-accion-para-escribir-directamente-en-el-response/) obtendriamos una acción similar a ésta:

package acciones;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.opensymphony.xwork2.ActionSupport;
import data.Palabras;
import data.PalabrasJpaController;
import java.lang.reflect.Type;
import java.util.List;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts2.interceptor.ServletResponseAware;

/**
 *
 * @author Alberto Moratilla <www.4lberto.com>
 */
public class AutocompletarDiccionario extends ActionSupport implements ServletResponseAware {

    String term;
    HttpServletResponse response;

    public AutocompletarDiccionario() {
    }


    /**
     * Devuelve el listado de palabras del diccionario en JSON que comienzan por el valor indicado
     * @return
     * @throws Exception
     */
    public String execute() throws Exception {


        PalabrasJpaController pjc = new PalabrasJpaController();
        List<Palabras> palabras = pjc.findPalabrasStartsWith(term);

        GsonBuilder gsonb = new GsonBuilder();
        gsonb.registerTypeAdapter(Palabras.class, new PalabrasSerializer());
        Gson gson = gsonb.create();

        ServletOutputStream sos = response.getOutputStream();
        sos.print(gson.toJson(palabras));

        return null;    //Devuelve la salida en jSON

    }

    public String getTerm() {
        return term;
    }

    public void setTerm(String term) {
        this.term = term;
    }

    public void setServletResponse(HttpServletResponse hsr) {
        response = hsr;
    }
}

/**
 * Serializador específico para "palabra"
 * @author Alberto Moratilla <alberto.moratilla@gmail.com>
 */
class PalabrasSerializer implements JsonSerializer<Palabras> {
  public JsonElement serialize(Palabras src, Type typeOfSrc, JsonSerializationContext context) {
      JsonObject jo = new JsonObject();
      jo.add("id", new JsonPrimitive(src.getId()));
      jo.add("label", new JsonPrimitive(src.getPalabras()));
      jo.add("value", new JsonPrimitive(src.getPalabras()));

      return jo;
  }
}

El archivo struts.xml donde se registra la acción es el siguiente:

<action class="acciones.AutocompletarDiccionario" name="autocompletarDiccionario">
            <result>nosecarganunca.jsp</result>
        </action>

Podemos hacer la llamada de forma “manual” en nuestro navegador para comprobar que funciona. En mi ejemplo haciendo la llamada a: http://localhost:8084/TwitterPrueba1/autocompletarDiccionario.action?term=alb , se obtienen palabras que empiezan por “alb”:

[{"id":3460,"label":"alba","value":"alba"},{"id":3461,"label":"albaca","value":"albaca"},{"id":3462,"label":"albacara","value":"albacara"},{"id":3463,"label":"albacea","value":"albacea"},{"id":3464,"label":"albaceato","value":"albaceato"},{"id":3465,"label":"albaceazgo","value":"albaceazgo"},{"id":3466,"label":"albaceteño","value":"albaceteño"},{"id":3467,"label":"albacetense","value":"albacetense"},{"id":3468,"label":"albacora","value":"albacora"},{"id":3469,"label":"albacorón","value":"albacorón"},{"id":3470,"label":"albada","value":"albada"},{"id":3471,"label":"albadena","value":"albadena"},
...]

Como se puede comprobar es el formato que estábamos esperando.

Lo último que queda es escribir el cliente, en este caso la página HTML que incluye las referencias a jQuery UI y su objeto “Autocomplete”. El código está claramente inspirado en el proporcionado en el ejemplo de la página oficial. Puedes encontrar una breve introdicción a jQuery UI en https://mysticalpotato.wordpress.com/2010/12/01/breve-introduccion-a-jquery-ui/.

El objeto en HTML es el siguiente:

  <h1>Ejemplo de uso de Autocompletar con JSON</h1>
        <div class="ui-widget">
            <label for="diccionario">Escribe una palabra:</label>
            <input id="diccionario" />
        </div>

Y como siempre, el código Javascript de jQuery UI que configura las capas para que actúen como se espera es:

<script type="text/javascript">
	$(document).ready(function() {
		$("#diccionario").autocomplete({
			source:"../autocompletarDiccionario.action",
			minLength:2,
			select:function(event,ui){
				alert("Has seleccionado: " + ui.item.value);}});
	});
</script>

El Input diccionario recibe la función “autocomplete”, que la configura para ser una caja de búsqueda autocompletable. Se configuran 3 parámetros:

  • La URL que actúa de fuente de datos, en nuestro caso la acción que hemos creado y que devuelve la lista JSON: ../autocompletarDiccionario.action
  • minLength:2, que pone límite al número de caracterers que como mínimo se van a esperar a que introduzca el usuario para hacer una llamada al origen de datos. Esto tiene su justificación en que si no se pone límite, cuando se pulse una sola tecla, devolverá un listado demasiado grande (por ejemplo todas las palabras que comiencen por A
  • Una función, en nuestro caso inline, que se ejecuta cuando el usuario selecciona uno de los términos como el correcto. En este caso muestra un alert indicando cuál ha sido seleccionado

Una vez lanzado el servidor Tomcat y arrancado PostgreSQL se puede comenzar a utilizar. Una captura de pantalla aparece a continuación:

Anuncios

3 comments

  1. Gracias Alberto, lo estoy implementado. Tendo dos dudas. Que contenido ha de tener “nosecarganunca.jsp”.
    Porque no usas anotaciones en la acción? como :

    @Action(value = “autocompletarDiccionario”, results = {
    @Result(name = “success”, location=”nosecarganada.jsp”)})
    public class AutocompletarDiccionario extends ActionSupport implements ServletResponseAware{

    ….
    return SUCCESS;
    }

    Gracias

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s