/**
 ******************************************************************************
 * Copyright © 2006-2008 EMC Corporation.  All Rights Reserved.
 ******************************************************************************
 *
 * Project        WDK
 * File           inlinerequest.js
 * Description    WDK Inline Request Infrastructure Library
 * Created on     29th November 2006
 * Tab width      3
 *
 ******************************************************************************
 *
 * PVCS Maintained Data
 *
 * Revision       $Revision: 10$
 * Modified on    $Date: 6/11/2008 5:03:31 AM$
 *
 * Log at EOF
 *
 ******************************************************************************
 */

// Ensure Trace_INLINEREQUEST is defined
if (typeof(Trace_INLINEREQUEST) == "undefined")
{
   Trace_INLINEREQUEST = false;
}

if (typeof InlineRequestType == "undefined")
{ // support portal

/********** InlineRequestType ************************************************/

/**
 * InlineRequestType
 *
 * An enumeration of the supported inline request types.
 *
 * NOTE: If you do not want any automated processing of the response, you
 *       should use a request type of PLAIN.
 */
var InlineRequestType = new function()
{
   this.PLAIN = 1;
   this.HTML = 2;
   this.XML = 3;
   this.JSON = 4;
   this.JAVASCRIPT = 5;

   /**
    * Determine whether or not the supplied value is a legal value of this
    * enumeration. Clients should use this function in preference to checking
    * the enumeration constants explicitly.
    *
    * @param value The value to be checked.
    *
    * @return true if the value is part of this enumeration; false otherwise.
    */
   this.isValid = function(value)
   {
      return (value >= this.PLAIN && value <= this.JAVASCRIPT);
   }
};

/********** InlineRequestEngine **********************************************/

/**
 * InlineRequestEngine
 *
 * Singleton engine through which inline requests are created and managed.
 */
var InlineRequestEngine = new function()
{
   // Use HTTP status code 404 to indicate general XHR failure
   this.GENERAL_FAILURE = 404;

   // Use HTTP status code 408 to indicate a timeout
   this.REQUEST_TIMEOUT = 408;

   /**
    * Creates and returns a new instance of the InlineRequestPreferences
    * class, and initializes it based on the supplied request type.
    *
    * @param type The type of the request, expressed as a value from the
    *             InlineRequestType enumeration.
    *
    * @return An initialized InlineRequestPreferences instance.
    */
   this.getPreferences = function(type)
   {
      if (arguments.length < 1)
      {
         type = InlineRequestType.PLAIN;
      }
      return new InlineRequestPreferences(type);
   };

   /**
    * Sends the inline request specified by the supplied preferences. If the
    * request is synchronous, the response will have been processed by the
    * time this function returns.
    *
    * The unique identifier returned from this function may be used to
    * subsequently refer to this request. However, this is only meaningful
    * for asynchronous requests, since synchronous requests will have already
    * completed.
    *
    * If an error occurs prior to the request being made (e.g. if the supplied
    * preferences are invalid or inconsistent), an exception will be thrown
    * from this function. If an error occurs after the request has been made,
    * the error callback will be invoked, if one was provided.
    *
    * @param prefs The preferences for the request to be made, expressed as an
    *              instance of the InlineRequestPreferences class.
    *
    * @return The unique identifier for this request.
    *
    * @throws Error if the supplied preferences are invalid or inconsistent.
    */
   this.sendRequest = function(prefs)
   {
      // Validate the incoming request preferences
      if (!validatePreferences(prefs))
      {
         if (Trace_INLINEREQUEST)
         {
            Trace_println("Preferences invalid");
            Trace_dump(prefs);
         }
         return null;
      }

      var xhr = getXHR();
      var xhrAborted = false;

      // Prepare the request
      var request = new InlineRequest(++nextRequestId, prefs, xhr);
      if (request.isAsync())
      {
         watchAsyncRequest(request);
      }

      // Prepare the XHR instance
      var method = request.getMethod();
      var url;
      if (method == "POST")
      {
         url = request.getBaseUrl();
      }
      else
      {
         url = request.getUrl();
      }

      xhr.open(method, url, request.isAsync());
      var headers = request.getHeaders();
      for (var header in headers)
      {
         xhr.setRequestHeader(header, headers[header]);
      }
      if (method == "POST" && !headers["Content-Type"])
      {
         xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
      }

      // Make the request
      try
      {
         if (Trace_INLINEREQUEST)
         {
            Trace_println("Inline request preferences:")
            Trace_dump(prefs, /^(set|add)/);
            Trace_println("Inline request parameters:")
            Trace_dump(prefs.parameters.getMap());
         }
         xhr.send(request.getContent());
      }
      catch (e)
      {
         xhr.abort();
         xhrAborted = true;
         if (Trace_INLINEREQUEST)
         {
            Trace_println("XHR exception: " + e.description);
         }
         request.getErrorCallback()(this.GENERAL_FAILURE, prefs);
         request.setXhr(null);
      }

      if (!xhrAborted && !request.isAsync())
      {
         processResponse(request);
      }

      return request.getId();
   };

   /**
    * Stops the request with the supplied identifier, if it is still in
    * progress. If the request has already completed, this function does
    * nothing.
    *
    * @param id The identifier of the request to be stopped. This is the
    *           same value that was returned from sendRequest when the
    *           request was initiated.
    *
    * @return true if the request was successfully stopped; false if the
    *         request was not found or could not be stopped.
    */
   this.stopRequest = function(id)
   {
      var stopped = false;
      var request = findRequest(id);

      if (request)
      {
         request.getXhr().abort();
         stopped = true;
      }

      return stopped;
   };

   /**
    * Evaluates any script elements in the DOM node provided, making the code
    * available for use thereafter. This function is normally invoked as part
    * of automatic HTML response processing, and is not usually required by
    * clients.
    *
    * @param node The node for which scripts are to be evaluated.
    */
   this.evalHtmlScripts = function(node)
   {
      var head = document.getElementsByTagName("head")[0];
      var tags = node.getElementsByTagName("script");
      for (var i = 0; i < tags.length; i++)
      {
         var tag = tags[i];
         if (tag.src)
         {
            var script = document.createElement('script');
            script.type = 'text/javascript';
            script.src = tag.src;
            head.appendChild(script);
         }
         else if (tag.innerHTML)
         {
            if (g_clientInfo.isBrowser(ClientInfo.MSIE))
            {
               /*
                * IE's eval function is broken, and does not follow the spec.
                * Specifically, eval always evaluates in the context of the
                * function from which it is called, instead of in the context
                * of the object upon which it is called. This means that it is
                * impossible to define functions from within an eval that will
                * persist outside of this (evalHtmlScripts) function. To work
                * around this, we hack the passed in script to explicitly add
                * any functions we find to the 'window' object. This really is
                * a disgusting hack, for many reasons, but it's the only way
                * I've been able to get this to work at all. [MFNC 2006-12-15]
                */
               try
               {
                  eval(tag.innerHTML.replace(/function ([a-zA-Z0-9_]*)\(/g,
                     "window[\"$1\"] = function("));
               }
               catch (e)
               {
                  // If the hack blew up, try to muddle on anyway
                  eval(tag.innerHTML);
               }
            }
            else
            {
               window.eval(tag.innerHTML);
            }
         }
      }
   };

   this.checkAsyncRequests = function()
   {
      var ongoingRequests = [];
      for (var i = 0; i < asyncRequests.length; i++)
      {
         var request = asyncRequests[i];
         var xhr = request.getXhr();
         var completed = false;

         if (xhr.readyState == 0)
         {
            // Done with this request
            completed = true;
         }
         else if (xhr.readyState == 4)
         {
            processResponse(request);
            completed = true;
         }
         else if (request.isTimedOut())
         {
            xhr.abort();
            if (Trace_INLINEREQUEST)
            {
               Trace_println("XHR timeout");
            }
            request.getErrorCallback()(this.REQUEST_TIMEOUT, request.getPreferences());
            completed = true;
         }

         if (completed)
         {
            // Null out XHR to avoid memory leaks
            request.setXhr(null);
         }
         else
         {
            // Request is still in progress
            ongoingRequests.push(request);
         }
      }
      asyncRequests = ongoingRequests;

      clearTimeout(asyncRequestTimer);
      asyncRequestTimer = null;

      if (asyncRequests.length > 0) {
         asyncRequestTimer = setTimeout(InlineRequestEngine.checkAsyncRequests, 10);
      }
   };

   function validatePreferences(prefs)
   {
      return ((prefs.method.toUpperCase() == "POST" || prefs.method.toUpperCase() == "GET")
         && prefs.url != null
         && InlineRequestType.isValid(prefs.type));
   }

   function getXHR()
   {
      var msVersions = ["Msxml2.XMLHTTP", "Microsoft.XMLHTTP"];
      var xhr = null;

      if (typeof window.XMLHttpRequest != 'undefined')
      {
         xhr = new XMLHttpRequest();
      }
      else
      {
         for (var i = 0; i < msVersions.length; i++)
         {
            try
            {
               xhr = new ActiveXObject(msVersions[i]);
               msVersions = [ msVersions[i] ]; // No need to try more than once
               break;
            }
            catch(e)
            {
               xhr = null;
            }
         }
      }
      return xhr;
   }

   var nextRequestId = 0;
   var asyncRequests = [];
   var asyncRequestTimer = null;

   function watchAsyncRequest(request)
   {
      request.setStartTime((new Date()).getTime());
      asyncRequests.push(request);

      if (!asyncRequestTimer){
         asyncRequestTimer = setTimeout(InlineRequestEngine.checkAsyncRequests, 10);
      }
   }

   function findRequest(id)
   {
      for (var i = 0; i < asyncRequests.length; i++)
      {
         var request = asyncRequests[i];
         if (request && request.getId() == id)
         {
            return request;
         }
      }
      return null;
   }

   function processResponse(request)
   {
      var xhr = request.getXhr();
      var status = xhr.status;

      // Check HTTP status code for failed requests
      if (!((status >= 200 && status < 300)
         || status == 304
         || (status == 0 && location.protocol == "file:")))
      {
         if (Trace_INLINEREQUEST)
         {
            Trace_println("XHR request failed; status = " + status);
         }
         request.getErrorCallback()(status, request.getPreferences());
         request.setXhr(null);
         return;
      }
      
      var responseData;

      switch (request.getType())
      {
      case InlineRequestType.XML:
         responseData = xhr.responseXML;
         break;

      case InlineRequestType.JSON:
         try
         {
            responseData = eval("(" + xhr.responseText + ")");
         }
         catch(e)
         {
            responseData = false;
         }
         break;

      case InlineRequestType.JAVASCRIPT:
         try
         {
            responseData = eval(xhr.responseText);
         }
         catch(e)
         {
            responseData = false;
         }
         break;

      case InlineRequestType.HTML:
         responseData = xhr.responseText;
         var node = request.getTargetNode();
         if (node)
         {
            if ((typeof node == "string") || (node instanceof String))
            {
               node = document.getElementById(node);
            }
            node.innerHTML = responseData;
            if (request.getEvalScripts())
            {
               InlineRequestEngine.evalHtmlScripts(node);
            }
         }
         break;

      case InlineRequestType.PLAIN:
      default:
         responseData = xhr.responseText;
         break;
      }

      request.getCallback()(responseData, request.getPreferences());
   }

   /**
    * InlineRequestPreferences
    *
    * This class is used by a client to specify the parameters and constraints
    * for an inline request. A client should not create instances of this class
    * directly, but should request them from the InlineRequestEngine class, to
    * ensure that they are initialised correctly.
    */
   function InlineRequestPreferences(type)
   {
      this.type = type;

      this.method = null;
      this.url = null;
      this.parameters = new wdk.lang.MultiMap();
      this.headers = {}; // Multi-valued headers are not supported
      this.isAsync = true;
      this.timeout = 0;
      this.callback = null;
      this.errorCallback = null;
      this.htmlOptions = {};
   }

   /**
    * Set the HTTP method to use for the request. Supported values are "GET"
    * and "POST".
    *
    * @param method The HTTP method to use.
    */
   InlineRequestPreferences.prototype.setMethod = function(method)
   {
      this.method = method;
   }

   /**
    * Set the URL for which the request will be made. This URL may include a
    * query string. However, when used in a POST request, such a query string
    * will remain a query string and will not be incorporated into the body
    * of the request. Use the parameter setter methods to add parameters to
    * the request such that they will be sent appropriately based on the HTTP
    * method.
    *
    * @param url The URL for which the request will be made.
    */
   InlineRequestPreferences.prototype.setUrl = function(url)
   {
      this.url = url;
   };

   /**
    * Set the parameters to be sent with the request. The parameters must be
    * provided as a MultiMap. Note that this allows for multi-valued parameters
    * if necessary.
    *
    * @param parameters The parameters as a MultiMap.
    */
   InlineRequestPreferences.prototype.setParameters = function(parameters)
   {
      this.parameters = parameters;
   };

   /**
    * Add a set of parameters to be sent with the request. The parameters must
    * be provided as a MultiMap. Note that this allows for multi-valued
    * parameters if necessary.
    *
    * @param parameters The parameters as a MultiMap.
    */
   InlineRequestPreferences.prototype.addParameters = function(parameters)
   {
      this.parameters.addMap(parameters);
   };

   /**
    * Set an individual parameter to be sent with the request. Any existing
    * parameter with the same name will be overwritten.
    *
    * @param name The name of the parameter.
    * @param value The value of the parameter.
    */
   InlineRequestPreferences.prototype.setParameter = function(name, value)
   {
      this.parameters.set(name, value);
   };

   /**
    * Add an individual parameter to be sent with the request. If a parameter
    * with this name already exists, the new value is added to those already
    * present.
    *
    * @param name The name of the parameter.
    * @param value The value of the parameter.
    */
   InlineRequestPreferences.prototype.addParameter = function(name, value)
   {
      this.parameters.add(name, value);
   };

   /**
    * Set a request header to be sent with this request. Any existing header with
    * the same name will be overwritten.
    *
    * @param name The name of the header.
    * @param value The value of the header.
    */
   InlineRequestPreferences.prototype.setHeader = function(name, value)
   {
      this.headers[name] = value;
   };

   /**
    * Set whether the request should be made asynchronously or not.
    *
    * @param isAsync True if the request should be asynchronous; false if the
    *                request should be synchronous.
    */
   InlineRequestPreferences.prototype.setAsync = function(isAsync)
   {
      this.isAsync = isAsync;
   };

   /**
    * Set the callback function to be invoked when the request has been completed
    * successfully. This function will not be invoked on error conditions.
    *
    * The callback should take a single parameter, that being the response data.
    * The type of that data is dependent upon the request type. No return value
    * is expected.
    *
    * @param callback The function to be called on success.
    */
   InlineRequestPreferences.prototype.setCallback = function(callback)
   {
      this.callback = callback;
   };

   /**
    * Set the callback function to be invoked when an error occurs during the
    * processing of the request. This function will not be invoked if the request
    * completes successfully.
    *
    * The signature of the callback should be as follows:
    *
    *   function callback(statusCode, preferences)
    *
    * where statusCode is the HTTP status code from the request and preferences
    * is the same preferences object originally passed to sendRequest. No return
    * value is expected.
    *
    * @param callback The function to be called on success.
    */
   InlineRequestPreferences.prototype.setErrorCallback = function(callback)
   {
      this.errorCallback = callback;
   };

   /**
    * Set the timeout value for this request, in milliseconds. If the request
    * is outstanding for longer than this, it will automatically be stopped.
    * Note that timeouts are supported only for asynchronous requests.
    *
    * @param timeout The timeout period, in milliseconds.
    */
   InlineRequestPreferences.prototype.setTimeout = function(timeout)
   {
      this.timeout = timeout;
   };

   /**
    * Set the options to use for automatic DOM node update. This applies for HTML
    * requests only. When using these options, an HTML response will be processed
    * automatically, updating the specified DOM node and optionally evaluating any
    * embedded script elements.
    *
    * @param targetNode The node whose content will be replaced with the content
    *                   of the response. Either the node itself or its id may be
    *                   provided.
    * @param bEvalScripts Whether or not any script elements embedded in the
    *                     response should be evaluated.
    */
   InlineRequestPreferences.prototype.setHtmlOptions = function(targetNode, bEvalScripts)
   {
      this.htmlOptions["targetNode"] = targetNode;
      this.htmlOptions["bEvalScripts"] = bEvalScripts;
   };

   /**
    * InlineRequest
    *
    * This class is used internally to represent an outstanding request. It is
    * essentially an aggregation of the client's preferences, the actual XHR
    * instance, and other related information.
    *
    * Many of the getter methods on this class simply return values from the
    * current object or its associated preferences. Others, however, provide
    * additional processing over and above this, primarily to eliminate the
    * need for further client processing.
    */
   function InlineRequest (id, prefs, xhr)
   {
      this.id = id;
      this.prefs = prefs;
      this.xhr = xhr;

      var type_map = ["text/plain", "text/html", "application/xml", "text/json", "text/javascript"];
      this.mimetype = type_map[this.prefs.type - 1];
   }

   /**
    * Retrieve the unique identifier for this request.
    *
    * @return The identifier for this request.
    */
   InlineRequest.prototype.getId = function()
   {
      return this.id;
   }

   /**
    * Retrieve the InlneRequestPreferences instance for this request.
    *
    * @return The original preferences for this request.
    */
   InlineRequest.prototype.getPreferences = function()
   {
      return this.prefs;
   }

   /**
    * Retrieve the XMLHttpRequest instance to use for this request.
    *
    * @return The XMLHttpRequest for this request.
    */
   InlineRequest.prototype.getXhr = function()
   {
      return this.xhr;
   }

   /**
    * Set the XMLHttpRequest instance to use for this request.
    *
    * @param xhr The XMLHttpRequest for this request.
    */
   InlineRequest.prototype.setXhr = function(xhr)
   {
      this.xhr = xhr;
   }

   /**
    * Retrieve the inline request type for this request.
    *
    * @return The request type, as one of the values of the InlineRequestType
    *         enumeration.
    */
   InlineRequest.prototype.getType = function()
   {
      return this.prefs.type;
   }

   /**
    * Retrieve the HTTP method to use for this request. The return value is
    * guaranteed to be in upper case.
    *
    * @ereturn The HTTP method for this request.
    */
   InlineRequest.prototype.getMethod = function()
   {
      var method = this.prefs.method || "POST";
      return method.toUpperCase();
   }

   /**
    * Retrieve the base URL for this request. The base URL is the URL configured
    * in the original InlineRequestPreferences instance, unmodified by any
    * additional parameters specified on that object or elsewhere.
    *
    * @return The base URL for this request.
    */
   InlineRequest.prototype.getBaseUrl = function()
   {
      return this.prefs.url;
   }

   /**
    * Retrieve the request headers for this request, as a map of name / value
    * pairs.
    *
    * @return The headers for this request.
    */
   InlineRequest.prototype.getHeaders = function()
   {
      return this.prefs.headers;
   }

   /**
    * Retrieve the callback function for this request. This callback will be
    * invoked to handle a successful request.
    *
    * If no callback function was provided by the client, a no-op function will
    * be returned from this function, so that a non-null valid function can be
    * assumed.
    *
    * @return The callback function for this request.
    */
   InlineRequest.prototype.getCallback = function()
   {
      return wdk.common.getSafeFunction("callback", this.prefs.callback);
   }

   /**
    * Retrieve the error callback function for this request. This callback will
    * be invoked to handle any errors encountered when making the request.
    *
    * If no error callback function was provided by the client, a no-op function
    * will be returned from this function, so that a non-null valid function can
    * be assumed.
    *
    * @return The error callback function for this request.
    */
   InlineRequest.prototype.getErrorCallback = function()
   {
      return wdk.common.getSafeFunction("error callback", this.prefs.errorCallback);
   }

   /**
    * Retrieve the timeout period for this request.
    *
    * @return The timeout, in milliseconds.
    */
   InlineRequest.prototype.getTimeout = function()
   {
      return this.prefs.timeout;
   }

   /**
    * Set the time at which this request was originally started.
    *
    * @param time The time at which this request was started.
    */
   InlineRequest.prototype.setStartTime = function(time)
   {
      this.startTime = time;
   }

   /**
    * Retrieve the time at which this request was originally started.
    *
    * @return The time at which this request was started.
    */
   InlineRequest.prototype.getStartTime = function()
   {
      return this.startTime;
   }

   /**
    * Determine whether or not this request has timed out. This function will
    * always return true if no timeout period was specified.
    *
    * @return true if the request has timed out; false otherwise.
    */
   InlineRequest.prototype.isTimedOut = function()
   {
      if (!this.prefs.timeout)
      {
         return false;
      }

      var now = (new Date()).getTime();
      return (now > this.startTime + this.prefs.timeout);
   }

   /**
    * Retrieve the target node, if any, specified for automated HTML DOM node
    * update.
    *
    * @return The target node, or null if none was specified.
    */
   InlineRequest.prototype.getTargetNode = function()
   {
      return this.prefs.htmlOptions["targetNode"];
   }

   /**
    * Retrieve the flag that determines whether or not embedded scripts should be
    * evaluated automatically in HTML responses.
    *
    * @return true if embedded scripts should be evaluated; false otherwise.
    */
   InlineRequest.prototype.getEvalScripts = function()
   {
      return !!(this.prefs.htmlOptions["bEvalScripts"]);
   }

   /**
    * Determine whether or not this request should be made asynchronously.
    *
    * @return true if the request is asynchronous; false otherwise.
    */
   InlineRequest.prototype.isAsync = function()
   {
      return this.prefs.isAsync;
   }

   /**
    * Retrieve the MIME type for this request. The MIME type is determined from
    * the inline request type.
    *
    * @return The MIME type for this request.
    */
   InlineRequest.prototype.getMimeType = function()
   {
      return this.mimetype;
   }

   /**
    * Retrieve the body content for this request. The body content includes all
    * of the parameters except any that were part of the original base URL. It
    * is only valid for POST requests, so this method returns null if the request
    * method is not POST.
    *
    * @return The body content, or null if it is not applicable.
    */
   InlineRequest.prototype.getContent = function()
   {
      if (this.getMethod() != "POST")
      {
         return null;
      }

      var builder = new wdk.common.UrlBuilder(null);
      builder.setParameters(this.prefs.parameters);
      return builder.getQueryString();
   }

   /**
    * Retrieve the URL to be used for this request. For a POST request, this will
    * be the same as the base URL. For a GET request, however, the URL includes
    * all of the parameters as well, encoded as part of the query string.
    *
    * @return The URL for this request.
    */
   InlineRequest.prototype.getUrl = function()
   {
      if (this.getMethod() == "POST")
      {
         return this.prefs.url;
      }
      var builder = new wdk.common.UrlBuilder(this.prefs.url);
      builder.setParameters(this.prefs.parameters);
      return builder.getUrl();
   }
};

} // end if if (typeof InlineRequestType == "undefined")

/**
 * This is a utility function that constructs an InlineRequestPreferences
 * instance from the object passed as a parameter. It is intended to be used
 * primarily when generating code, to avoid the need to generate code for
 * numerous calls to the InlineRequestPreferences API. It is <em>not</em>
 * recommended for general use, since it is much more prone to error than
 * using the API directly.
 *
 * Preference values are passed using the JavaScript keyword parameters idiom,
 * meaning that a single object is passed that includes all parameters as
 * key / value pairs. This allows for arbitrary combinations of parameters
 * without the need to pass dummy values for placement purposes.
 *
 * Valid keyword names and parameter values are:
 *
 *   type          - An InlineRequestType value. This is the only mandatory
 *                   parameter.
 *   method        - GET or POST. Default is POST.
 *   url           - The URL to submit to.
 *   isAsync       - true for an async request; false otherwise. Default is
 *                   true.
 *   timeout       - Timeout value in milliseconds. Default is no timeout.
 *   callback      - Callback function. Either the function name or the
 *                   function itself may be specified.
 *   errorCallback - Error callback function. Either the function name or the
 *                   function itself may be specified.
 *   parameters    - An object representing the request parameters to be
 *                   submitted. If a multi-valued parameter is required, an
 *                   array of values should be provided.
 *   headers       - An object representing the request headers to be
 *                   submitted. Multi-values headers are not supported.
 *   htmlOptions   - An object that specifies the 'targetNode' value (required)
 *                   and the 'bEvalScripts' value (optional).
 *
 * @param params The object containing the keyword parameters.
 *
 * @return A newly constructed and configured preferences instance.
 */
function makePrefs(params) {
   // Check for mandatory parameters (e.g. type) and throw an exception if missing
   if (!params["type"])
   {
      throw Error("Mandatory value missing: type");
   }

   var prefs = InlineRequestEngine.getPreferences(params["type"]);
   if (params["method"])
   {
      prefs.setMethod(params["method"]);
   }
   if (params["url"])
   {
      prefs.setUrl(params["url"]);
   }
   if (params["isAsync"] != null)
   {
      prefs.setAsync(params["isAsync"]);
   }
   if (params["timeout"])
   {
      prefs.setTimeout(params["timeout"]);
   }
   if (params["callback"])
   {
      prefs.setCallback(params["callback"]);
   }
   if (params["errorCallback"])
   {
      prefs.setErrorCallback(params["errorCallback"]);
   }
   if (params["parameters"])
   {
      var parameters = params["parameters"];
      for (var param in parameters)
      {
         var value = parameters[param];
         if (value instanceof Array)
         {
            for (var i = 0; i < value.length; i++)
            {
               prefs.addParameter(param, value[i]);
            }
         }
         else
         {
            prefs.addParameter(param, value);
         }
      }
   }
   if (params["headers"])
   {
      var headers = params["headers"];
      for (var header in headers)
      {
         prefs.setHeader(header, headers[header]);
      }
   }
   if (params["htmlOptions"])
   {
      var options = params["htmlOptions"];
      var targetNode = options["targetNode"];
      if (targetNode)
      {
         var bEvalScripts = !!options["bEvalScripts"];
         prefs.setHtmlOptions(targetNode, bEvalScripts);
      }
   }

   return prefs;
}


/********** High-Level API ********************************************/

/**
 * Post a server-side Web Form Event using an inline request.
 *
 * Note that this function is like a varargs method in that an arbitrary
 * number of (strEventArgName, strEventArgValue) pairs may be specified.
 *
 * @param  strFormId         id of the form to submit. If null, the first form
 *                           on the page is assumed.
 * @param  prefs             instance of InlineRequestPreferences specifying
 *                           how the inline request should be made
 * @param  strSrcCtrl        id of control firing event (optional)
 * @param  strHandlerCtrl    id of control handling the event (default => form)
 * @param  strHandlerMethod  method name of event handler
 * @param  strEventArgName   event argument name
 * @param  strEventArgValue  event argument value
 */
function postInlineServerEvent(strFormId, prefs, strSrcCtrl, strHandlerCtrl, strHandlerMethod, strEventArgName, strEventArgValue)
{
   if (Trace_INLINEREQUEST)
   {
      Trace_println("Entered postInlineServerEvent");
   }

   // Sanity Check
   if (strHandlerMethod == null || strHandlerMethod == "" || (typeof strHandlerMethod == "undefined"))
   {
      throwError("postInlineServerEvent: strHandlerMethod is a mandatory parameter");
      return;
   }

   // Test and acquire the event posting lock in a single statement
   if (acquireEventPostingLock() == false)
   {
      if (Trace_INLINEREQUEST)
      {
         Trace_println("...failed to get event posting lock");
      }

      return;
   }
   /* TODO Need to provide a way for this high level API to release the lock.
    * The issue here is that the lock needs to be released when the low level
    * API determines that the request is complete, but the low level API does
    * not know about this lock. For now, clients know to release the lock
    * explicitly from within their callback when using this function.
    */

   // Get Form to Post : Either from strFormId if it exists, or look for the first form on the page.
   if (strFormId == null)
   {
      // If form doesn't exist, Safari returns NULL, other browsers return type 'undefined'. This check covers both.
      if (document.forms && document.forms[0])
      {
         strFormId = document.forms[0].name;
      }
   }

   var formElement = null;
   var preSubmitHandler = null;

   if (strFormId != null)
   {
      formElement = document[strFormId];
      preSubmitHandler = window[strFormId + "_preSubmitForm"];
   }

   // Multipart requests (a.k.a. file uploads) are not supported
   if (formElement && formElement.enctype == "multipart/form-data")
   {
      throwError("postInlineServerEvent: multipart/form-data is not supported");
      return;
   }

   var functionArgs = arguments;

   // Ensure form and preSubmitHandler are defined [ done loading ], otherwise wait.
   if (formElement && preSubmitHandler)
   {
      // Establish default handler control, if required
      if (strHandlerCtrl == null || strHandlerCtrl == "" || (typeof strHandlerCtrl == "undefined"))
      {
         strHandlerCtrl = strFormId;
      }

      // Set event fields
      formElement.__dmfAction.value = strFormId + "_" + strHandlerMethod;
      formElement.__dmfHandler.value = strFormId + "_" + strHandlerCtrl;
      if (typeof(strSrcCtrl) != "undefined" && strSrcCtrl != null && strSrcCtrl != "")
      {
         formElement.__dmfControl.value = escapeUnicodeString(strFormId + "_" + strSrcCtrl);
      }
      else
      {
         formElement.__dmfControl.value = "";
      }

      // Generate Event Arguments
      var handlerArgs = new wdk.lang.MultiMap();
      var EVENTARG_OFFSET = 5;

      for (var iArg = EVENTARG_OFFSET; iArg < functionArgs.length; iArg+=2)
      {
         var strEventName = functionArgs[iArg];
         if (strEventName != null)
         {
            var strEventValue = functionArgs[iArg+1];
            if (typeof(strEventValue) != "undefined" && strEventValue != null)
            {
               handlerArgs.add(escapeUnicodeString(strEventName), escapeUnicodeString(strEventValue));
            }
         }
      }

      // Append key press arguments - set by setKeys()
      wdk.forms.addKeyStates(handlerArgs);

      var builder = new wdk.common.UrlBuilder(null);
      builder.setParameters(handlerArgs);
      formElement.__dmfHandlerArgs.value = strFormId + "_" + builder.getQueryString();

      if (Trace_INLINEREQUEST)
      {
         Trace_println("posting inline form event");
         Trace_println("... action = " + formElement.__dmfAction.value);
         Trace_println("... handler = " + formElement.__dmfHandler.value);
         Trace_println("... handlerArgs = " + formElement.__dmfHandlerArgs.value);
         Trace_println("... source control = " + formElement.__dmfControl.value);
      }

      // Store the current scroll position
      storeScrollPosition(strFormId, g_arrXId[strFormId], g_arrYId[strFormId]);

      var strControlId = null;
      if ( strSrcCtrl != null )
          {
         var elements = document.getElementsByName(strSrcCtrl);
         if ( elements && elements.length > 0 )
         {
            strControlId = elements[0].id;
         }
       }

      // Fire Generic Pre Submit. Handlers can implement presubmit behaviour common to all forms
      firePreSubmitClientEvent(postServerEvent.GENERIC_PRE_SUBMIT);

      // Call the preSubmitHandler for this Web Form
      preSubmitHandler(strFormId, strControlId);

      // Mark this as an inline request
      prefs.addParameter("__dmfInlineRequest", "true")

      // Submit the Web Form
      wdk.forms.submitInline(formElement, prefs);
   }
   else
   {
      if (Trace_INLINEREQUEST)
      {
         Trace_println("postServerEvent for " + ((strFormId != null) ? strFormId : "forms[0]")
                        + " in window '" + window.name + "', Form not found. Delaying Call by "
                           + postServerEvent.DELAY_PERIOD + "ms");
      }

      if ( postServerEvent.m_delayCount++ < postServerEvent.MAX_DELAY_COUNT )
      {
         // Haven't reached the max delay attempt limit, so try delaying [again]
         var delayed = _postInlineServerEventClosure.apply(this, functionArgs);
         return setTimeout(delayed, postServerEvent.DELAY_PERIOD);
      }
      else
      {
         // If giving up, reset count and free lock.
         g_serverEventLock = 0;
         postServerEvent.m_delayCount = 0;

         if (Trace_INLINEREQUEST)
         {
            Trace_println("postServerEvent for " + ((strFormId != null) ? strFormId : "forms[0]")
                           + " in window '" + window.name + "', ABORTING. Form not found after "
                              + postServerEvent.MAX_DELAY_COUNT * postServerEvent.DELAY_PERIOD + "ms");
         }
      }
   }
} // postInlineServerEvent

function _postInlineServerEventClosure()
{
   var functionArgs = arguments;
   return function()
   {
      g_serverEventLock = 0;
      postInlineServerEvent.apply(this, functionArgs);
   };
}

/**
 * Given a regular inline request callback, return a replacement callback that
 * takes care of the underpinnings of the action response infrastructure, as
 * well as calls the original callback with the response data.
 *
 * @param callback The original callback to be invoked with the response data.
 *
 * @return A new callback that wraps the original one, and handles the action
 *         response infrastructure.
 */
function makeActionCallback(callback)
{
   return (function(data){_processActionResponse(data, callback);});
}

/**
 * Process the response from an action request. This involves invoking the
 * original callback and then firing any client events that were returned
 * as part of the response.
 *
 * @param data The complete response from the server, including both the
 *             actual response data as well as any client events.
 * @param callback The original callback to be invoked with the actual
 *                 response data.
 */
function _processActionResponse(data, callback)
{
   releaseEventPostingLock();

   var exception = data["EXCEPTION"];

   if (exception)
   {
      var url = "/" + getVirtualDir() + "/component/errormessage";
      var options = "width=620,height=460,location=no,status=no,menubar=no,toolbar=no,resizable=yes,scrollbars=yes";
      var newwindow = window.open(url, "_blank", options);
      newwindow.focus();
      return; // Don't try to do anything else if there was an exception
   }

   var responseData = data["RESPONSE_DATA"];
   var clientEvents = data["CLIENT_EVENTS"];

   // start a transaction and ignore everything in between here and the end of client events.
   if (window.startRecordingTransaction)
   {
      startRecordingTransaction('ignoreControlEvent') ;
   }

   callback = wdk.common.getSafeFunction("action callback", callback);
   callback(responseData);

   processClientEvents(clientEvents);

   if (window.stopRecordingTransaction)
   {
      stopRecordingTransaction('ignoreControlEvent');
   }
}

function processClientEvents(clientEvents)
{
   if (clientEvents && clientEvents instanceof Array)
   {
      for (var i = 0; i < clientEvents.length; i++)
      {
         var event = clientEvents[i];
         var evtName = event["EVENT"];
         var evtArgs = event["ARGS"];

         evtArgs.unshift(evtName);
         fireClientEvent.apply(window, evtArgs);
      }
   }
}


