// Copyright (C) 2007-2012 Bristle Software, Inc.
// 
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 1, or (at your option)
// any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc.

/******************************************************************************
* com.bristle.jslib.Ajax.Util.js
*******************************************************************************
* Purpose:
*       This file contains utility routines that are reusable for doing Ajax
*       operations from any JavaScript or HTML file.
* Usage:
*       - A typical simplistic scenario for using this file from an HTML file is:
*
*         <script language="JavaScript" src='com.bristle.jslib.Util.js'></script>
*         <script language="JavaScript" src='com.bristle.jslib.Exception.js'></script>
*         <script language='JavaScript' src='com.bristle.jslib.Ajax.Util.js'></script>
*         <script language='JavaScript'>
*         function cmdOK_onClick(evt, obj)
*         {
*             txtOut.value += "Sending XML request asynchronously...\n";
*             com.bristle.jslib.Ajax.Util.getAjaxXML
*                 ("XmlKeepAlive.jsp?pageName=Login.jsp"
*                 ,reportSuccess);
*             txtOut.value += "... XML request sent.\n";
*         }
*         function reportSuccess(xml)
*         {
*             txtOut.value += 
*               "Data received: " 
*               + xml.getElementsByTagName("name")[0].firstChild.nodeValue
*               + "\n";
*         }
*
*       - A more complex example is shown below.  It uses different callbacks 
*         for success, failure, and timeout, shares the same callback for 
*         progress and loading, specifies a null callback for loaded, and 
*         omits the callback parameter for uninitialized.  Not a particularly 
*         useful combination, but you get the idea.
*
*         <script language="JavaScript" src='com.bristle.jslib.Util.js'></script>
*         <script language="JavaScript" src='com.bristle.jslib.Exception.js'></script>
*         <script language='JavaScript' src='com.bristle.jslib.Ajax.Util.js'></script>
*         <script language='JavaScript'>
*         function cmdOK_onClick(evt, obj)
*         {
*             txtOut.value += "Sending XML request asynchronously...\n";
*             com.bristle.jslib.Ajax.Util.getAjaxXML
*                 ("XmlKeepAlive.jsp?pageName=Login.jsp"
*                 ,reportSuccess
*                 ,reportFailure
*                 ,10000        // Timeout millisecs
*                 ,reportTimeout
*                 ,reportProgressAndLoading
*                 ,null
*                 ,reportProgressAndLoading);
*             txtOut.value += "... XML request sent.\n";
*         }
*         function reportSuccess(xml)
*         {
*             txtOut.value += 
*               "Data received: " 
*               + xml.getElementsByTagName("name")[0].firstChild.nodeValue
*               + "\n";
*         }
*         function reportFailure(xml, xhr)
*         {
*             txtOut.value += 
*               "HTTP Error: " + xhr.status + ": " + xhr.statusText + "\n";
*         }
*         function reportTimeout(xml, xhr)
*         {
*             txtOut.value += 
*               "Ajax operation timed out." + "\n";
*         }
*         function reportProgressAndLoading(xml, xhr)
*         {
*             if (xhr.readyState 
*                 == com.bristle.jslib.Ajax.Util.AJAX_STATE_INTERACTIVE)
*             {
*                 txtOut.value += "Progressing..." + "\n";
*             }
*             else if (xhr.readyState 
*                      == com.bristle.jslib.Ajax.Util.AJAX_STATE_LOADING)
*             {
*                 txtOut.value += "Loading..." + "\n";
*             }
*         }
*
* Assumptions:
*       - The file "com.bristle.jslib.Util.js" has already been loaded.
*       - The file "com.bristle.jslib.Exception.js" has already been loaded.
* Effects:
*       - May initiate or respond to Ajax operations at a remote Web server,
*         as directed by the caller.
* Anticipated Changes:
* Notes:
* Implementation Notes:
* Portability Issues:
* Revision History:
*   $Log$
******************************************************************************/

// Create the "namespace" to hold the functions in this file.
com.bristle.jslib.Ajax = {};
com.bristle.jslib.Ajax.Util = {};

/******************************************************************************
* Get a unique URL parameter to force a reload, bypassing any cached pages.
******************************************************************************/
com.bristle.jslib.Ajax.Util.getForceReloadParam =
function()
{
    // Generate a unique URL param to force a reload, bypassing the browser
    // cache.  This is especially useful for loading images via the SRC 
    // property of the IMG tag because otherwise, depending on the browser, 
    // and the user settings, the previously cached image may be re-used, 
    // despite any Cache-Control header that may be set.
    // Note:  Use a timestamp to avoid random duplicates.
    // Note:  Use a random number in case of multiple calls within the same
    //        millisecond.
    // Note:  Use getTime(), not getMilliseconds() which returns a value 0-999.
    intRandom = com.bristle.jslib.Util.randomInt(1,1000000);
    return "&forceReload=" + intRandom + "_" + new Date().getTime();
}

/******************************************************************************
* Returns a new XMLHttpRequest object for use with Ajax operations.
*
*@throws com.bristle.jslib.Exception.intEXC_UNABLE_TO_CREATE_AJAX_OBJECT
******************************************************************************/
com.bristle.jslib.Ajax.Util.createAjaxObject =
function()
{
    var xhr = null;
    if (window.XMLHttpRequest) 
    {
        // Create XMLHttpRequest object in most browsers.
        xhr = new XMLHttpRequest();
    }
    else if (window.ActiveXObject)
    {
        // Create XMLHttpRequest object in Microsoft browsers.
        try 
        {
            // Newer versions of Internet Explorer
            xhr = new ActiveXObject("Msxml2.XMLHTTP");
        }
        catch (e1)
        {
            try 
            {
                // Older versions of Internet Explorer
                xhr = new ActiveXObject("Microsoft.XMLHTTP");
            }
            catch (e2)
            {
                throw new com.bristle.jslib.Exception.Exception
                (com.bristle.jslib.Exception.intEXC_UNABLE_TO_CREATE_AJAX_OBJECT
                ,"Unable to create Ajax object"
                ,"com.bristle.jslib.Ajax.Util.createAjaxObject"
                );
            }
        }
    }
    else
    {
        throw new com.bristle.jslib.Exception.Exception
        (com.bristle.jslib.Exception.intEXC_UNABLE_TO_CREATE_AJAX_OBJECT
        ,"Unable to create Ajax object"
        ,"com.bristle.jslib.Ajax.Util.createAjaxObject"
        );
    }
    return xhr;
}

/******************************************************************************
* Registers the specified functions as callbacks for the specified 
* XMLHttpRequest object, and starts the timeout timer.  The callback functions 
* will be called asynchronously in response to the various state changes of 
* the XMLHttpRequest as it processes Ajax requests.
*
* Notes:
* - Each callback is optional.  Trailing callback params can be omitted 
*   entirely.  When not omitted, each callback can be specified as null, 
*   or as a JavaScript function with the signature:
*       function(XMLDOM, XMLHttpRequest)
*   where the 1st parameter is the XML DOM Document returned by the 
*   responseXML property of the XMLHttpRequest, and the 2nd parameter is
*   the XMLHttpRequest object itself.  The 2nd parameter is optional, 
*   which allows callbacks to ignore the XMLHttpRequest object and simply
*   operate on the XML DOM Document passed in the 1st parameter, as:
*       xmlDOM.getElementsByTagName("name")[0].firstChild.nodeValue
*   More advanced callbacks can use the 2nd parameter to access the other 
*   methods of the XMLHttpRequest object:
*       abort()
*       getResponseHeader()
*       getAllResponseHeaders()
*       etc.
*   and the other properties:
*       readyState
*       status
*       statusText
*       responseText
*       etc.
*   These can be especially useful if the same callback is registered to 
*   handle multiple states, or both success and failure statuses of the 
*   Complete state.  Or to handle non-XML responses like HTML, JSON, or plain
*   text, by using responseText instead of responseXML.  Also, callbacks
*   other than callbackSuccess will receive a null XML DOM and may have to 
*   use the XMLHttpRequest parameter to determine the details of the
*   HTTP progress or failure. 
*
*@param xhr                 The XMLHttpRequest object
*@param callbackSuccess     Function to be called when the state is Complete, 
                            and the HTTP status indicates success.
*@param callbackFailure     Function to be called when the state is Complete, 
                            and the HTTP status indicates failure.
*@param intTimeoutMillisecs Number of seconds before aborting the Ajax call
*                           and calling callbackTimeout.
*                           Optional.  Default=0 which means wait forever.
*@param callbackTimeout     Function to be called if the Ajax call times out.
*@param callbackInteractive Function to be called when the state is Interactive
*@param callbackLoaded      Function to be called when the state is Loaded
*@param callbackLoading     Function to be called when the state is Loading
*@param callbackUninitialized
*                           Function to be called when the state is Uninitialized
*@return  Handle to setTimeout() used for callbackTimeout so it can be 
*         canceled by the caller via clearTimeout() if necessary.
******************************************************************************/
com.bristle.jslib.Ajax.Util.AJAX_STATE_COMPLETE      = 4;
com.bristle.jslib.Ajax.Util.AJAX_STATE_INTERACTIVE   = 3;
com.bristle.jslib.Ajax.Util.AJAX_STATE_LOADED        = 2;
com.bristle.jslib.Ajax.Util.AJAX_STATE_LOADING       = 1;
com.bristle.jslib.Ajax.Util.AJAX_STATE_UNINITIALIZED = 0;
com.bristle.jslib.Ajax.Util.HTTP_STATUS_SUCCESS = 200;
com.bristle.jslib.Ajax.Util.registerAjaxCallbacksAndStartTimer =
function(xhr
        ,callbackSuccess
        ,callbackFailure
        ,intTimeoutMillisecs
        ,callbackTimeout
        ,callbackInteractive
        ,callbackLoaded
        ,callbackLoading
        ,callbackUninitialized)
{
    // Schedule a call to callbackTimeout.
    // Note: Use a closure (anonymous function) to keep track of which 
    //       XMLHttpRequest to pass to callbackTimeout when there are multiple
    //       XMLHttpRequests instead of one global singleton.
    // Note: Shouldn't really start the timer until we make the Ajax call.  
    //       Not now, when we are just registering the callbacks.  Should move 
    //       this code to getAjaxXML().  But, the callbacks need to know what 
    //       timer to cancel when the Ajax completes with failure or success, 
    //       so we need a handle to the timer when creating the callbacks, 
    //       and we don't know how get a handle to a timer without starting 
    //       the timer.  Could wrap the timer in another object that creates it
    //       when told and keeps a handle to it, and could then pass a handle
    //       to that timer to the callbacks.  Maybe later.  For now, this is
    //       sufficient, since most use is via getAjaxXML() which makes the
    //       Ajax call immediately after registering the callbacks.
    var timer = null;
    if (intTimeoutMillisecs != null 
        && typeof(intTimeoutMillisecs) != "undefined"
        && intTimeoutMillisecs > 0 )
    {
        timer = window.setTimeout
           (function() 
            {
                if (callbackTimeout != null 
                    && typeof(callbackTimeout) != "undefined")
                {
                    callbackTimeout(null, xhr);
                }
                xhr.abort();
            }
           ,intTimeoutMillisecs
           ,"JavaScript"
           );
    }

    // Note: Use a closure (anonymous function) to keep track of which 
    //       XMLHttpRequest to pass to the callbacks when there are multiple
    //       XMLHttpRequests instead of one global singleton.
    var callback = null;
    xhr.onreadystatechange = 
    function()    
    {
        // Determine which callback to call.
        if (xhr.readyState == com.bristle.jslib.Ajax.Util.AJAX_STATE_COMPLETE)
        {
            // Cancel the timeout immediately.  The operation has completed.
            // Note: The completion may have been caused by the timer's call
            //       to xhr.abort(), so the timer may already have fired.
            //       Canceling it after it fires is not a problem.
            if (timer != null)
            {
                window.clearTimeout(timer);
            }

            // Catch any errors that occur while trying to determine the 
            // reason for the completion of the operation.  Reasons can be:
            // 1. Normal completion with successful HTTP status code.
            // 2. Normal completion with error HTTP status code.
            // 3. Call to xhr.abort()
            //    Note: We could probably prevent this case, if we ever needed 
            //          to, by changing the value of xhr.onreadystatechange to 
            //          no longer refer to this code before calling xhr.abort().
            // 4. HTTP server was already down or inaccessible when Ajax call
            //    was made.
            // 5. HTTP server went down or became inaccessible before Ajax call
            //    completed normally.
            // We test xhr.status to distinguish between cases 1 and 2.
            // However, with Firefox 2.0.0.10, testing it in cases 3, 4, and 5 
            // causes an exception:
            //        Component returned failure code: 0x80040111 
            //        (NS_ERROR_NOT_AVAILABLE) [nsIXMLHttpRequest.status]
            // to be thrown.
            try
            {
                if (xhr.status == com.bristle.jslib.Ajax.Util.HTTP_STATUS_SUCCESS)
                {
                    callback = callbackSuccess;
                }
                else
                {
                    callback = callbackFailure;
                }
            }
            catch(exception)
            {
                // Treat exception while checking status as a failure.
                // This is to act the same in browsers where such an 
                // exception occurs as in browsers where it does not.
                // Since something has gone wrong that causes an exception 
                // in some browsers, other browsers would most likely not
                // have found an HTTP success code, and would have treated
                // this as a failure.
                callback = callbackFailure;
            }
        }
        else if (xhr.readyState == com.bristle.jslib.Ajax.Util.AJAX_STATE_INTERACTIVE)
        {
            callback = callbackInteractive;
        }
        else if (xhr.readyState == com.bristle.jslib.Ajax.Util.AJAX_STATE_LOADED)
        {
            callback = callbackLoaded;
        }
        else if (xhr.readyState == com.bristle.jslib.Ajax.Util.AJAX_STATE_LOADING)
        {
            callback = callbackLoading;
        }
        else if (xhr.readyState == com.bristle.jslib.Ajax.Util.AJAX_STATE_UNINITIALIZED)
        {
            callback = callbackUninitialized;
        }

        // Call the callback.
        if (callback != null && typeof(callback) != "undefined")
        {
            callback(xhr.responseXML, xhr);
        }
    }

    return timer;
}

/******************************************************************************
* Creates a new XMLHttpRequest object for use with Ajax operations, registers 
* the specified functions as callbacks, and queues an asynchronous request to 
* get XML from the specified URL.  The callback functions will be called 
* asynchronously in response to the various state changes of the 
* XMLHttpRequest as it processes the Ajax request.
*
* Notes:
* - See registerAjaxCallbacksAndStartTimer for details of the callback 
*   parameters, which are all optional.
*
*@param strURL              The URL to be accessed via Ajax
*@param callbackSuccess     Function to be called when the state is Complete, 
*                           and the HTTP status indicates success.
*@param callbackFailure     Function to be called when the state is Complete, 
*                           and the HTTP status indicates failure.
*@param intTimeoutMillisecs Number of seconds before aborting the Ajax call
*                           and calling callbackTimeout.
*                           Optional.  Default=0 which means wait forever.
*@param callbackTimeout     Function to be called if the Ajax call times out.
*@param callbackInteractive Function to be called when the state is Interactive
*@param callbackLoaded      Function to be called when the state is Loaded
*@param callbackLoading     Function to be called when the state is Loading
*@param callbackUninitialized
*                           Function to be called when the state is Uninitialized
*@throws com.bristle.jslib.Exception.intEXC_UNABLE_TO_CREATE_AJAX_OBJECT
*@throws com.bristle.jslib.Exception.intEXC_ERROR_DURING_AJAX_OPEN
*@throws com.bristle.jslib.Exception.intEXC_ERROR_DURING_AJAX_SEND
******************************************************************************/
com.bristle.jslib.Ajax.Util.getAjaxXML =
function(strURL
        ,callbackSuccess
        ,callbackFailure
        ,intTimeoutMillisecs
        ,callbackTimeout
        ,callbackInteractive
        ,callbackLoaded
        ,callbackLoading
        ,callbackUninitialized)
{
    var xhr = com.bristle.jslib.Ajax.Util.createAjaxObject();

    // Try block to catch all errors so the timer can be cancelled.
    var timer = null;
    try
    {
        timer = com.bristle.jslib.Ajax.Util.registerAjaxCallbacksAndStartTimer
            (xhr
            ,callbackSuccess
            ,callbackFailure
            ,intTimeoutMillisecs
            ,callbackTimeout
            ,callbackInteractive
            ,callbackLoaded
            ,callbackLoading
            ,callbackUninitialized
            );

        // Queue the asynchronous HTTP request.
        try 
        {
            var blnASYNC = true;
            xhr.open("GET", strURL, blnASYNC);    //?? Change to POST?
        }
        catch (exception)
        {
            throw new com.bristle.jslib.Exception.Exception
                    (com.bristle.jslib.Exception.intEXC_ERROR_DURING_AJAX_OPEN
                    ,"Error during open() of Ajax object"
                    ,"com.bristle.jslib.Ajax.Util.getAjaxXML"
                    );
        }
        try 
        {
            xhr.send("");
        }
        catch (exception)
        {
            throw new com.bristle.jslib.Exception.Exception
                    (com.bristle.jslib.Exception.intEXC_ERROR_DURING_AJAX_SEND
                    ,"Error during send() of Ajax object"
                    ,"com.bristle.jslib.Ajax.Util.getAjaxXML"
                    );
        }
    }
    catch (exception)
    {
        if (timer != null)
        {
            window.clearTimeout(timer);
        }
        throw exception;
    }
}

/******************************************************************************
* Creates a new XMLHttpRequest object and uses it synchronously to make an
* HTTP request.
*@param strURL              The URL to be accessed via Ajax
*@return The XMLHttpRequest object, so the caller can examine its properties:
*           status, statusText, responseText, responseXML
*@throws com.bristle.jslib.Exception.intEXC_UNABLE_TO_CREATE_AJAX_OBJECT
*@throws com.bristle.jslib.Exception.intEXC_ERROR_DURING_AJAX_OPEN
*@throws com.bristle.jslib.Exception.intEXC_ERROR_DURING_AJAX_SEND
******************************************************************************/
com.bristle.jslib.Ajax.Util.doSynchronousAjax =
function(strURL)
{
    var xhr = com.bristle.jslib.Ajax.Util.createAjaxObject();
    try 
    {
        var blnASYNC = true;
        xhr.open("GET", strURL, !blnASYNC);    //?? Change to POST?
    }
    catch (exception)
    {
        throw new com.bristle.jslib.Exception.Exception
                (com.bristle.jslib.Exception.intEXC_ERROR_DURING_AJAX_OPEN
                ,"Error during open() of Ajax object"
                ,"com.bristle.jslib.Ajax.Util.doSynchronousAjax"
                );
    }
    try 
    {
        xhr.send("");
    }
    catch (exception)
    {
        throw new com.bristle.jslib.Exception.Exception
                (com.bristle.jslib.Exception.intEXC_ERROR_DURING_AJAX_SEND
                ,"Error during send() of Ajax object"
                ,"com.bristle.jslib.Ajax.Util.doSynchronousAjax"
                );
    }
    return xhr;
}

/******************************************************************************
* Use Ajax operations to access the specified URL periodically to keep the 
* connection to the server from timing out.  The specified callback functions
* will be called asynchronously when the Ajax operation succeeds or fails.
*
* Notes:
* - See registerAjaxCallbacksAndStartTimer for details of the callback 
*   parameters, which are all optional.
*
*@param strURL          The URL to be accessed via Ajax
*@param intMillisecs    Number of milliseconds to wait between accesses.
*@param blnNow          Access the URL now, before the first delay, if true.
*@param callbackSuccess Function to be called when the state is Complete, 
*                       and the HTTP status indicates success.
*@param callbackFailure Function to be called when the state is Complete, 
*                       and the HTTP status indicates failure.
*@param intTimeoutMillisecs 
*                       Number of seconds before aborting the Ajax call
*                       and calling callbackTimeout.
*                       Optional.  Default=0 which means wait forever.
*@param callbackTimeout Function to be called if the Ajax call times out.
******************************************************************************/
com.bristle.jslib.Ajax.Util.blnDO_FIRST_KEEP_ALIVE_IMMEDIATELY = true;
com.bristle.jslib.Ajax.Util.schedulePeriodicKeepAlive =
function(strURL
        ,intMillisecs
        ,blnNow
        ,callbackSuccess
        ,callbackFailure
        ,intTimeoutMillisecs
        ,callbackTimeout
        )
{
    // Schedule the next call to this function via setTimeout.
    // Note: Can get setTimeout to pass the arguments on the scheduled call,
    //       without having to stringize them into a call string, by creating 
    //       an anonymous function (closure) that calls the desired function
    //       passing the arguments, which are known via the closure at the 
    //       time the anonymous function is generated.
    var timer = window.setTimeout
            (function() 
             {
                 com.bristle.jslib.Ajax.Util.schedulePeriodicKeepAlive
                        (strURL
                        ,intMillisecs
                        ,true
                        ,callbackSuccess
                        ,callbackFailure
                        ,intTimeoutMillisecs
                        ,callbackTimeout
                        );
             }
            ,intMillisecs
            ,"JavaScript"
            );

    if (blnNow)
    {
        try
        {
            com.bristle.jslib.Ajax.Util.getAjaxXML
                (strURL
                ,callbackSuccess
                ,callbackFailure
                ,intTimeoutMillisecs
                ,callbackTimeout
                );
        }
        catch(exception)
        {
            // Report error to the user.
            // Note: Seems like a bad idea for this library routine to show an
            //       error to the user, instead of just throwing it to the 
            //       caller.  However, after the first call has scheduled 
            //       further calls here via setTimeout(), there is no caller
            //       to throw to.  Any ideas?
            //       Factors that make this more acceptable:
            //       - This will only happen after the user has been idle for 
            //         the full keep-alive timeout period, at which point he 
            //         might well want a warning that he is approaching the 
            //         server timeout.
            //       - This only happens when we are unable to create the Ajax 
            //         object, or when an error occurs on the call to open() or
            //         send() to queue an asynchronous HTTP transaction.
            //         It does not occur if an error occurs during the HTTP 
            //         transaction.  That type of error is detected via the 
            //         status field of the Ajax callback.  Therefore, this 
            //         error is not caused by the unavailability of the server.
            //         Only by the existence of support for the Ajax object in 
            //         the current browser, the validity of the URL and its 
            //         params, etc., including attempts to use a URL in a 
            //         different domain than the current web app.  Such errors 
            //         should be found and resolved by developers, not end 
            //         users.
            // Note: The following specific errors may occur:
            //       - com.bristle.jslib.Exception.intEXC_UNABLE_TO_CREATE_AJAX_OBJECT
            //         - When unable to even create the Ajax object, because it 
            //           is not supported by this browser, or because the user 
            //           disallowed it.        
            //       - com.bristle.jslib.Exception.intEXC_ERROR_DURING_AJAX_OPEN
            //         - When there's a problem with the URL, including the 
            //           cross-domain security error.  In that case, Firefox 
            //           always throws the error, and IE only throws it if the 
            //           user answers Yes to the popup:
            //                  "This page is accessing information that is not 
            //                  under its control.  This poses a security risk.
            //                  Do you want to continue?"
            //       - com.bristle.jslib.Exception.intEXC_ERROR_DURING_AJAX_SEND
            //         - When there's a problem with the URL parameters.
            com.bristle.jslib.MsgBox.reportError
                ("An error occurred in the Ajax object used for the keep-alive"
                 + " function to URL " + strURL
                 + "<br />"
                 + "This browser does not support Ajax, or you have set an"
                 + " option to disable it.  Therefore, nothing"
                 + " will prevent the web server from timing out if"
                 + " you are idle long enough."
                ,exception
                ,com.bristle.jslib.MsgBox.blnMSGBOX_SET_FOCUS
                );

            // Cancel further keep-alive attempts if we couldn't even create
            // the Ajax object, and send the request.  This is not a problem
            // with the server; it is a problem with doing Ajax at all in this
            // browser.  Otherwise, errors will keep popping up on each 
            // attempt.
            //?? Also set a page variable that will go back at the next normal
            //?? (non-Ajax) submit to set a flag in the server session to not
            //?? do any more timeouts, or even no more Ajax, for the rest of
            //?? the session.
            window.clearTimeout(timer);
        }
    }
}

