Commit 720d75aa authored by Ivan Tyagov's avatar Ivan Tyagov

Merge branch 'GadgetCatalog'

parents 8650bed5 18958049
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global jIO: true, $: true, btoa: true */
// test here: http://enable-cors.org/
//http://metajack.im/2010/01/19/crossdomain-ajax-for-xmpp-http-binding-made-easy
jIO.addStorageType('dav', function (spec, my) {
spec = spec || {};
var that, priv, super_serialized;
that = my.basicStorage(spec, my);
priv = {};
super_serialized = that.serialized;
priv.secureDocId = function (string) {
var split = string.split('/'),
i;
if (split[0] === '') {
split = split.slice(1);
}
for (i = 0; i < split.length; i += 1) {
if (split[i] === '') {
return '';
}
}
return split.join('%2F');
};
priv.convertSlashes = function (string) {
return string.split('/').join('%2F');
};
priv.restoreSlashes = function (string) {
return string.split('%2F').join('/');
};
/**
* Checks if an object has no enumerable keys
* @method objectIsEmpty
* @param {object} obj The object
* @return {boolean} true if empty, else false
*/
priv.objectIsEmpty = function (obj) {
var k;
for (k in obj) {
if (obj.hasOwnProperty(k)) {
return false;
}
}
return true;
};
// ==================== Attributes ====================
priv.username = spec.username || '';
priv.secured_username = priv.convertSlashes(priv.username);
priv.password = spec.password || '';
priv.url = spec.url || '';
that.serialized = function () {
var o = super_serialized();
o.username = priv.username;
o.url = priv.url;
o.password = priv.password;
return o;
};
priv.newAsyncModule = function () {
var async = {};
async.call = function (obj, function_name, arglist) {
obj._wait = obj._wait || {};
if (obj._wait[function_name]) {
obj._wait[function_name] -= 1;
return function () {};
}
// ok if undef or 0
arglist = arglist || [];
return obj[function_name].apply(obj[function_name], arglist);
};
async.neverCall = function (obj, function_name) {
obj._wait = obj._wait || {};
obj._wait[function_name] = -1;
};
async.wait = function (obj, function_name, times) {
obj._wait = obj._wait || {};
obj._wait[function_name] = times;
};
async.end = function () {
async.call = function () {};
};
return async;
};
/**
* Checks if a browser supports cors (cross domain ajax requests)
* @method checkCors
* @return {boolean} true if supported, else false
*/
priv.checkCors = function () {
return $.support.cors;
};
/**
* Replaces last "." with "_." in document filenames
* @method underscoreFileExtenisons
* @param {string} url url to clean up
* @return {string} clean_url cleaned up URL
*/
priv.underscoreFileExtenisons = function (url) {
var clean_url = url.replace(/,\s(\w+)$/, "_.$1");
return clean_url;
};
priv.restoreDots = function (url) {
var clean_url = url.replace(/_\./g, '.');
return clean_url;
};
// wedDav methods rfc4918 (short summary)
// COPY Reproduces single resources (files) and collections (directory
// trees). Will overwrite files (if specified by request) but will
// respond 209 (Conflict) if it would overwrite a tree
// DELETE deletes files and directory trees
// GET just the vanilla HTTP/1.1 behaviour
// HEAD ditto
// LOCK locks a resources
// MKCOL creates a directory
// MOVE Moves (rename or copy) a file or a directory tree. Will
// 'overwrite' files (if specified by the request) but will respond
// 209 (Conflict) if it would overwrite a tree.
// OPTIONS If WebDAV is enabled and available for the path this reports the
// WebDAV extension methods
// PROPFIND Retrieves the requested file characteristics, DAV lock status
// and 'dead' properties for individual files, a directory and its
// child files, or a directory tree
// PROPPATCHset and remove 'dead' meta-data properties
// PUT Update or create resource or collections
// UNLOCK unlocks a resource
// Notes: all Ajax requests should be CORS (cross-domain)
// adding custom headers triggers preflight OPTIONS request!
// http://remysharp.com/2011/04/21/getting-cors-working/
priv.putOrPost = function (command, type) {
var docid = command.getDocId(),
secured_docid,
url;
// no docId
if (!(typeof docid === "string" && docid !== "")) {
that.error({
"status": 405,
"statusText": "Method Not Allowed",
"error": "method_not_allowed",
"message": "Cannot create document which id is undefined",
"reason": "Document id is undefined"
});
return;
}
// no cross domain ajax
if (priv.checkCors === false) {
that.error({
"status": 405,
"statusText": "Method Not Allowed",
"error": "method_not_allowed",
"message": "Browser does not support cross domain ajax requests",
"reason": "cors is undefined"
});
return;
}
secured_docid = priv.secureDocId(command.getDocId());
url = priv.url + '/' + priv.underscoreFileExtenisons(secured_docid);
// see if the document exists
$.ajax({
url: url + '?_=' + Date.now(),
type: "GET",
async: true,
dataType: 'text',
crossdomain : true,
headers : {
Authorization: 'Basic ' + btoa(
priv.username + ':' + priv.password
)
},
// xhrFields: {withCredentials: 'true'},
success: function () {
if (type === 'POST') {
// POST the document already exists
that.error({
"status": 409,
"statusText": "Conflicts",
"error": "conflicts",
"message": "Cannot create a new document",
"reason": "Document already exists"
});
return;
}
// PUT update document
$.ajax({
url: url,
type: type,
data: JSON.stringify(command.getDoc()),
async: true,
crossdomain: true,
headers : {
Authorization: 'Basic ' + btoa(
priv.username + ':' + priv.password
)
},
// xhrFields: {withCredentials: 'true'},
success: function () {
that.success({
ok: true,
id: command.getDocId()
});
},
error: function () {
that.error({
"status": 409,
"statusText": "Conflicts",
"error": "conflicts",
"message": "Cannot modify document",
"reason": "Error trying to write to remote storage"
});
}
});
},
error: function (err) {
// Firefox returns 0 instead of 404 on CORS?
if (err.status === 404 || err.status === 0) {
$.ajax({
url: url,
type: 'PUT',
data: JSON.stringify(command.getDoc()),
async: true,
crossdomain: true,
headers : {
Authorization: 'Basic ' + btoa(
priv.username + ':' + priv.password
)
},
// xhrFields: {withCredentials: 'true'},
success: function () {
that.success({
ok: true,
id: command.getDocId()
});
},
error: function () {
that.error({
"status": 409,
"statusText": "Conflicts",
"error": "conflicts",
"message": "Cannot modify document",
"reason": "Error trying to write to remote storage"
});
}
});
} else {
// error accessing remote storage
that.error({
"status": err.status,
"statusText": err.statusText,
"error": "error",
"message": err.message,
"reason": "Failed to access remote storage"
});
}
}
});
};
/**
* Creates a new document
* @method post
* @param {object} command The JIO command
*/
that.post = function (command) {
priv.putOrPost(command, 'POST');
};
/**
* Creates or updates a document
* @method put
* @param {object} command The JIO command
*/
that.put = function (command) {
priv.putOrPost(command, 'PUT');
};
/**
* Add an attachment to a document
* @method putAttachment
* @param {object} command The JIO command
*/
that.putAttachment = function (command) {
var docid = command.getDocId(),
doc,
url,
secured_docid,
secured_attachmentid,
attachment_url;
// no docId
if (!(typeof docid === "string" && docid !== "")) {
that.error({
"status": 405,
"statusText": "Method Not Allowed",
"error": "method_not_allowed",
"message": "Cannot create document which id is undefined",
"reason": "Document id is undefined"
});
return;
}
// no cross domain ajax
if (priv.checkCors === false) {
that.error({
"status": 405,
"statusText": "Method Not Allowed",
"error": "method_not_allowed",
"message": "Browser does not support cross domain ajax requests",
"reason": "cors is undefined"
});
return;
}
secured_docid = priv.secureDocId(docid);
url = priv.url + '/' + priv.underscoreFileExtenisons(secured_docid);
// see if the underlying document exists
$.ajax({
url: url + '?_=' + Date.now(),
type: 'GET',
async: true,
dataType: 'text',
crossdomain : true,
headers : {
Authorization: 'Basic ' + btoa(
priv.username + ':' + priv.password
)
},
success: function (response) {
doc = JSON.parse(response);
// the document exists - update document
doc._attachments = doc._attachments || {};
doc._attachments[command.getAttachmentId()] = {
"content_type": command.getAttachmentMimeType(),
"digest": "md5-" + command.md5SumAttachmentData(),
"length": command.getAttachmentLength()
};
// put updated document data
$.ajax({
url: url + '?_=' + Date.now(),
type: 'PUT',
data: JSON.stringify(doc),
async: true,
crossdomain: true,
headers : {
Authorization: 'Basic ' + btoa(
priv.username + ':' + priv.password
)
},
// xhrFields: {withCredentials: 'true'},
success: function () {
secured_attachmentid = priv.secureDocId(command.getAttachmentId());
attachment_url = url + '.' +
priv.underscoreFileExtenisons(secured_attachmentid);
$.ajax({
url: attachment_url + '?_=' + Date.now(),
type: 'PUT',
data: JSON.stringify(command.getDoc()),
async: true,
crossdomain: true,
headers : {
Authorization: 'Basic ' + btoa(
priv.username + ':' + priv.password
)
},
// xhrFields: {withCredentials: 'true'},
success: function () {
that.success({
ok: true,
id: command.getDocId() + '/' + command.getAttachmentId()
});
},
error: function () {
that.error({
"status": 409,
"statusText": "Conflicts",
"error": "conflicts",
"message": "Cannot modify document",
"reason": "Error trying to save attachment to remote storage"
});
return;
}
});
},
error: function () {
that.error({
"status": 409,
"statusText": "Conflicts",
"error": "conflicts",
"message": "Cannot modify document",
"reason": "Error trying to write to remote storage"
});
return;
}
});
},
error: function () {
// the document does not exist
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Impossible to add attachment",
"reason": "Document not found"
});
return;
}
});
};
/**
* Get a document or attachment from distant storage
* Options:
* - {boolean} revs Add simple revision history (false by default).
* - {boolean} revs_info Add revs info (false by default).
* - {boolean} conflicts Add conflict object (false by default).
* @method get
* @param {object} command The JIO command
*/
that.get = function (command) {
var docid = command.getDocId(),
doc,
url,
secured_docid,
secured_attachmentid,
attachment_url;
// no docId
if (!(typeof docid === "string" && docid !== "")) {
that.error({
"status": 405,
"statusText": "Method Not Allowed",
"error": "method_not_allowed",
"message": "Cannot create document which id is undefined",
"reason": "Document id is undefined"
});
return;
}
// no cors support
if (priv.checkCors === false) {
that.error({
"status": 405,
"statusText": "Method Not Allowed",
"error": "method_not_allowed",
"message": "Browser does not support cross domain ajax requests",
"reason": "cors is undefined"
});
return;
}
secured_docid = priv.secureDocId(command.getDocId());
url = priv.url + '/' + priv.underscoreFileExtenisons(secured_docid);
if (typeof command.getAttachmentId() === "string") {
secured_attachmentid = priv.secureDocId(command.getAttachmentId());
attachment_url = url + '.' + priv.underscoreFileExtenisons(
secured_attachmentid
);
// get attachment
$.ajax({
url: attachment_url + '?_=' + Date.now(),
type: 'GET',
async: true,
dataType: 'text',
crossdomain : true,
headers : {
Authorization: 'Basic ' + btoa(
priv.username + ':' + priv.password
)
},
success: function (response) {
doc = JSON.parse(response);
that.success(doc);
},
error: function () {
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Cannot find the attachment",
"reason": "Attachment does not exist"
});
}
});
} else {
// get document
$.ajax({
url: url + '?_=' + Date.now(),
type: 'GET',
async: true,
dataType: 'text',
crossdomain : true,
headers : {
Authorization: 'Basic ' + btoa(
priv.username + ':' + priv.password
)
},
success: function (response) {
// metadata_only should not be handled by jIO, as it is a
// webDav only option, shouldn't it?
// ditto for content_only
doc = JSON.parse(response);
that.success(doc);
},
error: function () {
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Cannot find the document",
"reason": "Document does not exist"
});
}
});
}
};
/**
* Remove a document or attachment
* @method remove
* @param {object} command The JIO command
*/
that.remove = function (command) {
var docid = command.getDocId(), doc, url,
secured_docid, secured_attachmentid, attachment_url,
attachment_list = [], i, j, k = 1, deleteAttachment;
// no docId
if (!(typeof docid === "string" && docid !== "")) {
that.error({
"status": 405,
"statusText": "Method Not Allowed",
"error": "method_not_allowed",
"message": "Cannot create document which id is undefined",
"reason": "Document id is undefined"
});
}
// no cors support
if (priv.checkCors === false) {
that.error({
"status": 405,
"statusText": "Method Not Allowed",
"error": "method_not_allowed",
"message": "Browser does not support cross domain ajax requests",
"reason": "cors is undefined"
});
}
secured_docid = priv.secureDocId(command.getDocId());
url = priv.url + '/' + priv.underscoreFileExtenisons(secured_docid);
// remove attachment
if (typeof command.getAttachmentId() === "string") {
secured_attachmentid = priv.secureDocId(command.getAttachmentId());
attachment_url = url + '.' + priv.underscoreFileExtenisons(
secured_attachmentid
);
$.ajax({
url: attachment_url + '?_=' + Date.now(),
type: 'DELETE',
async: true,
crossdomain : true,
headers : {
Authorization: 'Basic ' + btoa(
priv.username + ':' + priv.password
)
},
success: function () {
// retrieve underlying document
$.ajax({
url: url + '?_=' + Date.now(),
type: 'GET',
async: true,
dataType: 'text',
crossdomain : true,
headers : {
Authorization: 'Basic ' + btoa(
priv.username + ':' + priv.password
)
},
success: function (response) {
// underlying document
doc = JSON.parse(response);
// update doc._attachments
if (typeof doc._attachments === "object") {
if (typeof doc._attachments[command.getAttachmentId()] ===
"object") {
delete doc._attachments[command.getAttachmentId()];
if (priv.objectIsEmpty(doc._attachments)) {
delete doc._attachments;
}
// PUT back to server
$.ajax({
url: url + '?_=' + Date.now(),
type: 'PUT',
data: JSON.stringify(doc),
async: true,
crossdomain: true,
headers : {
Authorization: 'Basic ' + btoa(
priv.username + ':' + priv.password
)
},
// xhrFields: {withCredentials: 'true'},
success: function () {
that.success({
"ok": true,
"id": command.getDocId() + '/' +
command.getAttachmentId()
});
},
error: function () {
that.error({
"status": 409,
"statusText": "Conflicts",
"error": "conflicts",
"message": "Cannot modify document",
"reason": "Error trying to update document attachments"
});
}
});
} else {
// sure this if-else is needed?
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Cannot find the document",
"reason": "Error trying to update document attachments"
});
}
} else {
// no attachments, we are done
that.success({
"ok": true,
"id": command.getDocId() + '/' + command.getAttachmentId()
});
}
},
error: function () {
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Cannot find the document",
"reason": "Document does not exist"
});
}
});
},
error: function () {
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Cannot find the attachment",
"reason": "Error trying to remove attachment"
});
}
});
// remove document
} else {
// get document to also remove all attachments
$.ajax({
url: url + '?_=' + Date.now(),
type: 'GET',
async: true,
dataType: 'text',
crossdomain : true,
headers : {
Authorization: 'Basic ' + btoa(
priv.username + ':' + priv.password
)
},
success: function (response) {
var x;
doc = JSON.parse(response);
// prepare attachment loop
if (typeof doc._attachments === "object") {
// prepare list of attachments
for (x in doc._attachments) {
if (doc._attachments.hasOwnProperty(x)) {
attachment_list.push(x);
}
}
}
// delete document
$.ajax({
url: url + '?_=' + Date.now(),
type: 'DELETE',
async: true,
crossdomain : true,
headers : {
Authorization: 'Basic ' + btoa(
priv.username + ':' + priv.password
)
},
success: function () {
j = attachment_list.length;
// no attachments, done
if (j === 0) {
that.success({
"ok": true,
"id": command.getDocId()
});
} else {
deleteAttachment = function (attachment_url, j, k) {
$.ajax({
url: attachment_url + '?_=' + Date.now(),
type: 'DELETE',
async: true,
crossdomain : true,
headers : {
Authorization: 'Basic ' + btoa(
priv.username + ':' + priv.password
)
},
success: function () {
// all deleted, return response, need k as async couter
if (j === k) {
that.success({
"ok": true,
"id": command.getDocId()
});
} else {
k += 1;
}
},
error: function () {
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Cannot find the attachment",
"reason": "Error trying to remove attachment"
});
}
});
};
for (i = 0; i < j; i += 1) {
secured_attachmentid = priv.secureDocId(attachment_list[i]);
attachment_url = url + '.' + priv.underscoreFileExtenisons(
secured_attachmentid
);
deleteAttachment(attachment_url, j, k);
}
}
},
error: function () {
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Cannot find the document",
"reason": "Error trying to remove document"
});
}
});
},
error: function () {
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Cannot find the document",
"reason": "Document does not exist"
});
}
});
}
};
/**
* Gets a document list from a distant dav storage
* Options:
* - {boolean} include_docs Also retrieve the actual document content.
* @method allDocs
* @param {object} command The JIO command
*/
//{
// "total_rows": 4,
// "rows": [
// {
// "id": "otherdoc",
// "key": "otherdoc",
// "value": {
// "rev": "1-3753476B70A49EA4D8C9039E7B04254C"
// }
// },{...}
// ]
//}
that.allDocs = function (command) {
var rows = [], url,
am = priv.newAsyncModule(),
o = {};
o.getContent = function (file) {
var docid = priv.secureDocId(file.id),
url = priv.url + '/' + docid;
$.ajax({
url: url + '?_=' + Date.now(),
type: 'GET',
async: true,
dataType: 'text',
headers: {
'Authorization': 'Basic ' + btoa(
priv.username + ':' + priv.password
)
},
success: function (content) {
file.doc = JSON.parse(content);
rows.push(file);
am.call(o, 'success');
},
error: function (type) {
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Cannot find the document",
"reason": "Cannot get a document from DAVStorage"
});
am.call(o, 'error', [type]);
}
});
};
o.getDocumentList = function () {
url = priv.url + '/';
$.ajax({
url: url + '?_=' + Date.now(),
type: 'PROPFIND',
async: true,
dataType: "xml",
crossdomain : true,
headers : {
Authorization: 'Basic ' + btoa(
priv.username + ':' + priv.password
),
Depth: '1'
},
success: function (xml) {
var response = $(xml).find('D\\:response, response'),
len = response.length;
if (len === 1) {
return am.call(o, 'success');
}
am.wait(o, 'success', len - 2);
response.each(function (i, data) {
if (i > 0) { // exclude parent folder
var file = {
value: {}
};
$(data).find('D\\:href, href').each(function () {
var split = $(this).text().split('/');
file.id = split[split.length - 1];
file.id = priv.restoreSlashes(file.id);
file.key = file.id;
});
if (command.getOption('include_docs')) {
am.call(o, 'getContent', [file]);
} else {
rows.push(file);
am.call(o, 'success');
}
}
});
},
error: function (type) {
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Cannot find the document",
"reason": "Cannot get a document list from DAVStorage"
});
am.call(o, 'retry', [type]);
}
});
};
o.retry = function (error) {
am.neverCall(o, 'retry');
am.neverCall(o, 'success');
am.neverCall(o, 'error');
that.retry(error);
};
o.error = function (error) {
am.neverCall(o, 'retry');
am.neverCall(o, 'success');
am.neverCall(o, 'error');
that.error(error);
};
o.success = function () {
am.neverCall(o, 'retry');
am.neverCall(o, 'success');
am.neverCall(o, 'error');
that.success({
total_rows: rows.length,
rows: rows
});
};
// first get the XML list
am.call(o, 'getDocumentList');
};
return that;
});
\ No newline at end of file
(function (scope, hex_md5) {
"use strict";
var localstorage;
if (typeof localStorage !== "undefined") {
localstorage = {
getItem: function (item) {
var value = localStorage.getItem(item);
return value === null ? null : JSON.parse(value);
},
setItem: function (item, value) {
return localStorage.setItem(item, JSON.stringify(value));
},
removeItem: function (item) {
delete localStorage[item];
},
clone: function () {
return JSON.parse(JSON.stringify(localStorage));
}
};
} else {
(function () {
var pseudo_localStorage = {};
localstorage = {
getItem: function (item) {
var value = pseudo_localStorage[item];
return value === undefined ?
null : JSON.parse(pseudo_localStorage[item]);
},
setItem: function (item, value) {
pseudo_localStorage[item] = JSON.stringify(value);
},
removeItem: function (item) {
delete pseudo_localStorage[item];
},
clone: function () {
return JSON.parse(JSON.stringify(pseudo_localStorage));
}
};
}());
}
/*jslint indent:2, maxlen: 80, sloppy: true */
var jioException = function (spec, my) {
var that = {};
spec = spec || {};
my = my || {};
that.name = 'jioException';
that.message = spec.message || 'Unknown Reason.';
that.toString = function () {
return that.name + ': ' + that.message;
};
return that;
};
var invalidCommandState = function (spec, my) {
var that = jioException(spec, my), command = spec.command;
spec = spec || {};
that.name = 'invalidCommandState';
that.toString = function () {
return that.name + ': ' +
command.getLabel() + ', ' + that.message;
};
return that;
};
var invalidStorage = function (spec, my) {
var that = jioException(spec, my), type = spec.storage.getType();
spec = spec || {};
that.name = 'invalidStorage';
that.toString = function () {
return that.name + ': ' +
'Type "' + type + '", ' + that.message;
};
return that;
};
var invalidStorageType = function (spec, my) {
var that = jioException(spec, my), type = spec.type;
that.name = 'invalidStorageType';
that.toString = function () {
return that.name + ': ' +
type + ', ' + that.message;
};
return that;
};
var jobNotReadyException = function (spec, my) {
var that = jioException(spec, my);
that.name = 'jobNotReadyException';
return that;
};
var tooMuchTriesJobException = function (spec, my) {
var that = jioException(spec, my);
that.name = 'tooMuchTriesJobException';
return that;
};
var invalidJobException = function (spec, my) {
var that = jioException(spec, my);
that.name = 'invalidJobException';
return that;
};
var jio = function(spec) {
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global command: true, jobManager: true, job: true */
var storage = function (spec, my) {
var that = {}, priv = {};
spec = spec || {};
my = my || {};
// Attributes //
priv.type = spec.type || '';
// Methods //
Object.defineProperty(that, "getType", {
configurable: false,
enumerable: false,
writable: false,
value: function () {
return priv.type;
}
});
/**
* Execute the command on this storage.
* @method execute
* @param {object} command The command
*/
that.execute = function (command) {
that.success = command.success;
that.error = command.error;
that.retry = command.retry;
that.end = command.end;
if (that.validate(command)) {
command.executeOn(that);
}
};
/**
* Override this function to validate specifications.
* @method isValid
* @return {boolean} true if ok, else false.
*/
that.isValid = function () {
return true;
};
that.validate = function () {
var mess = that.validateState();
if (mess) {
that.error({
"status": 0,
"statusText": "Invalid Storage",
"error": "invalid_storage",
"message": mess,
"reason": mess
});
return false;
}
return true;
};
/**
* Returns a serialized version of this storage.
* @method serialized
* @return {object} The serialized storage.
*/
that.serialized = function () {
var o = that.specToStore() || {};
o.type = that.getType();
return o;
};
/**
* Returns an object containing spec to store on localStorage, in order to
* be restored later if something wrong happen.
* Override this method!
* @method specToStore
* @return {object} The spec to store
*/
that.specToStore = function () {
return {};
};
/**
* Validate the storage state. It returns a empty string all is ok.
* @method validateState
* @return {string} empty: ok, else error message.
*/
that.validateState = function () {
return '';
};
that.post = function () {
setTimeout(function () {
that.error({
"status": 0,
"statusText": "Not Implemented",
"error": "not_implemented",
"message": "\"Post\" command is not implemented",
"reason": "Command not implemented"
});
});
};
that.put = function () {
setTimeout(function () {
that.error({
"status": 0,
"statusText": "Not Implemented",
"error": "not_implemented",
"message": "\"Put\" command is not implemented",
"reason": "Command not implemented"
});
});
};
that.putAttachment = function () {
setTimeout(function () {
that.error({
"status": 0,
"statusText": "Not Implemented",
"error": "not_implemented",
"message": "\"PutAttachment\" command is not implemented",
"reason": "Command not implemented"
});
});
};
that.get = function () {
setTimeout(function () {
that.error({
"status": 0,
"statusText": "Not Implemented",
"error": "not_implemented",
"message": "\"Get\" command is not implemented",
"reason": "Command not implemented"
});
});
};
that.allDocs = function () {
setTimeout(function () {
that.error({
"status": 0,
"statusText": "Not Implemented",
"error": "not_implemented",
"message": "\"AllDocs\" command is not implemented",
"reason": "Command not implemented"
});
});
};
that.remove = function () {
setTimeout(function () {
that.error({
"status": 0,
"statusText": "Not Implemented",
"error": "not_implemented",
"message": "\"Remove\" command is not implemented",
"reason": "Command not implemented"
});
});
};
that.success = function () {};
that.retry = function () {};
that.error = function () {};
that.end = function () {}; // terminate the current job.
priv.newCommand = function (method, spec) {
var o = spec || {};
o.label = method;
return command(o, my);
};
priv.storage = my.storage;
delete my.storage;
that.addJob = function (method, storage_spec, doc, option, success, error) {
var command_opt = {
options: option,
callbacks: {success: success, error: error}
};
if (doc) {
if (method === 'get') {
command_opt.docid = doc;
} else {
command_opt.doc = doc;
}
}
jobManager.addJob(job({
storage: priv.storage(storage_spec || {}),
command: priv.newCommand(method, command_opt)
}, my));
};
return that;
};
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global command: true */
var allDocsCommand = function (spec, my) {
var that = command(spec, my);
spec = spec || {};
my = my || {};
// Attributes //
// Methods //
that.getLabel = function () {
return 'allDocs';
};
that.executeOn = function (storage) {
storage.allDocs(that);
};
that.canBeRestored = function () {
return false;
};
that.validateState = function () {
return true;
};
return that;
};
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global postCommand: true, putCommand: true, getCommand: true,
removeCommand: true, allDocsCommand: true,
putAttachmentCommand: true, failStatus: true, doneStatus: true,
hex_md5: true */
var command = function (spec, my) {
var that = {},
priv = {};
spec = spec || {};
my = my || {};
priv.commandlist = {
'post': postCommand,
'put': putCommand,
'get': getCommand,
'remove': removeCommand,
'allDocs': allDocsCommand,
'putAttachment': putAttachmentCommand
};
// creates the good command thanks to his label
if (spec.label && priv.commandlist[spec.label]) {
priv.label = spec.label;
delete spec.label;
return priv.commandlist[priv.label](spec, my);
}
priv.tried = 0;
priv.doc = spec.doc || {};
if (typeof priv.doc !== "object") {
priv.doc = {
"_id": priv.doc.toString()
};
}
priv.docid = spec.docid || priv.doc._id;
priv.option = spec.options || {};
priv.callbacks = spec.callbacks || {};
priv.success = priv.callbacks.success || function () {};
priv.error = priv.callbacks.error || function () {};
priv.retry = function () {
that.error({
status: 13,
statusText: 'Fail Retry',
error: 'fail_retry',
message: 'Impossible to retry.',
reason: 'Impossible to retry.'
});
};
priv.end = function () {};
priv.on_going = false;
// Methods //
/**
* Returns a serialized version of this command.
* @method serialized
* @return {object} The serialized command.
*/
that.serialized = function () {
var o = {};
o.label = that.getLabel();
o.tried = priv.tried;
o.doc = that.cloneDoc();
o.option = that.cloneOption();
return o;
};
/**
* Returns the label of the command.
* @method getLabel
* @return {string} The label.
*/
that.getLabel = function () {
return 'command';
};
/**
* Gets the document id
* @method getDocId
* @return {string} The document id
*/
that.getDocId = function () {
if (typeof priv.docid !== "string") {
return undefined;
}
return priv.docid.split('/')[0];
};
/**
* Gets the attachment id
* @method getAttachmentId
* @return {string} The attachment id
*/
that.getAttachmentId = function () {
if (typeof priv.docid !== "string") {
return undefined;
}
return priv.docid.split('/')[1];
};
/**
* Returns the label of the command.
* @method getDoc
* @return {object} The document.
*/
that.getDoc = function () {
return priv.doc;
};
/**
* Returns the data of the attachment
* @method getAttachmentData
* @return {string} The data
*/
that.getAttachmentData = function () {
return priv.doc._data || "";
};
/**
* Returns the data length of the attachment
* @method getAttachmentLength
* @return {number} The length
*/
that.getAttachmentLength = function () {
return (priv.doc._data || "").length;
};
/**
* Returns the mimetype of the attachment
* @method getAttachmentMimeType
* @return {string} The mimetype
*/
that.getAttachmentMimeType = function () {
return priv.doc._mimetype;
};
/**
* Generate the md5sum of the attachment data
* @method md5SumAttachmentData
* @return {string} The md5sum
*/
that.md5SumAttachmentData = function () {
return hex_md5(priv.doc._data || "");
};
/**
* Returns an information about the document.
* @method getDocInfo
* @param {string} infoname The info name.
* @return The info value.
*/
that.getDocInfo = function (infoname) {
return priv.doc[infoname];
};
/**
* Returns the value of an option.
* @method getOption
* @param {string} optionname The option name.
* @return The option value.
*/
that.getOption = function (optionname) {
return priv.option[optionname];
};
/**
* Validates the storage.
* @param {object} storage The storage.
*/
that.validate = function (storage) {
if (typeof priv.docid === "string" &&
!priv.docid.match("^[^\/]+([\/][^\/]+)?$")) {
that.error({
status: 21,
statusText: 'Invalid Document Id',
error: 'invalid_document_id',
message: 'The document id must be like "abc" or "abc/def".',
reason: 'The document id is no like "abc" or "abc/def"'
});
return false;
}
if (!that.validateState()) {
return false;
}
return storage.validate();
};
/*
* Extend this function
*/
that.validateState = function () {
return true;
};
/**
* Check if the command can be retried.
* @method canBeRetried
* @return {boolean} The result
*/
that.canBeRetried = function () {
return (priv.option.max_retry === undefined ||
priv.option.max_retry === 0 ||
priv.tried < priv.option.max_retry);
};
/**
* Gets the number time the command has been tried.
* @method getTried
* @return {number} The number of time the command has been tried
*/
that.getTried = function () {
return priv.tried;
};
/**
* Delegate actual excecution the storage.
* @param {object} storage The storage.
*/
that.execute = function (storage) {
if (!priv.on_going) {
if (that.validate(storage)) {
priv.tried += 1;
priv.on_going = true;
storage.execute(that);
}
}
};
/**
* Execute the good method from the storage.
* Override this function.
* @method executeOn
* @param {object} storage The storage.
*/
that.executeOn = function (storage) {};
that.success = function (return_value) {
priv.on_going = false;
priv.success(return_value);
priv.end(doneStatus());
};
that.retry = function (return_error) {
priv.on_going = false;
if (that.canBeRetried()) {
priv.retry();
} else {
that.error(return_error);
}
};
that.error = function (return_error) {
priv.on_going = false;
priv.error(return_error);
priv.end(failStatus());
};
that.end = function () {
priv.end(doneStatus());
};
that.onSuccessDo = function (fun) {
if (fun) {
priv.success = fun;
} else {
return priv.success;
}
};
that.onErrorDo = function (fun) {
if (fun) {
priv.error = fun;
} else {
return priv.error;
}
};
that.onEndDo = function (fun) {
priv.end = fun;
};
that.onRetryDo = function (fun) {
priv.retry = fun;
};
/**
* Is the command can be restored by another JIO : yes.
* @method canBeRestored
* @return {boolean} true
*/
that.canBeRestored = function () {
return true;
};
/**
* Clones the command and returns it.
* @method clone
* @return {object} The cloned command.
*/
that.clone = function () {
return command(that.serialized(), my);
};
/**
* Clones the command options and returns the clone version.
* @method cloneOption
* @return {object} The clone of the command options.
*/
that.cloneOption = function () {
return JSON.parse(JSON.stringify(priv.option));
};
/**
* Clones the document and returns the clone version.
* @method cloneDoc
* @return {object} The clone of the document.
*/
that.cloneDoc = function () {
return JSON.parse(JSON.stringify(priv.doc));
};
return that;
};
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global command: true */
var getCommand = function (spec, my) {
var that = command(spec, my);
spec = spec || {};
my = my || {};
// Attributes //
// Methods //
that.getLabel = function () {
return 'get';
};
that.validateState = function () {
if (!(typeof that.getDocId() === "string" &&
that.getDocId() !== "")) {
that.error({
"status": 20,
"statusText": "Document Id Required",
"error": "document_id_required",
"message": "The document id is not provided",
"reason": "Document id is undefined"
});
return false;
}
if (typeof that.getAttachmentId() === "string") {
if (that.getAttachmentId() === "") {
that.error({
"status": 23,
"statusText": "Invalid Attachment Id",
"error": "invalid_attachment_id",
"message": "The attachment id must not be an empty string",
"reason": "Attachment id is empty"
});
return false;
}
}
return true;
};
that.executeOn = function (storage) {
storage.get(that);
};
that.canBeRestored = function () {
return false;
};
return that;
};
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global command: true */
var postCommand = function (spec, my) {
var that = command(spec, my);
spec = spec || {};
my = my || {};
// Methods //
that.getLabel = function () {
return 'post';
};
that.validateState = function () {
if (that.getAttachmentId() !== undefined) {
that.error({
"status": 21,
"statusText": "Invalid Document Id",
"error": "invalid_document_id",
"message": "The document id contains '/' characters " +
"which are forbidden",
"reason": "Document id contains '/' character(s)"
});
return false;
}
return true;
};
that.executeOn = function (storage) {
storage.post(that);
};
return that;
};
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global command: true */
var putAttachmentCommand = function (spec, my) {
var that = command(spec, my);
spec = spec || {};
my = my || {};
// Attributes //
// Methods //
that.getLabel = function () {
return 'putAttachment';
};
that.executeOn = function (storage) {
storage.putAttachment(that);
};
that.validateState = function () {
if (!(typeof that.getDocId() === "string" && that.getDocId() !==
"")) {
that.error({
"status": 20,
"statusText": "Document Id Required",
"error": "document_id_required",
"message": "The document id is not provided",
"reason": "Document id is undefined"
});
return false;
}
if (typeof that.getAttachmentId() !== "string") {
that.error({
"status": 22,
"statusText": "Attachment Id Required",
"error": "attachment_id_required",
"message": "The attachment id must be set",
"reason": "Attachment id not set"
});
return false;
}
if (that.getAttachmentId() === "") {
that.error({
"status": 23,
"statusText": "Invalid Attachment Id",
"error": "invalid_attachment_id",
"message": "The attachment id must not be an empty string",
"reason": "Attachment id is empty"
});
}
return true;
};
return that;
};
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global command: true */
var putCommand = function (spec, my) {
var that = command(spec, my);
spec = spec || {};
my = my || {};
// Methods //
that.getLabel = function () {
return 'put';
};
that.validateState = function () {
if (!(typeof that.getDocId() === "string" && that.getDocId() !==
"")) {
that.error({
"status": 20,
"statusText": "Document Id Required",
"error": "document_id_required",
"message": "The document id is not provided",
"reason": "Document id is undefined"
});
return false;
}
if (that.getAttachmentId() !== undefined) {
that.error({
"status": 21,
"statusText": "Invalid Document Id",
"error": "invalid_document_id",
"message": "The document id contains '/' characters " +
"which are forbidden",
"reason": "Document id contains '/' character(s)"
});
return false;
}
return true;
};
that.executeOn = function (storage) {
storage.put(that);
};
return that;
};
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global command: true */
var removeCommand = function (spec, my) {
var that = command(spec, my);
spec = spec || {};
my = my || {};
// Attributes //
// Methods //
that.getLabel = function () {
return 'remove';
};
that.validateState = function () {
if (!(typeof that.getDocId() === "string" && that.getDocId() !==
"")) {
that.error({
"status": 20,
"statusText": "Document Id Required",
"error": "document_id_required",
"message": "The document id is not provided",
"reason": "Document id is undefined"
});
return false;
}
if (typeof that.getAttachmentId() === "string") {
if (that.getAttachmentId() === "") {
that.error({
"status": 23,
"statusText": "Invalid Attachment Id",
"error": "invalid_attachment_id",
"message": "The attachment id must not be an empty string",
"reason": "Attachment id is empty"
});
return false;
}
}
return true;
};
that.executeOn = function (storage) {
storage.remove(that);
};
return that;
};
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global jobStatus: true */
var doneStatus = function (spec, my) {
var that = jobStatus(spec, my);
spec = spec || {};
my = my || {};
// Attributes //
// Methods //
that.getLabel = function () {
return 'done';
};
that.canStart = function () {
return false;
};
that.canRestart = function () {
return false;
};
that.isDone = function () {
return true;
};
return that;
};
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global jobStatus: true */
var failStatus = function (spec, my) {
var that = jobStatus(spec, my);
spec = spec || {};
my = my || {};
// Attributes //
// Methods //
that.getLabel = function () {
return 'fail';
};
that.canStart = function () {
return false;
};
that.canRestart = function () {
return true;
};
return that;
};
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global jobStatus: true */
var initialStatus = function (spec, my) {
var that = jobStatus(spec, my);
spec = spec || {};
my = my || {};
// Attributes //
// Methods //
that.getLabel = function () {
return "initial";
};
that.canStart = function () {
return true;
};
that.canRestart = function () {
return true;
};
return that;
};
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global jobStatus: true */
var jobStatus = function (spec, my) {
var that = {};
spec = spec || {};
my = my || {};
// Attributes //
// Methods //
that.getLabel = function () {
return 'job status';
};
that.canStart = function () {};
that.canRestart = function () {};
that.serialized = function () {
return {"label": that.getLabel()};
};
that.isWaitStatus = function () {
return false;
};
that.isDone = function () {
return false;
};
return that;
};
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global jobStatus: true */
var onGoingStatus = function (spec, my) {
var that = jobStatus(spec, my);
spec = spec || {};
my = my || {};
// Attributes //
// Methods //
that.getLabel = function () {
return 'on going';
};
that.canStart = function () {
return false;
};
that.canRestart = function () {
return false;
};
return that;
};
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global jobStatus: true, jobManager: true */
var waitStatus = function (spec, my) {
var that = jobStatus(spec, my), priv = {};
spec = spec || {};
my = my || {};
// Attributes //
priv.job_id_array = spec.job_id_array || [];
priv.threshold = 0;
// Methods //
/**
* Returns the label of this status.
* @method getLabel
* @return {string} The label: 'wait'.
*/
that.getLabel = function () {
return 'wait';
};
/**
* Refresh the job id array to wait.
* @method refreshJobIdArray
*/
priv.refreshJobIdArray = function () {
var tmp_job_id_array = [], i;
for (i = 0; i < priv.job_id_array.length; i += 1) {
if (jobManager.jobIdExists(priv.job_id_array[i])) {
tmp_job_id_array.push(priv.job_id_array[i]);
}
}
priv.job_id_array = tmp_job_id_array;
};
/**
* The status must wait for the job end before start again.
* @method waitForJob
* @param {object} job The job to wait for.
*/
that.waitForJob = function (job) {
var i;
for (i = 0; i < priv.job_id_array.length; i += 1) {
if (priv.job_id_array[i] === job.getId()) {
return;
}
}
priv.job_id_array.push(job.getId());
};
/**
* The status stops to wait for this job.
* @method dontWaitForJob
* @param {object} job The job to stop waiting for.
*/
that.dontWaitForJob = function (job) {
var i, tmp_job_id_array = [];
for (i = 0; i < priv.job_id_array.length; i += 1) {
if (priv.job_id_array[i] !== job.getId()) {
tmp_job_id_array.push(priv.job_id_array[i]);
}
}
priv.job_id_array = tmp_job_id_array;
};
/**
* The status must wait for some milliseconds.
* @method waitForTime
* @param {number} ms The number of milliseconds
*/
that.waitForTime = function (ms) {
priv.threshold = Date.now() + ms;
};
/**
* The status stops to wait for some time.
* @method stopWaitForTime
*/
that.stopWaitForTime = function () {
priv.threshold = 0;
};
that.canStart = function () {
priv.refreshJobIdArray();
return (priv.job_id_array.length === 0 && Date.now() >= priv.threshold);
};
that.canRestart = function () {
return that.canStart();
};
that.serialized = function () {
return {
"label": that.getLabel(),
"waitfortime": priv.threshold,
"waitforjob": priv.job_id_array
};
};
/**
* Checks if this status is waitStatus
* @method isWaitStatus
* @return {boolean} true
*/
that.isWaitStatus = function () {
return true;
};
return that;
};
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global jobIdHandler: true, initialStatus: true, invalidJobException: true,
waitStatus: true, failStatus: true, tooMuchTriesJobException: true,
jobManager: true, jobNotReadyException: true, onGoingStatus: true */
var job = function (spec) {
var that = {},
priv = {};
spec = spec || {};
priv.id = jobIdHandler.nextId();
priv.command = spec.command;
priv.storage = spec.storage;
priv.status = initialStatus();
priv.date = new Date();
// Initialize //
if (!priv.storage) {
throw invalidJobException({
job: that,
message: 'No storage set'
});
}
if (!priv.command) {
throw invalidJobException({
job: that,
message: 'No command set'
});
}
// Methods //
/**
* Returns the job command.
* @method getCommand
* @return {object} The job command.
*/
that.getCommand = function () {
return priv.command;
};
that.getStatus = function () {
return priv.status;
};
that.getId = function () {
return priv.id;
};
that.getStorage = function () {
return priv.storage;
};
that.getDate = function () {
return priv.date;
};
/**
* Checks if the job is ready.
* @method isReady
* @return {boolean} true if ready, else false.
*/
that.isReady = function () {
if (priv.command.getTried() === 0) {
return priv.status.canStart();
}
return priv.status.canRestart();
};
/**
* Returns a serialized version of this job.
* @method serialized
* @return {object} The serialized job.
*/
that.serialized = function () {
return {
id: priv.id,
date: priv.date.getTime(),
status: priv.status.serialized(),
command: priv.command.serialized(),
storage: priv.storage.serialized()
};
};
/**
* Tells the job to wait for another one.
* @method waitForJob
* @param {object} job The job to wait for.
*/
that.waitForJob = function (job) {
if (priv.status.getLabel() !== 'wait') {
priv.status = waitStatus({});
}
priv.status.waitForJob(job);
};
/**
* Tells the job to do not wait for a job.
* @method dontWaitForJob
* @param {object} job The other job.
*/
that.dontWaitFor = function (job) {
if (priv.status.getLabel() === 'wait') {
priv.status.dontWaitForJob(job);
}
};
/**
* Tells the job to wait for a while.
* @method waitForTime
* @param {number} ms Time to wait in millisecond.
*/
that.waitForTime = function (ms) {
if (priv.status.getLabel() !== 'wait') {
priv.status = waitStatus({});
}
priv.status.waitForTime(ms);
};
/**
* Tells the job to do not wait for a while anymore.
* @method stopWaitForTime
*/
that.stopWaitForTime = function () {
if (priv.status.getLabel() === 'wait') {
priv.status.stopWaitForTime();
}
};
that.eliminated = function () {
priv.command.error({
status: 10,
statusText: 'Stopped',
error: 'stopped',
message: 'This job has been stopped by another one.',
reason: 'this job has been stopped by another one'
});
};
that.notAccepted = function () {
priv.command.onEndDo(function () {
priv.status = failStatus();
jobManager.terminateJob(that);
});
priv.command.error({
status: 11,
statusText: 'Not Accepted',
error: 'not_accepted',
message: 'This job is already running.',
reason: 'this job is already running'
});
};
/**
* Updates the date of the job with the another one.
* @method update
* @param {object} job The other job.
*/
that.update = function (job) {
priv.command.error({
status: 12,
statusText: 'Replaced',
error: 'replaced',
message: 'Job has been replaced by another one.',
reason: 'job has been replaced by another one'
});
priv.date = new Date(job.getDate().getTime());
priv.command = job.getCommand();
priv.status = job.getStatus();
};
/**
* Executes this job.
* @method execute
*/
that.execute = function () {
if (!that.getCommand().canBeRetried()) {
throw tooMuchTriesJobException({
job: that,
message: 'The job was invoked too much time.'
});
}
if (!that.isReady()) {
throw jobNotReadyException({
job: that,
message: 'Can not execute this job.'
});
}
priv.status = onGoingStatus();
priv.command.onRetryDo(function () {
var ms = priv.command.getTried();
ms = ms * ms * 200;
if (ms > 10000) {
ms = 10000;
}
that.waitForTime(ms);
});
priv.command.onEndDo(function (status) {
priv.status = status;
jobManager.terminateJob(that);
});
priv.command.execute(priv.storage);
};
return that;
};
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global announcement: true */
var announcement = function (spec, my) {
var that = {},
callback_a = [],
announcer = spec.announcer || {};
spec = spec || {};
my = my || {};
// Methods //
that.add = function (callback) {
callback_a.push(callback);
};
that.remove = function (callback) {
var i, tmp_callback_a = [];
for (i = 0; i < callback_a.length; i += 1) {
if (callback_a[i] !== callback) {
tmp_callback_a.push(callback_a[i]);
}
}
callback_a = tmp_callback_a;
};
that.register = function () {
announcer.register(that);
};
that.unregister = function () {
announcer.unregister(that);
};
that.trigger = function (args) {
var i;
for (i = 0; i < callback_a.length; i += 1) {
callback_a[i].apply(null, args);
}
};
return that;
};
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global localstorage: true, setInterval: true, clearInterval: true */
var activityUpdater = (function (spec, my) {
var that = {}, priv = {};
spec = spec || {};
my = my || {};
priv.id = spec.id || 0;
priv.interval = 400;
priv.interval_id = null;
// Methods //
/**
* Update the last activity date in the localStorage.
* @method touch
*/
priv.touch = function () {
localstorage.setItem('jio/id/' + priv.id, Date.now());
};
/**
* Sets the jio id into the activity.
* @method setId
* @param {number} id The jio id.
*/
that.setId = function (id) {
priv.id = id;
};
/**
* Sets the interval delay between two updates.
* @method setIntervalDelay
* @param {number} ms In milliseconds
*/
that.setIntervalDelay = function (ms) {
priv.interval = ms;
};
/**
* Gets the interval delay.
* @method getIntervalDelay
* @return {number} The interval delay.
*/
that.getIntervalDelay = function () {
return priv.interval;
};
/**
* Starts the activity updater. It will update regulary the last activity
* date in the localStorage to show to other jio instance that this instance
* is active.
* @method start
*/
that.start = function () {
if (!priv.interval_id) {
priv.touch();
priv.interval_id = setInterval(function () {
priv.touch();
}, priv.interval);
}
};
/**
* Stops the activity updater.
* @method stop
*/
that.stop = function () {
if (priv.interval_id !== null) {
clearInterval(priv.interval_id);
priv.interval_id = null;
}
};
return that;
}());
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global announcement: true */
var announcer = (function (spec, my) {
var that = {},
announcement_o = {};
spec = spec || {};
my = my || {};
// Methods //
that.register = function (name) {
if (!announcement_o[name]) {
announcement_o[name] = announcement();
}
};
that.unregister = function (name) {
if (announcement_o[name]) {
delete announcement_o[name];
}
};
that.at = function (name) {
return announcement_o[name];
};
that.on = function (name, callback) {
that.register(name);
that.at(name).add(callback);
};
that.trigger = function (name, args) {
that.at(name).trigger(args);
};
return that;
}());
/*jslint indent: 2, maxlen: 80, sloppy: true */
var jobIdHandler = (function (spec) {
var that = {},
id = 0;
spec = spec || {};
// Methods //
that.nextId = function () {
id = id + 1;
return id;
};
return that;
}());
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global localstorage: true, setInterval: true, clearInterval: true,
command: true, job: true, jobRules: true */
var jobManager = (function (spec) {
var that = {},
job_array_name = 'jio/job_array',
priv = {};
spec = spec || {};
// Attributes //
priv.id = spec.id;
priv.interval_id = null;
priv.interval = 200;
priv.job_array = [];
// Methods //
/**
* Get the job array name in the localStorage
* @method getJobArrayName
* @return {string} The job array name
*/
priv.getJobArrayName = function () {
return job_array_name + '/' + priv.id;
};
/**
* Returns the job array from the localStorage
* @method getJobArray
* @return {array} The job array.
*/
priv.getJobArray = function () {
return localstorage.getItem(priv.getJobArrayName()) || [];
};
/**
* Does a backup of the job array in the localStorage.
* @method copyJobArrayToLocal
*/
priv.copyJobArrayToLocal = function () {
var new_a = [],
i;
for (i = 0; i < priv.job_array.length; i += 1) {
new_a.push(priv.job_array[i].serialized());
}
localstorage.setItem(priv.getJobArrayName(), new_a);
};
/**
* Removes a job from the current job array.
* @method removeJob
* @param {object} job The job object.
*/
priv.removeJob = function (job) {
var i,
tmp_job_array = [];
for (i = 0; i < priv.job_array.length; i += 1) {
if (priv.job_array[i] !== job) {
tmp_job_array.push(priv.job_array[i]);
}
}
priv.job_array = tmp_job_array;
priv.copyJobArrayToLocal();
};
/**
* Sets the job manager id.
* @method setId
* @param {number} id The id.
*/
that.setId = function (id) {
priv.id = id;
};
/**
* Starts listening to the job array, executing them regulary.
* @method start
*/
that.start = function () {
var i;
if (priv.interval_id === null) {
priv.interval_id = setInterval(function () {
priv.restoreOldJio();
for (i = 0; i < priv.job_array.length; i += 1) {
that.execute(priv.job_array[i]);
}
}, priv.interval);
}
};
/**
* Stops listening to the job array.
* @method stop
*/
that.stop = function () {
if (priv.interval_id !== null) {
clearInterval(priv.interval_id);
priv.interval_id = null;
if (priv.job_array.length === 0) {
localstorage.removeItem(priv.getJobArrayName());
}
}
};
/**
* Try to restore an the inactive older jio instances.
* It will restore the on going or initial jobs from their job array
* and it will add them to this job array.
* @method restoreOldJio
*/
priv.restoreOldJio = function () {
var i,
jio_id_a;
priv.lastrestore = priv.lastrestore || 0;
if (priv.lastrestore > (Date.now()) - 2000) {
return;
}
jio_id_a = localstorage.getItem('jio/id_array') || [];
for (i = 0; i < jio_id_a.length; i += 1) {
priv.restoreOldJioId(jio_id_a[i]);
}
priv.lastrestore = Date.now();
};
/**
* Try to restore an old jio according to an id.
* @method restoreOldJioId
* @param {number} id The jio id.
*/
priv.restoreOldJioId = function (id) {
var jio_date;
jio_date = localstorage.getItem('jio/id/' + id) || 0;
if (new Date(jio_date).getTime() < (Date.now() - 10000)) { // 10 sec
priv.restoreOldJobFromJioId(id);
priv.removeOldJioId(id);
priv.removeJobArrayFromJioId(id);
}
};
/**
* Try to restore all jobs from another jio according to an id.
* @method restoreOldJobFromJioId
* @param {number} id The jio id.
*/
priv.restoreOldJobFromJioId = function (id) {
var i,
command_object,
jio_job_array;
jio_job_array = localstorage.getItem('jio/job_array/' + id) || [];
for (i = 0; i < jio_job_array.length; i += 1) {
command_object = command(jio_job_array[i].command);
if (command_object.canBeRestored()) {
that.addJob(job({
storage: that.storage(jio_job_array[i].storage),
command: command_object
}));
}
}
};
/**
* Removes a jio instance according to an id.
* @method removeOldJioId
* @param {number} id The jio id.
*/
priv.removeOldJioId = function (id) {
var i,
jio_id_array,
new_array = [];
jio_id_array = localstorage.getItem('jio/id_array') || [];
for (i = 0; i < jio_id_array.length; i += 1) {
if (jio_id_array[i] !== id) {
new_array.push(jio_id_array[i]);
}
}
localstorage.setItem('jio/id_array', new_array);
localstorage.removeItem('jio/id/' + id);
};
/**
* Removes a job array from a jio instance according to an id.
* @method removeJobArrayFromJioId
* @param {number} id The jio id.
*/
priv.removeJobArrayFromJioId = function (id) {
localstorage.removeItem('jio/job_array/' + id);
};
/**
* Executes a job.
* @method execute
* @param {object} job The job object.
*/
that.execute = function (job) {
try {
job.execute();
} catch (e) {
switch (e.name) {
case 'jobNotReadyException':
break; // do nothing
case 'tooMuchTriesJobException':
break; // do nothing
default:
throw e;
}
}
priv.copyJobArrayToLocal();
};
/**
* Checks if a job exists in the job array according to a job id.
* @method jobIdExists
* @param {number} id The job id.
* @return {boolean} true if exists, else false.
*/
that.jobIdExists = function (id) {
var i;
for (i = 0; i < priv.job_array.length; i += 1) {
if (priv.job_array[i].getId() === id) {
return true;
}
}
return false;
};
/**
* Terminate a job. It only remove it from the job array.
* @method terminateJob
* @param {object} job The job object
*/
that.terminateJob = function (job) {
priv.removeJob(job);
};
/**
* Adds a job to the current job array.
* @method addJob
* @param {object} job The new job.
*/
that.addJob = function (job) {
var result_array = that.validateJobAccordingToJobList(priv.job_array, job);
priv.appendJob(job, result_array);
};
/**
* Generate a result array containing action string to do with the good job.
* @method validateJobAccordingToJobList
* @param {array} job_array A job array.
* @param {object} job The new job to compare with.
* @return {array} A result array.
*/
that.validateJobAccordingToJobList = function (job_array, job) {
var i,
result_array = [];
for (i = 0; i < job_array.length; i += 1) {
result_array.push(jobRules.validateJobAccordingToJob(job_array[i], job));
}
return result_array;
};
/**
* It will manage the job in order to know what to do thanks to a result
* array. The new job can be added to the job array, but it can also be
* not accepted. It is this method which can tells jobs to wait for another
* one, to replace one or to eliminate some while browsing.
* @method appendJob
* @param {object} job The job to append.
* @param {array} result_array The result array.
*/
priv.appendJob = function (job, result_array) {
var i;
if (priv.job_array.length !== result_array.length) {
throw new RangeError("Array out of bound");
}
for (i = 0; i < result_array.length; i += 1) {
if (result_array[i].action === 'dont accept') {
return job.notAccepted();
}
}
for (i = 0; i < result_array.length; i += 1) {
switch (result_array[i].action) {
case 'eliminate':
result_array[i].job.eliminated();
priv.removeJob(result_array[i].job);
break;
case 'update':
result_array[i].job.update(job);
priv.copyJobArrayToLocal();
return;
case 'wait':
job.waitForJob(result_array[i].job);
break;
default:
break;
}
}
priv.job_array.push(job);
priv.copyJobArrayToLocal();
};
that.serialized = function () {
var a = [],
i,
job_array = priv.job_array || [];
for (i = 0; i < job_array.length; i += 1) {
a.push(job_array[i].serialized());
}
return a;
};
return that;
}());
/*jslint indent: 2, maxlen: 80, sloppy: true */
var jobRules = (function () {
var that = {}, priv = {};
priv.compare = {};
priv.action = {};
Object.defineProperty(that, "eliminate", {
configurable: false,
enumerable: false,
writable: false,
value: function () {
return 'eliminate';
}
});
Object.defineProperty(that, "update", {
configurable: false,
enumerable: false,
writable: false,
value: function () {
return 'update';
}
});
Object.defineProperty(that, "dontAccept", {
configurable: false,
enumerable: false,
writable: false,
value: function () {
return 'dont accept';
}
});
Object.defineProperty(that, "wait", {
configurable: false,
enumerable: false,
writable: false,
value: function () {
return 'wait';
}
});
Object.defineProperty(that, "none", {
configurable: false,
enumerable: false,
writable: false,
value: function () {
return 'none';
}
});
that.default_action = that.none;
that.default_compare = function (job1, job2) {
return (job1.getCommand().getDocId() === job2.getCommand().getDocId() &&
job1.getCommand().getDocInfo('_rev') ===
job2.getCommand().getDocInfo('_rev') &&
job1.getCommand().getOption('rev') ===
job2.getCommand().getOption('rev') &&
JSON.stringify(job1.getStorage().serialized()) ===
JSON.stringify(job2.getStorage().serialized()));
};
// Methods //
/**
* Returns an action according the jobs given in parameters.
* @method getAction
* @param {object} job1 The already existant job.
* @param {object} job2 The job to compare with.
* @return {string} An action string.
*/
priv.getAction = function (job1, job2) {
var j1label, j2label, j1status;
j1label = job1.getCommand().getLabel();
j2label = job2.getCommand().getLabel();
j1status = (job1.getStatus().getLabel() === 'on going' ?
'on going' : 'not on going');
if (priv.action[j1label] && priv.action[j1label][j1status] &&
priv.action[j1label][j1status][j2label]) {
return priv.action[j1label][j1status][j2label](job1, job2);
}
return that.default_action(job1, job2);
};
/**
* Checks if the two jobs are comparable.
* @method canCompare
* @param {object} job1 The already existant job.
* @param {object} job2 The job to compare with.
* @return {boolean} true if comparable, else false.
*/
priv.canCompare = function (job1, job2) {
var job1label = job1.getCommand().getLabel(),
job2label = job2.getCommand().getLabel();
if (priv.compare[job1label] && priv.compare[job2label]) {
return priv.compare[job1label][job2label](job1, job2);
}
return that.default_compare(job1, job2);
};
/**
* Returns an action string to show what to do if we want to add a job.
* @method validateJobAccordingToJob
* @param {object} job1 The current job.
* @param {object} job2 The new job.
* @return {string} The action string.
*/
Object.defineProperty(that, "validateJobAccordingToJob", {
configurable: false,
enumerable: false,
writable: false,
value: function (job1, job2) {
if (priv.canCompare(job1, job2)) {
return {
action: priv.getAction(job1, job2),
job: job1
};
}
return {
action: that.default_action(job1, job2),
job: job1
};
}
});
/**
* Adds a rule the action rules.
* @method addActionRule
* @param {string} method1 The action label from the current job.
* @param {boolean} ongoing Is this action is on going or not?
* @param {string} method2 The action label from the new job.
* @param {function} rule The rule that return an action string.
*/
Object.defineProperty(that, "addActionRule", {
configurable: false,
enumerable: false,
writable: false,
value: function (method1, ongoing, method2, rule) {
var ongoing_s = (ongoing ? 'on going' : 'not on going');
priv.action[method1] = priv.action[method1] || {};
priv.action[method1][ongoing_s] = priv.action[method1][ongoing_s] || {};
priv.action[method1][ongoing_s][method2] = rule;
}
});
/**
* Adds a rule the compare rules.
* @method addCompareRule
* @param {string} method1 The action label from the current job.
* @param {string} method2 The action label from the new job.
* @param {function} rule The rule that return a boolean
* - true if job1 and job2 can be compared, else false.
*/
Object.defineProperty(that, "addCompareRule", {
configurable: false,
enumerable: false,
writable: false,
value: function (method1, method2, rule) {
priv.compare[method1] = priv.compare[method1] || {};
priv.compare[method1][method2] = rule;
}
});
////////////////////////////////////////////////////////////////////////////
// Adding some rules
/*
LEGEND:
- s: storage
- m: method
- n: name
- c: content
- o: options
- =: are equal
- !: are not equal
select ALL s= n=
removefailordone fail|done
/ elim repl nacc wait
Remove !ongoing Save 1 x x x
Save !ongoing Remove 1 x x x
GetList !ongoing GetList 0 1 x x
Remove !ongoing Remove 0 1 x x
Load !ongoing Load 0 1 x x
Save c= !ongoing Save 0 1 x x
Save c! !ongoing Save 0 1 x x
GetList ongoing GetList 0 0 1 x
Remove ongoing Remove 0 0 1 x
Remove ongoing Load 0 0 1 x
Remove !ongoing Load 0 0 1 x
Load ongoing Load 0 0 1 x
Save c= ongoing Save 0 0 1 x
Remove ongoing Save 0 0 0 1
Load ongoing Remove 0 0 0 1
Load ongoing Save 0 0 0 1
Load !ongoing Remove 0 0 0 1
Load !ongoing Save 0 0 0 1
Save ongoing Remove 0 0 0 1
Save ongoing Load 0 0 0 1
Save c! ongoing Save 0 0 0 1
Save !ongoing Load 0 0 0 1
GetList ongoing Remove 0 0 0 0
GetList ongoing Load 0 0 0 0
GetList ongoing Save 0 0 0 0
GetList !ongoing Remove 0 0 0 0
GetList !ongoing Load 0 0 0 0
GetList !ongoing Save 0 0 0 0
Remove ongoing GetList 0 0 0 0
Remove !ongoing GetList 0 0 0 0
Load ongoing GetList 0 0 0 0
Load !ongoing GetList 0 0 0 0
Save ongoing GetList 0 0 0 0
Save !ongoing GetList 0 0 0 0
For more information, see documentation
*/
that.addActionRule('post', true, 'post', that.dontAccept);
that.addActionRule('post', true, 'put', that.wait);
that.addActionRule('post', true, 'get', that.wait);
that.addActionRule('post', true, 'remove', that.wait);
that.addActionRule('post', true, 'putAttachment', that.wait);
that.addActionRule('post', false, 'post', that.update);
that.addActionRule('post', false, 'put', that.wait);
that.addActionRule('post', false, 'get', that.wait);
that.addActionRule('post', false, 'remove', that.eliminate);
that.addActionRule('post', false, 'putAttachment', that.wait);
that.addActionRule('put', true, 'post', that.dontAccept);
that.addActionRule('put', true, 'put', that.wait);
that.addActionRule('put', true, 'get', that.wait);
that.addActionRule('put', true, 'remove', that.wait);
that.addActionRule('put', true, 'putAttachment', that.wait);
that.addActionRule('put', false, 'post', that.dontAccept);
that.addActionRule('put', false, 'put', that.update);
that.addActionRule('put', false, 'get', that.wait);
that.addActionRule('put', false, 'remove', that.eliminate);
that.addActionRule('put', false, 'putAttachment', that.wait);
that.addActionRule('get', true, 'post', that.wait);
that.addActionRule('get', true, 'put', that.wait);
that.addActionRule('get', true, 'get', that.dontAccept);
that.addActionRule('get', true, 'remove', that.wait);
that.addActionRule('get', true, 'putAttachment', that.wait);
that.addActionRule('get', false, 'post', that.wait);
that.addActionRule('get', false, 'put', that.wait);
that.addActionRule('get', false, 'get', that.update);
that.addActionRule('get', false, 'remove', that.wait);
that.addActionRule('get', false, 'putAttachment', that.wait);
that.addActionRule('remove', true, 'post', that.wait);
that.addActionRule('remove', true, 'get', that.dontAccept);
that.addActionRule('remove', true, 'remove', that.dontAccept);
that.addActionRule('remove', true, 'putAttachment', that.dontAccept);
that.addActionRule('remove', false, 'post', that.eliminate);
that.addActionRule('remove', false, 'put', that.dontAccept);
that.addActionRule('remove', false, 'get', that.dontAccept);
that.addActionRule('remove', false, 'remove', that.update);
that.addActionRule('remove', false, 'putAttachment', that.dontAccept);
that.addActionRule('allDocs', true, 'allDocs', that.dontAccept);
that.addActionRule('allDocs', false, 'allDocs', that.update);
that.addActionRule('putAttachment', true, 'post', that.dontAccept);
that.addActionRule('putAttachment', true, 'put', that.wait);
that.addActionRule('putAttachment', true, 'get', that.wait);
that.addActionRule('putAttachment', true, 'remove', that.wait);
that.addActionRule('putAttachment', true, 'putAttachment', that.wait);
that.addActionRule('putAttachment', false, 'post', that.dontAccept);
that.addActionRule('putAttachment', false, 'put', that.wait);
that.addActionRule('putAttachment', false, 'get', that.wait);
that.addActionRule('putAttachment', false, 'remove', that.eliminate);
that.addActionRule('putAttachment', false, 'putAttachment', that.update);
// end adding rules
////////////////////////////////////////////////////////////////////////////
return that;
}());
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global spec: true, localstorage: true,
activityUpdater: true, jobManager: true, storage: true,
storage_type_object: true, invalidStorageType: true, jobRules: true,
job: true, postCommand: true, putCommand: true, getCommand:true,
allDocsCommand: true, putAttachmentCommand: true,
removeCommand: true */
// Class jio
var that = {}, priv = {}, jio_id_array_name = 'jio/id_array';
spec = spec || {};
// Attributes //
priv.id = null;
priv.storage_spec = spec;
priv.environments = {};
// initialize //
priv.init = function () {
// Initialize the jio id and add the new id to the list
if (priv.id === null) {
var i, jio_id_a =
localstorage.getItem(jio_id_array_name) || [];
priv.id = 1;
for (i = 0; i < jio_id_a.length; i += 1) {
if (jio_id_a[i] >= priv.id) {
priv.id = jio_id_a[i] + 1;
}
}
jio_id_a.push(priv.id);
localstorage.setItem(jio_id_array_name, jio_id_a);
activityUpdater.setId(priv.id);
jobManager.setId(priv.id);
}
};
// Methods //
/**
* Returns a storage from a storage description.
* @method storage
* @param {object} spec The specifications.
* @param {object} my The protected object.
* @param {string} forcetype Force storage type
* @return {object} The storage object.
*/
Object.defineProperty(that, "storage", {
configurable: false,
enumerable: false,
writable: false,
value: function (spec, my, forcetype) {
var spec_str, type;
spec = spec || {};
my = my || {};
my.basicStorage = storage;
spec_str = JSON.stringify(spec);
// environment initialization
priv.environments[spec_str] = priv.environments[spec_str] || {};
my.env = priv.environments[spec_str];
my.storage = that.storage; // NOTE : or proxy storage
type = forcetype || spec.type || 'base';
if (type === 'base') {
return storage(spec, my);
}
if (!storage_type_object[type]) {
throw invalidStorageType({
"type": type,
"message": "Storage does not exists."
});
}
return storage_type_object[type](spec, my);
}
});
jobManager.storage = that.storage;
Object.defineProperty(that, "start", {
configurable: false,
enumerable: false,
writable: false,
value: function () {
priv.init();
activityUpdater.start();
jobManager.start();
}
});
Object.defineProperty(that, "stop", {
configurable: false,
enumerable: false,
writable: false,
value: function () {
jobManager.stop();
}
});
Object.defineProperty(that, "close", {
configurable: false,
enumerable: false,
writable: false,
value: function () {
activityUpdater.stop();
jobManager.stop();
priv.id = null;
}
});
/**
* Returns the jio id.
* @method getId
* @return {number} The jio id.
*/
Object.defineProperty(that, "getId", {
configurable: false,
enumerable: false,
writable: false,
value: function () {
return priv.id;
}
});
/**
* Returns the jio job rules object used by the job manager.
* @method getJobRules
* @return {object} The job rules object
*/
Object.defineProperty(that, "getJobRules", {
configurable: false,
enumerable: false,
writable: false,
value: function () {
return jobRules;
}
});
/**
* Checks if the storage description is valid or not.
* @method validateStorageDescription
* @param {object} description The description object.
* @return {boolean} true if ok, else false.
*/
Object.defineProperty(that, "validateStorageDescription", {
configurable: false,
enumerable: false,
writable: false,
value: function (description) {
return that.storage(description).isValid();
}
});
Object.defineProperty(that, "getJobArray", {
configurable: false,
enumerable: false,
writable: false,
value: function () {
return jobManager.serialized();
}
});
priv.makeCallbacks = function (param, callback1, callback2) {
param.callback = function (err, val) {
if (err) {
param.error(err);
} else {
param.success(val);
}
};
param.success = function (val) {
param.callback(undefined, val);
};
param.error = function (err) {
param.callback(err, undefined);
};
if (typeof callback1 === 'function') {
if (typeof callback2 === 'function') {
param.success = callback1;
param.error = callback2;
} else {
param.callback = callback1;
}
} else {
param.callback = function () {};
}
};
priv.parametersToObject = function (list, default_options) {
var k, i = 0, callbacks = [], param = {"options": {}};
for (i = 0; i < list.length; i += 1) {
if (typeof list[i] === 'object') {
// this is the option
param.options = list[i];
for (k in default_options) {
if ((typeof default_options[k]) !== (typeof list[i][k])) {
param.options[k] = default_options[k];
}
}
}
if (typeof list[i] === 'function') {
// this is a callback
callbacks.push(list[i]);
}
}
priv.makeCallbacks(param, callbacks[0], callbacks[1]);
return param;
};
priv.addJob = function (commandCreator, spec) {
jobManager.addJob(job({
"storage": that.storage(priv.storage_spec),
"command": commandCreator(spec)
}));
};
/**
* Post a document.
* @method post
* @param {object} doc The document object. Contains at least:
* - {string} _id The document id (optional), "/" are forbidden
* @param {object} options (optional) Contains some options:
* - {number} max_retry The number max of retries, 0 = infinity.
* - {boolean} revs Include revision history of the document.
* - {boolean} revs_info Retreive the revisions.
* - {boolean} conflicts Retreive the conflict list.
* @param {function} callback (optional) The callback(err,response).
* @param {function} error (optional) The callback on error, if this
* callback is given in parameter, "callback" is changed as "success",
* called on success.
*/
Object.defineProperty(that, "post", {
configurable: false,
enumerable: false,
writable: false,
value: function (doc, options, success, error) {
var param = priv.parametersToObject(
[options, success, error],
{max_retry: 0}
);
priv.addJob(postCommand, {
doc: doc,
options: param.options,
callbacks: {success: param.success, error: param.error}
});
}
});
/**
* Put a document.
* @method put
* @param {object} doc The document object. Contains at least:
* - {string} _id The document id, "/" are forbidden
* @param {object} options (optional) Contains some options:
* - {number} max_retry The number max of retries, 0 = infinity.
* - {boolean} revs Include revision history of the document.
* - {boolean} revs_info Retreive the revisions.
* - {boolean} conflicts Retreive the conflict list.
* @param {function} callback (optional) The callback(err,response).
* @param {function} error (optional) The callback on error, if this
* callback is given in parameter, "callback" is changed as "success",
* called on success.
*/
Object.defineProperty(that, "put", {
configurable: false,
enumerable: false,
writable: false,
value: function (doc, options, success, error) {
var param = priv.parametersToObject(
[options, success, error],
{max_retry: 0}
);
priv.addJob(putCommand, {
doc: doc,
options: param.options,
callbacks: {success: param.success, error: param.error}
});
}
});
/**
* Get a document.
* @method get
* @param {string} docid The document id: "doc_id" or "doc_id/attachmt_id".
* @param {object} options (optional) Contains some options:
* - {number} max_retry The number max of retries, 0 = infinity.
* - {string} rev The revision we want to get.
* - {boolean} revs Include revision history of the document.
* - {boolean} revs_info Include list of revisions, and their availability.
* - {boolean} conflicts Include a list of conflicts.
* @param {function} callback (optional) The callback(err,response).
* @param {function} error (optional) The callback on error, if this
* callback is given in parameter, "callback" is changed as "success",
* called on success.
*/
Object.defineProperty(that, "get", {
configurable: false,
enumerable: false,
writable: false,
value: function (id, options, success, error) {
var param = priv.parametersToObject(
[options, success, error],
{max_retry: 3}
);
priv.addJob(getCommand, {
docid: id,
options: param.options,
callbacks: {success: param.success, error: param.error}
});
}
});
/**
* Remove a document.
* @method remove
* @param {object} doc The document object. Contains at least:
* - {string} _id The document id: "doc_id" or "doc_id/attachment_id"
* @param {object} options (optional) Contains some options:
* - {number} max_retry The number max of retries, 0 = infinity.
* - {boolean} revs Include revision history of the document.
* - {boolean} revs_info Include list of revisions, and their availability.
* - {boolean} conflicts Include a list of conflicts.
* @param {function} callback (optional) The callback(err,response).
* @param {function} error (optional) The callback on error, if this
* callback is given in parameter, "callback" is changed as "success",
* called on success.
*/
Object.defineProperty(that, "remove", {
configurable: false,
enumerable: false,
writable: false,
value: function (doc, options, success, callback) {
var param = priv.parametersToObject(
[options, success, callback],
{max_retry: 0}
);
priv.addJob(removeCommand, {
doc: doc,
options: param.options,
callbacks: {success: param.success, error: param.error}
});
}
});
/**
* Get a list of documents.
* @method allDocs
* @param {object} options (optional) Contains some options:
* - {number} max_retry The number max of retries, 0 = infinity.
* - {boolean} include_docs Include document metadata
* - {boolean} revs Include revision history of the document.
* - {boolean} revs_info Include revisions.
* - {boolean} conflicts Include conflicts.
* @param {function} callback (optional) The callback(err,response).
* @param {function} error (optional) The callback on error, if this
* callback is given in parameter, "callback" is changed as "success",
* called on success.
*/
Object.defineProperty(that, "allDocs", {
configurable: false,
enumerable: false,
writable: false,
value: function (options, success, error) {
var param = priv.parametersToObject(
[options, success, error],
{max_retry: 3}
);
priv.addJob(allDocsCommand, {
options: param.options,
callbacks: {success: param.success, error: param.error}
});
}
});
/**
* Put an attachment to a document.
* @method putAttachment
* @param {object} doc The document object. Contains at least:
* - {string} id The document id: "doc_id/attchment_id"
* - {string} data Base64 attachment data
* - {string} mimetype The attachment mimetype
* - {string} rev The attachment revision
* @param {object} options (optional) Contains some options:
* - {number} max_retry The number max of retries, 0 = infinity.
* - {boolean} revs Include revision history of the document.
* - {boolean} revs_info Include revisions.
* - {boolean} conflicts Include conflicts.
* @param {function} callback (optional) The callback(err,respons)
* @param {function} error (optional) The callback on error, if this
* callback is given in parameter, "callback" is changed as "success",
* called on success.
*/
Object.defineProperty(that, "putAttachment", {
configurable: false,
enumerable: false,
writable: false,
value: function (doc, options, success, error) {
var param, k, doc_with_underscores = {};
param = priv.parametersToObject(
[options, success, error],
{max_retry: 0}
);
for (k in doc) {
if (doc.hasOwnProperty(k) && k.match('[^_].*')) {
doc_with_underscores["_" + k] = doc[k];
}
}
priv.addJob(putAttachmentCommand, {
doc: doc_with_underscores,
options: param.options,
callbacks: {success: param.success, error: param.error}
});
}
});
return that;
}; // End Class jio
/*jslint indent: 2, maxlen: 80, sloppy: true */
/*global jio: true, invalidStorageType: true */
var storage_type_object = { // -> 'key':constructorFunction
'base': function () {} // overriden by jio
};
var jioNamespace = (function (spec) {
var that = {};
spec = spec || {};
// Attributes //
// Methods //
/**
* Creates a new jio instance.
* @method newJio
* @param {object} spec The storage description
* @return {object} The new Jio instance.
*/
Object.defineProperty(that, "newJio", {
configurable: false,
enumerable: false,
writable: false,
value: function (spec) {
var storage = spec,
instance = null;
if (typeof storage === 'string') {
storage = JSON.parse(storage);
} else {
storage = JSON.stringify(storage);
if (storage !== undefined) {
storage = JSON.parse(storage);
}
}
storage = storage || {
type: 'base'
};
instance = jio(storage);
instance.start();
return instance;
}
});
/**
* Add a storage type to jio.
* @method addStorageType
* @param {string} type The storage type
* @param {function} constructor The associated constructor
*/
Object.defineProperty(that, "addStorageType", {
configurable: false,
enumerable: false,
writable: false,
value: function (type, constructor) {
constructor = constructor || function () {
return null;
};
if (storage_type_object[type]) {
throw invalidStorageType({
type: type,
message: 'Already known.'
});
}
storage_type_object[type] = constructor;
}
});
return that;
}());
Object.defineProperty(scope, "jIO", {
configurable: false,
enumerable: false,
writable: false,
value: jioNamespace
});
}(window, hex_md5));
/*
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
* Digest Algorithm, as defined in RFC 1321.
* Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
* Distributed under the BSD License
* See http://pajhome.org.uk/crypt/md5 for more info.
*/
/*
* Configurable variables. You may need to tweak these to be compatible with
* the server-side, but the defaults work in most cases.
*/
var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */
/*
* These are the functions you'll usually want to call
* They take string arguments and return either hex or base-64 encoded strings
*/
function hex_md5(s) { return rstr2hex(rstr_md5(str2rstr_utf8(s))); }
function b64_md5(s) { return rstr2b64(rstr_md5(str2rstr_utf8(s))); }
function any_md5(s, e) { return rstr2any(rstr_md5(str2rstr_utf8(s)), e); }
function hex_hmac_md5(k, d)
{ return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
function b64_hmac_md5(k, d)
{ return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
function any_hmac_md5(k, d, e)
{ return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e); }
/*
* Perform a simple self-test to see if the VM is working
*/
function md5_vm_test()
{
return hex_md5("abc").toLowerCase() == "900150983cd24fb0d6963f7d28e17f72";
}
/*
* Calculate the MD5 of a raw string
*/
function rstr_md5(s)
{
return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));
}
/*
* Calculate the HMAC-MD5, of a key and some data (raw strings)
*/
function rstr_hmac_md5(key, data)
{
var bkey = rstr2binl(key);
if(bkey.length > 16) bkey = binl_md5(bkey, key.length * 8);
var ipad = Array(16), opad = Array(16);
for(var i = 0; i < 16; i++)
{
ipad[i] = bkey[i] ^ 0x36363636;
opad[i] = bkey[i] ^ 0x5C5C5C5C;
}
var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));
}
/*
* Convert a raw string to a hex string
*/
function rstr2hex(input)
{
try { hexcase } catch(e) { hexcase=0; }
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
var output = "";
var x;
for(var i = 0; i < input.length; i++)
{
x = input.charCodeAt(i);
output += hex_tab.charAt((x >>> 4) & 0x0F)
+ hex_tab.charAt( x & 0x0F);
}
return output;
}
/*
* Convert a raw string to a base-64 string
*/
function rstr2b64(input)
{
try { b64pad } catch(e) { b64pad=''; }
var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var output = "";
var len = input.length;
for(var i = 0; i < len; i += 3)
{
var triplet = (input.charCodeAt(i) << 16)
| (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0)
| (i + 2 < len ? input.charCodeAt(i+2) : 0);
for(var j = 0; j < 4; j++)
{
if(i * 8 + j * 6 > input.length * 8) output += b64pad;
else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F);
}
}
return output;
}
/*
* Convert a raw string to an arbitrary string encoding
*/
function rstr2any(input, encoding)
{
var divisor = encoding.length;
var i, j, q, x, quotient;
/* Convert to an array of 16-bit big-endian values, forming the dividend */
var dividend = Array(Math.ceil(input.length / 2));
for(i = 0; i < dividend.length; i++)
{
dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1);
}
/*
* Repeatedly perform a long division. The binary array forms the dividend,
* the length of the encoding is the divisor. Once computed, the quotient
* forms the dividend for the next step. All remainders are stored for later
* use.
*/
var full_length = Math.ceil(input.length * 8 /
(Math.log(encoding.length) / Math.log(2)));
var remainders = Array(full_length);
for(j = 0; j < full_length; j++)
{
quotient = Array();
x = 0;
for(i = 0; i < dividend.length; i++)
{
x = (x << 16) + dividend[i];
q = Math.floor(x / divisor);
x -= q * divisor;
if(quotient.length > 0 || q > 0)
quotient[quotient.length] = q;
}
remainders[j] = x;
dividend = quotient;
}
/* Convert the remainders to the output string */
var output = "";
for(i = remainders.length - 1; i >= 0; i--)
output += encoding.charAt(remainders[i]);
return output;
}
/*
* Encode a string as utf-8.
* For efficiency, this assumes the input is valid utf-16.
*/
function str2rstr_utf8(input)
{
var output = "";
var i = -1;
var x, y;
while(++i < input.length)
{
/* Decode utf-16 surrogate pairs */
x = input.charCodeAt(i);
y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF)
{
x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF);
i++;
}
/* Encode output as utf-8 */
if(x <= 0x7F)
output += String.fromCharCode(x);
else if(x <= 0x7FF)
output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F),
0x80 | ( x & 0x3F));
else if(x <= 0xFFFF)
output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F),
0x80 | ((x >>> 6 ) & 0x3F),
0x80 | ( x & 0x3F));
else if(x <= 0x1FFFFF)
output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07),
0x80 | ((x >>> 12) & 0x3F),
0x80 | ((x >>> 6 ) & 0x3F),
0x80 | ( x & 0x3F));
}
return output;
}
/*
* Encode a string as utf-16
*/
function str2rstr_utf16le(input)
{
var output = "";
for(var i = 0; i < input.length; i++)
output += String.fromCharCode( input.charCodeAt(i) & 0xFF,
(input.charCodeAt(i) >>> 8) & 0xFF);
return output;
}
function str2rstr_utf16be(input)
{
var output = "";
for(var i = 0; i < input.length; i++)
output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF,
input.charCodeAt(i) & 0xFF);
return output;
}
/*
* Convert a raw string to an array of little-endian words
* Characters >255 have their high-byte silently ignored.
*/
function rstr2binl(input)
{
var output = Array(input.length >> 2);
for(var i = 0; i < output.length; i++)
output[i] = 0;
for(var i = 0; i < input.length * 8; i += 8)
output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (i%32);
return output;
}
/*
* Convert an array of little-endian words to a string
*/
function binl2rstr(input)
{
var output = "";
for(var i = 0; i < input.length * 32; i += 8)
output += String.fromCharCode((input[i>>5] >>> (i % 32)) & 0xFF);
return output;
}
/*
* Calculate the MD5 of an array of little-endian words, and a bit length.
*/
function binl_md5(x, len)
{
/* append padding */
x[len >> 5] |= 0x80 << ((len) % 32);
x[(((len + 64) >>> 9) << 4) + 14] = len;
var a = 1732584193;
var b = -271733879;
var c = -1732584194;
var d = 271733878;
for(var i = 0; i < x.length; i += 16)
{
var olda = a;
var oldb = b;
var oldc = c;
var oldd = d;
a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819);
b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426);
c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416);
d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682);
d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329);
a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
c = md5_gg(c, d, a, b, x[i+11], 14, 643717713);
b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083);
c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438);
d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501);
a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473);
b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562);
b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353);
c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174);
d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189);
a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
c = md5_hh(c, d, a, b, x[i+15], 16, 530742520);
b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415);
c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571);
d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359);
d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649);
a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259);
b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
a = safe_add(a, olda);
b = safe_add(b, oldb);
c = safe_add(c, oldc);
d = safe_add(d, oldd);
}
return Array(a, b, c, d);
}
/*
* These functions implement the four basic operations the algorithm uses.
*/
function md5_cmn(q, a, b, x, s, t)
{
return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
}
function md5_ff(a, b, c, d, x, s, t)
{
return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function md5_gg(a, b, c, d, x, s, t)
{
return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function md5_hh(a, b, c, d, x, s, t)
{
return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a, b, c, d, x, s, t)
{
return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
}
/*
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
* to work around bugs in some JS interpreters.
*/
function safe_add(x, y)
{
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xFFFF);
}
/*
* Bitwise rotate a 32-bit number to the left.
*/
function bit_rol(num, cnt)
{
return (num << cnt) | (num >>> (32 - cnt));
}
......@@ -552,6 +552,89 @@ var RenderJs = (function () {
};
}()),
GadgetCatalog : (function () {
/*
* Gadget catalog provides API to get list of gadgets from a repository
*/
var cache_id = "setGadgetIndexUrlList";
function updateGadgetIndexFromURL(url) {
// split to base and document url
var url_list = url.split('/'),
document_url = url_list[url_list.length-1];
url_list.splice($.inArray(document_url, url_list), 1);
var base_url = url_list.join('/'),
web_dav = jIO.newJio({
"type": "dav",
"username": "",
"password": "",
"url": base_url});
web_dav.get(document_url,
function (err, response) {
RenderJs.Cache.set(url, response);
});
};
return {
updateGadgetIndex: function () {
/*
* Update gadget index from all configured remote repositories.
*/
$.each(RenderJs.GadgetCatalog.getGadgetIndexUrlList(),
function(index, value) {
updateGadgetIndexFromURL(value);
});
},
setGadgetIndexUrlList: function (url_list) {
/*
* Set list of Gadget Index repositories.
*/
// store in Cache (html5 storage)
RenderJs.Cache.set(cache_id, url_list)
},
getGadgetIndexUrlList: function () {
/*
* Get list of Gadget Index repositories.
*/
// get from Cache (html5 storage)
return RenderJs.Cache.get(cache_id, undefined)
},
getGadgetListThatProvide: function (service) {
/*
* Return list of all gadgets that providen a given service.
* Read this list from data structure created in HTML5 local
* storage by updateGadgetIndexFromURL
*/
// XXX: get from Cache stored index and itterate over it
// to find matching ones
var gadget_list = new Array();
$.each(RenderJs.GadgetCatalog.getGadgetIndexUrlList(),
function(index, url) {
// get repos from cache
var cached_repo = RenderJs.Cache.get(url);
$.each(cached_repo['gadget_list'],
function(index, gadget) {
if (jQuery.inArray(service, gadget["service_list"]) > -1) {
// gadget provides a service, add to list
gadget_list.push(gadget);
}
}
)
});
return gadget_list;
},
registerServiceList: function (gadget, service_list) {
/*
* Register a service provided by a gadget.
*/
},
};
}()),
InteractionGadget : (function () {
/*
* Basic gadget interaction gadget implementation.
......
......@@ -3,11 +3,14 @@ require.config({
paths: {
jquery: "lib/jquery/jquery",
"jquery.json": "lib/json/jquery.json.min",
"davstorage": "lib/jio/davstorage",
"md5": "lib/jio/md5",
"jio": "lib/jio/jio",
renderjs: "renderjs"
},
shim: {
"jquery.json": [ "jquery" ],
renderjs: [ "jquery", "jquery.json" ]
renderjs: [ "jquery", "jquery.json", "md5", "jio", "davstorage"]
}
});
......
{
"gadget_list":[ {"title": "HTML WYSIWYG",
"description": "A simple HTML editor",
"url": "http://example.com/html-editor.html",
"service_list": ["edit_html", "view_html"]},
{"title": "SVG WYSIWYG",
"description": "A simple SVG editor",
"url": "http://example.com/svg-editor.html",
"service_list": ["edit_svg", "view_svg"]}
]
}
\ No newline at end of file
......@@ -24,6 +24,14 @@ function parseJSONAndUpdateNameSpace(result) {
last_name=result['last_name'];
}
function makeid() {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for( var i=0; i < 5; i++ )
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
}
function setupRenderJSTest(){
/*
* Main RenderJS test entry point
......@@ -164,5 +172,46 @@ function setupRenderJSTest(){
});
});
module("GadgetCatalog");
test('GadgetCatalog', function () {
cleanUp();
// allow test to be run alone (i.e. url contains arguments)
var base_url = window.location.protocol + "//" + window.location.hostname + window.location.pathname;
// generate random argument to test always with new cache id
var url_list = new Array(base_url + '/gadget_index/gadget_index.json?t='+makeid());
RenderJs.GadgetCatalog.setGadgetIndexUrlList(url_list)
deepEqual(url_list, RenderJs.GadgetCatalog.getGadgetIndexUrlList());
RenderJs.GadgetCatalog.updateGadgetIndex();
stop();
// XXX: until we have a way to know that update which runs asynchronously is over
// we use hard coded timeouts.
setTimeout(function(){
start();
cached = RenderJs.Cache.get(url_list[0]);
equal("HTML WYSIWYG", cached["gadget_list"][0]["title"]);
deepEqual(["edit_html", "view_html"], cached["gadget_list"][0]["service_list"]);
// check that we can find gadgets that provide some service_list
gadget_list = RenderJs.GadgetCatalog.getGadgetListThatProvide("edit_html");
equal("HTML WYSIWYG", gadget_list[0]["title"]);
deepEqual(["edit_html", "view_html"], gadget_list[0]["service_list"]);
gadget_list = RenderJs.GadgetCatalog.getGadgetListThatProvide("view_html");
equal("HTML WYSIWYG", gadget_list[0]["title"]);
deepEqual(["edit_html", "view_html"], gadget_list[0]["service_list"]);
gadget_list = RenderJs.GadgetCatalog.getGadgetListThatProvide("edit_svg");
equal("SVG WYSIWYG", gadget_list[0]["title"]);
deepEqual(["edit_svg", "view_svg"], gadget_list[0]["service_list"]);
// no such service is provided by gadget repos
equal(0, RenderJs.GadgetCatalog.getGadgetListThatProvide("edit_html1"));
}, 1000)
});
};
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment