/**
* Classes for using Opera XML Store
*
* @author Jani Hartikainen
*/
if(!CU)
var CU = {};
CU.opera = {};
/**
* XML Store URL
*/
CU.opera.STORE_URL = 'http://xmlstore.labs.opera.com/';
CU.opera.xmlStore = { };
/**
* XML Store proxy class
* @constructor
* @param {Object} params
*/
CU.opera.XmlStore = function(params) {
params = params || { };
this.storeUrl = params.storeUrl || CU.opera.STORE_URL;
};
CU.opera.XmlStore.prototype = {
user: null,
/**
* Perform a request to the store
* @method
* @param {String} method GET/PUT/POST/DELETE
* @param {String} path
* @param {Object|String} data
* @param {Function} callback
*/
_request: function(method, path, data, callback) {
if(this.user)
path = 'user/' + encodeURIComponent(this.user) + '/' + path;
var url = this.storeUrl + path;
var dataStr = '';
if (typeof(data) == 'string')
{
dataStr = data;
}
else
{
var dataArray = [];
for (var key in data)
dataArray.push(key + '=' + encodeURIComponent(data[key]));
dataStr = dataArray.join('&');
}
if(method == 'GET')
url += '?' + dataStr;
var xhr = new XMLHttpRequest();
//PUT etc need to use POST
if(method == 'GET')
xhr.open('GET', url, true);
else
xhr.open('POST', url, true);
//opera.postError(url);
xhr.setRequestHeader('Content-Type', 'text/xml');
xhr.setRequestHeader('X-Http-Accept-Override', 'text/xml');
if (method != 'POST' && method != 'GET')
{
// opera.postError('method-override: ' + method);
xhr.setRequestHeader('X-Http-Method-Override', method);
}
if (path.indexOf('login') == 0)
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function() {
if(this.readyState != 4)
return;
callback(this.status, this);
};
//opera.postError(dataStr);
if(method != 'GET')
xhr.send(dataStr);
else
xhr.send('');
},
/**
* Log in to the store
* @method
* @param {Object} params username, password
* @param {Function} callback
*/
login: function(params, callback) {
this.user = '';
var store = this;
this._request('POST', 'login', {
username: params.username,
password: params.password
}, function(status, request) {
if(status != 200)
{
callback(false);
return;
}
var xml = request.responseXML;
var sc = xml.selectSingleNode('//oxs:statuscode').textContent;
if (sc == 401)
callback(false);
else
{
store.user = params.username;
callback(true);
}
});
},
/**
* Creates or updates documents in the store
* @method
* @param {Object} doc must comply to CU.store.xmlStore.NoteDocument
* @param {Function} callback function(created/updated document)
*/
save: function(doc, callback) {
if(doc.id == '')
this.create(doc, callback);
else
this.update(doc, callback);
},
/**
* Creates new documents in the store
* @method
* @param {Object} doc must comply to CU.store.xmlStore.NoteDocument
* @param {Function} callback function(created document)
*/
create: function(doc, callback) {
this._request('POST', doc.model, doc.data, function(status, request) {
if(status != 200)
throw new CU.opera.xmlStore.Error('Create failed');
var xml = request.responseXML;
doc.id = xml.selectSingleNode('//oxs:id').textContent;
callback(doc);
});
},
/**
* Update a document
* @method
* @param {Object} doc must comply to CU.store.xmlStore.NoteDocument
* @param {Function} callback function(updated document)
*/
update: function(doc, callback) {
var path = doc.model + '/default/' + doc.id + '/?version=' + doc.version;
this._request('PUT', path, doc.data, function(status, request) {
if(status != 200)
throw new CU.opera.xmlStore.Error('Update failed');
var xml = request.responseXML;
var version = xml.selectSingleNode('//oxs:version').textContent;
if(xml.selectSingleNode('//oxs:statuscode').textContent == 409)
throw new CU.opera.xmlStore.VersionError(doc.version, version);
doc.version = version;
callback(doc);
});
},
/**
* Searches the store based on XPath
* @method
* @param {Function} type constructor for the document type to return
* @param {String} xpath XPath query for the store
* @param {Function} callback function(found document|null)
*/
findOneBy: function(type, xpath, callback) {
var doc = new type;
this._request('GET', doc.model, { q: xpath }, function(status, request) {
var xml = request.responseXML;
if(xml.selectSingleNode('//oxs:statuscode').textContent != 200)
throw new CU.opera.xmlStore.Error('Fetch failed');
var results = xml.selectSingleNode('//oxs:results');
if (results.getAttribute('count') < 1)
{
callback(null);
return;
}
doc.setFromXml(xml.selectSingleNode('//oxs:results/*[1]'));
callback(doc);
});
},
/**
* Get an item from the store by id
* @method
* @param {Function} type constructor for the document type to return
* @param {String} id document id
* @param {Function} callback function(found document|null)
*/
get: function(type, id, callback) {
var doc = new type;
this._request('GET', doc.model + '/default/' + id, { }, function(status, request) {
var xml = request.responseXML;
if(xml.selectSingleNode('//oxs:statuscode').textContent != 200)
throw new CU.opera.xmlStore.Error('Fetch failed');
var results = xml.selectSingleNode('//oxs:results');
if (results.getAttribute('count') < 1)
{
callback(null);
return;
}
doc.setFromXml(xml.selectSingleNode('//oxs:results/*[1]'));
callback(doc);
});
}
};
/**
* Generic error class
* @constructor
* @param {String} error message
*/
CU.opera.xmlStore.Error = function(error) {
this.message = error;
};
CU.opera.xmlStore.Error.prototype = Error.prototype;
/**
* Error thrown when encountering a version mismatch
* @constructor
* @param {Number} expectedVersion
* @param {Number} returnedVersion
*/
CU.opera.xmlStore.VersionError = function(expectedVersion, returnedVersion) {
this.expectedVersion = expectedVersion;
this.returnedVersion = returnedVersion;
this.message = 'Version mismatch: expected ' + this.expectedVersion
+ ', got ' + this.returnedVersion;
};
CU.opera.xmlStore.VersionError.prototype = Error.prototype;
/**
* Document class for the preferences datamodel
* @constructor
* @param {Object} params id, data, version
*/
CU.opera.xmlStore.PreferenceDocument = function(params) {
var params = params || { };
this.id = params.id || '';
this.setData(params.data);
this.version = params.version || 1;
};
CU.opera.xmlStore.PreferenceDocument.prototype = {
id: '',
data: '',
version: 1,
model: 'preferences',
entries: {},
widget: '',
setFromXml: function(xml) {
var ns = xml.lookupNamespaceURI('md');
this.id = xml.getAttributeNS(ns, 'id');
this.version = xml.getAttributeNS(ns, 'version');
var entryNodes = xml.selectNodes('//p:preferences/p:entry');
var entries = { };
for(var i = 0, len = entryNodes.length; i < len; i++)
{
var node = entryNodes[i];
entries[node.selectSingleNode('p:key').textContent] = node.selectSingleNode('p:value').textContent;
}
this.setData({
widget: xml.selectSingleNode('//p:preferences').getAttribute('id'),
entries: entries
});
},
setData: function(data) {
if(!data)
return;
this.widget = data.widget;
this.entries = data.entries;
this.data = [
'',
''
].join('');
for(var key in data.entries)
{
this.data += [
'',
'', key, '',
'', data.entries[key], '',
''
].join('');
}
this.data += '';
}
};
/**
* Document class for the notes datamodel
* @constructor
* @param {Object} params id, data, version
*/
CU.opera.xmlStore.NoteDocument = function(params) {
var params = params || { };
this.id = params.id || '';
this.setData(params.data);
this.version = params.version || 1;
};
CU.opera.xmlStore.NoteDocument.prototype = {
/**
* Document ID
* @type {String}
*/
id: '',
/**
* XML data for the document
* @type {String}
*/
data: '',
/**
* Document version
* @type {Number}
*/
version: 1,
/**
* datamodel name
* @type {String}
*/
model: 'notes',
/**
* Note title
* @type {String}
*/
title: '',
/**
* Note content
* @type {String}
*/
content: '',
/**
* Set object properties from XML
* @method
* @param {XMLNode} xml
*/
setFromXml: function(xml) {
var ns = xml.lookupNamespaceURI('md');
this.id = xml.getAttributeNS(ns, 'id');
this.version = xml.getAttributeNS(ns, 'version');
this.setData({
title: xml.selectSingleNode('//n:title').textContent,
content: xml.selectSingleNode('//n:content').textContent
});
},
/**
* Set document data
* @method
* @param {Object} data title, content
*/
setData: function(data) {
if(!data)
return;
this.title = data.title;
this.content = data.content;
this.data = [
'',
'',
'', data.title, '',
'', data.content, '',
''
].join('');
}
};