Thursday, June 6, 2013

Date Comparison using XSLT and XPath in DataPower

It was a brain-draining day at work. I had to implement a customized SLM policy enforcement for a customer where we pull a meta-data of policies from an xml and enforce it inside an XSL transform action. The trickiest part of the day was implementing the Allowed Time of Day policy. From the parameters defined in the xml snippet we had to reject transaction if the current time is beyond the start and stop times. This is the element that we are retrieving the start and stop time from:
<Daily StartTime="08:00:00+08:00" StopTime="17:00:00+08:00"/>

After hours of attempts, below is the solution I've came up with.
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet 
 version="1.0" 
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
 xmlns:dp="http://www.datapower.com/extensions" 
 xmlns:func="http://exslt.org/functions" 
 xmlns:df="http://lmls.ph/date_func" 
 xmlns:date="http://exslt.org/dates-and-times"
 extension-element-prefixes="dp slm func" 
 exclude-result-prefixes="dp slm func">
 
 <!-- removes the padded zero in front of HH or mm or ss -->
 <func:function name="df:removeFrontPaddedZeroes">
  <xsl:param name="pad_num" />
  <xsl:if test="starts-with($pad_num, '0')">
   <func:result select="substring($pad_num,2)" />
  </xsl:if>
  <func:result select="substring($pad_num,1)" />
 </func:function>
 
 <!-- converts the time format 00:00:00+00:00 into numbers for comparison -->
 <func:function name="df:convertTimeToNumber">
  <xsl:param name="zTime" />
  <xsl:if test="substring($zTime,9,1) = '+'">   
   <func:result select="((number(df:removeFrontPaddedZeroes(substring(string($zTime),1,2))) * 10000) + (number(df:removeFrontPaddedZeroes(substring(string($zTime),4,2))) * 100) + number(df:removeFrontPaddedZeroes(substring(string($zTime),7,2)))) + ((number(df:removeFrontPaddedZeroes(substring(string($zTime),10,2))) * 10000) + (number(df:removeFrontPaddedZeroes(substring(string($zTime),13,2))) * 100))" />   
  </xsl:if>
  <func:result select="((number(df:removeFrontPaddedZeroes(substring(string($zTime),1,2))) * 10000) + (number(df:removeFrontPaddedZeroes(substring(string($zTime),4,2))) * 100) + number(df:removeFrontPaddedZeroes(substring(string($zTime),7,2)))) - ((number(df:removeFrontPaddedZeroes(substring(string($zTime),10,2))) * 10000) + (number(df:removeFrontPaddedZeroes(substring(string($zTime),13,2))) * 100))" />  
 </func:function>

 <!-- function that evaluates if the current time is within the start and stop time bounds -->
 <func:function name="df:testTimeOfDay">
  <xsl:param name="start_time" />
  <xsl:param name="stop_time" />  
  <xsl:variable name="current_time" select="date:time()" />

  <xsl:variable name="start_time_c" select="df:convertTimeToNumber($start_time)" />
  <xsl:variable name="stop_time_c" select="df:convertTimeToNumber($stop_time)" />
  <xsl:variable name="current_time_c" select="df:convertTimeToNumber($current_time)" />

  <xsl:if test="(number($current_time_c) &gt;= number($start_time_c)) and (number($current_time_c) &lt;= number($stop_time_c))">
   <func:result select="true()" />
  </xsl:if>
  <func:result select="false()" />
 </func:function>
<xsl:stylesheet>

There are three functions in this XSL namely: df:testTimeOfDay, df:converTimeToNumber, and df:removeFrontPaddedZeroes the comments explain their purposes. The converTimeToNumber is where bulk of the logic goes and before I arrived to this code I had the split it up into smaller and more easy to understand pieces:
# The input parameter is called zTime, this comes in the format of 00:00:00+00:00
zTime = "00:00:00+08:00"

# Now in the format HH:mm:ss[\-\+]Hz:mz
HH = number(df:removeFrontPaddedZeroes(substring(string($zTime),1,2))) 
mm = number(df:removeFrontPaddedZeroes(substring(string($zTime),4,2)))
ss = number(df:removeFrontPaddedZeroes(substring(string($zTime),7,2)))
op = substring(string($zTime),9,1)
Hz = number(df:removeFrontPaddedZeroes(substring(string($zTime),10,2)))
mz = number(df:removeFrontPaddedZeroes(substring(string($zTime),13,2)))

# Then logically we get HHmmss by doing (HH * 10000) + (mm *100) + (ss)
# And, Hzmz by doing (Hz * 10000) + (mz * 100)
HHmmss = (number(df:removeFrontPaddedZeroes(substring(string($zTime),1,2))) * 10000) + (number(df:removeFrontPaddedZeroes(substring(string($zTime),4,2))) * 100) + number(df:removeFrontPaddedZeroes(substring(string($zTime),7,2)))
Hzmz = (number(df:removeFrontPaddedZeroes(substring(string($zTime),10,2))) * 10000) + (number(df:removeFrontPaddedZeroes(substring(string($zTime),13,2))) * 100)

# Then our if condition should be which provides the final output in HHmmss + or - Hzmz
if op = '+'
 result = ((number(df:removeFrontPaddedZeroes(substring(string($zTime),1,2))) * 10000) + (number(df:removeFrontPaddedZeroes(substring(string($zTime),4,2))) * 100) + number(df:removeFrontPaddedZeroes(substring(string($zTime),7,2)))) + ((number(df:removeFrontPaddedZeroes(substring(string($zTime),10,2))) * 10000) + (number(df:removeFrontPaddedZeroes(substring(string($zTime),13,2))) * 100))
else 
 result = ((number(df:removeFrontPaddedZeroes(substring(string($zTime),1,2))) * 10000) + (number(df:removeFrontPaddedZeroes(substring(string($zTime),4,2))) * 100) + number(df:removeFrontPaddedZeroes(substring(string($zTime),7,2)))) - ((number(df:removeFrontPaddedZeroes(substring(string($zTime),10,2))) * 10000) + (number(df:removeFrontPaddedZeroes(substring(string($zTime),13,2))) * 100))

Finally we can call the the df:testTimeOfDay to return true() when current time when current time is greater than or equal to start time AND current time less than or equal to stop time. See snippet below:
<xsl:variable name="start_time" select="$sla_snippet/Daily/@StartTime" />
<xsl:variable name="stop_time" select="$sla_snippet/Daily/@StopTime" />

<xsl:if test="df:testTimeOfDay($start_time,$stop_time) = false()">
 <dp:xreject reason="'Time of Day policy violated.'"/>
</xsl:if>

I haven't really done much testing and error handlings with this but I was able to test it making happy paths. This at least gives an idea of how you can perform comparison between dates.

No comments: