www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

dataObjectUtilities.js (16499B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2009 Center for History and New Media
      5                      George Mason University, Fairfax, Virginia, USA
      6                      http://zotero.org
      7     
      8     This file is part of Zotero.
      9     
     10     Zotero is free software: you can redistribute it and/or modify
     11     it under the terms of the GNU Affero General Public License as published by
     12     the Free Software Foundation, either version 3 of the License, or
     13     (at your option) any later version.
     14     
     15     Zotero is distributed in the hope that it will be useful,
     16     but WITHOUT ANY WARRANTY; without even the implied warranty of
     17     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18     GNU Affero General Public License for more details.
     19     
     20     You should have received a copy of the GNU Affero General Public License
     21     along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
     22     
     23     ***** END LICENSE BLOCK *****
     24 */
     25 
     26 
     27 Zotero.DataObjectUtilities = {
     28 	/**
     29 	 * Get all DataObject types
     30 	 *
     31 	 * @return {String[]} - An array of DataObject types
     32 	 */
     33 	getTypes: function () {
     34 		return ['collection', 'search', 'item'];
     35 	},
     36 	
     37 	/**
     38 	 * Get DataObject types that are valid for a given library
     39 	 *
     40 	 * @param {Integer} libraryID
     41 	 * @return {String[]} - An array of DataObject types
     42 	 */
     43 	getTypesForLibrary: function (libraryID) {
     44 		switch (Zotero.Libraries.get(libraryID).libraryType) {
     45 		case 'publications':
     46 			return ['item'];
     47 		
     48 		default:
     49 			return this.getTypes();
     50 		}
     51 	},
     52 	
     53 	"checkLibraryID": function (libraryID) {
     54 		if (!libraryID) {
     55 			throw new Error("libraryID not provided");
     56 		}
     57 		var intValue = parseInt(libraryID);
     58 		if (libraryID != intValue || intValue <= 0) {
     59 			throw new Error("libraryID must be a positive integer");
     60 		}
     61 		return intValue;
     62 	},
     63 	
     64 	"checkDataID": function(dataID) {
     65 		var intValue = parseInt(dataID);
     66 		if (dataID != intValue || dataID <= 0)
     67 			throw new Error("id must be a positive integer");
     68 		return intValue;
     69 	},
     70 	
     71 	
     72 	generateKey: function () {
     73 		return Zotero.Utilities.generateObjectKey();
     74 	},
     75 	
     76 	
     77 	"checkKey": function(key) {
     78 		if (!key && key !== 0) return null;
     79 		if (!Zotero.Utilities.isValidObjectKey(key)) {
     80 			throw new Error("key is not valid");
     81 		}
     82 		return key;
     83 	},
     84 	
     85 	
     86 	getObjectTypeSingular: function (objectTypePlural) {
     87 		return objectTypePlural.replace(/(s|es)$/, '');
     88 	},
     89 	
     90 	
     91 	"getObjectTypePlural": function(objectType) {
     92 		switch(objectType) {
     93 			case 'search':
     94 				return 'searches';
     95 			break;
     96 			case 'library':
     97 				return 'libraries';
     98 			break;
     99 			default:
    100 				return objectType + 's';
    101 		}
    102 	},
    103 	
    104 	
    105 	"getObjectsClassForObjectType": function(objectType) {
    106 		if (objectType == 'setting') objectType = 'syncedSetting';
    107 		
    108 		var objectTypePlural = this.getObjectTypePlural(objectType);
    109 		var className = objectTypePlural[0].toUpperCase() + objectTypePlural.substr(1);
    110 		return Zotero[className]
    111 	},
    112 	
    113 	
    114 	patch: function (base, obj) {
    115 		var target = {};
    116 		Object.assign(target, obj);
    117 		
    118 		for (let i in base) {
    119 			switch (i) {
    120 			case 'key':
    121 			case 'version':
    122 			case 'dateModified':
    123 				continue;
    124 			}
    125 			
    126 			// If field from base exists in the new version, delete it if it's the same
    127 			if (i in target) {
    128 				if (!this._fieldChanged(i, base[i], target[i])) {
    129 					delete target[i];
    130 				}
    131 			}
    132 			// If field from base doesn't exist in new version, clear it
    133 			else {
    134 				switch (i) {
    135 				// When changing an item from top-level to child, the collections property is
    136 				// no valid, so it doesn't need to be cleared
    137 				case 'collections':
    138 					break;
    139 				
    140 				case 'deleted':
    141 				case 'parentItem':
    142 				case 'inPublications':
    143 					target[i] = false;
    144 					break;
    145 				
    146 				default:
    147 					target[i] = '';
    148 				}
    149 			}
    150 		}
    151 		
    152 		return target;
    153 	},
    154 	
    155 	
    156 	/**
    157 	 * Determine whether two API JSON objects are equivalent
    158 	 *
    159 	 * Note: Currently unused
    160 	 *
    161 	 * @param {Object} data1 - API JSON of first object
    162 	 * @param {Object} data2 - API JSON of second object
    163 	 * @param {Array} [ignoreFields] - Fields to ignore
    164 	 * @return {Boolean} - True if objects are the same, false if not
    165 	 */
    166 	equals: function (data1, data2, ignoreFields) {
    167 		var skipFields = {};
    168 		for (let field of ['key', 'version'].concat(ignoreFields || [])) {
    169 			skipFields[field] = true;
    170 		}
    171 		
    172 		for (let field in data1) {
    173 			if (skipFields[field]) {
    174 				continue;
    175 			}
    176 			
    177 			let val1 = data1[field];
    178 			let val2 = data2[field];
    179 			let val1HasValue = val1 || val1 === 0;
    180 			let val2HasValue = val2 || val2 === 0;
    181 			
    182 			if (!val1HasValue && !val2HasValue) {
    183 				continue;
    184 			}
    185 			
    186 			let changed = this._fieldChanged(field, val1, val2);
    187 			if (changed) {
    188 				return false;
    189 			}
    190 			
    191 			skipFields[field] = true;
    192 		}
    193 		
    194 		for (let field in data2) {
    195 			// Skip ignored fields and fields we've already compared
    196 			if (skipFields[field]) {
    197 				continue;
    198 			}
    199 			
    200 			// All remaining fields don't exist in data1
    201 			
    202 			if (data2[field] === false) {
    203 				continue;
    204 			}
    205 			
    206 			return false;
    207 		}
    208 		
    209 		return true;
    210 	},
    211 	
    212 	_fieldChanged: function (fieldName, field1, field2) {
    213 		switch (fieldName) {
    214 		case 'collections':
    215 		case 'conditions':
    216 		case 'creators':
    217 		case 'tags':
    218 		case 'relations':
    219 			return this["_" + fieldName + "Changed"](field1, field2);
    220 		
    221 		default:
    222 			return field1 !== field2;
    223 		}
    224 	},
    225 	
    226 	_creatorsChanged: function (data1, data2) {
    227 		if (!data2 || data1.length != data2.length) return true;
    228 		for (let i = 0; i < data1.length; i++) {
    229 			if (!Zotero.Creators.equals(data1[i], data2[i])) {
    230 				return true;
    231 			}
    232 		}
    233 		return false;
    234 	},
    235 	
    236 	_conditionsChanged: function (data1, data2) {
    237 		if (!data2 || data1.length != data2.length) return true;
    238 		for (let i = 0; i < data1.length; i++) {
    239 			if (!Zotero.Searches.conditionEquals(data1[i], data2[i])) {
    240 				return true;
    241 			}
    242 		}
    243 		return false;
    244 	},
    245 	
    246 	_collectionsChanged: function (data1, data2) {
    247 		if (!data2 || data1.length != data2.length) return true;
    248 		let c1 = data1.concat();
    249 		let c2 = data2.concat();
    250 		c1.sort();
    251 		c2.sort();
    252 		return !Zotero.Utilities.arrayEquals(c1, c2);
    253 	},
    254 	
    255 	_tagsChanged: function (data1, data2) {
    256 		if (!data2 || data1.length != data2.length) return true;
    257 		for (let i = 0; i < data1.length; i++) {
    258 			if (!Zotero.Tags.equals(data1[i], data2[i])) {
    259 				return true;
    260 			}
    261 		}
    262 		return false;
    263 	},
    264 	
    265 	_relationsChanged: function (data1, data2) {
    266 		if (!data2) return true;
    267 		var pred1 = Object.keys(data1);
    268 		pred1.sort();
    269 		var pred2 = Object.keys(data2);
    270 		pred2.sort();
    271 		if (!Zotero.Utilities.arrayEquals(pred1, pred2)) return true;
    272 		for (let pred in pred1) {
    273 			let vals1 = typeof data1[pred] == 'string' ? [data1[pred]] : data1[pred];
    274 			let vals2 = (!data2[pred] || data2[pred] === '')
    275 				? []
    276 				: typeof data2[pred] == 'string' ? [data2[pred]] : data2[pred];
    277 			
    278 			if (!Zotero.Utilities.arrayEquals(vals1, vals2)) {
    279 				return true;
    280 			}
    281 		}
    282 		return false;
    283 	},
    284 	
    285 	
    286 	/**
    287 	 * Compare two API JSON objects and generate a changeset
    288 	 *
    289 	 * @param {Object} data1
    290 	 * @param {Object} data2
    291 	 * @param {String[]} [ignoreFields] - Fields to ignore
    292 	 */
    293 	diff: function (data1, data2, ignoreFields) {
    294 		var changeset = [];
    295 		
    296 		var skipFields = {};
    297 		for (let field of ['key', 'version'].concat(ignoreFields || [])) {
    298 			skipFields[field] = true;
    299 		}
    300 		
    301 		for (let field in data1) {
    302 			if (skipFields[field]) {
    303 				continue;
    304 			}
    305 			
    306 			let val1 = data1[field];
    307 			let val2 = data2[field];
    308 			let val1HasValue = (val1 && val1 !== "") || val1 === 0;
    309 			let val2HasValue = (val2 && val2 !== "") || val2 === 0;
    310 			
    311 			if (!val1HasValue && !val2HasValue) {
    312 				continue;
    313 			}
    314 			
    315 			switch (field) {
    316 			case 'creators':
    317 			case 'collections':
    318 			case 'conditions':
    319 			case 'relations':
    320 			case 'tags':
    321 				let changes = this["_" + field + "Diff"](val1, val2);
    322 				if (changes.length) {
    323 					changeset = changeset.concat(changes);
    324 				}
    325 				break;
    326 			
    327 			case 'note':
    328 				let change = this._htmlDiff(field, val1, val2);
    329 				if (change) {
    330 					changeset.push(change);
    331 				}
    332 				break;
    333 			
    334 			default:
    335 				var changed = val1 !== val2;
    336 				if (changed) {
    337 					if (val1HasValue && !val2HasValue) {
    338 						changeset.push({
    339 							field: field,
    340 							op: 'delete'
    341 						});
    342 					}
    343 					else if (!val1HasValue && val2HasValue) {
    344 						changeset.push({
    345 							field: field,
    346 							op: 'add',
    347 							value: val2
    348 						});
    349 					}
    350 					else {
    351 						changeset.push({
    352 							field: field,
    353 							op: 'modify',
    354 							value: val2
    355 						});
    356 					}
    357 				}
    358 			}
    359 			
    360 			skipFields[field] = true;
    361 		}
    362 		
    363 		for (let field in data2) {
    364 			// Skip ignored fields and fields we've already compared
    365 			if (skipFields[field]) {
    366 				continue;
    367 			}
    368 			
    369 			// All remaining fields don't exist in data1
    370 			
    371 			let val = data2[field];
    372 			if (val === false || val === "" || val === null
    373 					|| (typeof val == 'object' && Object.keys(val).length == 0)) {
    374 				continue;
    375 			}
    376 			
    377 			changeset.push({
    378 				field: field,
    379 				op: "add",
    380 				value: data2[field]
    381 			});
    382 		}
    383 		
    384 		return changeset;
    385 	},
    386 	
    387 	/**
    388 	 * For creators, just determine if changed, since ordering makes a full diff too complicated
    389 	 */
    390 	_creatorsDiff: function (data1, data2) {
    391 		if (!data2 || !data2.length) {
    392 			if (!data1.length) {
    393 				return [];
    394 			}
    395 			return [{
    396 				field: "creators",
    397 				op: "delete"
    398 			}];
    399 		}
    400 		if (this._creatorsChanged(data1, data2)) {
    401 			return [{
    402 				field: "creators",
    403 				op: "modify",
    404 				value: data2
    405 			}];
    406 		}
    407 		return [];
    408 	},
    409 	
    410 	_collectionsDiff: function (data1, data2 = []) {
    411 		var changeset = [];
    412 		var removed = Zotero.Utilities.arrayDiff(data1, data2);
    413 		for (let i = 0; i < removed.length; i++) {
    414 			changeset.push({
    415 				field: "collections",
    416 				op: "member-remove",
    417 				value: removed[i]
    418 			});
    419 		}
    420 		let added = Zotero.Utilities.arrayDiff(data2, data1);
    421 		for (let i = 0; i < added.length; i++) {
    422 			changeset.push({
    423 				field: "collections",
    424 				op: "member-add",
    425 				value: added[i]
    426 			});
    427 		}
    428 		return changeset;
    429 	},
    430 	
    431 	_conditionsDiff: function (data1, data2 = {}) {
    432 		var changeset = [];
    433 		outer:
    434 		for (let i = 0; i < data1.length; i++) {
    435 			for (let j = 0; j < data2.length; j++) {
    436 				if (Zotero.SearchConditions.equals(data1[i], data2[j])) {
    437 					continue outer;
    438 				}
    439 			}
    440 			changeset.push({
    441 				field: "conditions",
    442 				op: "member-remove",
    443 				value: data1[i]
    444 			});
    445 		}
    446 		outer:
    447 		for (let i = 0; i < data2.length; i++) {
    448 			for (let j = 0; j < data1.length; j++) {
    449 				if (Zotero.SearchConditions.equals(data2[i], data1[j])) {
    450 					continue outer;
    451 				}
    452 			}
    453 			changeset.push({
    454 				field: "conditions",
    455 				op: "member-add",
    456 				value: data2[i]
    457 			});
    458 		}
    459 		return changeset;
    460 	},
    461 	
    462 	_htmlDiff: function (field, html1, html2 = "") {
    463 		if (html1 == "" && html2 != "") {
    464 			return {
    465 				field,
    466 				op: "add",
    467 				value: html2
    468 			};
    469 		}
    470 		if (html1 != "" && html2 == "") {
    471 			return {
    472 				field,
    473 				op: "delete"
    474 			};
    475 		}
    476 		
    477 		// Until we have a consistent way of sanitizing HTML on client and server, account for differences
    478 		var mods = [
    479 			['<p>&nbsp;</p>', '<p>\u00a0</p>']
    480 		];
    481 		var a = html1;
    482 		var b = html2;
    483 		for (let mod of mods) {
    484 			a = a.replace(new RegExp(mod[0], 'g'), mod[1]);
    485 			b = b.replace(new RegExp(mod[0], 'g'), mod[1]);
    486 		}
    487 		if (a != b) {
    488 			Zotero.debug("HTML diff:");
    489 			Zotero.debug(a);
    490 			Zotero.debug(b);
    491 			return {
    492 				field,
    493 				op: "modify",
    494 				value: html2
    495 			};
    496 		}
    497 		
    498 		return false;
    499 	},
    500 	
    501 	_tagsDiff: function (data1, data2 = []) {
    502 		var changeset = [];
    503 		outer:
    504 		for (let i = 0; i < data1.length; i++) {
    505 			for (let j = 0; j < data2.length; j++) {
    506 				if (Zotero.Tags.equals(data1[i], data2[j])) {
    507 					continue outer;
    508 				}
    509 			}
    510 			changeset.push({
    511 				field: "tags",
    512 				op: "member-remove",
    513 				value: data1[i]
    514 			});
    515 		}
    516 		outer:
    517 		for (let i = 0; i < data2.length; i++) {
    518 			for (let j = 0; j < data1.length; j++) {
    519 				if (Zotero.Tags.equals(data2[i], data1[j])) {
    520 					continue outer;
    521 				}
    522 			}
    523 			changeset.push({
    524 				field: "tags",
    525 				op: "member-add",
    526 				value: data2[i]
    527 			});
    528 		}
    529 		return changeset;
    530 	},
    531 	
    532 	_relationsDiff: function (data1, data2 = {}) {
    533 		var changeset = [];
    534 		for (let pred in data1) {
    535 			let vals1 = typeof data1[pred] == 'string' ? [data1[pred]] : data1[pred];
    536 			let vals2 = (!data2[pred] || data2[pred] === '')
    537 				? []
    538 				: typeof data2[pred] == 'string' ? [data2[pred]] : data2[pred];
    539 			
    540 			var removed = Zotero.Utilities.arrayDiff(vals1, vals2);
    541 			for (let i = 0; i < removed.length; i++) {
    542 				changeset.push({
    543 					field: "relations",
    544 					op: "property-member-remove",
    545 					value: {
    546 						key: pred,
    547 						value: removed[i]
    548 					}
    549 				});
    550 			}
    551 			let added = Zotero.Utilities.arrayDiff(vals2, vals1);
    552 			for (let i = 0; i < added.length; i++) {
    553 				changeset.push({
    554 					field: "relations",
    555 					op: "property-member-add",
    556 					value: {
    557 						key: pred,
    558 						value: added[i]
    559 					}
    560 				});
    561 			}
    562 		}
    563 		for (let pred in data2) {
    564 			// Property in first object has already been handled
    565 			if (data1[pred]) continue;
    566 			
    567 			let vals = typeof data2[pred] == 'string' ? [data2[pred]] : data2[pred];
    568 			for (let i = 0; i < vals.length; i++) {
    569 				changeset.push({
    570 					field: "relations",
    571 					op: "property-member-add",
    572 					value: {
    573 						key: pred,
    574 						value: vals[i]
    575 					}
    576 				});
    577 			}
    578 		}
    579 		return changeset;
    580 	},
    581 	
    582 	
    583 	/**
    584 	 * Apply a set of changes generated by Zotero.DataObjectUtilities.diff() to an API JSON object
    585 	 *
    586 	 * @param {Object} json - API JSON object to modify
    587 	 * @param {Object[]} changeset - Change instructions, as generated by .diff()
    588 	 */
    589 	applyChanges: function (json, changeset) {
    590 		for (let i = 0; i < changeset.length; i++) {
    591 			let c = changeset[i];
    592 			if (c.op == 'delete') {
    593 				delete json[c.field];
    594 			}
    595 			else if (c.op == 'add' || c.op == 'modify') {
    596 				json[c.field] = c.value;
    597 			}
    598 			else if (c.op == 'member-add') {
    599 				switch (c.field) {
    600 				case 'collections':
    601 					if (json[c.field].indexOf(c.value) == -1) {
    602 						json[c.field].push(c.value);
    603 					}
    604 					break;
    605 				
    606 				case 'creators':
    607 					throw new Error("Unimplemented");
    608 					break;
    609 				
    610 				case 'conditions':
    611 				case 'tags':
    612 					let found = false;
    613 					let f = c.field == 'conditions' ? Zotero.SearchConditions : Zotero.Tags;
    614 					for (let i = 0; i < json[c.field].length; i++) {
    615 						if (f.equals(json[c.field][i], c.value)) {
    616 							found = true;
    617 							break;
    618 						}
    619 					}
    620 					if (!found) {
    621 						json[c.field].push(c.value);
    622 					}
    623 					break;
    624 					
    625 				default:
    626 					throw new Error("Unexpected field '" + c.field + "'");
    627 				}
    628 			}
    629 			else if (c.op == 'member-remove') {
    630 				switch (c.field) {
    631 				case 'collections':
    632 					let pos = json[c.field].indexOf(c.value);
    633 					if (pos == -1) {
    634 						continue;
    635 					}
    636 					json[c.field].splice(pos, 1);
    637 					break;
    638 				
    639 				case 'creators':
    640 					throw new Error("Unimplemented");
    641 					break;
    642 				
    643 				case 'conditions':
    644 				case 'tags':
    645 					let f = c.field == 'conditions' ? Zotero.SearchConditions : Zotero.Tags;
    646 					for (let i = 0; i < json[c.field].length; i++) {
    647 						if (f.equals(json[c.field][i], c.value)) {
    648 							json[c.field].splice(i, 1);
    649 							break;
    650 						}
    651 					}
    652 					break;
    653 					
    654 				default:
    655 					throw new Error("Unexpected field '" + c.field + "'");
    656 				}
    657 			}
    658 			else if (c.op == 'property-member-add') {
    659 				switch (c.field) {
    660 				case 'relations':
    661 					let obj = json[c.field];
    662 					let prop = c.value.key;
    663 					let val = c.value.value;
    664 					if (!obj) {
    665 						obj = json[c.field] = {};
    666 					}
    667 					if (!obj[prop]) {
    668 						obj[prop] = [];
    669 					}
    670 					// Convert string to array
    671 					if (typeof obj[prop] == 'string') {
    672 						obj[prop] = [obj[prop]];
    673 					}
    674 					if (obj[prop].indexOf(val) == -1) {
    675 						obj[prop].push(val);
    676 					}
    677 					break;
    678 					
    679 				default:
    680 					throw new Error("Unexpected field '" + c.field + "'");
    681 				}
    682 			}
    683 			else if (c.op == 'property-member-remove') {
    684 				switch (c.field) {
    685 				case 'relations':
    686 					let obj = json[c.field];
    687 					let prop = c.value.key;
    688 					let val = c.value.value;
    689 					if (!obj || !obj[prop]) {
    690 						continue;
    691 					}
    692 					if (typeof obj[prop] == 'string') {
    693 						// If propetty was the specified string, remove property
    694 						if (obj[prop] === val) {
    695 							delete obj[prop];
    696 						}
    697 						continue;
    698 					}
    699 					let pos = obj[prop].indexOf(val);
    700 					if (pos == -1) {
    701 						continue;
    702 					}
    703 					obj[prop].splice(pos, 1);
    704 					// If no more members in property array, remove property
    705 					if (obj[prop].length == 0) {
    706 						delete obj[prop];
    707 					}
    708 					break;
    709 					
    710 				default:
    711 					throw new Error("Unexpected field '" + c.field + "'");
    712 				}
    713 			}
    714 			else {
    715 				throw new Error("Unexpected change operation '" + c.op + "'");
    716 			}
    717 		}
    718 	}
    719 };