//*****************************************************************************
//     FILE: SoapClient.js
//  PURPOSE: Soap client.  Provides proxy methods for calling RtWP update and
//           for calling web service methods in general.
//
//===============================================================================
//  Copyright (c) 2004 OSIsoft, Inc. All rights reserved.  Unpublished - rights
//  reserved under the copyright law of the United States.  USE OF A COPYRIGHT
//  NOTICE IS PRECAUTIONARY ONLY AND DOES NOT IMPLY PUBLICATION OR DISCLOSURE.
//
//  THIS SOFTWARE CONTAINS CONFIDENTIAL INFORMATION AND TRADE SECRETS OF OSIsoft,
//  Inc.  USE, DISCLOSURE, OR REPRODUCTION IS PROHIBITED WITHOUT THE PRIOR EXPRESS
//  WRITTEN PERMISSION OF OSIsoft, Inc.
//
//  RESTRICTED RIGHTS LEGEND
//  Use, duplication, or disclosure by the Government is subject to restrictions as
//  set forth in subparagraph (c)(1)(ii) of the Rights in Technical Data and
//  Computer Software clause at DFARS 252.227.7013
//
//  OSIsoft, Inc.
//  777 Davis St., San Leandro CA 94577
//
//  Portions copyright (c) 2004 Omicron Consulting, Philadelphia, PA
//*****************************************************************************
//==============================================================================
//    CLASS: SoapClient()
//  PURPOSE: Contains method definitions and helper functions for making SOAP
//           calls.
//==============================================================================
function SoapClient(url, wpq) {
  var op;
  var xmlDoc;
  var t = this;
  this.ns       = new Array();
  this.soapmsg1 = new Array();
  this.soapmsg2 = new Array();
  this.parts    = new Array();
  this.types    = new Array();
  this.actn     = new Array();
  this.rsptypes = new Array();
  this.url      = url;
  this.wpq      = wpq;

  
  function unl() {
   // Mem leak fix - if xmlhttprequest object is not null, delete it
   if (t.xml != null) {
     t.xml.abort();
     t.xml = null;
   }
  }
  try {                                                        
    if (WPSC) {
      WPSC.RegisterForEvent("urn:schemas-microsoft-com:dhtml","onunload",unl);
    }
  }
  catch (e) { }

  this.createMSXML();

  xmlDoc    = new ActiveXObject("MSXML2.DOMDocument");
  
  this.invalid  = false;
  if (xmlDoc == null) {
    AddToRtWPErrors
      (this.wpq,
       "<label class='ms-error'>" + errMSXML + " " + this.wpq + ".</label>");
    this.invalid = true;
    return;  
  }

  try {
    this.xml.open("GET",url + "?wsdl",false);
    this.xml.send();
    xmlDoc.setProperty("SelectionNamespaces","xmlns:s=\"http://www.w3.org/2001/XMLSchema\"");
    xmlDoc.setProperty("SelectionNamespaces","xmlns:wsdl=\"http://schemas.xmlsoap.org/wsdl/\"");
    xmlDoc.loadXML(this.xml.responseText);
  }
  catch (e) {
    AddToRtWPErrors
      (this.wpq,
       "<label class='ms-error'>" + errWSDLException + " " + this.wpq + ".</label><br>" + 
       "<label class='ms-error'>" + e.description + "</label>");
    this.invalid = true;
    return;
  }

  var attr;
  var pfx = "";
  try {
    opnodes = xmlDoc.selectNodes("//" + pfx + "definitions/" + pfx + "binding/" + pfx + "operation");
    if (opnodes.length == 0) {
      pfx = "wsdl:";
      opnodes = xmlDoc.selectNodes("//" + pfx + "definitions/" + pfx + "binding/" + pfx + "operation");
    }

    //-----Loop through all operations to determine parameters and response types.
    if (opnodes.length == 0) {
      AddToRtWPErrors
        (this.wpq,
         "<label class='ms-error'>" + errWSDLNotRetrieved + " " + this.wpq + ".</label><br>");
      this.invalid = true;
      return;
    }
    for (j=0; j < opnodes.length; j++) {
      op = opnodes.item(j).getAttribute("name");
      method = xmlDoc.documentElement.selectSingleNode("//" + pfx + "definitions/" + pfx + "types/s:schema/s:element"
          + "[\@name=\"" + op + "\"]");

      if (method == null)
        continue;

      mthparams = method.selectNodes("s:complexType/s:sequence/s:element");
      if (mthparams == null)
        continue;

      rsp = xmlDoc.documentElement.selectSingleNode("//" + pfx + "definitions/" + pfx + "types/s:schema/s:element"
          + "[\@name=\"" + op + "Response\"]");
      if (rsp == null)
        continue;

      rsptype = rsp.selectSingleNode("s:complexType/s:sequence/s:element");
      if (rsptype == null)
        continue;

      actnnd = opnodes.item(j).selectSingleNode("soap:operation");
      if (actnnd == null)
        continue;

      this.actn[op]     = actnnd.getAttribute("soapAction");
      this.ns[op]       = method.parentNode.getAttribute("targetNamespace");
      this.soapmsg1[op] = '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>';
      this.soapmsg1[op] += '<soap:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">';
      this.soapmsg1[op] += ' <soap:Body>';

      this.parts[op] = new Array(mthparams.length);
      this.types[op] = new Array(mthparams.length);

      //-----Loop through parameters to get their names and types.
      for (k = 0; k < mthparams.length; k++) {
        this.parts[op][k] = mthparams.item(k).getAttribute("name");
        this.types[op][k] = mthparams.item(k).getAttribute("type");
      }

      this.soapmsg1[op] += '<' + op + ' xmlns="' + this.ns[op] + '"' + '>';
      this.soapmsg2[op] = '</' + op + '>';
      this.soapmsg2[op] += '</soap:Body>';
      this.soapmsg2[op] += '</soap:Envelope>';

      this.rsptypes[op + "Response"] = rsptype.getAttribute("type");
    }
  }
  catch (e) {
    AddToRtWPErrors
      (this.wpq,
       "<label class='ms-error'>" + errWSDLException + " " + this.wpq + ".</label><br>" + 
       "<label class='ms-error'>" + e.description  + "</label><br>");
    this.invalid = true;
    return;
  }

  delete xmlDoc;
}

//==============================================================================
//   METHOD: SoapClient::callDirect()
//  PURPOSE: Call named method with a parameter array passed in.
//    INPUT: mth           : method name to call
//         : mthparams     : array of arguments to pass
//         : soapCallback  : address of callback function
//==============================================================================
function callDirect(mth, mthparams, soapCallback) {
  var op          = arguments[0];
  var parms       = this.parts[mth];
  var types       = this.types[mth];
  var soapmsg     = this.soapmsg1[mth];

  for (i = 0; i < parms.length; i++) {
    soapmsg += '<' + parms[i] + '>';

    if (mthparams[i] != null) {
      //-----If argument is a string array, then loop through each element and
      //     add it; otherwise, add the single argument.
      if ((types[i] == "s0:ArrayOfString") || (types[i] == "tns:ArrayOfString")) {
        for (var j = 0; j < mthparams[i].length; j++) {
          soapmsg += '<string><![CDATA[' + mthparams[i][j] + ']]></string>';
        }
      }
      else {
        soapmsg += '<![CDATA[' + mthparams[i] + ']]>';
      }
    }

    soapmsg += '</' + parms[i] + '>';
  }

  this.callBack     = arguments[arguments.length - 1];
  this.callBackType = this.rsptypes[mth + "Response"];
  this.callBackMth  = mth;
  soapmsg          += this.soapmsg2[op];

  this.doCall(op, soapmsg);
}

//==============================================================================
//   METHOD: SoapClient::callNormal()
//  PURPOSE: Call
//    INPUT: mth           : method name to call
//         : soapCallback  : address of callback function
//         : [2...n]       : optional arguments to pass
//==============================================================================
function callNormal(mth, soapCallback) {
  var op      = arguments[0];
  var parms   = this.parts[mth];
  var types   = this.types[mth];
  var soapmsg = this.soapmsg1[mth];

  //-----Loop through parameters and build them into the SOAP message.
  for (i = 0; i < parms.length; i++) {
    soapmsg += '<' + parms[i] + '>';

    //-----If argument is a string array, then loop through each element and
    //     add it; otherwise, add the single argument.
    if ((types[i] == "s0:ArrayOfString") || (types[i] == "tns:ArrayOfString")) {
      for (var j = 0; j < arguments[i + 2].length; j++) {
        soapmsg += '<string><![CDATA[' + arguments[i + 2][j] + ']]></string>';
      }
    }
    else {
      soapmsg += '<![CDATA[' + arguments[i + 2] + ']]>';
    }

    soapmsg += '</' + parms[i] + '>';
  }

  this.callBack     = arguments[1];
  this.callBackType = this.rsptypes[mth + "Response"];
  this.callBackMth  = mth;
  soapmsg          += this.soapmsg2[op];

  this.doCall(op, soapmsg);
}

//==============================================================================
//   METHOD: SoapClient::callMethod()
//  PURPOSE: Call method to forward a call to an assembly and class name on
//           server.  Used typically for updates.
//    INPUT: mth           : method name to call
//         : assembly      : assembly name to load
//         : className     : class name to use (NOTE:  the class's method name
//                           is not specified; the web service method that is
//                           called here is predefined to call the appropriate
//                           method)
//         : WPQ           : unique web part identifier
//         : regSettings   : XML document decribing current regional settings
//         : lcid          : current locale
//         : mthparams     : array of arguments to pass
//         : soapCallback  : address of callback function
//==============================================================================
function callMethod(mth, assembly, className, WPQ, regSettings, lcid, langID, mthparams, soapCallback) {
  var op      = arguments[0];
  var parms   = this.parts[mth];
  var types   = this.types[mth];
  var soapmsg = this.soapmsg1[mth];

  //-----Loop through parameters and build them into the SOAP message.
  for (i = 0; i < parms.length; i++) {
    soapmsg += '<' + parms[i] + '>';

    //-----This code is not really correct because it ignores the method name and
    //     loops through the whole mthparams array instead of the array at the
    //     current element of mthparams; it works because the UPDATE method is
    //     always called and is defined to take only a single array of strings.
    if ((types[i] == "s0:ArrayOfString") || (types[i] == "tns:ArrayOfString")) {
      for (var j = 0; j < mthparams.length; j++) {
        soapmsg += '<string><![CDATA[' + mthparams[j] + ']]></string>';
      }
    }
    else {
      soapmsg += '<![CDATA[' + arguments[i + 1] + ']]>';
    }

    soapmsg += '</' + parms[i] + '>';
  }

  this.callBack     = arguments[arguments.length - 1];
  this.callBackType = this.rsptypes[mth + "Response"];
  this.callBackMth  = mth;
  soapmsg          += this.soapmsg2[op];

  this.doCall(op, soapmsg);
}

// used to govern acceptable level of recursion when parsing reply from web service
var maxStackDepth = 100;

function _findRHE(rslt,pos,rhepos,lvl) {
		  
	   if (lvl > maxStackDepth){
			rhepos = "PARSEERROR";
			return rhepos;
		}
	   
	   try {
		pos = rslt.indexOf("string>",pos);
		if (rslt.charAt(pos - 1) == '/') {
       		if (lvl == 0) {
       			rhepos = pos - 2;
       			return rhepos;
       		}
       		--lvl;
       		rhepos = _findRHE(rslt,pos + 7,rhepos,lvl);
		}
		else {
       		lvl++;
       		if (lvl > maxStackDepth){
       			rhepos = "PARSEERROR";
       			return rhepos;
       		}
       		rhepos = _findRHE(rslt,pos + 7,rhepos,lvl);
		}
	  }
	  catch(ex){
		lvl == 99;
		return rhepos;
	  }
       return rhepos;
}

//==============================================================================
//    EVENT: SoapClient::_handleReadyState()
//  PURPOSE: Fired when the XMLHTTP request's ready state changes.  Fires the
//           previously specified SOAP callback function when the ready state
//           reaches 4.
//==============================================================================
function _handleReadyState() {

  // Mem Leak Fix - check for a null xmlhttprequest object
  if (this.xml == null) return;

  var wpq = this.wpq;
  var lvl = 0;
  var rhepos;

  if (this.xml.readyState == 4) {
    var rslt = this.xml.responseText;

    //-----Replace character escapes with the actual characters.  Note that
    //     ampersands should be unescaped LAST, so that other, doubly-escaped
    //     entities are not unescaped prematurely.
    rslt = rslt.replace(/&lt;/g,   '<');
    rslt = rslt.replace(/&gt;/g,   '>');
    rslt = rslt.replace(/&apos;/g, "'");
    rslt = rslt.replace(/&quot;/g, '"');
    rslt = rslt.replace(/&amp;/g,  '&');

    pos = 0;
    if ((this.callBackType == "s0:ArrayOfString") || (this.callBackType == "tns:ArrayOfString")) {
    	var strs = new Array();
    	var i = 0;
    	pos = rslt.indexOf("<string>",pos);
    	while (pos != -1) {
    		pos += 8;
    		rhepos = _findRHE(rslt,pos,rhepos,lvl);
    		if (rhepos == "PARSEERROR"){
    			strs = null;
    			break;    			
    		}    		
    		strs[i++] = rslt.substring(pos,rhepos);
    		pos = rslt.indexOf("<string>",rhepos);
    	}	// end of while loop
    	
    	rslt = strs;
    }
    else {
     pos = rslt.indexOf("Result>");
     rslt = rslt.substring(pos + 7,rslt.indexOf("</" + this.callBackMth + "Result",pos + 7))
    }
    
    this.callBack(rslt);
    if (rhepos == "PARSEERROR") {
    				AddToRtWPErrors(wpq,"<label class='ms-error'>" + parseResponseErrorMessage + ".</label><br>");
    }

  }
}

//==============================================================================
//    METHOD: SoapClient::_doCall()
//   PURPOSE: Calls the specified method and action.  Private function.
//     INPUT: strOp         : method name to call
//          : strMessage    : constructed SOAP message to pass
// CALLED BY: callNormal
//            callMethod
//            callDirect
//==============================================================================
function _doCall(strOp, strMessage) {
    
  if (this.xml == null)
  {
    this.createMSXML();
  }
    
  var objThis = this;
  var load    = false;

  //-----FUNCTION:  OnReadyStateChange().  Nested functions have access to their
  //     parent's local variables.  Since we cannot directly fire an object's
  //     method from a page-level event, we use the inner function to pass the
  //     event on to the appropriate object.
  function OnReadyStateChange() {
    objThis.onreadystatechange();
  }

  try {
    this.xml.open("POST", this.url, true);
  }
  catch (e) {
    alert(e.description);
  }

  this.xml.onreadystatechange = OnReadyStateChange;
  this.xml.setRequestHeader("SOAPAction",    '"' + this.actn[strOp] + '"');
  this.xml.setRequestHeader("Content-Type",  'text/xml; charset="UTF-8"');
  this.xml.setRequestHeader("Content-Length", strMessage.length);

  var retries = 3;
  while ((!load) && (retries != 0)) {
    try {
      this.xml.send(strMessage);
      load = true;
    }
    catch (er) {
      this.xml.open("POST", this.url, true);
      this.xml.onreadystatechange = OnReadyStateChange;
      this.xml.setRequestHeader("SOAPAction",    '"' + this.actn[strOp] + '"');
      this.xml.setRequestHeader("Content-Type",  'text/xml; charset="UTF-8"');
      this.xml.setRequestHeader("Content-Length", strMessage.length);
      --retries;
    }
  }
  if (!load) {
    AddToRtWPErrors
      (this.wpq,
       "<label class='ms-error'>" + errSOAPFailed + " " + this.wpq + ".</label><br>" +
       "<label class='ms-error'>" + xml.statusText + "</label>");
    this.invalid = true;
  }
}

function xcallDirect(mth, mthparams, soapCallback) {
  window.status = 'WARNING:  SoapClient.CallDirect is obsolete -- use SoapClient.callDirect() (camelCase)';
  this.callDirect(mth, mthparams, soapCallback);
}

function _createMSXML()
{
  try {
    this.xml      = new ActiveXObject("MSXML2.XMLHTTP.6.0");
  }
  catch (e)
  {
    try {
      this.xml      = new ActiveXObject("MSXML2.XMLHTTP.4.0");
    }
    catch (e) {
      this.xml      = new ActiveXObject("MSXML2.XMLHTTP");
    }
  }
}

SoapClient.prototype.call               = callMethod;
SoapClient.prototype.callNormal         = callNormal;
SoapClient.prototype.callDirect         = callDirect;
SoapClient.prototype.CallDirect         = xcallDirect;
SoapClient.prototype.onreadystatechange = _handleReadyState;
SoapClient.prototype.doCall             = _doCall;
SoapClient.prototype.createMSXML        = _createMSXML;