| /* |
| * 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; |
| }; |
| |