modules/js/htdocs/component.js (467 lines of code) (raw):
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
/**
* Client component wiring API, supporting JSON and ATOM bindings.
*/
var JSONClient = {};
/**
* Construct an HTTPBindingClient.
*/
function HTTPBindingClient(name, uri, domain) {
this.name = name;
this.domain = domain;
this.uri = uri;
this.apply = this.createApplyMethod();
}
/**
* HTTPBindingClient implementation
*/
/**
* Run a function asynchronously.
*/
HTTPBindingClient.delaying = false;
HTTPBindingClient.delay = function(f) {
if (HTTPBindingClient.delaying)
return window.setTimeout(f, 0);
else
return f();
};
/**
* Generate client proxy apply method.
*/
HTTPBindingClient.prototype.createApplyMethod = function() {
var fn = function() {
var methodName = arguments[0];
var args = [];
for(var i = 1; i < arguments.length; i++)
args[args.length] = arguments[i];
var cb;
if(typeof args[args.length - 1] == 'function')
cb = args.pop();
var req = HTTPBindingClient.makeJSONRequest(methodName, args, cb);
return fn.client.jsonApply(req);
};
fn.client = this;
return fn;
};
/**
* JSON-RPC request counter.
*/
HTTPBindingClient.jsonrpcID = 1;
/**
* Make a JSON-RPC request.
*/
HTTPBindingClient.makeJSONRequest = function(methodName, args, cb) {
var req = {};
req.id = HTTPBindingClient.jsonrpcID++;
if(cb)
req.cb = cb;
var obj = {};
obj.id = req.id;
obj.method = methodName;
obj.params = args;
req.data = JSON.stringify(obj);
return req;
};
/**
* Return the JSON result from an XMLHttpRequest.
*/
HTTPBindingClient.jsonResult = function(http) {
var obj = JSON.parse(http.responseText);
return obj.result;
};
/**
* Schedule async requests, limiting the number of concurrent running requests.
*/
HTTPBindingClient.queuedRequests = [];
HTTPBindingClient.runningRequests = [];
HTTPBindingClient.concurrentRequests = 4;
HTTPBindingClient.scheduleAsyncRequest = function(f, cancelable) {
debug('component schedule async request', 'running', HTTPBindingClient.runningRequests.length, 'queued', HTTPBindingClient.queuedRequests.length);
// Queue the request function
var req = new Object();
req.f = f;
req.cancelable = cancelable;
req.canceled = false;
HTTPBindingClient.queuedRequests[HTTPBindingClient.queuedRequests.length] = req;
// Execute any requests in the queue
return HTTPBindingClient.runAsyncRequests();
};
HTTPBindingClient.forgetRequest = function(req) {
req.http = undefined;
// Remove a request from the list of running requests
for (var i in HTTPBindingClient.runningRequests) {
if (HTTPBindingClient.runningRequests[i] == req) {
HTTPBindingClient.runningRequests.splice(i, 1);
debug('forget async request', 'running', HTTPBindingClient.runningRequests.length, 'queued', HTTPBindingClient.queuedRequests.length);
return true;
}
}
return false;
};
HTTPBindingClient.cancelRequests = function() {
debug('component cancel async requests', 'running', HTTPBindingClient.runningRequests.length, 'queued', HTTPBindingClient.queuedRequests.length);
// Cancel any cancelable in flight HTTP requests
for (var i in HTTPBindingClient.queuedRequests) {
var req = HTTPBindingClient.queuedRequests[i];
if (req.cancelable)
req.canceled = true;
}
for (var i in HTTPBindingClient.runningRequests) {
var req = HTTPBindingClient.runningRequests[i];
if (req.cancelable) {
req.canceled = true;
if (req.http) {
req.http.aborted = true;
req.http.abort();
req.http = undefined;
debug('component abort async request', 'running', HTTPBindingClient.runningRequests.length, 'queued', HTTPBindingClient.queuedRequests.length);
}
}
}
// Flush the queue
return HTTPBindingClient.runAsyncRequests();
}
HTTPBindingClient.runAsyncRequests = function() {
// Stop now if we already have enough requests running or there's no request in the queue
if(HTTPBindingClient.runningRequests.length >= HTTPBindingClient.concurrentRequests || HTTPBindingClient.queuedRequests.length == 0)
return true;
// Run the first request in the queue
var req = HTTPBindingClient.queuedRequests.shift();
if (!req.canceled) {
HTTPBindingClient.runningRequests[HTTPBindingClient.runningRequests.length] = req;
debug('component run async request', 'running', HTTPBindingClient.runningRequests.length, 'queued', HTTPBindingClient.queuedRequests.length);
if (req.canceled) {
HTTPBindingClient.forgetRequest(req);
debug('component canceled timed async request', 'running', HTTPBindingClient.runningRequests.length, 'queued', HTTPBindingClient.queuedRequests.length);
return false;
}
HTTPBindingClient.delay(function asyncRequest() {
try {
req.http = new XMLHttpRequest();
req.http.aborted = false;
return req.f(req.http, function asyncRequestDone() {
// Execute any requests left in the queue
HTTPBindingClient.forgetRequest(req);
debug('component done async request', 'running', HTTPBindingClient.runningRequests.length, 'queued', HTTPBindingClient.queuedRequests.length);
HTTPBindingClient.runAsyncRequests();
return true;
});
} catch(e) {
// Execute any requests left in the queue
HTTPBindingClient.forgetRequest(req);
debug('component async request error', 'running', HTTPBindingClient.runningRequests.length, 'queued', HTTPBindingClient.queuedRequests.length, 'error', e);
HTTPBindingClient.runAsyncRequests();
}
return false;
});
} else {
debug('component canceled queued async request', 'running', HTTPBindingClient.runningRequests.length, 'queued', HTTPBindingClient.queuedRequests.length);
}
// Execute any requests left in the queue
HTTPBindingClient.runAsyncRequests();
};
/**
* Get a cache item from local storage.
*/
HTTPBindingClient.getCacheItem = function(k) {
var ls = lstorage || localStorage;
return ls.getItem('dc.d.' + k);
};
/**
* Set a cache item in local storage.
*/
HTTPBindingClient.setCacheItem = function(k, v) {
if (v && v.length > 65535)
return HTTPBindingClient.removeCacheItem(k);
HTTPBindingClient.collectCacheItems();
var ls = lstorage || localStorage;
var s = ls.getItem('dc.size');
var size = s? parseInt(s) : 0;
var ov = ls.getItem('dc.d.' + k);
var nsize = size - (ov? ov.length : 0) + (v? v.length : 0);
if (nsize != size)
ls.setItem('dc.size', nsize.toString());
return ls.setItem('dc.d.' + k, v);
};
/**
* Remove a cache item from local storage.
*/
HTTPBindingClient.removeCacheItem = function(k) {
var ls = lstorage || localStorage;
var s = ls.getItem('dc.size');
var size = s? parseInt(s) : 0;
var ov = ls.getItem('dc.d.' + k);
if (ov) {
var nsize = size - ov.length;
ls.setItem('dc.size', nsize.toString());
}
return ls.removeItem('dc.d.' + k);
};
/**
* Keep local storage cache entries under 2MB.
*/
HTTPBindingClient.maxCacheSize = /* 20000; */ 2097152;
HTTPBindingClient.collectCacheSize = /* 10000; */ 1048576;
HTTPBindingClient.collectCacheItems = function() {
var ls = window.lstorage || localStorage;
var nkeys = window.lstorage? function() { return ls.length(); } : function() { return ls.length; };
// Get the current cache size
var size = 0;
var s = ls.getItem('dc.size');
if(!s) {
// Calculate and store initial cache size
debug('component calculating cache size');
var n = nkeys();
for(var i = 0; i < n; i++) {
var k = ls.key(i);
if(!k || k.substr(0, 5) != 'dc.d.')
continue;
var v = ls.getItem(k);
if(!v)
continue;
size += v.length;
}
ls.setItem('dc.size', size.toString());
} else
size = parseInt(s);
// Nothing to do if it's below the max size
debug('component cache size', size);
if (size <= HTTPBindingClient.maxCacheSize)
return false;
// Collect random cache entries until we reach our min size
debug('component collecting cache items');
var keys = [];
var n = nkeys();
for(var i = 0; i < n; i++) {
var k = ls.key(i);
if(!k || k.substr(0, 5) != 'dc.d.')
continue;
keys[keys.length] = k;
}
while (keys.length != 0 && size >= HTTPBindingClient.collectCacheSize) {
var r = Math.floor(keys.length * Math.random());
if (r == keys.length)
continue;
var k = keys[r];
var v = ls.getItem(k);
debug('component collect cache item', k);
ls.removeItem(k);
keys.splice(r, 1);
if (v)
size = size - v.length;
}
// Store the new cache size
debug('component updated cache size', size);
ls.setItem('dc.size', size.toString());
return true;
};
/**
* Apply a function remotely using JSON-RPC.
*/
HTTPBindingClient.prototype.jsonApply = function(req) {
var hascb = req.cb? true : false;
// Call asynchronously with a callback
if(hascb) {
var u = this.uri;
return HTTPBindingClient.scheduleAsyncRequest(function jsonApplyRequest(http, done) {
http.open('POST', u, true);
http.setRequestHeader('Accept', '*/*');
http.setRequestHeader('Content-Type', 'application/json-rpc');
http.setRequestHeader('X-Cache-Control', 'no-cache');
http.onreadystatechange = function() {
if(http.readyState == 4) {
// Pass the result or exception
if(http.status == 200) {
var res = HTTPBindingClient.jsonResult(http);
req.cb(res);
} if(!http.aborted) {
error('jsonApply error', 'status', http.status, http.statusText);
req.cb(undefined, new Error('' + http.status + ' ' + http.statusText));
}
return done();
}
};
// Send the request
http.send(req.data);
return req.id;
}, false);
}
// Call synchronously and return the result or exception
var http = new XMLHttpRequest();
http.open('POST', this.uri, false);
http.setRequestHeader('Accept', '*/*');
http.setRequestHeader('Content-Type', 'application/json-rpc');
http.setRequestHeader('X-Cache-Control', 'no-cache');
http.send(req.data);
if(http.status == 200)
return HTTPBindingClient.jsonResult(http);
error('jsonApply error', 'status', http.status, http.statusText);
throw new Error('' + http.status + ' ' + http.statusText);
};
/**
* REST GET method.
*/
HTTPBindingClient.prototype.get = function(id, cb, mode) {
var u = id? (this.uri? this.uri + '/' + id : id) : this.uri;
var hascb = cb? true : false;
// Get from local storage first
var item;
if(mode != 'remote') {
item = HTTPBindingClient.getCacheItem(u);
if(item && item != '') {
if(!hascb)
return item;
// Pass local result to callback
cb(item);
}
}
// Call asynchronously with a callback
if(hascb) {
return HTTPBindingClient.scheduleAsyncRequest(function getRequest(http, done) {
http.open('GET', u, true);
http.setRequestHeader('Accept', '*/*');
http.setRequestHeader('X-Cache-Control', 'no-cache');
http.onreadystatechange = function() {
if(http.readyState == 4) {
// Pass result if different from local result
//debug('readystate', http.readyState, 'status', http.status, 'headers', http.getAllResponseHeaders());
if(http.status == 200) {
var ct = http.getResponseHeader('Content-Type');
if(http.responseText == '' || !ct || ct == '') {
// Report empty response
error('get received empty response', 'url', u);
cb(undefined, new Error('500 No-Content'));
return done();
} else if(!item || http.responseText != item) {
// Store retrieved entry in local storage
//debug('received response', 'url', u, 'response', http.responseText);
if(http.responseText != null)
HTTPBindingClient.setCacheItem(u, http.responseText);
cb(http.responseText);
return done();
}
} else if (http.status == 403) {
// Redirect to login page
error('get received 403 response', 'url', u);
var le = new Error('' + http.status + ' ' + http.statusText);
if(window.onloginredirect)
window.onloginredirect(le);
cb(undefined, le);
return done();
} else if(!http.aborted) {
// Pass exception if we didn't have a local result
error('get received error', 'url', u, 'status', http.status, http.statusText);
if(!item) {
cb(undefined, new Error('' + http.status + ' ' + http.statusText));
return done();
}
}
return done();
}
};
// Send the request
http.send(null);
return true;
}, true);
}
// Call synchronously and return the result or exception
var http = new XMLHttpRequest();
http.open('GET', u, false);
http.setRequestHeader('Accept', '*/*');
http.setRequestHeader('X-Cache-Control', 'no-cache');
http.send(null);
if(http.status == 200) {
var ct = http.getResponseHeader('Content-Type');
if(http.responseText == '' || !ct || ct == '') {
// Report empty response
error('get received empty response', 'url', u);
throw new Error('500 No Content');
}
return http.responseText;
}
if(http.status == 403) {
// Redirect to login page
error('get received 403 response', 'url', u);
var le = new Error('' + http.status + ' ' + http.statusText);
if(window.onloginredirect)
window.onloginredirect(le);
throw le;
}
error('get received error', 'url', u, 'status', http.status, http.statusText);
throw new Error('' + http.status + ' ' + http.statusText);
};
/**
* REST POST method.
*/
HTTPBindingClient.prototype.post = function (entry, cb) {
var hascb = cb? true : false;
// Call asynchronously with a callback
if(hascb) {
var u = this.uri;
return HTTPBindingClient.scheduleAsyncRequest(function postRequest(http, done) {
http.open('POST', u, true);
http.setRequestHeader('Accept', '*/*');
http.setRequestHeader('Content-Type', 'application/atom+xml');
http.setRequestHeader('X-Cache-Control', 'no-cache');
http.onreadystatechange = function() {
if(http.readyState == 4) {
if(http.status == 201) {
// Successful result
cb(http.responseText);
}
else {
// Report status code as an exception
cb(undefined, new Error('' + http.status + ' ' + http.statusText));
}
return done();
}
};
// Send the request
http.send(entry);
return true;
}, false);
}
// Call synchronously
var http = new XMLHttpRequest();
var hascb = cb? true : false;
http.open('POST', this.uri, false);
http.setRequestHeader('Accept', '*/*');
http.setRequestHeader('Content-Type', 'application/atom+xml');
http.setRequestHeader('X-Cache-Control', 'no-cache');
http.send(entry);
if(http.status == 201)
return http.responseText;
// Return status code as an exception
throw new Error('' + http.status + ' ' + http.statusText);
};
/**
* REST PUT method.
*/
HTTPBindingClient.prototype.put = function(id, entry, cb, mode) {
var u = id? (this.uri? this.uri + '/' + id : id) : this.uri;
var hascb = cb? true : false;
// Update local storage
var oentry;
if(mode != 'remote') {
oentry = HTTPBindingClient.getCacheItem(u);
HTTPBindingClient.setCacheItem(u, entry);
}
// Call asynchronously with a callback
if(hascb) {
return HTTPBindingClient.scheduleAsyncRequest(function putRequest(http, done) {
http.open('PUT', u, true);
http.setRequestHeader('Accept', '*/*');
http.setRequestHeader('Content-Type', 'application/atom+xml');
http.setRequestHeader('X-Cache-Control', 'no-cache');
http.onreadystatechange = function() {
if(http.readyState == 4) {
if(http.status == 200) {
// Successful result
cb();
} else {
if(http.status == 404) {
// Undo local storage update
if(mode != 'remote') {
if(oentry)
HTTPBindingClient.setCacheItem(u, oentry);
else
HTTPBindingClient.removeCacheItem(u);
}
}
// Report status code as an exception
cb(new Error('' + http.status + ' ' + http.statusText));
}
return done();
}
};
// Send the request
http.send(entry);
return true;
}, false);
}
// Call synchronously
var http = new XMLHttpRequest();
http.open('PUT', u, false);
http.setRequestHeader('Accept', '*/*');
http.setRequestHeader('Content-Type', 'application/atom+xml');
http.setRequestHeader('X-Cache-Control', 'no-cache');
http.send(entry);
if(http.status == 200)
return true;
if(http.status == 404) {
// Undo local storage update
if(mode != 'remote') {
if(oentry)
HTTPBindingClient.setCacheItem(u, oentry);
else
HTTPBindingClient.removeCacheItem(u);
}
}
// Return status code as an exception
throw new Error('' + http.status + ' ' + http.statusText);
};
/**
* REST DELETE method.
*/
HTTPBindingClient.prototype.del = function(id, cb, mode) {
var u = id? (this.uri? this.uri + '/' + id : id) : this.uri;
var hascb = cb? true : false;
// Update local storage
var ls = window.lstorage || localStorage;
if(mode != 'remote')
HTTPBindingClient.removeCacheItem(u);
// Call asynchronously with a callback
if(hascb) {
return HTTPBindingClient.scheduleAsyncRequest(function delRequest(http, done) {
http.open('DELETE', u, true);
http.setRequestHeader('Accept', '*/*');
http.setRequestHeader('X-Cache-Control', 'no-cache');
http.onreadystatechange = function() {
if(http.readyState == 4) {
if(http.status == 200) {
// Successful result
cb();
}
else {
// Report status code as an exception
cb(new Error('' + http.status + ' ' + http.statusText));
}
return done();
}
};
// Send the request
http.send(null);
return true;
}, false);
}
// Call synchronously
var http = new XMLHttpRequest();
http.open('DELETE', u, false);
http.setRequestHeader('Accept', '*/*');
http.setRequestHeader('X-Cache-Control', 'no-cache');
http.send(null);
if(http.status == 200)
return true;
// Report status code as an exception
throw new Error('' + http.status + ' ' + http.statusText);
};
/**
* Public API.
*/
var sca = {};
/**
* Return an HTTP client proxy.
*/
sca.httpClient = function(name, uri, domain) {
return new HTTPBindingClient(name, uri, domain);
};
/**
* Return a component proxy.
*/
sca.component = function(name, domain) {
if(!domain)
return new HTTPBindingClient(name, '/c/' + name, domain);
return new HTTPBindingClient(name, '/' + domain + '/c/' + name, domain);
};
/**
* Return a reference proxy.
*/
sca.reference = function(comp, rname) {
if(!comp.domain)
return new HTTPBindingClient(comp.name + '/' + rname, '/r/' + comp.name + '/' + rname, comp.domain);
return new HTTPBindingClient(comp.name + '/' + rname, '/' + comp.domain + '/r/' + comp.name + '/' + rname, comp.domain);
};
/**
* Add proxy functions to a reference proxy.
*/
sca.defun = function(ref) {
function defapply(name) {
return function() {
var args = [];
args[0] = name;
for(var i = 0; i < arguments.length; i++)
args[i + 1] = arguments[i];
return this.apply.apply(this, args);
};
}
for(var f = 1; f < arguments.length; f++) {
var fn = arguments[f];
ref[fn]= defapply(fn);
}
return ref;
};