zotero/chrome/content/zotero/xpcom/data/dataObjectUtilities.js
Dan Stillman e45aabd03c Special handling of objects class for SyncedSettings
(Though maybe it should just be renamed Zotero.Settings, since
non-synced settings aren't used much and are just handled via DB
queries.)
2015-06-17 01:57:42 -04:00

532 lines
12 KiB
JavaScript

/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2009 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
Zotero.DataObjectUtilities = {
"checkLibraryID": function (libraryID) {
if (!libraryID) {
throw new Error("libraryID not provided");
}
var intValue = parseInt(libraryID);
if (libraryID != intValue || intValue <= 0) {
throw new Error("libraryID must be a positive integer");
}
return intValue;
},
"checkDataID": function(dataID) {
var intValue = parseInt(dataID);
if (dataID != intValue || dataID <= 0)
throw new Error("id must be a positive integer");
return intValue;
},
generateKey: function () {
return Zotero.Utilities.generateObjectKey();
},
"checkKey": function(key) {
if (!key) return null;
if (!Zotero.Utilities.isValidObjectKey(key)) {
throw new Error("key is not valid");
}
return key;
},
getObjectTypeSingular: function (objectTypePlural) {
return objectTypePlural.replace(/(s|es)$/, '');
},
"getObjectTypePlural": function(objectType) {
return objectType == 'search' ? 'searches' : objectType + 's';
},
"getObjectsClassForObjectType": function(objectType) {
if (objectType == 'setting') objectType = 'syncedSetting';
var objectTypePlural = this.getObjectTypePlural(objectType);
var className = objectTypePlural[0].toUpperCase() + objectTypePlural.substr(1);
return Zotero[className]
},
/**
* Determine whether two API JSON objects are equivalent
*
* @param {Object} data1 - API JSON of first object
* @param {Object} data2 - API JSON of second object
* @param {Array} [ignoreFields] - Fields to ignore
* @param {Boolean} - True if objects are the same, false if not
*/
equals: function (data1, data2, ignoreFields) {
var skipFields = {};
for (let field of ['key', 'version'].concat(ignoreFields || [])) {
skipFields[field] = true;
}
for (let field in data1) {
if (skipFields[field]) {
continue;
}
let val1 = data1[field];
let val2 = data2[field];
let val1HasValue = val1 || val1 === 0;
let val2HasValue = val2 || val2 === 0;
if (!val1HasValue && !val2HasValue) {
continue;
}
let changed;
switch (field) {
case 'creators':
case 'collections':
case 'tags':
case 'relations':
changed = this["_" + field + "Equals"](val1, val2);
if (changed) {
return true;
}
break;
default:
changed = val1 !== val2;
if (changed) {
return true;
}
}
skipFields[field] = true;
}
for (let field in data2) {
// Skip ignored fields and fields we've already compared
if (skipFields[field]) {
continue;
}
// All remaining fields don't exist in data1
if (data2[field] === false) {
continue;
}
return true;
}
return false;
},
_creatorsEquals: function (data1, data2) {
if (!data2 || data1.length != data2.length) return true;
for (let i = 0; i < data1.length; i++) {
if (!Zotero.Creators.equals(data1[i], data2[i])) {
return false;
}
}
return true;
},
_collectionsEquals: function (data1, data2) {
if (!data2 || data1.length != data2.length) return true;
let c1 = data1.concat();
let c2 = data2.concat();
c1.sort();
c2.sort();
return Zotero.Utilities.arrayEquals(c1, c2);
},
_tagsEquals: function (data1, data2) {
if (!data2 || data1.length != data2.length) return true;
for (let i = 0; i < data1.length; i++) {
if (!Zotero.Tags.equals(data1[i], data2[i])) {
return false;
}
}
return true;
},
_relationsEquals: function (data1, data2) {
if (!data2) return true;
var pred1 = Object.keys(data1);
pred1.sort();
var pred2 = Object.keys(data2);
pred2.sort();
if (!Zotero.Utilities.arrayEquals(pred1, pred2)) return false;
for (let i in pred1) {
if (!Zotero.Utilities.arrayEquals(pred1[i], pred2[i])) {
return false;
}
}
return true;
},
/**
* Compare two API JSON objects and generate a changeset
*
* @param {Object} data1
* @param {Object} data2
* @param {String[]} [ignoreFields] - Fields to ignore
*/
diff: function (data1, data2, ignoreFields) {
var changeset = [];
var skipFields = {};
for (let field of ['key', 'version'].concat(ignoreFields || [])) {
skipFields[field] = true;
}
for (let field in data1) {
if (skipFields[field]) {
continue;
}
let val1 = data1[field];
let val2 = data2[field];
let val1HasValue = (val1 && val1 !== "") || val1 === 0;
let val2HasValue = (val2 && val2 !== "") || val2 === 0;
if (!val1HasValue && !val2HasValue) {
continue;
}
switch (field) {
case 'creators':
case 'collections':
case 'relations':
case 'tags':
let changes = this["_" + field + "Diff"](val1, val2);
if (changes.length) {
changeset = changeset.concat(changes);
}
break;
default:
var changed = val1 !== val2;
if (changed) {
if (val1HasValue && !val2HasValue) {
changeset.push({
field: field,
op: 'delete'
});
}
else if (!val1HasValue && val2HasValue) {
changeset.push({
field: field,
op: 'add',
value: val2
});
}
else {
changeset.push({
field: field,
op: 'modify',
value: val2
});
}
}
}
skipFields[field] = true;
}
for (let field in data2) {
// Skip ignored fields and fields we've already compared
if (skipFields[field]) {
continue;
}
// All remaining fields don't exist in data1
let val = data2[field];
if (val === false || val === ""
|| (typeof val == 'object' && Object.keys(val).length == 0)) {
continue;
}
changeset.push({
field: field,
op: "add",
value: data2[field]
});
}
return changeset;
},
/**
* For creators, just determine if changed, since ordering makes a full diff too complicated
*/
_creatorsDiff: function (data1, data2) {
if (!data2 || !data2.length) {
if (!data1.length) {
return [];
}
return [{
field: "creators",
op: "delete"
}];
}
if (!this._creatorsEquals(data1, data2)) {
return [{
field: "creators",
op: "modify",
value: data2
}];
}
return [];
},
_collectionsDiff: function (data1, data2) {
data2 = data2 || [];
var changeset = [];
var removed = Zotero.Utilities.arrayDiff(data1, data2);
for (let i = 0; i < removed.length; i++) {
changeset.push({
field: "collections",
op: "member-remove",
value: removed[i]
});
}
let added = Zotero.Utilities.arrayDiff(data2, data1);
for (let i = 0; i < added.length; i++) {
changeset.push({
field: "collections",
op: "member-add",
value: added[i]
});
}
return changeset;
},
_tagsDiff: function (data1, data2) {
data2 = data2 || [];
var changeset = [];
outer:
for (let i = 0; i < data1.length; i++) {
for (let j = 0; j < data2.length; j++) {
if (Zotero.Tags.equals(data1[i], data2[j])) {
continue outer;
}
}
changeset.push({
field: "tags",
op: "member-remove",
value: data1[i]
});
}
outer:
for (let i = 0; i < data2.length; i++) {
for (let j = 0; j < data1.length; j++) {
if (Zotero.Tags.equals(data2[i], data1[j])) {
continue outer;
}
}
changeset.push({
field: "tags",
op: "member-add",
value: data2[i]
});
}
return changeset;
},
_relationsDiff: function (data1, data2) {
data2 = data2 || {};
var changeset = [];
for (let pred in data1) {
let vals1 = typeof data1[pred] == 'string' ? [data1[pred]] : data1[pred];
let vals2 = (!data2[pred] || data2[pred] === '')
? []
: typeof data2[pred] == 'string' ? [data2[pred]] : data2[pred];
var removed = Zotero.Utilities.arrayDiff(vals1, vals2);
for (let i = 0; i < removed.length; i++) {
changeset.push({
field: "relations",
op: "property-member-remove",
value: {
key: pred,
value: removed[i]
}
});
}
let added = Zotero.Utilities.arrayDiff(vals2, vals1);
for (let i = 0; i < added.length; i++) {
changeset.push({
field: "relations",
op: "property-member-add",
value: {
key: pred,
value: added[i]
}
});
}
}
return changeset;
},
/**
* Apply a set of changes generated by Zotero.DataObjectUtilities.diff() to an API JSON object
*
* @param {Object} json - API JSON object to modify
* @param {Object[]} changeset - Change instructions, as generated by .diff()
*/
applyChanges: function (json, changeset) {
for (let i = 0; i < changeset.length; i++) {
let c = changeset[i];
if (c.op == 'delete') {
delete json[c.field];
}
else if (c.op == 'add' || c.op == 'modify') {
json[c.field] = c.value;
}
else if (c.op == 'member-add') {
switch (c.field) {
case 'collections':
if (json[c.field].indexOf(c.value) == -1) {
json[c.field].push(c.value);
}
break;
case 'creators':
throw new Error("Unimplemented");
break;
case 'tags':
let found = false;
for (let i = 0; i < json[c.field].length; i++) {
if (Zotero.Tags.equals(json[c.field][i], c.value)) {
found = true;
break;
}
}
if (!found) {
json[c.field].push(c.value);
}
break;
default:
throw new Error("Unexpected field '" + c.field + "'");
}
}
else if (c.op == 'member-remove') {
switch (c.field) {
case 'collections':
let pos = json[c.field].indexOf(c.value);
if (pos == -1) {
continue;
}
json[c.field].splice(pos, 1);
break;
case 'creators':
throw new Error("Unimplemented");
break;
case 'tags':
for (let i = 0; i < json[c.field].length; i++) {
if (Zotero.Tags.equals(json[c.field][i], c.value)) {
json[c.field].splice(i, 1);
break;
}
}
break;
default:
throw new Error("Unexpected field '" + c.field + "'");
}
}
else if (c.op == 'property-member-add') {
switch (c.field) {
case 'relations':
let obj = json[c.field];
let prop = c.value.key;
let val = c.value.value;
if (!obj) {
obj = json[c.field] = {};
}
if (!obj[prop]) {
obj[prop] = [];
}
// Convert string to array
if (typeof obj[prop] == 'string') {
obj[prop] = [obj[prop]];
}
if (obj[prop].indexOf(val) == -1) {
obj[prop].push(val);
}
break;
default:
throw new Error("Unexpected field '" + c.field + "'");
}
}
else if (c.op == 'property-member-remove') {
switch (c.field) {
case 'relations':
let obj = json[c.field];
let prop = c.value.key;
let val = c.value.value;
if (!obj || !obj[prop]) {
continue;
}
if (typeof obj[prop] == 'string') {
// If propetty was the specified string, remove property
if (obj[prop] === val) {
delete obj[prop];
}
continue;
}
let pos = obj[prop].indexOf(val);
if (pos == -1) {
continue;
}
obj[prop].splice(pos, 1);
// If no more members in property array, remove property
if (obj[prop].length == 0) {
delete obj[prop];
}
break;
default:
throw new Error("Unexpected field '" + c.field + "'");
}
}
else {
throw new Error("Unexpected change operation '" + c.op + "'");
}
}
}
};