Página principal del curso de XML
Usando hojas de estilo para generar WML
Otros cursos y tutoriales: comercio electrónico, WAP, Webmaster
Página principal del grupo GeNeura

Transformando documentos XML usando XSLT

J. J. Merelo

Introducción: programación funcional

Al igual que XML, XSLT es un lenguage de programación. Forma parte de la trilogía transformadora de XML, compuesta por las CSS (Cascading Style Sheets, hojas de estilo en cascada), que permite dar una apariencia en el navegador determinada a cada una de las etiquetas XML; las XSLT (XML Stylesheets Transformation Language, o lenguaje de transformación basado en hojas de estilo); y las XSL:FO, (Formatting Objects, objetos de formateo), o transformaciones para fotocomposición, o, en general, para cualquier cosa que no sea XML, como por ejemplo HTML "del viejo" o PDF (el formato de Adobe).

XHTML sí es XML, sigue un DTD, y sólo admite documentos "bien formados". HTML no lo es, aunque pude convertirse fácilmente en XHTML usando utilidades tales como Tidy.

XSLT es pues, un lenguaje que se usa para convertir documentos XML en otros documentos XML; puede convertir un XML que obedezca a un DTD a otro que obedezca otro diferente, un documento XML bien formado a otro que siga un DTD, o, lo más habitual, convertirlo a "formatos finales, tales como WML o HTML.

Los programas XSLT están escritos en XML, y generalmente, se necesita un procesador de hojas de estilo, o stylesheet processor para procesarlas, aplicándolas a un fichero XML.

El estilo de programación con las hojas XSLT es totalmente diferente a los otros lenguajes a los que estamos acostumbrados, tales como C++, PERL, pareciéndose más a "lenguajes" tales como el AWK, o a otros lenguajes funcionales, tales como ML o Scheme. En la práctica, eso significa dos cosas:

En resumen, programar con las XSLT puede ser un poco frustante, pero cuando uno aprende a usarlas, no puede vivir sin ellas. En realidad, son la única alternativa cuando uno quiere adaptar un contenido descrito con XML a diferentes clientes. Lo que consiguen las hojas de estilo es separar la información de su presentación, usando en cada caso las transformaciones que sean necesarias para que el contenido aparezca de la forma más adecuada en el cliente. Es más, se pueden usar diferentes hojas de estilo, o incluso la misma, para presentar la información de diferentes maneras dependiendo de los deseos o de las condiciones del usuario.

En realidad, XSLT son un lenguaje de programación, y por tanto se podría hacer cualquier cosa con ellas; incluso calcular la célebre criba de Eratóstenes. Pero a nosotros nos va a interesar más como simple herramienta de transformación de XML.

Actualmente hay dos versiones del estándar XSLT: la versión 1.0, que es la que implementan la mayoría de los procesadores, y se denomina "recomendación", es decir, para el consorcio W3, lo equivalente a un estándar, y la versión 1.1 , un "working draft", o borrador de trabajo, que es el paso previo a un estándar. Algunos procesadores, tales como el Saxon, implementan ya esta última versión. Hay pocas diferencias; la principal es que se pueden crear varios documentos de salida, en vez de uno solo.

Cómo se usan las hojas de estilo

Hay muchas formas de usar las hojas de estilo. Lo más normal es usarlas dentro de un entorno de publicación tal como el Cocoon, o el IBM Transcoding Publisher, AxKit u otros por el estilo. Un entorno de publicación de XML permite mantener sitios completos basados en XML, y generar páginas en diferentes formatos a partir de ellos. En este tutorial usaremos Cocoon, que es una herramienta gratuita y Open Source basada en Java, y además una de las más avanzadas en el sector. Para una solución profesional, es mejor el IBM TP, pues forma parte del servidor de aplicaciones WebSphere, y cuenta con interfaz gráfico avanzado; el problema es el coste.

La principal diferencia entre la versión 2 de Xalan y las anteriores es que implementa el llamado TrAX, una API para poder aplicar transformaciones a árboles XML; probablemente incorpore también la versión 1.1 de XSLT

En muchos casos, lo que se necesita es aplicar hojas de estilo dentro de una aplicación, o usarlas desde línea de comandos a partir de otra aplicación o otro lenguaje de programación. En ese caso, lo más útil es usar un procesador de hojas de estilo, que habitualmente se encuentra en conjunción con un parser XML, con o sin validación. De estos, hay los siguientes:

Contenido de esta sección
  • Ejecutando hojas de estilo en diferentes entornos
  • Hoja de estilo básica: templates
  • xsl:stylesheet, xsl:processing-instruction, xsl:apply-templates, xsl:value-of

Hojas de estilo básicas

Supongamos que tenemos que dar un curso del Centro de Formación Continua, y el director del curso, que es un capullo, nos ha mandado que hagamos una encuesta para medir la satisfacción de los alumnos con el curso. Y claro, como se trata de un curso de XML, pos hay que hacerlo en XML. Para empezar, habrá que hacer en XML una descripción de la encuesta. No hace falta, en principio, que se use un DTD, basta con que sea XML bien formado. Por ejemplo, una encuesta en XML podría ser de la forma siguiente:(encuesta1.xml)

<?xml version="1.0" encoding='ISO-8859-1'?>
<?xml-stylesheet href="encuesta1.xsl" type="text/xsl"?>
<?cocoon-process type="xslt"?>
<encuesta>
  <cuestion tipo='multiple'>
    <pregunta>¿Eres un listo?    </pregunta>
    <respuesta>Si    </respuesta>
    <respuesta>No    </respuesta>
    <respuesta>Lo que diga mi señora    </respuesta>
    <respuesta>La gallina    </respuesta>
  </cuestion>
  <cuestion tipo='multiple' varios='1'>
    <pregunta>¿Porque pierdes tu tiempo contestando encuestas?    </pregunta>
    <respuesta>Porque no tengo nada mejor que hacer    </respuesta>
    <respuesta>Porque me lo ha dicho mi señora    </respuesta>
    <respuesta>A tí te lo voy a decir, so listo    </respuesta>
    <respuesta>La gallina    </respuesta>
  </cuestion>
</encuesta>

Por lo pronto, no es nada complicado: una encuesta consiste en una serie de cuestiones, que pueden ser de diferentes tipos calificados por el atributo tipo, y cada cuestión tiene una sola pregunta y una o varias respuestas. Lo que nos proponemos, es, que, por orden de nuestro jefe, a partir de ahí se genere una página HTML que tenga, efectivamente, una encuesta. Así que, para ello, usaremos nuestra primera hoja XSLT, que será la siguiente (encuesta1.xsl):

<?xml version="1.0" encoding='ISO-8859-1'?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="encuesta"> <xsl:processing-instruction name="cocoon-format">type="text/html"</xsl:processing-instruction> <html> <head> <title>Encuesta, procesada con encuesta1.xsl</title> </head> <body> <p><br /></p> <center> <table border="0" width="80%" bgcolor="#000000" cellspacing="0" cellpadding="0"> <tr> <td width="100%"> <table border="0" width="100%" cellpadding="4"> <tr> <td width="100%" bgcolor="#c0c0c0" align="right"> <big><big>Encuesta chuli</big></big> </td> </tr> <tr> <td width="100%" bgcolor="#ffffff" align="center"> <xsl:apply-templates/> <p><br/></p> </td> </tr> </table> </td> </tr> </table> </center> <p align="center"> <font size="-1"> Copyright © 1999-2001 <a href="http://geneura.ugr.es/~jmerelo">JJ Merelo</a>.<br/> </font> </p> </body> </html> </xsl:template> <xsl:template match="cuestion"> <p><br/></p> <table border="0" width="90%" bgcolor="#000000" cellspacing="0" cellpadding="0"> <tr> <td width="100%"> <table border="0" width="100%" cellpadding="4"> <tr> <td width="100%" bgcolor="#e0e0e0"> <big> <xsl:value-of select="pregunta"/></big> </td> </tr> <xsl:apply-templates select='respuesta'/> </table> </td> </tr> </table> </xsl:template> <xsl:template match='respuesta'> <tr> <td width="100%" bgcolor="white"> <xsl:value-of select="."/> </td> </tr> </xsl:template> </xsl:stylesheet>

El resultado de aplicar esta hoja al fichero xml anterior, se puede ver en el servidor que usa Cocoon, o directamente en el fichero encuesta1.htm:

Para conseguir eso, se puede hacer de diferentes maneras:

A todo esto, todavía no sabemos muy bien qué es lo que hace la hoja de estilo, y como funciona, y como da la salida a partir de la entrada. Trataremos de analizar la hoja de estilo anterior poco a poco. Para empezar, nos encontramos con la línea <?xml version="1.0" encoding='ISO-8859-1'?>, que simplemente indica que una hoja de estilo es también un fichero XML, y por tanto tiene que ser procesado como tal; y que además, usamos el conjunto de caracteres ISO-8859-1, que incluye caracteres latinos; si no incluyéramos esa declaración, no podríamos usar nuestros queridos acentos y eñes.

En la siguiente línea que dice algo, se encuentra la etiqueta raíz de todas las hojas de estilo:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

Su correspondiente antietiqueta está al final del texto. En esta declaración se incluye una declaración de espacio de nombres; indica que se va a usar el prefijo xsl para todas las órdenes XSLT, y entre comillas algo que parece un URL, pero que no lo es: es simplemente un identificador único para cada espacio de nombres; de hecho, si uno va a ese URL, no hay nada. Igual podía haberse puesto cualquier otro prefijo, tal como xslt, pero convencionalmente se usa siempre xsl.

A continuación, en las siguientes líneas, viene el programa XSLT en sí. Los programas XSLT son habitualmente del tipo: regla => código a incluir en la salida. Las reglas son los denominados templates, o patrones. Un template se "dispara" si la etiqueta que se encuentra en el fichero XML coincide con la que se encuentra en su atributo match. En el caso de

<xsl:template match="encuesta">

se disparará cuando encuentre la etiqueta encuesta, que es la etiqueta raíz del fichero de entrada, en cuyo caso se incluirá en la salida todo lo incluido entre esa etiqueta y su contraetiqueta, aplicándole también las órdenes XSLT correspondientes. En este caso, se incluye una instrucción para el cocoon:

<xsl:processing-instruction name="cocoon-format">type="text/html"</xsl:processing-instruction>

para que el servidor entregue la página al cliente como un fichero tipo html. A continuación viene simplemente código HTML que hay que incluir en la salida, salvo la instrucción:

<xsl:apply-templates />type="text/html"</xsl:processing-instruction>

que indica al procesador que tiene que seguir aplicando templates al resto de las etiquetas del fichero de entrada. Si no se incluyera esta instrucción, el fichero de salida contendría simplemente el código HTML de este template; pero, como hay muchas más etiquetas que procesar, de esta forma se le indica que también tienen su importancia, las probeticas.

Para procesar el resto de las etiquetas hay sus templates correspondientes, que incluyen también código HTML. Pero si queremos incluir en la salida el contenido de la etiqueta, por ejemplo, lo que hay entre <pregunta> y su antitag, se usa la siguiente instrucción:

<big> <xsl:value-of select="pregunta"/></big>

<xsl:value-of select="[Etiqueta]"/> incluye en la salida el contenido de esa etiqueta, en este caso, el contenido de la pregunta. ¿Cómo sabe a qué pregunta se refiera? Esta instrucción tiene en cuenta el contexto; para cada cuestion que procese, incluirá la pregunta que descienda directamente de la etiqueta cuestion que se esté procesando. El template que se encarga de procesar la etiqueta cuestion, también incluye otra orden apply-templates, para que se aplique a todas las respuestas posibles incluidas en la etiqueta. Estas serán procesadas con el template

<xsl:template match='respuesta'>

que simplemente incluye el valor del contenido del tag:

<xsl:value-of select="."/>

dentro de las celdas de las tablas definidas anteriormente. En este caso, se indica . para indicar que lo que hay que incluir es el contenido de la etiqueta que se está procesando en ese momento, es decir, el contenido de cada una de las repuestas.

apply-templates tiene un atributo, select, que se puede usar para seleccionar a qué etiquetas del fichero original XML se va a aplicar los templates, o dicho de otro modo, qué templates se van a usar a continuación. En el fichero anterior, por ejemplo, se usa así:

<xsl:apply-templates select='respuesta'/>

En este caso se aplicarán solamente los templates que tengan dentro de su atributo select esa condición, que en este caso serán los que se apliquen a las etiquetas respuesta del fichero original. El template se aplicará una vez por cada respuesta, y los resultados saldrán en orden documental, es decir, en el orden que se encuentran en el documento original.

Un documento XML puede llevar asociadas diferentes hojas de estilo, y el propio Cocoon se encargará de seleccionarla dependiendo del navegador que use el cliente. Por ejemplo, se puede añadir esta línea al fichero XML original:

<?xml-stylesheet href="encuesta1-lynx.xsl" type="text/xsl" media="lynx"?>

[Lo feo que se ve con el
lynx

En este caso, Cocoon usará la hoja de estilo encuesta1-lynx.xsl cuando el navegador sea el Lynx. Esta hoja de estilo está bastante más simplificada que la anterior, dando un código HTML más simple y visible en modo texto. En el Lynx, se vería como aparece en la imagen.

¿Qué ocurre si queremos sacar los resultados por orden alfabético, en vez del mismo orden que en el documento? Habrá que usar una nueva opción de apply-templates, sort. Por ejemplo, si queremos formatear el siguiente fichero XML (ordenar.xml):

<raiz>
  <cosa>Pepe  </cosa>
  <cosa>Juan  </cosa>
  <cosa>Enrique  </cosa>
  <cosa>Xabier  </cosa>
  <cosa>Aarón  </cosa>
</raiz>

Se puede hacer con la siguiente hoja de estilo, de la cual extraemos solo las órdenes pertinentes:

11 <xsl:apply-templates select='cosa'> 12 <xsl:sort select='.' data-type='text'/> 13 </xsl:apply-templates><br />

El resultado se puede ver en el fichero ordenar.htm o calentito, recién salido del Cocoon. En este caso, en vez de aplicar la orden apply-templates en un solo tag "vacío" (con el / al final) se aplica en forma de tag y antitag, con las opciones en medio. La opción xsl:sortsirve para ordenar, y como clave de ordenación se puede usar cualquier atributo, XPath, o en este caso, el contenido en sí de la etiqueta cosa. En la salida, saldrán ordenados, Aarón primero y Xabier el último. El atributo data-type sirve para indicar el criterio de ordenación, text para texto y number para ordenación numérica.

Ejercicios
1. Hacer un fichero XML con varios equipos de 1ª división, su nombre, goles a favor, goles en contra, y puntuación, y una hoja XSL que formatee la hoja como una tabla, con el nombre del equipo en negrita.
2. A partir de una libreta de direcciones en XML, con nombre, dirección y teléfono, crear otra hoja XML con sólo los nombres, encerrados dentro del tag <NAME>.
3. Hacer un fichero XML con datos de alumnos: apellidos, nombre y nota; hacer dos hojas de estilo que saquen la salida en una tabla ordenada por nota y por apellido.

Contenido de esta sección
  • Usando atributos de las etiquetas
  • xsl:when/xsl:choose

Atributos y decisiones

Como el jefe del curso es un listo, dice que puede haber más tipos de preguntas que de respuesta múltiple, y que eso hay que tenerlo en cuenta. Que además, si las respuestas son de tipo Si/No, no hace falta incluirlas, sino que debe ser implícito. Así que nos ponemos a currar, y sacamos la segunda versión del fichero XML (encuesta2.xml):

<?xml version="1.0" encoding='ISO-8859-1'?>
<?xml-stylesheet href="encuesta2.xsl" type="text/xsl"?>
<?cocoon-process type="xslt"?>
<encuesta>
  <cuestion tipo='multiple'>
    <pregunta>¿Eres un listo?    </pregunta>
    <respuesta>Si    </respuesta>
    <respuesta>No    </respuesta>
    <respuesta>Lo que diga mi señora    </respuesta>
    <respuesta>La gallina    </respuesta>
  </cuestion>
  <cuestion tipo='multiple' varios='1'>
    <pregunta>¿Porque pierdes tu tiempo contestando encuestas?    </pregunta>
    <respuesta>Porque no tengo nada mejor que hacer    </respuesta>
    <respuesta>Porque me lo ha dicho mi señora    </respuesta>
    <respuesta>A tí te lo voy a decir, so listo    </respuesta>
    <respuesta>La gallina    </respuesta>
  </cuestion>
  <cuestion tipo='numero' rango='10'>
    <pregunta>Si tuvieras que calificar tu propia estupidez, ¿qué puntuación le pondrías?    </pregunta>
  </cuestion>
  <cuestion tipo='bool'>
    <pregunta>¿Me quieres?    </pregunta>
  </cuestion>
  <cuestion tipo='rollete'>
    <pregunta>¿Algo más que añadir?    </pregunta>
  </cuestion>
</encuesta>

con su correspondiente hoja de estilo (encuesta2.xsl)

1 <?xml version="1.0" encoding='ISO-8859-1'?>
2 3 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> 4 5 <xsl:template match="encuesta"> 6 <xsl:processing-instruction name="cocoon-format">type="text/html"</xsl:processing-instruction> 7 <html> 8 <head> 9 <title>Encuesta, procesada con <code>encuesta2.xsl</code></title> 10 </head> 11 <body> 12 13 <p><br /></p> 14 15 <center> 16 <form> 17 <table border="0" width="80%" bgcolor="#000000" cellspacing="0" cellpadding="0"> 18 <tr> 19 <td width="100%"> 20 <table border="0" width="100%" cellpadding="4"> 21 <tr> 22 <td width="100%" bgcolor="#c0c0c0" align="right"> 23 <big><big>Encuesta chuli</big></big> 24 </td> 25 </tr> 26 <tr> 27 <td width="100%" bgcolor="#ffffff" align="center"> 28 <xsl:apply-templates/> 29 <p><br/></p> 30 </td> 31 </tr> 32 </table> 33 </td> 34 </tr> 35 </table> 36 </form> 37 </center> 38 39 <p align="center"> 40 <font size="-1"> 41 Copyright © 1999-2001 <a href="http://geneura.ugr.es/~jmerelo">JJ Merelo</a>.<br/> 42 </font> 43 </p> 44 </body> 45 </html> 46 </xsl:template> 47 48 <xsl:template match="cuestion"> 49 <p><br/></p> 50 <table border="0" width="90%" bgcolor="#000000" cellspacing="0" cellpadding="0"> 51 <tr> 52 <td width="100%"> 53 <table border="0" width="100%" cellpadding="4"> 54 <tr> 55 <td width="100%" colspan='2' bgcolor="#e0e0e0"> 56 <big> <xsl:value-of select="pregunta"/></big> 57 </td> 58 </tr> 59 <xsl:choose> 60 <xsl:when test='@tipo="multiple"'><xsl:apply-templates select='respuesta'//></xsl:when> 61 <xsl:when test='@tipo="numero"'> 62 <tr> 63 <td colspan ='2' width="100%" bgcolor="white"> 64 <input type='text' /> 65 </td> 66 </tr> 67 </xsl:when> 68 <xsl:when test='@tipo="rollete"'> 69 <tr> 70 <td colspan='2' width="100%" bgcolor="white"> 71 <input type='text' /> 72 </td> 73 </tr> 74 </xsl:when> 75 <xsl:when test='@tipo="bool"'> 76 <tr> 77 <td bgcolor="white"> 78 Si 79 </td> 80 <td bgcolor="white"> 81 <input type='radio' /> 82 </td> 83 </tr> 84 <tr> 85 <td bgcolor="white"> 86 No 87 </td> 88 <td bgcolor="white"> 89 <input type='radio' /> 90 </td> 91 </tr> 92 </xsl:when> 93 </xsl:choose> 94 </table> 95 </td> 96 </tr> 97 </table> 98 </xsl:template> 99 100 <xsl:template match='respuesta'> 101 <tr> 102 <td width="100%" bgcolor="white"> 103 <xsl:value-of select="."/> 104 </td> 105 <td width="100%" bgcolor="white"> 106 <input type='checkbox' /> 107 </td> 108 </tr> 109 </xsl:template> 110 </xsl:stylesheet>

Los resultados se pueden ver en el servidor Tomcat+Cocoon o directamente en encuesta2.html.

Aunque ya se usaban atributos en el ejemplo anterior, el atributo multiple en la etiqueta cuestion, en este caso tenemos varios tipos de atributos, y los contenidos de las etiquetas tendrán que transformarse de forma diferente dependiendo del valor de esos atributos. Hemos añadido varios tipos más: bool, para respuestas de tipo si/no, numero, con un rango, para respuestas que puedan tomar diferentes valores numéricos, de 0 hasta un máximo, y rollete, para respuestas en las que se pueda contestar cualquier cosa. El tipo inicial, multiple, tiene además otro atributo, varios, que indica si se pueden seleccionar varias respuestas o no. Si pretendemos generar un formulario con esto, cada uno de ellos necesitará diferentes elementos del formulario, y generación de diferente código de salida; de eso se encarga la hoja de estilo.

En concreto, los cambios con respecto a la hoja de estilo anterior comienzan a partir de la línea 59, dentro del template que procesa el tag cuestion. En estas líneas se usan las órdenes xsl:choose/xsl:when/xsl:otherwise, que son equivalentes a las switch/case/default de C o C++, es decir, dependiendo del valor de una variable, se ejecuta un código u otro. En este caso, xsl:choose no lleva ninguna variable asociada; cada uno de los xsl:when, a través del atributo test, prueba cuando una condición se cumple o no. En este caso, comprueba el valor del atributo tipo, de esta forma

<xsl:when test='@tipo="bool"'>

El código correspondiente a esa condición se ejecutará, o mejor dicho, se incluirá en la salida si se cumple esa condición. La arroba @ se usa para referirse a atributos de la etiqueta que se esté procesando en cada momento, tal como se ve en la especificación XPath; dentro de las expresiones XSLT se puede usar cualquier expresión XPath para referirse a fragmentos o grupos de fragmentos de un documento XML. Un test similar se usa para los otros valores posibles del atributo; en cada caso se incluye un elemento de formulario diferente: botones, casillas, o incluso si/no.

Ejercicios
1. Repetir el ejercicio 1 del bloque anterior, es decir, los equipos de la liga, pero poniendo los goles a favor y en contra como atributos de una etiqueta principal, en vez de otra etiqueta, por ejemplo <equipo gf='33' gc='88' puntos='33'>.
2. A partir de un fichero con movimientos de una cuenta corriente, que utilice como atributo si son ingresos o retiradas de fondos, formatearlo en una tabla, poniéndolo en diferentes columnas según se trate de un ingreso o una retirada.

Contenido de esta sección
  • Llamando a templates explícitamente
  • Usando recursividad
  • xsl:call-template/xsl:param/xsl:with-param/xsl:if, variables
  • Usando extensiones de XSL en el procesador Saxon

Clamando al cielo

Como era de esperar, el jefe no está contento. Si la respuesta numérica, para empezar, no se comprueba el rango, y el usuario puede responder cualquier cosa, como "Ushuaia", y se tiene que comprobar en el servidor si es correcto o no. Sabiendo el rango, y que sólo pueden ser enteros, no se podía poner un menucito con los numeros para que la gente elija, ¿eh?. Pues sí, pero, teniendo en cuenta lo que se ha dicho anteriormente de los bucles, es un tanto complicado. Usando el mismo fichero XML anterior, habría que aplicar la siguiente hoja (encuesta3.xsl):

61 <xsl:when test='@tipo="numero"'> 62 <tr> 63 <td colspan ='2' width="100%" bgcolor="white"> 64 <select name='numero'> 65 <xsl:call-template name='option-range'> 66 <xsl:with-param name='range' select='@rango'/> 67 </xsl:call-template> 68 </select> 69 </td> 70 </tr> 71 </xsl:when> 115 <xsl:template name='option-range'> 116 <xsl:param name='range' /> 117 <xsl:param name='i'>0</xsl:param> 118 <xsl:if test='$range != $i'> 119 <option><xsl:value-of select='$i' /></option> 120 <xsl:call-template name='option-range'> 121 <xsl:with-param name='range' select='$range'/> 122 <xsl:with-param name='i' select='$i+1'/> 123 </xsl:call-template> 124 </xsl:if> 125 </xsl:template> 126 </xsl:stylesheet>

Como siempre, aquí está la salida y lo mismo, usando cocoon. Del listado anterior hemos suprimido todas las líneas que eran similares al anterior.

La principal complicación de este tipo de problemas, que se resuelven fácilmente con un bucle for en cualquier lenguaje de programación pachanguero, en lenguajes funcionales pueden ser un poco más complicados. Por eso, muchos procesadores de XSL tienen extensiones que permiten hacer bucles convencionales, como veremos más adelante.

El principal problema es que, dado que no se pueden actualizar variables por la ausencia de efectos secundarios, no se puede crear un bucle que vaya comprobando el valor de una variable y actualizándolo en cada iteración. La única solución es sustituir los bucles clásicos por bucles recursivos, en los cuales se llama recursivamente a una subrutina (en este caso, template) hasta que se cumple una condición.

En este caso, vamos a incluir una etiqueta select y sus correspondientes option, una para cada uno de las opciones que haya. Y no tenemos más remedio que llamar a una rutina recursivamente. Eso se hace en la línea 65. En este caso en vez de dejar que los templates se disparen según los valores de una etiqueta, lo llamamos explícitamente con la orden xsl:call-template. Y dado que se trata de una especie de subrutina o procedimiento, hay que llamarlo también con sus correspondientes parámetros, para lo cual se usa xsl:with-param. Es decir, las siguientes órdenes

<xsl:call-template name='option-range'>
    <xsl:with-param name='range' select='@rango'/>
</xsl:call-template>

corresponderían en cualquier otro lenguaje de programación a option-range( range ).

De la misma forma, la "declaración" de un template es un tanto peculiar (línea 115):

<xsl:template name='option-range'>
   <xsl:param name='range' />
   <xsl:param name='i'>0</xsl:param>

Esto correspondería a algo así como void function option-range(var range, var i=0), es decir, declaración de dos variables, una de las cuales tiene un valor por defecto, el 0, y otra sin valor por defecto, que tiene que ser dado por el usuario.

El resto de ese template, a partir de la línea 118, comprueba si el range es distinto al contador que usamos, i, y si lo es, imprime las etiquetas HTML, y vuelve a llamarse recursivamente, incrementando el contador. Cuando range y i sean iguales, la recursión para.

En este template se usan también variables, las que se pasan como parámetro. Las variables, aunque no lo son en el sentido clásico, se pueden actualizar dentro de un template, asignársele valor y pasar como parámetro a otros templates; para asignarles valor se puede usar esta construcción:

<xsl:param name='i'>0</xsl:param>

o bien con esta, que es equivalente

<xsl:param name='i' select='0' />

En ambos casos se le asignaría el valor 0 a la variable i. Para recuperar el valor de la variable i se usa con el $ delante, de esta forma $i. Por ejemplo, si queremos asignar a una variable el valor de otra, se podría hacer de dos formas:

<xsl:param name='i' select='$j'>
<xsl:param name='i'>{$j}<xsl:param>

En el segundo caso, las llaves indican que, a pesar de encontrarse la variable fuera de una etiqueta XSL, debe ser evaluada.

Las variables y parámetros se declaran sin tipo: pueden contener una cadena, un node-set, o un resulting tree fragment, que se puede convertir en una cadena, pero no en un node-set; según el contexto, se usarán como conjuntos de nodos, árboles o cadenas. La transformación a cadenas se hace automáticamente, pero hacerlo al revés es bastante fastidiado. Algunos procesadores incluyen extensiones que permiten

También se usa la orden xsl:if, cuyo significado es similar a cualquier otro lenguaje. Si la condición que se le pasa en el atributo test es cierta, se incluye en la salida el fragmento correspondiente; si no, no se hace nada. Como no tiene nada parecido al "else", si hay más de una opción, lo mejor es usar xsl:when en tal caso.

En resumen, con esta hoja de estilo se obtiene una encuesta3-saxon.xsl. En este caso, se tiene que añadir al principio del fichero una declaración de espacio de nombres y de prefijo:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:saxon="http://icl.com/saxon" extension-element-prefixes="saxon">

y con esa declaración (y lo necesario para usar Saxon, que se ha explicado anteriormente), se puede incluir el menú de la forma siguiente:

<xsl:for-each select='saxon:range(0,@rango)'> <option><xsl:value-of select='.' /></option> </xsl:for-each>

Esta construcción saxon:range equivale a un bucle que se ejecuta desde 0 hasta el número indicado en el atributo rango (@rango). Con esto, nos ahorramos toda la historia de la recursión y todo eso. Y el resultado es exactamente el mismo. En realidad lo que hace es que crea un conjunto de nodos, cada uno de los cuales tiene un valor en el rango indicado; pero qué se le va a hacer, es la forma de hacer las cosas. El problema de usar estas extensiones es que solamente funcionan con el procesador que las implementa, no con el resto. Una construcción usando elementos estándar XSL funcionará, teóricamente, en cualquier procesador. En realidad, la orden xsl:for-each la veremos más adelante.

Ejercicios
1. Hacer un template que sume números desde 1 hasta el número indicado. Por ejemplo, si se le llama con el 10, sumará todos los números de 1 a 10. El número se pasará en el atributo de una etiqueta XML.

Contenido de esta sección
  • Funciones XPath
  • xsl:variable
  • Bucles y node-sets

Funciones y bucles

El jefe ya está un poquitín más contento, la encuesta sale chuli y tal, pero, puntilloso como siempre, nos indica que toda encuesta que se precie tiene los numeritos de la pregunta delante de las preguntas mismamente. De nada sirve explicarle la ausencia de bucles for (o casi ausencia) en XSLT. Si hay que poner numeritos, hay que poner numeritos. Además, la encuesta tal cual tiene buena pinta, pero en realidad no sirve para nada, porque cada campo no tiene nombre propio ni nada de eso. O sea, que hay que ponerle nombre propio, para poder procesarlo de verdad como un formulario. Como el jefe no dice que haya que hacerlo de verdad, ya veremos como lo haremos luego. Y hacemos la siguiente hoja de estilo (encuesta4.xsl, de la cual ponemos sólo los sitios que más han cambiado.:

59 <xsl:template match="cuestion"> 60 <p><br/></p> 61 <table border="0" width="90%" bgcolor="#000000" cellspacing="0" cellpadding="0"> 62 <tr> 63 <td width="100%"> 64 <table border="0" width="100%" cellpadding="4"> 65 <tr> 66 <td width="100%" colspan='2' bgcolor="#e0e0e0"> 67 <big> <xsl:value-of select="pregunta"/></big> 68 </td> 69 </tr> 70 <xsl:choose> 71 <xsl:when test='@tipo="multiple"'> 72 <xsl:variable name='pos' select='position()' /> 73 <xsl:variable name='varios' select='@varios' /> 74 <xsl:for-each select='.//respuesta'> 75 <tr> 76 <td width="100%" bgcolor="white"> 77 <xsl:value-of select='position()' />. <xsl:value-of select='.' /> 78 </td> 79 <td width="100%" bgcolor="white"> 80 <xsl:choose> 81 <xsl:when test='$varios=1'> 82 <input type='radio' name='radio{$pos}' /> 83 </xsl:when> 84 <xsl:otherwise> 85 <input type='checkbox' name='cb{$pos}' /> 86 </xsl:otherwise> 87 </xsl:choose> 88 </td> 89 </tr> 90 </xsl:for-each> 91 </xsl:when> 92 <xsl:when test='@tipo="numero"'> 93 <tr> 94 <td colspan ='2' width="100%" bgcolor="white"> 95 <select name='{concat("select",position())}'> 96 <xsl:call-template name='option-range'> 97 <xsl:with-param name='range' select='@rango'/> 98 </xsl:call-template> 99 </select> 100 </td> 101 </tr> 102 </xsl:when> 103 <xsl:when test='@tipo="rollete"'> 104 <tr> 105 <td colspan='2' width="100%" bgcolor="white"> 106 <input type='text' name='{concat("radio",position())}'/> 107 </td> 108 </tr> 109 </xsl:when> 110 <xsl:when test='@tipo="bool"'> 111 <tr> 112 <td bgcolor="white"> 113 Si 114 </td> 115 <td bgcolor="white"> 116 <input type='radio' name='{concat("radio",position())}'/> 117 </td> 118 </tr> 119 <tr> 120 <td bgcolor="white"> 121 No 122 </td> 123 <td bgcolor="white"> 124 <input type='radio' name='{concat("radio",position())}' /> 125 </td> 126 </tr> 127 </xsl:when> 128 </xsl:choose> 129 </table> 130 </td> 131 </tr> 132 </table> 133 </xsl:template>

Los resultados se pueden ver en forma de página o servida por nuestro querido Cocoon. En cualquier caso, una de las estructuras nuevas (o casi) está en la línea 74:<xsl:for-each select='.//respuesta'>, que introduce la orden xsl:for-each. Esta orden es lo más parecido a un bucle que tiene XSLT: realiza el código que le sigue (hasta el antitag correspondiente) aplicándolo una vez por cada nodo del node-set incluido en el atributo select. En este caso, usa un XPath que indica que se tienen que coger todos los tags respuesta que desciendan del tag actual (.), sean o no descendientes directos; aunque en este caso son todos descendientes directos; en este caso, serán todas las respuestas correspondientes a una pregunta determinada.

Previamente al bucle, en las líneas 72 y 73, se usa xsl:variable, que es bastante parecido a xsl:param hasta el punto que yo no sabría diferenciarlas... En realidad, se diferencian en cómo se les puede asignar un valor en el caso de que sean variables globales. A una xsl:variable se le puede asignar cualquier valor fuera de un template, pero a un xsl:param sólo se le puede asignar valores externos, procedentes de parámetros con los que se llama a la página. Además, las variables no se pueden usar para pasar valores a templates. En lo que sí se diferencian ambos de las variables "normales", es que su valor no se puede guardar entre una invocación y otra de un template; si se quiere conservar el valor de una variable, hay que llamar al template explícitamente. En este caso, usamos las variables para tomar valores que son dependientes del contexto, la posición y un atributo, que se pierden cuando se está procesando otro tag.

También en la línea 77 se usa una función XPath, position(), que devuelve la posición del nodo actual dentro del contexto que se está procesando, es decir, en este caso, el número de orden de la respuesta actual dentro de todas las respuestas que hay.

Las dos variables declaradas, varios y pos, se usan más adelante. varios se usa para generar diferentes tags HTML en el caso de que se permita respuestas múltiples (una novedad en esta hoja), o si no se permiten; en el primer caso se usarán checkboxes, y en el segundo botones de radio (se podía haber usado en ambos casos una combo box con la opción múltiple o no, pero bueno...). El nombre de los elementos del formulario se halla concatenando un prefijo tal como "cb" o "radio" a la posición de la pregunta en el contexto, multiplicada por dos (porque en realidad, procesamos uno de cada dos nodos). En todo caso, consigue lo que nos interesa: que cada elemento del formulario tenga un nombre único.

En la línea 95 se usa otra función XPath, concat, que aunque en realidad, que nos permite concatenar la posición del nodo al prefijo. Algo similar se usa en la línea 106. concat es una función que devuelve como salida una cadena que concatena todo lo que se le meta como entrada.

Ejercicios
1. A partir de un fichero XML con los equipos de fútbol tales como los que se han usado en ejercicios anteriores, devolver otro fichero XML que contenga sólo los equipos que tengan más goles a favor que en contra.
2. A partir de un fichero XML con direcciones tales como los que se han usado en ejercicios anteriores, devolver sólo aquellos cuyo número de teléfono comience por 958. Usar la función contains.

Contenido de esta sección
  • Procesando con XSL como si fuera un CGI
  • Algunas extensiones Xalan para escribir ficheros, y lo mismo en XSLT 1.1

Procesando información

En fin, que el jefe que dice que vale, que el formulario está perfecto, pero a ver, tanto XML y tanta gaita gallega, pero al final el formulario habrá que procesarlo usando PERL o algún otro lenguaje serio. Así que, para demostrarle que no tiene razón, tendremos que procesar también el formulario usando XSLT, aunque sea nada más que por eso. Y lo hacemos sobre el siguiente fichero de encuesta (encuesta5.xml):

<encuesta>
  <cuestion tipo='multiple'>
    <pregunta>¿Eres un listo?    </pregunta>
    <respuesta>Si    </respuesta>
    <respuesta>No    </respuesta>
    <respuesta>Lo que diga mi señora    </respuesta>
    <respuesta>La gallina    </respuesta>
  </cuestion>
  <cuestion tipo='multiple'>
    <pregunta>¿Porque pierdes tu tiempo contestando encuestas?    </pregunta>
    <respuesta>Porque no tengo nada mejor que hacer    </respuesta>
    <respuesta>Porque me lo ha dicho mi señora    </respuesta>
    <respuesta>A tí te lo voy a decir, so listo    </respuesta>
    <respuesta>La gallina    </respuesta>
  </cuestion>
  <cuestion tipo='multiple'>
    <pregunta>¿Cómo considera la coyuntura actual de la mejora de las prestaciones en orden a una europeización más completa?    </pregunta>
    <respuesta>Epatante    </respuesta>
    <respuesta>No contestaré a preguntas excluyentes    </respuesta>
    <respuesta>La considero positivante y conducente a unas relaciones más normalizadas con el conjunto de los agentes socioeconómicos    </respuesta>
    <respuesta>La gallina    </respuesta>
  </cuestion>
</encuesta>

Esta encuesta la procesaremos con la siguiente hoja de estilo (encuesta5.xsl), de la cual sólo mostramos las líneas en las que se diferencia con la anterior:

1 <?xml version="1.0" encoding='ISO-8859-1'?>
2 3 <xsl:stylesheet version="1.0" 4 xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 5 xmlns:redirect='org.apache.xalan.xslt.extensions.Redirect' 6 extension-element-prefixes="redirect"> 7 8 <xsl:param name='radio2'></xsl:param> 9 <xsl:param name='radio4'></xsl:param> 10 <xsl:param name='radio6'></xsl:param> 11 12 <xsl:template match='/'> 13 <xsl:choose> 14 <xsl:when test="$radio2"> 15 <xsl:call-template name='procesa-encuesta'> 16 </xsl:call-template> 17 </xsl:when> 18 <xsl:otherwise> 19 <xsl:apply-templates /> 20 </xsl:otherwise> 21 </xsl:choose> 22 </xsl:template> 23 24 <xsl:template name="procesa-encuesta"> 25 <xsl:processing-instruction name="cocoon-format">type="text/html"</xsl:processing-instruction> 26 <redirect:write select='"/tmp/encuesta.resultados.1"'> 27 Resultados: 28 <xsl:value-of select='$radio2' /> - 29 <xsl:value-of select='$radio4' /> - 30 <xsl:value-of select='$radio6' /> 31 </redirect:write> 32 <html> 33 <head> 34 <title>Procesando encuesta</title> 35 </head> 36 <body> 37 <h1>Procesando encuesta</h1> 38 39 <table border="0" width="80%" bgcolor="#000000" cellspacing="0" cellpadding="0"> 40 <tr> 41 <td width="100%"> 42 <table border="0" width="100%" cellpadding="4"> 43 <tr> 44 <td width="100%" bgcolor="#c0c0c0" align="right"> 45 <big><big>Encuesta chuli</big></big> 46 </td> 47 </tr> 48 <tr> 49 <td width="100%" bgcolor="#ffffff" align="center"> 50 <xsl:for-each select='.//cuestion'> 51 <table border="0" width="90%" bgcolor="#000000" cellspacing="0" cellpadding="0"> 52 <tr> 53 <td width="100%"> 54 <table border="0" width="100%" cellpadding="4"> 55 <tr> 56 <td width="100%" bgcolor="#e0e0e0"> 57 <big> <xsl:value-of select="pregunta"/></big> 58 </td> 59 <td width="100%" bgcolor="white"> 60 <xsl:choose> 61 <xsl:when test='position()=1'> <xsl:value-of select='$radio2' /> </xsl:when> 62 <xsl:when test='position()=2'> <xsl:value-of select='$radio4' /> </xsl:when> 63 <xsl:when test='position()=3'> <xsl:value-of select='$radio6' /> </xsl:when> 64 </xsl:choose> 65 </td> 66 </tr> 67 </table> 68 </td> 69 </tr> 70 </table> 71 <p><br/></p> 72 73 </xsl:for-each> 74 </td> 75 </tr> 76 </table> 77 </td> 78 </tr> 79 </table> 80 </body> 81 </html> 82 </xsl:template> 83

En realidad, estamos haciendo trampa; lo cierto es que XSLT, sólo, no puede realizar cosas tan potentes como un CGI o cualquier otro método de procesamiento en el servidor, tales como los JSPs; XSLT está enfocado sólo a transformaciones de documentos XML, y por eso, tenemos que forzar un poco el estándar usando extensiones. En este caso, sería mucho más adecuado usar XSP, por ejemplo, que permite incluir programas en Java (u otro lenguaje) dentro de un documento XML, o incluso las extensiones Xalan, que permiten incluir JavaScript dentro del documento.

En este caso, si queremos generar la página usando Xalan, hay que añadir una librería más al CLASSPATH:

export CLASSPATH=$CLASSPATH:/usr/local/xalan-j_1_2_2/bsf.jar

(sustituyendo /usr/local/xalan... por dondequiera que esté instalado Xalan). La librería que se añade es la que contiene el Bean Scripting Framework, que permite usar diferentes lenguajes dentro de la máquina virtual Java; las extensiones de Xalan están escritas usando BSF, por eso hay que hacerlo. Y una vez hecho, se puede generar la página siguiente (encuesta5.htm), pero para verla actuar de veras, necesitarás toda la potencia de Cocoon.

Por eso, para empezar, a partir de la línea 3, se tienen que declarar las extensiones de Xalan en la declaración de la hoja de estilo. En este caso se declara un namespace redirect, que será el prefijo que usen las extensiones que se van a usar, y se indica que corresponde a una clase determinada dentro del conjunto de clases en Java que incluye Xalan. Estas extensiones se usarán más adelante para poder escribir en disco desde una hoja de estilo.

Más adelante, en las líneas 8,9 y 10, se tienen que declarar los parámetros del formulario que se van a usar. En este caso, el formulario que se ha creado tiene 3 elementos: radio2, 4 y 6. A la vez, se inicializan con un valor nulo; en la misma sentencia se le podría asignar un valor por defecto, simplemente metiéndolo dentro de los tags. En nuestro caso, no nos conviene, porque vamos a usarlo para saber si el fichero XML se está procesando "de primeras" o no, o se está procesando después de rellenar el formulario. Hay otra alternativa a declarar esas variables, y es usar las taglibs que vienen con el Cocoon; en concreto, request:get-parameter-names. Una vez más, no son universales, o sea que no van a funcionar en todos los sistemas, aunque probablemente haya algo muy similar en el resto de los sistemas.

Si se está usando Xalan desde la línea de comandos, se le pueden incluir también los parámetros de la forma siguiente:

java org.apache.xalan.xslt.Process -in encuesta5.xml -xsl encuesta5.xsl -out encuesta5-form.htm -param radio2 3 -param radio4 5 -param radio6 3

que daría como salida un fichero HTML (encuesta5-form.html), y a la vez crea el fichero de salida, como veremos más adelante.

De hecho, eso es lo que se intenta dilucidar en las líneas a partir de la 12. Esta hoja de estilo sirve a la vez para presentar la encuesta en forma de formulario, y para procesar el formulario una vez rellenado; y todo ello sobre el mismo documento XML y usando la misma hoja de estilo XSLT. ¡Toma ya!

El template que comienza en la línea 12 comprueba si existe alguno de los elementos del formulario; si existe, llama al template procesa-encuesta, si no, aplica los templates de toda la vida, empezando por el raíz.

Pasamos entonces al nuevo template, procesa-encuesta, que para empezar, inserta en la salida la instrucción de siempre, que le dice al Cocoon que tiene que servir la página como HTML, y luego viene lo bueno: la instrucción específica de Xalan redirect:write, que hace lo que es de esperar: escribe un fichero con el nombre que se le pasa en el atributo select. El nombre está entrecomillado para indicar que se trata de una cadena, no de un XPath o algo peor; el fichero se escribirá en el directorio /tmp, que está en todas las distribuciones Unix con permisos de escritura. Pero, ojo, no quiero decir que uséis esto en un entorno de producción: podría ser muy peligroso. En tal caso sería mejor que usárais una base de datos, con las autorizaciones adecuadas. En el fichero se escribe lo que hay entre los dos tags, es decir, "Resultados " y el valor de los parámetros. Saldrá algo así:

<?xml version="1.0" encoding="ISO-8859-1"?> Resultados: 3 - 5 - 3

con su orden XML al principio y tó, tan mona. Si os dáis cuenta, hemos tenido que hacer una verdadera chapuza: tenemos que poner de uno en uno los valores de los parámetros; y además, solo admitimos un tipo de parámetro, el más fácil de procesar. En realidad, lo cierto es que XSLT no es tan potente, para este tipo de cosas, como un CGI, al menos sin extensiones.

A continuación, a partir de la línea 50, imprimimos también cada una de las preguntas y la respuesta correspondiente en la página HTML que se muestra al usuario; usamos for-each y el XPath .//cuestion, que forma un node-set con todas las etiquetas cuestion que desciendan del nodo actual. Para seleccionar la respuesta a esa pregunta que se va a imprimir, se hace un truco un tanto sucio en la línea 60: con un xsl:choose, se mira si la posición de la pregunta es la primera, segunda o tercera, usando la función de XPath position(), que ya hemos visto anteriormente, y se selecciona el parámetro correspondiente. Y es una chapuza porque lo suyo es que se carculara, pero es muy difícil convertir el contenido de una variable en parte del nombre de un nodo, salvo que se usen extensiones, así que lo dejamos como está.

En otros procesadores XSLT, habrá que usar otras alternativas. Y si usamos un procesador XSL que siga la especificación XSLT 1.1, se puede usar xsl:document de esta forma:

<xsl:document href="encuesta_resultados_s.1"> Resultados: <xsl:value-of select='$radio2' /> - <xsl:value-of select='$radio4' /> - <xsl:value-of select='$radio6' /> </xsl:document>

La última versión de Saxon, usa esa orden. Probablemente, en siguientes versiones de Xalan (y por tanto de Cocoon) se incluya también. Para usar los parámetros desde Saxon, y obtener el mismo resultado, hay que escribir

java com.icl.saxon.StyleSheet -o encuesta5-saxon.html encuesta5.xml encuesta5-saxon.xsl radio2=3 radio4=6 radio6=1

Es decir, se le pasan los parámetros de la forma parametro=valor y ya está. El resultado es exactamente el mismo.

En realidad, lo más adecuado sería usar XSP, JSP o un servlet para procesarla. Pero bueno, por lo menos hemos probado lo que se puede intentar hacer.

Ejercicios
1. Sobre cualquier documento XML, hacer una hoja de estilo que tome como parámetro un XPath y presente ese XPath del documento original.
2. Sobre una encuesta con el formato anterior, hacer una hoja de estilo que tenga dos parámetros, el número de pregunta y el número de respuesta, y escribir ese número de pregunta y respuesta. Habrá que construir un XPath a partir de los parámetros.
3. A partir de un documento XML que contenga una tabla HTML, hacer una hoja de estilo, que, según el valor de un parámetro, escriba sólo el valor de la primera columna o sólo el contenido de la tabla, sin etiquetas.

Bibliografía y enlaces relacionados con XSLT

Esta es la bibliografía que ha aparecido hasta el momento sobre el tema, especialmente en castellano

Hay también algunos tutoriales bastante buenos en inglés