Search

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 http://isnoop.net/tracking and the some details can be found at http://isnoop.net/blog/?p=19.

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 http://blog.tech-cats.net/examples/universalPackageTracking.cfm and download it from http://blog.tech-cats.net/examples/universalPackageTracking.txt

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
 'http://isnoop.net/tracking/index.php?t=85642012466&rss=1'.

 If you call it without setting the 'rss' variable as in:
 'http://isnoop.net/tracking/index.php?t=85642012466' 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="http://isnoop.net/tracking/index.php" />

<!---
Function:  parseRss

Arguments:

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

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

Description:
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 = "">

 <cftry>
  <!--- 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">
  <cfelse>
   <cfset xPath = "//:item">
  </cfif>

  <!--- 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 node.link = 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)>
  </cfloop>
  <cfcatch>
  </cfcatch>
 </cftry>

 <cfreturn result>
</cffunction>

<cfoutput>
<!---
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#" />
 </cfhttp>

 <!--- 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>
  </div>
  <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>
  </cfloop>
 <cfelse>
  <div id="rssItem">
   <span id="description">No tracking data found for tracking number
   '<span id="trackingNumber">#url.trackingNumber#</span>'</span>
  </div>
 </cfif>
<cfelse>
 <!--- 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" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
 <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; }
 </style>
 </head>
 <body>
 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 win=window.open(url+params+escape(searchString));}else{location.href=url+params+escape(searchString);}}})();">
 Track Packages
 </a>
 <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" />
 </form>
 <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>
 </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
    $(this.formID).reset();

    // Activate the first element on the form
    $(this.formID).findFirstElement().activate();
   }
  },
  /*
  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
    $(this.formID).disable();

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

   // Prevent the form from being submitted
   Event.stop(event);
  },
  /*
  Function: processResults
  Description: Processes the server results
  */
  processResults: function() {
   // Enable the form
   $(this.formID).enable();
 
   // Reset the form
   $(this.formID).reset();

   // Activate the first element on the form
   $(this.formID).findFirstElement().activate();
  },
  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;

   Ajax.Responders.register({
    onCreate: function() {
     $(ajaxStatusContainer).show();
    },
    onComplete: function() {
     $(ajaxStatusContainer).hide();
    }
   });
  }
 });
 </script>
 </body>
 </html>
</cfif>
</cfoutput>
// //]]>