En los títulos y los textos vais a encontrar unas cuantas citaciones cinematográficas (y si, soy un cinéfilo). Si no os interesan podéis fingir no verlas, ya que no son fundamentales para la comprensión de los post...

Este blog es la versión en Español de mi blog en Italiano L'arte della programmazione in C. Espero que mis traducciones sean comprensibles...

sábado, 28 de noviembre de 2015

Mission: Impossible - XML Serializer
cómo escribir un XML Serializer en C

La misión de hoy, en realidad, no es en absoluto imposible: escribir un XML Serializer en C, sobre todo con la ayuda de una excelente biblioteca open-source como libXML2 (licencia MIT), es relativamente simple. ¿Qué es un serializador? Es simplemente una función que nos permite transformar una estructura de datos, interna a un programa, en su equivalente en XML.
y bien que no era imposible...
Vamos al grano: supongamos de tener la siguiente estructura de datos (de hecho, una estructura anidada, para complicarnos un poco la vida):
// tipo Film para test
typedef struct {
    char title[32];
    char director[32];
    int  year;
} Film;

// tipo Catalog para test
typedef struct {
    time_t t_lastupd;
    Film films[2];
} Catalog;
en un programa podemos llenarla con un poco de datos (beh, en este caso unos pocos, es sólo un ejemplo) y con nuestro XML Serializer tratar de conseguir un documento XML como esto:
<?xml version="1.0"?>
<CATALOG>
  <LASTUPDATE>28/11/15 12:26:04</LASTUPDATE>
  <FILM>
    <TITLE>Mad Max 2: The Road Warrior</TITLE>
    <DIRECTOR>George Miller</DIRECTOR>
    <YEAR>1979</YEAR>
  </FILM>
  <FILM>
    <TITLE>Mad Max: Fury Road</TITLE>
    <DIRECTOR>George Miller</DIRECTOR>
    <YEAR>2015</YEAR>
  </FILM>
</CATALOG>
Bueno, ya que estamos ansiosos por saber cómo hacerlo, vamos directamente al código:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <libxml/parser.h>

// tipo Film para test
typedef struct {
    char title[32];
    char director[32];
    int  year;
} Film;

// tipo Catalog para test
typedef struct {
    time_t t_lastupd;
    Film films[2];
} Catalog;

// prototipos locales
static char* serialize(char* document, const Catalog* catalog);

// main() del programa de test
int main(int argc, char* argv[])
{
    // test argumentos
    if (argc != 1) {
        printf("%s: wrong arguments counts\n", argv[0]);
        printf("usage: %s [e.g.: %s]\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    /* libxml2: inicializa la librería y testea potenciales ABI mismatches
       entre la versión compilada y la actual shared library usada */
    LIBXML_TEST_VERSION;

    // prepara datos para test
    char dest_document_xml[2048];
    Catalog catalog;
    catalog.t_lastupd = time(NULL);
    strcpy(catalog.films[0].title, "Mad Max 2: The Road Warrior");
    strcpy(catalog.films[0].director, "George Miller");
    catalog.films[0].year = 1979;
    strcpy(catalog.films[1].title, "Mad Max: Fury Road");
    strcpy(catalog.films[1].director, "George Miller");
    catalog.films[1].year = 2015;

    // serializa XML
    serialize(dest_document_xml, &catalog);

    // escribe los resultados en un file
    FILE* fp = fopen("catalog.xml", "w");
    fprintf(fp, "%s", dest_document_xml);
    fclose(fp);

    // sale con Ok
    return EXIT_SUCCESS;
}

// función serialize()
static char* serialize(
    char*          document,        // documento destino serializato
    const Catalog* catalog)         // estructura documento
{
    // crea documento con root node <CATALOG>
    xmlDocPtr doc = xmlNewDoc(BAD_CAST "1.0");
    xmlNodePtr root_node = xmlNewNode(NULL, BAD_CAST "CATALOG");
    xmlDocSetRootElement(doc, root_node);

    // añade <LASTUPD> child-field al nodo <CATALOG>
    char str_lastupd[64];
    strftime(str_lastupd, sizeof(str_lastupd), "%d/%m/%y %H:%M:%S", localtime(&catalog->t_lastupd));
    xmlNewChild(root_node, NULL, BAD_CAST "LASTUPDATE", BAD_CAST str_lastupd);

    // loop para añadir los child-nodes <FILM> y child-fields al nodo <CATALOG>
    int i;
    for (i = 0; i < sizeof(catalog->films) / sizeof(Film); i++) {
        // añade el child-node <FILM> al nodo <CATALOG>
        xmlNodePtr film_node = xmlNewChild(root_node, NULL, BAD_CAST "FILM", BAD_CAST NULL);

        // añade los child-fields al nodo <FILM>
        xmlNewChild(film_node, NULL, BAD_CAST "TITLE", BAD_CAST catalog->films[i].title);
        xmlNewChild(film_node, NULL, BAD_CAST "DIRECTOR", BAD_CAST catalog->films[i].director);
        char s_year[8];
        sprintf(s_year, "%d", catalog->films[i].year);
        xmlNewChild(film_node, NULL, BAD_CAST "YEAR", BAD_CAST s_year);
    }

    // copia el document al documento destino
    xmlChar *xmlbuff;
    int buffersize;
    xmlDocDumpFormatMemory(doc, &xmlbuff, &buffersize, 1);
    strcpy(document, (char *)xmlbuff);

    // libera recursos
    xmlFree(xmlbuff);
    xmlFreeDoc(doc);
    xmlCleanupParser();

    // return un pointer al document formateado
    return document;
}
Ok, como se nota es (como siempre en este blog) ampliamente comentado y así se auto-explica, por lo cual no voy a detenerme sobre las instrucciónes y/o grupos de instrucciones (¡leer los comentarios! ¡Estan ahi para eso!), pero voy a añadir, solamente (como de costumbre), algunos detalles estructurales.

El main() es solo un ejemplo funcional: inicializa la librería (¡leer los comentarios!), prepara los datos para el test, los pasa al Serializer, escribe los resultados en un file.

Lo que importa es la función serialize(), que puede utilizarse dónde y cómo se quiere en un programa que utiliza las estructuras de datos descritas anteriormente, y que, sobre todo, se puede utilizar como un ejemplo para escribir funciones similares para otras estructuras de datos: es muy simple y se puede adaptar fácilmente a cualquier otro caso. Como se puede ver utiliza diversas funciones de biblioteca libXML2 y el mecanismo que le permite llenar el documento de destino es simple: crea un nodo inicial (root-node) y le agrega los hijos (child-nodes) a raíz de la forma de la estructura de datos original.

Me olvidaba: para probar el programa en Linux (lo cual hice, por supuesto...) se debe instalar antes la libXML2 (desde el repository de la distribución), y luego compilar el programa con:
gcc serxml.c -I/usr/include/libxml2 -o serxml -lxml2 
En el próximo post (si no cambio de idea mientras tanto) veremos la función inversa, un XML Deserializer (y aquí estaría bien un "Ohhhhhhh" de sorpresa).

¡Hasta el próximo post!