/*
* Modelled Trace API
*
* This file is part of ktbs4js.
*
* ktbs4js is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* ktbs4js is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with ktbs4js. If not, see <http://www.gnu.org/licenses/>.
*
*/
(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['jquery'], factory);
} else {
// Browser globals
window.tracemanager = factory(jQuery);
}
}(function ($) {
/**
* @constant
* @desc If there are more than MAX_FAILURE_COUNT synchronisation failures, then disable synchronisation
*/
var MAX_FAILURE_COUNT = 20;
/**
* @constant
* @desc If there are more than MAX_BUFFER_SIZE obsels in the buffer, then "compress" them as a single "ktbsFullBuffer"
*/
var MAX_BUFFER_SIZE = 500;
/** Log a message
*/
function logmsg() {
if (window.console) {
window.console.log.apply(console, [ "ktbs4js" ].concat([].slice.call(arguments)));
}
};
/**
* @name BufferedService
* @class
* @constructor
* @param {string} url: the Trace url
* @param {string} mode: the HTTP method to use ("GET" or "POST")
* @param {string} format: the serialization format ("json", "json-compact", "turtle")
* @param {boolean} handshake: if true, send an initial handshake to the server
*
* @desc It takes care of enqueuing obsels and sending them possibly in batches to the server.
*/
function BufferedService(url, mode, format, handshake) {
this.url = url;
this.buffer = [];
this.isReady = !handshake;
this.timer = null;
this.failureCount = 0;
// sync_mode is either "none", "sync" or "buffered"
this.sync_mode = "none";
/* mode can be either POST or GET */
if (mode == 'POST' || mode == 'GET')
this.mode = mode;
else
this.mode = 'POST';
/* Data format */
this.format = (this.mode === 'GET' ? 'json-compact' : 'json');
if (format !== undefined)
this.format = format;
/* Flush buffer every timeOut ms if the sync_mode is delayed */
this.timeOut = 2000;
};
BufferedService.prototype = /** @lends BufferedService.prototype */ {
// url: "",
// buffer: [],
// isReady: false,
// timer: null,
// failureCount: 0,
/**
* Flush buffer
*/
flush: function() {
// FIXME: add mutex on this.buffer
if (! this.isReady)
{
logmsg("Sync service not ready");
} else if (this.failureCount > MAX_FAILURE_COUNT)
{
logmsg("Too many failures, disabling trace synchronisation");
// Disable synchronisation
this.set_sync_mode('none');
} else if (this.buffer.length) {
var temp = this.buffer;
this.buffer = [];
var content_type = 'application/json';
var data;
if (this.format === 'turtle') {
content_type = "text/turtle";
data = [ "@prefix : <http://liris.cnrs.fr/silex/2009/ktbs#>.\n",
// Assume that the model for a trace has for URI <trace_uri> + "model#"
"@prefix m: <model#>.\n" ]
.concat(temp.map(function(o) { return o.toTurtle(); }))
.join("\n");
} else if (this.format === 'json-compact') {
content_type = "application/x-json-compact";
// We mark the "compressed" nature of the
// generated JSON by prefixing it with c
data = 'c' + JSON.stringify(temp.map(function (o) { return o.toCompactJSON(); }));
// Swap " (very frequent, which will be
// serialized into %22) and ; (rather rare), this
// saves some bytes
data = data.replace(/[;"#]/g, function(s){ return s == ';' ? '"' : ( s == '"' ? ';' : '%23'); });
} else {
// Default format: json
data = JSON.stringify(temp.map(function (o) { return o.toJSON(); }));
}
if (this.mode == 'GET')
{
data = "post=" + encodeURIComponent(data);
if (content_type !== 'application/x-json-compact')
data = 'content-type=' + content_type + '&' + data;
// FIXME: check data length (< 2K is safe)
var request=$('<img />').error( function() { this.failureCount += 1; })
.load( function() { this.failureCount = 0; })
.attr('src', this.url + '?' + data);
}
else
{
$.ajax({ url: this.url,
type: 'POST',
contentType: content_type,
data: data,
processData: false,
// Type of the returned data.
dataType: "text",
error: function(jqXHR, textStatus, errorThrown) {
logmsg("Error when sending buffer:", textStatus + ' ' + JSON.stringify(errorThrown));
this.failureCount += 1;
},
success: function(data, textStatus, jqXHR) {
// Reset failureCount to 0 as soon as there is 1 valid answer
this.failureCount = 0;
}
});
}
}
},
/**
* Set sync mode: "delayed", "sync" (immediate sync), "none"
* (no synchronisation with server, the trace has to be
* explicitly saved if needed).
* @param {string} mode: "delayed", "sync" or "none"
* @param {string} default_subject: Default subject id
*/
set_sync_mode: function(mode, default_subject) {
this.sync_mode = mode;
if (! this.isReady && mode !== "none")
this.init(default_subject);
if (mode == 'delayed') {
this.start_timer();
} else {
this.stop_timer();
}
},
/**
* Enqueue an obsel
* @param {Obsel} obsel: the obsel to enqueue
**/
enqueue: function(obsel) {
if (this.buffer.length > MAX_BUFFER_SIZE)
{
obsel = new Obsel(undefined, 'ktbsFullBuffer', this.buffer[0].begin,
this.buffer[this.buffer.length - 1].end, this.buffer[0].subject);
obsel.trace = this.buffer[0].trace;
this.buffer = [];
}
this.buffer.push(obsel);
if (this.sync_mode === 'sync') {
// Immediate sync of the obsel.
this.flush();
}
},
/**
* Start the timer for delayed sync
*/
start_timer: function() {
var self = this;
if (this.timer === null) {
this.timer = window.setInterval(function() {
self.flush();
}, this.timeOut);
}
},
/**
* Stop the timer for delayed sync
*/
stop_timer: function() {
if (this.timer !== null) {
window.clearInterval(this.timer);
this.timer = null;
}
},
/**
* Initialize the sync service
* @param {string} default_subject: the default subject
*/
init: function(default_subject) {
var self = this;
if (this.isReady)
/* Already initialized */
return;
if (typeof default_subject === 'undefined' || default_subject === "")
{
this.isReady = true;
return;
}
if (this.mode == 'GET')
{
var request=$('<img/>').attr('src', this.url + 'login?userinfo={"default_subject": "' + default_subject + '"}');
// Do not wait for the return, assume it is
// initialized. This assumption will not work anymore
// if login returns some necessary information
this.isReady = true;
}
else
{
$.ajax({ url: this.url + 'login',
type: 'POST',
data: 'userinfo={"default_subject":"' + default_subject + '"}',
success: function(data, textStatus, jqXHR) {
self.isReady = true;
if (self.buffer.length) {
self.flush();
}
}
});
}
}
};
/**
* @name Trace
* @class
* @constructor
* @param {string} uri: the Trace uri
* @param {string} mode: the access mode ("r", "w" or "rw")
* @param {string} requestmode: the HTTP method used for synchronization ("GET" or "POST")
* @param {string} format: the serialization format (see {@link BufferedService})
* @param {boolean} handshake: see {@link BufferedService}
*/
function Trace(uri, mode, requestmode, format, handshake) {
/* FIXME: We could/should use a sorted list such as
http://closure-library.googlecode.com/svn/docs/class_goog_structs_AvlTree.html
to speed up queries based on time */
this.obsels = [];
/* Trace URI */
if (uri === undefined)
uri = "";
if (mode === undefined)
mode = "r";
this.mode = mode;
this.uri = uri;
this.sync_mode = "none";
this.default_subject = "";
this.shorthands = {};
/* baseuri is used a the base URI to resolve relative attribute names in obsels */
this.baseuri = "";
if (this.mode.indexOf("w") >= 0) {
this.syncservice = new BufferedService(uri, requestmode, format, handshake);
$(window).unload( function () {
if (this.syncservice && this.sync_mode !== 'none') {
this.syncservice.flush();
this.syncservice.stop_timer();
}
});
}
if (this.mode.indexOf("r") >= 0) {
// Now that all is set up, we can try to load existing obsels.
this.load_obsels();
}
};
Trace.prototype = /** @lends Trace.prototype */ {
/* FIXME: We could/should use a sorted list such as
http://closure-library.googlecode.com/svn/docs/class_goog_structs_AvlTree.html
to speed up queries based on time */
/** List of obsels */
obsels: [],
/** Trace URI */
uri: "",
/** mode defines the intended usage mode of the trace. It can be either
"r" : read-only - we will only read existing obsels
"w" : write-only - we will only create obsels to be stored on a server
"rw" : read-write - we will both read and write obsels.
*/
mode: "rw",
/** Default subject */
default_subject: "",
/** baseuri is used as the base URI to resolve relative
* attribute-type names in obsels. Strictly speaking, this
* should rather be expressed as a reference to model, or
* more generically, as a qname/URI dict */
baseuri: "",
/** Mapping of obsel type or property name to a compact
* representation (shorthands).
*/
shorthands: null,
syncservice: null,
/** Define the trace URI */
set_uri: function(uri) {
this.uri = uri;
},
/** Get the trace URI */
get_uri: function() {
return this.uri;
},
/** Get the trace id */
get_id: function() {
var i = this.uri.split("/").reverse();
if (i[0] !== "")
return i[0];
else
return i[1];
},
/** Indicates wether the trace can be written to */
get_readonly: function() {
return (this.mode.indexOf("w") === -1);
},
/** Remove the trace from the server */
remove: function() {
$.ajax({ url: this.uri,
type: 'DELETE',
async: false,
error: function(jqXHR, textStatus, errorThrown) {
throw "Cannot delete trace " + this.uri + ": " + textStatus + ' ' + JSON.stringify(errorThrown);
}
});
return true;
},
/**
* Set sync mode. See {@link BufferedService.set_sync_mode} for details.
* @param {string} mode: "delayed", "sync" or "none"
*/
set_sync_mode: function(mode) {
if (this.syncservice !== null) {
this.syncservice.set_sync_mode(mode, this.default_subject);
}
},
/** Load obsels from the Trace url
* options is an object that contains optional parameters:
*
* - page: the page number
* - pagesize: the page size
* - from: the minimum timestamp
* - to: the maximum timestamp
*
* @param {object} options: optional parameters
*/
load_obsels: function(options) {
var self = this;
var params = [];
if (options !== undefined) {
if (options.page !== undefined)
params.push("page=" + options.page);
if (options.pagesize !== undefined)
params.push("pageSize=" + options.pagesize);
if (options.from !== undefined)
// FIXME: convert from Date to ms if necessary
params.push("from=" + options.from);
if (options.to !== undefined)
params.push("to=" + options.to);
}
$.ajax({ url: this.uri + "@obsels" + (params.length ? ("?" + params.join('&')) : ""),
type: 'GET',
// Type of the returned data.
dataType: "json",
statusCode: {
413: function(jqXHR, textStatus, errorThrown) {
// Entity request too large.
// Resend query with restriction
self.load_obsels({ page: 1 });
}
},
error: function(jqXHR, textStatus, errorThrown) {
logmsg("Cannot load obsels: ", textStatus + ' ' + JSON.stringify(errorThrown));
},
success: function(data, textStatus, jqXHR) {
// Parse received data to populate this.obsels
self.parseJSON(data);
}
});
},
/** Force trace refresh */
force_state_refresh: function() {
this.load_obsels();
},
/**
* Return a list of the obsels of this trace matching the parameters
* @param {integer} _begin: the minimum begin time
* @param {integer} _end: the maximum end time
* @param {boolean} _reverse: return obsels in reverse chronological order
* @return {list} a list of {@link Obsel}
*/
list_obsels: function(_begin, _end, _reverse) {
var res;
if (typeof _begin !== 'undefined' || typeof _end !== 'undefined') {
/*
* Not optimized yet.
*/
res = [];
var l = this.obsels.length;
for (var i = 0; i < l; i++) {
var o = this.obsels[i];
if ((typeof _begin !== 'undefined' && o.begin > _begin) && (typeof _end !== 'undefined' && o.end < _end)) {
res.push(o);
}
}
}
if (typeof _reverse !== 'undefined') {
if (res !== undefined) {
/* Should reverse the whole list. Make a copy. */
res = this.obsels.slice(0);
}
res.sort(function(a, b) { return b.begin - a.begin; });
return res;
}
if (res === undefined) {
res = this.obsels;
}
return res;
},
/**
* Return the obsel of this trace identified by the URI, or undefined
* @return {Obsel} an obsel
*/
get_obsel: function(id) {
for (var i = 0; i < this.obsels.length; i++) {
/* FIXME: should check against variations of id/uri, take this.baseuri into account */
if (this.obsels[i].uri === id) {
return this.obsels[i];
}
}
return undefined;
},
/** Set the default subject
* @param {string} subject
*/
set_default_subject: function(subject) {
// FIXME: if we call this method after the sync_service
// init method, then the default_subject will not be
// consistent anymore. Maybe we should then call init() again?
this.default_subject = subject;
},
/** Get the default subject
* @return {string} the default subject
*/
get_default_subject: function() {
return this.default_subject;
},
/* (ident: id, type:ObselType, begin:int, end:int?, subject:str?, attributes:[AttributeType=>any]?) */
/** Create a new obsel and add it to the trace
* @param {string} ident: the obsel id
* @param {string} type: the obsel type
* @param {integer} begin: the obsel begin time
* @param {integer} end: the obsel end time
* @param {object} _attributes: the obsel attributes
* @return {Obsel} the created obsel
*/
create_obsel: function(ident, type, begin, end, subject, _attributes) {
var o = new Obsel(ident, type, begin, end, subject);
if (typeof _attributes !== 'undefined') {
o.attributes = _attributes;
}
o.trace = this;
this.obsels.push(o);
if (this.syncservice !== null)
this.syncservice.enqueue(o);
$(this).trigger('updated');
return o;
},
/* Helper methods */
/**
* Parse a JSON-LD trace representation and add resulting
* obsels to the trace obsels.
*/
parseJSON: function(data) {
var self = this;
// FIXME: check @context
// Parse received data to populate this.obsels
if (data.hasOwnProperty('obsels')) {
var o;
data.obsels.forEach(function(j) {
o = (new Obsel()).fromJSON(j);
o.trace = this;
self.obsels.push(o);
});
$(this).trigger('updated');
}
},
/** Create a new obsel with the given attributes
* @param {string} type: the obsel type
* @param {object} _attributes: the obsel attributes
* @param {integer} [_begin]: the obsel begin time (current time if omitted)
* @param {integer} [_end]: the obsel end time (same as begin if omitted)
* @param {string} [_subject]: the obsel subject (trace default subject if omitted)
*/
trace: function(type, _attributes, _begin, _end, _subject) {
if (typeof begin === 'undefined') {
var t = (new Date()).getTime();
_begin = t;
}
if (typeof end === 'undefined') {
_end = _begin;
}
if (typeof subject === 'undefined') {
_subject = this.default_subject;
}
if (typeof _attributes === 'undefined') {
_attributes = {};
}
return this.create_obsel(undefined, type, _begin, _end, _subject, _attributes);
}
};
/**
* @name Obsel
* @class
* @constructor
* @param {string} ident: the obsel id
* @param {string} type: the obsel type
* @param {integer} begin: the obsel begin time
* @param {integer} end: the obsel end time
* @param {string} subject: the obsel subject
* @param {object} attributes: the obsel attributes
*/
function Obsel(ident, type, begin, end, subject, attributes) {
this.trace = undefined;
this.uri = "";
this.id = ident || "";
this.type = type;
this.begin = begin;
this.end = end;
this.subject = subject;
/* Is the obsel synced with the server ? */
this.sync_status = false;
/* Dictionary indexed by ObselType URIs */
this.attributes = {};
};
Obsel.prototype = /** @lends Obsel.prototype */ {
/* The following attributes are here for documentation
* purposes. They MUST be defined in the constructor
* function. */
trace: undefined,
type: undefined,
begin: undefined,
end: undefined,
subject: undefined,
/* Dictionary indexed by ObselType URIs */
attributes: {},
/** Get the Obsel URI
* @return {string} the URI
*/
get_uri: function() {
if (this.uri)
return this.uri;
else if (this.id && this.trace !== undefined)
return (this.trace.uri + this.id);
else
return "";
},
/** Get the Obsel id
* @return {string} the id
*/
get_id: function() {
return this.id;
},
/** Force reload */
force_state_refresh: function() {
$.ajax({ url: this.uri,
type: 'GET',
// Type of the returned data.
dataType: "json",
error: function(jqXHR, textStatus, errorThrown) {
logmsg("Cannot refresh obsel " + this.uri + ": ", textStatus + ' ' + JSON.stringify(errorThrown));
},
success: function(data, textStatus, jqXHR) {
// Parse received data to populate this.obsels
self.fromJSON(data);
}
});
},
// FIXME: to implement
get_readonly: function() {
return true;
},
/** Delete the obsel from the trace
* @throws Will throw an exception if it could not be removed.
*/
remove: function() {
$.ajax({ url: this.uri,
type: 'DELETE',
async: false,
error: function(jqXHR, textStatus, errorThrown) {
throw "Cannot delete obsel " + this.uri + ": " + textStatus + ' ' + JSON.stringify(errorThrown);
}
});
return true;
},
// FIXME: implement properly
get_label: function() {
return this.id;
},
get_trace: function() {
return this.trace;
},
get_obsel_type: function() {
return this.type;
},
get_begin: function() {
return this.begin;
},
get_end: function() {
return this.end;
},
get_subject: function() {
return this.subject;
},
/** List attribute types
*/
list_attribute_types: function() {
var result = [];
for (var prop in this.attributes) {
if (this.attributes.hasOwnProperty(prop))
result.push(prop);
}
/* FIXME: we return URIs here instead of AttributeType elements */
return result;
},
list_relation_types: function() {
/* FIXME: not implemented yet */
},
list_related_obsels: function (rt) {
/* FIXME: not implemented yet */
},
list_inverse_relation_types: function () {
/* FIXME: not implemented yet */
},
list_relating_obsels: function (rt) {
/* FIXME: not implemented yet */
},
/**
* Return the value of the given attribute type for this obsel
*/
get_attribute_value: function(at) {
if (typeof at === "string")
/* It is a URI */
return this.attributes[at];
else
/* FIXME: check that at is instance of AttributeType */
return this.attributes[at.uri];
},
/* obsel modification (trace amendment) */
/** Set the value of an attribute */
set_attribute_value: function(at, value) {
if (typeof at === "string")
/* It is a URI */
this.attributes[at] = value;
/* FIXME: check that at is instance of AttributeType */
else
this.attributes[at.uri] = value;
},
/** Delete an attribute */
del_attribute_value: function(at) {
if (typeof at === "string")
/* It is a URI */
delete this.attributes[at];
/* FIXME: check that at is instance of AttributeType */
else
delete this.attributes[at.uri];
},
/** Add a related obsel */
add_related_obsel: function(rt, value) {
/* FIXME: not implemented yet */
},
/** Delete a related obsel */
del_related_obsel: function(rt, value) {
/* FIXME: not implemented yet */
},
/**
* Return a JSON representation of the obsel
*/
toJSON: function() {
var r = {
"@id": this.id,
"@type": this.type,
"begin": this.begin,
"end": this.end,
"subject": this.subject
};
for (var prop in this.attributes) {
if (this.attributes.hasOwnProperty(prop))
r[prop] = this.attributes[prop];
}
return r;
},
PRIVATE_JSON_PROPERTIES: [ '@id', '@type', 'subject', 'begin', 'end' ],
/**
* Initialize the Obsel attributes from the given JSON-LD representation
* @param {object} j: a JSON-LD serialization
*/
fromJSON: function(j) {
this.id = j['@id'];
this.type = j['@type'];
this.subject = j.subject;
this.begin = j.begin;
this.end = j.end;
this.attributes = {};
for (var k in j) {
if (j.hasOwnProperty(k) && this.PRIVATE_JSON_PROPERTIES.indexOf(k) === -1) {
// Not a private property, copy it into attributes
this.attributes[k] = j[k];
}
}
return this;
},
/**
* Return a compact JSON representation of the obsel.
* Use predefined + custom shorthands for types/properties
*/
toCompactJSON: function() {
var r = {
"@t": (this.trace.shorthands.hasOwnProperty(this.type) ? this.trace.shorthands[this.type] : this.type),
"@b": this.begin
};
// Transmit subject only if different from default_subject
if (this.subject !== this.trace.default_subject)
r["@s"] = this.subject;
// Store duration (to save some bytes) and only if it is non-null
if (this.begin !== this.end)
r["@d"] = this.end - this.begin;
// Store id only if != ""
if (this.id !== "")
r["@i"] = this.id;
for (var prop in this.attributes) {
if (this.attributes.hasOwnProperty(prop))
{
var v = this.attributes[prop];
r[prop] = this.trace.shorthands.hasOwnProperty(v) ? this.trace.shorthands[v] : v;
}
}
return r;
},
/** Return a JSON string serialization */
toJSONstring: function() {
return JSON.stringify(this.toJSON());
},
/**
* Return a Turtle representation of the obsel,
* assuming that : is bound to the KTBS namespace,
* and that m: is bound to the model namespace.
*/
toTurtle: function () {
var prop;
var data = [ "[ :hasTrace <> ;",
" a m:" + this.type + " ;" ];
if (this.begin) { data.push(" :hasBegin " + this.begin + " ;"); }
if (this.end) { data.push(" :hasEnd " + this.end + " ;"); }
if (this.subject) { data.push(" :hasSubject \"" + this.subject + "\" ;"); }
for (prop in this.attributes) {
if (this.attributes.hasOwnProperty(prop)) {
data.push(" m:" + prop + " " + JSON.stringify(this.attributes[prop]));
}
}
data.push("] .\n");
return data.join("\n");
}
};
/**
* @name TraceManager
* @class
* @constructor
* @desc It manages a set of traces.
*/
function TraceManager() {
this.traces = {};
};
TraceManager.prototype = /** @lends TraceManager.prototype */ {
traces: [],
/**
* Return the trace with id name
* If it was not registered, return undefined.
* @param {string} name
*/
get_trace: function(name) {
return this.traces[name];
},
/**
* Explicitly create and initialize a new trace with the given name.
* The optional uri parameter allows to initialize the trace URI.
* If another trace exists with the same name, then it is replaced by a new one.
*
* See the {@link Trace} documentation for the definition of parameters. They are:
*
* - url: the Trace url (for reading or writing)
* - mode: the access mode - "r", "w", "rw" (default)
* - requestmode: the HTTP method for creating obsels - "GET", "POST" (default)
* - syncmode: the synchronization mode - "delayed", "sync", "none" (default)
* - format: the serialization format - "json" (default), "json-compact", "turtle"
* - default_subject: the default subject id (default: "")
* - handshake: is an initial handshake needed (default: false)?
*
* @param {string} name: the trace name
* @param {object} params: the trace parameters
*/
init_trace: function(name, params)
{
var url = params.url ? params.url : "";
var mode = params.mode || "rw";
var requestmode = params.requestmode ? params.requestmode : "POST";
var syncmode = params.syncmode ? params.syncmode : "none";
var format = params.format || "json";
var default_subject = params.default_subject || "";
var handshake = params.handshake;
var t = new Trace(url, mode, requestmode, format, handshake);
t.set_default_subject(default_subject);
t.set_sync_mode(syncmode);
this.traces[name] = t;
return t;
}
};
/** @name tracemanager
* @desc The {@link TraceManager} instance (singleton)
*/
var tracemanager = new TraceManager();
return tracemanager;
}));