Recipe8.2.Converting Elements to Attributes


Recipe 8.2. Converting Elements to Attributes

Problem

You have a document that encodes information using child elements, and you would like to use attributes instead.

Solution

As with Recipe 8.1, you can use the overriding copy idiom. However, when transforming elements to attributes, you must selectively determine where the transformation will be applied. This is because the idea of transforming all elements to attributes is nonsensical. The following stylesheet reverses the attribute-to-element transformation we performed in Recipe 8.1:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">     <xsl:import href="copy.xslt"/>     <xsl:output method="xml" version="1.0" encoding="UTF-8"/>     <xsl:template match="person">   <xsl:copy>     <xsl:for-each select="*">       <xsl:attribute name="{local-name(.)}">         <xsl:value-of select="."/>       </xsl:attribute>       </xsl:for-each>   </xsl:copy> </xsl:template>     </xsl:stylesheet>

Discussion

XSLT 1.0

Converting from elements to attributes is not always as straightforward as transforming in the opposite direction. If the elements being converted to attributes have attributes themselves, you must decide what will become of them. In the preceding solution, they would be lost. Another alternative would be to promote them to the new parent:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">     <xsl:import href="copy.xslt"/>     <xsl:output method="xml" version="1.0" encoding="UTF-8"/>     <xsl:template match="person">   <xsl:copy>     <xsl:for-each select="*">       <xsl:attribute name="{local-name(.)}">         <xsl:value-of select="."/>       </xsl:attribute>         <xsl:copy-of select="@*"/>     </xsl:for-each>   </xsl:copy> </xsl:template>     </xsl:stylesheet>

However, this works only if all the attributes names in question are unique. If this is not the case, you will have to rename attributes, perhaps as follows:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">     <xsl:import href="copy.xslt"/>     <xsl:output method="xml" version="1.0" encoding="UTF-8"/>     <xsl:template match="person">   <xsl:copy>     <xsl:for-each select="*">       <xsl:attribute name="{local-name(.)}">         <xsl:value-of select="."/>       </xsl:attribute>         <xsl:variable name="elem-name" select="local-name(.)"/>       <xsl:for-each select="@*">         <xsl:attribute name="{concat($elem-name,'-',local-name(.))}">           <xsl:value-of select="."/>         </xsl:attribute>         </xsl:for-each>     </xsl:for-each>   </xsl:copy> </xsl:template>     </xsl:stylesheet>

Another complication arises if the sibling elements do not have unique names, because in this case, they would clash upon becoming attributes. Another possible strategy is to create an attribute from an element only if the element does not have attributes or element children of its own, does not repeat in its parent element, and has parents without attributes:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">     <xsl:import href="copy.xslt"/>     <xsl:output method="xml" indent="yes" version="1.0" encoding="UTF-8"/>     <!-- Match elements that are parents --> <xsl:template match="*[*]">   <xsl:choose>     <!-- Only convert children if this element has no attributes -->     <!-- of its own -->     <xsl:when test="not(@*)">       <xsl:copy>         <!-- Convert children to attributes if the child has -->         <!-- no children or attributes and has a unique name -->         <!-- amoung its siblings -->         <xsl:for-each select="*">           <xsl:choose>             <xsl:when test="not(*) and not(@*) and                             not(preceding-sibling::*[name( ) =                                                      name(current( ))])                              and                              not(following-sibling::*[name( ) =                                                       name(current( ))])">               <xsl:attribute name="{local-name(.)}">                 <xsl:value-of select="."/>               </xsl:attribute>               </xsl:when>             <xsl:otherwise>               <xsl:apply-templates select="."/>             </xsl:otherwise>           </xsl:choose>         </xsl:for-each>       </xsl:copy>     </xsl:when>     <xsl:otherwise>       <xsl:copy>         <xsl:apply-templates/>       </xsl:copy>     </xsl:otherwise>   </xsl:choose> </xsl:template>     </xsl:stylesheet>

XSLT 2.0

Here you partially simplify and speed up the 1.0 solution by utilizing xsl:for-each-group. The trick is to use group-by="name( )" to determine if there are siblings with identical names. In addition, this solution promotes the attributes of the converted element, to the parent, which you can do as well in the 1.0 solution:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:import href="copy.xslt"/>     <xsl:output method="xml" indent="yes" version="1.0" encoding="UTF-8"/>     <!-- Match elements that are parents --> <xsl:template match="*[*]">   <xsl:choose>     <!-- Only convert children if this element has no attributes -->     <!-- of its own -->     <xsl:when test="not(@*)">       <xsl:copy>         <!-- Convert children to attributes if the child has -->         <!-- no children or attributes and has a unique name -->         <!-- amoung its siblings -->         <xsl:for-each-group select="*" group-by="name( )">           <xsl:choose>             <xsl:when test="not(*) and count(current-group( )) eq 1">               <xsl:attribute name="{local-name(.)}">                 <xsl:value-of select="."/>               </xsl:attribute>               <!-- Copy attributes of child to parent element -->               <xsl:copy-of select="@*"/>               </xsl:when>             <xsl:otherwise>               <xsl:apply-templates select="current-group( )"/>             </xsl:otherwise>           </xsl:choose>         </xsl:for-each-group>       </xsl:copy>     </xsl:when>     <xsl:otherwise>       <xsl:copy>         <xsl:apply-templates select="@*| node( )"/>       </xsl:copy>     </xsl:otherwise>   </xsl:choose> </xsl:template>    </xsl:stylesheet>

There is a limitation in both the 1.0 and 2.0 stylesheets in that they assume a certain kind of canonical structure where at a certain level, all elements that qualify to be converted to attributes appear before those that do not. Below is an example document that violates this assumption:

<E1>     <E2>         <e31>a</e31>         <e32>b</e32>         <e33>c</e33>     </E2>     <test>a</test>     <E2>         <e31>u</e31>         <e32>v</e32>         <e33>w</e33>     </E2>     <E2>         <e31>x</e31>         <e32>y</e32>         <e33>z</e33>     </E2> </E1>

Notice how the test element could, in theory, be converted to an attribute of E1. In fact, the stylesheet will attempt to do so. However, it will fail because of a constraint in XSLT that attributes can only be copied to a node before any other nodes are copied. If you need to deal with messy documents such as this, you can do so in two passes. During the first pass, do not actually convert the elements to attributes but rather copy them as elements tagged with a special attribute indicating they are eligible for conversion. Then on the second pass, convert all tagged elements to attributes of their parent first and then copy all other child elements to the parent unchanged.





XSLT Cookbook
XSLT Cookbook: Solutions and Examples for XML and XSLT Developers, 2nd Edition
ISBN: 0596009747
EAN: 2147483647
Year: 2003
Pages: 208
Authors: Sal Mangano

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net