tags:

views:

82

answers:

5

Here is my contrived example that illustrates what I am attempting to accomplish. I have an input XML file that I wish to flatten for further processing.

Input file:

<BICYCLES>
    <BICYCLE>
        <COLOR>BLUE</COLOR>
        <WHEELS>
            <WHEEL>
                <WHEEL_TYPE>FRONT</WHEEL_TYPE>
                <FLAT>NO</FLAT>
                <REFLECTORS>
                    <REFLECTOR>
                        <REFLECTOR_NUM>1</REFLECTOR_NUM>
                        <SHAPE>SQUARE</SHAPE>
                    </REFLECTOR>
                    <REFLECTOR>
                        <REFLECTOR_NUM>2</REFLECTOR_NUM>
                        <SHAPE>ROUND</SHAPE>
                    </REFLECTOR>
                </REFLECTORS>
            </WHEEL>
            <WHEEL>
                <WHEEL_TYPE>REAR</WHEEL_TYPE>
                <FLAT>NO</FLAT>
            </WHEEL>
        </WHEELS>
    </BICYCLE>
</BICYCLES>

The input is a list of <BICYCLE> nodes. Each <BICYCLE> has a <COLOR> and optionally has <WHEELS>.

<WHEELS> is a list of <WHEEL> nodes, each of which has a few attributes, and optionally has <REFLECTORS>.

<REFLECTORS> is a list of <REFLECTOR> nodes, each of which has a few attributes.

The goal is to flatten this XML. This is the XSL I'm using:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions"&gt;
<xsl:output method="xml" encoding="UTF-8" indent="yes" omit-xml-declaration="yes" xml:space="preserve"/> 

<xsl:template match="/">
<BICYCLES>
<xsl:apply-templates/>
</BICYCLES>
</xsl:template>

<xsl:template match="BICYCLE">
<xsl:choose>
    <xsl:when test="WHEELS">
        <xsl:apply-templates select="WHEELS"/>
    </xsl:when>
    <xsl:otherwise>
        <BICYCLE>
            <COLOR><xsl:value-of select="COLOR"/></COLOR>
            <WHEEL_TYPE/>
            <FLAT/>
            <REFLECTOR_NUM/>
            <SHAPE/>
        </BICYCLE>
    </xsl:otherwise>
</xsl:choose>
</xsl:template>

<xsl:template match="WHEELS">
<xsl:apply-templates select="WHEEL"/>
</xsl:template>

<xsl:template match="WHEEL">
    <xsl:choose>
        <xsl:when test="REFLECTORS">
            <xsl:apply-templates select="REFLECTORS"/>
        </xsl:when>
        <xsl:otherwise>
            <BICYCLE>
                <COLOR><xsl:value-of select="../../COLOR"/></COLOR>
                <WHEEL_TYPE><xsl:value-of select="WHEEL_TYPE"/></WHEEL_TYPE>
                <FLAT><xsl:value-of select="FLAT"/></FLAT>
                <REFLECTOR_NUM/>
                <SHAPE/>
            </BICYCLE>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

<xsl:template match="REFLECTORS">
    <xsl:apply-templates select="REFLECTOR"/>
</xsl:template>

<xsl:template match="REFLECTOR"> 
    <BICYCLE>
        <COLOR><xsl:value-of select="../../../../COLOR"/></COLOR>
        <WHEEL_TYPE><xsl:value-of select="../../WHEEL_TYPE"/></WHEEL_TYPE>
        <FLAT><xsl:value-of select="../../FLAT"/></FLAT>
        <REFLECTOR_NUM><xsl:value-of select="REFLECTOR_NUM"/></REFLECTOR_NUM>
        <SHAPE><xsl:value-of select="SHAPE"/></SHAPE>
    </BICYCLE>
</xsl:template>

</xsl:stylesheet>

The output is:

<BICYCLES xmlns:fn="http://www.w3.org/2005/xpath-functions" xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:xs="http://www.w3.org/2001/XMLSchema"&gt;
    <BICYCLE>
        <COLOR>BLUE</COLOR>
        <WHEEL_TYPE>FRONT</WHEEL_TYPE>
        <FLAT>NO</FLAT>
        <REFLECTOR_NUM>1</REFLECTOR_NUM>
        <SHAPE>SQUARE</SHAPE>
    </BICYCLE>
    <BICYCLE>
        <COLOR>BLUE</COLOR>
        <WHEEL_TYPE>FRONT</WHEEL_TYPE>
        <FLAT>NO</FLAT>
        <REFLECTOR_NUM>2</REFLECTOR_NUM>
        <SHAPE>ROUND</SHAPE>
    </BICYCLE>
    <BICYCLE>
        <COLOR>BLUE</COLOR>
        <WHEEL_TYPE>REAR</WHEEL_TYPE>
        <FLAT>NO</FLAT>
        <REFLECTOR_NUM/>
        <SHAPE/>
    </BICYCLE>
</BICYCLES>

What I don't like about this is that I'm outputting the color attribute in several forms:

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

It seems like there ought to be a way to make a named template and invoke it from the various places where it is needed and pass some parameter that represents the path back to the <BICYCLE> node to which it refers.

Is there a way to clean this up, say with a named template for bicycle fields, for wheel fields and for reflector fields?

In the real world example this is based on, there are many more attributes to a "bicycle" than just color, and I want to make this XSL easy to change to include or exclude fields without having to change the XSL in multiple places.

+2  A: 

You can name templates by using the name attribute. You invoke a template by name using <xsl:call-template>, and it's valid (IIRC) anywhere <xsl:apply-templates> is valid.

UPDATE (from the comments): Sounds like you want a different axis, probably ancestor. Something like ancestor::bicycle/color?

Hank Gay
But was path goes in the template? If I put in <xsl:value-of select="COLOR"/>, it won't find the color if the template is invoked from the REFECTOR template.
@meomaxy Sounds like you want a different [axis](http://www.w3schools.com/xpath/xpath_axes.asp), probably `ancestor`. Something like `ancestor::bicycle/color`?
Hank Gay
YES!!! ancestor-or-self::BICYCLE/COLOR THANKS!
@Hank: I don't want to disput the answer, but I think you also have to say `ancestor` axis is not efficient for big XML and deepest path.
Alejandro
@Alejandro efficiency is a legitimate concern (so I upvoted your comment), but when I have to choose, I tend to favor trading performance for clarity except in hotspots. If this is a hotspot, it might be worth looking into `key` etc.
Hank Gay
Note that Dmitre's solution bypasses any efficiency issues that using the `ancestor` axis might encounter.
Robert Rossney
A: 

You mean like:

<xsl:template name="wheelblock">
  <xsl:param name="color"></xsl:param>
  <!-- Do something here -->
</xsl:template>

<xsl:template match="WHEEL">
  <xsl:call-template name="wheelblock">
    <xsl:with-param name="color">whatever element/etc</xsl:with-param>
  </xsl:call-template>
</xsl:template>
darelf
No, I don't want to make the color the parameter, I want to make the path to the BICYCLE node be the parameter so that I can write one template that can find the bicycle color regardless of whether it's invoked from the BICYCLE, WHEEL or the REFLECTOR template. If the template can walk itself back up to find the right node, so much the better, but at least the caller knows the right path.
+2  A: 

What I don't like about this is that I'm outputting the color attribute in several forms:

It seems like there ought to be a way to make a named template and invoke it from the various places where it is needed and pass some parameter that represents the path back to the node to which it refers.

Is there a way to clean this up, say with a named template for bicycle fields, for wheel fields and for reflector fields?

You can achieve even more reuse than this.

The principle is to avoid conditional logic and leave the XSLT processor choose which template to select for processing. Any needed value should be passed down the path as a parameter to the applied templates.

The following transformation demonstrates these principles:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:template match="node()|@*">
  <xsl:copy>
    <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template match="BICYCLE">
  <xsl:apply-templates>
    <xsl:with-param name="pColor" select="COLOR"/>
  </xsl:apply-templates>
 </xsl:template>

 <xsl:template match="BICYCLE[not(WHEELS)]">
    <BICYCLE>
        <COLOR><xsl:value-of select="COLOR"/></COLOR>
        <WHEEL_TYPE/>
        <FLAT/>
        <REFLECTOR_NUM/>
        <COLOR/>
        <SHAPE/>
    </BICYCLE>
 </xsl:template>

 <xsl:template match="WHEELS">
   <xsl:param name="pColor"/>

   <xsl:apply-templates>
    <xsl:with-param name="pColor" select="$pColor"/>
   </xsl:apply-templates>
 </xsl:template>

 <xsl:template match="WHEEL[REFLECTORS]">
   <xsl:param name="pColor"/>

   <xsl:apply-templates select="REFLECTORS">
    <xsl:with-param name="pColor" select="$pColor"/>
    <xsl:with-param name="pWheel_Type" select="WHEEL_TYPE"/>
    <xsl:with-param name="pFlat" select="FLAT"/>
   </xsl:apply-templates>
 </xsl:template>

 <xsl:template match="WHEEL">
   <xsl:param name="pColor"/>

    <BICYCLE>
        <COLOR><xsl:value-of select="$pColor"/></COLOR>
        <WHEEL_TYPE><xsl:value-of select="WHEEL_TYPE"/></WHEEL_TYPE>
        <FLAT><xsl:value-of select="FLAT"/></FLAT>
        <REFLECTOR_NUM/>
        <COLOR/>
        <SHAPE/>
    </BICYCLE>
 </xsl:template>

 <xsl:template match="REFLECTORS">
  <xsl:param name="pColor"/>
  <xsl:param name="pWheel_Type"/>
  <xsl:param name="pFlat"/>

  <xsl:apply-templates select="REFLECTOR">
    <xsl:with-param name="pColor" select="$pColor"/>
    <xsl:with-param name="pWheel_Type" select="$pWheel_Type"/>
    <xsl:with-param name="pFlat" select="$pFlat"/>
  </xsl:apply-templates>
</xsl:template>

 <xsl:template match="REFLECTOR">
  <xsl:param name="pColor"/>
  <xsl:param name="pWheel_Type"/>
  <xsl:param name="pFlat"/>

    <BICYCLE>
        <COLOR><xsl:value-of select="$pColor"/></COLOR>
        <WHEEL_TYPE><xsl:value-of select="$pWheel_Type"/></WHEEL_TYPE>
        <FLAT><xsl:value-of select="$pFlat"/></FLAT>
        <REFLECTOR_NUM><xsl:value-of select="REFLECTOR_NUM"/></REFLECTOR_NUM>
        <COLOR><xsl:value-of select="COLOR"/></COLOR>
        <SHAPE><xsl:value-of select="SHAPE"/></SHAPE>
    </BICYCLE>
 </xsl:template>

 <xsl:template match="BICYCLE/COLOR"/>
</xsl:stylesheet>

When applied on the provided XML document, the wanted result is produced:

<BICYCLES>
 <BICYCLE>
  <COLOR>BLUE</COLOR>
  <WHEEL_TYPE>FRONT</WHEEL_TYPE>
  <FLAT>NO</FLAT>
  <REFLECTOR_NUM>1</REFLECTOR_NUM>
  <COLOR>RED</COLOR>
  <SHAPE>SQUARE</SHAPE>
 </BICYCLE>
 <BICYCLE>
  <COLOR>BLUE</COLOR>
  <WHEEL_TYPE>FRONT</WHEEL_TYPE>
  <FLAT>NO</FLAT>
  <REFLECTOR_NUM>2</REFLECTOR_NUM>
  <COLOR>WHITE</COLOR>
  <SHAPE>ROUND</SHAPE>
 </BICYCLE>
 <BICYCLE>
  <COLOR>BLUE</COLOR>
  <WHEEL_TYPE>REAR</WHEEL_TYPE>
  <FLAT>NO</FLAT>
  <REFLECTOR_NUM/>
  <COLOR/>
  <SHAPE/>
 </BICYCLE>
</BICYCLES>
Dimitre Novatchev
A: 

Thanks Hank Gay! This is what I was looking for. The following XSL has the same output as my original:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions"&gt;
<xsl:output method="xml" encoding="UTF-8" indent="yes" omit-xml-declaration="yes" xml:space="preserve"/> 

<xsl:template name="allfields">
    <BICYCLE>
        <COLOR><xsl:value-of select="ancestor-or-self::BICYCLE/COLOR"/></COLOR>
        <WHEEL_TYPE><xsl:value-of select="ancestor-or-self::WHEEL/WHEEL_TYPE"/></WHEEL_TYPE>
        <FLAT><xsl:value-of select="ancestor-or-self::WHEEL/FLAT"/></FLAT>
        <REFLECTOR_NUM><xsl:value-of select="ancestor-or-self::REFLECTOR/REFLECTOR_NUM"/></REFLECTOR_NUM>
        <SHAPE><xsl:value-of select="ancestor-or-self::REFLECTOR/SHAPE"/></SHAPE>
    </BICYCLE>
</xsl:template>

<xsl:template match="/">
<BICYCLES>
    <xsl:apply-templates/>
</BICYCLES>
</xsl:template>

<xsl:template match="BICYCLE">
<xsl:choose>
    <xsl:when test="WHEELS">
        <xsl:apply-templates/>
    </xsl:when>
    <xsl:otherwise>
        <xsl:call-template name="allfields"/>
    </xsl:otherwise>
</xsl:choose>
</xsl:template>

<xsl:template match="WHEEL">
            <xsl:apply-templates/>
    <xsl:choose>
        <xsl:when test="REFLECTORS">
            <xsl:apply-templates/>
        </xsl:when>
        <xsl:otherwise>
            <xsl:call-template name="allfields"/>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

<xsl:template match="REFLECTOR"> 
    <xsl:call-template name="allfields"/>
</xsl:template>

</xsl:stylesheet>
This is quite more inefficient than passing the necessary parameters to the templates. The `ancestor:` axis will have to be scanned many times. My answer doesn't have such inefficiencies. Another observation is that you can simplify the code by removing conditional processing (`<xsl:if>` and `<xsl:choose>`) and leave this work to the XSLT processor. To summarize, this code has a lot of space for improvement.
Dimitre Novatchev
A: 

Two ways.

Preserving your use of ancestor-or-self axis:

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

<xsl:template match="/"> 
    <BICYCLES> 
        <xsl:for-each select="BICYCLE[not(WHELLS)]|//REFLECTOR|//WHEEL[not(REFLECTORS)]"> 
            <BICYCLE> 
                <COLOR><xsl:value-of select="ancestor-or-self::BICYCLE/COLOR"/></COLOR> 
                <WHEEL_TYPE><xsl:value-of select="ancestor-or-self::WHEEL/WHEEL_TYPE"/></WHEEL_TYPE> 
                <FLAT><xsl:value-of select="ancestor-or-self::WHEEL/FLAT"/></FLAT> 
                <REFLECTOR_NUM><xsl:value-of select="ancestor-or-self::REFLECTOR/REFLECTOR_NUM"/></REFLECTOR_NUM> 
                <COLOR><xsl:value-of select="ancestor-or-self::REFLECTOR/COLOR"/></COLOR> 
                <SHAPE><xsl:value-of select="ancestor-or-self::REFLECTOR/SHAPE"/></SHAPE> 
            </BICYCLE> 
        </xsl:for-each> 
    </BICYCLES> 
</xsl:template> 

</xsl:stylesheet> 

This is just one example of misuse of the rule-processing model.

In contrast, with Dimitre post you can avoid condicional logic and apply template to your desired branch.

I think that descendant and ancestor axes are not efficient for long XML input, in this case. So, if for your further processing empty nodes and not existing nodes are the same (like with database result), this general pattern should work:

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

    <xsl:output indent="yes" method="xml"/>  

    <xsl:template match="/"> 
        <BICYCLES> 
            <xsl:apply-templates/> 
        </BICYCLES> 
    </xsl:template>  

    <!--
        When you don't want a leaf in your result, just:
        <xsl:template match="here the leaf name" /> 
    --> 

    <xsl:template match="*"> 
        <xsl:copy> 
            <xsl:apply-templates/> 
        </xsl:copy> 
    </xsl:template>  

    <xsl:template match="*[*/*]"> 
        <xsl:param name="prev" /> 
        <xsl:apply-templates select="*[*]">
            <xsl:with-param name="prev">
                <xsl:copy-of select="$prev"/>
                <xsl:apply-templates select="*[not(*)]"/>
            </xsl:with-param>
        </xsl:apply-templates> 
    </xsl:template>  

    <xsl:template match="*[* and not(*/*)]"> 
        <xsl:param name="prev" /> 
        <BICYCLE>
            <xsl:copy-of select="$prev"/>
            <xsl:apply-templates select="*"/>
        </BICYCLE>
    </xsl:template>  

</xsl:stylesheet> 

Note: you don't have to know wich are the leaf.

Edit: A worse example of using descendant and ancestor axes, and some tweaks to my proposal

Edit2: Adding a new posible leaf (I missed that the first time!) for worse implementation. Adding the possibility to exclude some leaf for second implementation.

Edit3: Just a rewrite to clarify my point.

Alejandro
Thanks! Regarding 1st implementation, is match="REFLECTOR|WHEEL[not(REFLECTORS)]"> assuming there is always a WHEELS? I don't want it to skip bicycles that have no wheels. Regarding the 2nd, in my case I actually don't want to output ALL leaves, I want to output just the leaves that I specify, but this is all VERY helpful. Thanks.
@meomaxy: The first implementation is the worst! If you have to reuse it, you need to figure out wich are the leaf (in this case every REFLECTOR defines one kind of bicycle, and also any WHEEL even if it doesn't have REFLECTORS). Regarding the second implementation, I will edit it in order to easily catch your requeriment.
Alejandro