/*****************************************************************************
 * AjaxRequest Javascript Object
 *
 * Author: Rex Staples
 * Date:   27-January-2006
 *
 * Copyright 2006 Interwoven, Inc. All Rights Reserved.
 *
 * Interwoven trademarks, service marks, logos, and taglines are
 * exclusively owned by Interwoven, Inc.
 *
 *
 * OVERVIEW
 *
 * The AjaxRequest object encapsulates an XMLHttpRequest delivering a simple
 * framework for dispatching both synchronous and asynchronous requests to
 * the server. The various complexities of configuring a request handler or
 * monitoring the request to completion are all handled behind the scenes.
 * 
 * The object is designed to simplify handling of request parameters which
 * may either be added one at a time, or by digesting an entire form. In both
 * of these cases all url-encoding is done automatically by the object.
 *
 * There is support for specialized data formats (xml, csv, home-grown) by
 * exposing the request body. If you go this route be sure to read the notes
 * in the comments of the setRequestBody definition further down the page.
 *
 * At its simplest, the AjaxRequest object needs nothing more than a url and
 * a response handler function(1). The request will be sent to the specified
 * url as a POST, and the handler function will be invoked when the response
 * completes.
 *
 * The handler function is invoked with three parameters. The first is a W3C
 * DOM Document object response; the second is a string representation of
 * the response data; and the third is the xml http request object which ori-
 * ginated the request. If the response data is not well-formed XML, or if the
 * Content-Type response header from the server is not "text/xml" then the
 * Document object will be empty(2). The response text always contains the
 * data from the response -- perfect for applications where simple data is 
 * sufficient and well-formed xml is overkill. The xml http request object
 * useful to query response headers from the request using either function:
 * request.getAllResponseHeaders() or request.getResponseHeader(headerName).
 *
 * If the request is set as synchronous, the call to submit the request will
 * block until the response returns.  For synchronous calls there is no need
 * to setup a handler function since the reponse xml and response text are
 * available as soon as the request completes.
 *
 * INTERFACE
 *
 * Each function listed below along with its parameters is documented further
 * down the page with its implementation.  This is a fluent interface, so all
 * the Configuration functions below return an object reference to promote
 * method chaining.  Example: req.setMethod('GET').setUrl(url).submit();
 *
 * Creation
 *
 *       AjaxRequest(url, handlerFunction);
 *
 * Configuration
 *
 *       setUrl(url)
 *       setMethod(method)
 *       setHandler(handlerFunction)
 *
 *       setAsynchronous(true|false) 
 *       setContentType(contentType)
 *       setRequestBody(data)
 *
 *       setParameter(key, value)
 *       setParameters(form)
 *       clearParameters()
 *
 * Execution
 *
 *       submit()
 *
 *
 * USAGE
 *
 * Example of instantiating an AjaxRequest where the url and handler are
 * passed in the constructor, a single request parameter is set, and the
 * request is submitted.  The handler function extracts a value from the
 * first node in the response Document.
 *    
 *       function handleShippingQuery(xmlDoc, text, xmlHttpRequest)
 *       {
 *         setShippingCost(xmlDoc.documentElement.firstChild.nodeValue);
 *       }
 *    
 *       var req = new AjaxRequest('shipCalc.jsp', handleShippingQuery);
 *       req.setParameter('zipcode', '94086');
 *       req.submit();
 *
 * NOTES
 *
 * (1) If the server response is unimportant the handler function need not be
 *     configured. The response is monitored through completion for errors,
 *     but no handler is invoked.
 *
 * (2) Firefox creates an error document with a single <parsererror/> node.
 *
 *****************************************************************************/

/**
 * CONSTANTS
 *
 * These constants are defined without the prototype keyword so they can be
 * referenced without requiring an object instantiation (mainly for the ready
 * state change handler). They are globals scoped to window.AjaxRequest.
 *
 */

/**
 * request method
 */
AjaxRequest.METHOD_POST = 'POST';

/**
 * request method
 */
AjaxRequest.METHOD_GET  = 'GET';

/**
 * request method
 */
AjaxRequest.METHOD_PUT  = 'PUT';

/**
 * request ready state
 */
AjaxRequest.READY_STATE_UNINITIALIZED = 0;

/**
 * request ready state
 */
AjaxRequest.READY_STATE_LOADING       = 1;

/**
 * request ready state
 */
AjaxRequest.READY_STATE_LOADED        = 2;

/**
 * request ready state
 */
AjaxRequest.READY_STATE_INTERACTIVE   = 3;

/**
 * request ready state
 */
AjaxRequest.READY_STATE_COMPLETE      = 4;

/**
 * request status
 */
AjaxRequest.STATUS_OK        = 200;

/**
 * request status
 */
AjaxRequest.STATUS_NOT_FOUND = 404;

/**
 * content type request header
 */
AjaxRequest.CONTENT_TYPE = 'Content-Type';

/**
 * content type for form url-encoded key/value data
 */
AjaxRequest.CONTENT_TYPE_URL_ENCODED = 'application/x-www-form-urlencoded';

/**
 * ActiveX application/classname for newer IE browsers
 */
AjaxRequest.ACTIVEX_MSXML2_XMLHTTP = "Msxml2.XMLHTTP";

/**
 * ActiveX application/classname for legacy IE browser
 */
AjaxRequest.ACTIVEX_LEGACY_XMLHTTP = "Microsoft.XMLHTTP";

/**
 * form field type
 */
AjaxRequest.FORM_FIELD_BUTTON = 'button';

/**
 * form field type
 */
AjaxRequest.FORM_FIELD_SUBMIT = 'submit';

/**
 * form field type
 */
AjaxRequest.FORM_FIELD_TEXT = 'text';

/**
 * form field type
 */
AjaxRequest.FORM_FIELD_HIDDEN = 'hidden';

/**
 * form field type
 */
AjaxRequest.FORM_FIELD_TEXTAREA = 'textarea';

/**
 * form field type
 */
AjaxRequest.FORM_FIELD_RADIO = 'radio';

/**
 * form field type
 */
AjaxRequest.FORM_FIELD_CHECKBOX = 'checkbox';

/**
 * form field type
 */
AjaxRequest.FORM_FIELD_SELECT_ONE = 'select-one';

/**
 * form field type
 */
AjaxRequest.FORM_FIELD_SELECT_MULTIPLE = 'select-multiple';

/**
 * query string delimiter
 */
AjaxRequest.DELIM_QS = '?';

/**
 * request parameter delimiter
 */
AjaxRequest.DELIM_PARAM = '&';

/**
 * parameter key/value delimiter
 */
AjaxRequest.DELIM_KEY_VALUE = '=';

/**
 * error message
 */
AjaxRequest.ERROR_CREATE_REQUEST = 'Unable to create ActiveX XMLHttpRequest.'

/**
 * error message (tokens replaced at run-time)
 */
AjaxRequest.ERROR_REQUEST_STATE = 'Error in XMLHttpRequest [$code/$text]';


/**
 * Constructor creates an encapsulated XMLHttpRequest object. Both parameters
 * to the constructor are optional, and may be configured later.
 *
 * @param url             the url to which the request is sent
 * @param handlerFunction the function to invoke and pass the response
 *                        xml dom and the response text
 */
function AjaxRequest(url, handlerFunction)
{
  // Object properties

  this.mUrl         = url;
  this.mHandler     = handlerFunction;
//changed by SR 8th Sept 09
//  this.mMethod      = AjaxRequest.METHOD_POST;
  this.mMethod      = AjaxRequest.METHOD_GET;

  this.mContentType = null;
  this.mRequestBody = null;
  this.mDebug       = false;

  this.mAsynchronous = true;

  this.clearParameters();
}


/**
 * Sets whether to enable debug mode. When enabled, after the request is made
 * the responseText will be printed in a new browser window.
 *
 * @param isDebugMode whether to enable debug mode
 * @return a self-reference for method chaining
 */
AjaxRequest.prototype.setDebug = function(isDebugMode)
{
  this.mDebug = isDebugMode;
  return this;
}


/**
 * Sets the url to which the request is sent.
 *
 * @param url the url to which the request is sent
 * @return a self-reference for method chaining
 */
AjaxRequest.prototype.setUrl = function(url)
{
  this.mUrl = url;
  return this;
}


/**
 * Sets the ready state change handler for the XMLHttpRequest. Once the 
 * request completes, the handler is invoked and passed the response data.
 *
 * @param handlerFunction the function to invoke and pass the response
 *                        xml dom and the response text
 * @return a self-reference for method chaining
 */
AjaxRequest.prototype.setHandler = function(handlerFunction)
{
  this.mHandler = handlerFunction;
  return this;
}


/**
 * Sets the type of server request to send. The default is POST.
 *
 * @param method the type of server request
 * @return a self-reference for method chaining
 */
AjaxRequest.prototype.setMethod = function(method)
{
  this.mMethod = method;
  return this;
}


/**
 * Sets the content type of the data in the request body. The default is
 * application/x-www-form-urlencoded.
 *
 * @param contentType the type of data to send in the request body
 * @return a self-reference for method chaining
 */
AjaxRequest.prototype.setContentType = function(contentType)
{
  this.mContentType = contentType;
  return this;
}


/**
 * Sets the data to send in the request body. This function is only needed
 * to send non-url-encoded data in the request (for example, an xml block).
 *
 * If you are sending normal url-encoded form data, use the setParameter
 * function to set key/value pairs or use the setParameters function to
 * digest an entire html form.
 *
 * Caveat: If the request method is POST and the request body has been set,
 * then any parameters that have been set will appear in the query string 
 * of the url.
 *
 * Note: A default content type (url form encoded) is used in the request if
 * the request body is not set.  If the body is set, then only an explicit
 * call to setContentType will cause a content type header to be sent.
 *
 * @param data the data to send in the request body
 * @return a self-reference for method chaining
 */
AjaxRequest.prototype.setRequestBody = function(data)
{
  this.mRequestBody = data;
  return this;
}

/**
 * Sets whether the request should be asynchronous. Request are asynchronous
 * by default.
 *
 * @param isAsynchronous true if the request should be asynchronous
 * @return a self-reference for method chaining
 */
AjaxRequest.prototype.setAsynchronous = function(isAsynchronous)
{
  this.mAsynchronous = isAsynchronous;
  return this;
}


/**
 * Clears the request parameter list.
 *
 * @return a self-reference for method chaining
 */
AjaxRequest.prototype.clearParameters = function()
{
  this.mParams = new Array();
  return this;
}


/**
 * Sets a request parameter. The key and value are automatically url-encoded.
 *
 * @param key   the request parameter key
 * @param value the request parameter value
 * @return a self-reference for method chaining
 */
AjaxRequest.prototype.setParameter = function(key, value)
{
  this.mParams[this.mParams.length] = encodeURIComponent(key)
      + AjaxRequest.DELIM_KEY_VALUE + encodeURIComponent(value);
  return this;
}


/**
 * Builds the request parameters from the elements in the passed form.
 * Button, submit, and disabled form fields are automatically skipped.
 *
 * @param form the form to build the request parameters from
 * @return a self-reference for method chaining
 */
AjaxRequest.prototype.setParameters = function(form)
{
  var fields = form.elements;

  for (var i=0; i < fields.length; i++)
  {
    if (fields[i].disabled) continue;

    var name = fields[i].name;

    switch(fields[i].type)
    {
      case AjaxRequest.FORM_FIELD_TEXT:
      case AjaxRequest.FORM_FIELD_HIDDEN:
      case AjaxRequest.FORM_FIELD_TEXTAREA:
        this.setParameter(name, fields[i].value);
        break;
      
      case AjaxRequest.FORM_FIELD_RADIO:
      case AjaxRequest.FORM_FIELD_CHECKBOX:
        if (fields[i].checked)
        {
          this.setParameter(name, fields[i].value);
        }
        break;
      
      case AjaxRequest.FORM_FIELD_SELECT_ONE:
      case AjaxRequest.FORM_FIELD_SELECT_MULTIPLE:
        var options = fields[i].options;

        for (var j=0; j < options.length; j++)
        {
          if (options[j].selected)
          {
            this.setParameter(name, options[j].value);
          }
        }
        break;
    }
  }
  return this;
}


/**
 * Submits the request to the url using the data configured in the object.
 */
AjaxRequest.prototype.submit = function()
{
  var request = null;

  // INSTANTIATE THE XMLHttpRequest

  if (window.XMLHttpRequest)
  {
    request = new XMLHttpRequest();
  }
  else if (window.ActiveXObject) // MSIE 6 and older
  {
    try
    {
      request = new ActiveXObject(AjaxRequest.ACTIVEX_MSXML2_XMLHTTP);
    }
    catch (errorActiveX)
    {
      try
      {
        // older versions of MSIE
        request = new ActiveXObject(AjaxRequest.ACTIVEX_LEGACY_XMLHTTP);
      }
      catch (errorLegacyActiveX)
      {
        alert(AjaxRequest.ERROR_CREATE_REQUEST);
      }
    }
  }

  // SET THE READY STATE CHANGE HANDLER

  // cannot reference object member from the anonymous function
  var handlerFunction  = this.mHandler;
  var showResponseText = this.mDebug;

  var readyStateChangeFunction = function() {
    if (AjaxRequest.READY_STATE_COMPLETE == request.readyState)
    {
      if (AjaxRequest.STATUS_OK == request.status)
      {
        if (showResponseText)
        {
          window.open().document.write('<xmp>' + request.responseText + '</xmp>');
        }

        if (handlerFunction) // only invoke if defined
        {
          handlerFunction(request.responseXML, request.responseText, request);
        }
      }
      else
      {
        alert(AjaxRequest.ERROR_REQUEST_STATE
          .replace(/\$code/, request.status)
          .replace(/\$text/, request.statusText));
      }
    }
  }

  // PREPARE THE REQUEST PROPERTIES AND ANY DEFAULTS

  var url    = this.mUrl;
  var body   = this.mRequestBody;
  var type   = this.mContentType;
  var params = this.mParams.join(AjaxRequest.DELIM_PARAM);

  // if no parameters are set, then there is no need to dispatch them

  if (params.length > 0)
  {
    var paramDelim = (0 > url.indexOf("?"))   // if a url already has a query
                   ? AjaxRequest.DELIM_QS     // string delimiter (?), use an
                   : AjaxRequest.DELIM_PARAM  // ampersand (&) instead

    if (body != null)      // User-set request body, all params go in
    {                      // the query string for both POST and GET.
      url += paramDelim + params;
    }
    else                   // no request body
    {
      if (this.mMethod == AjaxRequest.METHOD_GET)
      {
        url += paramDelim + params;
      }
      else
      {
        body = params;

        if (type == null)  // assign default if no type is set
        {
          type = AjaxRequest.CONTENT_TYPE_URL_ENCODED;
        }
      }
    }
  }

  // SUBMIT THE REQUEST

  if(this.mAsynchronous)
  {
    request.onreadystatechange = readyStateChangeFunction;
  }

  request.open(this.mMethod, url, this.mAsynchronous);

  if (type != null)
  {
    request.setRequestHeader(AjaxRequest.CONTENT_TYPE, type);
  }

  request.send(body);

  return request;
}

