www

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

history.js (13836B)


      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 Zotero.History = new function(){
     27 	this.begin = begin;
     28 	this.setAssociatedID = setAssociatedID;
     29 	this.add = add;
     30 	this.modify = modify;
     31 	this.remove = remove;
     32 	this.commit = commit;
     33 	this.cancel = cancel;
     34 	this.getPreviousEvent = getPreviousEvent;
     35 	this.getNextEvent = getNextEvent;
     36 	this.undo = undo;
     37 	this.redo = redo;
     38 	this.clear = clear;
     39 	this.clearAfter = clearAfter;
     40 	
     41 	var _firstTime = true;
     42 	var _currentID = 0;
     43 	var _activeID;
     44 	var _activeEvent;
     45 	var _maxID = 0;
     46 	
     47 	
     48 	/**
     49 	* Begin a transaction set
     50 	*
     51 	* event: 'item-add', 'item-delete', 'item-modify', 'collection-add',
     52 	*		'collection-modify', 'collection-delete'...
     53 	*
     54 	* id: An id or array of ids that will be passed to
     55 	* 		Zotero.Notifier.trigger() on an undo or redo
     56 	**/
     57 	function begin(event, id){
     58 		if (_activeID){
     59 			throw('History transaction set already in progress');
     60 		}
     61 		
     62 		// If running for the first time this session or we're in the middle of
     63 		// the history, clear any transaction sets after the current position
     64 		if (_firstTime || _currentID<_maxID){
     65 			_firstTime = false;
     66 			this.clearAfter();
     67 		}
     68 		
     69 		Zotero.debug('Beginning history transaction set ' + event);
     70 		var sql = "INSERT INTO transactionSets (event, id) VALUES "
     71 			+ "('" + event + "', ";
     72 		if (!id){
     73 			sql += '0';
     74 		}
     75 		// If array, insert hyphen-delimited string
     76 		else if (typeof id=='object'){
     77 			sql += "'" + id.join('-') + "'"
     78 		}
     79 		else {
     80 			sql += id;
     81 		}
     82 		sql += ")";
     83 		
     84 		Zotero.DB.beginTransaction();
     85 		_activeID = Zotero.DB.query(sql);
     86 		_activeEvent = event;
     87 	}
     88 	
     89 	
     90 	/**
     91 	* Associate an id or array of ids with the transaction set --
     92 	* 	for use if the ids weren't available at when begin() was called
     93 	*
     94 	* id: An id or array of ids that will be passed to
     95 	* 		Zotero.Notifier.trigger() on an undo or redo
     96 	**/
     97 	function setAssociatedID(id){
     98 		if (!_activeID){
     99 			throw('Cannot call setAssociatedID() with no history transaction set in progress');
    100 		}
    101 		
    102 		var sql = "UPDATE transactionSets SET id=";
    103 		if (!id){
    104 			sql += '0';
    105 		}
    106 		// If array, insert hyphen-delimited string
    107 		else if (typeof id=='object'){
    108 			sql += "'" + id.join('-') + "'"
    109 		}
    110 		else {
    111 			sql += id;
    112 		}
    113 		sql += " WHERE transactionSetID=" + _activeID;
    114 		Zotero.DB.query(sql);
    115 	}
    116 	
    117 	
    118 	/**
    119 	* Add an add transaction to the current set
    120 	*
    121 	* Can be called before or after an INSERT statement
    122 	*
    123 	* key is a hyphen-delimited list of columns identifying the row
    124 	* 		e.g. 'itemID-creatorID'
    125 	*
    126 	* keyValues is a hyphen-delimited list of values matching the key parts
    127 	* 		e.g. '1-1'
    128 	**/
    129 	function add(table, key, keyValues){
    130 		return _addTransaction('add', table, key, keyValues);
    131 	}
    132 	
    133 	
    134 	/**
    135 	* Add a modify transaction to the current set
    136 	*
    137 	* Must be called before an UPDATE statement
    138 	*
    139 	* key is a hyphen-delimited list of columns identifying the row
    140 	* 		e.g. 'itemID-creatorID'
    141 	*
    142 	* keyValues is an array or hyphen-delimited string of values matching
    143 	*	the key parts (e.g. [1, 1] or '1-1')
    144 	*
    145 	* _field_ is optional -- otherwise all fields are saved
    146 	**/
    147 	function modify(table, key, keyValues, field){
    148 		return _addTransaction('modify', table, key, keyValues, field);
    149 	}
    150 	
    151 	
    152 	/**
    153 	* Add a remove transaction to the current set
    154 	*
    155 	* Must be called before a DELETE statement
    156 	*
    157 	* key is a hyphen-delimited list of columns identifying the row
    158 	* 		e.g. 'itemID-creatorID'
    159 	*
    160 	* keyValues is a hyphen-delimited list of values matching the key parts
    161 	* 		e.g. '1-1'
    162 	**/
    163 	function remove(table, key, keyValues){
    164 		return _addTransaction('remove', table, key, keyValues);
    165 	}
    166 	
    167 	
    168 	/**
    169 	* Commit the current transaction set
    170 	**/
    171 	function commit(){
    172 		Zotero.debug('Committing history transaction set ' + _activeEvent);
    173 		Zotero.DB.commitTransaction();
    174 		_currentID = _activeID;
    175 		_maxID = _activeID;
    176 		_activeID = null;
    177 		_activeEvent = null;
    178 	}
    179 	
    180 	
    181 	/**
    182 	* Cancel the current transaction set
    183 	**/
    184 	function cancel(){
    185 		Zotero.debug('Cancelling history transaction set ' + _activeEvent);
    186 		Zotero.DB.rollbackTransaction();
    187 		_activeID = null;
    188 		_activeEvent = null;
    189 	}
    190 	
    191 	
    192 	/**
    193 	* Get the next event to undo, or false if none
    194 	**/
    195 	function getPreviousEvent(){
    196 		if (!_currentID){
    197 			return false;
    198 		}
    199 		
    200 		var sql = "SELECT event FROM transactionSets WHERE transactionSetID="
    201 			+ _currentID;
    202 		return Zotero.DB.valueQuery(sql);
    203 	}
    204 	
    205 	
    206 	/**
    207 	* Get the next event to redo, or false if none
    208 	**/
    209 	function getNextEvent(){
    210 		var sql = "SELECT event FROM transactionSets WHERE transactionSetID="
    211 			+ (_currentID + 1);
    212 		return Zotero.DB.valueQuery(sql);
    213 	}
    214 	
    215 	
    216 	/**
    217 	* Undo the last transaction set
    218 	**/
    219 	function undo(){
    220 		if (!_currentID){
    221 			throw('No transaction set to undo');
    222 			return false;
    223 		}
    224 		
    225 		var id = _currentID;
    226 		Zotero.debug('Undoing transaction set ' + id);
    227 		Zotero.DB.beginTransaction();
    228 		var undone = _do('undo');
    229 		_currentID--;
    230 		Zotero.DB.commitTransaction();
    231 		_reloadAndNotify(id);
    232 		return true;
    233 	}
    234 	
    235 	
    236 	/**
    237 	* Redo the next transaction set
    238 	**/
    239 	function redo(){
    240 		var id = _currentID + 1;
    241 		Zotero.debug('Redoing transaction set ' + id);
    242 		Zotero.DB.beginTransaction();
    243 		var redone = _do('redo');
    244 		_currentID++;
    245 		Zotero.DB.commitTransaction();
    246 		_reloadAndNotify(id, true);
    247 		return redone;
    248 	}
    249 	
    250 	
    251 	/**
    252 	* Clear the entire history
    253 	**/
    254 	function clear(){
    255 		Zotero.DB.beginTransaction();
    256 		Zotero.DB.query("DELETE FROM transactionSets");
    257 		Zotero.DB.query("DELETE FROM transactions");
    258 		Zotero.DB.query("DELETE FROM transactionLog");
    259 		_currentID = null;
    260 		_activeID = null;
    261 		_activeEvent = null;
    262 		_maxID = null;
    263 		Zotero.DB.commitTransaction();
    264 	}
    265 	
    266 	
    267 	/**
    268 	* Clear all transactions in history after the current one
    269 	**/
    270 	function clearAfter(){
    271 		Zotero.DB.beginTransaction();
    272 		var min = Zotero.DB.valueQuery("SELECT MIN(transactionID) FROM "
    273 			+ "transactions WHERE transactionSetID=" + (_currentID + 1));
    274 		
    275 		if (!min){
    276 			Zotero.DB.commitTransaction();
    277 			return;
    278 		}
    279 		
    280 		Zotero.DB.query("DELETE FROM transactionLog "
    281 			+ "WHERE transactionID>=" + min);
    282 		Zotero.DB.query("DELETE FROM transactions "
    283 			+ "WHERE transactionID>=" + min);
    284 		Zotero.DB.query("DELETE FROM transactionSets "
    285 			+ "WHERE transactionSetID>" + _currentID);
    286 		
    287 		_maxID = _currentID;
    288 		_activeID = null;
    289 		Zotero.DB.commitTransaction();
    290 		return;
    291 	}
    292 	
    293 	
    294 	//
    295 	// Private methods
    296 	//
    297 	
    298 	function _addTransaction(action, table, key, keyValues, field){
    299 		if (!_activeID){
    300 			throw('Cannot add history transaction with no transaction set in progress');
    301 		}
    302 		
    303 		if (typeof keyValues == 'object'){
    304 			keyValues = keyValues.join('-');
    305 		}
    306 		
    307 		var contextString = table + '.' + key + '.' + keyValues;
    308 		var context = _parseContext(contextString);
    309 		var fromClause = _contextToSQLFrom(context);
    310 		
    311 		var sql = "INSERT INTO transactions (transactionSetID, context, action) "
    312 			+ "VALUES (" + _activeID + ", '" + contextString
    313 			+ "', '" + action + "')";
    314 			
    315 		var transactionID = Zotero.DB.query(sql);
    316 		
    317 		switch (action){
    318 			case 'add':
    319 				// No need to store an add, since we'll just delete it to reverse
    320 				break;
    321 			case 'modify':
    322 				// Only save one field -- _do() won't know about this, but the
    323 				// UPDATE statements on the other fields just won't do anything
    324 				if (field){
    325 					var sql = "INSERT INTO transactionLog SELECT " + transactionID
    326 						+ ", '" + field + "', " + field + fromClause;
    327 					Zotero.DB.query(sql);
    328 					break;
    329 				}
    330 				// Fall through if no field specified and save all
    331 			case 'remove':
    332 				var cols = Zotero.DB.getColumns(table);
    333 				for (var i in cols){
    334 					// If column is not part of the key, log it
    335 					if (!context['keys'].indexOf(cols[i]) === -1){
    336 						var sql = "INSERT INTO transactionLog "
    337 							+ "SELECT " + transactionID + ", '" + cols[i]
    338 							+ "', " + cols[i] + fromClause;
    339 						Zotero.DB.query(sql);
    340 					}
    341 				}
    342 				break;
    343 			default:
    344 				Zotero.DB.rollbackTransaction();
    345 				throw("Invalid history action '" + action + "'");
    346 		}
    347 	}
    348 	
    349 	
    350 	function _do(mode){
    351 		switch (mode){
    352 			case 'undo':
    353 				var id = _currentID;
    354 				break;
    355 			case 'redo':
    356 				var id = _currentID + 1;
    357 				break;
    358 		}
    359 		
    360 		var sql = "SELECT transactionID, context, action FROM transactions "
    361 			+ "WHERE transactionSetID=" + id;
    362 		var transactions = Zotero.DB.query(sql);
    363 		
    364 		if (!transactions){
    365 			throw('Transaction set not found for '
    366 				+ (mode=='undo' ? 'current' : 'next') + id);
    367 		}
    368 		
    369 		for (var i in transactions){
    370 			var transactionID = transactions[i]['transactionID'];
    371 			var context = _parseContext(transactions[i]['context']);
    372 			
    373 			// If in redo mode, swap 'add' and 'remove'
    374 			if (mode=='redo'){
    375 				switch (transactions[i]['action']){
    376 					case 'add':
    377 						transactions[i]['action'] = 'remove';
    378 						break;
    379 					case 'remove':
    380 						transactions[i]['action'] = 'add';
    381 						break;
    382 				}
    383 			}
    384 			
    385 			switch (transactions[i]['action']){
    386 				case 'add':
    387 					var fromClause = _contextToSQLFrom(context);
    388 					
    389 					// First, store the row we're about to delete for later redo
    390 					var cols = Zotero.DB.getColumns(context['table']);
    391 					for (var i in cols){
    392 						// If column is not part of the key, log it
    393 						if (!context['keys'].indexOf(cols[i]) === -1){
    394 							var sql = "INSERT INTO transactionLog "
    395 								+ "SELECT " + transactionID + ", '" + cols[i]
    396 								+ "', " + cols[i] + fromClause;
    397 							Zotero.DB.query(sql);
    398 						}
    399 					}
    400 					
    401 					// And delete the row
    402 					var sql = "DELETE" + fromClause;
    403 					Zotero.DB.query(sql);
    404 					break;
    405 					
    406 				case 'modify':
    407 					// Retrieve old values
    408 					var sql = "SELECT field, value FROM transactionLog "
    409 						+ "WHERE transactionID=" + transactionID;
    410 					var oldFieldValues = Zotero.DB.query(sql);
    411 					
    412 					// Retrieve new values
    413 					var sql = "SELECT *" + _contextToSQLFrom(context);
    414 					var newValues = Zotero.DB.rowQuery(sql);
    415 					
    416 					// Update row with old values
    417 					var sql = "UPDATE " + context['table'] + " SET ";
    418 					var values = [];
    419 					for (var i in oldFieldValues){
    420 						sql += oldFieldValues[i]['field'] + '=?, ';
    421 						values.push(oldFieldValues[i]['value']);
    422 					}
    423 					sql = sql.substr(0, sql.length-2) + _contextToSQLWhere(context);
    424 					Zotero.DB.query(sql, values);
    425 					
    426 					// Update log with new values for later redo
    427 					for (var i in newValues){
    428 						if (context['keys'].indexOf(i) === -1){
    429 							var sql = "UPDATE transactionLog SET "
    430 								+ "value=? WHERE transactionID=? AND field=?";
    431 							Zotero.DB.query(sql, [i, newValues[i], transactionID]);
    432 						}
    433 					}
    434 					break;
    435 					
    436 				case 'remove':
    437 					// Retrieve old values
    438 					var sql = "SELECT field, value FROM transactionLog "
    439 						+ "WHERE transactionID=" + transactionID;
    440 					var oldFieldValues = Zotero.DB.query(sql);
    441 					
    442 					// Add key to parameters
    443 					var fields = [], values = [], marks = [];
    444 					for (var i=0; i<context['keys'].length; i++){
    445 						fields.push(context['keys'][i]);
    446 						values.push(context['values'][i]);
    447 						marks.push('?');
    448 					}
    449 					
    450 					// Add other fields to parameters
    451 					for (var i in oldFieldValues){
    452 						fields.push(oldFieldValues[i]['field']);
    453 						values.push(oldFieldValues[i]['value']);
    454 						marks.push('?');
    455 					}
    456 					
    457 					// Insert old values into table
    458 					var sql = "INSERT INTO " + context['table'] + "("
    459 						+ fields.join() + ") VALUES (" + marks.join() + ")";
    460 					Zotero.DB.query(sql, values);
    461 					
    462 					// Delete restored data from transactionLog
    463 					var sql = "DELETE FROM transactionLog WHERE transactionID="
    464 						+ transactionID;
    465 					Zotero.DB.query(sql);
    466 					break;
    467 			}
    468 		}
    469 	}
    470 	
    471 	
    472 	function _parseContext(context){
    473 		var parts = context.split('.');
    474 		var parsed = {
    475 			table:parts[0],
    476 			keys:parts[1].split('-'),
    477 			values:parts[2].split('-')
    478 		}
    479 		if (parsed['keys'].length!=parsed['values'].length){
    480 			throw("Different number of keys and values in _parseContext('"
    481 				+ context + "')");
    482 		}
    483 		
    484 		return parsed;
    485 	}
    486 	
    487 	
    488 	function _contextToSQLFrom(parsed){
    489 		return " FROM " + parsed['table'] + _contextToSQLWhere(parsed);
    490 	}
    491 	
    492 	
    493 	function _contextToSQLWhere(parsed){
    494 		var sql = " WHERE ";
    495 		for (var i=0; i<parsed['keys'].length; i++){
    496 			// DEBUG: type?
    497 			sql += parsed['keys'][i] + "='" + parsed['values'][i] + "' AND ";
    498 		}
    499 		return sql.substr(0, sql.length-5);
    500 	}
    501 	
    502 	
    503 	/**
    504 	* Get the ids associated with a particular transaction set
    505 	**/
    506 	function _getSetData(transactionSetID){
    507 		var sql = "SELECT event, id FROM transactionSets WHERE transactionSetID="
    508 			+ transactionSetID;
    509 		return Zotero.DB.rowQuery(sql);
    510 	}
    511 	
    512 	
    513 	function _reloadAndNotify(transactionSetID, redo){
    514 		var data = _getSetData(transactionSetID);
    515 		var eventParts = data['event'].split('-'); // e.g. modify-item
    516 		if (redo){
    517 			switch (eventParts[0]){
    518 				case 'add':
    519 					eventParts[0] = 'remove';
    520 					break;
    521 				case 'remove':
    522 					eventParts[0] = 'add';
    523 					break;
    524 			}
    525 		}
    526 		switch (eventParts[1]){
    527 			case 'item':
    528 				Zotero.Items.reload(data['id']);
    529 				break;
    530 		}
    531 		
    532 		Zotero.Notifier.trigger(eventParts[0], eventParts[1], data['id']);
    533 	}
    534 }