diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js index 6c11d803b..bfeb9cda6 100644 --- 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 index c94b5ddf3..c1a8a55ef 100644 --- 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|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 index 6db8053e7..958392d4b 100644 --- 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 index 972186b40..81d8c3e1c 100644 --- 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 index 005b1fc15..ff51bf079 100644 --- 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} + * @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} - 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 index 84388c3e5..cf0485bce 100644 --- 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 index 63862d479..e6e279762 100644 --- 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'; content += escapeXML(relItem.getDisplayTitle()); diff --git a/chrome/content/zotero/xpcom/uri.js b/chrome/content/zotero/xpcom/uri.js index a0b7e784a..5cf8856b9 100644 --- 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} + * @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} + * @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 index e3b493657..fcdb5b4bc 100644 --- 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 index 25706b839..959b1f72a 100644 --- 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 index 13f178d6c..29251a6d2 100644 --- 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 index d5e7898aa..6c7aac032 100644 --- 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);