From b114266fb314fa2837417670712836ee6bdc0d7d Mon Sep 17 00:00:00 2001 From: Simon Kornblith Date: Thu, 30 Jun 2011 01:08:30 +0000 Subject: [PATCH] - Closes #1832, Connectors should be able to retrieve translator data and code from server in the absence of Zotero Standalone - Closes #1831, Connectors should be able to save via API in the absence of Zotero Standalone - Fixes Zotero.Utilities.deepCopy() for arrays - Fixes some circumstances where an error would not be saved for future error reporting - Fixes connector status checking --- .../zotero/xpcom/connector/cachedTypes.js | 70 +++++-- .../zotero/xpcom/connector/connector.js | 192 ++++-------------- .../zotero/xpcom/connector/connector_debug.js | 2 +- chrome/content/zotero/xpcom/connector/repo.js | 170 ++++++++++++++++ .../zotero/xpcom/connector/translate_item.js | 83 +++++++- .../zotero/xpcom/connector/translator.js | 145 ++++++++++--- .../zotero/xpcom/connector/typeSchemaData.js | 1 + .../content/zotero/xpcom/server_connector.js | 120 +++-------- .../zotero/xpcom/translation/translate.js | 2 +- chrome/content/zotero/xpcom/utilities.js | 2 +- chrome/content/zotero/xpcom/zotero.js | 18 +- components/zotero-service.js | 4 +- defaults/preferences/zotero.js | 6 +- 13 files changed, 508 insertions(+), 307 deletions(-) create mode 100644 chrome/content/zotero/xpcom/connector/repo.js create mode 100644 chrome/content/zotero/xpcom/connector/typeSchemaData.js diff --git a/chrome/content/zotero/xpcom/connector/cachedTypes.js b/chrome/content/zotero/xpcom/connector/cachedTypes.js index 182f6caccf..ee81017d1d 100644 --- a/chrome/content/zotero/xpcom/connector/cachedTypes.js +++ b/chrome/content/zotero/xpcom/connector/cachedTypes.js @@ -30,42 +30,49 @@ /** * @namespace */ -if(!Zotero.Connector) Zotero.Connector = {}; -Zotero.Connector.Types = new function() { +Zotero.Connector_Types = new function() { /** * Initializes types * @param {Object} typeSchema typeSchema generated by Zotero.Connector.GetData#_generateTypeSchema */ - this.init = function(typeSchema) { + this.init = function() { const schemaTypes = ["itemTypes", "creatorTypes", "fields"]; // attach IDs and make referenceable by either ID or name for(var i=0; i. + + ***** END LICENSE BLOCK ***** +*/ + +const TRANSLATOR_CODE_PREFIX = "translatorCode-"; +Zotero.Repo = new function() { + var _nextCheck; + var _timeoutID; + + /** + * Try to retrieve translator metadata from Zotero Standalone and initialize repository check + * timer + */ + this.init = function() { + // get time of next check + _nextCheck = Zotero.Prefs.get("connector.repo.lastCheck.localTime") + +ZOTERO_CONFIG.REPOSITORY_CHECK_INTERVAL*1000; + + // update from standalone, but only cascade to repo if we are overdue + _updateFromStandalone(_nextCheck <= Date.now()); + }; + + /** + * Reset all translators and code + */ + this.reset = function(callback) { + Zotero.Prefs.set("connector.repo.lastCheck.repoTime", 0); + this.update(true); + }; + + /** + * Force updating translators + */ + var update = this.update = function(reset, callback) { + _updateFromStandalone(true, reset); + }; + + /** + * Get translator code from repository + * @param {String} translatorID ID of the translator to retrieve code for + * @param {Function} callback Callback to pass code when retreived + */ + this.getTranslatorCode = function(translatorID, callback) { + // we might have code in localstorage + if(!Zotero.isFx) { + var localCode = localStorage[TRANSLATOR_CODE_PREFIX+translatorID]; + if(localCode) { + callback(localCode); + return; + } + } + + // otherwise, try standalone + Zotero.Connector.callMethod("getTranslatorCode", {"translatorID":translatorID}, function(result) { + if(result) { + _haveCode(result, translatorID, callback); + return; + } + + // then try repo + Zotero.HTTP.doGet(ZOTERO_CONFIG.REPOSITORY_URL+"/code/"+translatorID, function(xmlhttp) { + _haveCode(xmlhttp.status === 200 ? xmlhttp.responseText : false, translatorID, callback); + }); + }); + }; + + /** + * Called when code has been retrieved from standalone or repo + */ + function _haveCode(code, translatorID, callback) { + if(!code) { + callback(false); + return; + } + + if(!Zotero.isFx) { + localStorage["translatorCode-"+translatorID] = Zotero.Translators.preprocessCode(code); + } + callback(code); + } + + /** + * Retrieve translator metadata from Zotero Standalone + * @param {Boolean} [tryRepoOnFailure] If true, run _updateFromRepo() if standalone cannot be + * contacted + */ + function _updateFromStandalone(tryRepoOnFailure, reset, callback) { + Zotero.Connector.callMethod("getTranslators", {}, function(result) { + if(!result && tryRepoOnFailure) { + _updateFromRepo(reset, callback); + } else { + _handleResponse(result, reset); + if(callback) callback(!!result); + } + }); + } + + /** + * Retrieve metadata from repository + */ + function _updateFromRepo(reset, callback) { + var url = ZOTERO_CONFIG.REPOSITORY_URL+"/metadata?last="+ + Zotero.Prefs.get("connector.repo.lastCheck.repoTime"); + + Zotero.HTTP.doGet(url, function(xmlhttp) { + var success = xmlhttp.status === 200; + _handleResponse(success ? JSON.parse(xmlhttp.responseText) : false, reset); + + if(success) { + var date = xmlhttp.getResponseHeader("Date"); + Zotero.Prefs.set("connector.repo.lastCheck.repoTime", + Math.floor(Date.parse(date)/1000)); + } + if(callback) callback(!!result); + }); + } + + /** + * Handle response from Zotero Standalone or repository and set timer for next update + */ + function _handleResponse(result, reset) { + // set up timer + var now = Date.now(); + + if(result) { + Zotero.Translators.update(result, reset); + Zotero.Prefs.set("connector.repo.lastCheck.localTime", now); + Zotero.debug("Repo: Check succeeded"); + } else { + Zotero.debug("Repo: Check failed"); + } + + if(result || _nextCheck <= now) { + // if we failed a scheduled check, then use retry interval + _nextCheck = now+(result + ? ZOTERO_CONFIG.REPOSITORY_CHECK_INTERVAL + : ZOTERO_CONFIG.REPOSITORY_RETRY_INTERVAL)*1000; + } else if(_timeoutID) { + // if we didn't fail a scheduled check and another is already scheduled, leave it + return; + } + + // remove old timeout and create a new one + if(_timeoutID) clearTimeout(_timeoutID); + var nextCheckIn = (_nextCheck-now+2000); + _timeoutID = setTimeout(update, nextCheckIn); + Zotero.debug("Repo: Next check in "+nextCheckIn); + } +} \ No newline at end of file diff --git a/chrome/content/zotero/xpcom/connector/translate_item.js b/chrome/content/zotero/xpcom/connector/translate_item.js index bced5f3f60..ab3cd0e5af 100644 --- a/chrome/content/zotero/xpcom/connector/translate_item.js +++ b/chrome/content/zotero/xpcom/connector/translate_item.js @@ -25,6 +25,9 @@ Zotero.Translate.ItemSaver = function(libraryID, attachmentMode, forceTagType) { this.newItems = []; + + this._itemsToSaveToServer = []; + this._timeoutID = null; } Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE = 0; @@ -43,6 +46,84 @@ Zotero.Translate.ItemSaver.prototype = { // save items this.newItems.push(item); - Zotero.Connector.callMethod("saveItems", {"items":[item]}, function(success) {}); + var me = this; + Zotero.Connector.callMethod("saveItems", {"items":[item]}, function(success) { + if(success === false && !Zotero.isFx) { + // attempt to save to server on a timer + if(me._timeoutID) clearTimeout(me._timeoutID); + me._itemsToSaveToServer.push(item); + setTimeout(function() { me._saveToServer() }, 2000); + } + }); + }, + + "_saveToServer":function() { + const IGNORE_FIELDS = ["seeAlso", "attachments", "complete"]; + + // clear timeout, since saving has begin + this._timeoutID = null; + + var newItems = new Array(this._itemsToSaveToServer.length); + for(var i in this._itemsToSaveToServer) { + var item = this._itemsToSaveToServer[i]; + var newItem = newItems[i] = {}; + + var typeID = Zotero.ItemTypes.getID(item.itemType); + var fieldID; + for(var field in item) { + if(IGNORE_FIELDS.indexOf(field) !== -1) continue; + + var val = item[field]; + + if(field === "itemType") { + newItem[field] = val; + } else if(field === "creators") { + // TODO normalize + newItem[field] = val; + } else if(field === "tags") { + // TODO normalize + newItem[field] = val; + } else if(field === "notes") { + // TODO normalize + newItem[field] = val; + } else if(fieldID = Zotero.ItemFields.getID(field)) { + // if content is not a string, either stringify it or delete it + if(typeof val !== "string") { + if(val || val === 0) { + val = val.toString(); + } else { + continue; + } + } + + // map from base field if possible + var itemFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(typeID, fieldID); + if(itemFieldID) { + newItem[Zotero.ItemFields.getName(itemFieldID)] = val; + continue; // already know this is valid + } + + // if field is valid for this type, set field + if(Zotero.ItemFields.isValidForType(fieldID, typeID)) { + newItem[field] = val; + } else { + Zotero.debug("Translate: Discarded field "+field+": field not valid for type "+item.itemType, 3); + } + } else if(field !== "complete") { + Zotero.debug("Translate: Discarded unknown field "+field, 3); + } + } + } + + var url = 'users/%%USERID%%/items?key=%%APIKEY%%'; + var payload = JSON.stringify({"items":newItems}); + this._itemsToSaveToServer = []; + + Zotero.OAuth.doAuthenticatedPost(url, payload, function(status, message) { + if(!status) { + Zotero.Messaging.sendMessage("saveDialog_error", status); + throw new Error("Translate: Save to server failed: "+message); + } + }, true); } }; \ No newline at end of file diff --git a/chrome/content/zotero/xpcom/connector/translator.js b/chrome/content/zotero/xpcom/connector/translator.js index 236ea15803..ff834230a0 100644 --- a/chrome/content/zotero/xpcom/connector/translator.js +++ b/chrome/content/zotero/xpcom/connector/translator.js @@ -36,15 +36,28 @@ Zotero.Translators = new function() { /** * Initializes translator cache, loading all relevant translators into memory + * @param {Zotero.Translate[]} [translators] List of translators. If not specified, it will be + * retrieved from storage. */ - this.init = function() { + this.init = function(translators) { + if(!translators) { + translators = []; + if(!Zotero.isFx && localStorage["translatorMetadata"]) { + try { + translators = JSON.parse(localStorage["translatorMetadata"]); + if(typeof translators !== "object") { + translators = []; + } + } catch(e) {} + } + } + _cache = {"import":[], "export":[], "web":[], "search":[]}; _translators = {}; _initialized = true; // Build caches - var translators = Zotero.Connector.data.translators; - for(var i=0; i 1) { diff --git a/chrome/content/zotero/xpcom/utilities.js b/chrome/content/zotero/xpcom/utilities.js index e899d9f9ce..bb5ee8cd39 100644 --- a/chrome/content/zotero/xpcom/utilities.js +++ b/chrome/content/zotero/xpcom/utilities.js @@ -538,7 +538,7 @@ Zotero.Utilities = { * @return {Object} */ "deepCopy":function(obj) { - var obj2 = {}; + var obj2 = (obj instanceof Array ? [] : {}); for(var i in obj) { if(typeof obj[i] === "object") { obj2[i] = Zotero.Utilities.deepCopy(obj[i]); diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js index 20b18d1e00..7afc1a0f7a 100644 --- a/chrome/content/zotero/xpcom/zotero.js +++ b/chrome/content/zotero/xpcom/zotero.js @@ -424,7 +424,16 @@ if(appInfo.platformVersion[0] >= 2) { // Load additional info for connector or not if(Zotero.isConnector) { Zotero.debug("Loading in connector mode"); - Zotero.Connector.init(); + Zotero.Connector_Types.init(); + + if(!Zotero.isFirstLoadThisSession) { + // wait for initComplete message if we switched to connector because standalone was + // started + _waitingForInitComplete = true; + while(_waitingForInitComplete) Zotero.mainThread.processNextEvent(true); + } + + Zotero.Repo.init(); } else { Zotero.debug("Loading in full mode"); _initFull(); @@ -444,13 +453,6 @@ if(appInfo.platformVersion[0] >= 2) { Zotero.debug("Initialized in "+((new Date()).getTime() - start)+" ms"); if(!Zotero.isFirstLoadThisSession) { - if(Zotero.isConnector) { - // wait for initComplete message if we switched to connector because standalone was - // started - _waitingForInitComplete = true; - while(_waitingForInitComplete) Zotero.mainThread.processNextEvent(true); - } - // trigger zotero-reloaded event Zotero.debug('Triggering "zotero-reloaded" event'); observerService.notifyObservers(Zotero, "zotero-reloaded", null); diff --git a/components/zotero-service.js b/components/zotero-service.js index b8a5aa4428..c7bd615fff 100644 --- a/components/zotero-service.js +++ b/components/zotero-service.js @@ -114,7 +114,9 @@ const xpcomFilesConnector = [ 'connector/translate_item', 'connector/translator', 'connector/connector', - 'connector/cachedTypes' + 'connector/cachedTypes', + 'connector/repo', + 'connector/typeSchemaData' ]; Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); diff --git a/defaults/preferences/zotero.js b/defaults/preferences/zotero.js index 832baccd8b..7fb63053c1 100644 --- a/defaults/preferences/zotero.js +++ b/defaults/preferences/zotero.js @@ -152,4 +152,8 @@ pref("extensions.zotero.purge.tags", false); pref("extensions.zotero.pane.persist", ''); // Domains allowed to import, separated by a semicolon -pref("extensions.zotero.ingester.allowedSites", ""); \ No newline at end of file +pref("extensions.zotero.ingester.allowedSites", ""); + +// Connector +pref("extensions.zotero.connector.repo.lastCheck.localTime", 0); +pref("extensions.zotero.connector.repo.lastCheck.repoTime", 0); \ No newline at end of file