Tutorial: creando aplicaciones basadas en Linked Data (parte 2/3)
En el artículo anterior vimos los principios básicos para crear consultas SPARQL, enviarlas a un endpoint y obtener un resultado. Generalmente los resultados obtenidos en un browser serán entregados como una tabla HTML, sin embargo, los endpoint pueden entregar resultados en otros formatos, como veremos a continuación.
Javascript y jQuery
Por años, el uso de Javascript en páginas y aplicaciones web sólo significaba una cosa para los desarrolladores: sufrimiento. Esto porque era difícil hacer lo mismo en todos los browsers, el código estaba mezclado con la presentación (entre medio de los tags HTML), entre otros. Pero como dice Aldrin Martoq el lenguaje no es lo más importante, sino las bibliotecas que se crean encima de él. Es así que jQuery se ha transformado en el estándar de facto a la hora de utilizar Javascript.
AJAX
Probablemente muchos conozcan AJAX (Asychronous JavaScript and XML, XML y JavaScript Asíncrono) Es un método para obtener datos de manera dinámica y asíncrona utilizando Javascript. En mi experiencia, XML como formato de intercambio es muy poco usado y ha sido reemplazado por JSON.
Ejemplo básico
Aunque Javascript es un lenguaje multiparadigma, jQuery tiene un enfoque más funcional. Otra característica muy útil de jQuery es que hace muy fácil el manejo de los diferente elementos de una página web. Para empezar, lo primero es esperar a que todos los elementos de la página (imágenes, CSS, etc.) hayan sido cargados. Así por ejemplo:
Puedes ver esta página acá.
La línea $(document).ready(function() { espera a que los archivos necesarios de la página web hayan sido cargados. Una vez ahí se ejecuta la función, que en este ejemplo tiene una sola instrucción. Esta instrucción ($("#results").append("Hola Mundo");) se traduce como "Anda al elemento cuyo atributo id sea 'results' (el div) y dentro de éste agrega al final el text 'Hola Mundo'".
Usando eventos
En Javascript es posible definir eventos (cuando el usuario hace click sobre o pasa el mouse encima de un elemento). Los eventos son útiles para darle dinamismo a las páginas y es algo que usaremos después. En jQuery podemos definir eventos para elementos que incluso no existen todavía, usando .live():
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
$(".boton").live('click', function(e){
$("#results").append("Hola Mundo");
});
});
</script>
</head>
<body>
<button class="boton">Presiona!</button>
<div id="results"></div>
</body>
</html>
Puedes ver esta página acá.
En este caso, definimos un botón de clase "boton" y además definimos un evento sobre cualquier elemento de clase boton (definido por el selector ".boton") sobre el cual se haga click: Al ocurrir este evento, se agregará al final del elemento results un "Hola Mundo".
Mi primer AJAX
Como mencioné anteriormente, ajax permite la carga asíncrona de datos. Éstos pueden venir de un archivo de texto plano, una base de datos relacional, etc... Nuestro primer ejemplo será
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
$.ajax({
dataType: 'json',
url: 'datos.json',
success: function(data){
$("#nombre").append(data.nombre);
$("#direccion").append(data.direccion);
$("#twitter").append('<a href="http://twitter.com/'+data.twitter+'">'+data.twitter+'</a>');
$(data.webpage).each(function(i, item){$("#paginas").append("<a href='"+item+"'>"+item+"</a> ")});
}
});
});
</script>
</head>
<body>
<div>Nombre: <span id="nombre"></span></div>
<div>Dirección: <span id="direccion"></span></div>
<div>Twitter: <span id="twitter"></span></div>
<div>Páginas: <span id="paginas"></span></div>
</body>
</html>
Resultado disponble acá.
El código contenido dentro de $.ajax define qué y cómo vamos a obtener datos: dataType indica que esperamos un objeto JSON (y no XML), url indica en qué dirección está la información que queremos. La función definida en success indica qué ocurrirá cuando obtengamos los datos. En este caso utilizamos nuestro conocido método append para agregar información dentro de los tags del HTML. Al respecto, quiero comentar varios puntos importantes:
- El agregar código (como los elementos <a href...) como texto es una mala práctica, pero por ahora simplifica el código).
- Como queremos incluir todas las páginas web listadas en un arreglo, debemos iterar utilizando .each()
- Finalmente, por problemas de seguridad no es posible hacer una petición AJAX a otros dominios (ej. si mi javascript está en http://foo.com y el objeto JSON está en http://bar.net). Para esto se utiliza un truco llamadoJSON con Padding (JSONP), que básicamente consiste en que quien sirve el objeto JSON lo "envuelva" como una función Javascript (usando un parámetro entregado por quien hace la petición). En jQuery, casi siempre basta con cambiar dataType: 'json' por dataType: 'jsonp', pero siempre es bueno chequear la documentación de $.ajax()
SPARQL + jQuery + AJAX = Mi primera aplicación
Lo que necesitamos para crear nuestra primera (y muy sencilla) aplicación es unir lo que vimos en la primera parte de este tutorial sobre SPARQL con lo que hemos visto ahora. Tomaremos el código anterior y agregaremos algunas cosas:
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
var q='PREFIX dcterms: <http://purl.org/dc/terms/>\
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\
PREFIX dbp: <http://dbpedia.org/ontology/>\
\
SELECT ?poeta ?nombrePoeta ?fechaNacimiento ?fechaFallecimiento WHERE{\
?poeta dcterms:subject <http://dbpedia.org/resource/Category:Chilean_poets>;\
rdfs:label ?nombrePoeta ;\
dbp:birthDate ?fechaNacimiento ;\
dbp:deathDate ?fechaFallecimiento .\
FILTER (LANG(?nombrePoeta) = "es")\
}';
$.ajax({
dataType: 'jsonp',
data: {
query: q,
format: 'application/sparql-results+json'
},
url: 'http://dbpedia.org/sparql',
success: function(data){
$(data.results.bindings).each(function(i, item){
$("#results").append("<tr><td><a href='"+item.poeta.value+"'>"+item.nombrePoeta.value+"</a></td>\
<td>"+item.fechaNacimiento.value+"</td></tr>");
});
}
});
});
</script>
</head>
<body>
<table id="results">
<tr><th>Poeta</th><th>Fecha Nacimiento</th></tr>
</table>
</body>
</html>
Resultado disponible acá.
En primer lugar definimos la variable q en la que guardamos la consulta SPARQL de la primera parte de este tutorial (los \ son sólo para poder tener variables multilínea). El comando $.ajax() es muy similar al anterior, salvo por un par de cosas:
- Cambiamos dataType a jsonp. Esto se hace para acceder a servicios en otro dominio.
- Cambiamos el campo url a http://dbpedia.org/sparql que es el endpoint de DBpedia.
- Además debemos indicarle al endpoint qué consulta queremos enviar, por lo que usamos el campo data, el cual permite definir qué parametros enviar. En nuestro caso queremos enviar la consulta como parámetro query y obligar al endpoint a devolvernos un objeto JSON, por lo que usamos format: 'application/sparql-results+json'.
Lo que obtenemos de dbpedia es algo como
"results": { "distinct": false, "ordered": true, "bindings": [
{ "poeta": {
"type": "uri", "value": "http://dbpedia.org/resource/V%C3%ADctor_Jara" } ,
"nombrePoeta": {
"type": "literal",
"xml:lang": "es",
"value": "V\u00EDctor Jara"
}, "fechaNacimiento": {
"type": "typed-literal",
"datatype": "http://www.w3.org/2001/XMLSchema#date",
"value": "1932-09-28"
}, "fechaFallecimiento": {
"type": "typed-literal",
"datatype": "http://www.w3.org/2001/XMLSchema#date",
"value": "1973-09-16"
}
},
{ "poeta": ....
Esta es la estructura estándar que retorna una consulta SPARQL. Lo que nos interesa por ahora es lo que está en cada value de cada variable, por lo que iteramos sobre data.results.bindings y llenamos la tabla results.
ACTUALIZACION: Cómo separar consulta de código
Varias personas me han indicado correctamente que tener la consulta junto al código y el html es una mala práctica. Aunque resulta práctico en términos del tutorial tener un solo pedazo de código para copiar y pegar, por completitud aquí va la versión del ejemplo anterior en 3 archivos separados:
Primero, el HTML, al que llamaremos ex3b.html:
En esta página incluimos ex3b.js, el cual queda definido como:
$.ajax({
dataType: 'text',
url: 'ex3b.sparql',
success: function(data){
llamaDBpedia(data);
}
});
function llamaDBpedia(q){
$.ajax({
dataType: 'jsonp',
data: {
query: q,
format: 'application/sparql-results+json'
},
url: 'http://dbpedia.org/sparql',
success: function(data){
$(data.results.bindings).each(function(i, item){
$("#results").append("<tr><td><a href='"+item.poeta.value+"'>"+item.nombrePoeta.value+"</a></td><td>"+item.fechaNacimiento.value+"</td></tr>");
});
}
});
}
});
Esta versión es una modificación al código anterior: Primero buscamos la consulta en ex3b.sparql y una vez obtenida, llamamos a llamaDBpediacon ella (recordar que las llamadas AJAX son asíncronascon ella). Finalmente, el archivo ex3b.sparql consiste en
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX dbp: <http://dbpedia.org/ontology/>
SELECT ?poeta ?nombrePoeta ?fechaNacimiento ?fechaFallecimiento WHERE{
?poeta dcterms:subject <http://dbpedia.org/resource/Category:Chilean_poets>;
rdfs:label ?nombrePoeta ;
dbp:birthDate ?fechaNacimiento ;
dbp:deathDate ?fechaFallecimiento .
FILTER (LANG(?nombrePoeta) = "es")
}
El resultado final se puede ver acá.
Por brevedad, mantendré el resto de los ejemplo de la forma original, pero las consultas, el código y de la presentación no debiesen ir todos mezclados. Otra opción que muchos SPARQL endpoints permiten es enviar la URL de la consulta SPARQL directamente (usando query-uri en vez de query). Sin embargo está opción generalmente no está disponible cuando la URL no pertenece al mismo dominio, por lo que no nos sirve para nuestros ejemplos con DBpedia.
Consultando datos similares
Para terminar, podemos buscar personas que hayan nacido el mismo día que algún poeta. Para ello, tomaremos la URI de un poeta en particular. En este caso, la de Nicanor Parra con la que haremos la siguiente consulta:
PREFIX dbp: <http://dbpedia.org/ontology/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT DISTINCT ?persona ?nombre WHERE{
?persona rdfs:label ?nombre ;
dbp:birthDate ?x .
<http://dbpedia.org/resource/Nicanor_Parra> dbp:birthDate ?x .
FILTER(LANG(?nombre) = "en")
}LIMIT 5
Esto se traduce en algo como "Dame todas las URIs de algo que tiene un nombre y cuya fecha de nacimiento sea igual a la de http://dbpedia.org/resource/Nicanor_Parra. Además, el nombre debe estar en idioma inglés". Esto último lo hacemos simplemente para que no se repita la misma persona con su nombre en inglés, castellano, japonés, etc.
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
var q='PREFIX dcterms: <http://purl.org/dc/terms/>\
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\
PREFIX dbp: <http://dbpedia.org/ontology/>\
\
SELECT ?poeta ?nombrePoeta ?fechaNacimiento WHERE{\
?poeta dcterms:subject <http://dbpedia.org/resource/Category:Chilean_poets>;\
rdfs:label ?nombrePoeta ;\
dbp:birthDate ?fechaNacimiento .\
FILTER (LANG(?nombrePoeta) = "es")\
}';
$.ajax({
dataType: 'jsonp',
data: {
query: q,
format: 'application/sparql-results+json'
},
url: 'http://dbpedia.org/sparql',
success: function(data){
$(data.results.bindings).each(function(i, item){
$("#results").append("<tr><td class='persona'>"+item.nombrePoeta.value+"</td>\
<td><a href='"+item.poeta.value+"' class='uri'>"+item.nombrePoeta.value+"</a></td>\
<td class='nacimiento'>"+item.fechaNacimiento.value+"</td>");
});
}
});
$(".persona").live('click', function(e){
var uri = $(this).siblings().children("a.uri").attr("href");
var otros = 'PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\
PREFIX dbp: <http://dbpedia.org/ontology/>\
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\
PREFIX foaf: <http://xmlns.com/foaf/0.1/>\
SELECT DISTINCT ?persona ?nombre WHERE{\
?persona rdfs:label ?nombre ;\
dbp:birthDate ?x .\
<'+uri+'> dbp:birthDate ?x .\
FILTER(LANG(?nombre) = "en")\
}LIMIT 5';
$.ajax({
dataType: 'jsonp',
data: {
query: otros,
format: 'application/sparql-results+json',
},
url: 'http://dbpedia.org/sparql',
success: function(data){
$("#otros").html("<tr><th>Otras personas que nacieron el mismo día</th></tr>");
$(data.results.bindings).each(function(i, item){
$("#otros").append("<tr><td><a href='"+item.persona.value+"'>"+item.nombre.value+"</a></td></tr>")
});
}
});
});
});
</script>
</head>
<body>
<table id="results" style="float: left">
<tr><th>Poeta</th><th>URI</th><th>Fecha Nacimiento</th></tr>
</table>
<table id="otros" style="float: right">
<tr></tr>
</table>
</body>
</html>
Resultado disponble acá.
En este último ejemplo, agregamos un evento sobre las entidades de clase "persona" (la primera columna de la tabla), de modo que al hacer click realice una consulta SPARQL basada en la URI de la persona (Dada la estructura de la tabla, obtener la URI usando var uri = $(this).siblings().children("a.uri").attr("href"); es un poco truculento ya que busca "el hermano del elemento donde se hizo click --es decir el otro <td%gt;-- y luego el "hijo" de ese, que sea de tipo anchor (a) y clase uri. Finalmente se soma el valor del atributo href de éste).
Como último paso, utilizamos ese valor para completar nuestra consulta, la ejecutamos y llenamos la tabla otros con los valores obtenidos.
Conclusiones
Es posible seguir extendiendo estos ejercicios mucho más. Por ejemplo, buscar los lugares donde nacieron estos poetas y obtener la latitud y longitud de éstos para luego ponerlos en un mapa. También es posible utilizar datos en otros endpoints, basados en las URIs obtenidas en DBpedia, aunque sospecho que no hay mucha información sobre poetas chilenos como Linked Data. Ahora les invito a que empiecen a jugar con este código y como siempre los comentarios y críticas son siempre bienvenidos :-)











