Em Novembro de 1999, o W3C oficializou a especificação de transformações XSL, ou XSLT, passando-a ao estado de Recomendação – um tipo de standard W3C. O objectivo desta linguagem, escrita em XML, é permitir aos programadores transformarem os dados e estrutura de um documento XML noutro formato qualquer à medida das necessidades dos utilizadores, como PDF, HTML ou XML.
O processamento de documentos XML através de um processador XSLT significa basicamente que o processador usa o XML original para gerar uma árvore a partir do documento, e converte-a para uma árvore representante do documento final, através da execução de instruções especificadas num stylesheet.
Um stylesheet XSLT contém blocos, cada um contendo uma expressão indicando qual o elemento ou elementos do XML original que lhe interessa processar (ou, alternativamente, um nome identificativo do bloco). A linguagem que permite definir, sob a forma de expressões, quais os elementos e atributos a seleccionar para processamento, é o XML Path Language, ou XPath.
A um ficheiro XSLT dá-se o nome de stylesheet, e tem normalmente extensão .xslt
(por vezes .xsl
). Neste artigo, documento significa um documento XML, e stylesheet significa o ficheiro XSLT que irá ser usado para transformar o documento. Para testar transformações, o mais simples é usar o xsltproc, um processador XSLT de linha de comando disponível em Linux, Windows e Mac.
Transformações básicas
Imagine que tem de montar um site com conteúdos dinâmicos. A base de dados dá-lhe os conteúdos em XML. No fim, tem de produzir HTML. Este é o cenário mais comum do uso de XSLT – transformar os dados obtidos de uma base de dados ou de ficheiros XML, e transformar a estrutura destes dados para HTML, de modo a que possam ser apresentados num browser. Esta transformação pode ocorrer directamente no browser, ou pode ocorrer no servidor, na aplicação web que esteja a servir o site.
A transformação mais básica que se pode realizar é, simplesmente, fazer uma cópia integral do documento original. Para isso, só precisamos do seguinte stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" version="1.0" encoding="UTF-8"/> <xsl:template match="/"> <xsl:copy-of select="." /> </xsl:template> </xsl:stylesheet>
Este stylesheet contém um bloco de processamento (linha 5 a 7 – template
). Este bloco está interessado em processar o elemento raíz do XML (linha 5 – match="/"
), copia o elemento para o documento final (linha 6 – copy-of select="."
), e termina a execução.
Como o objectivo deste stylesheet é copiar o documento original sem tocar na estrutura ou nos dados, só precisamos de um bloco que seleccione o elemento raiz do XML, e não precisamos de processar mais nenhum elemento, pois a instrução copy-of
copia toda a árvore a partir do elemento que está definido no filtro select
. O .
representa, em XPath, o elemento corrente, e como o bloco seleccionou a raiz através do match="/"
, o ponto representa a raíz, e o copy-of
vai copiar tudo o que está abaixo do elemento raiz, inclusive.
As restantes linhas definem configurações para o processador. A linha 1 é obrigatória, e define que este é um stylesheet que corresponde à especificação XSLT. O prefixo xsl:
, que está definido na linha 3, aponta para a especificação do XSLT, e todas as tags referentes a instruções desta especificação têm este prefixo. O prefixo é arbitrário, podia ser xpto:
, mas por convenção usa-se xsl:
ou xslt:
. Um stylesheet tem sempre, no mínimo, um prefixo, que é o da especificação, e pode ter outros prefixos definidos, se for necessário. Os prefixos são representações de namespaces, que serão abordados um pouco mais à frente neste artigo.
A linha 4 controla como irá ser produzido o documento final. Neste caso, estamos a dizer que o documento final irá ser XML (method="xml"
). Em alternativa, se o resultado final for HTML, podemos colocar html
nesta propriedade.
Blocos e XPath
Quando o processador começa a transformação, a primeira coisa que faz é procurar, no stylesheet, um bloco <xsl:template>
que esteja interessado em processar o elemento raiz do XML. Caso não encontre nenhum, passa para o elemento de XML que vem a seguir, hierarquicamente, e tenta outra vez, e assim sucessivamente, até o processamento entrar num bloco, ou o processador chegar ao fim do XML.
A propriedade que define qual o elemento a processar dentro de um determinado bloco é match="expressão XPath"
. No nosso primeiro exemplo, a expressão XPath no match era "/"
, que representa o elemento raiz de um documento XML. Vejamos o seguinte documento XML:
<noticias> <noticia criada="20-10-2007" publicada="30-10-2007" > <titulo>Esta notícia já está publicada!</titulo> <autor>AvG</autor> </noticia> <noticia criada="21-10-2007"> <titulo>Esta ainda não está publicada</titulo> <texto>Teste de texto para a notícia.</texto> <autor>AvG</autor> </noticia> </noticias>
As expressões mais comuns de XPath que se aplicam a este documento são:
/ | Nó raíz: <noticias> |
noticia | Todos os elementos <noticia> |
/noticia/autor | Todos os elementos <autor> filhos de elementos <noticia> |
* | Todos os elementos |
noticia[@publicada] | Todos os elementos <noticia> que contenham a propriedade publicada |
text() | Todos os elementos que contenham texto. |
noticia/@* | Todas as propriedades do elemento <noticia> : criada , publicada |
Com estas expressões, já podemos construir um stylesheet mais complexo, para produzir um HTML com, por exemplo, todas as notícias que estejam publicadas:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" > <xsl:output method="html" version="1.0" encoding="UTF-8"/> <xsl:template match="/"> <html> <body> <xsl:apply-templates /> </body> </html> </xsl:template> <xsl:template match="noticia[@publicada]"> <div> <p><xsl:value-of select="titulo" /></p> <p><xsl:value-of select="texto" /></p> <xsl:if test="autor"> <p> criada em <xsl:value-of select="@criada"/> <xsl:text> </xsl:text> <i>by <xsl:value-of select="autor"/></i> </p> </xsl:if> </div> <xsl:apply-templates /> </xsl:template> <xsl:template match="*"> <xsl:apply-templates /> </xsl:template> <xsl:template match="text()"> </xsl:template> </xsl:stylesheet>
Resultado da transformação (indentação alterada para melhor leitura)
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"> <html> <body> <div> <p>Esta notícia já está publicada!</p> <p></p> <p>criada em 20-10-2007 <i>by AvG</i></p> </div> </body> </html>
O início do stylesheet continua o mesmo: na tag stylesheet
definimos a versão e o prefixo xsl
, que aponta para o namespace da especificação. Na tag output
definimos que o documento final irá ser HTML, em UTF-8.
O primeiro bloco selecciona o elemento raiz <noticias>
. Este bloco é normalmente usado para produzir a parte fixa do HTML, como se pode ver nas linhas 7-8, 10-11. Ao contrário do primeiro exemplo, no qual não havia necessidade de processar toda a árvore, neste queremos processar certos elementos e ignorar outros, e queremos que o html produzido pelos outros blocos seja inserido entre as tags <body>
, definidas nas linhas 8 e 10. A instrução apply-templates
diz ao processador para continuar a processar o XML hierarquicamente – o elemento processado a seguir será o <noticias>
. Não há nenhum bloco que processe explicitamente noticias
, por isso este elemento é processado pelo bloco match="*"
, na linha 30, que não produz nenhum output, mas indica ao processador para continuar hierarquicamente.
O processamento passa agora para o primeiro elemento <noticia>
, e este pode entrar num de dois blocos: o da linha 13, match="noticia[@publicada]"
, ou o da linha 30, match="*"
. A condição do bloco da linha 13 selecciona todos os elementos <noticia>
que tenham uma propriedade de nome publicada
. As propriedades são seleccionadas através do símbolo @
, e os parêntesis rectos indicam uma condição a aplicar ao elemento indicado. Visto que o elemento que estamos a processar tem efectivamente uma propriedade chamada publicada
, o processamento continua no bloco da linha 13.
Este bloco produz então um div
com três parágrafos contendo o texto que está dentro dos elementos <titulo>
, <texto>
e <autor>
, bem como a data de criação da notícia, que está dentro da propriedade criada
. A instrução value-of
copia o conteúdo do elemento ou propriedade indicada pela expressão no select
para o documento final. O processamento não é interrompido se alguma das propriedades ou elementos não existir: o processador simplesmente não produz resultados, e continua o processamento.
Neste exemplo, a notícia que estamos a processar não tem nenhum elemento <texto>
, e como não estamos a verificar se o elemento existe ou não, é produzido um parágrafo vazio. Por outro lado, o parágrafo com o autor e a data da notícia está englobado num if
, que testa se o elemento <autor>
realmente existe antes de produzir o parágrafo. É importante notar que, neste exemplo algo torcido, se o <autor>
não existir, a data da notícia não aparece no resultado final, o que não seria de todo desejável uma aplicação real.
Depois da produção (ou não) do HTML desta notícia, o processamento continua com os filhos do elemento <noticia>
. Isto não é tecnicamente necessário para o nosso exemplo, visto que não queremos processar nenhum dos elementos dentro de uma notícia. Ter a instrução apply-templates
na linha 26 produz exactamente o mesmo resultado que não a ter.
No fim disto tudo, só ainda não falei do bloco da linha 34, com a condição match="text()"
. Este bloco permite apanhar todos os elementos que contenham texto, e serve para impedir o processador de incluir todo o texto de todos os elementos, que não tenham sido processados explicitamente, no resultado final. Todos os elementos que não sejam apanhados na condição *
serão apanhados na condição text()
. A melhor maneira de perceber como estes blocos funcionam é comentando-os e vendo o que é produzido na transformação. O que acontecerá se o bloco text()
for comentado? E se a instrução apply-templates
na linha 26 for também comentada?
Named templates
Named templates é o nome que se dá a blocos template que, em vez de terem uma propriedade match=""
que define uma selecção, têm uma propriedade name=""
. Estes blocos têm de ser explicitamente invocados durante o processamento, e o seu contexto de execução é o mesmo do bloco que o chamou.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="html" version="1.0" encoding="UTF-8"/> <xsl:template match="texto"> <xsl:call-template name="Heading" /> </xsl:template> <xsl:template match="titulo"> <xsl:call-template name="Heading" /> </xsl:template> <xsl:template match="autor"> <xsl:call-template name="Heading" /> </xsl:template> <xsl:template name="Heading"> <h1><xsl:value-of select="."/></h1> </xsl:template> </xsl:stylesheet>
Processar elementos sem apply-templates
A maneira mais natural de trabalhar hierarquicamente XML é usar o apply-templates
para ir dizendo ao processador para onde ir a seguir, mas não é a única. A instrução for-each
pode ser usado em qualquer lado dentro de um bloco para percorrer uma lista de elementos sequencialmente.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="html" version="1.0" encoding="UTF-8"/> <xsl:template match="/"> <ul> <xsl:for-each select="noticias/noticia"> <li><xsl:value-of select="titulo" /><li> </xsl:for-each> </ul> </xsl:template> </xsl:stylesheet>
O for-each
é muito usado quando se pretende produzir listas ou realizar pequenos processamentos. O código é mais legível e fácil de manter com um pequeno for-each
do que usando blocos, principalmente se precisarmos de dois ciclos, um dentro do outro.
Parâmetros
Qualquer bloco pode receber parâmetros, seja um bloco com condições ou com nome. Os parâmetros são declarados logo a seguir à tag template
, e podem ter valores por defeito. Independentemente da forma como um bloco é invocado (seja por apply-templates
ou por call-template
), o envio de parâmetros nunca é obrigatório, mesmo que não tenham valores por defeito. É importante ter isto em conta, porque um bloco com uma condição pode ser invocado n vezes em n pontos diferentes do processamento, e basta uma alteração ao XML original para de repente um bloco passar a ser invocado pelo processador sem que ninguém se dê conta, e se esse bloco depender de certos parâmetros para continuar o processamento, parâmetros esses que de repente não estão a ser enviados, toda a transformação pode sair mal.
É bem mais fácil controlar como e quando os named templates são invocados, e por isso é com este tipo de blocos que os parâmetros são mais usados.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="string"> <string> <xsl:call-template name="split"> <xsl:with-param name="str"><xsl:value-of select="."/></xsl:with-param> <xsl:with-param name="separator">:</xsl:with-param> </xsl:call-template> </string> </xsl:template> <xsl:template name="split"> <xsl:param name="str"/> <xsl:param name="separator">:</xsl:param> <xsl:choose> <xsl:when test="contains($str,$separator)"> <parte><xsl:value-of select="substring-before($str,$separator)" /></parte> <xsl:call-template name="split"> <xsl:with-param name="str" select="substring-after($str,$separator)"/> <xsl:with-param name="separator" select="$separator"/> </xsl:call-template> </xsl:when> <xsl:otherwise> <parte><xsl:value-of select="$str" /></parte> </xsl:otherwise> </xsl:choose> </xsl:template> </xsl:stylesheet>
<string>esta:string:precisa:de:um:split</string>
A stylesheet anterior pega no texto dentro de um elemento <string>
e corta-o em partes delimitadas por um separador, chamando para isso um bloco “split”, e passando-lhe o texto a cortar, e o caracter que irá servir de separador – neste caso, :
. Nas linhas 5 e 6, dentro da instrução call-template
, são passados os parâmetros para o bloco, através das instruções with-param
. No início do bloco, são declarados os dois parâmetros, str
e separator
, através da instrução param
. O parâmetro separator
é declarado com um valor por defeito, :
, que é usado caso o parâmetro seja omitido na chamada.
A instrução choose
é o equivalente a um if–else; o if simples em XSLT não contém else, por alguma razão impossível de perceber, por isso a instrução choose–when–otherwise faz as honras da casa. Todos os parâmetros são acedidos pelo nome, com prefixo $
.
Este bloco é chamado recursivamente enquanto o texto contido em str
tenha pelo menos um caracter $separator
– o call-template
da linha 18 é equivalente à chamada original na linha 4, excepto que vai retirando ao texto a parte que já foi processada, através função XPath substring-after
, que devolve tudo o que está em $str
após o $separator
. Quando não existirem mais caracteres $separator
em $str
, a última parte de $str
é produzida (linha 24).
Variáveis
As variáveis em XSLT não têm tipo explícito, servindo sobretudo para guardar valores temporários para ajudar no processamento, sobretudo quando as condições para seleccionar elementos começam a crescer e a serem difíceis de manter. Têm, no entanto, um senão: não é possível alterar o valor inicial de uma variável, tal como não é possível alterar valores de parâmetros.
Uma variável é definida pela instrução variable
, que lhe dá o valor, e é acedida com o prefixo $
, tal como os parâmetros. As variáveis podem ser globais ou locais.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:datetime="http://exslt.org/dates-and-times" exclude-result-prefixes="datetime"> <xsl:variable name="Todas">0</xsl:variable> <xsl:variable name="Hoje"><xsl:value-of select="substring(datetime:date(), 0, 11)" /></xsl:variable> <xsl:template match="/"> <xsl:variable name="hoje"><xsl:value-of select="translate($Hoje, '-', '')" /></xsl:variable> <xsl:choose> <xsl:when test="$Todas = 1">Todas as notícias</xsl:when> <xsl:otherwise>Todas as notícias publicadas até <xsl:value-of select="$Hoje" /></xsl:otherwise> </xsl:choose> <br/> <xsl:apply-templates select="noticias/noticia"> <xsl:with-param name="hoje" select="$hoje" /> </xsl:apply-templates> </xsl:template> <xsl:template match="noticia"> <xsl:param name="hoje" /> <xsl:if test="$Todas = 1 or translate(@publicada, '-', '') <= $hoje"> Titulo: <xsl:value-of select="titulo"/><br/> </xsl:if> </xsl:template> </xsl:stylesheet>
O exemplo usa o XML listado no início para mostrar uma lista de todas as notícias, que podem ser todas as publicadas até ao dia corrente, ou todas as existentes, independentemente de estarem ou não publicadas. O primeiro problema que se coloca em condições que envolvem datas é que, logo à partida, o XSLT não tem funções de obtenção da data do sistema ou processamento de datas, e como as variáveis não têm tipo, as comparações são mais complicadas.
O problema da obtenção da data do sistema é resolvido recorrendo a uma extensão de XSLT que suporta datas e horas, declarada na linha 2. O prefixo datetime:
vai ser usado para chamar as funções suportadas pelo EXSLT, que pode ser consultado em http://exslt.org/date/index.html.
A variável Todas
controla se vamos mostrar todas as notícias ou somente as que estão publicadas. O seu valor neste exemplo é 0
, por isso só estamos a mostrar as publicadas. Para mostrar todas as notícias, o valor desta variável deverá ser 1
.
A variável Hoje
guarda a data corrente do sistema. A data é obtida através da chamada datetime:date()
, e são só guardados os primeiros 10 caracteres de retorno; as datas têm o formato AAAA-MM-DD+TZ
, TZ sendo o fuso horário, que não nos interessa, daí o uso do substring
.
No primeiro bloco, é criada a variável local hoje
, que tem o valor da data de sistema que está em $Hoje
, mas transformada para número, para facilitar as comparações. A instrução translate
substitui todos os caracteres -
por nada (removendo-os). É de notar que a variável $hoje
é diferente de $Hoje
.
A variável $Todas
é usada no choose
para mudar o título, mostrando a data de sistema se estivermos a ver só as notícias publicadas. O operador de comparação é =
(e não ==
, como em muitas linguagens).
A instrução da linha 16 é uma novidade; já vimos várias vezes o uso de apply-templates
simples, mas este exemplo usa uma expressão de condição para indicar ao processador XSLT que só estamos interessados em processar os elementos <noticia>
debaixo de <noticias>
, tal e qual como fizemos anteriormente com o for-each
. Além disso, passamos um parâmetro com o apply-templates
, a data do sistema com os -
removidos que guardámos na variável $hoje
.
Na linha 21 está o bloco que vai mostrar as notícias, com a declaração do parâmetro hoje
logo no início. O if
da linha 23 verifica simplesmente se a variável $Todas
tem valor 1 – se sim, mostra a notícia. Se $Todas
for diferente de 1, converte a data de publicação guardada na propriedade @publicada
retirando os traços, e compara-a com o valor passado no parâmetro $hoje
. A comparação verifica se a data de publicação é menor ou igual à data de hoje, e como o caracter <
é reservado no XML e por isso não pode ser usado, tem de se usar o código HTML equivalente, <
.
Namespaces
Não podia acabar esta introdução sem referir os namespaces, já mencionados várias vezes. Como indica a especificação XML, os namespaces são um método simples de qualificar elementos e atributos, identificando-os através de URIs. E porquê? Esta é daquelas que precisa mesmo de um exemplo, por isso, imaginemos que há duas empresas, A e B, que se dedicam à distribuição de produtos. Estas duas empresas desenvolveram internamente aplicações para gerir os seus produtos, e geram todos os meses listas com os produtos em catálogo para os seus revendedores. Um destes revendedores revende produtos de ambas as empresas, e como isto hoje em dia está tudo informatizado e integrado, o revendedor tem uma aplicação que vai buscar as novas listas de produtos das duas empresas por web services disponibilizados por ambas. Ambos os web services devolvem XML com informações semelhantes, mas cada um tem seu formato próprio que cada uma das empresas inventou.
XML da empresa A
<Produtos xmlns="urn:a-empresa:catalogo"> <Produto nome="" pvp=""/> <Produto nome="" pvp=""/> <Produto nome="" pvp=""/> </Produtos>
XML da empresa B
<Produtos xmlns="urn:empresa-b:produtos"> <Marca> <Produto> <Nome /> <PVP /> </Produto> </Marca> </Produtos>
A aplicação do revendedor vai buscar todos os XML de todas as empresas, e transforma-os usando um único XSLT, para gerar um catálogo combinado de todos os produtos. O problema, essencialmente, é distinguir entre o XML da empresa A, e o da empresa B. Ambos começam com um elemento Produtos
, pelo que logo à partida não é possível distingui-los por aí, e é aqui que entram os namespaces.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:a="urn:a-empresa:catalogo" xmlns:b="urn:empresa-b:produtos" exclude-result-prefixes="a b"> <xsl:template match="a:Produtos|b:Produtos"> <xsl:apply-templates /> </xsl:template> <xsl:template match="a:Produto"> </xsl:template> <xsl:template match="b:Produto"> </xsl:template> </xsl:stylesheet>
Este stylesheet define dois namespaces, a
e b
, iguais aos definidos em cada um dos XML. Com esta definição, o XSLT já pode indicar qual dos elementos Produtos
e Produto
quer. Na linha 6 vemos que o bloco apanha tanto o Produtos
da empresa A como o empresa B, através da condição OR representada pelo |
. Os blocos da linha 10 e 13 apanham, respectivamente, o Produto
da empresa A, e o da empresa B. Assim já não há confusões nem conflitos em relação a qual elemento se está a tratar. Podem definir-se quantos namespaces forem necessários para qualificar todos os elementos, tanto de XML como do próprio XSLT; como vimos, todas as tags XSLT são qualificadas por um prefixo que aponta para a especificação, e quando usámos a extensão para as datas, definimos um outro namespace para qualificar as tags específicas a essa extensão.
É importante notar que se um XML tiver um namespace, este terá que ser definido no XSLT e todos os elementos do XML em questão terão de levar o prefixo do namespace respectivo, senão o XSLT não os irá processar. Um namespace afecta o elemento onde está definido e todos os elementos abaixo hierarquicamente. Se algum elemento tiver outro namespace definido entretanto, ele e todos os elementos abaixo passarão para o novo namespace. Os namespaces não são cumulativos.
Conclusão
Esta introdução não é de todo exaustiva; o XSLT e o XPath têm muitas features que não mencionei, como os modos ou a criação de XML dentro de variáveis para posterior processamento, sem falar na integração com outras linguagens ou na especificação e validação através de XSD.
A referência mais completa e concisa de XSLT e XPath é o MSXML SDK da Microsoft, que está online em http://msdn2.microsoft.com/en-us/library/ms256177.aspx. Há algumas referências a elementos que são extensões da Microsoft, e que não pertencem à especificação, mas estes estão bem assinalados na documentação. De resto, há todo um mundo de tutoriais disponíveis na internet.
Espero que esta pequena introdução seja útil, e boas transformações!