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