// 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.XMLUtil.js
*******************************************************************************
* Purpose:
*       This file contains utility routines for manipulating XML.
* Usage:
*       - The typical scenario for using this file from an HTML file is:
*         <script language='JavaScript' src='com.bristle.jslib.XMLUtil.js'></script>
*         Call the various functions that reside here.
* Assumptions:
*       - The file "com.bristle.jslib.Util.js" has already been loaded.
*       - The file "com.bristle.jslib.MsgBox.js" has already been loaded.
*       - The file "com.bristle.jslib.ListBox.js" has already been loaded.
*       - The following ActiveX objects are installed.
*         - MSXML.DLL  - XML and XSL support
* Effects:
* Anticipated Changes:
* Notes:
* Implementation Notes:
* Portability Issues:
* Revision History:
*   $Log$
******************************************************************************/

// Create the "namespace" to hold the functions in this file.
com.bristle.jslib.XMLUtil = {};

/******************************************************************************
* Get the owner document of the specified node.  If the node is itself a 
* document, return it.
*
*@param xmlNode     DOM node to get the owner document of.
*@return            Document node.
*@throws            None.
******************************************************************************/
com.bristle.jslib.XMLUtil.getOwnerDocument =
function(xmlNode)
{
    // Note:  The only node that a null owner document is the document
    //        itself.  Can test this as shown below or by comparing the 
    //        the nodeTypeString to "document" or the nodeType to 9.  
    //        This seemed the most reliable.  If the node type numbers 
    //        or strings change in the future, it will still work.
    return (xmlNode.ownerDocument == null)
           ? xmlNode
           : xmlNode.ownerDocument;
}

/******************************************************************************
* Append a named DOM node to the specified DOM node.
*
*@param xmlNode     DOM node to append child to.
*@param strName     Name of the node to append.
*@return            Node that was created.
*@throws            None.
******************************************************************************/
com.bristle.jslib.XMLUtil.appendNamedNode =
function(xmlNode, strName)
{
    return xmlNode.appendChild
            (com.bristle.jslib.XMLUtil.getOwnerDocument
                                        (xmlNode).createElement(strName));

}

/******************************************************************************
* Add an attribute to the specified DOM node.
*
*@param xmlNode     DOM node to add attribute to.
*@param strName     Name of the attribute to add.
*@param strValue    Value of the attribute.
*@return            None.
*@throws            None.
******************************************************************************/
com.bristle.jslib.XMLUtil.addAttribute =
function(xmlNode, strName, strValue)
{
    var NODE_ATTRIBUTE = 2; //?? How to avoid harcoding this number?
    var xmlAttribute = com.bristle.jslib.XMLUtil.getOwnerDocument
                           (xmlNode).createNode(NODE_ATTRIBUTE, strName, "");
    xmlAttribute.text = strValue;
    xmlNode.attributes.setNamedItem(xmlAttribute);
}

/******************************************************************************
* Append a named DOM node containing a text node to the specified DOM node.
*
*@param xmlNode     DOM node to append to.
*@param strName     String to use as name of new DOM node.
*@param strText     String to put in text node.
*@return            Node that was created.
*@throws            None.
******************************************************************************/
com.bristle.jslib.XMLUtil.appendNamedNodeContainingTextNode =
function(xmlNode, strName, strText)
{
    var xmlNewNode = com.bristle.jslib.XMLUtil.appendNamedNode(xmlNode, strName);
    xmlNewNode.text = strText
    return xmlNewNode;
}

/******************************************************************************
* Get the child node with the specified name.  If not found, append a new
* one to the specified node.
*
*@param xmlNode     DOM node to append child to.
*@param strName     Name of the node to get or create.
*@return            Node that was found or created.
*@throws            None.
******************************************************************************/
com.bristle.jslib.XMLUtil.getOrAppendNamedNode =
function(xmlNode, strName)
{
    var xmlRC = xmlNode.selectSingleNode(strName);
    if (xmlRC == null)
    {
        xmlRC = com.bristle.jslib.XMLUtil.appendNamedNode(xmlNode, strName);
    }
    return xmlRC;
}

/******************************************************************************
* Test the functions in this file.
******************************************************************************/
com.bristle.jslib.XMLUtil.testXMLUtil =
function()
{
    var xmldomHTML = new ActiveXObject("msxml.DOMDocument");

    var xmlHTMLTable = com.bristle.jslib.XMLUtil.appendNamedNode
                                                        (xmldomHTML, "table");
    com.bristle.jslib.XMLUtil.addAttribute(xmlHTMLTable, "border", "1");

    var xmlHTMLTableRow = com.bristle.jslib.XMLUtil.appendNamedNode
                                        (xmlHTMLTable, "tr");
    com.bristle.jslib.XMLUtil.appendNamedNodeContainingTextNode
                                        (xmlHTMLTableRow, "td", "CDP");
    com.bristle.jslib.XMLUtil.appendNamedNodeContainingTextNode
                                        (xmlHTMLTableRow, "td", "Study");
    com.bristle.jslib.XMLUtil.appendNamedNodeContainingTextNode
                                        (xmlHTMLTableRow, "td", "Country");
    com.bristle.jslib.XMLUtil.appendNamedNodeContainingTextNode
                                        (xmlHTMLTableRow, "td", "Containers");

    xmlHTMLTableRow = com.bristle.jslib.XMLUtil.appendNamedNode
                                        (xmlHTMLTable, "tr");
    com.bristle.jslib.XMLUtil.appendNamedNodeContainingTextNode
                                        (xmlHTMLTableRow, "td", "A1A");
    com.bristle.jslib.XMLUtil.appendNamedNodeContainingTextNode
                                        (xmlHTMLTableRow, "td", "123456");
    com.bristle.jslib.XMLUtil.appendNamedNodeContainingTextNode
                                        (xmlHTMLTableRow, "td", "UK");
    com.bristle.jslib.XMLUtil.appendNamedNodeContainingTextNode
                                        (xmlHTMLTableRow, "td", "1,2,3");
    alert(xmldomHTML.xml);
}

/******************************************************************************
* Formats the fields of the specified XMLDOMParseError object as an
* HTML string for reporting to the user.
******************************************************************************/
com.bristle.jslib.XMLUtil.formatParseError =
function(xmlParseError)
{
    // Generate hyphens to indent pointer char (caret) to point to error.
    var strHyphens = "";
    for (var i = 1; i < xmlParseError.linepos; i++)
    {
        strHyphens += "-";
    }

    // Note:  Must use <xmp> in case xmlParseError.reason
    //        contains anything that looks like an XML/HTML
    //        tag.  Even <pre> does not escape the tags like
    //        <xmp> does.  Otherwise, the tag is stripped out
    //        of the text when displayed as HTML.
    var strRC = "<xmp>"
              + "  Error: " + xmlParseError.reason    + "\n"
              + "  Code:  " + xmlParseError.errorCode + "\n"
              + "  Line:  " + xmlParseError.line      + "\n"
              + "  Char:  " + xmlParseError.linepos   + "\n"
              + "  URL:   " + xmlParseError.url       + "\n"
              ;
    if (xmlParseError.line != 0)
    {
        strRC += xmlParseError.srcText + "\n"
              +  strHyphens + "^\n"
              ;
    }
    strRC += "</xmp>";

    return strRC;
}

/******************************************************************************
* Reports an XML parsing error message to the user.
******************************************************************************/
com.bristle.jslib.XMLUtil.reportParseError =
function(strMessage, xmlParseError)
{
    com.bristle.jslib.MsgBox.reportError
                (strMessage + "<br />" + 
                com.bristle.jslib.XMLUtil.formatParseError(xmlParseError));
}

/******************************************************************************
* Formats the fields of the specified server error (XML DOM
* containing error details) as an HTML string for reporting to the
* user.
******************************************************************************/
com.bristle.jslib.XMLUtil.formatServerError =
function(xmlError)
{
    // Note:  Must use <xmp> in case the error details contain
    //        anything that looks like an XML/HTML tag.  Even
    //        <pre> does not escape the tags like <xmp> does.
    //        Otherwise, the tag is stripped out of the text
    //        when displayed as HTML.
    //        ?? Unfortunately, <xmp> also prevents long lines 
    //        ?? from wrapping so the stack trace appears all on 
    //        ?? one long line.  Where did the line breaks get 
    //        ?? stripped out of the stack trace?
    //        ?? Possible answer is to search the string for any
    //        ?? troublesome chars and use <xmp> only when necessary.
    //        ?? Another possibility it to use <![CDATA[...]] to 
    //        ?? escape the troublesome chars.
    //        ?? Once we allow the long lines to wrap, we should use
    //        ?? a table to layout the Error, Source, Message, etc.
    var strRC =
           "<xmp>"
         + "Error:   "   + xmlError.selectSingleNode("Name").text    + "\n"
         + "Source:  "   + xmlError.selectSingleNode("Source").text  + "\n"
         + "Message: "   + xmlError.selectSingleNode("Message").text + "\n"
    if (xmlError.selectSingleNode("Note") != null)
    {
        strRC = strRC
         + "Note:    " + xmlError.selectSingleNode("Note").text      + "\n";
    }
    strRC = strRC
         + "Stack Trace:\n"
         +                 xmlError.selectSingleNode("StackTrace").text + "\n"
         + "</xmp>\n"
         ;
    return strRC;
}

/******************************************************************************
* Checks the specified XML DOM to see if it contains an error message
* from the server.  If so, it reports the error to the user and
* returns true.
******************************************************************************/
com.bristle.jslib.XMLUtil.checkForAndReportServerError =
function(strMessage, xmlDOM)
{
    var xmlError = xmlDOM.selectSingleNode("//Error");
    if (xmlError == null)
    {
        // No error found.
        return false;
    }
    else
    {
        // Error found.  Report it to the user.
        com.bristle.jslib.MsgBox.reportError
                (strMessage + "<br />" + 
                        com.bristle.jslib.XMLUtil.formatServerError(xmlError));
        return true;
    }
}

/******************************************************************************
* Formats the fields of the specified server debug info (XML DOM
* containing error details) as an HTML string for reporting to the
* user.
*
* Generates an HTML table like:
*
*             Delta   Used       Total
*  Thread     Time    Memory     Memory
*  Name       (sec)   (MB)       (MB)                Message
*  ---------  ------  ---------  ---------  ------------------------------
*  Thread-12  0       23.179104  27.770872  BEGIN getReport() 
*  Thread-12  0.001   23.213264  27.770872  . BEGIN getResultSet 
*  Thread-12  0.902   23.247248  27.770872  . END   getResultSet 
*  Thread-12  0.001   23.281744  27.770872  . BEGIN Generate XML 
*  Thread-12  0.09    23.356848  27.770872  . END   Generate XML 
*  Thread-12  0.001   23.391008  27.770872  . BEGIN cleanupDBContext
*  Thread-12  0.089   23.476144  27.770872  . END   cleanupDBContext
*  Thread-12  0.001   23.510888  27.770872  END   getReport() 
*  ---------  ------  ---------  ---------  ------------------------------
*  Thread-12  1.085   23.510888  27.770872  Total Time and Maximum Memory
*
* where:
*  - The last row shows the total of the delta times, and the max value
*    for the memory columns.
*  - The number of dots at the front of each message shows the logging
*    level of the message.
*
******************************************************************************/
com.bristle.jslib.XMLUtil.formatServerDebugInfo =
function(xmlDebug)
{
    var strRC = "\n<font size='4'>Server Log</font>"
         + "\n<table border='1'>"
         + "\n <thead>"
         + "\n  <tr>"
         + "\n   <th>Thread<br />Name</th>"
         + "\n   <th>Delta<br />Time<br />(sec)</th>"
         + "\n   <th>Used<br />Memory<br />(MB)</th>"
         + "\n   <th>Total<br />Memory<br />(MB)</th>"
         + "\n   <th>Message</th>"
         + "\n  </tr>"
         + "\n </thead>"
         ;
    var xmlLines = xmlDebug.selectNodes("Entry");
    var intStartTime = 0;
    var intTotalTime = 0;
    var intMaxUsed   = Number.MIN_VALUE; // First Math.max will be less.
    var intMaxTotal  = Number.MIN_VALUE; // First Math.max will be less.
    for (var xmlLine = xmlLines.nextNode();
         xmlLine;
         xmlLine = xmlLines.nextNode())
    {
        var strThreadName = xmlLine.selectSingleNode("ThreadName").text;
        var intTime       = xmlLine.selectSingleNode("Time").text;
        var intDeltaTime  = (intStartTime == 0) ? 0 : intTime - intStartTime;
            intStartTime  = intTime;
            intTotalTime += intDeltaTime;
        var intUsedMem    = xmlLine.selectSingleNode("UsedMem").text;
        var intTotalMem   = xmlLine.selectSingleNode("TotalMem").text;
            intMaxUsed    = Math.max (intMaxUsed,    intUsedMem);
            intMaxTotal   = Math.max (intMaxTotal,   intTotalMem);
        var intLevel      = xmlLine.selectSingleNode("Level").text;
        strRC = strRC         
         + "\n <tr>"
         + "\n  <td>" + strThreadName       +  "</td>"
         + "\n  <td>" + (intDeltaTime/1000) +  "</td>"
         + "\n  <td>" + (intUsedMem/1000000)   +  "</td>"
         + "\n  <td>" + (intTotalMem/1000000)  +  "</td>"
         + "\n  <td>";
        // Show dots to indicate nesting level of messages.
        for (var i = 1; i < intLevel; i++)
        {
            strRC += ". ";
        }
        strRC = strRC         
         +        xmlLine.selectSingleNode("Msg").text + "</td>";
    }
    strRC = strRC         
         + "\n <tr>"
         + "\n <tfoot>"
         + "\n  <tr>"
         + "\n   <td><b>" + strThreadName       +  "</b></td>"
         + "\n   <td><b>" + (intTotalTime/1000) +  "</b></td>"
         + "\n   <td><b>" + (intMaxUsed/1000000)   +  "</b></td>"
         + "\n   <td><b>" + (intMaxTotal/1000000)  +  "</b></td>"
         + "\n   <td><b>Total Time and Maximum Memory</b></td>"
         + "\n  </tr>"
         + "\n </tfoot>"
         + "\n</table>"
         ;
    return strRC;
}

/******************************************************************************
* Checks the specified XML DOM to see if it contains debug info from the
* the server.  If so, it reports the info to the user.
******************************************************************************/
com.bristle.jslib.XMLUtil.checkForAndReportServerDebugInfo =
function(xmlDOM)
{
    var xmlDebug = xmlDOM.selectSingleNode("/Report/Debug");
    if (xmlDebug == null)
    {
        // No debug info found.
        return false;
    }
    else
    {
        // Debug info found.  Report it to the user.
        com.bristle.jslib.MsgBox.reportPopupMessage
                (com.bristle.jslib.Util.formatWithBorder
                        (com.bristle.jslib.XMLUtil.formatServerDebugInfo
                                (xmlDebug)));
        return true;
    }
}

/******************************************************************************
* Load an XML stream from the specified URL into the specified DOM.
* Report any error to the user.  
* Also, search the successfully loaded XML for errors reported by the 
* server and show them to the user.
* Also, search the successfully loaded XML for logging info from the 
* server and show it to the user.
* Return false if unable to load the XML, or if the XML contained a 
* report of a server error; true otherwise.
******************************************************************************/
com.bristle.jslib.XMLUtil.loadDOMFromURL =
function(xmlDOM, strURL)
{
    // Load the DOM.
    xmlDOM.async = false;
    xmlDOM.load (strURL);

    // Test for invalid XML -- couldn't even be loaded.
    if (xmlDOM.parseError.errorCode != 0)
    {
        com.bristle.jslib.XMLUtil.reportParseError 
                 ("Error loading XML from: " + strURL, 
                  xmlDOM.parseError);
        return false;
    }

    // Test for XML that holds an error message from the server instead
    // of the expected data.
    if (com.bristle.jslib.XMLUtil.checkForAndReportServerError
                 ("Error reported by server via XML from: " + strURL, 
                  xmlDOM))
    {
        return false;
    }

    // Test for XML that contains debug info from the server, perhaps 
    // in addition to the expected data.  If so, display the debug info
    // in a separate window.
    com.bristle.jslib.XMLUtil.checkForAndReportServerDebugInfo(xmlDOM);

    // Normal successful return.
    return true;
}

/******************************************************************************
* Load an XML stream from the specified string into the specified DOM.
* Return true if successful; false otherwise.
******************************************************************************/
com.bristle.jslib.XMLUtil.loadDOMFromString =
function(xmlDOM, strXML)
{
    xmlDOM.async = false;
    xmlDOM.loadXML (strXML);
    return (xmlDOM.parseError.errorCode == 0);
}

/******************************************************************************
* Applies the specified XSL transformation to the specified XML DOM,
* leaving the original DOM untouched, and returning a new DOM.
* Can also safely be called to update a DOM in place as:
*          xmldomXML = com.bristle.jslib.XMLUtil.transformXML
*                                              (xmldomXML, xmldomXSL);
* unlike the similar call:
*          xmldomXML.transformNodeToObject(xmldomXSL, xmldomXML);
* which deletes the contents of xmldomXML.
******************************************************************************/
com.bristle.jslib.XMLUtil.transformXML =
function(xmldomXML, xmldomXSL)
{
    // Use the XSL to transform the XML.
    // Note:  If there is anything wrong with the XML,
    //        transformNodeToObject will raise an exception which is
    //        propagated to the caller.
    var xmldomNewXML = new ActiveXObject("msxml.DOMDocument");
    xmldomNewXML.async = false;
    xmldomXML.transformNodeToObject(xmldomXSL, xmldomNewXML);

    // Return the DOM of the transformed XML.
    return xmldomNewXML;
}

/******************************************************************************
* XSL Identity Transformation.  Used to copy XML streams unchanged.
******************************************************************************/
com.bristle.jslib.XMLUtil.strIdentityTransformationXSLTemplate =
        "\n  <!-- Identity transformation - Copy all XML constructs"
      + "\n       not mentioned more explicitly later in the same"
      + "\n       transformation.  -->"
      + "\n  <xsl:template>"
      + "\n   <xsl:copy>"
      // Note:  Can't add pi() to the following list.  Otherwise,
      //        the sort tries to preserve the processing
      //        instruction:  <?xml version='1.0'?>
      //        in the XML, and for some reason, inserts a line
      //        break before the close quote after 1.0.  This
      //        makes it invalid XML and further transforms fail.
      + "\n    <xsl:apply-templates select='@* | * | comment() | text()'/>"
      + "\n   </xsl:copy>"
      + "\n  </xsl:template>"
      ;

/******************************************************************************
* XSL Sorting Transformation  Used to sort XML streams.
* Usage:  Must set the VALUE_TO_BE_SET_LATER values before applying the 
*         transformation.  See the com.bristle.jslib.XMLUtil.sortXML() 
*         function for an example.
******************************************************************************/
com.bristle.jslib.XMLUtil.strSortingTransformationXSLTemplate =
           com.bristle.jslib.XMLUtil.strIdentityTransformationXSLTemplate
      + "\n  <!-- Sorting transformation -->"
      + "\n  <xsl:template match='VALUE_TO_BE_SET_LATER'>"
      + "\n   <xsl:copy>"
      + "\n    <xsl:apply-templates select='@*'/>"
      + "\n    <xsl:apply-templates select='*'"
      +        " order-by='VALUE_TO_BE_SET_LATER'/>"
      + "\n   </xsl:copy>"
      + "\n </xsl:template>"
      ;

/******************************************************************************
* XSL DOM used to sort XML streams.
******************************************************************************/
com.bristle.jslib.XMLUtil.strSortXSL =
        "<?xml version='1.0' ?>"
      + "\n<xsl:stylesheet xmlns:xsl='http://www.w3.org/TR/WD-xsl'>"
      +  strSortingTransformationXSLTemplate
      + "\n</xsl:stylesheet>"
      ;
com.bristle.jslib.XMLUtil.xmldomSortXSL = new ActiveXObject("msxml.DOMDocument");
if (!com.bristle.jslib.XMLUtil.loadDOMFromString 
        (com.bristle.jslib.XMLUtil.xmldomSortXSL, 
         com.bristle.jslib.XMLUtil.strSortXSL))
{
    com.bristle.jslib.XMLUtil.reportParseError 
            ("Internal error:  Unable to load sort XSL into DOM", 
             com.bristle.jslib.XMLUtil.xmldomSortXSL.parseError);
}

/******************************************************************************
* Sort the specified node list of the specified XML by the specified sort key.
******************************************************************************/
com.bristle.jslib.XMLUtil.sortXML =
function(xmldomXML, strNodeListToSort, strSortKey)
{
    // Insert the specified node list name and sort key into the XSL DOM.
    com.bristle.jslib.XMLUtil.xmldomSortXSL.selectSingleNode
                        ("//*/@match").nodeValue = strNodeListToSort;
    com.bristle.jslib.XMLUtil.xmldomSortXSL.selectSingleNode
                        ("//*/@order-by").nodeValue = strSortKey;

    // Use the XSL to sort the XML.
    // Note:  If there is anything wrong with the XML,
    //        com.bristle.jslib.XMLUtil.transformXML will raise an 
    //        exception which is propagated to the caller.
    return com.bristle.jslib.XMLUtil.transformXML
                        (xmldomXML, com.bristle.jslib.XMLUtil.xmldomSortXSL);
}

/******************************************************************************
* Load the values of all nodes with the specified name from the
* specified DOM into the specified select box, preceded by a default
* entry with the specified display string and value, optionally
* skipping consecutive duplicate display values.
*
*@param xmlDOM              XML DOM to get values from
*@param sel                 HTML SELECT to load values into
*@param strNodeName         Name of XML node containing display value
*@param strDefaultText      String to display as default value.
*@param strDefaultValue     String to use as default coded ID value.
*@param blnSkipConsecutiveDuplicates
*                           Optional.  Default:  False
*                           Flag specifying whether to omit display values
*                           that are identical to the immediately preceding
*                           display value in the SELECT.
*@param strIDNodeName       Optional.  Default:  Same as strNodeName
*                           Name of XML node containing coded ID value
*@param strAdditionalText   Optional.  String to display as additional 
*                           value, after the default value and before the
*                           the values from the XML.  
*                           Default: No additional value displayed.
*@param strAdditionalValue  Optional.  String to use as additional coded 
*                           ID value.
*                           Default: No additional value displayed.
* Notes:
*  - Filtering of duplicates is intentionally done based on the display
*    values, not the coded ID values.  There is little reason to show the
*    user multiple identical choices and expect him to intelligently
*    choose between them.  However, it is useful to present the user
*    with multiple (apparently different) choices that all map to the
*    same coded ID value behind the scenes.  This sort of aliasing is
*    useful when different users use different names for the same value,
*    or when the name of a value is changed but both names must be
*    supported during the transition.
* Anticipated Changes:
*  - Currently, we preload up to two values other than the contents of
*    the DOM -- the default value and one additional value.  Might be 
*    better to offer a choice to not clear the select box before loading
*    the items from the DOM.  Then the caller could pre-load as many 
*    items as desired.
******************************************************************************/
com.bristle.jslib.XMLUtil.blnSKIP_CONSECUTIVE_DUPLICATES = true;
com.bristle.jslib.XMLUtil.blnSHOW_CONSECUTIVE_DUPLICATES = false;
com.bristle.jslib.XMLUtil.loadSelectBoxFromDOM =
function(xmlDOM,
         sel,
         strNodeName,
         strDefaultText,
         strDefaultValue,
         blnSkipConsecutiveDuplicates,
         strIDNodeName,
         strAdditionalText,
         strAdditionalValue)
{
    com.bristle.jslib.ListBox.clearSelectBox(sel);
    if (strDefaultText != "")
    {
        // Note:  Don't bother scrolling into view, since we just 
        //        cleared the select box.  The first item added is 
        //        already visible.
        com.bristle.jslib.ListBox.addToSelectBoxAndSelect 
                    (sel, 
                     strDefaultText, 
                     strDefaultValue,
                     !com.bristle.jslib.ListBox.blnSCROLL_INTO_VIEW);
    }
    if (   (typeof (strAdditionalText) != "undefined")
        && (strAdditionalText != "")
        && (typeof (strAdditionalValue) != "undefined")
        && (strAdditionalValue != "")
       )
    {
        com.bristle.jslib.ListBox.addToSelectBox 
                (sel, strAdditionalText, strAdditionalValue);
    }
    // Note:  The ".//xxx" pattern searches the entire tree
    //        pointed to by xmlDOM for xxx elements.
    var xmlItems = xmlDOM.selectNodes(".//" + strNodeName);
    var strPreviousDisplayValue = strDefaultText;
    for (var xmlItem = xmlItems.nextNode();
         xmlItem;
         xmlItem = xmlItems.nextNode())
    {
        var strDisplayValue = xmlItem.text;
        var strID = strDisplayValue;  // For now.
        // Don't bother adding blank display values, and don't add 
        // consecutive duplicates if told not to.
        if (   (strDisplayValue != "")
            && (   !blnSkipConsecutiveDuplicates
                || (strDisplayValue != strPreviousDisplayValue)
               )
           )
        {
            if (   (typeof (strIDNodeName) != "undefined")
                && (strIDNodeName != ""))
            {
                // Get the ID value from the specified sibling node of
                // the display value.
                strID = xmlItem.selectSingleNode("../" + strIDNodeName).text;
            }
            com.bristle.jslib.ListBox.addToSelectBox 
                        (sel, strDisplayValue, strID);
            strPreviousDisplayValue = strDisplayValue;
        }
    }
}


