XML en GNOME

Tabla de contenidos

libxml (o GNOME-XML)

XML es un formato, estándar, avalado por el W3C, que permite estructurar la información lógicamente. En contra de otros lenguajes de "marcación", como HTML, por ejemplo, XML no especifica la forma en la que la información debe ser mostrada, sino que sólo se ocupa de estructurarla de una forma lógica.

Esta característica (almacenamiento de información estructurada), añadida a la independencia de la forma de presentación de la información, hace de XML un formato ideal para almacenar datos en las aplicaciones, a la vez que, al ser un estándar reconocido internacionalmente, y, por tanto, soportado en multitud de aplicaciones, es tambien ideal para la compartición de datos entre aplicaciones, incluidas aplicaciones para distintos sistemas operativos.

Por todo esto, el proyecto GNOME, siguiendo con su afán de únicamente utilizar estándares reconocidos, vio en XML la herramienta ideal para el almacenamiento de datos. Y así, ya desde las primeras versiones, multitud de aplicaciones desarrolladas para el proyecto GNOME hacen uso de XML, como por ejemplo, Gnumeric, Dia, GNOME-DB, AbiWord, etc. Estos usos van desde el simple almacenamiento de datos de configuración (GConf), pasando por el formato de ficheros propio (Glade, Dia, Gnumeric), hasta usos más avanzados como la transferencia de datos entre SGBDR (GNOME-DB) o mecanismos de RPC ("Remote Procedure Call", Llamadas a procedimientos remotos) recientemente incorporados con la liberación de Soup™, que es una implementación de SOAP desarrollada por Ximian.

libxml (o GNOME-XML)

Como ha ocurrido con otras partes de la arquitectura, es dentro del propio proyecto GNOME donde se ha implementado todo lo necesario para integrar XML dentro de la arquitecura, y así, está disponible libxml, tambien conocida como GNOME-XML, que es una librería que implementa un analizador sintáctico XML que permite fácilmente hacer uso de XML en aplicaciones. Es de agradecer que, a pesar de haber sido desarrollada en el seno del proyecto GNOME, esta librería no tiene ninguna dependencia con este entorno, por lo que puede ser usada perfectamente en aplicaciones totalmente ajenas a GNOME, si así se desea.

Características

libxml™ incluye todo lo que cabría esperar de una librería de estas características. Es decir, con libxml™ se puede leer, validar y modificar documentos XML, todo ello de una forma bastante clara y sencilla.

Para manejar documentos XML con libxml™, se utilizan dos tipos de datos básicos: xmlDocPtr, usado para representar un documento XML, y xmlNodePtr, que se usa para representar cada uno de los nodos contenidos en los documentos XML procesados en los programas. Estos dos tipos de datos son simplemente punteros a dos estructuras que estan definidas en los ficheros de cabecera de libxml™, y más concretamente en el fichero tree.h. Estas estructuras son xmlDoc y xmlNode, cuyo contenido debe ser conocido, pues a la hora de trabajar con libxml™, se usa continuamente para acceder a las propiedades de los objetos XML (documentos, nodos, etc) asociados.

Aunque este libro se centra en la libxml2™, que es la usada en todo GNOME 2, es importante conocer que existen una versión anterior de esta librería, conocida como libxml1™, que aun es usada por multitud de aplicaciones (muchas de ellas ajenas al proyecto GNOME). Por ello, es indispensable conocer alguna de las diferencias entre las dos versiones, para evitar problemas de compilación segun se use una u otra versión. Una de estas diferencias radica en los nombres de algunos campos de estas estructuras comentadas anteriormente. Así, se deben usar una serie de #define's que se encuentran en los ficheros de cabecera ambas versiones, que permiten usar el mismo código para las dos versiones. Estos #define's son:

  • xmlChildrenNode, usado para acceder a los nodos hijo de un determinado nodo (xmlNode).

  • xmlRootNode, usado para acceder al nodo raiz de un documento XML (xmlDoc).

Se deberan usar estos nombres para acceder a los campos especificados de las estructuras, para así asegurar que la aplicación escrita va a poder ser compilada sin cambios con cualquiera de las dos versiones de libxml™.

Otra cosa a destacar es que libxml™ no usa GLibGLib™, por lo que no usa los tipos de datos definidos en esta librería. Más bien, incluso define los suyos propios, como es el caso de xmlChar, que es el equivalente, en libxml™ del gchar de glib™ (y del char de C). Por supuesto, ambas librerías son perfectamente compatibles, por lo que se pueden usar las dos al mismo tiempo sin ningun problema. De hecho, eso es lo que se va a hacer en los ejemplos de código de este artículo.

Hechas estas aclaraciones, es tiempo de pasar al uso "real" de la librería libxml™, para ello, se usará el siguiente ejemplo:

Hay que almacenar en disco los datos de nuestra aplicación de gestión de libros. Para almacenar los datos de cada libro, se usa una estructura definida de la siguiente manera:

	  typedef struct _libro libro;

	  struct _libro {
	        gchar *titulo;
	        gchar *autor;
	        gchar *tema;
	  }
	

Se quiere guardar la información en un documento XML. Para ello, se podría crear el siguiente documento:

	  <listado_libros>
	    <libro autor="José Ortega y Gasset" titulo="La rebelión de las masas" tema="Filosofia"/>
	  </listado_libros>
	

o bien este otro:

	  <listado_libros>
	    <libro>
	      <titulo>La rebelión de las masas</titulo>
	      <autor>José Ortega y Gasset</autor>
	      <tema>Filosofia</tema>
	    </libro>
	  </listado_libros>
	

A lo largo del capítulo se explicará la forma de hacerlo de las dos maneras.

Carga de documentos

Un documento XML es, a fin de cuentas, una larga cadena de caracteres. Es decir, que un documento XML no tiene porqué estar asociado a un fichero en el disco, aunque, por supuesto, puede estarlo. Así, libxml™ ofrece dos funciones para permitir la carga de documentos XML tanto leidos del disco, como desde una posición de memoria previamente inicializada (por ejemplo, si se obtuviera el documento XML a través de Internet, sería almacenado en una posición de memoria determinada). Estas dos funciones son:

	  xmlDocPtr xmlParseMemory (char *buffer, int size);
	  xmlDocPtr xmlParseFile (const char *filename);
	

La diferencia entre ellas, como ya se ha comentado anteriormente, es que una (xmlParseMemory) carga el documento desde una posición de memoria específica (buffer), mientras que la otra (xmlParseFile) lo hace desde el fichero especificado como parámetro.

Así, por ejemplo, para cargar un fichero XML, se usaría el siguiente código:

	  xmlDocPtr doc;

	  doc = xmlParseFile("/home/rodrigo/gastos.xml");
	  if (!doc) {
	    g_print("Error al cargar documento XML\n");
	  }
	

Tras ejecutar este código, y si no se produce ningun error, se obtiene, en la variable doc, una referencia al documento XML. Esta variable, de tipo xmlDocPtr, va a permitir el acceso al contenido del documento XML cargado, en este caso, del fichero /home/rodrigo/gastos.xml.

Similarmente, si el documento no está en un fichero, sino que, por ejemplo, se obtiene a través de Internet, como hemos mencionado antes, usariamos el siguiente código:

	  xmlDocPtr doc;
	  gchar *buffer;

	  /* llamamos a la función (ficticia) que nos devuelve el documento XML */
	  buffer = obtener_documento_xml("http://mi.servidor.com/gastos.xml");
	  doc = xmlParseMemory(buffer, strlen(buffer));
	  ...
	

Una vez que se tiene el documento XML cargado en memoria, es decir, cargado en una variable de tipo xmlDocPtr, ya sólo queda, para terminar de procesar el documento, acceder a su contenido. Para ello, se usan los campos de la estructura xmlDoc y xmlNode.

Así, lo primero que se tiene que hacer es obtener una referencia al nodo raiz del documento. Para ello, se usa la función xmlDocGetRootElement:

	  xmlNodePtr root;

	  root = xmlDocGetRootElement(doc);
	

La función xmlDocGetRootElement devuelve un valor de tipo xmlNodePtr, que no es más que un puntero al nodo raiz del documento. A su vez, cada nodo XML puede tener otros nodos colindantes (es decir, al mismo nivel), así como otros nodos dependiendo de él (es decir, nodos hijo). Así, para acceder a toda esta información almacenada en nodos, se usa, como es de esperar, el tipo xmlNodePtr. Por ejemplo, para recorrer la lista de nodos de primer nivel del documento XML que hemos cargado, se usaría el siguiente código:

	  xmlDocPtr doc;
	  xmlNodePtr root;
	  xmlNodePtr node;

	  /* abrimos documento y obtenemos referencia al nodo raiz */
	  doc = xmlParseFile ("/home/rodrigo/gastos.xml");
	  root = xmlDocGetRootElement (doc);
	  node = root->xmlChildrenNode;
	  while (node != NULL) {
	    g_print ("Encontrado nodo %s\n", node->name);
	    node = node->next;
	  }
	

Como se aprecia en este ejemplo, se accede directamente a los campos de la estructura xmlNode. Más concretamente, primero se usa

root->xmlChildrenNode

, que devuelve una referencia al primer hijo de este nodo, en este caso el nodo raiz, para luego usar node->name, que contiene el nombre de la etiqueta XML del nodo al que se hace referencia. Así, por ejemplo, en el caso de la etiqueta <libro>, node->name contendría el valor "libro". Seguidamente, se usa node->next, que apunta al siguiente nodo en la lista, es decir, al siguiente nodo colindante con el actual.

Cada nodo puede tener a su vez otros nodos dependiendo de él. Así, si este fuera el caso, se podría cambiar el código anterior por lo siguiente:

	  ...
	  while (node != NULL) {
	    xmlNodePtr children;
	    g_print("Encontrado nodo %s\n", node->name);

	    /* procesamos los hijos de este nodo */
	    children = node->xmlChildrenNode;
	    while (children != NULL) {
	      g_print("\tEncontrado hijo %s\", children->name);
	      children = children->next;
	    }
	    node = node->next;
	  }
	

Por supuesto, este bucle puede tornarse casi infinito, pues a su vez, los nodos hijos de node (children) pueden a su vez contener otros nodos, y éstos otros, y así sucesivamente.

Siguiendo con el ejemplo, se va a crear una función que cargue el documento que especificad como parámetro y devuelva un puntero a una estructura xmlDoc.

	  xmlDocPtr xml_open_file (const gchar *filename) {
	       xmlDocPtr doc;

	       doc = xmlParseFile (filename);

	       if (!doc) {
	            g_print ("Error al cargar el documento %s \n", filename);
	            return NULL;
	       }

	       return doc;
	  }
	

Cada entrada del documento se corresponde con una estructura de tipo libro, así que se creará una función que tendrá como parámetro de entrada el nodo a leer y que devolverá la estructura libro que contiene. Para cargar el primero de los formatos propuestos, se usa el siguiente código:

	  libro *xml_get_entry (xmlNodePtr child)
	  {
	       libro *lb;

	       lb = g_new0 (libro, 1);

	       lb->titulo = xmlGetProp (child, "titulo");
	       lb->autor = xmlGetProp (child, "autor");
	       lb->tema = xmlGetProp (child, "tema");

	       return lb;
	  }
	

Para obtener las propiedades del nodo, en este caso titulo, autor y tema se usa la función xmlGetProp que, como puede observarse en el ejemplo, recibe como parámetros el nodo a leer y la propiedad a recuperar.

Se puede usar esta función para cargar el documento "libros.xml" en un GtkCList (aunque este widget ha quedado obsoleto en GNOME2) de la siguiente manera:

	  xmlNodePtr root, child;
	  xmlDocPtr doc;

	  gchar *text[3];
	  libro lb;

	  doc = xml_open_file ("libro.xml");

	  root = xmlDocGetRootElement (doc);

	  child = root->xmlChildrenNode;

	  while (child != NULL) {
	       lb = xml_get_entry (child);
	       text[0] = lb.autor;
	       text[1] = lb.titulo;
	       text[2] = lb.tema;

	       gtk_clist_append (GTK_CLIST (clist_book), text);
	       child = child->next;
	  }
	

Para cargar el segundo formato propuesto, la función xml_get_entry será algo diferente, en primer lugar no se usará la función xmlGetProp que recupera el valor de una propiedad, si no que se usará la función xmlNodeGetContent que devuelve el contenido del nodo. La nueva función quedará así:

	  libro *xml_get_entry (xmlNodePtr child)
	  {
	       xmlNodePtr node;
	       libro *lb;

	       lb = g_new0 (libro, 1);

	       node = child->xmlChildrenNode;

	       lb->titulo = xmlNodeGetContent (node);
	       node = node->next;

	       lb->autor = xmlNodeGetContent (node);
	       node = node->next;

	       lb->tema = xmlNodeGetContent (node);

	       return lb;
	  }
	

En primer lugar, el parametro que recibe esta función debe ser una entrada, en este caso el nodo con etiqueta <libro>, a la variable node se le asigna el primer nodo hijo usando child->xmlChildrenNode y se obtiene su contenido mediante la función xmlNodeGetContent y con la asignación node = node->next se avanza al siguiente nodo.

Para cargar el documento "libro.xml" sería válido el mismo ejemplo citado anteriormente, ya que son las modificaciones realizadas en la función xml_get_entry las que procesarán correctamente el documento.

Creación de ficheros XML

Cargar documentos XML es muy útil, pero de nada serviría si no se pudiera, paralelamente, crear esos documentos XML. Así, libxml™ incluye tambien funciones tanto para salvar documentos XML a disco o a memoria, como para crear/modificar el contenido de dichos documentos.

Lo primero que querremos hacer para crear de cero un documento XML es crear la estructura xmlDoc asociada, que posteriormente usaremos, como hemos visto en el apartado anterior, para acceder al contenido del documento. Así, la primera función que usaremos es xmlNewDoc:

	  xmlDocPtr doc;

	  doc = xmlNewDoc("1.0");
	

El parámetro que recibe esta función es el número de versión de la especificación XML que queremos usar para nuestro documento. De momento, simplemente usaremos la 1.0.

Esta función nos devuelve un valor de tipo xmlDocPtr que, como vemos, es el mismo que usamos para cargar el documento desde un fichero/posición de memoria.

Así pues, lo primero que debemos hacer es crear la estructura xmlDoc, para ello creamos una función que nos devuelve un puntero a la estructura xmlDoc y como parametro le pasamos el nombre que queremos darle al nodo raiz:

			/* crea la estructura xmlDoc con nodo raiz "name" */
			xmlDocPtr xml_new_doc (const gchar *name) {

				xmlNodePtr root;
				xmlDocPtr doc;

				doc = xmlNewDoc ("1.0");

				root = xmlNewDocNode (doc, NULL, name, NULL);
				xmlDocSetRootElement (doc, root);

				return doc;
			}
		

Como vemos, se usa la función xmlNewDocNode para crear el nodo raiz. A esta función, aparte de la referencia a un documento XML (doc), le pasamos el nombre de la etiqueta XML raiz, en este caso, como vemos en el documento XML mostrado anteriormente, "listado_libros". Seguidamente, usamos la función xmlDocSetRootElement para asociar el nodo recien creado con el documento XML especificado.

Pero, como es de suponer, en este caso el documento está vacío. Para añadirle contenido, lo que hacemos es crear nuevos nodos y añadirlos a otros nodos, aunque lo primero que tendremos que hacer es crear el nodo raiz de nuestro documento. Para ello, suponiendo que queramos crear un documento con el primer formato propuesto podemos crear la siguiente función:

			void xml_new_entry (xmlDocPtr doc, libro nuevo) {

				xmlNodePtr root;
				xmlNodePtr node;

				/* nodo raiz */
				root = xmlDocGetRootElement (doc);

				node = xmlNewChild (root, NULL, "libro", NULL);

				xmlSetProp (node, "titulo", nuevo.titulo);
				xmlSetProp (node, "autor", nuevo.autor);
				xmlSetProp (node, "tema", nuevo.tema);
			}
		

Como se puede apreciar, el funcionamiento es similar a la creación del nodo raiz. En este caso, usamos la función xmlNewChild, a la que le pasamos una referencia al nodo padre (en este caso usamos el nodo raiz, pero podriamos haber usado cualquier otro), así como el nombre de la etiqueta XML del nodo, en este caso "libro". Seguidamente, usamos la función xmlSetProp para establecer las propiedades del nodo, que en este caso son autor, titulo y tema. Estas propiedades como hemos visto en la sección de carga de documentos las podemos más tarde recuperar mediante la función xmlGetProp, que nos devuelve el valor contenido en la propiedade especificada.

Así, con esta función, nuestro programa para escribir el documento XML mostrado anteriormente quedaría de la siguiente forma:

		xmlNodePtr root, child;
		xmlDocPtr doc;
		libro lb;

		doc = xml_new_doc ("listado_libros");

		lb.autor = "José Ortega y Gasset";
		lb.titulo = "La rebelión de las massas";
		lb.tema = "Filosofia";

		xml_new_entry (doc, lb);

		lb.autor = "José Luis Balcazar";
		lb.titulo = "Programación metódica";
		lb.tema = "Informática";

		xml_new_entry (doc, lb);
		

Obteniendo como resultado el siguiente documento:

			<?xml version="1.0"?>
			<listado_libros>
				<libro titulo="La rebelión de las massas" autor="José Ortega y Gasset" tema="Filosofia"/>
				<libro titulo="Programación metódica" autor="José Luis Balcazar" tema="Informática"/>
			</listado_libros>
		

Para crear un documento con el segundo formato propuesto, deberemos modificar la función xml_new_entry ya que en lugar de añadir propiedades ahora añadiremos contenido al nodo utilizando para ello la función xmlNewChild que además de crear un nuevo nodo, nos permite añadirle contenido. La función quedaria de la siguiente manera:

			/* añade una nueva entrada al documento */
			/* una entrada es de tipo libro */
			void xml_new_entry (xmlDocPtr doc, libro nuevo) {

				xmlNodePtr root;
				xmlNodePtr libro;

				/* nodo raiz */
				root = xmlDocGetRootElement (doc);

				libro = xmlNewChild (root, NULL, "libro", NULL);

				xmlNewChild (libro, NULL, "titulo", nuevo.titulo);
				xmlNewChild (libro, NULL, "autor", nuevo.autor);
				xmlNewChild (libro, NULL, "tema", nuevo.tema);
			}
		

Podemos observar los pasos para crear la entrada en el documento. Primero obtenemos el nodo raiz mediante la función xmlDocGetRootElement y después empezamos con la nueva entrada al documento creando primero el nodo hijo usando la función xmlNewChild, ha esta función le pasamos como argumentos el nodo padre (root) y la etiqueta de nodo (libro). Para introducir los datos, utilizamos la misma función xmlNewChild aunque de forma diferente, en este caso le pasamos como nodo padre (libro) la etiqueta que queremos darle al nuevo nodo (titulo) y el contenido a almacenar en el (nuevo.titulo).

Obteniendo un archivo libro.xml con el siguiente contenido:

			<?xml version="1.0"?>
			<listado_libros>
				<libro>
					<titulo>La rebelión de las masas</titulo>
					<autor>José Ortega y Gasset</autor>
					<tema>Filosofia</tema>
				</libro>
				<libro>
					<titulo>Programación metódica</titulo>
					<autor>José Luis Balcazar</autor>
					<tema>Informática</tema>
				</libro>
			</listado_libros>
		

Salvando documentos XML

Ya sólo nos queda, para terminar esta introducción al uso de libxml™, ver cómo almacenar las documentos que modifiquemos en nuestro programas. Para ello, al igual que ocurría para la carga de documentos, libxml™ nos ofrece un conjunto de funciones que podremos usar para almacenar los documentos XML tanto en disco como en memoria.

En primer lugar, tenemos las funciones:

	  void xmlDocDumpMemory (xmlDocPtr doc, xmlChar **buffer, int *size);
	  int  xmlSaveFile      (const char *filename, xmlDocPtr doc);
	

que nos permiten salvar un documento XML (referenciado por una variable de tipo xmlDocPtr tanto en memoria (xmlDocDumpMemory) como en disco (xmlSaveFile).

Junto a estas dos funciones, tenemos otras dos que pueden sernos realmente útiles en algunas ocasiones:

	  void xmlDocDump  (FILE *f, xmlDocPtr doc);
	  void xmlElemDump (FILE *f, xmlDocPtr doc, xmlNodePtr elem);
	

Como vemos, ambas funciones tienen en comun que tienen un parámetro de tipo FILE (un tipo usado en las funciones de E/S de C), por lo que pueden sernos útiles, como comentábamos anteriormente, en el caso de que ya tengamos un fichero abierto. Además, la segunda de ellas (xmlElemDump) es especialmente útil si lo que deseamos es almacenar en disco simplemente un nodo XML y todos los nodos que cuelguen de él, es decir, sin necesidad de almacenar todo el documento, sólo se almacena una parte de él.

Otra función interesante es xmlFreeDoc (xmlDocPtr cur) que nos permite liberar la memoria utilizada por un documento.

Una característica muy interesante de libxml™ es que permite almacenar los documentos XML comprimidos, de forma que ocupen menos espacio en disco. Para ello, podemos usar las siguientes funciones:

	  int xmlGetDocCompressMode (xmlDocPtr doc);
	  void xmlSetDocCompressMode (xmlDocPtr doc, int mode);
	  int xmlGetCompressMode (void);
	  void xmlSetCompressMode (int mode);
	

Las dos primeras nos permiten establecer/obtener el modo de compresión de un documento previamente cargado, mientras que las dos últimas nos permiten establecer/obtener el modo de compresión por defecto de la librería, es decir, el que se usará por defecto si no es especificado por cada documento.