jolokia
Version:
Implementation of jolokia JMX client
451 lines (389 loc) • 13.6 kB
JavaScript
var utils = require('./utils');
var q = require('q');
/**
* Constructor method.
*
* Accepts one argument, either an URL of backend
* to use with requests or hash of options (refer to documentation of Jolokia.request
* for available options).
*
* @param {String|Object} options Hash of options or String URL to use with requests
*/
var Jolokia = module.exports = function(options) {
if (typeof(options) === 'string') {
options = { url: options };
}
options = utils.extend({}, options);
this.options = function() {
return utils.extend({}, options);
};
};
/**
* Escapes !, " and /, then URI encodes string
*
* @param {String} string String to be escaped
* @return {String} Escaped string
*/
Jolokia.escape = function(string) {
return encodeURIComponent(string.replace(/(["!\/])/g, '!$1'));
};
/**
* Returns true if passed response object represents an error state
*
* @param {Object} response Jolokia server response
* @return {Boolean} True if response object represents an error
*/
Jolokia.isError = function(response) {
return !response.status || response.status !== 200;
};
/**
* Converts a value to string to be used to construct URL for GET requests.
* Value is not escaped though, it must be done separately via Jolokia.escape method
*
* @param {Any} value Value to be converted
* @return {String} String representation of the value
*/
Jolokia.toString = function(value) {
if (utils.isArray(value)) {
return value.map(valueToString).join(',');
} else {
return valueToString(value);
}
};
/**
* Automatically determine HTTP request method to be used for particular request.
* This follows the specification for jolokia requests that can be found at
* http://www.jolokia.org/reference/html/clients.html#js-request-sync-async
*
* @param {Object|Array} request Jolokia request
* @return {String} Lowercase HTTP method, either post or get
*/
Jolokia.prototype.determineRequestMethod = function(request) {
var method =
utils.isArray(request) ||
request.config ||
(request.type.match(/read/i) && utils.isArray(request.attribute)) ||
request.target ?
'post' : 'get';
return method;
};
/**
* Create an url to be used with GET requests
*
* @param {Object} request Jolokia request
* @return {String} URL to be used with HTTP GET request
*/
Jolokia.prototype.constructUrl = function(request) {
var type = request.type.toLowerCase();
var urlParts = [];
var urlSuffix = '';
if (type === 'read') {
if (request.attribute) {
urlParts = [request.mbean, request.attribute];
urlSuffix = request.path;
} else {
urlParts = [request.mbean];
}
} else if (type === 'write') {
urlParts = [request.mbean, request.attribute, Jolokia.toString(request.value)];
urlSuffix = request.path;
} else if (type === 'exec') {
urlParts = [request.mbean, request.operation];
if (request.arguments) {
urlParts = urlParts.concat(request.arguments.map(Jolokia.toString));
}
} else if (type === 'search') {
urlParts = [request.mbean];
} else if (type === 'list') {
urlSuffix = request.path;
}
if (utils.isArray(urlSuffix)) {
urlSuffix = urlSuffix.map(Jolokia.escape).join('/');
}
// Add request operation at the beginning of the URL
urlParts.unshift(type);
urlParts = urlParts.map(Jolokia.escape);
urlSuffix = urlSuffix ? urlSuffix.replace(/^\//, '') : '';
return urlParts.join('/') + '/' + urlSuffix;
};
/**
* Central method used to create HTTP requests
*
* @param {Object|Array} request Jolokia request object or an array of objects
* @param {Object} options [optional] Additional options for request
* @return {jQuery.Promise} jQuery jXHR promise resolved with an array of jolokia server response objects
*/
Jolokia.prototype.request = function (request, options) {
options = utils.extend({}, this.options(), options);
if (!options.url || typeof(options.url) !== 'string') {
throw 'Invalid URL : ' + options.url;
}
var method = (options.method || this.determineRequestMethod(request)).toLowerCase();
if (method !== 'get' && method !== 'post') {
throw new Error('Unsupported request method: ' + method);
}
if (method === 'get') {
if (utils.isArray(request)) {
throw new Error('Cannot use GET with bulk requests');
}
if (request.type.match(/read/i) && utils.isArray(request.attribute)) {
throw new Error('Cannot use GET for read with multiple attributes');
}
if (request.target) {
throw new Error('Cannot use GET request with proxy mode');
}
if (request.config) {
throw new Error('Cannot use GET with request specific config');
}
}
// Add trailing slash to URL
options.url = options.url.replace(/\/*$/, '/');
if (method === 'get') {
options.url += this.constructUrl(request, options);
} else {
options.data = JSON.stringify(request);
}
// Add request method and URL
options.method = method;
options.url = addQueryParamsToUrl(options.url, options.query || {});
return this.httpRequest(options).then(function(response) {
var responses = utils.isArray(response) ? response: [response];
if (typeof(options.success) === 'function') {
options.success.call(this, responses);
}
return responses;
}.bind(this), options.error);
};
/**
* Get one or more attributes
*
* @param {String} mbean name of MBean to query. Can be a pattern.
* @param {String} attribute attribute name. If an array, multiple attributes are fetched.
* If <code>null</code>, all attributes are fetched.
* @param {String} path optional path within the return value.
* For multi-attribute fetch, the path is ignored.
* @param {Object} options options passed to Jolokia.request()
*
* @return {jQuery.Promise}
*/
Jolokia.prototype.get = function(mbean, attribute, path, options) {
if (arguments.length === 3 && utils.isObject(path)) {
options = path;
path = null;
} else if (arguments.length === 2 && utils.isObject(attribute)) {
options = attribute;
attribute = null;
path = null;
}
var request = { type: 'read', mbean: mbean, attribute: attribute };
if (path) {
request.path = path;
}
return this.request(request, options).then(returnValueOrThrow);
};
/**
* Set an attribute on a MBean.
*
* @param mbean objectname of MBean to set
* @param attribute the attribute to set
* @param value the value to set
* @param path an optional <em>inner path</em> which, when given, is used to determine
* an inner object to set the value on
* @param opts additional options passed to Jolokia.request()
* @return the previous value
*/
Jolokia.prototype.set = function(mbean, attribute, value, path, options) {
if (arguments.length === 4 && utils.isObject(path)) {
options = path;
path = null;
}
var request = { type: 'write', mbean: mbean, attribute: attribute, value: value };
if (path) {
request.path = path;
}
return this.request(request, options).then(returnValueOrThrow);
};
/**
* Execute a JMX operation and return the result value
*
* @param mbean objectname of the MBean to operate on
* @param operation name of operation to execute. Can contain a signature in case overloaded
* operations are to be called (comma separated fully qualified argument types
* append to the operation name within parentheses)
* @param arg1, arg2, ..... one or more argument required for executing the operation.
* @param opts optional options for Jolokia.request() (must be an object)
* @return the return value of the JMX operation.
*/
Jolokia.prototype.execute = function(mbean, operation) {
var request = { type: 'exec', mbean: mbean, operation: operation };
var options,
args = Array.prototype.slice.call(arguments, 0);
if (args.length > 2 && utils.isObject(args[args.length - 1])) {
options = args.pop();
}
if (args.length > 2) {
request.arguments = args.slice(2);
}
return this.request(request, options).then(returnValueOrThrow);
};
/**
* Search for MBean based on a pattern and return a reference to the list of found
* MBeans names (as string). If no MBean can be found, <code>null</code> is returned. For
* example,
*
* jolokia.search("*:j2eeType=J2EEServer,*")
*
* searches all MBeans whose name are matching this pattern, which are according
* to JSR77 all application servers in all available domains.
*
* @param mbeanPattern pattern to search for
* @param opts optional options for Jolokia.request()
* @return an array with ObjectNames as string
*/
Jolokia.prototype.search = function(mbeanPattern, options) {
var request = { type: 'search', mbean: mbeanPattern };
return this.request(request, options).then(returnValueOrThrow);
};
/**
* This method return the version of the agent and the Jolokia protocol
* version as part of an object. If available, server specific information
* like the application server's name are returned as wel.
* A typical response looks like
*
* <pre>
* {
* protocol: "4.0",
* agent: "0.82",
* info: {
* product: "glassfish",
* vendor": "Sun",
* extraInfo: {
* amxBooted: false
* }
* }
* </pre>
*
* @param opts optional options for Jolokia.request()
* @param version and other meta information as object
*/
Jolokia.prototype.version = function(options) {
return this.request({ type: 'version' }, options).then(returnValueOrThrow);
};
/**
* Get all MBeans as registered at the specified server. A C<$path> can be
* specified in order to fetchy only a subset of the information. When no path is
* given, the returned value has the following format
*
* <pre>
* {
* <domain> :
* {
* <canonical property list> :
* {
* "attr" :
* {
* <atrribute name> :
* {
* desc : <description of attribute>
* type : <java type>,
* rw : true/false
* },
* ....
* },
* "op" :
* {
* <operation name> :
* {
* "desc" : <description of operation>
* "ret" : <return java type>
* "args" :
* [
* {
* "desc" : <description>,
* "name" : <name>,
* "type" : <java type>
* },
* ....
* ]
* },
* ....
* },
* ....
* }
* ....
* }
* </pre>
*
* A complete path has the format <domain>/property
* list>/("attribute"|"operation")/<index>">
* (e.g. <code>java.lang/name=Code Cache,type=MemoryPool/attribute/0</code>). A path can be
* provided partially, in which case the remaining map/array is returned. The path given must
* be already properly escaped (i.e. slashes must be escaped like <code>!/</code> and exlamation
* marks like <code>!!</code>.
* See also the Jolokia Reference Manual for a more detailed discussion of inner paths and escaping.
*
*
* @param path optional path for diving into the list
* @param opts optional opts passed to Jolokia.request()
*/
Jolokia.prototype.list = function(path, options) {
if (arguments.length === 1 && !utils.isArray(path) && utils.isObject(path)) {
options = path;
path = null;
}
var request = { type: 'list' };
if (path) {
request.path = path;
}
return this.request(request, options).then(returnValueOrThrow);
};
/**
* Private helper function.
* Returns the value attribute from server response or throws an exception
* in case of error.
*
* @param {Array} responses Jolokia server responses
* @return {Mixed} Response value
*/
function returnValueOrThrow(responses) {
if (responses[0].status !== 200) {
return q.reject(responses[0]);
}
return responses[0].value;
}
/**
* Private helper function.
* Serializes value to be passed over to Jolokia backend
* following Jolokia protocol.
*
* @param {Object} value Value to serialize
* @return {String} Serialized value
*/
function valueToString(value) {
if (value === null || value === undefined) return '[null]';
if (value === '') return '""';
return value.toString();
}
/**
* Private helper function.
* Adds URL query parameters to URL.
*
* @param {String} url URL
* @param {Object} params Hash containing query parameters
*/
function addQueryParamsToUrl(url, params) {
var query = [];
for (var key in params) {
query.push(key + '=' + params[key]);
}
query = query.join('&');
if (query) {
if (url.indexOf('?') === -1) {
url = url + '?' + query;
} else {
url = url.replace('?', '?' + query + '&');
}
}
return url;
}
;