XML.com: XML From the Inside Out
oreilly.comSafari Bookshelf.Conferences.

advertisement

Tracking Packages with RSS
by Yakov Shafranovich | Pages: 1, 2

Playing with XSLT

In the end, the track2rss project consisted of two parts: a set of XSLT stylesheets to process the XML and a 200 line wrapper script written in Perl. I started off generating the correct XML input packet. The resulting stylesheet simply takes an empty XML file and several parameters as passed into the XSLT processor, and puts them into the right places in the XML (see the stylesheet and sample input packet). Of course it is not necessary to use XSLT or the XML APIs to do this; I could have simply recorded the whole packet inside the wrapper script and used some sort of a replace function to plug in the parameters. But since I am using the XSLT for output anyway, I decided to use it for input as well as follows :

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method = "xml" indent="yes"/>
   
<xsl:param name="service_key"/>
<xsl:param name="service_username"/>
<xsl:param name="service_password"/>
<xsl:param name="tracking_number"/>
<xsl:param name="version"/>
   
<xsl:template match="/">	
	<AccessRequest xml:lang="en-US">
	<AccessLicenseNumber><xsl:value-of select="$service_key"/>
	</AccessLicenseNumber>
	<UserId><xsl:value-of select="$service_username"/></UserId>
	<Password><xsl:value-of select="$service_password"/></Password>
	</AccessRequest>
   
	<xsl:text disable-output-escaping="yes">&lt;?xml version='1.0'?&gt;</xsl:text>
	<TrackRequest xml:lang='en-US'>
	<Request>
	<TransactionReference>
	<CustomerContext><xsl:value-of select="$version"/></CustomerContext>
	<XpciVersion>1.0001</XpciVersion>
	</TransactionReference>
	<RequestAction>Track</RequestAction>
	<RequestOption>activity</RequestOption>
	</Request>
	<TrackingNumber><xsl:value-of select="$tracking_number"/></TrackingNumber>
	</TrackRequest>
</xsl:template>
   
<xsl:template match="text()">
</xsl:template>
   
</xsl:stylesheet>

Unlike the input stylesheet, the output stylesheet operates directly on the XML packets received from the carrier. However, I also wanted to include several other things in the feed produced by this project:

  • An error message if the transaction failed.
  • A link to the HTML version of the carrier's tracking site.
  • A mandatory disclaimer.
  • The version and name of the program that generated the feed.
  • The ability to set a custom CSS stylesheet to format the output RSS feed in a browser.

I started the output stylesheet by passing two parameters indicating a URL to the CSS stylesheet and the version of the program:


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

<xsl:param name="url_stylesheet"/>
<xsl:param name="version"/>

The first step in the actual template is to check for the CSS parameter ($url_stylesheet) and set it if present. Being that the CSS stylesheet is an XML processing instruction ("<?xml-stylesheet?>"), it must be placed in the beginning of the output XML file (note that the use of the xsl:text tags to escape it):

<xsl:template match="/">
<xsl:if test="$url_stylesheet">
	<xsl:text disable-output-escaping="yes">&lt;?xml-stylesheet href="</xsl:text>
		<xsl:value-of select="$url_stylesheet"/>
	<xsl:text disable-output-escaping="yes">" type="text/css"?&gt;</xsl:text>
</xsl:if>

At this point I need to start creating the actual RSS feed. The basic RSS 2.0 feed consists of a single root rss element containing within it a single channel element. Within the channel element there are some metadata elements containing information about the feed and multiple item elements. I put some text and the tracking number into the title element, along with a link to UPS's tracking website which goes into the link element :

<rss version="2.0">

<channel> <title>UPS Tracking Information for <xsl:value-of select="TrackResponse/Shipment/Package/TrackingNumber"/></title> <link>http://wwwapps.ups.com/WebTracking/processInputRequest? sort_by=status&amp;tracknums_displayed=1 &amp;TypeOfInquiryNumber=T&amp;loc=en_US&amp;InquiryNumber1= <xsl:value-of select="TrackResponse/Shipment/Package/TrackingNumber"/> &amp;track.x=0&amp;track.y=0</link>

To comply with UPS's license for the API, I include a disclaimer in the description element along with the tracking number (note the use of HTML br tags for formatting the output in RSS readers):

<description>This RSS feed tracks UPS package #
   <xsl:value-of select="TrackResponse/Shipment/Package/TrackingNumber"/>
   &lt;br/&gt;
   DISCLAIMER GOES HERE
</description>

Here I indicate the language of the feed and the version of the program, using the $version parameter :

<language>en-us</language>
<generator><xsl:value-of select="$version"/></generator>

The next step is to check the /TrackResponse/Response/ResponseStatusCode element for errors. If there are no errors, then we can call the rest of the stylesheet. Otherwise, an item element will be created with the error message instead. As the last step, we close off all the tags :

<xsl:choose>
   <xsl:when test="/TrackResponse/Response/ResponseStatusCode = '1'">
   	<xsl:apply-templates select="TrackResponse/Shipment/Package/Activity"/>
   </xsl:when>
   <xsl:otherwise>
   <item>
   	<title>TRACKING REQUEST FAILED</title>
   	<description>Failed to retrieve data from UPS, error message:
		<xsl:value-of select="TrackResponse/Response/Error/ErrorDescription"/>
	</description>
   </item>
   </xsl:otherwise>
</xsl:choose>
</channel>
</rss>
</xsl:template>

Now that the main channel element has been taken care of, the next step is to process the actual tracking information. For this we have to match all of the individual Activity elements and transform them into the corresponding RSS item elements in a separate template. Since UPS uses special codes for activity types, we need to use xsl:choose to transform them into human-readable descriptions and put them inside the title element :

<xsl:template match="Activity">
<item>
<title>
<xsl:choose>
   <xsl:when test="Status/StatusType/Code = 'I'">IN TRANSIT</xsl:when>

<xsl:when test="Status/StatusType/Code = 'D'">DELIVERED</xsl:when>

<xsl:when test="Status/StatusType/Code = 'X'">EXCEPTION</xsl:when>

<xsl:when test="Status/StatusType/Code = 'P'">PICKUP</xsl:when>

<xsl:when test="Status/StatusType/Code = 'M'">MANIFEST PICKUP</xsl:when>

<xsl:otherwise>UNKNOWN</xsl:otherwise>

</xsl:choose>

</title>

For the description element, we include the date/time, location and status of the tracking event. In order to transform the UPS's date/time formats, some processing is required with the XSLT substring function. Additionally, since the ActivityLocation/Address/City element is optional, an xsl:if statement is needed (note the use of br tags for formatting) :

<description>
   Date/Time :
   <xsl:value-of select="substring(Date, 5, 2)"/>/
   <xsl:value-of select="substring(Date, 7, 2)"/>/
   <xsl:value-of select="substring(Date, 1, 4)"/>
   &#160;   
   <xsl:value-of select="substring(Time, 1, 2)"/>:
   <xsl:value-of select="substring(Time, 3, 2)"/>:
   <xsl:value-of select="substring(Time, 5, 2)"/>
   &lt;br/&gt;
   
   Location :
   <xsl:if test="ActivityLocation/Address/City">
   	<xsl:value-of select="ActivityLocation/Address/City"/>,&#160;
   </xsl:if>
   <xsl:value-of select="ActivityLocation/Address/StateProvinceCode"/>&#160;
   <xsl:value-of select="ActivityLocation/Address/CountryCode"/>
   &lt;br/&gt;
   Status: <xsl:value-of select="Status/StatusType/Description"/>
</description>

We end up with a link element, which is identical to the link element we used in the channel element above, and follow by closing the tags. Even though the RSS 2.0 specification does not mandate the link element, some readers like FireFox will not parse the feed without it (note that we navigate back up to get the tracking number) :

<link>http://wwwapps.ups.com/WebTracking/processInputRequest?
     sort_by=status&amp;tracknums_displayed=1
&amp;TypeOfInquiryNumber=T&amp;loc=en_US&amp;InquiryNumber1=
<xsl:value-of select="../../../../TrackResponse/Shipment/Package/TrackingNumber"/>
     &amp;track.x=0&amp;track.y=0
</link>
</item>
</xsl:template>

The complete output stylesheet can be found here along with the resulting RSS feed.

Writing the Wrapper Script

Once we are done with the XSLT part of the project, its time to move to the wrapper script that actually runs it. It consists of four parts:

  1. The first part contains the necessary settings for each carrier such as authentication information, URLs, etc. Due to security and portability issues, it would be better to store them in a secure fashion in a separate configuration file, but to keep things simple I chose not to do that here.
  2. The second part checks that all the input parameters to the script are not empty, and parses them from the HTTP request.
  3. The third part generates the request packet by invoking the XSLT processor with the input template, and sends the request to UPS.
  4. The fourth part processes the response and transforms the resulting XML into RSS via another call to the XSLT processor.

Leaving aside the mechanics of setting variables and parsing HTTP requests, I want to concentrate on the interaction between the wrapper and the XSLT templates. In this example, I picked the XML:libXSLT Perl module which itself interfaces to the GNOME's libXSLT library (the other XSLT processor for Perl, XML::XSLT did not support enough XSLT features to process my stylesheets correctly). To use the library, I needed to add the following to the Perl script:

use XML::LibXSLT;

Getting past all the initial code, I initialize the XSLT library and create an empty XML object which will be used to generate the input XML packet:

my $parser = XML::LibXML->new();
my $xslt = XML::LibXSLT->new();
my $source = $parser->parse_string('<?xml version="1.0"?><xml/>');

After that, we parse the input XSLT stylesheet from a file and invoke the XSLT processor. The parameters which are passed to the processor are defined in the beginning of the script and contain the authentication information for UPS, and the tracking number of the package (which is extracted from the input request when the script is called) :

my $style_doc = $parser->parse_file($input_xsl);
my $stylesheet = $xslt->parse_stylesheet($style_doc);
my $results = $stylesheet->transform($source,
	XML::LibXSLT::xpath_to_string(service_key => $service_key),
   	XML::LibXSLT::xpath_to_string(service_username => $service_username),
	XML::LibXSLT::xpath_to_string(service_password => $service_password),
	XML::LibXSLT::xpath_to_string(tracking_number => $tracking_number)
	);

Once the request packet is generated via XSLT, I send it on its merry way by using the famous libwww-perl library:


my $req;
$ua = LWP::UserAgent->new;

$req = HTTP::Request->new(POST => $service_url_track);
$req->content_type('application/x-www-form-urlencoded');
$req->add_content($stylesheet->output_string($results));

my $res = $ua->request($req);

To process the response, I follow a similar routine, except that I use the response returned from UPS instead of the empty XML:

my $source = $parser->parse_string($response->content);
my $style_doc = $parser->parse_file($output_xsl);
my $stylesheet = $xslt->parse_stylesheet($style_doc);
my $results = $stylesheet->transform($source,
	XML::LibXSLT::xpath_to_string(version => $version)
	XML::LibXSLT::xpath_to_string(url_stylesheet => $url_stylesheet)
   );

Once processed, we simply print the output to the user and exit:


print "Content-Type: application/xml\n\n";
print $stylesheet->output_string($results);
exit;

The full script can be downloaded here. Figures 1 and 2 below show how the resulting feed appears in the Bloglines web-based RSS reader and Liferea desktop RSS reader for Linux/GNOME.

Bloglines Screenshot
Figure 1. BlogLines Screenshot

Liferea Screenshot
Figure 2. Liferea Screenshot

Conclusion

Using XSLT for generating RSS feeds for UPS package tracking turned out to be a much simpler task than writing a straightforward program. Having XSLT do the heavy lifting of dealing with XML freed me to concentrate on the mundane programming tasks of working with HTTP requests and responses, illustrating how XSLT can reduce the complexity of XML processing. In the future, I plan to integrate additional carriers into the track2rss project, as well as additional output formats and languages.