//
// Jala Project [http://opensvn.csie.org/traccgi/jala]
//
// Copyright 2004 ORF Online und Teletext GmbH
//
// Licensed under the Apache License, Version 2.0 (the ``License'');
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an ``AS IS'' BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// $Revision$
// $LastChangedBy$
// $LastChangedDate$
// $HeadURL$
//


/**
 * @fileoverview Fields and methods of the jala.XmlRpcRequest class.
 */


// Define the global namespace for Jala modules
if (!global.jala) {
   global.jala = {};
}


/**
 * A constructor for XmlRpc request objects
 * @class Instances of this class provide the necessary functionality
 * for issueing XmlRpc requests to a remote service.
 * @param {String} url The url of the XmlRpc entry point
 * @param {String} methodName The name of the method to call
 * @returns A newly created jala.XmlRpcRequest instance
 * @constructor
 */
jala.XmlRpcRequest = function(url, methodName) {
   /** @ignore */
   var proxy = null;
   /** @ignore */
   var timeout = {
      "connect": 0,
      "socket": 0
   };
   /** @ignore */
   var debug = false;
   /** @ignore */
   var credentials = null;
   // default input and output encoding
   /** @ignore */
   var inputEncoding = "UTF-8";
   /** @ignore */
   var outputEncoding = "UTF-8";

   /**
    * Returns the URL of this request
    * @returns The URL of this request
    * @type java.net.URL
    */
   this.getUrl = function() {
      return new java.net.URL(url);
   };

   /**
    * Sets the proxy host and port. For Java runtimes < 1.5 this method
    * sets the appropriate system properties (so this has an effect on
    * all requests based on java.net.URL), for all others the proxy
    * is only set for this request.
    * @param {String} proxyString The proxy string in the form 'fqdn:port'
    * (eg. my.proxy.com:3128)
    */
   this.setProxy = function(proxyString) {
      if (proxyString && proxyString.trim()) {
         var idx = proxyString.indexOf(":");
         if (idx > 0) {
            var host = proxyString.substring(0, idx);
            var port = proxyString.substring(idx+1);
            if (host != null && port != null) {
               if (java.lang.Class.forName("java.net.Proxy") != null) {
                  // construct a proxy instance
                  var socket = new java.net.InetSocketAddress(host, port);
                  proxy = new java.net.Proxy(java.net.Proxy.Type.HTTP, socket);
               } else {
                  // the pre jdk1.5 way: set the system properties
                  var sys = java.lang.System.getProperties();
                  if (host) {
                     app.log("[Jala XmlRpc Client] WARNING: setting system http proxy to "
                             + host + ":" + port);
                     sys.put("http.proxySet", "true");
                     sys.put("http.proxyHost", host);
                     sys.put("http.proxyPort", port);
                  }
               }
            }
         }
      }
      return;
   };

   /**
    * Returns the proxy object. This method will only return
    * a value if using a java runtime > 1.5
    * @returns The proxy to use for this request
    * @type java.net.Proxy
    * @see #setProxy
    */
   this.getProxy = function() {
      return proxy;
   };
   
   /**
    * Sets the credentials for basic http authentication to
    * use with this request.
    * @param {String} username The username
    * @param {String} password The password
    */
   this.setCredentials = function(username, password) {
      var str = username + ":" + password;
      credentials = str.enbase64();
      return;
   };

   /**
    * Returns the credentials of this request
    * @returns The base46 encoded credentials of this request
    * @type String
    */
   this.getCredentials = function() {
      return credentials;
   };

   /**
    * Sets the connection timeout to the specified milliseconds.
    * @param {Number} millis The timeout to use as connection timeout
    */
   this.setTimeout = function(millis) {
      timeout.connect = millis;
      return;
   };

   /**
    * Sets the socket timeout to the specified milliseconds.
    * @param {Number} millis The timeout to use as socket timeout
    */
   this.setReadTimeout = function(millis) {
      timeout.socket = millis;
      return;
   };

   /**
    * Returns the connection timeout of this request
    * @returns The connection timeout value in milliseconds
    * @type Number
    */
   this.getTimeout = function() {
      return timeout.connect;
   };

   /**
    * Returns the socket timeout of this request
    * @returns The socket timeout value in milliseconds
    * @type Number
    */
   this.getReadTimeout = function() {
      return timeout.socket;
   };

   /**
    * Returns the name of the remote function to call
    * @returns The name of the remote function
    * @type String
    */
   this.getMethodName = function() {
      return methodName;
   };

   /**
    * Sets both input and output encoding to the
    * specified encoding string
    * @param {String} enc The encoding to use for
    * both input and output. This must be a valid
    * java encoding string.
    */
   this.setEncoding = function(enc) {
      inputEncoding = enc;
      outputEncoding = enc;
      return;
   };

   /**
    * Sets the input encoding to the specified encoding string
    * @param {String} enc The encoding to use for input. This must be a valid
    * java encoding string.
    */
   this.setInputEncoding = function(enc) {
      inputEncoding = enc;
      return;
   };

   /**
    * Sets the output encoding to the specified encoding string
    * @param {String} enc The encoding to use for output. This must be a valid
    * java encoding string.
    */
   this.setOutputEncoding = function(enc) {
      outputEncoding = enc;
      return;
   };

   /**
    * Returns the input encoding
    * @returns The input encoding used by this request
    * @type String
    */
   this.getInputEncoding = function() {
      return inputEncoding;
   };

   /**
    * Returns the output encoding
    * @returns The output encoding used by this request
    * @type String
    */
   this.getOutputEncoding = function() {
      return outputEncoding;
   };

   /**
    * Enables or disables the debug mode. If enabled the xml source
    * of both request and response is included in the result properties
    * 'requestXml' and 'responseXml'
    * @param {Boolean} flag True or false.
    */
   this.setDebug = function(flag) {
      debug = flag;
      return;
   };

   /**
    * Returns true if debug is enabled for this request, false otherwise
    * @returns True if debugging is enabled, false otherwise
    * @type Boolean
    */
   this.debug = function() {
      return debug == true;
   };

   return this;
};

/** @ignore */
jala.XmlRpcRequest.prototype.toString = function() {
   return "[Jala XmlRpc Request]";
};

/**
 * Calling this method executes the remote method using
 * the arguments specified.
 * @returns The result of this XmlRpc request
 * @type Object
 */
jala.XmlRpcRequest.prototype.execute = function(/** [arg1][, arg2][, ...] */) {
   // if in debug mode, log the time the request took to event log
   if (app.__app__.debug() == true) {
      var start = new Date();
   }

   var tz = java.util.TimeZone.getDefault();
   var reqProcessor = new Packages.org.apache.xmlrpc.XmlRpcClientRequestProcessor(tz);
   var resProcessor = new Packages.org.apache.xmlrpc.XmlRpcClientResponseProcessor(tz);
   // create the result object
   var result = {
      error: null,
      result: null,
      requestXml: null,
      responseXml: null
   };

   // convert arguments into their appropriate java representations
   var params = new java.util.Vector();
   for (var i=0;i<arguments.length;i++) {
      params.add(jala.XmlRpcRequest.convertArgument(arguments[i]));
   }

   var r = new Packages.org.apache.xmlrpc.XmlRpcRequest(this.getMethodName(), params);
   var requestBytes = reqProcessor.encodeRequestBytes(r, this.getOutputEncoding());
   if (this.debug() == true) {
      result.requestXml = String(new java.lang.String(requestBytes));
   }
   // open the connection
   var proxy = this.getProxy();
   var url = this.getUrl();
   var conn = proxy ? url.openConnection(proxy) : url.openConnection();
   conn.setAllowUserInteraction(false);
   conn.setRequestMethod("POST");
   conn.setRequestProperty("User-Agent", "Jala XmlRpc Client");
   conn.setRequestProperty("Content-Type", "text/xml");
   conn.setRequestProperty("Content-Length", requestBytes.length);
   // set timeouts if defined and possible
   if (parseFloat(java.lang.System.getProperty("java.specification.version"), 10) >= 1.5) {
      conn.setConnectTimeout(this.getTimeout());
      conn.setReadTimeout(this.getReadTimeout());
   } else {
      app.logger.debug("WARNING: timeouts can only be using a Java runtime >= 1.5");
   }
   // set authentication credentials if defined
   if (this.getCredentials() != null) {
      conn.setRequestProperty("Authorization", "Basic " + this.getCredentials());
   }

   try {
      conn.setDoOutput(true);
      var outStream = conn.getOutputStream();
      outStream["write(byte[])"](requestBytes);
      outStream.flush();
      outStream.close();
      
      if (conn.getContentLength() > 0) {
         var inStream = conn.getInputStream();
         if (this.debug() == true) {
            inStream = new java.io.BufferedInputStream(conn.getInputStream());
            var outStream = new java.io.ByteArrayOutputStream();
            var buf = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 1024);
            var bytes;
            while ((bytes = inStream.read(buf)) > -1) {
                outStream.write(buf, 0, bytes);
            }
            result.responseXml = outStream.toString(this.getInputEncoding());
            inStream.close();
            // change the inStream and don't set the input encoding of
            // the response processor, since the conversion already happened above
            inStream = new java.io.ByteArrayInputStream(outStream.toByteArray());
         }
         resProcessor.setInputEncoding(this.getInputEncoding());
         var parsedResult = resProcessor.decodeResponse(inStream);
         if (parsedResult instanceof java.lang.Exception) {
            result.error = parsedResult;
         } else {
            result.result = jala.XmlRpcRequest.convertResult(parsedResult);
         }
      }
   } catch (e) {
      result.error = "[Jala XmlRpc Request] Error executing " + this.getMethodName()
                     + " with arguments " + jala.XmlRpcRequest.argumentsToString(arguments)
                     + ", the error is: " + e.toString();
   }
   if (app.__app__.debug() == true) {
      app.logger.debug("[Jala XmlRpc Request] (" + ((new Date()) - start) + " ms): executed '"
                       + this.getMethodName() + "' with arguments: "
                       + jala.XmlRpcRequest.argumentsToString(arguments));
   }
   return result;
};

/**
 * Helper method for converting a Javascript object into
 * its appropriate Java object.
 * @param {Object} obj The Javascript object to convert
 * @returns The appropriate Java representation of the object
 * @type java.lang.Object
 */
jala.XmlRpcRequest.convertArgument = function(obj) {
   var result;
   if (obj instanceof Array) {
      // convert into Vector
      result = new java.util.Vector(obj.length);
      for (var i=0;i<obj.length;i++) {
         result.add(i, jala.XmlRpcRequest.convertArgument(obj[i]));
      }
   } else if (obj instanceof Date) {
      // convert into java.util.Date
      result = new java.util.Date(obj.getTime());
   } else if (typeof(obj) == "boolean" || obj instanceof Boolean) {
      result = obj;
   } else if (typeof(obj) == "string" || obj instanceof String) {
      result = obj;
   } else if (isNaN(obj) == false) {
      // convert into Integer or Double
      if (obj - obj.toFixed() > 0) {
         result = new java.lang.Double(obj);
      } else {
         result = new java.lang.Integer(obj);
      }
   } else if (obj instanceof Object) {
      // convert into Hashtable
      result = new java.util.Hashtable();
      for (var key in obj) {
         if (obj[key] != null) {
            result.put(key, jala.XmlRpcRequest.convertArgument(obj[key]));
         }
      }
   } else {
      result = obj;
   }
   return result;
};

/**
 * Converts a Java object into its appropriate Javascript representation.
 * @param {java.lang.Object} obj The Java object to convert
 * @returns The appropriate Javascript representation of the Java object
 * @type Object
 */
jala.XmlRpcRequest.convertResult = function(obj) {
   var result;
   if (obj instanceof java.util.Vector) {
      // convert into Array
      result = [];
      var e = obj.elements();
      while (e.hasMoreElements()) {
         result.push(jala.XmlRpcRequest.convertResult(e.nextElement()));
      }
   } else if (obj instanceof java.util.Hashtable) {
      // convert into Object
      result = {};
      var e = obj.keys();
      var key;
      while (e.hasMoreElements()) {
         key = e.nextElement();
         result[key] = jala.XmlRpcRequest.convertResult(obj.get(key));
      }
   } else if (obj instanceof java.lang.String) {
      result = String(obj);
   } else if (obj instanceof java.lang.Number) {
      result = Number(obj);
   } else if (obj instanceof java.lang.Boolean) {
      result = Boolean(obj);
   } else if (obj instanceof java.lang.Date) {
      result = new Date(obj.getTime());
   } else {
      result = obj;
   }
   return result;
};

/**
 * Helper method to format an arguments array into
 * a string useable for debugging output.
 * @param {Object} args An arguments array
 * @returns The arguments array formatted as string
 * @type String
 */
jala.XmlRpcRequest.argumentsToString = function(args) {
   var arr = [];
   for (var i=0;i<args.length;i++) {
      if (typeof(args[i]) == "string") {
         arr.push('"' + args[i] + '"');
      } else {
         arr.push(args[i]);
      }
   }
   return arr.join(", ");
};