zotero/chrome/chromeFiles/content/scholar/xpcom/history.js
2006-06-27 05:42:35 +00:00

509 lines
13 KiB
JavaScript

Scholar.History = new function(){
this.begin = begin;
this.setAssociatedID = setAssociatedID;
this.add = add;
this.modify = modify;
this.remove = remove;
this.commit = commit;
this.cancel = cancel;
this.getPreviousEvent = getPreviousEvent;
this.getNextEvent = getNextEvent;
this.undo = undo;
this.redo = redo;
this.clear = clear;
this.clearAfter = clearAfter;
var _firstTime = true;
var _currentID = 0;
var _activeID;
var _activeEvent;
var _maxID = 0;
/**
* Begin a transaction set
*
* event: 'item-add', 'item-delete', 'item-modify', 'collection-add',
* 'collection-modify', 'collection-delete'...
*
* id: An id or array of ids that will be passed to
* Scholar.Notifier.trigger() on an undo or redo
**/
function begin(event, id){
if (_activeID){
throw('History transaction set already in progress');
}
// If running for the first time this session or we're in the middle of
// the history, clear any transaction sets after the current position
if (_firstTime || _currentID<_maxID){
_firstTime = false;
this.clearAfter();
}
Scholar.debug('Beginning history transaction set ' + event);
var sql = "INSERT INTO transactionSets (event, id) VALUES "
+ "('" + event + "', ";
if (!id){
sql += '0';
}
// If array, insert hyphen-delimited string
else if (typeof id=='object'){
sql += "'" + id.join('-') + "'"
}
else {
sql += id;
}
sql += ")";
Scholar.DB.beginTransaction();
_activeID = Scholar.DB.query(sql);
_activeEvent = event;
}
/**
* Associate an id or array of ids with the transaction set --
* for use if the ids weren't available at when begin() was called
*
* id: An id or array of ids that will be passed to
* Scholar.Notifier.trigger() on an undo or redo
**/
function setAssociatedID(id){
if (!_activeID){
throw('Cannot call setAssociatedID() with no history transaction set in progress');
}
var sql = "UPDATE transactionSets SET id=";
if (!id){
sql += '0';
}
// If array, insert hyphen-delimited string
else if (typeof id=='object'){
sql += "'" + id.join('-') + "'"
}
else {
sql += id;
}
sql += " WHERE transactionSetID=" + _activeID;
Scholar.DB.query(sql);
}
/**
* Add an add transaction to the current set
*
* Can be called before or after an INSERT statement
*
* key is a hyphen-delimited list of columns identifying the row
* e.g. 'itemID-creatorID'
*
* keyValues is a hyphen-delimited list of values matching the key parts
* e.g. '1-1'
**/
function add(table, key, keyValues){
return _addTransaction('add', table, key, keyValues);
}
/**
* Add a modify transaction to the current set
*
* Must be called before an UPDATE statement
*
* key is a hyphen-delimited list of columns identifying the row
* e.g. 'itemID-creatorID'
*
* keyValues is an array or hyphen-delimited string of values matching
* the key parts (e.g. [1, 1] or '1-1')
*
* _field_ is optional -- otherwise all fields are saved
**/
function modify(table, key, keyValues, field){
return _addTransaction('modify', table, key, keyValues, field);
}
/**
* Add a remove transaction to the current set
*
* Must be called before a DELETE statement
*
* key is a hyphen-delimited list of columns identifying the row
* e.g. 'itemID-creatorID'
*
* keyValues is a hyphen-delimited list of values matching the key parts
* e.g. '1-1'
**/
function remove(table, key, keyValues){
return _addTransaction('remove', table, key, keyValues);
}
/**
* Commit the current transaction set
**/
function commit(){
Scholar.debug('Committing history transaction set ' + _activeEvent);
Scholar.DB.commitTransaction();
_currentID = _activeID;
_maxID = _activeID;
_activeID = null;
_activeEvent = null;
}
/**
* Cancel the current transaction set
**/
function cancel(){
Scholar.debug('Cancelling history transaction set ' + _activeEvent);
Scholar.DB.rollbackTransaction();
_activeID = null;
_activeEvent = null;
}
/**
* Get the next event to undo, or false if none
**/
function getPreviousEvent(){
if (!_currentID){
return false;
}
var sql = "SELECT event FROM transactionSets WHERE transactionSetID="
+ _currentID;
return Scholar.DB.valueQuery(sql);
}
/**
* Get the next event to redo, or false if none
**/
function getNextEvent(){
var sql = "SELECT event FROM transactionSets WHERE transactionSetID="
+ (_currentID + 1);
return Scholar.DB.valueQuery(sql);
}
/**
* Undo the last transaction set
**/
function undo(){
if (!_currentID){
throw('No transaction set to undo');
return false;
}
var id = _currentID;
Scholar.debug('Undoing transaction set ' + id);
Scholar.DB.beginTransaction();
var undone = _do('undo');
_currentID--;
Scholar.DB.commitTransaction();
_reloadAndNotify(id);
return true;
}
/**
* Redo the next transaction set
**/
function redo(){
var id = _currentID + 1;
Scholar.debug('Redoing transaction set ' + id);
Scholar.DB.beginTransaction();
var redone = _do('redo');
_currentID++;
Scholar.DB.commitTransaction();
_reloadAndNotify(id, true);
return redone;
}
/**
* Clear the entire history
**/
function clear(){
Scholar.DB.beginTransaction();
Scholar.DB.query("DELETE FROM transactionSets");
Scholar.DB.query("DELETE FROM transactions");
Scholar.DB.query("DELETE FROM transactionLog");
_currentID = null;
_activeID = null;
_activeEvent = null;
_maxID = null;
Scholar.DB.commitTransaction();
}
/**
* Clear all transactions in history after the current one
**/
function clearAfter(){
Scholar.DB.beginTransaction();
var min = Scholar.DB.valueQuery("SELECT MIN(transactionID) FROM "
+ "transactions WHERE transactionSetID=" + (_currentID + 1));
if (!min){
Scholar.DB.commitTransaction();
return;
}
Scholar.DB.query("DELETE FROM transactionLog "
+ "WHERE transactionID>=" + min);
Scholar.DB.query("DELETE FROM transactions "
+ "WHERE transactionID>=" + min);
Scholar.DB.query("DELETE FROM transactionSets "
+ "WHERE transactionSetID>" + _currentID);
_maxID = _currentID;
_activeID = null;
Scholar.DB.commitTransaction();
return;
}
//
// Private methods
//
function _addTransaction(action, table, key, keyValues, field){
if (!_activeID){
throw('Cannot add history transaction with no transaction set in progress');
}
if (typeof keyValues == 'object'){
keyValues = keyValues.join('-');
}
var contextString = table + '.' + key + '.' + keyValues;
var context = _parseContext(contextString);
var fromClause = _contextToSQLFrom(context);
var sql = "INSERT INTO transactions (transactionSetID, context, action) "
+ "VALUES (" + _activeID + ", '" + contextString
+ "', '" + action + "')";
var transactionID = Scholar.DB.query(sql);
switch (action){
case 'add':
// No need to store an add, since we'll just delete it to reverse
break;
case 'modify':
// Only save one field -- _do() won't know about this, but the
// UPDATE statements on the other fields just won't do anything
if (field){
var sql = "INSERT INTO transactionLog SELECT " + transactionID
+ ", '" + field + "', " + field + fromClause;
Scholar.DB.query(sql);
break;
}
// Fall through if no field specified and save all
case 'remove':
var cols = Scholar.DB.getColumns(table);
for (var i in cols){
// If column is not part of the key, log it
if (!Scholar.inArray(cols[i], context['keys'])){
var sql = "INSERT INTO transactionLog "
+ "SELECT " + transactionID + ", '" + cols[i]
+ "', " + cols[i] + fromClause;
Scholar.DB.query(sql);
}
}
break;
default:
Scholar.DB.rollbackTransaction();
throw("Invalid history action '" + action + "'");
}
}
function _do(mode){
switch (mode){
case 'undo':
var id = _currentID;
break;
case 'redo':
var id = _currentID + 1;
break;
}
var sql = "SELECT transactionID, context, action FROM transactions "
+ "WHERE transactionSetID=" + id;
var transactions = Scholar.DB.query(sql);
if (!transactions){
throw('Transaction set not found for '
+ (mode=='undo' ? 'current' : 'next') + id);
}
for (var i in transactions){
var transactionID = transactions[i]['transactionID'];
var context = _parseContext(transactions[i]['context']);
// If in redo mode, swap 'add' and 'remove'
if (mode=='redo'){
switch (transactions[i]['action']){
case 'add':
transactions[i]['action'] = 'remove';
break;
case 'remove':
transactions[i]['action'] = 'add';
break;
}
}
switch (transactions[i]['action']){
case 'add':
var fromClause = _contextToSQLFrom(context);
// First, store the row we're about to delete for later redo
var cols = Scholar.DB.getColumns(context['table']);
for (var i in cols){
// If column is not part of the key, log it
if (!Scholar.inArray(cols[i], context['keys'])){
var sql = "INSERT INTO transactionLog "
+ "SELECT " + transactionID + ", '" + cols[i]
+ "', " + cols[i] + fromClause;
Scholar.DB.query(sql);
}
}
// And delete the row
var sql = "DELETE" + fromClause;
Scholar.DB.query(sql);
break;
case 'modify':
// Retrieve old values
var sql = "SELECT field, value FROM transactionLog "
+ "WHERE transactionID=" + transactionID;
var oldFieldValues = Scholar.DB.query(sql);
// Retrieve new values
var sql = "SELECT *" + _contextToSQLFrom(context);
var newValues = Scholar.DB.rowQuery(sql);
// Update row with old values
var sql = "UPDATE " + context['table'] + " SET ";
var values = [];
for (var i in oldFieldValues){
sql += oldFieldValues[i]['field'] + '=?, ';
values.push(oldFieldValues[i]['value']);
}
sql = sql.substr(0, sql.length-2) + _contextToSQLWhere(context);
Scholar.DB.query(sql, values);
// Update log with new values for later redo
for (var i in newValues){
if (!Scholar.inArray(i, context['keys'])){
var sql = "UPDATE transactionLog SET "
+ "value=? WHERE transactionID=? AND field=?";
Scholar.DB.query(sql, [i, newValues[i], transactionID]);
}
}
break;
case 'remove':
// Retrieve old values
var sql = "SELECT field, value FROM transactionLog "
+ "WHERE transactionID=" + transactionID;
var oldFieldValues = Scholar.DB.query(sql);
// Add key to parameters
var fields = [], values = [], marks = [];
for (var i=0; i<context['keys'].length; i++){
fields.push(context['keys'][i]);
values.push(context['values'][i]);
marks.push('?');
}
// Add other fields to parameters
for (var i in oldFieldValues){
fields.push(oldFieldValues[i]['field']);
values.push(oldFieldValues[i]['value']);
marks.push('?');
}
// Insert old values into table
var sql = "INSERT INTO " + context['table'] + "("
+ fields.join() + ") VALUES (" + marks.join() + ")";
Scholar.DB.query(sql, values);
// Delete restored data from transactionLog
var sql = "DELETE FROM transactionLog WHERE transactionID="
+ transactionID;
Scholar.DB.query(sql);
break;
}
}
}
function _parseContext(context){
var parts = context.split('.');
var parsed = {
table:parts[0],
keys:parts[1].split('-'),
values:parts[2].split('-')
}
if (parsed['keys'].length!=parsed['values'].length){
throw("Different number of keys and values in _parseContext('"
+ context + "')");
}
return parsed;
}
function _contextToSQLFrom(parsed){
return " FROM " + parsed['table'] + _contextToSQLWhere(parsed);
}
function _contextToSQLWhere(parsed){
var sql = " WHERE ";
for (var i=0; i<parsed['keys'].length; i++){
// DEBUG: type?
sql += parsed['keys'][i] + "='" + parsed['values'][i] + "' AND ";
}
return sql.substr(0, sql.length-5);
}
/**
* Get the ids associated with a particular transaction set
**/
function _getSetData(transactionSetID){
var sql = "SELECT event, id FROM transactionSets WHERE transactionSetID="
+ transactionSetID;
return Scholar.DB.rowQuery(sql);
}
function _reloadAndNotify(transactionSetID, redo){
var data = _getSetData(transactionSetID);
var eventParts = data['event'].split('-'); // e.g. modify-item
if (redo){
switch (eventParts[0]){
case 'add':
eventParts[0] = 'remove';
break;
case 'remove':
eventParts[0] = 'add';
break;
}
}
switch (eventParts[1]){
case 'item':
Scholar.Items.reload(data['id']);
break;
}
Scholar.Notifier.trigger(eventParts[0], eventParts[1], data['id']);
}
}