updateBundledStyles() asyncification and related changes

- Use async DB and OS.File for bundled file updates
- Remove support for translator/style ZIP files -- the two options are
  now non-unpacked XPIs with subfolders or unpacked source installations
- Now that we have async file access, don't store translator code in
  database cache -- just store metadata so that it's available without
  reading each translator file
- Change the (previously partially asyncified) Zotero.Styles/Translators
  APIs a bit -- while the getAll/getVisible methods are asynchronous and
  will wait for loading, the get() methods are synchronous and require
  styles/translators to be initialized before they're called. Most
  places that end up calling get() probably call getAll/getVisible first
  and should therefore be async, but if there's any way to trigger a
  get() first, that will need to be adjusted.
- Asyncify various other style/translator-related code

XPI support is untested, as is style/translator usage, so there are
almost certainly bugs. The latter depends on updated export format
support (#659), since toArray() no longer exists on this branch.

Addresses #529 and #520
This commit is contained in:
Dan Stillman 2015-03-22 02:06:40 -04:00
parent 842082f818
commit d8f3be4bee
11 changed files with 1030 additions and 946 deletions

View file

@ -134,8 +134,7 @@ Zotero_Preferences.Advanced = {
if (Zotero_Preferences.Export) { if (Zotero_Preferences.Export) {
Zotero_Preferences.Export.populateQuickCopyList(); Zotero_Preferences.Export.populateQuickCopyList();
} }
}) });
.done();
} }
}, },
@ -160,8 +159,7 @@ Zotero_Preferences.Advanced = {
if (Zotero_Preferences.Export) { if (Zotero_Preferences.Export) {
Zotero_Preferences.Export.populateQuickCopyList(); Zotero_Preferences.Export.populateQuickCopyList();
} }
}) });
.done();
} }
}, },
@ -186,8 +184,7 @@ Zotero_Preferences.Advanced = {
if (Zotero_Preferences.Export) { if (Zotero_Preferences.Export) {
Zotero_Preferences.Export.populateQuickCopyList(); Zotero_Preferences.Export.populateQuickCopyList();
} }
}) });
.done();
} }
}, },

View file

@ -26,10 +26,10 @@
"use strict"; "use strict";
Zotero_Preferences.Cite = { Zotero_Preferences.Cite = {
init: function () { init: Zotero.Promise.coroutine(function* () {
this.updateWordProcessorInstructions(); this.updateWordProcessorInstructions();
this.refreshStylesList(); yield this.refreshStylesList();
}, }),
/** /**
@ -48,8 +48,9 @@ Zotero_Preferences.Cite = {
/** /**
* Refreshes the list of styles in the styles pane * Refreshes the list of styles in the styles pane
* @param {String} cslID Style to select * @param {String} cslID Style to select
* @return {Promise}
*/ */
refreshStylesList: function (cslID) { refreshStylesList: Zotero.Promise.coroutine(function* (cslID) {
Zotero.debug("Refreshing styles list"); Zotero.debug("Refreshing styles list");
var treechildren = document.getElementById('styleManager-rows'); var treechildren = document.getElementById('styleManager-rows');
@ -57,11 +58,9 @@ Zotero_Preferences.Cite = {
treechildren.removeChild(treechildren.firstChild); treechildren.removeChild(treechildren.firstChild);
} }
var styles = Zotero.Styles.getVisible(); var styles = yield Zotero.Styles.getVisible();
var selectIndex = false; var selectIndex = false;
var i = 0; styles.forEach(function (style, i) {
for each(var style in styles) {
var treeitem = document.createElement('treeitem'); var treeitem = document.createElement('treeitem');
var treerow = document.createElement('treerow'); var treerow = document.createElement('treerow');
var titleCell = document.createElement('treecell'); var titleCell = document.createElement('treecell');
@ -86,9 +85,8 @@ Zotero_Preferences.Cite = {
if (cslID == style.styleID) { if (cslID == style.styleID) {
document.getElementById('styleManager').view.selection.select(i); document.getElementById('styleManager').view.selection.select(i);
} }
i++; });
} }),
},
/** /**
@ -112,7 +110,7 @@ Zotero_Preferences.Cite = {
/** /**
* Deletes selected styles from the styles pane * Deletes selected styles from the styles pane
**/ **/
deleteStyle: function () { deleteStyle: Zotero.Promise.coroutine(function* () {
// get selected cslIDs // get selected cslIDs
var tree = document.getElementById('styleManager'); var tree = document.getElementById('styleManager');
var treeItems = tree.lastChild.childNodes; var treeItems = tree.lastChild.childNodes;
@ -141,17 +139,17 @@ Zotero_Preferences.Cite = {
if(ps.confirm(null, '', text)) { if(ps.confirm(null, '', text)) {
// delete if requested // delete if requested
if(cslIDs.length == 1) { if(cslIDs.length == 1) {
selectedStyle.remove(); yield selectedStyle.remove();
} else { } else {
for(var i=0; i<cslIDs.length; i++) { for(var i=0; i<cslIDs.length; i++) {
Zotero.Styles.get(cslIDs[i]).remove(); yield Zotero.Styles.get(cslIDs[i]).remove();
} }
} }
this.refreshStylesList(); yield this.refreshStylesList();
document.getElementById('styleManager-delete').disabled = true; document.getElementById('styleManager-delete').disabled = true;
} }
}, }),
/** /**

View file

@ -41,24 +41,24 @@ Zotero_Preferences.Export = {
/* /*
* Builds the main Quick Copy drop-down from the current global pref * Builds the main Quick Copy drop-down from the current global pref
*/ */
populateQuickCopyList: function () { populateQuickCopyList: Zotero.Promise.coroutine(function* () {
// Initialize default format drop-down // Initialize default format drop-down
var format = Zotero.Prefs.get("export.quickCopy.setting"); var format = Zotero.Prefs.get("export.quickCopy.setting");
var menulist = document.getElementById("zotero-quickCopy-menu"); var menulist = document.getElementById("zotero-quickCopy-menu");
menulist.setAttribute('preference', "pref-quickCopy-setting"); menulist.setAttribute('preference', "pref-quickCopy-setting");
this.buildQuickCopyFormatDropDown(menulist, Zotero.QuickCopy.getContentType(format), format); yield this.buildQuickCopyFormatDropDown(menulist, Zotero.QuickCopy.getContentType(format), format);
this.updateQuickCopyHTMLCheckbox(document); this.updateQuickCopyHTMLCheckbox(document);
if (!Zotero.isStandalone) { if (!Zotero.isStandalone) {
this.refreshQuickCopySiteList(); yield this.refreshQuickCopySiteList();
} }
}, }),
/* /*
* Builds a Quick Copy drop-down * Builds a Quick Copy drop-down
*/ */
buildQuickCopyFormatDropDown: function (menulist, contentType, currentFormat) { buildQuickCopyFormatDropDown: Zotero.Promise.coroutine(function* (menulist, contentType, currentFormat) {
if (!currentFormat) { if (!currentFormat) {
currentFormat = menulist.value; currentFormat = menulist.value;
} }
@ -84,8 +84,8 @@ Zotero_Preferences.Export = {
popup.appendChild(itemNode); popup.appendChild(itemNode);
// add styles to list // add styles to list
var styles = Zotero.Styles.getVisible(); var styles = yield Zotero.Styles.getVisible();
for each(var style in styles) { styles.forEach(function (style) {
var baseVal = 'bibliography=' + style.styleID; var baseVal = 'bibliography=' + style.styleID;
var val = 'bibliography' + (contentType == 'html' ? '/html' : '') + '=' + style.styleID; var val = 'bibliography' + (contentType == 'html' ? '/html' : '') + '=' + style.styleID;
var itemNode = document.createElement("menuitem"); var itemNode = document.createElement("menuitem");
@ -97,7 +97,7 @@ Zotero_Preferences.Export = {
if (baseVal == currentFormat) { if (baseVal == currentFormat) {
menulist.selectedItem = itemNode; menulist.selectedItem = itemNode;
} }
} });
var itemNode = document.createElement("menuitem"); var itemNode = document.createElement("menuitem");
itemNode.setAttribute("label", Zotero.getString('zotero.preferences.export.quickCopy.exportFormats')); itemNode.setAttribute("label", Zotero.getString('zotero.preferences.export.quickCopy.exportFormats'));
@ -106,31 +106,28 @@ Zotero_Preferences.Export = {
// add export formats to list // add export formats to list
var translation = new Zotero.Translate("export"); var translation = new Zotero.Translate("export");
translation.getTranslators() var translators = yield translation.getTranslators();
.then(function (translators) { translators.forEach(function (translator) {
for (var i=0; i<translators.length; i++) { // Skip RDF formats
// Skip RDF formats switch (translator.translatorID) {
switch (translators[i].translatorID) { case '6e372642-ed9d-4934-b5d1-c11ac758ebb7':
case '6e372642-ed9d-4934-b5d1-c11ac758ebb7': case '14763d24-8ba0-45df-8f52-b8d1108e7ac9':
case '14763d24-8ba0-45df-8f52-b8d1108e7ac9': return;
continue;
}
var val = 'export=' + translators[i].translatorID;
var itemNode = document.createElement("menuitem");
itemNode.setAttribute("value", val);
itemNode.setAttribute("label", translators[i].label);
itemNode.setAttribute("oncommand", 'Zotero_Preferences.Export.updateQuickCopyHTMLCheckbox(document)');
popup.appendChild(itemNode);
if (val == currentFormat) {
menulist.selectedItem = itemNode;
}
} }
var val = 'export=' + translator.translatorID;
var itemNode = document.createElement("menuitem");
itemNode.setAttribute("value", val);
itemNode.setAttribute("label", translator.label);
itemNode.setAttribute("oncommand", 'Zotero_Preferences.Export.updateQuickCopyHTMLCheckbox(document)');
popup.appendChild(itemNode);
menulist.click(); if (val == currentFormat) {
}) menulist.selectedItem = itemNode;
.done(); }
}, });
menulist.click();
}),
updateQuickCopyHTMLCheckbox: function (doc) { updateQuickCopyHTMLCheckbox: function (doc) {
@ -219,7 +216,7 @@ Zotero_Preferences.Export = {
var domainPath = treeitem.firstChild.firstChild.getAttribute('label'); var domainPath = treeitem.firstChild.firstChild.getAttribute('label');
yield Zotero.DB.queryAsync("DELETE FROM settings WHERE setting='quickCopySite' AND key=?", [domainPath]); yield Zotero.DB.queryAsync("DELETE FROM settings WHERE setting='quickCopySite' AND key=?", [domainPath]);
yield Zotero.QuickCopy.loadSiteSettings(); yield Zotero.QuickCopy.loadSiteSettings();
this.refreshQuickCopySiteList(); yield this.refreshQuickCopySiteList();
}), }),

View file

@ -39,27 +39,24 @@ Zotero_Preferences.General = {
}, },
updateTranslators: function () { updateTranslators: Zotero.Promise.coroutine(function* () {
Zotero.Schema.updateFromRepository(true) var updated = yield Zotero.Schema.updateFromRepository(true);
.then(function (updated) { var button = document.getElementById('updateButton');
var button = document.getElementById('updateButton'); if (button) {
if (button) { if (updated===-1) {
if (updated===-1) { var label = Zotero.getString('zotero.preferences.update.upToDate');
var label = Zotero.getString('zotero.preferences.update.upToDate');
}
else if (updated) {
var label = Zotero.getString('zotero.preferences.update.updated');
}
else {
var label = Zotero.getString('zotero.preferences.update.error');
}
button.setAttribute('label', label);
if (updated && Zotero_Preferences.Cite) {
Zotero_Preferences.Cite.refreshStylesList();
}
} }
}) else if (updated) {
.done(); var label = Zotero.getString('zotero.preferences.update.updated');
} }
else {
var label = Zotero.getString('zotero.preferences.update.error');
}
button.setAttribute('label', label);
if (updated && Zotero_Preferences.Cite) {
yield Zotero_Preferences.Cite.refreshStylesList();
}
}
})
} }

View file

@ -28,7 +28,6 @@
* @namespace * @namespace
*/ */
Zotero.File = new function(){ Zotero.File = new function(){
//Components.utils.import("resource://zotero/bluebird.js");
Components.utils.import("resource://gre/modules/NetUtil.jsm"); Components.utils.import("resource://gre/modules/NetUtil.jsm");
Components.utils.import("resource://gre/modules/FileUtils.jsm"); Components.utils.import("resource://gre/modules/FileUtils.jsm");
@ -44,15 +43,20 @@ Zotero.File = new function(){
this.pathToFile = function (pathOrFile) { this.pathToFile = function (pathOrFile) {
if (typeof pathOrFile == 'string') { if (typeof pathOrFile == 'string') {
let nsIFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); return new FileUtils.File(pathOrFile);
nsIFile.initWithPath(pathOrFile);
return nsIFile;
} }
else if (pathOrFile instanceof Ci.nsIFile) { else if (pathOrFile instanceof Ci.nsIFile) {
return pathOrFile; return pathOrFile;
} }
throw new Error('Unexpected value provided to Zotero.File.pathToFile() (' + pathOrFile + ')');
throw new Error('Unexpected value provided to Zotero.MIME.pathToFile() (' + pathOrFile + ')'); }
this.pathToFileURI = function (path) {
var file = new FileUtils.File(path);
var ios = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
return ios.newFileURI(file).spec;
} }
@ -133,7 +137,7 @@ Zotero.File = new function(){
/** /**
* Get the contents of a file or input stream * Get the contents of a file or input stream
* @param {nsIFile|nsIInputStream} file The file to read * @param {nsIFile|nsIInputStream|string path} file The file to read
* @param {String} [charset] The character set; defaults to UTF-8 * @param {String} [charset] The character set; defaults to UTF-8
* @param {Integer} [maxLength] The maximum number of bytes to read * @param {Integer} [maxLength] The maximum number of bytes to read
* @return {String} The contents of the file * @return {String} The contents of the file
@ -141,6 +145,11 @@ Zotero.File = new function(){
*/ */
this.getContents = function (file, charset, maxLength){ this.getContents = function (file, charset, maxLength){
var fis; var fis;
if (typeof file == 'string') {
file = new FileUtils.File(file);
}
if(file instanceof Components.interfaces.nsIInputStream) { if(file instanceof Components.interfaces.nsIInputStream) {
fis = file; fis = file;
} else if(file instanceof Components.interfaces.nsIFile) { } else if(file instanceof Components.interfaces.nsIFile) {
@ -282,7 +291,7 @@ Zotero.File = new function(){
* Return a promise for the contents of a URL as a string * Return a promise for the contents of a URL as a string
*/ */
this.getContentsFromURLAsync = function (url) { this.getContentsFromURLAsync = function (url) {
return Zotero.HTTP.promise("GET", url, { responseType: "text" }) return Zotero.HTTP.request("GET", url, { responseType: "text" })
.then(function (xmlhttp) { .then(function (xmlhttp) {
return xmlhttp.response; return xmlhttp.response;
}); });
@ -364,16 +373,16 @@ Zotero.File = new function(){
/** /**
* Delete a file if it exists, asynchronously * Delete a file if it exists, asynchronously
* *
* @return {Promise<Boolean>} A Q promise for TRUE if file was deleted, * @return {Promise<Boolean>} A promise for TRUE if file was deleted, FALSE if missing
* FALSE if missing
*/ */
this.deleteIfExists = function deleteIfExists(path) { this.removeIfExists = function (path) {
return Zotero.Promise.resolve(OS.File.remove(path)) return Zotero.Promise.resolve(OS.File.remove(path))
.thenResolve(true) .return(true)
.catch(function (e) { .catch(function (e) {
if (e instanceof OS.File.Error && e.becauseNoSuchFile) { if (e instanceof OS.File.Error && e.becauseNoSuchFile) {
return false; return false;
} }
Zotero.debug(path, 1);
throw e; throw e;
}); });
} }
@ -575,6 +584,19 @@ Zotero.File = new function(){
} }
this.createDirectoryIfMissingAsync = function (path) {
return Zotero.Promise.resolve(
OS.File.makeDir(
path,
{
ignoreExisting: true,
unixMode: 0755
}
)
);
}
/** /**
* Check whether a directory is an ancestor directory of another directory/file * Check whether a directory is an ancestor directory of another directory/file
*/ */

File diff suppressed because it is too large Load diff

View file

@ -30,28 +30,50 @@
*/ */
Zotero.Styles = new function() { Zotero.Styles = new function() {
var _initialized = false; var _initialized = false;
var _styles, _visibleStyles, _cacheTranslatorData; var _styles, _visibleStyles;
var _renamedStyles = null; var _renamedStyles = null;
//Components.utils.import("resource://zotero/bluebird.js");
Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/FileUtils.jsm");
this.xsltProcessor = null; this.xsltProcessor = null;
this.ios = Components.classes["@mozilla.org/network/io-service;1"].
getService(Components.interfaces.nsIIOService);
this.ns = { this.ns = {
"csl":"http://purl.org/net/xbiblio/csl" "csl":"http://purl.org/net/xbiblio/csl"
}; };
// TEMP
// Until we get asynchronous style loading, load renamed styles at startup, since the /**
// synchronous call we were using breaks the first drag of the session (on OS X, at least) * Initializes styles cache, loading metadata for styles into memory
this.preinit = function () { */
this.reinit = Zotero.Promise.coroutine(function* () {
Zotero.debug("Initializing styles");
var start = new Date;
_initialized = true;
_styles = {};
_visibleStyles = [];
this.lastCSL = null;
// main dir
var dir = Zotero.getStylesDirectory().path;
var num = yield _readStylesFromDirectory(dir, false);
// hidden dir
var hiddenDir = OS.Path.join(dir, 'hidden');
if (yield OS.File.exists(hiddenDir)) {
num += yield _readStylesFromDirectory(hiddenDir, true);
}
Zotero.debug("Cached " + num + " styles in " + (new Date - start) + " ms");
_renamedStyles = {}; _renamedStyles = {};
Zotero.HTTP.promise( yield Zotero.HTTP.request(
"GET", "resource://zotero/schema/renamed-styles.json", { responseType: 'json' } "GET",
"resource://zotero/schema/renamed-styles.json",
{
responseType: 'json'
}
) )
.then(function (xmlhttp) { .then(function (xmlhttp) {
// Map some obsolete styles to current ones // Map some obsolete styles to current ones
@ -59,87 +81,70 @@ Zotero.Styles = new function() {
_renamedStyles = xmlhttp.response; _renamedStyles = xmlhttp.response;
} }
}) })
.done(); });
} this.init = Zotero.lazy(this.reinit);
/**
* Initializes styles cache, loading metadata for styles into memory
*/
this.init = function() {
_initialized = true;
var start = (new Date()).getTime()
_styles = {};
_visibleStyles = [];
_cacheTranslatorData = Zotero.Prefs.get("cacheTranslatorData");
this.lastCSL = null;
// main dir
var dir = Zotero.getStylesDirectory();
var i = _readStylesFromDirectory(dir, false);
// hidden dir
dir.append("hidden");
if(dir.exists()) i += _readStylesFromDirectory(dir, true);
Zotero.debug("Cached "+i+" styles in "+((new Date()).getTime() - start)+" ms");
}
/** /**
* Reads all styles from a given directory and caches their metadata * Reads all styles from a given directory and caches their metadata
* @private * @private
*/ */
function _readStylesFromDirectory(dir, hidden) { var _readStylesFromDirectory = Zotero.Promise.coroutine(function* (dir, hidden) {
var i = 0; var numCached = 0;
var contents = dir.directoryEntries;
while(contents.hasMoreElements()) { var iterator = new OS.File.DirectoryIterator(dir);
var file = contents.getNext().QueryInterface(Components.interfaces.nsIFile), try {
filename = file.leafName; while (true) {
if(!filename || filename[0] === "." let entries = yield iterator.nextBatch(10); // TODO: adjust as necessary
|| filename.substr(-4).toLowerCase() !== ".csl" if (!entries.length) break;
|| file.isDirectory()) continue;
for (let i = 0; i < entries.length; i++) {
try { let entry = entries[i];
var style = new Zotero.Style(file); let path = entry.path;
} let fileName = entry.name;
catch (e) { if (!fileName || fileName[0] === "."
Zotero.log( || fileName.substr(-4).toLowerCase() !== ".csl"
"Error loading style '" + file.leafName + "': " + e.message, || entry.isDir) continue;
"error",
file.path, try {
null, let code = yield Zotero.File.getContentsAsync(path);
e.lineNumber var style = new Zotero.Style(code, path);
); }
continue; catch (e) {
} Components.utils.reportError(e);
if(style.styleID) { Zotero.debug(e, 1);
if(_styles[style.styleID]) { continue;
// same style is already cached }
Zotero.log('Style with ID '+style.styleID+' already loaded from "'+ if(style.styleID) {
_styles[style.styleID].file.leafName+'"', "error", // same style is already cached
Zotero.Styles.ios.newFileURI(style.file).spec); if (_styles[style.styleID]) {
} else { Components.utils.reportError('Style with ID ' + style.styleID
// add to cache + ' already loaded from ' + _styles[style.styleID].fileName);
_styles[style.styleID] = style; } else {
_styles[style.styleID].hidden = hidden; // add to cache
if(!hidden) _visibleStyles.push(style); _styles[style.styleID] = style;
_styles[style.styleID].hidden = hidden;
if(!hidden) _visibleStyles.push(style);
}
}
numCached++;
} }
} }
i++;
} }
return i; finally {
} iterator.close();
}
return numCached;
});
/** /**
* Gets a style with a given ID * Gets a style with a given ID
* @param {String} id * @param {String} id
* @param {Boolean} skipMappings Don't automatically return renamed style * @param {Boolean} skipMappings Don't automatically return renamed style
*/ */
this.get = function(id, skipMappings) { this.get = function (id, skipMappings) {
if(!_initialized) this.init(); if (!_initialized) {
throw new Zotero.Exception.UnloadedDataException("Styles not yet loaded", 'styles');
// TODO: With asynchronous style loading, move renamedStyles call back here }
if(!skipMappings) { if(!skipMappings) {
var prefix = "http://www.zotero.org/styles/"; var prefix = "http://www.zotero.org/styles/";
@ -156,20 +161,24 @@ Zotero.Styles = new function() {
/** /**
* Gets all visible styles * Gets all visible styles
* @return {Zotero.Style[]} An array of Zotero.Style objects * @return {Promise<Zotero.Style[]>} A promise for an array of Zotero.Style objects
*/ */
this.getVisible = function() { this.getVisible = function () {
if(!_initialized || !_cacheTranslatorData) this.init(); return this.init().then(function () {
return _visibleStyles.slice(0); return _visibleStyles.slice(0);
});
} }
/** /**
* Gets all styles * Gets all styles
* @return {Object} An object whose keys are style IDs, and whose values are Zotero.Style objects *
* @return {Promise<Object>} A promise for an object with style IDs for keys and
* Zotero.Style objects for values
*/ */
this.getAll = function() { this.getAll = function () {
if(!_initialized || !_cacheTranslatorData) this.init(); return this.init().then(function () {
return _styles; return _styles;
});
} }
/** /**
@ -200,8 +209,9 @@ Zotero.Styles = new function() {
* @param {String} origin The origin of the style, either a filename or URL, to be * @param {String} origin The origin of the style, either a filename or URL, to be
* displayed in dialogs referencing the style * displayed in dialogs referencing the style
*/ */
this.install = function(style, origin) { this.install = Zotero.Promise.coroutine(function* (style, origin) {
var styleInstalled; var styleInstalled;
if(style instanceof Components.interfaces.nsIFile) { if(style instanceof Components.interfaces.nsIFile) {
// handle nsIFiles // handle nsIFiles
var url = Services.io.newFileURI(style); var url = Services.io.newFileURI(style);
@ -224,7 +234,7 @@ Zotero.Styles = new function() {
origin, "styles.install.title", error)).present(); origin, "styles.install.title", error)).present();
} }
}).done(); }).done();
} });
/** /**
* Installs a style * Installs a style
@ -234,176 +244,183 @@ Zotero.Styles = new function() {
* @param {Boolean} [hidden] Whether style is to be hidden. * @param {Boolean} [hidden] Whether style is to be hidden.
* @return {Promise} * @return {Promise}
*/ */
function _install(style, origin, hidden) { var _install = Zotero.Promise.coroutine(function* (style, origin, hidden) {
if(!_initialized || !_cacheTranslatorData) Zotero.Styles.init(); if (!_initialized) yield Zotero.Styles.init();
var existingFile, destFile, source, styleID var existingFile, destFile, source, styleID
return Zotero.Promise.try(function() {
// First, parse style and make sure it's valid XML // First, parse style and make sure it's valid XML
var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
.createInstance(Components.interfaces.nsIDOMParser), .createInstance(Components.interfaces.nsIDOMParser),
doc = parser.parseFromString(style, "application/xml"); doc = parser.parseFromString(style, "application/xml");
styleID = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:id[1]', styleID = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:id[1]',
Zotero.Styles.ns), Zotero.Styles.ns),
// Get file name from URL // Get file name from URL
m = /[^\/]+$/.exec(styleID), m = /[^\/]+$/.exec(styleID),
fileName = Zotero.File.getValidFileName(m ? m[0] : styleID), fileName = Zotero.File.getValidFileName(m ? m[0] : styleID),
title = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:title[1]', title = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:title[1]',
Zotero.Styles.ns);
if(!styleID || !title) {
// If it's not valid XML, we'll return a promise that immediately resolves
// to an error
throw new Zotero.Exception.Alert("styles.installError", origin,
"styles.install.title", "Style is not valid XML, or the styleID or title is missing");
}
// look for a parent
source = Zotero.Utilities.xpathText(doc,
'/csl:style/csl:info[1]/csl:link[@rel="source" or @rel="independent-parent"][1]/@href',
Zotero.Styles.ns); Zotero.Styles.ns);
if(source == styleID) {
throw new Zotero.Exception.Alert("styles.installError", origin, if(!styleID || !title) {
"styles.install.title", "Style references itself as source"); // If it's not valid XML, we'll return a promise that immediately resolves
// to an error
throw new Zotero.Exception.Alert("styles.installError", origin,
"styles.install.title", "Style is not valid XML, or the styleID or title is missing");
}
// look for a parent
source = Zotero.Utilities.xpathText(doc,
'/csl:style/csl:info[1]/csl:link[@rel="source" or @rel="independent-parent"][1]/@href',
Zotero.Styles.ns);
if(source == styleID) {
throw new Zotero.Exception.Alert("styles.installError", origin,
"styles.install.title", "Style references itself as source");
}
// ensure csl extension
if(fileName.substr(-4).toLowerCase() != ".csl") fileName += ".csl";
destFile = Zotero.getStylesDirectory();
var destFileHidden = destFile.clone();
destFile.append(fileName);
destFileHidden.append("hidden");
if(hidden) Zotero.File.createDirectoryIfMissing(destFileHidden);
destFileHidden.append(fileName);
// look for an existing style with the same styleID or filename
var existingTitle;
if(_styles[styleID]) {
existingFile = _styles[styleID].file;
existingTitle = _styles[styleID].title;
} else {
if(destFile.exists()) {
existingFile = destFile;
} else if(destFileHidden.exists()) {
existingFile = destFileHidden;
} }
// ensure csl extension if(existingFile) {
if(fileName.substr(-4).toLowerCase() != ".csl") fileName += ".csl"; // find associated style
for each(var existingStyle in _styles) {
destFile = Zotero.getStylesDirectory(); if(destFile.equals(existingStyle.file)) {
var destFileHidden = destFile.clone();
destFile.append(fileName);
destFileHidden.append("hidden");
if(hidden) Zotero.File.createDirectoryIfMissing(destFileHidden);
destFileHidden.append(fileName);
// look for an existing style with the same styleID or filename
var existingTitle;
if(_styles[styleID]) {
existingFile = _styles[styleID].file;
existingTitle = _styles[styleID].title;
} else {
if(destFile.exists()) {
existingFile = destFile;
} else if(destFileHidden.exists()) {
existingFile = destFileHidden;
}
if(existingFile) {
// find associated style
for each(var existingStyle in _styles) {
if(destFile.equals(existingStyle.file)) {
existingTitle = existingStyle.title;
break;
}
}
}
}
// also look for an existing style with the same title
if(!existingFile) {
for each(var existingStyle in Zotero.Styles.getAll()) {
if(title === existingStyle.title) {
existingFile = existingStyle.file;
existingTitle = existingStyle.title; existingTitle = existingStyle.title;
break; break;
} }
} }
} }
}
// display a dialog to tell the user we're about to install the style
if(hidden) {
destFile = destFileHidden;
} else {
if(existingTitle) {
var text = Zotero.getString('styles.updateStyle', [existingTitle, title, origin]);
} else {
var text = Zotero.getString('styles.installStyle', [title, origin]);
}
var index = Services.prompt.confirmEx(null, Zotero.getString('styles.install.title'),
text,
((Services.prompt.BUTTON_POS_0) * (Services.prompt.BUTTON_TITLE_IS_STRING)
+ (Services.prompt.BUTTON_POS_1) * (Services.prompt.BUTTON_TITLE_CANCEL)),
Zotero.getString('general.install'), null, null, null, {}
);
if(index !== 0) {
throw new Zotero.Exception.UserCancelled("style installation");
}
}
return Zotero.Styles.validate(style).catch(function(validationErrors) { // also look for an existing style with the same title
Zotero.logError("Style from "+origin+" failed to validate:\n\n"+validationErrors); if(!existingFile) {
let styles = yield Zotero.Styles.getAll();
// If validation fails on the parent of a dependent style, ignore it (for now) for (let i in styles) {
if(hidden) return; let existingStyle = styles[i];
if(title === existingStyle.title) {
// If validation fails on a different style, we ask the user if s/he really existingFile = existingStyle.file;
// wants to install it existingTitle = existingStyle.title;
Components.utils.import("resource://gre/modules/Services.jsm"); break;
var shouldInstall = Services.prompt.confirmEx(null,
Zotero.getString('styles.install.title'),
Zotero.getString('styles.validationWarning', origin),
(Services.prompt.BUTTON_POS_0) * (Services.prompt.BUTTON_TITLE_OK)
+ (Services.prompt.BUTTON_POS_1) * (Services.prompt.BUTTON_TITLE_CANCEL)
+ Services.prompt.BUTTON_POS_1_DEFAULT + Services.prompt.BUTTON_DELAY_ENABLE,
null, null, null, null, {}
);
if(shouldInstall !== 0) {
throw new Zotero.Exception.UserCancelled("style installation");
}
});
}).then(function() {
// User wants to install/update
if(source && !_styles[source]) {
// Need to fetch source
if(source.substr(0, 7) === "http://" || source.substr(0, 8) === "https://") {
return Zotero.HTTP.promise("GET", source).then(function(xmlhttp) {
return _install(xmlhttp.responseText, origin, true);
}).catch(function(error) {
if(typeof error === "object" && error instanceof Zotero.Exception.Alert) {
throw new Zotero.Exception.Alert("styles.installSourceError", [origin, source],
"styles.install.title", error);
} else {
throw error;
}
});
} else {
throw new Zotero.Exception.Alert("styles.installSourceError", [origin, source],
"styles.install.title", "Source CSL URI is invalid");
} }
} }
}).then(function() { }
// Dependent style has been retrieved if there was one, so we're ready to
// continue // display a dialog to tell the user we're about to install the style
if(hidden) {
destFile = destFileHidden;
} else {
if(existingTitle) {
var text = Zotero.getString('styles.updateStyle', [existingTitle, title, origin]);
} else {
var text = Zotero.getString('styles.installStyle', [title, origin]);
}
// Remove any existing file with a different name var index = Services.prompt.confirmEx(null, Zotero.getString('styles.install.title'),
if(existingFile) existingFile.remove(false); text,
((Services.prompt.BUTTON_POS_0) * (Services.prompt.BUTTON_TITLE_IS_STRING)
+ (Services.prompt.BUTTON_POS_1) * (Services.prompt.BUTTON_TITLE_CANCEL)),
Zotero.getString('general.install'), null, null, null, {}
);
return Zotero.File.putContentsAsync(destFile, style); if(index !== 0) {
}).then(function() { throw new Zotero.Exception.UserCancelled("style installation");
// Cache }
Zotero.Styles.init(); }
yield Zotero.Styles.validate(style)
.catch(function(validationErrors) {
Zotero.logError("Style from " + origin + " failed to validate:\n\n" + validationErrors);
// Refresh preferences windows // If validation fails on the parent of a dependent style, ignore it (for now)
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]. if(hidden) return;
getService(Components.interfaces.nsIWindowMediator);
var enumerator = wm.getEnumerator("zotero:pref"); // If validation fails on a different style, we ask the user if s/he really
while(enumerator.hasMoreElements()) { // wants to install it
var win = enumerator.getNext(); Components.utils.import("resource://gre/modules/Services.jsm");
if(win.Zotero_Preferences.Cite) { var shouldInstall = Services.prompt.confirmEx(null,
win.Zotero_Preferences.Cite.refreshStylesList(styleID); Zotero.getString('styles.install.title'),
} Zotero.getString('styles.validationWarning', origin),
(Services.prompt.BUTTON_POS_0) * (Services.prompt.BUTTON_TITLE_OK)
+ (Services.prompt.BUTTON_POS_1) * (Services.prompt.BUTTON_TITLE_CANCEL)
+ Services.prompt.BUTTON_POS_1_DEFAULT + Services.prompt.BUTTON_DELAY_ENABLE,
null, null, null, null, {}
);
if(shouldInstall !== 0) {
throw new Zotero.Exception.UserCancelled("style installation");
} }
}); });
}
// User wants to install/update
if(source && !_styles[source]) {
// Need to fetch source
if(source.substr(0, 7) === "http://" || source.substr(0, 8) === "https://") {
try {
let xmlhttp = yield Zotero.HTTP.request("GET", source);
yield _install(xmlhttp.responseText, origin, true);
}
catch (e) {
if (typeof e === "object" && e instanceof Zotero.Exception.Alert) {
throw new Zotero.Exception.Alert(
"styles.installSourceError",
[origin, source],
"styles.install.title",
e
);
}
throw e;
}
} else {
throw new Zotero.Exception.Alert("styles.installSourceError", [origin, source],
"styles.install.title", "Source CSL URI is invalid");
}
}
// Dependent style has been retrieved if there was one, so we're ready to
// continue
// Remove any existing file with a different name
if(existingFile) existingFile.remove(false);
yield Zotero.File.putContentsAsync(destFile, style);
yield Zotero.Styles.reinit();
// Refresh preferences windows
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].
getService(Components.interfaces.nsIWindowMediator);
var enumerator = wm.getEnumerator("zotero:pref");
while(enumerator.hasMoreElements()) {
var win = enumerator.getNext();
if(win.Zotero_Preferences.Cite) {
yield win.Zotero_Preferences.Cite.refreshStylesList(styleID);
}
}
});
} }
/** /**
* @class Represents a style file and its metadata * @class Represents a style file and its metadata
* @property {nsIFile} file The path to the style file * @property {String} path The path to the style file
* @property {String} fileName The name of the style file
* @property {String} styleID * @property {String} styleID
* @property {String} url The URL where the style can be found (rel="self") * @property {String} url The URL where the style can be found (rel="self")
* @property {String} type "csl" for CSL styles * @property {String} type "csl" for CSL styles
@ -416,25 +433,25 @@ Zotero.Styles = new function() {
* @property {Boolean} hidden True if this style is hidden in style selection dialogs, false if it * @property {Boolean} hidden True if this style is hidden in style selection dialogs, false if it
* is not * is not
*/ */
Zotero.Style = function(arg) { Zotero.Style = function (style, path) {
if(typeof arg === "string") { if (typeof style != "string") {
this.string = arg; throw new Error("Style code must be a string");
} else if(typeof arg === "object") {
this.file = arg;
} else {
throw "Invalid argument passed to Zotero.Style";
} }
this.type = "csl"; this.type = "csl";
var style = typeof arg === "string" ? arg : Zotero.File.getContents(arg), var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
.createInstance(Components.interfaces.nsIDOMParser), .createInstance(Components.interfaces.nsIDOMParser),
doc = parser.parseFromString(style, "application/xml"); doc = parser.parseFromString(style, "application/xml");
if(doc.documentElement.localName === "parsererror") { if(doc.documentElement.localName === "parsererror") {
throw new Error("File is not valid XML"); throw new Error("File is not valid XML");
} }
if (path) {
this.path = path;
this.fileName = OS.Path.basename(path);
}
this.styleID = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:id[1]', this.styleID = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:id[1]',
Zotero.Styles.ns); Zotero.Styles.ns);
this.url = Zotero.Utilities.xpathText(doc, this.url = Zotero.Utilities.xpathText(doc,
@ -494,8 +511,10 @@ Zotero.Style.prototype.getCiteProc = function(automaticJournalAbbreviations) {
if(this.source) { if(this.source) {
var parentStyle = Zotero.Styles.get(this.source); var parentStyle = Zotero.Styles.get(this.source);
if(!parentStyle) { if(!parentStyle) {
throw(new Error('Style references '+this.source+', but this style is not installed', throw new Error(
Zotero.Styles.ios.newFileURI(this.file).spec, null)); 'Style references ' + this.source + ', but this style is not installed',
Zotero.Utilities.pathToFileURI(this.path)
);
} }
var version = parentStyle._version; var version = parentStyle._version;
@ -552,11 +571,6 @@ Zotero.Style.prototype.getCiteProc = function(automaticJournalAbbreviations) {
} }
}; };
Zotero.Style.prototype.__defineGetter__("csl", function() {
Zotero.logError("Zotero.Style.csl is deprecated. Use Zotero.Style.getCiteProc()");
return this.getCiteProc();
});
Zotero.Style.prototype.__defineGetter__("class", Zotero.Style.prototype.__defineGetter__("class",
/** /**
* Retrieves the style class, either from the metadata that's already loaded or by loading the file * Retrieves the style class, either from the metadata that's already loaded or by loading the file
@ -578,8 +592,7 @@ function() {
// use hasBibliography from source style // use hasBibliography from source style
var parentStyle = Zotero.Styles.get(this.source); var parentStyle = Zotero.Styles.get(this.source);
if(!parentStyle) { if(!parentStyle) {
throw(new Error('Style references '+this.source+', but this style is not installed', throw new Error('Style references missing parent ' + this.source);
Zotero.Styles.ios.newFileURI(this.file).spec, null));
} }
return parentStyle.hasBibliography; return parentStyle.hasBibliography;
} }
@ -610,12 +623,11 @@ function() {
// parent/child // parent/child
var formatCSL = Zotero.Styles.get(this.source); var formatCSL = Zotero.Styles.get(this.source);
if(!formatCSL) { if(!formatCSL) {
throw(new Error('Style references '+this.source+', but this style is not installed', throw new Error('Style references missing parent ' + this.source);
Zotero.Styles.ios.newFileURI(this.file).spec, null));
} }
return formatCSL.file; return formatCSL.path;
} else if(this.file) { } else if (this.path) {
return this.file; return this.path;
} }
return null; return null;
}); });
@ -633,15 +645,16 @@ Zotero.Style.prototype.getXML = function() {
/** /**
* Deletes a style * Deletes a style
*/ */
Zotero.Style.prototype.remove = function() { Zotero.Style.prototype.remove = Zotero.Promise.coroutine(function* () {
if(!this.file) { if (!this.path) {
throw "Cannot delete a style with no associated file." throw new Error("Cannot delete a style with no associated file")
} }
// make sure no styles depend on this one // make sure no styles depend on this one
var dependentStyles = false; var dependentStyles = false;
var styles = Zotero.Styles.getAll(); var styles = yield Zotero.Styles.getAll();
for each(var style in styles) { for (let i in styles) {
let style = styles[i];
if(style.source == this.styleID) { if(style.source == this.styleID) {
dependentStyles = true; dependentStyles = true;
break; break;
@ -650,13 +663,12 @@ Zotero.Style.prototype.remove = function() {
if(dependentStyles) { if(dependentStyles) {
// copy dependent styles to hidden directory // copy dependent styles to hidden directory
var hiddenDir = Zotero.getStylesDirectory(); let hiddenDir = OS.Path.join(Zotero.getStylesDirectory().path, 'hidden');
hiddenDir.append("hidden"); yield Zotero.File.createDirectoryIfMissingAsync(hiddenDir);
Zotero.File.createDirectoryIfMissing(hiddenDir); yield OS.File.move(this.path, OS.Path.join(hiddenDir, OS.Path.basename(this.path)));
this.file.moveTo(hiddenDir, null);
} else { } else {
// remove defunct files // remove defunct files
this.file.remove(false); yield OS.File.remove(this.path);
} }
// check to see if this style depended on a hidden one // check to see if this style depended on a hidden one
@ -666,7 +678,9 @@ Zotero.Style.prototype.remove = function() {
var deleteSource = true; var deleteSource = true;
// check to see if any other styles depend on the hidden one // check to see if any other styles depend on the hidden one
for each(var style in Zotero.Styles.getAll()) { let styles = yield Zotero.Styles.getAll();
for (let i in styles) {
let style = styles[i];
if(style.source == this.source && style.styleID != this.styleID) { if(style.source == this.source && style.styleID != this.styleID) {
deleteSource = false; deleteSource = false;
break; break;
@ -675,10 +689,10 @@ Zotero.Style.prototype.remove = function() {
// if it was only this style with the dependency, delete the source // if it was only this style with the dependency, delete the source
if(deleteSource) { if(deleteSource) {
source.remove(); yield source.remove();
} }
} }
} }
Zotero.Styles.init(); return Zotero.Styles.reinit();
} });

View file

@ -61,7 +61,8 @@ var TRANSLATOR_SAVE_PROPERTIES = TRANSLATOR_REQUIRED_PROPERTIES.concat(["browser
* @property {String} lastUpdated SQL-style date and time of translator's last update * @property {String} lastUpdated SQL-style date and time of translator's last update
* @property {String} code The executable JavaScript for the translator * @property {String} code The executable JavaScript for the translator
* @property {Boolean} cacheCode Whether to cache code for this session (non-connector only) * @property {Boolean} cacheCode Whether to cache code for this session (non-connector only)
* @property {nsIFile} [file] File corresponding to this translator (non-connector only) * @property {String} [path] File path corresponding to this translator (non-connector only)
* @property {String} [fileName] File name corresponding to this translator (non-connector only)
*/ */
Zotero.Translator = function(info) { Zotero.Translator = function(info) {
this.init(info); this.init(info);
@ -119,7 +120,10 @@ Zotero.Translator.prototype.init = function(info) {
delete this.webRegexp; delete this.webRegexp;
} }
if(info.file) this.file = info.file; if (info.path) {
this.path = info.path;
this.fileName = OS.Path.basename(info.path);
}
if(info.code && this.cacheCode) { if(info.code && this.cacheCode) {
this.code = info.code; this.code = info.code;
} else if(this.hasOwnProperty("code")) { } else if(this.hasOwnProperty("code")) {
@ -148,7 +152,7 @@ Zotero.Translator.prototype.getCode = function() {
return code; return code;
}); });
} else { } else {
var promise = Zotero.File.getContentsAsync(this.file); var promise = Zotero.File.getContentsAsync(this.path);
if(this.cacheCode) { if(this.cacheCode) {
// Cache target-less web translators for session, since we // Cache target-less web translators for session, since we
// will use them a lot // will use them a lot
@ -168,7 +172,7 @@ Zotero.Translator.prototype.serialize = function(properties) {
var info = {}; var info = {};
for(var i in properties) { for(var i in properties) {
var property = properties[i]; var property = properties[i];
info[property] = translator[property]; info[property] = this[property];
} }
return info; return info;
} }
@ -182,10 +186,12 @@ Zotero.Translator.prototype.serialize = function(properties) {
* @param {Integer} colNumber * @param {Integer} colNumber
*/ */
Zotero.Translator.prototype.logError = function(message, type, line, lineNumber, colNumber) { Zotero.Translator.prototype.logError = function(message, type, line, lineNumber, colNumber) {
if(Zotero.isFx && this.file) { if (Zotero.isFx && this.path) {
Components.utils.import("resource://gre/modules/FileUtils.jsm");
var file = new FileUtils.File(this.path);
var ios = Components.classes["@mozilla.org/network/io-service;1"]. var ios = Components.classes["@mozilla.org/network/io-service;1"].
getService(Components.interfaces.nsIIOService); getService(Components.interfaces.nsIIOService);
Zotero.log(message, type ? type : "error", ios.newFileURI(this.file).spec); Zotero.log(message, type ? type : "error", ios.newFileURI(file).spec);
} else { } else {
Zotero.logError(message); Zotero.logError(message);
} }

View file

@ -35,85 +35,113 @@ Zotero.Translators = new function() {
var _initialized = false; var _initialized = false;
/** /**
* Initializes translator cache, loading all relevant translators into memory * Initializes translator cache, loading all translator metadata into memory
*/ */
this.reinit = Zotero.Promise.coroutine(function* () { this.reinit = Zotero.Promise.coroutine(function* () {
var start = (new Date()).getTime(); if (_initialized) {
var transactionStarted = false; Zotero.debug("Translators already initialized", 2);
return;
}
Zotero.debug("Initializing translators");
var start = new Date;
_initialized = true;
_cache = {"import":[], "export":[], "web":[], "search":[]}; _cache = {"import":[], "export":[], "web":[], "search":[]};
_translators = {}; _translators = {};
var dbCacheResults = yield Zotero.DB.queryAsync("SELECT leafName, translatorJSON, "+ var sql = "SELECT fileName, metadataJSON, lastModifiedTime FROM translatorCache";
"code, lastModifiedTime FROM translatorCache"); var dbCacheResults = yield Zotero.DB.queryAsync(sql);
var dbCache = {}; var dbCache = {};
for each(var cacheEntry in dbCacheResults) { for (let i = 0; i < dbCacheResults.length; i++) {
dbCache[cacheEntry.leafName] = cacheEntry; let entry = dbCacheResults[i];
dbCache[entry.fileName] = entry;
} }
var i = 0; var numCached = 0;
var filesInCache = {}; var filesInCache = {};
var contents = Zotero.getTranslatorsDirectory().directoryEntries; var translatorsDir = Zotero.getTranslatorsDirectory().path;
while(contents.hasMoreElements()) { var iterator = new OS.File.DirectoryIterator(translatorsDir);
var file = contents.getNext().QueryInterface(Components.interfaces.nsIFile); try {
var leafName = file.leafName; while (true) {
if(!(/^[^.].*\.js$/.test(leafName))) continue; let entries = yield iterator.nextBatch(5); // TODO: adjust as necessary
var lastModifiedTime = file.lastModifiedTime; if (!entries.length) break;
for (let i = 0; i < entries.length; i++) {
var dbCacheEntry = false; let entry = entries[i];
if(dbCache[leafName]) { let path = entry.path;
filesInCache[leafName] = true; let fileName = entry.name;
if(dbCache[leafName].lastModifiedTime == lastModifiedTime) {
dbCacheEntry = dbCache[file.leafName]; if (!(/^[^.].*\.js$/.test(fileName))) continue;
}
} let lastModifiedTime;
if ('winLastWriteDate' in entry) {
if(dbCacheEntry) { lastModifiedTime = entry.winLastWriteDate.getTime();
// get JSON from cache if possible }
var translator = Zotero.Translators.load(file, dbCacheEntry.translatorJSON, dbCacheEntry.code); else {
filesInCache[leafName] = true; lastModifiedTime = (yield OS.File.stat(path)).lastModificationDate.getTime();
} else { }
// otherwise, load from file let lastModified
var translator = yield Zotero.Translators.loadFromDisk(file);
} var dbCacheEntry = false;
if (dbCache[fileName]) {
if(translator.translatorID) { filesInCache[fileName] = true;
if(_translators[translator.translatorID]) { if (dbCache[fileName].lastModifiedTime == lastModifiedTime) {
// same translator is already cached dbCacheEntry = dbCache[fileName];
translator.logError('Translator with ID '+
translator.translatorID+' already loaded from "'+
_translators[translator.translatorID].file.leafName+'"');
} else {
// add to cache
_translators[translator.translatorID] = translator;
for(var type in TRANSLATOR_TYPES) {
if(translator.translatorType & TRANSLATOR_TYPES[type]) {
_cache[type].push(translator);
} }
} }
if(!dbCacheEntry) { if(dbCacheEntry) {
var code = yield translator.getCode(); // get JSON from cache if possible
yield Zotero.Translators.cacheInDB( var translator = Zotero.Translators.load(dbCacheEntry.metadataJSON, path);
leafName, filesInCache[fileName] = true;
translator.serialize(TRANSLATOR_REQUIRED_PROPERTIES. } else {
concat(TRANSLATOR_OPTIONAL_PROPERTIES)), // otherwise, load from file
translator.cacheCode ? translator.code : null, var translator = yield Zotero.Translators.loadFromFile(path);
lastModifiedTime
);
delete translator.metadataString;
} }
// When can this happen?
if (!translator.translatorID) {
Zotero.debug("Translator ID for " + path + " not found");
continue;
}
if (_translators[translator.translatorID]) {
// same translator is already cached
translator.logError('Translator with ID '+
translator.translatorID+' already loaded from "'+
_translators[translator.translatorID].fileName + '"');
} else {
// add to cache
_translators[translator.translatorID] = translator;
for(var type in TRANSLATOR_TYPES) {
if(translator.translatorType & TRANSLATOR_TYPES[type]) {
_cache[type].push(translator);
}
}
if (!dbCacheEntry) {
yield Zotero.Translators.cacheInDB(
fileName,
translator.serialize(TRANSLATOR_REQUIRED_PROPERTIES.
concat(TRANSLATOR_OPTIONAL_PROPERTIES)),
lastModifiedTime
);
}
}
numCached++;
} }
} }
}
i++; finally {
iterator.close();
} }
// Remove translators from DB as necessary // Remove translators from DB as necessary
for(var leafName in dbCache) { for (let fileName in dbCache) {
if(!filesInCache[leafName]) { if (!filesInCache[fileName]) {
yield Zotero.DB.queryAsync( yield Zotero.DB.queryAsync(
"DELETE FROM translatorCache WHERE leafName = ?", [leafName] "DELETE FROM translatorCache WHERE fileName = ?", fileName
); );
} }
} }
@ -133,56 +161,55 @@ Zotero.Translators = new function() {
_cache[type].sort(cmp); _cache[type].sort(cmp);
} }
Zotero.debug("Cached "+i+" translators in "+((new Date()).getTime() - start)+" ms"); Zotero.debug("Cached " + numCached + " translators in " + ((new Date) - start) + " ms");
}); });
this.init = Zotero.lazy(this.reinit); this.init = Zotero.lazy(this.reinit);
/** /**
* Loads a translator from JSON, with optional code * Loads a translator from JSON, with optional code
*/ */
this.load = function(file, json, code) { this.load = function (json, path, code) {
var info = JSON.parse(json); var info = JSON.parse(json);
info.file = file; info.path = path;
info.code = code; info.code = code;
return new Zotero.Translator(info); return new Zotero.Translator(info);
} }
/** /**
* Loads a translator from the disk * Loads a translator from the disk
*
* @param {String} file - Path to translator file
*/ */
this.loadFromDisk = function(file) { this.loadFromFile = function(path) {
const infoRe = /^\s*{[\S\s]*?}\s*?[\r\n]/; const infoRe = /^\s*{[\S\s]*?}\s*?[\r\n]/;
return Zotero.File.getContentsAsync(file) return Zotero.File.getContentsAsync(path)
.then(function(source) { .then(function(source) {
return Zotero.Translators.load(file, infoRe.exec(source)[0], source); return Zotero.Translators.load(infoRe.exec(source)[0], path, source);
}) })
.catch(function() { .catch(function() {
throw "Invalid or missing translator metadata JSON object in " + file.leafName; throw "Invalid or missing translator metadata JSON object in " + OS.Path.basename(path);
}); });
} }
/** /**
* Gets the translator that corresponds to a given ID * Gets the translator that corresponds to a given ID
*
* @param {String} id The ID of the translator * @param {String} id The ID of the translator
* @param {Function} [callback] An optional callback to be executed when translators have been
* retrieved. If no callback is specified, translators are
* returned.
*/ */
this.get = function(id) { this.get = function(id) {
return this.init().then(function() { if (!_initialized) {
return _translators[id] ? _translators[id] : false throw new Zotero.Exception.UnloadedDataException("Translators not yet loaded", 'translators');
}); }
return _translators[id] ? _translators[id] : false
} }
/** /**
* Gets all translators for a specific type of translation * Gets all translators for a specific type of translation
*
* @param {String} type The type of translators to get (import, export, web, or search) * @param {String} type The type of translators to get (import, export, web, or search)
* @param {Function} [callback] An optional callback to be executed when translators have been
* retrieved. If no callback is specified, translators are
* returned.
*/ */
this.getAllForType = function(type) { this.getAllForType = function(type) {
return this.init().then(function() { return this.init().then(function () {
return _cache[type].slice(); return _cache[type].slice();
}); });
} }
@ -191,21 +218,16 @@ Zotero.Translators = new function() {
* Gets all translators for a specific type of translation * Gets all translators for a specific type of translation
*/ */
this.getAll = function() { this.getAll = function() {
return this.init().then(function() { return this.init().then(function () {
return [translator for each(translator in _translators)]; return Object.keys(_translators);
}); });
} }
/** /**
* Gets web translators for a specific location * Gets web translators for a specific location
* @param {String} uri The URI for which to look for translators * @param {String} uri The URI for which to look for translators
* @param {Function} [callback] An optional callback to be executed when translators have been
* retrieved. If no callback is specified, translators are
* returned. The callback is passed a set of functions for
* converting URLs from proper to proxied forms as the second
* argument.
*/ */
this.getWebTranslatorsForLocation = function(uri, callback) { this.getWebTranslatorsForLocation = function(uri) {
return this.getAllForType("web").then(function(allTranslators) { return this.getAllForType("web").then(function(allTranslators) {
var potentialTranslators = []; var potentialTranslators = [];
@ -415,10 +437,10 @@ Zotero.Translators = new function() {
}); });
} }
this.cacheInDB = function(fileName, metadataJSON, code, lastModifiedTime) { this.cacheInDB = function(fileName, metadataJSON, lastModifiedTime) {
return Zotero.DB.queryAsync( return Zotero.DB.queryAsync(
"REPLACE INTO translatorCache VALUES (?, ?, ?, ?)", "REPLACE INTO translatorCache VALUES (?, ?, ?)",
[fileName, metadataJSON, code, lastModifiedTime] [fileName, JSON.stringify(metadataJSON), lastModifiedTime]
); );
} }
} }

View file

@ -485,7 +485,7 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
// Recreate database with no quick start guide // Recreate database with no quick start guide
Zotero.Schema.skipDefaultData = true; Zotero.Schema.skipDefaultData = true;
Zotero.Schema.updateSchema(); yield Zotero.Schema.updateSchema();
Zotero.restoreFromServer = true; Zotero.restoreFromServer = true;
} }
@ -577,7 +577,6 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
Zotero.locked = false; Zotero.locked = false;
// Initialize various services // Initialize various services
Zotero.Styles.preinit();
Zotero.Integration.init(); Zotero.Integration.init();
if(Zotero.Prefs.get("httpServer.enabled")) { if(Zotero.Prefs.get("httpServer.enabled")) {
@ -605,8 +604,8 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
Zotero.Searches.init(); Zotero.Searches.init();
Zotero.Groups.init(); Zotero.Groups.init();
// TODO: Delay until after UI is shown
yield Zotero.QuickCopy.init(); yield Zotero.QuickCopy.init();
Zotero.Items.startEmptyTrashTimer(); Zotero.Items.startEmptyTrashTimer();
} }
catch (e) { catch (e) {

View file

@ -411,8 +411,7 @@ CREATE INDEX customBaseFieldMappings_baseFieldID ON customBaseFieldMappings(base
CREATE INDEX customBaseFieldMappings_customFieldID ON customBaseFieldMappings(customFieldID); CREATE INDEX customBaseFieldMappings_customFieldID ON customBaseFieldMappings(customFieldID);
CREATE TABLE translatorCache ( CREATE TABLE translatorCache (
leafName TEXT PRIMARY KEY, fileName TEXT PRIMARY KEY,
translatorJSON TEXT, metadataJSON TEXT,
code TEXT, lastModifiedTime INT
lastModifiedTime INT
); );