Consultas DBpedia con Java – Linked Data

Dentro de la Web semántica, una de las iniciativas que de a poco va ganando relevancia es la Web de datos o también conocida como Linked Data o también como Linked Open Data, aquí una presentación que explica Linked Data, que a través de 4 principios determina la forma en la que los datos deben ser descritos y publicados. Bajo estos preceptos se han generado varios Datasets, uno de ellos es DBpedia que es la versión semántica de la wikipedia. La mayoría de los Dataset poseen mecanismos de consulta que a través de SPARQL nos permiten tener acceso a la información que ahí se publica. Los mecanismos de consulta son servicios Web basados en REST.

En este post mostraremos como realizar consultas a la DBPedia a través de Java. Para ellos usaremos Jersey que es un API que no permite trabajar con servicios REST. Antes de pasar a realizar cualquier explicación les describiré brevemente la aplicación, el objetivo de la aplicación es encontrar el país al cual pertenece un punto cardinal expresado en latitud y longitud. Para ello vamos a realizar la siguiente consulta:

SELECT ?pais ?lat ?long WHERE {
?pais rdf:type <http://dbpedia.org/ontology/Country>.
?pais geo:lat ?lat FILTER (datatype(?lat) = xsd:float && (?lat-0.2273363048115043) < 0.005 && (-0.2273363048115043-?lat) < 0.005).
?pais geo:long ?long FILTER (datatype(?long) = xsd:float && (?long-78.892578125) < 0.005 && (-78.892578125-?long) < 0.005)
}

Para ejecutar la consulta y ver los resultados click aquí. Podemos ver en la consulta que estamos consultando los países (rdf:type <http://dbpedia.org/ontology/Country) que tenga sus valores de latitud y longitud como números tipos float (datatype(?lat) = xsd:float  y datatype(?long) = xsd:float) y ademas calculamos la diferencia en la latitud y longitud de cada país con los valores que los obtenemos de alguna manera (yo los obtuve con google maps). Si bien este método no es el mejor ha sido el que relativamente a funcionado mejor y según leí en algún lugar es una recomendación de la dbpedia.

Actualización 19/08/2010: La consulta anterior no dio buenos resultados por lo que busqué otras formas de consultar el país y encontré dos. La primera y que uso es invocando a un servicio, obviamente REST, CountryCode / reverse geocoding cuyo detalle lo pueden encontrar aquí – GeoNames. La segunda opción es usar Google Maps con su clase GClientGeoCoder y su método getLocations, detalles aquí. Que tiene su contraparte con la librería de Maps en GWT.

Con la consulta ya estructurada es hora de armar la URL del servicio que consumiremos. La URL tiene la siguiente forma: http://dbpedia.org/sparql?default-graph-uri=<valor>&query=<consulta>&output=<tipo_salida>

Para el ejemplo los valores son (para <consulta> el valor es la consulta SPARQL que les mostré anteriormente):

<valor> = http://dbpedia.org

<tipo_salida> = json

Veamos el código Java del programa


import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.WebResource;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import javax.ws.rs.core.MediaType;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
/**
*
* @author jorgaf
*/

public class ClienteDBpedia {
   public static final String WS_URL_DBPEDIA = "http://dbpedia.org/sparql";
   public static void main(String[] args) {
      String defaultGraph = "http://dbpedia.org";
      String qry = "SELECT ?nombre ?tGobierno ?capital ?area ?moneda "
                + "?imagen ?descripcion\n WHERE { \n"
                + "?pais rdf:type <http://dbpedia.org/ontology/Country>. \n"
                + "OPTIONAL{?pais dbpprop:areaKm ?area.\n"
                + "?pais dbpprop:currencyCode ?moneda.\n"
                + "?pais dbpedia-owl:capital ?capital}.\n"
                + "OPTIONAL{?gobierno rdf:type <http://dbpedia.org/class/yago/FormsOfGovernment>.\n"
                + "    ?pais dbpedia-owl:governmentType ?tipoGobierno FILTER (?tipoGobierno = ?gobierno)}.\n"
                + "?pais rdfs:label ?nombre FILTER langMatches( lang(?nombre), \"ES\" ).\n"
                + "OPTIONAL{?gobierno rdfs:label ?tGobierno FILTER langMatches(lang(?tGobierno), \"ES\")}.\n"
                + "?pais rdfs:label ?nombreEng FILTER (langMatches( lang(?nombreEng), \"EN\" ) && ?nombreEng = \"Ecuador\"@en).\n"
                + "?pais dbpedia-owl:abstract ?descripcion FILTER (langMatches( lang(?descripcion), \"ES\" )).\n"
                + "?pais dbpedia-owl:thumbnail ?imagen\n"
                + "}";
      String salida = "&output=json";
      String url = "";
      try {
         defaultGraph = URLEncoder.encode(defaultGraph, "UTF-8");
         qry = URLEncoder.encode(qry, "UTF-8");
      } catch (UnsupportedEncodingException ex) {}

      Client client = new Client();
      url = WS_URL_DBPEDIA + "?default-graph-uri=" + defaultGraph
         + "&query=" + qry + salida;
      WebResource resource = client.resource(url);
      String result = resource.accept("application/sparql-results+json").
         get(String.class);
   try {
      procesar(result);
   } catch (JSONException ex) {}
 }

 private static void procesar(String res) throws JSONException{
    JSONObject result = new JSONObject(res).getJSONObject("results");
    JSONArray bindings = result.getJSONArray("bindings");
    JSONObject pais;
    JSONObject lat, lon;

    for (int i = 0; i < bindings.length(); i++) {
       pais = bindings.getJSONObject(i).getJSONObject("pais");
       lat = bindings.getJSONObject(i).getJSONObject("lat");
       lon = bindings.getJSONObject(i).getJSONObject("long");
       System.out.printf("País: %s Lat: %s Lon: %s\n",
       pais.get("value"),
       lat.get("value"),
       lon.get("value"));
    }
  }
}

Podemos ver como es necesario codificar los valores de las default-graph-uri, query y output. El programa muestra también como trabajar con información en formato JSON que fue el valor que le asignamos para la salida, vea le método procesar para ver como se pueden obtener los datos. La salida que se obtiene son las siguientes:

{ "head": { "link": [], "vars": ["pais", "lat", "long"] },
"results": { "distinct": false, "ordered": true, "bindings": [
{ "pais": { "type": "uri", "value": "http://dbpedia.org/resource/Gabon" }        , "lat": { "type": "typed-literal", "datatype": "http://www.w3.org/2001/XMLSchema#float", "value": "0.3833333253860474" }        , "long": { "type": "typed-literal", "datatype": "http://www.w3.org/2001/XMLSchema#float", "value": "9.449999809265137" }},
{ "pais": { "type": "uri", "value": "http://dbpedia.org/resource/Kenya" }        , "lat": { "type": "typed-literal", "datatype": "http://www.w3.org/2001/XMLSchema#float", "value": "-1.266666650772095" }        , "long": { "type": "typed-literal", "datatype": "http://www.w3.org/2001/XMLSchema#float", "value": "36.79999923706055" }},
{ "pais": { "type": "uri", "value": "http://dbpedia.org/resource/S%C3%A3o_Tom%C3%A9_and_Pr%C3%ADncipe" }        , "lat": { "type": "typed-literal", "datatype": "http://www.w3.org/2001/XMLSchema#float", "value": "0.3333333432674408" }        , "long": { "type": "typed-literal", "datatype": "http://www.w3.org/2001/XMLSchema#float", "value": "6.733333110809326" }},
{ "pais": { "type": "uri", "value": "http://dbpedia.org/resource/Rwanda" }        , "lat": { "type": "typed-literal", "datatype": "http://www.w3.org/2001/XMLSchema#float", "value": "-1.943883299827576" }        , "long": { "type": "typed-literal", "datatype": "http://www.w3.org/2001/XMLSchema#float", "value": "30.05945014953613" }},
{ "pais": { "type": "uri", "value": "http://dbpedia.org/resource/Royal_Audience_of_Quito" }        , "lat": { "type": "typed-literal", "datatype": "http://www.w3.org/2001/XMLSchema#float", "value": "-0.25" }        , "long": { "type": "typed-literal", "datatype": "http://www.w3.org/2001/XMLSchema#float", "value": "-78.58333587646484" }},
{ "pais": { "type": "uri", "value": "http://dbpedia.org/resource/Ecuador" }        , "lat": { "type": "typed-literal", "datatype": "http://www.w3.org/2001/XMLSchema#float", "value": "-0.1500000059604645" }        , "long": { "type": "typed-literal", "datatype": "http://www.w3.org/2001/XMLSchema#float", "value": "-78.34999847412109" }},
{ "pais": { "type": "uri", "value": "http://dbpedia.org/resource/Somalia" }        , "lat": { "type": "typed-literal", "datatype": "http://www.w3.org/2001/XMLSchema#float", "value": "2.033333301544189" }        , "long": { "type": "typed-literal", "datatype": "http://www.w3.org/2001/XMLSchema#float", "value": "45.34999847412109" }} ] } }

Mientras que la salida ya procesada es la siguiente:


País: http://dbpedia.org/resource/Gabon Lat: 0.3833333253860474 Lon: 9.449999809265137
País: http://dbpedia.org/resource/Kenya Lat: -1.266666650772095 Lon: 36.79999923706055
País: http://dbpedia.org/resource/S%C3%A3o_Tom%C3%A9_and_Pr%C3%ADncipe Lat: 0.3333333432674408 Lon: 6.733333110809326
País: http://dbpedia.org/resource/Rwanda Lat: -1.943883299827576 Lon: 30.05945014953613
País: http://dbpedia.org/resource/Royal_Audience_of_Quito Lat: -0.25 Lon: -78.58333587646484
País: http://dbpedia.org/resource/Ecuador Lat: -0.1500000059604645 Lon: -78.34999847412109
País: http://dbpedia.org/resource/Somalia Lat: 2.033333301544189 Lon: 45.34999847412109

Hasta aquí no existe complicación alguna, pero lamentablemente y no sé porqué algunas veces el programa no funciona y presenta una excepción 406 Not Acceptable Actualización 19/08/2010: el problema anterior se resolvió cambiando la línea: resource.accept(MediaType.APPLICATION_JSON).get(String.class) por resource.accept(«application/sparql-results+json»).get(String.class); . La única forma de que trabaje es copiando la URL a la barra de dirección del navegador y luego de un par de intentos funciona en el navegador y luego en el programa Java. Si alguno de ustedes me puede ayudar con alguna solución se los agradeceré mucho.

Espero que les ayude y les sirva como base para futuros trabajos.

Un cliente Jersey que consume Google AJAX search API – Rest & Java

Uno de los API’s, dentro del mundo Java, que permiten trabajar con servicios Web basados en REST se denomina Jersey. Este API viene incluido con NetBeans desde ya hace algunas versiones atrás. Inclusive en el sitio de NetBeans existe documentación en donde se muestra como crear un servicio Web Rest basado en una tabla de una base de datos. Pero no existe ninguna documentación, al menos en sitio de NetBeans, que muestre como crear un cliente Jersey que consuma algún servicio Rest.

Buscando en la Web encontré un par de ejemplos. Uno de ellos Consuming RESTful Web Services With the Jersey Client API, desarrolla un cliente (en modo consola) de Twitter.

Otro ejemplo bastante sencillo, pero no menos ilustrativo se llama Yahoo Search Restful Client using Jersey, aquí se desarrolla un cliente que consume el servicio de búsqueda de Yahoo. Dicho servicio de búsqueda devuelve los resultados en XML así que es necesario usar JAXB Binding para obtener las clases que representaran la información.

No he encontrado un ejemplo en donde se trabaje con JSON, tomando en cuenta que ese formato es bastante popular hoy por hoy. Así que me decidí a elaborar un cliente Jersey que consuma el API de Google AJAX search que devuelve información en JSON. El código desarrollado es el siguiente:

package ecc.edu.utpl;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.WebResource;
import java.util.Scanner;
import javax.ws.rs.core.MediaType;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 *
 * @author jorgaf
 */
public class ClienteGoogle {

    public static void main(String[] args) {
        Scanner lector = new Scanner(System.in);
        String consulta;
        char opc = 'S';
        String URI = "http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=";
        Client cliente = Client.create();
        do {
            do {
                System.out.print("Ingrese el texto a buscar: ");
                consulta = lector.nextLine();
                consulta = consulta.trim();
                consulta = consulta.replace(" ", "%20");
            } while (consulta == null || consulta.length() == 0);

            URI += consulta;
            WebResource recurso = cliente.resource(URI);
            String result = recurso.accept(MediaType.APPLICATION_JSON).get(String.class);
            try {
                System.out.printf("---RESULTADOS:%s---\n", consulta);
                mostarResultados(result);
                System.out.printf("---%d páginas aprox.---\n",
                        obtenerTotalResultados(result));
            } catch (JSONException ex) {
                ex.printStackTrace();
            }
            System.out.println("\n-------------------------------");
            System.out.println("¿Desea realizar otra búsqueda?");
            System.out.print("Presione S(Si) o N(No): ");
            opc = lector.next().charAt(0);
            System.out.println();
        } while (opc != 'N');

    }

    public static void mostarResultados(String respuesta) throws JSONException {
        JSONObject objJSON = new JSONObject(respuesta);
        JSONObject responseData = objJSON.getJSONObject("responseData");
        JSONArray results = responseData.getJSONArray("results");
        for (int i = 0; i < results.length(); i++) {
            JSONObject link = results.getJSONObject(i);
            System.out.printf("URL: %s\n", link.getString("url"));
            System.out.printf("Title: %s\n", link.getString("title"));
            System.out.printf("Contenido: %s\n", link.getString("content"));
            System.out.println();
        }
    }

    public static long obtenerTotalResultados(String respuesta){
        long total = 0;
        try {
            JSONObject objJSON = new JSONObject(respuesta);
            JSONObject responseData = objJSON.getJSONObject("responseData");
            JSONObject cursor = responseData.getJSONObject("cursor");
            total = cursor.getLong("estimatedResultCount");
        } catch (JSONException ex) {
            ex.printStackTrace();
        }
        return total;
    }
}

Para poder realizar este cliente tuve que agregar las siguientes librerías adicionales:

Las tres primeras librerías se pueden descargar desde la Web haciendo clic en cada uno de los enlaces. Pero el último lo agregué desde NetBeans. La siguientes imágenes les guiarán en ese proceso.

Menu a utilizar para agregar la librería
Librería que debe agregar

Los métodos mostrarResultados y obtenerTotalResultados, son los encargados de trabajar con JSON. Para conocer la estructura JSON de la información devuelta por Google hagan clic aquí y busquen el tema «Entornos Flash y otros entornos que no utilizan JavaScript» que casi se encuentra al final.

Espero que este post les ayude.