Wednesday, February 21, 2018

Spring Integration and XSLT with parameters

I have had the opportunity to use Spring Integration to execute the XSLT on the projects I've been on. It is actually quite easy and very fast. It allows you to pass in properties to the XSLT, which can be very useful. For instance, maybe there is a database lookup that you need to perform to get a value, or perhaps there are values in the Headers that are needed in the transformed message.

When dealing with XML it is always a good idea to have an XSD the defines what the valid XML should look like. There should be an XSD for the incoming XML and another XSD for the transformed XML. It is good practice to perform XSD validation on both to ensure the messages that are being received and the messages that are being sent conform to the XSD specifications.

Let's start with the Spring Configuration. The following spring code was part of a chain, so it doesn't have the input and output channels explicitly defined. There are a few interesting parts that some may not be familiar with I'd like to point out. First, this code is making use of properties that get substituted at run time by a Spring PropertyPlaceholderConfigurer. Both of the property's below are paths, and the values are the relative path inside the class path.

The second interesting part is the parameter that is being passed in. It is accessing a value defined in a Java class, this could also just as easily call a Java method to return a value. Again the class name includes the relative path from the class path.

<int-xml:validating-filter schema-location="${my.relative.incoming.xsd.file.location}" throw-exception-on-rejection="true"/>
<int-xml:xslt-transformer          
    xsl-resource="${my.relative.xslt.file.location}" >
    <int-xml:xslt-param name="trackingId" 
        expression="headers.get(T(com.mycompany.package.MyClass).MESSAGE_ID)"/>
</int-xml:xslt-transformer>
<int-xml:validating-filter schema-location="${my.relative.outgoing.xsd.file.location}" throw-exception-on-rejection="true"/>

The above code will first validate the incoming XML against the incoming XSD. If it validates correctly it will then perform the XSLT transformation, and finally it will validate that the outgoing XML matches what is expected from the outgoing XSD. That's really all there is to it.

Some will wonder what happens if it fails either validation or the transformation. It will throw an error and the error handling that was defined for the flow will do what ever it was configured to do. Error handling is out of scope for this blog.

The below XSLT demonstrates how to receive the parameter into the XSLT, and how to use it in the XSLT. The line above the template match line, is where the variable is received by the XSLT. To use the variable you just access it using $trackingId.

<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="no"/>
    <xsl:param name="trackingId" />
    <xsl:template match="/weatherDetail">
        <WeatherResponse>
            <Header>
                <Timestamp>
                    <xsl:value-of select="current-dateTime()" />
                </Timestamp>
            </Header>
            <Body>
                <externalId>
                    <xsl:value-of select="$trackingId" />
                </externalId>
            </Body>
        </WeatherResponse>
    </xsl:template>
</xsl:stylesheet> 

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.