commit da45df06cc6bc0e0afb99c3619922f7ab8e91626
parent 5d3e7f555c23feb16aceb36619ce00bdc2e5001e
Author: Dan Stillman <dstillman@zotero.org>
Date: Fri, 18 Mar 2016 04:04:33 -0400
Load reverse relations mappings at startup
This allows Zotero.Relations.getByPredicateAndObject()/getByObject() and
Zotero.Item::getLinkedItem()/Zotero.Collection::getLinkedCollection() to
be synchronous, which is necessary for word processor integration.
Diffstat:
12 files changed, 187 insertions(+), 92 deletions(-)
diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js
@@ -1517,7 +1517,7 @@ Zotero.CollectionTreeView.prototype.canDropCheckAsync = Zotero.Promise.coroutine
// Cross-library drag
if (treeRow.ref.libraryID != item.libraryID) {
- let linkedItem = yield item.getLinkedItem(treeRow.ref.libraryID, true);
+ let linkedItem = item.getLinkedItem(treeRow.ref.libraryID, true);
if (linkedItem && !linkedItem.deleted) {
// For drag to root, skip if linked item exists
if (treeRow.isLibrary(true)) {
@@ -1623,7 +1623,7 @@ Zotero.CollectionTreeView.prototype.drop = Zotero.Promise.coroutine(function* (r
var targetLibraryType = Zotero.Libraries.get(targetLibraryID).libraryType;
// Check if there's already a copy of this item in the library
- var linkedItem = yield item.getLinkedItem(targetLibraryID, true);
+ var linkedItem = item.getLinkedItem(targetLibraryID, true);
if (linkedItem) {
// If linked item is in the trash, undelete it and remove it from collections
// (since it shouldn't be restored to previous collections)
diff --git a/chrome/content/zotero/xpcom/data/dataObject.js b/chrome/content/zotero/xpcom/data/dataObject.js
@@ -446,9 +446,9 @@ Zotero.DataObject.prototype.setRelations = function (newRelations) {
* calling this directly.
*
* @param {Integer} [libraryID]
- * @return {Promise<Zotero.DataObject>|false} Linked object, or false if not found
+ * @return {Zotero.DataObject|false} Linked object, or false if not found
*/
-Zotero.DataObject.prototype._getLinkedObject = Zotero.Promise.coroutine(function* (libraryID, bidirectional) {
+Zotero.DataObject.prototype._getLinkedObject = function (libraryID, bidirectional) {
if (!libraryID) {
throw new Error("libraryID not provided");
}
@@ -466,7 +466,7 @@ Zotero.DataObject.prototype._getLinkedObject = Zotero.Promise.coroutine(function
for (let i = 0; i < uris.length; i++) {
let uri = uris[i];
if (uri.startsWith(libraryObjectPrefix)) {
- let obj = yield Zotero.URI['getURI' + this._ObjectType](uri);
+ let obj = Zotero.URI['getURI' + this._ObjectType](uri);
if (!obj) {
Zotero.debug("Referenced linked " + this._objectType + " '" + uri + "' not found "
+ "in Zotero." + this._ObjectType + "::getLinked" + this._ObjectType + "()", 2);
@@ -479,7 +479,7 @@ Zotero.DataObject.prototype._getLinkedObject = Zotero.Promise.coroutine(function
// Then try relations with this as an object
if (bidirectional) {
var thisURI = Zotero.URI['get' + this._ObjectType + 'URI'](this);
- var objects = yield Zotero.Relations.getByPredicateAndObject(
+ var objects = Zotero.Relations.getByPredicateAndObject(
this._objectType, predicate, thisURI
);
for (let i = 0; i < objects.length; i++) {
@@ -496,7 +496,7 @@ Zotero.DataObject.prototype._getLinkedObject = Zotero.Promise.coroutine(function
}
return false;
-});
+};
/**
@@ -830,14 +830,14 @@ Zotero.DataObject.prototype.save = Zotero.Promise.coroutine(function* (options)
}
// Create transaction
+ let result
if (env.options.tx) {
- let result = yield Zotero.DB.executeTransaction(function* () {
+ result = yield Zotero.DB.executeTransaction(function* () {
Zotero.DataObject.prototype._saveData.call(this, env);
yield this._saveData(env);
yield Zotero.DataObject.prototype._finalizeSave.call(this, env);
return this._finalizeSave(env);
}.bind(this), env.transactionOptions);
- return result;
}
// Use existing transaction
else {
@@ -845,8 +845,10 @@ Zotero.DataObject.prototype.save = Zotero.Promise.coroutine(function* (options)
Zotero.DataObject.prototype._saveData.call(this, env);
yield this._saveData(env);
yield Zotero.DataObject.prototype._finalizeSave.call(this, env);
- return this._finalizeSave(env);
+ result = this._finalizeSave(env);
}
+ this._postSave(env);
+ return result;
}
catch(e) {
return this._recoverFromSaveError(env, e)
@@ -906,6 +908,9 @@ Zotero.DataObject.prototype._initSave = Zotero.Promise.coroutine(function* (env)
Zotero.DB.addCurrentCallback("rollback", func);
}
+ env.relationsToRegister = [];
+ env.relationsToUnregister = [];
+
return true;
});
@@ -967,6 +972,7 @@ Zotero.DataObject.prototype._finalizeSave = Zotero.Promise.coroutine(function* (
// Convert predicates to ids
for (let i = 0; i < toAdd.length; i++) {
toAdd[i][0] = yield Zotero.RelationPredicates.add(toAdd[i][0]);
+ env.relationsToRegister.push([toAdd[i][0], toAdd[i][1]]);
}
yield Zotero.DB.queryAsync(
sql + toAdd.map(x => "(?, ?, ?)").join(", "),
@@ -987,13 +993,15 @@ Zotero.DataObject.prototype._finalizeSave = Zotero.Promise.coroutine(function* (
toRemove[i][1]
]
);
+ env.relationsToUnregister.push([toRemove[i][0], toRemove[i][1]]);
}
}
}
if (env.isNew) {
if (!env.skipCache) {
- // Register this object's identifiers in Zotero.DataObjects
+ // Register this object's identifiers in Zotero.DataObjects. This has to happen here so
+ // that the object exists for the reload() in objects' finalizeSave methods.
this.ObjectsClass.registerObject(this);
}
// If object isn't being reloaded, disable it, since its data may be out of date
@@ -1006,6 +1014,23 @@ Zotero.DataObject.prototype._finalizeSave = Zotero.Promise.coroutine(function* (
}
});
+
+/**
+ * Actions to perform after DB transaction
+ */
+Zotero.DataObject.prototype._postSave = function (env) {
+ for (let i = 0; i < env.relationsToRegister.length; i++) {
+ let rel = env.relationsToRegister[i];
+ Zotero.debug(rel);
+ Zotero.Relations.register(this._objectType, this.id, rel[0], rel[1]);
+ }
+ for (let i = 0; i < env.relationsToUnregister.length; i++) {
+ let rel = env.relationsToUnregister[i];
+ Zotero.Relations.unregister(this._objectType, this.id, rel[0], rel[1]);
+ }
+};
+
+
Zotero.DataObject.prototype._recoverFromSaveError = Zotero.Promise.coroutine(function* (env) {
yield this.reload(null, true);
this._clearChanged();
diff --git a/chrome/content/zotero/xpcom/data/dataObjects.js b/chrome/content/zotero/xpcom/data/dataObjects.js
@@ -500,7 +500,7 @@ Zotero.DataObjects.prototype._loadRelations = Zotero.Promise.coroutine(function*
let objectURI = getURI(this);
// Related items are bidirectional, so include any pointing to this object
- let objects = yield Zotero.Relations.getByPredicateAndObject(
+ let objects = Zotero.Relations.getByPredicateAndObject(
Zotero.Relations.relatedItemPredicate, objectURI
);
for (let i = 0; i < objects.length; i++) {
@@ -508,7 +508,7 @@ Zotero.DataObjects.prototype._loadRelations = Zotero.Promise.coroutine(function*
}
// Also include any owl:sameAs relations pointing to this object
- objects = yield Zotero.Relations.getByPredicateAndObject(
+ objects = Zotero.Relations.getByPredicateAndObject(
Zotero.Relations.linkedObjectPredicate, objectURI
);
for (let i = 0; i < objects.length; i++) {
diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js
@@ -1492,7 +1492,7 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
// If undeleting, remove any merge-tracking relations
let predicate = Zotero.Relations.replacedItemPredicate;
let thisURI = Zotero.URI.getItemURI(this);
- let mergeItems = yield Zotero.Relations.getByPredicateAndObject(
+ let mergeItems = Zotero.Relations.getByPredicateAndObject(
'item', predicate, thisURI
);
for (let mergeItem of mergeItems) {
diff --git a/chrome/content/zotero/xpcom/data/relations.js b/chrome/content/zotero/xpcom/data/relations.js
@@ -36,6 +36,82 @@ Zotero.Relations = new function () {
};
var _types = ['collection', 'item'];
+ var _subjectsByPredicateIDAndObject = {};
+ var _subjectPredicatesByObject = {};
+
+
+ this.init = Zotero.Promise.coroutine(function* () {
+ // Load relations for different types
+ for (let type of _types) {
+ let t = new Date();
+ Zotero.debug(`Loading ${type} relations`);
+
+ let sql = "SELECT * FROM " + type + "Relations "
+ + "JOIN relationPredicates USING (predicateID)";
+ yield Zotero.DB.queryAsync(
+ sql,
+ false,
+ {
+ onRow: function (row) {
+ this.register(
+ type,
+ row.getResultByIndex(0),
+ row.getResultByIndex(1),
+ row.getResultByIndex(2)
+ );
+ }.bind(this)
+ }
+ );
+
+ Zotero.debug(`Loaded ${type} relations in ${new Date() - t} ms`);
+ }
+ });
+
+
+ this.register = function (objectType, subjectID, predicate, object) {
+ var predicateID = Zotero.RelationPredicates.getID(predicate);
+
+ if (!_subjectsByPredicateIDAndObject[objectType]) {
+ _subjectsByPredicateIDAndObject[objectType] = {};
+ }
+ if (!_subjectPredicatesByObject[objectType]) {
+ _subjectPredicatesByObject[objectType] = {};
+ }
+
+ // _subjectsByPredicateIDAndObject
+ var o = _subjectsByPredicateIDAndObject[objectType];
+ if (!o[predicateID]) {
+ o[predicateID] = {};
+ }
+ if (!o[predicateID][object]) {
+ o[predicateID][object] = new Set();
+ }
+ o[predicateID][object].add(subjectID);
+
+ // _subjectPredicatesByObject
+ o = _subjectPredicatesByObject[objectType];
+ if (!o[object]) {
+ o[object] = {};
+ }
+ if (!o[object][predicateID]) {
+ o[object][predicateID] = new Set();
+ }
+ o[object][predicateID].add(subjectID);
+ };
+
+
+ this.unregister = function (objectType, subjectID, predicate, object) {
+ var predicateID = Zotero.RelationPredicates.getID(predicate);
+
+ if (!_subjectsByPredicateIDAndObject[objectType]
+ || !_subjectsByPredicateIDAndObject[objectType][predicateID]
+ || !_subjectsByPredicateIDAndObject[objectType][predicateID][object]) {
+ return;
+ }
+
+ _subjectsByPredicateIDAndObject[objectType][predicateID][object].delete(subjectID)
+ _subjectPredicatesByObject[objectType][object][predicateID].delete(subjectID)
+ };
/**
@@ -44,18 +120,22 @@ Zotero.Relations = new function () {
* @param {String} objectType - Type of relation to search for (e.g., 'item')
* @param {String} predicate
* @param {String} object
- * @return {Promise<Zotero.DataObject[]>}
+ * @return {Zotero.DataObject[]}
*/
- this.getByPredicateAndObject = Zotero.Promise.coroutine(function* (objectType, predicate, object) {
+ this.getByPredicateAndObject = function (objectType, predicate, object) {
var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
if (predicate) {
predicate = this._getPrefixAndValue(predicate).join(':');
}
- var sql = "SELECT " + objectsClass.idColumn + " FROM " + objectType + "Relations "
- + "JOIN relationPredicates USING (predicateID) WHERE predicate=? AND object=?";
- var ids = yield Zotero.DB.columnQueryAsync(sql, [predicate, object]);
- return yield objectsClass.getAsync(ids, { noCache: true });
- });
+
+ var predicateID = Zotero.RelationPredicates.getID(predicate);
+
+ var o = _subjectsByPredicateIDAndObject[objectType];
+ if (!o || !o[predicateID] || !o[predicateID][object]) {
+ return [];
+ }
+ return objectsClass.get(Array.from(o[predicateID][object].values()));
+ };
/**
@@ -63,24 +143,25 @@ Zotero.Relations = new function () {
*
* @param {String} objectType - Type of relation to search for (e.g., 'item')
* @param {String} object
- * @return {Promise<Object[]>} - Promise for an object with a Zotero.DataObject as 'subject'
- * and a predicate string as 'predicate'
+ * @return {Object[]} - An array of objects with a Zotero.DataObject as 'subject'
+ * and a predicate string as 'predicate'
*/
- this.getByObject = Zotero.Promise.coroutine(function* (objectType, object) {
+ this.getByObject = function (objectType, object) {
var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
- var sql = "SELECT " + objectsClass.idColumn + " AS id, predicate "
- + "FROM " + objectType + "Relations JOIN relationPredicates USING (predicateID) "
- + "WHERE object=?";
+ var predicateIDs = [];
+ var o = _subjectPredicatesByObject[objectType][object];
+ if (!o) {
+ return [];
+ }
var toReturn = [];
- var rows = yield Zotero.DB.queryAsync(sql, object);
- for (let i = 0; i < rows.length; i++) {
- toReturn.push({
- subject: yield objectsClass.getAsync(rows[i].id, { noCache: true }),
- predicate: rows[i].predicate
- });
+ for (let predicateID in o) {
+ o[predicateID].forEach(subjectID => toReturn.push({
+ subject: objectsClass.get(subjectID),
+ predicate: Zotero.RelationPredicates.getName(predicateID)
+ }));
}
return toReturn;
- });
+ };
this.updateUser = Zotero.Promise.coroutine(function* (toUserID) {
@@ -93,14 +174,32 @@ Zotero.Relations = new function () {
}
Zotero.DB.requireTransaction();
for (let type of _types) {
- var sql = "UPDATE " + type + "Relations SET "
- + "object=REPLACE(object, 'zotero.org/users/" + fromUserID + "', "
- + "'zotero.org/users/" + toUserID + "')";
+ let sql = `SELECT DISTINCT object FROM ${type}Relations WHERE object LIKE ?`;
+ let objects = yield Zotero.DB.columnQueryAsync(
+ sql, 'http://zotero.org/users/' + fromUserID + '/%'
+ );
+ Zotero.DB.addCurrentCallback("commit", function () {
+ for (let object of objects) {
+ let subPrefs = this.getByObject(type, object);
+ let newObject = object.replace(
+ new RegExp("^http://zotero.org/users/" + fromUserID + "/(.*)"),
+ "http://zotero.org/users/" + toUserID + "/$1"
+ );
+ for (let subPref of subPrefs) {
+ this.unregister(type, subPref.subject.id, subPref.predicate, object);
+ this.register(type, subPref.subject.id, subPref.predicate, newObject);
+ }
+ }
+ }.bind(this));
+
+ sql = "UPDATE " + type + "Relations SET "
+ + "object=REPLACE(object, 'zotero.org/users/" + fromUserID + "/', "
+ + "'zotero.org/users/" + toUserID + "/')";
yield Zotero.DB.queryAsync(sql);
var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
- var objects = objectsClass.getLoaded();
- for (let object of objects) {
+ let loadedObjects = objectsClass.getLoaded();
+ for (let object of loadedObjects) {
yield object.reload(['relations'], true);
}
}
diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js
@@ -3139,8 +3139,6 @@ Zotero.Integration.URIMap.prototype.getZoteroItemForURIs = function(uris) {
// Next try getting URI directly
try {
- // TEMP
- throw new Error("getURIItem() is now async");
zoteroItem = Zotero.URI.getURIItem(uri);
if(zoteroItem) {
// Ignore items in the trash
@@ -3152,42 +3150,14 @@ Zotero.Integration.URIMap.prototype.getZoteroItemForURIs = function(uris) {
}
} catch(e) {}
- // Try merged item mappings
- var seen = [];
-
- // Follow merged item relations until we find an item or hit a dead end
- while (!zoteroItem) {
- var relations = Zotero.Relations.getByURIs(uri, Zotero.Relations.replacedItemPredicate);
- // No merged items found
- if(!relations.length) {
- break;
- }
-
- uri = relations[0].object;
-
- // Keep track of mapped URIs in case there's a circular relation
- if(seen.indexOf(uri) != -1) {
- var msg = "Circular relation for '" + uri + "' in merged item mapping resolution";
- Zotero.debug(msg, 2);
- Components.utils.reportError(msg);
- break;
- }
- seen.push(uri);
-
- try {
- // TEMP
- throw new Error("getURIItem() is now async");
- zoteroItem = Zotero.URI.getURIItem(uri);
- if(zoteroItem) {
- // Ignore items in the trash
- if(zoteroItem.deleted) {
- zoteroItem = false;
- } else {
- break;
- }
- }
- } catch(e) {}
+ // Try merged item mapping
+ var replacer = Zotero.Relations.getByPredicateAndObject(
+ 'item', Zotero.Relations.replacedItemPredicate, uri
+ );
+ if (replacer.length && !replacer[0].deleted) {
+ zoteroItem = replacer;
}
+
if(zoteroItem) break;
}
diff --git a/chrome/content/zotero/xpcom/report.js b/chrome/content/zotero/xpcom/report.js
@@ -143,7 +143,7 @@ Zotero.Report.HTML = new function () {
}
for (let i=0; i<rels.length; i++) {
let rel = rels[i];
- let relItem = yield Zotero.URI.getURIItem(rel);
+ let relItem = Zotero.URI.getURIItem(rel);
if (relItem) {
content += '\t\t\t\t\t<li id="item_' + relItem.key + '">';
content += escapeXML(relItem.getDisplayTitle());
diff --git a/chrome/content/zotero/xpcom/uri.js b/chrome/content/zotero/xpcom/uri.js
@@ -191,13 +191,13 @@ Zotero.URI = new function () {
* Convert an item URI into an item
*
* @param {String} itemURI
- * @return {Promise<Zotero.Item|FALSE>}
+ * @return {Zotero.Item|false}
*/
- this.getURIItem = Zotero.Promise.method(function (itemURI) {
+ this.getURIItem = function (itemURI) {
var obj = this._getURIObject(itemURI, 'item');
if (!obj) return false;
- return Zotero.Items.getByLibraryAndKeyAsync(obj.libraryID, obj.key);
- });
+ return Zotero.Items.getByLibraryAndKey(obj.libraryID, obj.key);
+ };
/**
@@ -225,13 +225,13 @@ Zotero.URI = new function () {
*
* @param {String} collectionURI
* @param {Zotero.Collection|FALSE}
- * @return {Promise<Zotero.Collection|FALSE>}
+ * @return {Zotero.Collection|false}
*/
- this.getURICollection = Zotero.Promise.method(function (collectionURI) {
+ this.getURICollection = function (collectionURI) {
var obj = this._getURIObject(collectionURI, 'collection');
if (!obj) return false;
- return Zotero.Collections.getByLibraryAndKeyAsync(obj.libraryID, obj.key);
- });
+ return Zotero.Collections.getByLibraryAndKey(obj.libraryID, obj.key);
+ };
/**
diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js
@@ -626,6 +626,7 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
yield Zotero.Searches.init();
yield Zotero.Creators.init();
yield Zotero.Groups.init();
+ yield Zotero.Relations.init()
let libraryIDs = Zotero.Libraries.getAll().map(x => x.libraryID);
for (let libraryID of libraryIDs) {
diff --git a/test/tests/collectionTreeViewTest.js b/test/tests/collectionTreeViewTest.js
@@ -424,7 +424,7 @@ describe("Zotero.CollectionTreeView", function() {
assert.equal(treeRow.ref.libraryID, group.libraryID);
assert.equal(treeRow.ref.id, ids[0]);
// New item should link back to original
- var linked = yield item.getLinkedItem(group.libraryID);
+ var linked = item.getLinkedItem(group.libraryID);
assert.equal(linked.id, treeRow.ref.id);
// Check attachment
@@ -434,7 +434,7 @@ describe("Zotero.CollectionTreeView", function() {
treeRow = itemsView.getRow(1);
assert.equal(treeRow.ref.id, ids[1]);
// New attachment should link back to original
- linked = yield attachment.getLinkedItem(group.libraryID);
+ linked = attachment.getLinkedItem(group.libraryID);
assert.equal(linked.id, treeRow.ref.id);
return group.eraseTx();
@@ -466,7 +466,7 @@ describe("Zotero.CollectionTreeView", function() {
var item = yield createDataObject('item', false, { skipSelect: true });
yield drop('item', 'L' + group.libraryID, [item.id]);
- var droppedItem = yield item.getLinkedItem(group.libraryID);
+ var droppedItem = item.getLinkedItem(group.libraryID);
droppedItem.setCollections([collection.id]);
droppedItem.deleted = true;
yield droppedItem.saveTx();
diff --git a/test/tests/dataObjectTest.js b/test/tests/dataObjectTest.js
@@ -411,7 +411,7 @@ describe("Zotero.DataObject", function() {
var item2URI = Zotero.URI.getItemURI(item2);
yield item2.addLinkedItem(item1);
- var linkedItem = yield item1.getLinkedItem(item2.libraryID);
+ var linkedItem = item1.getLinkedItem(item2.libraryID);
assert.equal(linkedItem.id, item2.id);
})
@@ -422,7 +422,7 @@ describe("Zotero.DataObject", function() {
var item2 = yield createDataObject('item', { libraryID: group.libraryID });
yield item2.addLinkedItem(item1);
- var linkedItem = yield item2.getLinkedItem(item1.libraryID);
+ var linkedItem = item2.getLinkedItem(item1.libraryID);
assert.isFalse(linkedItem);
})
@@ -433,7 +433,7 @@ describe("Zotero.DataObject", function() {
var item2 = yield createDataObject('item', { libraryID: group.libraryID });
yield item2.addLinkedItem(item1);
- var linkedItem = yield item2.getLinkedItem(item1.libraryID, true);
+ var linkedItem = item2.getLinkedItem(item1.libraryID, true);
assert.equal(linkedItem.id, item1.id);
})
})
diff --git a/test/tests/relationsTest.js b/test/tests/relationsTest.js
@@ -14,7 +14,7 @@ describe("Zotero.Relations", function () {
]
})
yield item.saveTx();
- var objects = yield Zotero.Relations.getByPredicateAndObject(
+ var objects = Zotero.Relations.getByPredicateAndObject(
'item', 'owl:sameAs', 'http://zotero.org/groups/1/items/SRRMGSRM'
);
assert.lengthOf(objects, 1);