pátek 20. února 2004

Náhrada <xsl:call-template> pomocí <xsl:apply-templates>

Tento článek začíná úvahou nad formáty XML dat, které se používají pro zobrazení výsledku SQL dotazu. Nejde mi zde o konkrétní formáty, ale spíš o stavbu takového XML, jeho čitelnost a zpracovatelnost pomocí XSLT. Proč nemám jako Roman v oblibě formáty s neurčitými názvy tagů jako <datapacket> a naopak preferuji názvy tagů podle reálných objektů. Tato úvaha je jakýmsi "předskokanem" ke hlavnímu tématu článku, který už se chystám dlouho napsat, a jeho délkou si to tu na blogu zas vyberu na půl roku dopředu :-) - proč dávám v XSLT přednost aplikovaným šablonám (<xsl:template match=...>, <xsl:apply-templates ...>) před pojmenovanými (<xsl:template name=...>, <xsl:call-template ...>) a že běžné úlohy v XSLT jsou pomocí aplikovaných šablon nejen řešitelné, ale řešitelné mnohem elegantněji, byť na první pohled je čitelnější řešení s pojmenovanými šablonami (zvláště je-li ochucené <xsl:for-each>, při té příležitosti si vzpomínám na slavnou větu "Opravdový programátor umí psát fortranské programy v každém jazyce." :-).

V našich intranetových aplikacích je odpověď na request obvykle vytvořena z výsledku nějakého SQL dotazu. Pro zobrazení tohoto výsledku používáme XML data, ze kterých se následnou XSLT transformací vygeneruje HTML. Právě data tabulkového charakteru jsou nejčastěji používaným materiálem, a proto je důležité s nimi manipulovat efektivně při současném zachování čitelnosti kódu a výhod technologie XSLT. Pojďme si to demonstrovat na příkladu, kdy máme zobrazit výsledek dotazu select id, name, born from person.

Formát XML dat

Jedním z diskutovaných témat je formát XML dat. Tady stojíme na rozcestí. První způsob je, že použijeme formát, který se dívá na vrácená data primárně jako na tabulku a bude podle toho nazývat XML tagy. Kořenový element pak bude mít pod sebou strom, jehož hloubka nebude velká (zpravidla 2). XML pak může vypadat např. takto:

<datapacket>
 <rowdata name="person">
  <coldata name="id">1</coldata>
  <coldata name="name">Adámek</coldata>
  <coldata name="born">1.2.1934</coldata>
 </rowdata>
 <rowdata name="person">
  ...
</datapacket>

Některé takové formáty jsou známé (nevím zda přímo standardizované), setkal jsem se např. s formáty MIDAS a DIML. V každém případě kdo jednou četl z databáze a převáděl data na XML, toho muselo toto řešení napadnout a pokud nepoužil známý formát (nebo i specifikovaný vnitrofiremně), navrhl si nějaký vlastní. Výhody jsou zřejmé: jednoduchý kód pro tvorbu XML a snadno napsaná šablona. Nevýhodou však je, že takové XML nekoresponduje tak dobře s objekty reálného světa. Nezajímá nás přece, že máme řádek, nás zajímá, že máme osobu! Řádek je pouze implementační záležitost a takto nazvaný tag ztíží vše, co přinese případné zveřejnění dat (výměna dat, čitelnost, příkladů by se našlo jistě více). Název, který představuje podstatu objektu, by měl podle mého názoru být názvem tagu, zatímco atribut představuje vlastnosti, bez kterých objekt neztrácí svou podstatu.

Druhý způsob (hybridy mezi oběma způsoby pro nás nejsou zajímavé) je tedy např. následující XML formát:

<people>
 <person>
  <id>1</id>
  <name>Adámek</name>
  <born>1.2.1934</born>
 </person>
 <person>
  ...
</people>

Formát je čitelnější a z názvu tagů ihned vidíme, jaký objekt tag představuje. Má zde smysl, aby potomci tagu <person> byli dále strukturovaní, např. <children><child id="101" /><child id="102" /></children>, zatímco u tagu <coldata> by takové strukturování bylo matoucí.

Použití aplikovaných šablon

První z uvedených způsobů nás zláká ještě více v okamžiku, kdy zjistíme, že potřebujeme ve dvou různých XSL souborech zobrazit tabulku stejným způsobem pro různé dotazy. Např. mějme druhý dotaz select iddoc, title, content from document. Použijeme-li první způsob, je nejjednodušší v obou XSL souborech provést <xsl:include href="common.xsl" /> a v souboru common.xsl definujeme šablony

<xsl:template match="datapacket">
 <table>
  <xsl:apply-templates select="rowdata" />
 </table>
</xsl:template>

<xsl:template match="rowdata">
 <tr>
  <xsl:apply-templates select="coldata" />
 </tr>
</xsl:template>

V místě, kam se má zobrazit tabulka, pak stačí napsat

... <xsl:apply-templates select="datapacket" /> ...

Použijeme-li druhý způsob, musíme definovat šablonu v každém z nich, protože každá se matchuje na jiný tag. V prvním souboru se definuje tato šablona, analogicky tomu je ve druhém.

... <xsl:apply-templates select="people" /> ...

<xsl:template match="people">
 <table>
  <xsl:apply-templates select="*" />
 </table>
</xsl:template>

<xsl:template match="person">
 <tr>
  <xsl:apply-templates select="*" />
 </tr>
</xsl:template>

Máme sice pěkně pojmenované tagy, ale kód nám za prvé narostl a za druhé se stejné věci opakují na několika místech. Přitom nelze šablony dát v této podobě do common.xsl, protože se pokaždé matchují na jiný tag. Můžeme dát do commonu šablony <xsl:template match="people|documents"> a <xsl:template match="person|document">, ale není to dostatečně obecné řešení. Pro každý nový tag by se matchovací výraz musel rozšířit o další alternativu. Navíc si tím koledujeme o průšvih v případě, kdy by se "tabulkový" element jmenoval stejně jako "řádkový" element. Ukažme si radši jiné řešení, představím zde dvě:

1. Použít pojmenované šablony. V souboru common.xsl definovat následující šablonu. Bude se volat pomocí <xsl:call-template select="maketable"><xsl:with-param name="rootnode" select="people" >.

<xsl:template name="maketable">
<xsl:param name="rootnode">
 <table>
  <xsl:for-each select="$rootnode/*">
   <xsl:call-template name="makerow">
    <xsl:with-param name="rownode" select="." />
   </xsl:call-template>
  </xsl:for-each>
 </table>
</xsl:template>

Podobně definovat šablonu makerow atd. To je podle mého názoru druhý nejhorší způsob, jak to udělat. (Horší už je jen rozepsat tělo šablony makerow do šablony maketable a získat šablonu, která připomíná spíš program - jedna dlouhá nudle s několika vnořenými cykly.) Používání pojmenovaných šablon ve spojení s <xsl:for-each> není pro tyto případy vhodné, protože

  • mění kontextový uzel a tím narušuje přirozené procházení dokumentu pomocí aplikovaných šablon.
  • znemožňuje použít defaultně aplikovanou šablonu.
  • tím, že umožňuje v jedné šabloně definovat výstup pro více typů uzlů, přestává platit, že každý uzel je zodpovědný za svůj výstup, což dělá kód méně čitelným.
  • zatímco aplikovaných šablon se může matchovat k jistému uzlu více a existuje mezi nimi pravidlo o rozhodnutí konfliktů, pojmenovaná šablona se nesmí vyskytnout více než jednou (při vícenásobném výskytu dojde k chybě, pravidlo "pozdější vyhrává" zde neplatí). Tím je znemožněno definovat případy s výjimečným chováním.

Celkově shrnuto vede tento přístup k procedurálnímu stylu programování (snad proto je taky tak častý) a nevyužívá hlavní síly tohoto deklarativního jazyka. Tím nechci říct, že pojmenované šablony nemají žádné využití. Hodí se dobře jako náhražka za funkce, které ač je to s podivem, v XSLT 1.0 nejsou (náhrada řetězce v řetězci, práce s datumy). Dále se hodí pro generování fragmentů HTML kódu, které se opakují nebo které se nevztahují k žádnému uzlu v XML datech (pole navigačních buttonů). Samozřejmě mohou být užitečné, i když mají uzly jako parametry, pokud je to mimo přirozený průchod uzly pomocí <xsl:apply-templates>. Např. chceme obarvit řádky podle stáří lidí. Přes <xsl:apply-templates> dojdeme k tagu <person>, v šabloně matchované na tento uzel zavoláme <xsl:call-template name="colorize"><xsl:with-param name="bornnode" select="born">, tím se pouze nastaví pozadí elementu <tr>, ale k uzlu <born> pak za účelem vytvoření výstupu přistupujeme opět přes <xsl:apply-templates>.

2. Použít aplikované šablony s módem. Poslední nevýhoda prvního řešení nás vede zpět k deklarativnímu přístupu a k použití aplikovaných šablon. Je třeba najít způsob, jak zachovat "pojmenování" šablony, a přitom mít šablonu aplikovanou. A tady nám pomohou módy. Volání bude tentokrát vypadat <xsl:apply-templates select="." mode="maketable"> a šablona v common.xsl bude vypadat takto:

<xsl:template match="*" mode="maketable">
 <table>
  <xsl:apply-templates select="*" mode="makerow" />
 </table>
</xsl:template>

Tím se odstranily prakticky všechny nevýhody prvního řešení: při konstrukci tabulky se prochází uzly pomocí <xsl:apply-templates> a jednotlivé uzly mají ve svých šablonách pouze kód bezprostředně zodpovídající za jejich zobrazení. Jestliže se má některá tabulka (nabo řádek nebo buňka) vykreslit jinak než obvykle, výjimka se snadno dosáhne definováním šablony s vyšší prioritou (deklarované později než šablona v commonu). Přitom je zachována možnost mít uzly pokaždé jinak nazvané a současně ti, kdo používají první způsob formátování dat, mohou tento způsob využívat také.

Takto jsme nahradili pojmenovanou šablonu aplikovanou šablonou a přitom název šablony pouze přešel v název módu. Uzel rootnode předávaný původně jako parametr je v našem případě kontextovým uzlem šablony. Obecně ale nemusí mít pojmenovaná šablona žádné parametry a do match se pak může uvést jakýkoli uzel, který je v XML jen jednou, já jsem si zvykl psát match="/". BTW i aplikované šabloně lze zadat parametry pomocí <xsl:with-param>.

Framework pro podobné dokumenty

Zmíněná náhrada nám usnadní práci v případě, kdy vytváříme několik dokumentů, které mají stejnou kostru, ale liší se v detailech. Pak je možné si kostru definovat v common.xsl a v jednotlivých šablonách pouze specifické detaily. Příklad: Chceme, aby první stránka nás "přivítala" alertem, zatímco jiná stránka má mít zvláštní styly. V common.xsl bude toto:

<xsl:template match="/">
<html>
 <head>
  <xsl:apply-templates match="/" mode="specialni-styly" />
 </head>
 <body>
  <xsl:apply-templates match="/" mode="uvitaci-alert" />
  <xsl:apply-templates select="root" />
 </body>
</html>
</xsl:template>

<xsl:template match="/" mode="specialni-styly" />

<xsl:template match="/" mode="uvitaci-alert" />

V XSL souboru pro první stránku pak bude pouze toto:

<xsl:template match="/" mode="uvitaci-alert">
 <xsl:attribute name="onload">alert('ahoj Hele, tady Jů!');</xsl:attribute>
</xsl:template>

Podobně v XSL souboru pro stránku se zvláštními styly bude pouze template s mode="specialni-styly". Soubor common.xsl musíme samozřejmě vložit na začátek, aby se uplatnilo pravidlo "poslední vyhrává". Kdybychom použili instrukci <xsl:call-template>, museli bychom ve všech šablonách kromě té výjimečné definovat prázdné tělo a při přidání nové šablony do kostry bychom museli provést zásah do všech šablon. Takto jsme vlastně dosáhli možnosti nepovinného vložení šablony. Může se stát, že nějaká šablona je tak důležitá, že vyžadujeme její realizaci v každém XSL souboru, v takovém případě je použití pojmenované šablony vhodné.

Závěr

Formáty XML dat, které vyjadřují tabulková data, a které se liší názvy tagů a přitom mají stejnou nebo podobnou strukturu, je možné převádět na výstup jednotným způsobem. XSL má dostatečně silné jazykové prostředky na to, aby se maximálně eliminovalo opakování stejných částí kódu. Neomezuje tedy návrh formátu XML dat. Případné požadavky na výjimky je možné snadno vyjádřit předefinováním některých částí, takže je dotčeno jen tolik šablon, kolik je nutné. Instrukcí <xsl:apply-templates> je možno nahradit instrukci <xsl:call-template>, což vede k efektivnějšímu návrhu koster.