/*
License: GNU Lesser General Public License (http://www.gnu.org/licenses/lgpl.html)
Copyright (C) 2005 tagnetic.org. Please do not remove this copyright/license comment.
*/
//*************************************************************************
// Start Dsr <view:if><view:test>useDsr</view:test>
//*************************************************************************
/**
 * The Dsr class allows you to fetch data from a server using a SCRIPT tag.
 * Notes:
 * - Only HTTP GET requests to the server are possible.
 * - Be aware of the limitations on the size of a GET request. It seems like 1KB is
 *   a safe bet.
 * - The data from the server must be returned as script. HTML/XML should not be the result.
 * - There is no inherent, cross-browser way to know when the data has been loaded. More
 *   information below.
 *   
 * How to know when the data has been loaded
 * =========================================
 * 1) Have the server call a javascript method in your page to tell it when it is loaded.
 *    This call should be made at the end of the response from the server. This is the
 *    preferred solution, if you own the data source and the UI framework.
 * 2) Use a timer to periodically check for a data member that is part of the response.
 *    The Dsr class below allows you to specify a test to use as part of a timer
 *    test. This solution can be used when you don't own the data source.
 *
 * Credits:
 * ========
 * The main mechanism of attaching the script to the DOM was inspired by:
 * http://cssing.blogspot.com/2004/08/including-script-files-from-script.html
 */

Dsr = 
  {
  //*******************************************************************    
  /**
   * Adds a script tag to the page, but waits for the included script to
   * call back a specified handler (window.onscriptload, defined in this file)
   * to indicate the script has finished loading.
   * This method also supports segmenting long URLs into a multipart script request.
   * 
   * Multipart script requests mean that Dsr will segment the
   * server requests into multiple requests if the URL is bigger than about
   * 1 KB of data. Multipart requests require the server to call window.onscriptload with a status of
   * 100 in order for this library to work. 
   * 
   * This method will take care of determining if an URL will be segmented and add the appropriate URL
   * parameters for you. However, you will need to know about the parameters if you are a server 
   * developer that is receiving such requests.
   * 
   * Please see the Dynamic Server Request API at:
   * 
   * http://tagneto.org/how/reference/js/DynamicScriptRequest.html
   * 
   * NOTE: Are there charset issues between origin page and server data feed?
   * NOTE: This method does account for fragment identifiers (#identifier on an URL).
   *       Don't use it if you have fragment identifiers. If there is a great enough
   *       need for fragment identifier support, then it might be added later.
   * 
   * FUNCTION PARAMETERS:
   * 
   * @param {String} url: The URL for the script source.
   * 
   * @param {Object} listener: Optional, but recommended.
   * An object that contains function callbacks for the different events:
   * onLoad(), onError(status, statusText, response) and onTimeout(). A timeout in number of
   * seconds can be specified too. Example listener object:
   * 
   * var listener = { 
   *                onLoad: function() { alert('loaded'); },
   *                onError: function(status, statusText, response) { alert('status: ' + status + ', error: ' + response); },
   *                onTimeout: function() { alert('timeout'); },
   *                timeout: 30
   *                };
   * 
   * @param {String} apiId: Optional.
   * The server URL's API ID, if it defines one. This only needs to be set if the server does
   * use one of the following for the onscriptload event.id:
   * - the _dsrid parameter that Dsr.send adds to the URL
   * - The URL that is the first parameter of this function
   * - The URL that is actually used in the SCRIPT SRC attribute
   * or if the API for the data service specifically mentions an API ID.
   * 
   * @param {String} constantParams: Optional.
   * URL parameters that should always be sent up. Particularly useful if
   * the URL is split across multiple SCRIPT tags. This allows you to maintain
   * your own state/context for the request, if you don't want to rely on the basic
   * multipart URL API parameters. Note that the server can override these values.
   * Format of the data should be the normal URL-encoded form:
   * 
   * name=urlEncodedValue&name=urlEncodedValue
   */
  send: function(url, listener, apiId, constantParams)
    {
    //Make sure we have a good listener
    if (!listener || !listener.onLoad)
      {
      var message = 'Dsr.send requires an onLoad method on the listener';
      if (listener && listener.onError)
        listener.onError(message);
      else
        throw message;
      return;
      }

    //Set up the state for this request.
    var state = Dsr._createState(listener, apiId);

    state.constantParams = (constantParams == null ? '' : constantParams);
    
    //Start the timer, if the caller wants to have a timeout.
    if (listener.onTimeout)
      state.intervalId = setInterval("Dsr._checkLoad('" + state.id + "')", 200);
      
    //Start constructing URL parameters that will be added.
    state.idParam = '_dsrid=' + state.id;

    //Get total length URL, if we were to do it as one URL.
    //8 is for some padding, extra & separators.
    var urlLength = url.length + state.constantParams.length + state.idParam.length + 8;
     
    if (urlLength > Dsr._kMaxUrlLength)
      {
      //Break off the domain/path of the URL.
      var end = url.indexOf('?');
      if (end == -1)
        {
        //Error. The URL domain and path are too long. We can't
        //segment that, so return an error.
        Dsr._finish(state, 'onError', {status: 500, statusText: 'url.tooBig'});
        return;
        }
      else
        {
        state.href = url.substring(0, end);
        state.query = url.substring(end + 1, url.length); //Strip off the ? with the + 1.
        }
      
        //Start the multiple requests.
        Dsr._multiAttach(state, 1);
      }
    else
      {
      //Send one URL. Does the URL already have query parameters?
      var paramPrefix = (url.indexOf('?') == -1 ? '?' : '&');
      var finalUrl = url + paramPrefix + state.idParam;
      if (state.constantParams)
        finalUrl += '&' + state.constantParams;
      
      //Save the original URL for later, in case that is the id the server
      //uses for the event ID.
      state.url = url;
      
      Dsr._attach(state.id, finalUrl);
      }
    },

  //*******************************************************************
  /**
   * Adds a script src to a page, and watches for a string of javascript
   * that when evaled to something that is not undefined indicates that the
   * script has finished loading.
   * 
   * @param {String} url: The URL for the script source.
   * 
   * @param {String} checkString: Optional, but recommended.
   * String of javascript that if evals to to true means the script 
   * src has loaded. Required if listener is specified.
   * 
   * Don't specify this parameter if you are relying on server callbacks
   * to indicate the script has finished loading (and you don't want to use
   * send(), which requires certain URL API).
   *
   * @param {Object} listener: Optional, but recommended.
   * An object that contains function callbacks for the different events:
   * onLoad(), onError(error) and onTimeout(). A timeout in number of
   * seconds can be specified too. Example listener object:
   * 
   * var listener = { 
   *                onLoad: function() { alert('loaded'); },
   *                onError: function(status, statusText, response) { alert('status: ' + status + ', error: ' + response); },
   *                onTimeout: function() { alert('timeout'); },
   *                timeout: 30
   *                };
   */
  sendAndPoll: function(url, checkString, listener)
    {
    var state = Dsr._createState(listener);
    state.check = checkString;
    
    //Allow for not setting a checkString. Assuming the server will know what to call
    //when the script finishes loading.
    if (checkString)
      state.intervalId = setInterval("Dsr._checkLoad('" + state.id + "')", 200);

    Dsr._attach(state.id, url);
    },

  //*******************************************************************
  /**
   * Clears any script tags from the DOM that may have been added by Dsr.
   * Be careful though, by removing them from the script, you may invalidate some
   * script objects that were defined by the js file that was pulled in as the
   * src of the script tag. Test carefully if you decide to call this method.
   * 
   * In MSIE 6, if you removed the script element while part of the script is still executing,
   * the browser will crash.
   */
  clear: function()
    {
    var scripts = document.getElementsByTagName('script');
    for (var i = 0; scripts && i < scripts.length; i++)
      {
      var scriptTag = scripts[i];
      if (scriptTag.getAttribute('class') == 'Dsr')
        {
        var parent = scriptTag.parentNode;
        parent.removeChild(scriptTag);
        i--; //Set the index back one since we removed an item.
        }
      }
    },
    
  //*******************************************************************
  //Private properties/methods.
  _kMaxUrlLength: 1000, //Used to calculate if script request should be multipart.
    
  //*******************************************************************
  _createState: function(listener, apiId)
    {
    var id = apiId ? apiId : 'id' + this._counter++;
    Dsr._state[id] = {
                               id: id,
                               listener: listener,
                               startTime: (new Date()).getTime()
                               };

    return Dsr._state[id];
    },
    
  //*******************************************************************
  _attach: function(id, url)
    {
    //Attach the script to the DOM.
    var element = document.createElement('script');
    element.type = 'text/javascript';
    element.src = url;
    element.id = id;
    element.setAttribute('class', 'Dsr');
    document.getElementsByTagName('head')[0].appendChild(element);
    },

  //*******************************************************************
  _multiAttach: function(state, part)
    {
    //Check to make sure we still have a query to send up. This is mostly
    //a protection from a goof on the server side when it sends a part OK
    //response instead of a final response.
    if (state.query == null)
      {
      Dsr._finish(state, 'onError', {status: 500, statusText: 'query.null'});
      return;
      }
    
    if (!state.constantParams)
      state.constantParams = '';

    //How much of the query can we take?
    //Add a padding constant to account for _part and a couple extra amperstands.
    //Also add space for id since we'll need it now.
    var queryMax = Dsr._kMaxUrlLength - state.idParam.length
                 - state.constantParams.length - state.href.length - 16;
      
    //Figure out if this is the last part.
    var isDone = state.query.length < queryMax;

    //Break up the query string if necessary.
    var currentQuery;
    if (isDone)
      {
      currentQuery = state.query;
      state.query = null;
      }
    else
      {
      //Find the & or = nearest the max url length.
      var ampEnd = state.query.lastIndexOf('&', queryMax - 1);
      var eqEnd = state.query.lastIndexOf('=', queryMax - 1);
      
      //See if & is closer, or if = is right at the edge,
      //which means we should put it on the next URL.
      if (ampEnd > eqEnd || eqEnd == queryMax - 1)
        {
        //& is nearer the end. So just chop off from there.
        currentQuery = state.query.substring(0, ampEnd);
        state.query = state.query.substring(ampEnd + 1, state.query.length) //strip off amperstand with the + 1.
        }
      else
        {
        //= is nearer the end. Take the max amount possible. 
        currentQuery = state.query.substring(0, queryMax);
       
        //Find the last query name in the currentQuery so we can prepend it to
        //ampEnd could be -1 (not there), so account for that.
        var queryName = currentQuery.substring((ampEnd == -1 ? 0 : ampEnd + 1), eqEnd);
        state.query = queryName + '=' + state.query.substring(queryMax, state.query.length);
        }
      }
    
    //Now send a part of the script
    var url = state.href + '?' + currentQuery + '&' + state.idParam;
    
    if (state.constantParams)
      url += '&' + state.constantParams;
    if (!isDone)
      url += '&_part=' + part;

    Dsr._attach(state.id + '_' + part, url);
    },
  
  //*******************************************************************
  _checkLoad: function(id)
    {
    var state = Dsr._state[id];
    var listener = state.listener;
    try
      {
      if (state.check && eval("typeof(" + state.check + ") != 'undefined'"))
        this._finish(state, 'onLoad');
      else if (listener && listener.onTimeout)
        {
        if (state.startTime + (listener.timeout * 1000) < (new Date()).getTime())
          this._finish(state, 'onTimeout');
        }
      }
    catch(e)
      {
      this._finish(state, 'onError', {status: 500, response: e});
      }
    },
    
  //*******************************************************************
  _finish: function(state, callback, event)
    {
    clearInterval(state.intervalId);
    
    //Only attempt to dispatch result if the listener is defined.
    //Ignore 'onPartOk' because that is an internal callback for Dsr.
    if (callback != 'onPartOk' && (!state.listener || !state.listener[callback]))
      {
      if (callback == 'onError')
        throw event;
      }
    else
      {
      switch (callback)
        {
        case 'onLoad':
          var response = event ? event.response : null;
          state.listener[callback](response);
          break;
        case 'onPartOk':
          var part = parseInt(event.response.part, 10) + 1;
          //Update the constant params, if any.
          if (event.response.constantParams)
            state.constantParams = event.response.constantParams;
          Dsr._multiAttach(state, part);
          break;
        case 'onError':
          state.listener[callback](event.status, event.statusText, event.response);
          break;
        default:
          state.listener[callback](event);
        }
      }
    },

  //*******************************************************************
  _counter: 1
  };

//Private variable used by Dsr to hold state.
Dsr._state = new Object();

//******************************************************************
//Define callback handler.
window.onscriptload = function(event)
  {
  var state = null;
  //Find the matching state object for event ID.
  if (Dsr._state[event.id])
    state = Dsr._state[event.id];
  else
    {
    //The ID did not match directly to an entry in the state list.
    //Try searching the state objects for a matching original URL.
    var tempState;
    for (var param in Dsr._state)
      {
      tempState = Dsr._state[param];
      if (tempState.url && tempState.url == event.id)
        {
        state = tempState;
        break;
        }
      }
    
    //If no matching original URL is found, then use the URL that was actually used
    //in the SCRIPT SRC attribute.
    if (state == null)
      {
      var scripts = document.getElementsByTagName('script');
      for (var i = 0; scripts && i < scripts.length; i++)
        {
        var scriptTag = scripts[i];
        if (scriptTag.getAttribute('class') == 'Dsr' && scriptTag.src == event.id)
          {
          state = Dsr._state[scriptTag.id];
          break;
          }
        }
      }
    
    //If state is still null, then throw an error.
    if (state == null)
      throw 'No matching state for onscriptload event.id: ' + event.id;
    }
  
 var callbackName = 'onError';
 switch (event.status)
    {
    case 100:
      //A part of a multipart request.
      callbackName = 'onPartOk';
      break;
    case 200:
      //Successful reponse.
      callbackName = 'onLoad';
      break;
    }
  
  Dsr._finish(state, callbackName, event);
  };

//*************************************************************************
//End Dsr </view:if>
//*************************************************************************

