/*
* Copyright (C) 2011 Develnix.com
*
* 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.
*
* This code has been originally taken from JSON/XML-RPC Client
* .
*
* It has been modified to support only JSON-RPC
*
* JSON/XML-RPC Client
* Version: 0.8.0.2 (2007-12-06)
* Copyright: 2007, Weston Ruter
* License: Dual licensed under MIT
* and GPL licenses.
*
* Original inspiration for the design of this implementation is from jsolait, from which
* are taken the "ServiceProxy" name and the interface for synchronous method calls.
*/
var JsonRpc = {
version:"1.0.0.1",
requestCount: 0
};
JsonRpc.ServiceProxy = function (serviceUrl, options) {
this.__serviceURL = serviceUrl;
this.__isCrossSite = false;
var urlParts = this.__serviceURL.match(/^(\w+:)\/\/([^\/]+?)(?::(\d+))?(?:$|\/)/);
if (urlParts) {
this.__isCrossSite = (
location.protocol != urlParts[1] ||
document.domain != urlParts[2] ||
location.port != (urlParts[3] || "")
);
}
if (this.__isCrossSite) {
throw new Error("Cross site rpc not supported yet");
}
//Set other default options
var providedMethodList;
this.__isAsynchronous = true;
this.__authUsername = null;
this.__authPassword = null;
this.__dateEncoding = 'ISO8601'; // ("@timestamp@" || "@ticks@") || "classHinting" || "ASP.NET"
this.__decodeISO8601 = true; //JSON only
//Get the provided options
if (options instanceof Object) {
if (options.asynchronous !== undefined) {
this.__isAsynchronous = !!options.asynchronous;
}
if (options.user != undefined)
this.__authUsername = options.user;
if (options.password != undefined)
this.__authPassword = options.password;
if (options.dateEncoding != undefined)
this.__dateEncoding = options.dateEncoding;
if (options.decodeISO8601 != undefined)
this.__decodeISO8601 = !!options.decodeISO8601;
providedMethodList = options.methods;
}
// Obtain the list of methods made available by the server
if (providedMethodList) {
this.__methodList = providedMethodList;
} else {
var async = this.__isAsynchronous;
this.__isAsynchronous = false;
this.__methodList = this.__callMethod("system.listMethods", []);
this.__isAsynchronous = async;
}
this.__methodList.push("system.listMethods");
//Create local "wrapper" functions which reference the methods obtained above
for (var methodName, i = 0; methodName = this.__methodList[i]; i++) {
//Make available the received methods in the form of chained property lists (eg. "parent.child.methodName")
var methodObject = this;
var propChain = methodName.split(/\./);
for (var j = 0; j + 1 < propChain.length; j++) {
if (!methodObject[propChain[j]])
methodObject[propChain[j]] = {};
methodObject = methodObject[propChain[j]];
}
//Create a wrapper to this.__callMethod with this instance and this methodName bound
var wrapper = (function(instance, methodName) {
var call = {instance:instance, methodName:methodName}; //Pass parameters into closure
return function() {
if (call.instance.__isAsynchronous) {
if (arguments.length == 1 && arguments[0] instanceof Object) {
call.instance.__callMethod(call.methodName,
arguments[0].params,
arguments[0].onSuccess,
arguments[0].onException,
arguments[0].onComplete);
}
else {
call.instance.__callMethod(call.methodName,
arguments[0],
arguments[1],
arguments[2],
arguments[3]);
}
return undefined;
}
else return call.instance.__callMethod(call.methodName, JsonRpc.toArray(arguments));
};
})(this, methodName);
methodObject[propChain[propChain.length - 1]] = wrapper;
}
};
JsonRpc.setAsynchronous = function(serviceProxy, isAsynchronous) {
serviceProxy.__isAsynchronous = !!isAsynchronous;
};
JsonRpc.ServiceProxy.prototype.__callMethod = function(methodName, params, successHandler, exceptionHandler, completeHandler) {
JsonRpc.requestCount++;
//Verify that successHandler, exceptionHandler, and completeHandler are functions
if (this.__isAsynchronous) {
if (successHandler && typeof successHandler != 'function')
throw Error('The asynchronous onSuccess handler callback function you provided is invalid; the value you provided (' + successHandler.toString() + ') is of type "' + typeof(successHandler) + '".');
if (exceptionHandler && typeof exceptionHandler != 'function')
throw Error('The asynchronous onException handler callback function you provided is invalid; the value you provided (' + exceptionHandler.toString() + ') is of type "' + typeof(exceptionHandler) + '".');
if (completeHandler && typeof completeHandler != 'function')
throw Error('The asynchronous onComplete handler callback function you provided is invalid; the value you provided (' + completeHandler.toString() + ') is of type "' + typeof(completeHandler) + '".');
}
try {
//Assign the provided callback function to the response lookup table
if (this.__isAsynchronous) {
JsonRpc.pendingRequests[String(JsonRpc.requestCount)] = {
//method:methodName,
onSuccess:successHandler,
onException:exceptionHandler,
onComplete:completeHandler
};
}
//Obtain and verify the parameters
if (params && (!(params instanceof Object) || params instanceof Date)) //JSON-RPC 1.1 allows params to be a hash not just an array
throw Error('When making asynchronous calls, the parameters for the method must be passed as an array (or a hash); the value you supplied (' + String(params) + ') is of type "' + typeof(params) + '".');
//Prepare the XML-RPC request
var request,postData;
request = {
version:"2.0",
method:methodName,
id:JsonRpc.requestCount
};
if (params)
request.params = params;
postData = this.__toJSON(request);
//XMLHttpRequest chosen (over Ajax.Request) because it propogates uncaught exceptions
var xhr;
if (window.XMLHttpRequest)
xhr = new XMLHttpRequest();
else if (window.ActiveXObject) {
try {
xhr = new ActiveXObject('Msxml2.XMLHTTP');
} catch(err) {
xhr = new ActiveXObject('Microsoft.XMLHTTP');
}
}
xhr.open('POST', this.__serviceURL, this.__isAsynchronous, this.__authUsername, this.__authPassword);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Accept', 'application/json');
if (this.__isAsynchronous) {
//Send the request
xhr.send(postData);
//Handle the response
var instance = this;
var requestInfo = {id:JsonRpc.requestCount}; //for XML-RPC since the 'request' object cannot contain request ID
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
//XML-RPC
var response = instance.__evalJSON(xhr.responseText, instance.__isResponseSanitized);
if (!response.id)
response.id = requestInfo.id;
instance.__doCallback(response);
}
};
return undefined;
} else {
//Send the request
xhr.send(postData);
var response;
response = this.__evalJSON(xhr.responseText, this.__isResponseSanitized);
//Note that this error must be caught with a try/catch block instead of by passing a onException callback
if (response.error)
throw Error('Unable to call "' + methodName + '". Server responsed with error (code ' + response.error.code + '): ' + response.error.message);
this.__upgradeValuesFromJSON(response);
return response.result;
}
} catch(err) {
//err.locationCode = PRE-REQUEST Cleint
var isCaught = false;
if (exceptionHandler)
isCaught = exceptionHandler(err); //add error location
if (completeHandler)
completeHandler();
if (!isCaught)
throw err;
}
};
//This acts as a lookup table for the response callback to execute the user-defined
// callbacks and to clean up after a request
JsonRpc.pendingRequests = {};
//Ad hoc cross-site callback functions keyed by request ID; when a cross-site request
// is made, a function is created
JsonRpc.callbacks = {};
//Called by asychronous calls when their responses have loaded
JsonRpc.ServiceProxy.prototype.__doCallback = function(response) {
if (typeof response != 'object')
throw Error('The server did not respond with a response object.');
if (!response.id)
throw Error('The server did not respond with the required response id for asynchronous calls.');
if (!JsonRpc.pendingRequests[response.id])
throw Error('Fatal error with RPC code: no ID "' + response.id + '" found in pendingRequests.');
//Remove the SCRIPT element from the DOM tree for cross-site (JSON-in-Script) requests
if (JsonRpc.pendingRequests[response.id].scriptElement) {
var script = JsonRpc.pendingRequests[response.id].scriptElement;
script.parentNode.removeChild(script);
}
//Remove the ad hoc cross-site callback function
if (JsonRpc.callbacks[response.id])
delete JsonRpc.callbacks['r' + response.id];
var uncaughtExceptions = [];
//Handle errors returned by the server
if (response.error !== undefined) {
var err = new Error(response.error.message);
err.code = response.error.code;
//err.locationCode = SERVER
if (JsonRpc.pendingRequests[response.id].onException) {
try {
if (!JsonRpc.pendingRequests[response.id].onException(err))
uncaughtExceptions.push(err);
}
catch(err2) { //If the onException handler also fails
uncaughtExceptions.push(err);
uncaughtExceptions.push(err2);
}
}
else uncaughtExceptions.push(err);
}
//Process the valid result
else if (response.result !== undefined) {
//iterate over all values and substitute date strings with Date objects
//Note that response.result is not passed because the values contained
// need to be modified by reference, and the only way to do so is
// but accessing an object's properties. Thus an extra level of
// abstraction allows for accessing all of the results members by reference.
this.__upgradeValuesFromJSON(response);
if (JsonRpc.pendingRequests[response.id].onSuccess) {
try {
JsonRpc.pendingRequests[response.id].onSuccess(response.result);
}
//If the onSuccess callback itself fails, then call the onException handler as above
catch(err) {
//err3.locationCode = CLIENT;
if (JsonRpc.pendingRequests[response.id].onException) {
try {
if (!JsonRpc.pendingRequests[response.id].onException(err))
uncaughtExceptions.push(err);
}
catch(err2) { //If the onException handler also fails
uncaughtExceptions.push(err);
uncaughtExceptions.push(err2);
}
}
else uncaughtExceptions.push(err);
}
}
}
//Call the onComplete handler
try {
if (JsonRpc.pendingRequests[response.id].onComplete)
JsonRpc.pendingRequests[response.id].onComplete(response);
}
catch(err) { //If the onComplete handler fails
//err3.locationCode = CLIENT;
if (JsonRpc.pendingRequests[response.id].onException) {
try {
if (!JsonRpc.pendingRequests[response.id].onException(err))
uncaughtExceptions.push(err);
}
catch(err2) { //If the onException handler also fails
uncaughtExceptions.push(err);
uncaughtExceptions.push(err2);
}
}
else uncaughtExceptions.push(err);
}
delete JsonRpc.pendingRequests[response.id];
//Merge any exception raised by onComplete into the previous one(s) and throw it
if (uncaughtExceptions.length) {
var code;
var message = 'There ' + (uncaughtExceptions.length == 1 ?
'was 1 uncaught exception' :
'were ' + uncaughtExceptions.length + ' uncaught exceptions') + ': ';
for (var i = 0; i < uncaughtExceptions.length; i++) {
if (i)
message += "; ";
message += uncaughtExceptions[i].message;
if (uncaughtExceptions[i].code)
code = uncaughtExceptions[i].code;
}
var err = new Error(message);
err.code = code;
throw err;
}
};
/*******************************************************************************************
* JSON-RPC Specific Functions
******************************************************************************************/
JsonRpc.ServiceProxy.prototype.__toJSON = function(value) {
switch (typeof value) {
case 'number':
return isFinite(value) ? value.toString() : 'null';
case 'boolean':
return value.toString();
case 'string':
//Taken from Ext JSON.js
var specialChars = {
"\b": '\\b',
"\t": '\\t',
"\n": '\\n',
"\f": '\\f',
"\r": '\\r',
'"' : '\\"',
"\\": '\\\\',
"/" : '\/'
};
return '"' + value.replace(/([\x00-\x1f\\"])/g, function(a, b) {
var c = specialChars[b];
if (c)
return c;
c = b.charCodeAt();
//return "\\u00" + Math.floor(c / 16).toString(16) + (c % 16).toString(16);
return '\\u00' + JsonRpc.zeroPad(c.toString(16));
}) + '"';
case 'object':
if (value === null)
return 'null';
else if (value instanceof Array) {
var json = ['[']; //Ext's JSON.js reminds me that Array.join is faster than += in MSIE
for (var i = 0; i < value.length; i++) {
if (i)
json.push(',');
json.push(this.__toJSON(value[i]));
}
json.push(']');
return json.join('');
}
else if (value instanceof Date) {
switch (this.__dateEncoding) {
case 'classHinting': //{"__jsonclass__":["constructor", [param1,...]], "prop1": ...}
return '{"__jsonclass__":["Date",[' + value.valueOf() + ']]}';
case '@timestamp@':
case '@ticks@':
return '"@' + value.valueOf() + '@"';
case 'ASP.NET':
return '"\\/Date(' + value.valueOf() + ')\\/"';
default:
return '"' + JsonRpc.dateToISO8601(value) + '"';
}
}
else if (value instanceof Number || value instanceof String || value instanceof Boolean)
return this.__toJSON(value.valueOf());
else {
var useHasOwn = {}.hasOwnProperty ? true : false; //From Ext's JSON.js
var json = ['{'];
for (var key in value) {
if (!useHasOwn || value.hasOwnProperty(key)) {
if (json.length > 1)
json.push(',');
json.push(this.__toJSON(key) + ':' + this.__toJSON(value[key]));
}
}
json.push('}');
return json.join('');
}
//case 'undefined':
//case 'function':
//case 'unknown':
//default:
}
throw new TypeError('Unable to convert the value of type "' + typeof(value) + '" to JSON.'); //(' + String(value) + ')
};
JsonRpc.isJSON = function(string) { //from Prototype String.isJSON()
var testStr = string.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(testStr);
};
JsonRpc.ServiceProxy.prototype.__evalJSON = function(json, sanitize) { //from Prototype String.evalJSON()
//Remove security comment delimiters
json = json.replace(/^\/\*-secure-([\s\S]*)\*\/\s*$/, "$1");
var err;
try {
if (!sanitize || JsonRpc.isJSON(json))
return eval('(' + json + ')');
}
catch(e) {
err = e;
}
throw new SyntaxError('Badly formed JSON string: ' + json + " ... " + (err ? err.message : ''));
};
//This function iterates over the properties of the passed object and converts them
// into more appropriate data types, i.e. ISO8601 strings are converted to Date objects.
JsonRpc.ServiceProxy.prototype.__upgradeValuesFromJSON = function(obj) {
var matches, useHasOwn = {}.hasOwnProperty ? true : false;
for (var key in obj) {
if (!useHasOwn || obj.hasOwnProperty(key)) {
//Parse date strings
if (typeof obj[key] == 'string') {
//ISO8601
if (this.__decodeISO8601 && (matches = obj[key].match(/^(?:(\d\d\d\d)-(\d\d)(?:-(\d\d)(?:T(\d\d)(?::(\d\d)(?::(\d\d)(?:\.(\d+))?)?)?)?)?)$/))) {
obj[key] = new Date(0);
if (matches[1]) obj[key].setUTCFullYear(parseInt(matches[1]));
if (matches[2]) obj[key].setUTCMonth(parseInt(matches[2] - 1));
if (matches[3]) obj[key].setUTCDate(parseInt(matches[3]));
if (matches[4]) obj[key].setUTCHours(parseInt(matches[4]));
if (matches[5]) obj[key].setUTCMinutes(parseInt(matches[5]));
if (matches[6]) obj[key].setUTCMilliseconds(parseInt(matches[6]));
}
//@timestamp@ / @ticks@
else if (matches = obj[key].match(/^@(\d+)@$/)) {
obj[key] = new Date(parseInt(matches[1]))
}
//ASP.NET
else if (matches = obj[key].match(/^\/Date\((\d+)\)\/$/)) {
obj[key] = new Date(parseInt(matches[1]))
}
}
else if (obj[key] instanceof Object) {
//JSON 1.0 Class Hinting: {"__jsonclass__":["constructor", [param1,...]], "prop1": ...}
if (obj[key].__jsonclass__ instanceof Array) {
//console.info('good1');
if (obj[key].__jsonclass__[0] == 'Date') {
//console.info('good2');
if (obj[key].__jsonclass__[1] instanceof Array && obj[key].__jsonclass__[1][0])
obj[key] = new Date(obj[key].__jsonclass__[1][0]);
else
obj[key] = new Date();
}
}
else this.__upgradeValuesFromJSON(obj[key]);
}
}
}
};
/*******************************************************************************************
* Other helper functions
******************************************************************************************/
//Takes an array or hash and coverts it into a query string, converting dates to ISO8601
// and throwing an exception if nested hashes or nested arrays appear.
JsonRpc.toQueryString = function(params) {
if (!(params instanceof Object || params instanceof Array) || params instanceof Date)
throw Error('You must supply either an array or object type to convert into a query string. You supplied: ' + params.constructor);
var str = '';
var useHasOwn = {}.hasOwnProperty ? true : false;
for (var key in params) {
if (useHasOwn && params.hasOwnProperty(key)) {
//Process an array
if (params[key] instanceof Array) {
for (var i = 0; i < params[key].length; i++) {
if (str)
str += '&';
str += encodeURIComponent(key) + "=";
if (params[key][i] instanceof Date)
str += encodeURIComponent(JsonRpc.dateToISO8601(params[key][i]));
else if (params[key][i] instanceof Object)
throw Error('Unable to pass nested arrays nor objects as parameters while in making a cross-site request. The object in question has this constructor: ' + params[key][i].constructor);
else str += encodeURIComponent(String(params[key][i]));
}
}
else {
if (str)
str += '&';
str += encodeURIComponent(key) + "=";
if (params[key] instanceof Date)
str += encodeURIComponent(JsonRpc.dateToISO8601(params[key]));
else if (params[key] instanceof Object)
throw Error('Unable to pass objects as parameters while in making a cross-site request. The object in question has this constructor: ' + params[key].constructor);
else str += encodeURIComponent(String(params[key]));
}
}
}
return str;
};
//Converts an iterateable value into an array; similar to Prototype's $A function
JsonRpc.toArray = function(value) {
//if(value && value.length){
if (value instanceof Array)
return value;
var array = [];
for (var i = 0; i < value.length; i++)
array.push(value[i]);
return array;
//}
//throw Error("Unable to convert to an array the value: " + String(value));
};
//Returns an ISO8601 string *in UTC* for the provided date (Prototype's Date.toJSON() returns localtime)
JsonRpc.dateToISO8601 = function(date) {
//var jsonDate = date.toJSON();
//return jsonDate.substring(1, jsonDate.length-1); //strip double quotes
return date.getUTCFullYear() + '-' +
JsonRpc.zeroPad(date.getUTCMonth() + 1) + '-' +
JsonRpc.zeroPad(date.getUTCDate()) + 'T' +
JsonRpc.zeroPad(date.getUTCHours()) + ':' +
JsonRpc.zeroPad(date.getUTCMinutes()) + ':' +
JsonRpc.zeroPad(date.getUTCSeconds()) + '.' +
//Prototype's Date.toJSON() method does not include milliseconds
JsonRpc.zeroPad(date.getUTCMilliseconds(), 3);
};
JsonRpc.zeroPad = function(value, width) {
if (!width)
width = 2;
value = (value == undefined ? '' : String(value))
while (value.length < width)
value = '0' + value;
return value;
};