//=============================================================================
//
// File: /jsLibraries/bif/ltd.js
//
// Language: JavaScript
//
// Contents: The Lightweight Tabular Data (LTD) protocol for asynchronous
// HTTP service request and response.
//
// Author: Joe Honton © 2009
//
// Initial date: June 22, 2009
//
// Notes: This protocol uses an XMLHttpRequest object to send a request to
// a service. The service is expected to respond with a Content-type
// of "text/html" that is formatted with a prescribed layout. This
// prescribed layout uses a tabular collection of rows and columns
// formatted using the comma separated value (CSV) encoding. The
// response contains four parts: 1) the protocol version identifier,
// 2) the metadata, 3) the payload, and 4) the status indicator.
//
// An example of a simple response might look like this:
/*
Lightweight Tabular Data,1.0
"ID","Date","Value","Description"
1,2009-07-01,1234.56,"One thousand, two hundred thirty four point fifty six"
2,2009-07-02,,Nothing
3,2009-07-03,78.00,"Seventy-eight ""Exactly"""
0,OK
*/
//=============================================================================
//-----------------------------------------------
// namespace
var bif;
if (!bif) bif = {};
//-----------------------------------------------
// module
bif.ltd = {}; // the class providing XMLHttpRequest functionality
bif.ltdobj = {}; // the object returned by the response
//=============================================================================
// bif.ltd
//=============================================================================
//-------------------------------------
bif.ltd.XMLHttpFactories = [
function () {return new XMLHttpRequest()},
function () {return new ActiveXObject("Msxml2.XMLHTTP")},
function () {return new ActiveXObject("Msxml3.XMLHTTP")},
function () {return new ActiveXObject("Microsoft.XMLHTTP")}
];
//-------------------------------------
//^ The initialize function instantiates the request object. This function
// is called internally by the request function
bif.ltd.initialize = function()
{
var xmlhttp = false;
for ( var i=0; i < bif.ltd.XMLHttpFactories.length; i++ )
{
try
{
xmlhttp = bif.ltd.XMLHttpFactories[i]();
}
catch (e)
{
continue;
}
break;
}
return xmlhttp;
};
//-------------------------------------
//^ The request function sends an HTTP GET or POST request to the specified service
//> url is the fully qualified URL of the service, including the "http://" protocol.
// The service must be designed to return a Content-type of "text/html" using
// the UTF-8 charset with the TLD protocol's prescribed data layout.
//> callback is the function to call when the data is ready.
// The callback function signature includes a single variable that recieves the LTD object.
//> postData is the HTTP POST form data to send to the service, or null when using HTTP GET
// The postData should be a string containing keyword-value pairs something like this: "keyword=one&keyword2=two"
//> returns true if the request was successfully sent, or false if the request couldn't be initiated.
//
bif.ltd.request = function( url, callback, postData )
{
var h = bif.ltd.initialize();
if ( !h )
return false;
// open the request
var method = (postData) ? "POST" : "GET";
h.open( method, url, true );
// h.setRequestHeader( 'User-Agent', 'XMLHTTP/1.0' ); // Chrome says "Refused to set unsafe header"
if ( postData )
{
h.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' );
// h.setRequestHeader( 'Content-Length', postData.length ); // Chrome says "Refused to set unsafe header"
// h.setRequestHeader( 'Connection', 'close' ); // Chrome says "Refused to set unsafe header"
}
//-------------------------------------
// private anonymous callback function
h.onreadystatechange = function ()
{
// if the readystate is not 4, the request is not yet "done", continue.
if ( h.readyState != 4 )
return;
// A status of 200 means "OK" and a status of "304" means that the request was satisfied through the browser's cache
if ( h.status == 200 || h.status == 304 )
{
bif.ltd.response( h, callback );
return;
}
// When doing cross domain requests, XMLHttpRequest sends an HTTP OPTIONS request rather than a GET or POST
// and this may return a status of 0, meaning that there is no response, the server has ignored the client request,
// and the server provides no information, even in the "statusText" field. In Firefox (3.6) this will result in an uncaught
// exception showing in Firebug: uncaught exception: [Exception... "Component returned failure code: 0x80040111 (NS_ERROR_NOT_AVAILABLE) [nsIXMLHttpRequest.statusText]" ... data: no]
//
// Note that a cross domain request will occur when the source page is hosted on "example.com" and the XMLHttpRequest is
// for a page on "www.example.com"
//
else if ( h.status == 0 )
{
var obj = new bif.ltdobj();
obj.httpStatus = h.status;
try
{
obj.httpStatusText = h.statusText;
}
catch(err)
{
// uncaught exception: [Exception... "Component returned failure code: 0x80040111 (NS_ERROR_NOT_AVAILABLE) [nsIXMLHttpRequest.statusText]" ... data: no]
obj.httpStatusText = 'unknown status text returned by XMLHttpRequest.statusText';
}
obj.errorCode = 1;
obj.errorMessage = 'Cross domain XMLHttpRequest failed';
callback( obj );
return;
}
// all other typical failures, such as 404 "not found"
else // ( h.status != 0 && h.status != 200 && h.status != 304 )
{
var obj = new bif.ltdobj();
obj.httpStatus = h.status;
obj.httpStatusText = h.statusText;
obj.errorCode = 1;
obj.errorMessage = 'XMLHttpRequest failed';
callback( obj );
return;
}
};
if ( h.readyState == 4 )
return false;
// send the request
h.send( postData );
return true;
};
//-------------------------------------
//^ The response function is called asynchronously by the XMLHttpRequest object
// when the HTTP request is ready. This function will parse the responseText
// into the ltdobj response object, then pass it on to the user's registered callback function.
//
bif.ltd.response = function( httpObject, callback )
{
var obj = new bif.ltdobj();
obj.httpStatus = httpObject.status;
obj.httpStatusText = httpObject.statusText;
obj.rawResponse = httpObject.responseText;
var lines = httpObject.responseText.split('\n');
var countLines = lines.length;
// remove any trailing blank lines
while (lines[countLines-1] == '' )
{
countLines--;
}
if ( countLines == 0 )
{
obj.errorCode = 401;
obj.errorMessage = 'The service did not return anything.';
obj.errorMessage += ' XMLHttpRequest returned status code ' + obj.httpStatus + ': ' + obj.httpStatusText;
}
else
{
// read the first line, which should contain the protocol pronouncement
var pronouncement = bif.ltd.splitCSV( lines[0] );
if ( countLines < 2 || pronouncement[0] != 'Lightweight Tabular Data' || pronouncement[1] != '1.0' )
{
obj.errorCode = 402;
obj.errorMessage = 'The service response is not in Lightweight Tabular Data version 1.0 format.';
obj.errorMessage += ' XMLHttpRequest returned status code ' + obj.httpStatus + ': ' + obj.httpStatusText;
}
else
{
// It is legal for the response to contain only two lines: the pronouncement and the status indicator.
// If the response contains three or more lines, then go ahead and start reading here:
if ( countLines > 2 )
{
var i = 0; // careful when using i in more than one for loop
// parse the second line into the column names, and put them into both the classic array and the associative lookup array
cols = bif.ltd.splitCSV( lines[1] );
obj.columnCount = cols.length;
obj.columnNames = new Array( obj.columnCount );
obj.columnNameLookup = new Array( obj.columnCount );
for ( i = 0; i < obj.columnCount; i++ )
{
obj.columnNames[i] = cols[i];
obj.columnNameLookup[ cols[i] ] = i;
}
// loop through the payload, everything except line 0, line 1, and line n-1 (so iterate over three less than the total)
obj.data = new Array(); // the outer array of rows
for ( i = 0; i < countLines - 3; i++ )
{
var ii = i+2;
if ( lines[ii] != '' )
{
// each line of the payload is composed of items surrounded by double quotes and separated by commas.
var items = bif.ltd.splitCSV( lines[ii] );
var countItems = items.length;
if ( countItems != obj.columnCount )
{
obj.errorCode = 403;
obj.errorMessage = 'There are ' + obj.columnCount + ' column headers, but item ' + i + ' contains ' + countItems + ' columns of data';
}
obj.data[i] = new Array( countItems ); // an inner array of columns
for ( var j = 0; j < countItems; j++ )
obj.data[i][j] = items[j];
}
}
obj.rowCount = obj.data.length;
}
// the last line should contain the success code and the success message (or error code and error message),
// as returned from the remote service.
var success = bif.ltd.splitCSV( lines[countLines-1] );
if ( success.length >= 2 )
{
obj.errorCode = success[0];
obj.errorMessage = success[1];
}
}
}
// call the registered callback function with the response object
callback( obj );
};
//-------------------------------------
//^ The splitCSV function accepts a single "line" of CSV encoded values and returns an
// array containing a collection of column values. The string being parsed
// is expected to be a single line of data in comma separated value format where individual columns
// are separated by commas and column values are optionally enclosed in double quotes
// and double quotes within a column value are escaped by doubling, e.g. "column ""quoted"" value".
// Escaped line feeds, "\n", are converted to 0x0A
//> strLine is the line of CSV text to split
//> sep is a comma or other column delimiter
//< returns an array of strings
//
// (This code originally appears in string.js)
//
bif.ltd.splitCSV = function( strLine, sep )
{
// unescape linefeeds, "\n"
var unescapedLF = strLine.replace( /\\n/g, '\x0A' );
// Source: http://www.greywyvern.com/?post=258
for (var foo = unescapedLF.split(sep = sep || ","), x = foo.length - 1, tl; x >= 0; x--)
{
if (foo[x].replace(/"\s+$/, '"').charAt(foo[x].length - 1) == '"')
{
if ((tl = foo[x].replace(/^\s+"/, '"')).length > 1 && tl.charAt(0) == '"')
{
foo[x] = foo[x].replace(/^\s*"|"\s*$/g, '').replace(/""/g, '"');
}
else if (x)
{
foo.splice(x - 1, 2, [foo[x - 1], foo[x]].join(sep));
}
else
foo = foo.shift().split(sep).concat(foo);
}
else foo[x].replace(/""/g, '"');
}
return foo;
};
//=============================================================================
// bif.ltdobj
//=============================================================================
//-----------------------------------------------
// Constructor
bif.ltdobj = function()
{
this.httpStatus = null; // the XMLHttpRequest status code
this.httpStatusText = null; // the XMLHttpRequest status text
this.errorCode = null; // the error code returned by the remote service, where zero is success, and not zero is failure.
this.errorMessage = null; // the error message or other status information returned by the service when the errorCode is not equal to zero.
this.columnCount = 0; // the number of columns of data in the payload
this.columnNames = null; // a classic array, zero-based, containing the names of the columns of data in the payload
this.columnNameLookup = null; // an associative array, for reverse lookup of the column index that holds the given column of data
this.rowCount = 0; // the number of rows of data in the payload
this.data = null; // a two dimensional array, in row-column order, holding the payload. Each cell is normalized by the response object by stripping any enclosing double quotes and unescaping and escaped double quotes.
};
//-------------------------------------
// The dumpText function prints the object using carriage returns.
// This is suitable for use in an alert call.
bif.ltdobj.prototype.dumpText = function()
{
var s;
s = 'httpStatus: ' + this.httpStatus + '\n';
s += 'httpStatusText: ' + this.httpStatusText + '\n';
s += 'errorCode: ' + this.errorCode + '\n';
s += 'errorMessage: ' + this.errorMessage + '\n';
s += 'columnCount: ' + this.columnCount + '\n';
s += 'rowCount: ' + this.rowCount + '\n';
for ( var i = 0; i < this.columnCount; i++ )
s += 'columnNames[' + i + ']: ' + this.columnNames[i] + '\n';
if ( this.data )
{
var itemCount = this.data.length;
for ( var i = 0; i < itemCount; i++ )
{
var columnCount = this.data[i].length;
for ( var j = 0; j < columnCount; j++ )
s += 'data[' + i + '][' + j + ']: ' + this.data[i][j] + '\n';
}
}
s += 'rawResponse: ' + this.rawResponse;
return s;
};
//-------------------------------------
// The dumpHTML function prints the object to a new window using HTML.
//
bif.ltdobj.prototype.dumpHTML = function()
{
var s;
s = 'httpStatus: ' + this.httpStatus + '
';
s += 'httpStatusText: ' + this.httpStatusText + '
';
s += 'errorCode: ' + this.errorCode + '
';
s += 'errorMessage: ' + this.errorMessage + '
';
s += 'columnCount: ' + this.columnCount + '
';
s += 'rowCount: ' + this.rowCount + '
';
for ( var i = 0; i < this.columnCount; i++ )
s += 'columnNames[' + i + ']: ' + this.columnNames[i] + '
';
if ( this.data )
{
var itemCount = this.data.length;
for ( var i = 0; i < itemCount; i++ )
{
var columnCount = this.data[i].length;
for ( var j = 0; j < columnCount; j++ )
s += 'data[' + i + '][' + j + ']: ' + this.data[i][j] + '
';
}
}
s += 'rawResponse: ' + this.rawResponse;
return s;
// w = window.open ( '', 'mywindow' );
// w.document.write( s );
// w.document.close();
};