Tuesday, February 19, 2008

Universal Package Tracking in ColdFusion

A while back, I was in need of a universal package tracking tool that is not specific to the carrier. Almost every carrier provides some kind of an API but it can be a pain to set up each one separately. Fortunately, I found a nifty tracking tool which is universal and can be leveraged through RSS. It works with UPS, FedEx, USPS, or DHL/AirBorne without having to specify which carrier you need. Instead, it determines the carrier from the tracking number.

You can check it out at and the some details can be found at

While this might not be good commercial solution, it is still usable for personal or a small site. The code below shows how to leverage this with ColdFusion. The Ajax/JavaScript implementation relies on Prototype.js. You can see it in action at and download it from

The code is well documented and should be easy to read/understand:

<cfsetting enablecfoutputonly="yes">
<!--- Setup default parameters and constants --->

<!--- Is this call to the page from javascript (Ajax) --->
<cfparam name="url.isAjaxCall" default="false" />

<!--- The message to display while loading --->
<cfset loadingMessage = "Loading..." />

<!--- Default tracking number as a url variable (trackingNumber) --->
<cfparam name="url.trackingNumber" default="" />
 Tracking page url: very nice free tracking for all carriers that you can
 call to get an rss feed generated based on your tracking number as in

 If you call it without setting the 'rss' variable as in:
 '' you can see a nice
 google map of where in route your package is.
 This service works for UPS, FedEx, USPS, or DHL/AirBorne without having
 to specify the carrier as it determins it from the tracking number.
<cfparam name="trackingPageUrl" default="" />

Function:  parseRss


rssData  string (The string of rss xml retrieved with cfhttp)
debugMode boolean

Return Value:
An array of structures containing the parsed rss feed. Example:
 link - the link from the rss item
 title - the title from the rss feed
 description - the description from the rss feed

Parses the RSS feed passed in --->
<cffunction name="parseRss" returntype="array" output="true" hint="Parses the RSS feed passed in">
 <cfargument name="rssData" type="string" required="true">
 <cfargument name="debugMode" type="string" required="false">

 <!--- Set default variables --->
 <cfset var xmlData = "">
 <cfset var result = arrayNew(1)>
 <cfset var x = "">
 <cfset var items = "">
 <cfset var xPath = "">
 <cfset var node = "">

  <!--- Parse the data as xml --->
  <cfset xmlData = xmlParse(arguments.rssData)>

  <!--- Create xpath search string based on the xml root name --->
  <cfif xmlData.xmlRoot.xmlName is "rss">
   <cfset xPath = "//item">
   <cfset xPath = "//:item">

  <!--- Get all the xml nodes matching the xpath search string --->
  <cfset items = xmlSearch(xmlData, xPath)>

  <!--- Loop through the found xml nodes and build an array of structures --->
  <cfloop index="i" from="1" to="#arrayLen(items)#">
   <cfset node = structNew()>
   <cfset = items[i].link.xmlText>
   <cfset node.title = items[i].title.xmlText>
   <cfset node.description = items[i].description.xmlText>

   <cfset result[arrayLen(result) + 1] = duplicate(node)>

 <cfreturn result>

If this is an ajax call, get the tracking results trackingPageUrl specified above --->
<cfif url.isAjaxCall and url.trackingNumber neq ''>
 <cfhttp method="get" url="#trackingPageUrl#" result="test" charset="windows-1252">
  <!--- Set the 'rss' url variable --->
  <cfhttpparam name="rss" type="url" value="1" />
  <!--- Set the tracking url variable --->
  <cfhttpparam name="t" type="url" value="#url.trackingNumber#" />

 <!--- Parse the rss feed from the contents returned by cfhttp --->
 <cfset rssFeed = parseRss(test.filecontent) />

 <cfif arraylen(rssFeed) gt 0>
  <div id="rssItem">
   <span id="description">Tracking data for tracking number
   '<span id="trackingNumber">#url.trackingNumber#</span>'</span>
  <br />
  <!--- Loop through the contents of the rss feed and display them --->
  <cfloop index="i" from="1" to="#arrayLen(rssFeed)#">
  <div id="rssItem">
   <span id="description">#rssFeed[i].description.replaceall("Package update on ", "")#</span>
  <div id="rssItem">
   <span id="description">No tracking data found for tracking number
   '<span id="trackingNumber">#url.trackingNumber#</span>'</span>
 <!--- This is not an ajax call, so display a form for the user to enter a tracking number --->
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "">
 <html xmlns="">
 <script type="text/javascript" src="/js/prototype.js"></script>
 <style type="text/css">
 span##trackingNumber { font-weight: 700; }
 div##statusContainer { position:absolute;left:0px;top:0px;width:100%;height:10px; }
 div##statusMessageContainer { position:absolute;background-color:##000000;color:white;width:70px;font-family:Arial, Helvetica, sans-serif;padding:2px;left:0px; }
 Browser Bookmarklet:
 <a href="javascript:(function(){var url='http://#cgi.http_host##cgi.script_name#';searchString=prompt('Enter your tracking number:','');searchString=((searchString==null)?'':searchString.replace(/^(\s+)?(.*?)(\s+)?$/gi,'$2'));var params='?isAjaxCall=true&t=';if(searchString!=''){if(location.href.indexOf(url)==-1){var;}else{location.href=url+params+escape(searchString);}}})();">
 Track Packages
 <br /><br />
 <form id="trackingForm" name="trackingForm" method="get" action="#cgi.script_name#" class="ajaxForm">
 <input type="hidden" id="isAjaxCall" name="isAjaxCall" value="true" />
 <input type="text" id="trackingNumber" name="trackingNumber" value="1Z04WF350314328154" />
 <input type="submit" id="getTrackingResults" name="getTrackingResults" value="Track" />
 <br />
 <!-- Results container that will be updated with the results of the request --->
 <div id="resultsContainer" class="ajaxContent"></div>
 <br />
 <!-- Status container that will be display durring processing --->
 <div id="statusContainer" style="display: none;" class="ajaxStatus">
  <div id="statusMessageContainer">Loading...</div>
 <script language="javascript" type="text/javascript">
 var trackingForm = Class.create();
 trackingForm.prototype = {
  ajaxContainerElement: 'div',
  formID: '',
  ajaxUrl: '',
  resultsContainer: '',
  Function: initialize
  Description: Performs various intiliazion tasks for the form
  initialize: function() {
   var ajaxFormsList = $$('form.ajaxForm');
   var ajaxContainersList = $$(this.ajaxContainerElement + '.ajaxContent');

   if (ajaxFormsList.length > 0 && ajaxContainersList.length > 0) {
    this.formID = ajaxFormsList[0].id;
    this.ajaxUrl = $(this.formID).action;
    this.resultsContainer = ajaxContainersList[0].id;

    // Tie the submit event to the submitForm function
    $(this.formID).observe('submit', this.submitForm.bind(this));

    // Reset the form

    // Activate the first element on the form
  Function: submitForm
  Description: Submits the form
  submitForm: function(event) {
   // Serialize the form parameters to pass them along as part of the form submission
   var params = $(this.formID).serialize(true);

   // Check if the tracking number is empty
   if (!params.trackingNumber.empty()) {
    // Disable the form

    // Make an ajax request passing it the serialized form    
    new Ajax.Updater(
     method: 'get',
     parameters: params,
     onFailure: this.reportError.bindAsEventListener(this),
     onSuccess: this.processResults.bindAsEventListener(this),
     evalScripts: true

   // Prevent the form from being submitted
  Function: processResults
  Description: Processes the server results
  processResults: function() {
   // Enable the form
   // Reset the form

   // Activate the first element on the form
  reportError: function(request){}

 Event.observe(window, 'load', function() {
  var ajaxStatusContainersList = $$('div.ajaxStatus');
  var ajaxStatusContainer = '';

  // Create an instance of the form object defined above
  trackingFormInstance = new trackingForm();

  if (ajaxStatusContainersList.length > 0) {
   ajaxStatusContainer = $$('div.ajaxStatus')[0].id;

    onCreate: function() {
    onComplete: function() {


// //]]>