/*******************************************************************************
* Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com)
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser Public License v3
* which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt
******************************************************************************/
package com.opendoorlogistics.core.tables.decorators.datastores.undoredo;
import java.util.HashSet;
import java.util.concurrent.Callable;
import com.opendoorlogistics.api.tables.HasUndoStateListeners;
import com.opendoorlogistics.api.tables.ODLColumnType;
import com.opendoorlogistics.api.tables.ODLDatastore;
import com.opendoorlogistics.api.tables.ODLDatastoreAlterable;
import com.opendoorlogistics.api.tables.ODLDatastoreUndoable;
import com.opendoorlogistics.api.tables.ODLTable;
import com.opendoorlogistics.api.tables.ODLTableAlterable;
import com.opendoorlogistics.api.tables.ODLTableDefinition;
import com.opendoorlogistics.api.tables.ODLTableReadOnly;
import com.opendoorlogistics.core.tables.commands.Command;
import com.opendoorlogistics.core.tables.commands.CreateTable;
import com.opendoorlogistics.core.tables.commands.DeleteEmptyCol;
import com.opendoorlogistics.core.tables.commands.DeleteEmptyTable;
import com.opendoorlogistics.core.tables.commands.DeleteEmptyRow;
import com.opendoorlogistics.core.tables.commands.InsertEmptyCol;
import com.opendoorlogistics.core.tables.commands.InsertEmptyRow;
import com.opendoorlogistics.core.tables.commands.Set;
import com.opendoorlogistics.core.tables.commands.SetByRowId;
import com.opendoorlogistics.core.tables.commands.SetColumnProperty;
import com.opendoorlogistics.core.tables.commands.SetColumnProperty.PropertyType;
import com.opendoorlogistics.core.tables.commands.SetRowFlags;
import com.opendoorlogistics.core.tables.commands.SetTableName;
import com.opendoorlogistics.core.tables.commands.SetTableProperty;
import com.opendoorlogistics.core.tables.decorators.datastores.SimpleDecorator;
import com.opendoorlogistics.core.tables.utils.ExampleData;
import com.opendoorlogistics.core.tables.utils.TableUtils;
final public class UndoRedoDecorator<T extends ODLTableDefinition> extends SimpleDecorator<T>implements ODLDatastoreUndoable<T> {
private static final long DEFAULT_MAX_BUFFER_SIZE_BYTES = 1024 * 1024 * 256;
private static final long serialVersionUID = -3961824291406998570L;
private final UndoRedoBuffer buffer = new UndoRedoBuffer();
private final long maxBufferSizeBytes;
private long nextTransactionNb = 1;
private long currentTransactionNb = -1;
private long position;
private long trimCount;
private HashSet<UndoStateChangedListener<T>> undoStateListeners = new HashSet<>();
private UndoState lastFiredUndoState;
private static class UndoState {
boolean hasUndo;
boolean hasRedo;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (hasRedo ? 1231 : 1237);
result = prime * result + (hasUndo ? 1231 : 1237);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
UndoState other = (UndoState) obj;
if (hasRedo != other.hasRedo)
return false;
if (hasUndo != other.hasUndo)
return false;
return true;
}
}
public UndoRedoDecorator(Class<T> tableClass, ODLDatastore<? extends T> decorated) {
this(tableClass, decorated, DEFAULT_MAX_BUFFER_SIZE_BYTES);
}
public UndoRedoDecorator(Class<T> tableClass, ODLDatastore<? extends T> decorated, long maxBufferSizeBytes) {
super(tableClass, decorated);
this.maxBufferSizeBytes = maxBufferSizeBytes;
}
private long getNextTransactionNb() {
if (nextTransactionNb == Long.MAX_VALUE) {
nextTransactionNb = 1;
} else {
nextTransactionNb++;
}
return nextTransactionNb;
}
@Override
public void undo() {
checkNotInTransaction();
if (hasUndo()) {
disableListeners();
buffer.get(--position).undo(decorated);
enableListeners();
fireUndoStateListeners();
}
}
@Override
public void redo() {
checkNotInTransaction();
if (hasRedo()) {
disableListeners();
buffer.get(position++).redo(decorated);
enableListeners();
fireUndoStateListeners();
}
}
@Override
public boolean hasRedo() {
return position < buffer.size();
}
@Override
public boolean hasUndo() {
return position > 0;
}
private void trim() {
checkNotInTransaction();
if (buffer.size() < 2 || buffer.sizeInBytes() < maxBufferSizeBytes) {
return;
}
// Keep deleting whilst (a) the buffer still has a couple of undo/redos, (b) we're above the target size
// and (c) the current undo / redo position is not too close to the buffer start
long targetSize = maxBufferSizeBytes / 2;
boolean didTrim = false;
while (buffer.size() >= 2 && buffer.sizeInBytes() > targetSize && position > 5) {
buffer.removeTransaction(0);
position--;
didTrim = true;
}
if (didTrim) {
trimCount++;
}
fireUndoStateListeners();
}
public void clearRedos() {
while (hasRedo()) {
buffer.removeTransaction(buffer.size() - 1);
}
fireUndoStateListeners();
}
@Override
public void startTransaction() {
if (currentTransactionNb != -1) {
throw new RuntimeException("Datastore is already in a transaction");
}
decorated.disableListeners();
checkNotInTransaction();
trim();
currentTransactionNb = getNextTransactionNb();
}
@Override
public void endTransaction() {
if (currentTransactionNb == -1) {
throw new RuntimeException();
}
currentTransactionNb = -1;
decorated.enableListeners();
}
private void checkNotInTransaction() {
if (isInTransaction()) {
throw new RuntimeException();
}
}
@Override
public boolean isInTransaction() {
return currentTransactionNb != -1;
}
@Override
public int getTableCount() {
return decorated.getTableCount();
}
private Command doCommand(Command command) {
if (isInTransaction() == false) {
trim();
}
clearRedos();
// if undo is null then the command was not performed
Command undo = command.doCommand(decorated);
if (undo != null) {
if (buffer.addUndoRedo(new UndoRedo(undo, command, currentTransactionNb))) {
position++;
}
}
fireUndoStateListeners();
return undo;
}
@Override
public void setValueAt(int id, Object aValue, int rowIndex, int columnIndex) {
doCommand(new Set(id, rowIndex, columnIndex, aValue));
}
@Override
public void setValueById(int tableId, Object aValue, long rowId, int columnIndex) {
doCommand(new SetByRowId(tableId, rowId, columnIndex, aValue));
}
@Override
public int createEmptyRow(int tableId, long rowId) {
doCommand(new InsertEmptyRow(tableId, ((ODLTableReadOnly) decorated.getTableByImmutableId(tableId)).getRowCount(), rowId));
return ((ODLTableReadOnly) decorated.getTableByImmutableId(tableId)).getRowCount() - 1;
}
@Override
public void insertEmptyRow(int tableId, int insertAtRowNb, long rowId) {
doCommand(new InsertEmptyRow(tableId, insertAtRowNb, rowId));
}
@Override
public int addColumn(int tableId, int id, String name, ODLColumnType type, long flags) {
DeleteEmptyCol undo = (DeleteEmptyCol) doCommand(new InsertEmptyCol(tableId, id, decorated.getTableByImmutableId(tableId).getColumnCount(), name, type, flags, false));
if (undo == null) {
return -1;
}
return undo.getColumnIndex();
}
@Override
public boolean insertCol(int tableId, int id, int col, String name, ODLColumnType type, long flags, boolean allowDuplicateNames) {
return doCommand(new InsertEmptyCol(tableId, id, col, name, type, flags, allowDuplicateNames)) != null;
}
@Override
public String toString() {
return decorated.toString();
}
@Override
public T createTable(String tablename, int id) {
Command undo = doCommand(new CreateTable(tablename, id, 0));
if (undo != null) {
return getTableByImmutableId(undo.getTableId());
}
return null;
}
@Override
public void deleteRow(int tableId, int rowNumber) {
TableUtils.runTransaction(this, new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
ODLTable table = (ODLTable) decorated.getTableByImmutableId(tableId);
if (table != null) {
// blank values first
int nbCol = table.getColumnCount();
for (int col = 0; col < nbCol; col++) {
setValueAt(tableId, null, rowNumber, col);
}
// blank row flags
long rowid = table.getRowId(rowNumber);
setRowFlags(tableId, 0, rowid);
// finally delete the row
doCommand(new DeleteEmptyRow(tableId, rowNumber));
}
return true;
}
}, null, true);
}
@Override
public void deleteCol(int tableId, int col) {
TableUtils.runTransaction(this, new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
ODLTable table = (ODLTable) decorated.getTableByImmutableId(tableId);
if (table != null) {
// blank all values first
int nbRows = table.getRowCount();
for (int i = 0; i < nbRows; i++) {
setValueAt(tableId, null, i, col);
}
// blank column properties which can't be recreated within the undo's insertcolumn command,
// so we need a separate commands for them
doCommand(new SetColumnProperty(tableId, col, PropertyType.DESCRIPTION, null));
doCommand(new SetColumnProperty(tableId, col, PropertyType.DEFAULT_VALUE, null));
doCommand(new SetColumnProperty(tableId, col, PropertyType.TAGS, null));
// then delete empty col
doCommand(new DeleteEmptyCol(tableId, col));
}
return true;
}
}, null, true);
}
@Override
public void setColumnFlags(int tableId, int col, long flags) {
doCommand(new SetColumnProperty(tableId, col, SetColumnProperty.PropertyType.FLAGS, flags));
}
@Override
public void setRowFlags(int tableId, long flags, long rowId) {
doCommand(new SetRowFlags(tableId, rowId, flags));
}
@Override
public void setFlags(int tableId, long flags) {
doCommand(new SetTableProperty(tableId, SetTableProperty.PropertyType.FLAGS, flags));
}
@Override
public boolean setTableName(int tableId, String newName) {
return doCommand(new SetTableName(tableId, newName)) != null;
}
@Override
public void deleteTableById(int tableId) {
TableUtils.runTransaction(this, new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
ODLTable table = (ODLTable) decorated.getTableByImmutableId(tableId);
if (table != null) {
// delete all data rows first (i.e. the data)
while (table.getRowCount() > 0) {
deleteRow(tableId, table.getRowCount() - 1);
}
// then delete all columns (i.e. the table structure)
while (table.getColumnCount() > 0) {
deleteCol(tableId, table.getColumnCount() - 1);
}
doCommand(new SetTableProperty(tableId, SetTableProperty.PropertyType.TAGS, null));
// finally delete table itself
doCommand(new DeleteEmptyTable(tableId));
}
return true;
}
}, null, true);
}
@Override
public void rollbackTransaction() {
// save the current transaction nunmber as endTransaction resets it
long transactionNb = currentTransactionNb;
endTransaction();
if (position > 0 && buffer.get(position - 1).transactionNb == transactionNb) {
undo();
}
fireUndoStateListeners();
}
@Override
public void setColumnTags(int tableId, int col, java.util.Set<String> tags) {
doCommand(new SetColumnProperty(tableId, col, SetColumnProperty.PropertyType.TAGS, tags));
}
@Override
public void setTags(int tableId, java.util.Set<String> tags) {
doCommand(new SetTableProperty(tableId, SetTableProperty.PropertyType.TAGS, tags));
}
@Override
public void setColumnDefaultValue(int tableId, int col, Object value) {
doCommand(new SetColumnProperty(tableId, col, SetColumnProperty.PropertyType.DEFAULT_VALUE, value));
}
@Override
public void setColumnDescription(int tableId, int col, String description) {
doCommand(new SetColumnProperty(tableId, col, SetColumnProperty.PropertyType.DESCRIPTION, description));
}
@Override
public void addUndoStateListener(HasUndoStateListeners.UndoStateChangedListener<T> listener) {
undoStateListeners.add(listener);
}
@Override
public void removeUndoStateListener(HasUndoStateListeners.UndoStateChangedListener<T> listener) {
undoStateListeners.remove(listener);
}
/**
* Checks if a change to the hasUndo / hasRedo state has occurred and fires listeners if so.
*/
private void fireUndoStateListeners() {
UndoState state = new UndoState();
state.hasRedo = hasRedo();
state.hasUndo = hasUndo();
if (lastFiredUndoState == null || lastFiredUndoState.equals(state) == false) {
lastFiredUndoState = state;
for (UndoStateChangedListener<T> listener : undoStateListeners) {
listener.undoStateChanged(this);
}
}
}
@Override
public boolean isRollbackSupported() {
return true;
}
public long getTrimCount() {
return trimCount;
}
public long getEstimatedBufferSizeInBytes() {
return buffer.sizeInBytes();
}
@Override
public void clearUndoBuffer() {
position = 0;
buffer.clear();
}
public static void main(String[] args) {
ODLDatastoreAlterable<ODLTableAlterable> ds = ExampleData.createTerritoriesExample(1);
UndoRedoDecorator<ODLTableAlterable> undoRedo = new UndoRedoDecorator<>(ODLTableAlterable.class, ds);
System.out.println("Created datastore");
System.out.println(undoRedo);
// delete all columns
ODLTableAlterable table = undoRedo.getTableAt(0);
while (table.getColumnCount() > 0) {
table.deleteColumn(0);
}
System.out.println("Removed all columns");
System.out.println(undoRedo);
// undo all
while (undoRedo.hasUndo()) {
undoRedo.undo();
System.out.println("Re-added column");
System.out.println(undoRedo);
}
System.out.println("Undone remove all columns");
System.out.println(undoRedo);
// add new columns
int nbNew = 3;
for (int i = 0; i < nbNew; i++) {
table.addColumn(-1, "New col" + i, ODLColumnType.STRING, 0);
}
System.out.println("Added new columns");
System.out.println(undoRedo);
// remove new columns
for (int i = 0; i < nbNew; i++) {
undoRedo.undo();
}
System.out.println("Removed new columns");
System.out.println(undoRedo);
// re-add new columns by redo
for (int i = 0; i < nbNew; i++) {
undoRedo.redo();
}
System.out.println("Readded new columns by redo");
System.out.println(undoRedo);
}
}