olokia-client-javascript.1.2.3.source-code.jolokia.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jolokia-client-javascript
Show all versions of jolokia-client-javascript
Jolokia :: Client :: JavaScript
/*
* Copyright 2009-2012 Roland Huss
*
* 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.
*/
/* =================================
* Jolokia Javascript Client Library
* =================================
*
* Requires jquery.js and json2.js
* (if no native JSON.stringify() support is available,
* look here where this is the case: http://caniuse.com/json)
*/
(function() {
var _jolokiaConstructorFunc = function ($) {
// Default paramerters for GET and POST requests
var DEFAULT_CLIENT_PARAMS = {
type:"POST",
jsonp:false
};
var GET_AJAX_PARAMS = {
type:"GET"
};
var POST_AJAX_PARAMS = {
type:"POST",
processData:false,
dataType:"json",
contentType:"text/json"
};
// Processing parameters which are added to the
// URL as query parameters if given as options
var PROCESSING_PARAMS = ["maxDepth", "maxCollectionSize", "maxObjects", "ignoreErrors", "canonicalNaming",
"serializeException", "includeStackTrace", "ifModifiedSince"];
/**
* Constructor for creating a client to the Jolokia agent.
*
* An object containing the default parameters can be provided as argument. For the possible parameters
* see {@link #request()}.
*
* @param param either a string in which case it is used as the URL to the agent or
* an object with the default parameters as key-value pairs
*/
function Jolokia(param) {
// If called without 'new', we are constructing an object
// nevertheless
if (!(this instanceof arguments.callee)) {
return new Jolokia(param);
}
// Jolokia Javascript Client version
this.CLIENT_VERSION = "1.2.3";
// Registered requests for fetching periodically
var jobs = [];
// Options used for every request
var agentOptions = {};
// State of the scheduler
var pollerIsRunning = false;
// Allow a single URL parameter as well
if (typeof param === "string") {
param = {url:param};
}
$.extend(agentOptions, DEFAULT_CLIENT_PARAMS, param);
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Public methods
/**
* The request method using one or more JSON requests and sending it to the agent. Beside the
* request a bunch of options can be given, which are merged with the options provided
* at the constructor (where the options given here take precedence).
*
* Known options are:
*
*
* - url
* - Agent URL, which is mandatory
* - method
* -
* Either "post" or "get" depending on the desired HTTP method (case does not matter).
* Please note, that bulk requests are not possible with "get". On the other
* hand, JSONP requests are not possible with "post" (which obviously implies
* that bulk request cannot be used with JSONP requests). Also, when using a
*
read
type request for multiple attributes, this also can
* only be sent as "post" requests. If not given, a HTTP method is determined
* dyamically. If a method is selected which doesn't fit to the request, an error
* is raised.
*
* - jsonp
* -
* Whether the request should be sent via JSONP (a technique for allowing cross
* domain request circumventing the infamous "same-origin-policy"). This can be
* used only with HTTP "get" requests.
*
* - success
* -
* Callback function which is called for a successful request. The callback receives
* the response as single argument. If no
success
callback is given, then
* the request is performed synchronously and gives back the response as return
* value.
*
* - error
* -
* Callback in case a Jolokia error occurs. A Jolokia error is one, in which the HTTP request
* suceeded with a status code of 200, but the response object contains a status other
* than OK (200) which happens if the request JMX operation fails. This callback receives
* the full Jolokia response object (with a key
error
set). If no error callback
* is given, but an asynchronous operation is performed, the error response is printed
* to the Javascript console by default.
*
* - ajaxError
* -
* Global error callback called when the Ajax request itself failed. It obtains the same arguments
* as the error callback given for
jQuery.ajax()
, i.e. the XmlHttpResonse
,
* a text status and an error thrown. Refer to the jQuery documentation for more information about
* this error handler.
*
* - username
* - A username used for HTTP authentication
* - password
* - A password used for HTTP authentication
* - timeout
* - Timeout for the HTTP request
* - maxDepth
* - Maximum traversal depth for serialization of complex return values
* - maxCollectionSize
* -
* Maximum size of collections returned during serialization.
* If larger, the collection is returned truncated.
*
* - maxObjects
* -
* Maximum number of objects contained in the response.
*
* - ignoreErrors
* -
* If set to true, errors during JMX operations and JSON serialization
* are ignored. Otherwise if a single deserialization fails, the whole request
* returns with an error. This works only for certain operations like pattern reads..
*
*
*
* @param request the request to send
* @param params parameters used for sending the request
* @return the response object if called synchronously or nothing if called for asynchronous operation.
*/
this.request = function (request, params) {
var opts = $.extend({}, agentOptions, params);
assertNotNull(opts.url, "No URL given");
var ajaxParams = {};
// Copy over direct params for the jQuery ajax call
$.each(["username", "password", "timeout"], function (i, key) {
if (opts[key]) {
ajaxParams[key] = opts[key];
}
});
if (ajaxParams['username'] && ajaxParams['password']) {
// If we have btoa() then we set the authentication preemptively,
// Otherwise (e.g. for IE < 10) an extra roundtrip might be necessary
// when using 'username' and 'password' in xhr.open(..)
// See http://stackoverflow.com/questions/5507234/how-to-use-basic-auth-and-jquery-and-ajax
// for details
if (window.btoa) {
ajaxParams.beforeSend = function (xhr) {
var tok = ajaxParams['username'] + ':' + ajaxParams['password'];
xhr.setRequestHeader('Authorization', "Basic " + window.btoa(tok));
};
}
// Add appropriate field for CORS access
ajaxParams.xhrFields = {
// Please note that for CORS access with credentials, the request
// must be asynchronous (see https://dvcs.w3.org/hg/xhr/raw-file/tip/Overview.html#the-withcredentials-attribute)
// It works synchronously in Chrome nevertheless, but fails in Firefox.
withCredentials: true
};
}
if (extractMethod(request, opts) === "post") {
$.extend(ajaxParams, POST_AJAX_PARAMS);
ajaxParams.data = JSON.stringify(request);
ajaxParams.url = ensureTrailingSlash(opts.url);
} else {
$.extend(ajaxParams, GET_AJAX_PARAMS);
ajaxParams.dataType = opts.jsonp ? "jsonp" : "json";
ajaxParams.url = opts.url + "/" + constructGetUrlPath(request);
}
// Add processing parameters as query parameters
ajaxParams.url = addProcessingParameters(ajaxParams.url, opts);
// Global error handler
if (opts.ajaxError) {
ajaxParams.error = opts.ajaxError;
}
// Dispatch Callbacks to error and success handlers
if (opts.success) {
var success_callback = constructCallbackDispatcher(opts.success);
var error_callback = constructCallbackDispatcher(opts.error);
ajaxParams.success = function (data) {
var responses = $.isArray(data) ? data : [ data ];
for (var idx = 0; idx < responses.length; idx++) {
var resp = responses[idx];
if (Jolokia.isError(resp)) {
error_callback(resp, idx);
} else {
success_callback(resp, idx);
}
}
};
// Perform the request
$.ajax(ajaxParams);
return null;
} else {
// Synchronous operation requested (i.e. no callbacks provided)
if (opts.jsonp) {
throw Error("JSONP is not supported for synchronous requests");
}
ajaxParams.async = false;
var xhr = $.ajax(ajaxParams);
if (httpSuccess(xhr)) {
return $.parseJSON(xhr.responseText);
} else {
return null;
}
}
};
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Scheduler related methods
/**
* Register one or more requests for periodically polling the agent along with a callback to call on receipt
* of the response.
*
* The first argument can be either an object or a function. The remaining arguments are interpreted
* as Jolokia request objects
*
* If a function is given or an object with an attribute callback
holding a function, then
* this function is called with all responses received as argument, regardless whether the individual response
* indicates a success or error state.
*
* If the first argument is an object with two callback attributes success
and error
,
* these functions are called for each response separately, depending whether the response
* indicates success or an error state. If multiple requests have been registered along with this callback object,
* the callback is called multiple times, one for each request in the same order as the request are given.
* As second argument, the handle which is returned by this method is given and as third argument the index
* within the list of requests.
*
* If the first argument is an object, an additional 'config' attribute with processing parameters can
* be given which is used as default for the registered requests.
* Request with a 'config' section take precedence.
*
* @param callback and options specification.
* @param request, request, .... One or more requests to be registered for this single callback
* @return handle which can be used for unregistering the request again or for correlation purposes in the callbacks
*/
this.register = function() {
if (arguments.length < 2) {
throw "At a least one request must be provided";
}
var callback = arguments[0],
requests = Array.prototype.slice.call(arguments,1),
job;
if (typeof callback === 'object') {
if (callback.success && callback.error) {
job = {
success: callback.success,
error: callback.error
};
} else if (callback.callback) {
job = {
callback: callback.callback
};
} else {
throw "Either 'callback' or ('success' and 'error') callback must be provided " +
"when registering a Jolokia job";
}
job = $.extend(job,{
config: callback.config,
onlyIfModified: callback.onlyIfModified
});
} else if (typeof callback === 'function') {
// Simplest version without config possibility
job = {
success: null,
error: null,
callback: callback
};
} else {
throw "First argument must be either a callback func " +
"or an object with 'success' and 'error' attributes";
}
if (!requests) {
throw "No requests given";
}
job.requests = requests;
var idx = jobs.length;
jobs[idx] = job;
return idx;
};
/**
* Unregister a one or more request which has been registered with {@link #registerRequest}. As parameter
* the handle returned during the registration process must be given
* @param handle
*/
this.unregister = function(handle) {
if (handle < jobs.length) {
jobs[handle] = undefined;
}
};
/**
* Return an array of handles for currently registered jobs.
* @return Array of job handles or an empty array
*/
this.jobs = function() {
var ret = [],
len = jobs.length;
for (var i = 0; i < len; i++) {
if (jobs[i]) {
ret.push(i);
}
}
return ret;
};
/**
* Start the poller. The interval between two polling attempts can be optionally given or are taken from
* the parameter fetchInterval
given at construction time. If no interval is given at all,
* 30 seconds is the default.
*
* If the poller is already running (i.e. {@link #isRunning()} is true
then the scheduler
* is restarted, but only if the new interval differs from the currently active one.
*
* @param interval interval in milliseconds between two polling attempts
*/
this.start = function(interval) {
interval = interval || agentOptions.fetchInterval || 30000;
if (pollerIsRunning) {
if (interval === agentOptions.fetchInterval) {
// Nothing to do
return;
}
// Re-start with new interval
this.stop();
}
agentOptions.fetchInterval = interval;
this.timerId = setInterval(callJolokia(this,jobs), interval);
pollerIsRunning = true;
};
/**
* Stop the poller. If the poller is not running, no operation is performed.
*/
this.stop = function() {
if (!pollerIsRunning && this.timerId != undefined) {
return;
}
clearInterval(this.timerId);
this.timerId = null;
pollerIsRunning = false;
};
/**
* Check whether the poller is running.
* @return true if the poller is running, false otherwise.
*/
this.isRunning = function() {
return pollerIsRunning;
};
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
}
// ========================================================================
// Private Methods:
// Create a function called by a timer, which requests the registered requests
// calling the stored callback on receipt. jolokia and jobs are put into the closure
function callJolokia(jolokia,jobs) {
return function() {
var errorCbs = [],
successCbs = [],
i, j,
len = jobs.length;
var requests = [];
for (i = 0; i < len; i++) {
var job = jobs[i];
// Can happen when job has been deleted
// TODO: Can be probably optimized so that only the existing keys of jobs can be visited
if (!job) { continue; }
var reqsLen = job.requests.length;
if (job.success) {
// Success/error pair of callbacks. For multiple request,
// these callback will be called multiple times
var successCb = cbSuccessClosure(job,i);
var errorCb = cbErrorClosure(job,i);
for (j = 0; j < reqsLen; j++) {
requests.push(prepareRequest(job,j));
successCbs.push(successCb);
errorCbs.push(errorCb);
}
} else {
// Job should have a single callback (job.callback) which will be
// called once with all responses at once as an array
var callback = cbCallbackClosure(job,jolokia);
// Add callbacks which collect the responses
for (j = 0; j < reqsLen - 1; j++) {
requests.push(prepareRequest(job,j));
successCbs.push(callback.cb);
errorCbs.push(callback.cb);
}
// Add final callback which finally will call the job.callback with all
// collected responses.
requests.push(prepareRequest(job,reqsLen-1));
successCbs.push(callback.lcb);
errorCbs.push(callback.lcb);
}
}
var opts = {
// Dispatch to the build up callbacks, request by request
success: function(resp, j) {
return successCbs[j].apply(jolokia, [resp, j]);
},
error: function(resp, j) {
return errorCbs[j].apply(jolokia, [resp, j]);
}
};
return jolokia.request(requests, opts);
};
}
// Prepare a request with the proper configuration
function prepareRequest(job,idx) {
var request = job.requests[idx],
config = job.config || {},
// Add the proper ifModifiedSince parameter if already called at least once
extra = job.onlyIfModified && job.lastModified ? { ifModifiedSince: job.lastModified } : {};
request.config = $.extend({}, config, request.config, extra);
return request;
}
// Closure for a full callback which stores the responses in an (closed) array
// which the finally is feed in to the callback as array
function cbCallbackClosure(job,jolokia) {
var responses = [],
callback = job.callback,
lastModified = 0;
return {
cb : addResponse,
lcb : function(resp,j) {
addResponse(resp);
// Callback is called only if at least one non-cached response
// is obtained. Update job's timestamp internally
if (responses.length > 0) {
job.lastModified = lastModified;
callback.apply(jolokia,responses);
}
}
};
function addResponse(resp,j) {
// Only remember responses with values and remember lowest timetamp, too.
if (resp.status != 304) {
if (lastModified == 0 || resp.timestamp < lastModified ) {
lastModified = resp.timestamp;
}
responses.push(resp);
}
}
}
// Own function for creating a closure to avoid reference to mutable state in the loop
function cbErrorClosure(job, i) {
var callback = job.error;
return function(resp,j) {
// If we get a "304 - Not Modified" 'error', we do nothing
if (resp.status == 304) {
return;
}
if (callback) {
callback(resp,i,j)
}
}
}
function cbSuccessClosure(job, i) {
var callback = job.success;
return function(resp,j) {
if (callback) {
// Remember last success callback
if (job.onlyIfModified) {
job.lastModified = resp.timestamp;
}
callback(resp,i,j)
}
}
}
// Construct a callback dispatcher for appropriately dispatching
// to a single callback or within an array of callbacks
function constructCallbackDispatcher(callback) {
if (callback == null) {
return function (response) {
console.warn("Ignoring response " + JSON.stringify(response));
};
} else if (callback === "ignore") {
// Ignore the return value
return function () {
};
}
var callbackArray = $.isArray(callback) ? callback : [ callback ];
return function (response, idx) {
callbackArray[idx % callbackArray.length](response, idx);
}
}
// Extract the HTTP-Method to use and make some sanity checks if
// the method was provided as part of the options, but dont fit
// to the request given
function extractMethod(request, opts) {
var methodGiven = opts && opts.method ? opts.method.toLowerCase() : null,
method;
if (methodGiven) {
if (methodGiven === "get") {
if ($.isArray(request)) {
throw new Error("Cannot use GET with bulk requests");
}
if (request.type.toLowerCase() === "read" && $.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");
}
}
method = methodGiven;
} else {
// Determine method dynamically
method = $.isArray(request) ||
request.config ||
(request.type.toLowerCase() === "read" && $.isArray(request.attribute)) ||
request.target ?
"post" : "get";
}
if (opts.jsonp && method === "post") {
throw new Error("Can not use JSONP with POST requests");
}
return method;
}
// Add processing parameters given as request options
// to an URL as GET query parameters
function addProcessingParameters(url, opts) {
var sep = url.indexOf("?") > 0 ? "&" : "?";
$.each(PROCESSING_PARAMS, function (i, key) {
if (opts[key] != null) {
url += sep + key + "=" + opts[key];
sep = "&";
}
});
return url;
}
// ========================================================================
// GET-Request handling
// Create the URL used for a GET request
function constructGetUrlPath(request) {
var type = request.type;
assertNotNull(type, "No request type given for building a GET request");
type = type.toLowerCase();
var extractor = GET_URL_EXTRACTORS[type];
assertNotNull(extractor, "Unknown request type " + type);
var result = extractor(request);
var parts = result.parts || [];
var url = type;
$.each(parts, function (i, v) {
url += "/" + Jolokia.escape(v)
});
if (result.path) {
url += (result.path[0] == '/' ? "" : "/") + result.path;
}
console.log(url);
return url;
}
// For POST requests it is recommended to have a trailing slash at the URL
// in order to avoid a redirect which then results in a GET request.
// See also https://bugs.eclipse.org/bugs/show_bug.cgi?id=331194#c1
// for an explanation
function ensureTrailingSlash(url) {
// Squeeze any URL to a single one, optionally adding one
return url.replace(/\/*$/, "/");
}
// Extractors used for preparing a GET request, i.e. for creating a stack
// of arguments which gets appended to create the proper access URL
// key: lowercase request type.
// The return value is an object with two properties: The 'parts' to glue together, where
// each part gets escaped and a 'path' which is appended literally
var GET_URL_EXTRACTORS = {
"read":function (request) {
if (request.attribute == null) {
// Path gets ignored for multiple attribute fetch
return { parts:[ request.mbean, '*' ], path:request.path };
} else {
return { parts:[ request.mbean, request.attribute ], path:request.path };
}
},
"write":function (request) {
return { parts:[request.mbean, request.attribute, valueToString(request.value)], path:request.path};
},
"exec":function (request) {
var ret = [ request.mbean, request.operation ];
if (request.arguments && request.arguments.length > 0) {
$.each(request.arguments, function (index, value) {
ret.push(valueToString(value));
});
}
return {parts:ret};
},
"version":function () {
return {};
},
"search":function (request) {
return { parts:[request.mbean]};
},
"list":function (request) {
return { path:request.path};
}
};
// Convert a value to a string for passing it to the Jolokia agent via
// a get request (write, exec). Value can be either a single object or an array
function valueToString(value) {
if (value == null) {
return "[null]";
}
if ($.isArray(value)) {
var ret = "";
for (var i = 0; i < value.length; i++) {
ret += value == null ? "[null]" : singleValueToString(value[i]);
if (i < value.length - 1) {
ret += ",";
}
}
return ret;
} else {
return singleValueToString(value);
}
}
// Single value conversion for write/exec GET requests
function singleValueToString(value) {
if (typeof value === "string" && value.length == 0) {
return "\"\"";
} else {
return value.toString();
}
}
// Check whether a synchronous request was a success or not
// Taken from jQuery 1.4
function httpSuccess(xhr) {
try {
return !xhr.status && location.protocol === "file:" ||
xhr.status >= 200 && xhr.status < 300 ||
xhr.status === 304 || xhr.status === 1223;
} catch (e) {
}
return false;
}
// ===============================================================================================
// Utility methods:
function assertNotNull(object, message) {
if (object == null) {
throw new Error(message);
}
}
// ================================================================================================
// Escape a path part, can be used as a static method outside this function too
Jolokia.prototype.escape = Jolokia.escape = function (part) {
return encodeURIComponent(part.replace(/!/g, "!!").replace(/\//g, "!/"));
};
/**
* Utility method which checks whether a response is an error or a success
* @param resp response to check
* @return true if response is an error, false otherwise
*/
Jolokia.prototype.isError = Jolokia.isError = function(resp) {
return resp.status == null || resp.status != 200;
};
// Return back exported function/constructor
return Jolokia;
};
// =====================================================================================================
// Register either as global or as AMD module
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as a named module
define(["jquery"], factory);
} else {
// Browser globals
root.Jolokia = factory(root.jQuery);
}
}(this, function (jQuery) {
return _jolokiaConstructorFunc(jQuery);
}));
}());