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.
This commit is contained in:
Dan Stillman 2016-03-18 04:04:33 -04:00
parent 5d3e7f555c
commit da45df06cc
12 changed files with 187 additions and 92 deletions

View file

@ -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)

View file

@ -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();

View file

@ -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++) {

View file

@ -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) {

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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());

View file

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

View file

@ -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) {

View file

@ -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();

View file

@ -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);
})
})

View file

@ -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);