Tuesday, February 20, 2018

XSLT Templates and filtering based on elements

Sometimes when processing XML, there is a need to loop over a list. Templates are a very powerful way to accomplish this. Consider the following XML Response from a weather system. For, the sake of space am only including 3 of the hourly points, but you can easily imagine a whole days worth. There is also a second section that contains some daily weather totals with a type of CUM or AVG.

<weatherdetail>
   <icao>KJLN</icao>
   <startdatetime>2018-01-17-00.00.00</startdatetime>
   <enddatetime>2018-01-18-00.00.00</enddatetime>
   <weatherdata>
      <weatherdatalist>
         <hour>0</hour>
         <temp>20</temp>
         <relhumidity>0</relhumidity>
         <wind>5</wind>
         <precip>0.0</precip>
      </weatherdatalist>
      <weatherdatalist>
         <hour>1</hour>
         <temp>25</temp>
         <relhumidity>10</relhumidity>
         <wind>15</wind>
         <precip>0.1</precip>
      </weatherdatalist>
      <weatherdatalist>
         <hour>23</hour>
         <temp>10</temp>
         <relhumidity>0</relhumidity>
         <wind>5</wind>
         <precip>0.0</precip>
      </weatherdatalist>
   </weatherdata>
   <weathertotals>
      <dailytotal>
         <type>CUM</type>
         <parameter>precip</parameter>
         <value>0.50</value>
         <date>2018-01-17</date>
      </dailytotal>
      <dailytotal>
         <type>AVG</type>
         <parameter>precip</parameter>
         <value>0.05</value>
         <date>2018-01-17</date>
      </dailytotal>
      <dailytotal>
         <type>AVG</type>
         <parameter>temp</parameter>
         <value>19</value>
         <date>2018-01-17</date>
      </dailytotal>
      <dailytotal>
         <type>AVG</type>
         <parameter>relHumid</parameter>
         <value>5</value>
         <date>2018-01-17</date>
      </dailytotal>
      <dailytotal>
         <type>AVG</type>
         <parameter>Wind</parameter>
         <value>12</value>
         <date>2018-01-17</date>
      </dailytotal>
   </weathertotals>
</weatherdetail>


Suppose one customer has the need receive a response as follows for graphing purposes. You need an easy way to transform the above XML into this.

<weatherresponse>
   </weatherresponse><header>
      <timestamp>2018-02-20T20:09:48.546Z</timestamp>
      <icao>KJLN</icao>
      <startdatetime>2018-01-17T00:00:00.000</startdatetime>
      <enddatetime>2018-01-18T00:00:00.000</enddatetime>
   </header>
   <body>
   <data>
         <hour>0</hour>
         <temperature>20</temperature>
         <humidity>0</humidity>
         <wind>5</wind>
         <precip>0.0</precip>
      </data>
      <data>
         <hour>1</hour>
         <temperature>25</temperature>
         <humidity>10</humidity>
         <wind>15</wind>
         <precip>0.1</precip>
      </data>
      <data>
         <hour>23</hour>
         <temperature>10</temperature>
         <humidity>0</humidity>
         <wind>5</wind>
         <precip>0.0</precip>
      </data>
   </body>

Let's look at the following XSLT.

There are two different types of templates in this XSLT. One is a named template, formatMyDate. This is a slick way to create essentially a method to do something. In this case, it converts the date format from the incoming format to the format required for the outgoing XML. Multiple parameters can be passed in to this method, but in this example only one is being passed in.

The second type of template is the main one being covered today. This one is applying a template to a specific XPATH, if your XPATH was further nested you would just provide the additional nodes, like: <xsl:apply-templates select="weatherData/additionalNode1/additionalNode2">

As you can see you can also pass parameters into this template call. I added that purely for demonstration as I am not really using the parameter for anything.

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

    <xsl:template match="/weatherDetail">
        <weatherresponse>
            <header>
                <timestamp>
                    <xsl:value-of select="current-dateTime()">
                </xsl:value-of></timestamp>
                <icao>
                    <xsl:value-of select="icao">
                </xsl:value-of></icao>
                <startdatetime>
                    <xsl:call-template name="formatMyDate">
                        <xsl:with-param name="dateValue">
                            <xsl:value-of select="startDateTime">
                        </xsl:value-of></xsl:with-param>
                    </xsl:call-template>
                </startdatetime>
                <enddatetime>
                    <xsl:call-template name="formatMyDate">
                        <xsl:with-param name="dateValue">
                            <xsl:value-of select="endDateTime">
                        </xsl:value-of></xsl:with-param>
                    </xsl:call-template>
                </enddatetime>
            </header>
            <body>
              <xsl:apply-templates select="weatherData">
                 <xsl:with-param name="icao"> 
                    <xsl:value-of select="icao">
                 </xsl:value-of></xsl:with-param>    
              </xsl:apply-templates> 
           </body>
        </weatherresponse>
    </xsl:template>

    <!-- This is the template for hourly Processing -->
    <xsl:template match="weatherDataList">
       <xsl:param name="icao">
       <data>
          <hour>
             <xsl:value-of select="hour">
          </xsl:value-of></hour>
          <temperature>
             <xsl:value-of select="temp">
          </xsl:value-of></temperature>
          <humidity>
             <xsl:value-of select="relHumidity">
          </xsl:value-of></humidity>
          <wind>
             <xsl:value-of select="wind">
          </xsl:value-of></wind>
          <precip>
             <xsl:value-of select="precip">
          </xsl:value-of></precip>
       </data>
    </xsl:param></xsl:template>
    
    <xsl:template name="formatMyDate">
        <xsl:param name="dateValue">
        <xsl:variable name="datePart" select="substring($dateValue, 1, 10)">
        <xsl:variable name="timePart" select="translate(substring($dateValue, 12), '.',  ':')">
        <xsl:value-of select="concat($datePart,'T', $timePart,'.000')">
    </xsl:value-of></xsl:variable></xsl:variable></xsl:param></xsl:template>

</xsl:stylesheet> 

That was pretty slick. Now let's suppose there is another customer who wants to process the daily values, but only ones that are of type AVG. How do we filter on an element in the XML we are trying to process? Check this out:


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

    <xsl:template match="/weatherDetail">
        <weatherresponse>
            <header>
                <timestamp>
                    <xsl:value-of select="current-dateTime()">
                </xsl:value-of></timestamp>
                <icao>
                    <xsl:value-of select="icao">
                </xsl:value-of></icao>
                <startdatetime>
                    <xsl:call-template name="formatMyDate">
                        <xsl:with-param name="dateValue">
                            <xsl:value-of select="startDateTime">
                        </xsl:value-of></xsl:with-param>
                    </xsl:call-template>
                </startdatetime>
                <enddatetime>
                    <xsl:call-template name="formatMyDate">
                        <xsl:with-param name="dateValue">
                            <xsl:value-of select="endDateTime">
                        </xsl:value-of></xsl:with-param>
                    </xsl:call-template>
                </enddatetime>
            </header>
            <body>
                 <xsl:apply-templates select="weatherTotals">
                    <xsl:with-param name="icao"> 
                       <xsl:value-of select="icao">
                    </xsl:value-of></xsl:with-param>    
                 </xsl:apply-templates> 
           </body>
        </weatherresponse>
    </xsl:template>
  
    <!-- This is the template for AVG daily Processing -->
    <xsl:template match="dailyTotal[type='AVG']">
       <xsl:param name="icao">
       <data>
         <parameter>
            <xsl:value-of select="parameter">
         </xsl:value-of></parameter>
         <avg>
            <xsl:value-of select="value">
         </xsl:value-of></avg>
         <date>
            <xsl:value-of select="date">
         </xsl:value-of></date>
       </data>
   </xsl:param></xsl:template>
   
   <xsl:template match="dailyTotal[type!='AVG']">
   </xsl:template>

    
    <xsl:template name="formatMyDate">
        <xsl:param name="dateValue">
        <xsl:variable name="datePart" select="substring($dateValue, 1, 10)">
        <xsl:variable name="timePart" select="translate(substring($dateValue, 12), '.',  ':')">
        <xsl:value-of select="concat($datePart,'T', $timePart,'.000')">
    </xsl:value-of></xsl:variable></xsl:variable></xsl:param></xsl:template>

</xsl:stylesheet> 


The above XSLT generates the desired output:


<weatherresponse>
   <header>
      <timestamp>2018-02-20T20:26:53.832Z</timestamp>
      <icao>KJLN</icao>
      <startdatetime>2018-01-17T00:00:00.000</startdatetime>
      <enddatetime>2018-01-18T00:00:00.000</enddatetime>
   </header>
   <body>
      <data>
         <parameter>precip</parameter>
         <avg>0.05</avg>
         <date>2018-01-17</date>
      </data>
      <data>
         <parameter>temp</parameter>
         <avg>19</avg>
         <date>2018-01-17</date>
      </data>
      <data>
         <parameter>relHumid</parameter>
         <avg>5</avg>
         <date>2018-01-17</date>
      </data>
      <data>
         <parameter>Wind</parameter>
         <avg>12</avg>
         <date>2018-01-17</date>
      </data>
   </body>
</weatherresponse>

A easy way to validate your XSLT against some known XML is to use one of the free online XSLT testing tools.

No comments: