/**
* Copyright 2004-2016 Riccardo Solmi. All rights reserved.
* This file is part of the Whole Platform.
*
* The Whole Platform is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Whole Platform 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the Whole Platform. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whole.lang.lifecycle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import org.whole.lang.commands.BooleanChangeCommand;
import org.whole.lang.commands.ByteChangeCommand;
import org.whole.lang.commands.CharChangeCommand;
import org.whole.lang.commands.CommandKind;
import org.whole.lang.commands.CompoundCommand;
import org.whole.lang.commands.DateChangeCommand;
import org.whole.lang.commands.DoubleChangeCommand;
import org.whole.lang.commands.EntityAddCommand;
import org.whole.lang.commands.EntityChangeCommand;
import org.whole.lang.commands.EntityRemoveCommand;
import org.whole.lang.commands.EntitySetCommand;
import org.whole.lang.commands.EnumValueChangeCommand;
import org.whole.lang.commands.FloatChangeCommand;
import org.whole.lang.commands.ICommand;
import org.whole.lang.commands.IntChangeCommand;
import org.whole.lang.commands.LongChangeCommand;
import org.whole.lang.commands.NullCommand;
import org.whole.lang.commands.ObjectChangeCommand;
import org.whole.lang.commands.ShortChangeCommand;
import org.whole.lang.commands.StringChangeCommand;
import org.whole.lang.events.IdentityChangeEventHandler;
import org.whole.lang.model.EnumValue;
import org.whole.lang.model.ICompoundModel;
import org.whole.lang.model.IEntity;
import org.whole.lang.reflect.FeatureDescriptor;
/**
* @author Riccardo Solmi
*/
public class HistoryManager extends IdentityChangeEventHandler implements IHistoryManager {
private static final long serialVersionUID = 1L;
public HistoryManager() {
transactionBegin[0] = -1;
transactionStatus[0] = Status.NO_TRANSACTION;
}
private static final int initial_history_capacity = 1024;
private List<ICommand> history = new ArrayList<ICommand>(initial_history_capacity);
private int firstExecutedIndex = 0;
private int lastExecutedIndex = -1;
private static final int initial_nesting_capacity = 2;
private int[] transactionBegin = new int[initial_nesting_capacity];
private Status[] transactionStatus = new Status[initial_nesting_capacity];
private int transactionLevel;
protected void ensureNestingCapacity(int minCapacity) {
int oldCapacity = transactionBegin.length;
if (minCapacity > oldCapacity) {
int newCapacity = (oldCapacity * 3) / 2 + 1;
if (newCapacity < minCapacity)
newCapacity = minCapacity;
int[] oldData = transactionBegin;
transactionBegin = new int[newCapacity];
System.arraycopy(oldData, 0, transactionBegin, 0, transactionLevel+1);
Status[] oldStatus = transactionStatus;
transactionStatus = new Status[newCapacity];
System.arraycopy(oldStatus, 0, transactionStatus, 0, transactionLevel+1);
}
}
public Status getStatus() {
return transactionStatus[transactionLevel];
}
private void setStatus(Status status) {
transactionStatus[transactionLevel] = status;
}
public void begin() {
ensureNestingCapacity(++transactionLevel +1);
transactionBegin[transactionLevel] = lastExecutedIndex+1;
setStatus(Status.ACTIVE);
}
public ICommand commit() {
if (getStatus() == Status.MARKED_ROLLBACK) {
rollback();
throw new RollbackException();
} else if (getStatus() == Status.NO_TRANSACTION)
throw new IllegalStateException();
else
return compound(transactionBegin[transactionLevel--]);
}
public ICommand mergeCommit(ICommand command) {
if (getStatus() == Status.MARKED_ROLLBACK) {
rollback();
throw new RollbackException();
} else if (getStatus() == Status.NO_TRANSACTION)
throw new IllegalStateException();
else {
int index = transactionBegin[transactionLevel--];
if (index>0 && history.get(index-1) == command)
//TODO append to command
return compound(index-1);
else
return compound(index);
}
}
protected CompoundCommand compound(int beginIndex) {
CompoundCommand command = null;
int size = lastExecutedIndex+1 - beginIndex;
if (size > 0) {
List<ICommand> subList = history.subList(beginIndex, lastExecutedIndex+1);
command = new CompoundCommand(nextExecutionTime(),
reduce(subList).toArray(new ICommand[0]));
subList.clear();
addCommand(command);
}
return command;
}
//TODO called by model to delta Stream if changes.size > model.size
protected List<ICommand> reduce(List<ICommand> commands) {
if (commands.isEmpty())
return commands;
List<ICommand> list = new ArrayList<ICommand>();
for (int i=0, size=commands.size(); i<size; i++) {
ICommand command = commands.get(i);
if (CommandKind.COMPOUND == command.getKind()) {
for (ICommand child : ((CompoundCommand) command).commands)
list.add(child);
} else
list.add(command);
}
int index = list.size()-1;
while (index > 0) {
ICommand lastCommand = list.get(index--);
if (CommandKind.CHANGE == lastCommand.getKind()) {
IEntity lastSource = lastCommand.getSource();
FeatureDescriptor lastSourceFD = lastCommand.getSourceFeatureDescriptor();
int firstIndex = index;
while (firstIndex >= 0) {
ICommand firstCommand = list.get(firstIndex);
if (CommandKind.CHANGE != firstCommand.getKind() ||
firstCommand.getSource() != lastSource ||
firstCommand.getSourceFeatureDescriptor() != lastSourceFD)
break;
else
firstIndex--;
}
if (firstIndex < index) {
if (lastSourceFD == null)
lastCommand.setOldObject(list.get(firstIndex+1).getOldObject());
else
lastCommand.setOldEntity(list.get(firstIndex+1).getOldEntity());
for (int i=firstIndex+1; i<=index; i++)
list.remove(firstIndex+1).dispose();
index = firstIndex;
}
}
}
return list;
// iterate over model tree to exclude overridden commands
// for each command discard overridden prevCommands
}
public void rollback() {
if (getStatus() == Status.NO_TRANSACTION)
throw new IllegalStateException();
discard(transactionBegin[transactionLevel--]);
}
protected void discard(int beginIndex) {
disposeRedoCommands();
boolean isEnabled = setHistoryEnabled(false);
setHistoryEvent(true);
while (lastExecutedIndex >= beginIndex) {
ICommand command = history.remove(lastExecutedIndex--);
command.undo();
command.dispose();
}
setHistoryEvent(false);
setHistoryEnabled(isEnabled, true);
}
public void setRollbackOnly() {
if (getStatus() == Status.NO_TRANSACTION)
throw new IllegalStateException();
setStatus(Status.MARKED_ROLLBACK);
}
private boolean historyEvent = false;
public boolean isHistoryEvent() {
return historyEvent;
}
public boolean setHistoryEvent(boolean value) {
return historyEvent = value;
}
private boolean historyEnabled = false;
public boolean isHistoryEnabled() {
return historyEnabled;
}
public boolean setHistoryEnabled(boolean value) {
return setHistoryEnabled(value, false);
}
public boolean setHistoryEnabled(boolean value, boolean keepHistory) {
boolean oldValue = historyEnabled;
historyEnabled = value;
if (!oldValue && historyEnabled && !keepHistory)
clearHistory();
return oldValue;
}
private int historyCapacity = Integer.MAX_VALUE;
public int getHistoryCapacity() {
return historyCapacity;
}
// capacity <= 0 means unlimited
public void setHistoryCapacity(int capacity) {
if (capacity <= 0)
capacity = Integer.MAX_VALUE;
safeTrimHistory(capacity);
historyCapacity = capacity;
}
public void safeTrimHistory(int size) {
int trimSize = getUndoSize() - size;
if (trimSize <= 0)
return;
if (getStatus() != Status.NO_TRANSACTION && transactionBegin[0] < firstExecutedIndex+trimSize)
trimSize = transactionBegin[0] - firstExecutedIndex;
for (int i=0; i<trimSize; i++)
history.set(firstExecutedIndex++, null).dispose();
}
public void trimHistory(int size) {
int trimSize = getUndoSize() - size;
if (getStatus() != Status.NO_TRANSACTION && transactionBegin[0] < firstExecutedIndex+trimSize)
throw new IllegalStateException();
for (int i=0; i<trimSize; i++)
history.set(firstExecutedIndex++, null).dispose();
}
public void clearHistory() {
trimHistory(0);
compactHistory();
}
protected void ensureHistoryCapacity() {
safeTrimHistory(getHistoryCapacity()-1);
if (firstExecutedIndex >= initial_history_capacity)
compactHistory();
}
protected void compactHistory() {
if (firstExecutedIndex > 0) {
history.subList(0, firstExecutedIndex).clear();
lastExecutedIndex -= firstExecutedIndex;
for (int i=1; i<=transactionLevel; i++)
transactionBegin[i] -= firstExecutedIndex;
firstExecutedIndex = 0;
}
}
public boolean equals(IHistoryManager other) {
return other instanceof ICompoundModel ? other.equals(this) : super.equals(other);
}
//assume that subHistory will be replaced by the returned one
public IHistoryManager mergeHistory(IHistoryManager subHistory) {
int size = subHistory.getUndoSize();
if (size > 0 && !subHistory.equals(this)) {
ICommand[] changes = subHistory.getUndoCommands().toArray(new ICommand[size]);
ICommand command = new CompoundCommand(nextExecutionTime(), changes);
addCommand(command);
}
return this;
}
public int getUndoSize() {
return lastExecutedIndex -firstExecutedIndex +1;
}
public int getRedoSize() {
return history.size() -lastExecutedIndex -1;
}
public List<ICommand> getUndoCommands() {
if (getUndoSize() > 0)
return Collections.unmodifiableList(
history.subList(firstExecutedIndex, lastExecutedIndex+1));
else
return Collections.emptyList();
}
public List<ICommand> getRedoCommands() {
if (getRedoSize() > 0)
return Collections.unmodifiableList(
history.subList(lastExecutedIndex+1, history.size()));
else
return Collections.emptyList();
}
public ICommand getUndoCommand() {
if (getUndoSize() < 1)
return NullCommand.instance;
else
return history.get(lastExecutedIndex);
}
public ICommand getRedoCommand() {
if (getRedoSize() < 1)
return NullCommand.instance;
else
return history.get(lastExecutedIndex+1);
}
public void undo() {
if (getUndoSize() < 1)
throw new IllegalStateException();
boolean isEnabled = setHistoryEnabled(false);
setHistoryEvent(true);
history.get(lastExecutedIndex--).undo();
setHistoryEvent(false);
setHistoryEnabled(isEnabled, true);
}
public void redo() {
if (getRedoSize() < 1)
throw new IllegalStateException();
boolean isEnabled = setHistoryEnabled(false);
setHistoryEvent(true);
history.get(++lastExecutedIndex).redo();
setHistoryEvent(false);
setHistoryEnabled(isEnabled, true);
}
protected void addCommand(ICommand command) {
disposeRedoCommands();
ensureHistoryCapacity();
lastExecutedIndex = history.size();
history.add(command);
}
private int executionTime;
protected int nextExecutionTime() {
if (executionTime == Integer.MAX_VALUE) {
executionTime = 0;
for (int i=firstExecutedIndex; i<lastExecutedIndex; i++)
history.get(i).setExecutionTime(executionTime++);
}
return executionTime++;
}
protected void disposeRedoCommands() {
for (int i=history.size()-1; i>lastExecutedIndex; i--)
history.remove(i).dispose();
}
public void notifyAdded(IEntity source, FeatureDescriptor fd, int index, IEntity newValue) {
if (isHistoryEnabled())
addCommand(new EntityAddCommand(nextExecutionTime(), source, index, newValue));
}
public void notifyRemoved(IEntity source, FeatureDescriptor fd, int index, IEntity oldValue) {
if (isHistoryEnabled())
addCommand(new EntityRemoveCommand(nextExecutionTime(), source, index, oldValue));
}
public void notifyChanged(IEntity source, FeatureDescriptor fd, int index, IEntity oldValue, IEntity newValue) {
if (isHistoryEnabled())
addCommand(new EntitySetCommand(nextExecutionTime(), source, index, oldValue, newValue));
}
public void notifyChanged(IEntity source, FeatureDescriptor fd, IEntity oldValue, IEntity newValue) {
if (isHistoryEnabled())
addCommand(new EntityChangeCommand(nextExecutionTime(), source, fd, oldValue, newValue));
}
public void notifyChanged(IEntity source, FeatureDescriptor fd, boolean oldValue, boolean newValue) {
if (isHistoryEnabled())
addCommand(new BooleanChangeCommand(nextExecutionTime(), source, oldValue, newValue));
}
public void notifyChanged(IEntity source, FeatureDescriptor fd, byte oldValue, byte newValue) {
if (isHistoryEnabled())
addCommand(new ByteChangeCommand(nextExecutionTime(), source, oldValue, newValue));
}
public void notifyChanged(IEntity source, FeatureDescriptor fd, char oldValue, char newValue) {
if (isHistoryEnabled())
addCommand(new CharChangeCommand(nextExecutionTime(), source, oldValue, newValue));
}
public void notifyChanged(IEntity source, FeatureDescriptor fd, double oldValue, double newValue) {
if (isHistoryEnabled())
addCommand(new DoubleChangeCommand(nextExecutionTime(), source, oldValue, newValue));
}
public void notifyChanged(IEntity source, FeatureDescriptor fd, float oldValue, float newValue) {
if (isHistoryEnabled())
addCommand(new FloatChangeCommand(nextExecutionTime(), source, oldValue, newValue));
}
public void notifyChanged(IEntity source, FeatureDescriptor fd, int oldValue, int newValue) {
if (isHistoryEnabled())
addCommand(new IntChangeCommand(nextExecutionTime(), source, oldValue, newValue));
}
public void notifyChanged(IEntity source, FeatureDescriptor fd, long oldValue, long newValue) {
if (isHistoryEnabled())
addCommand(new LongChangeCommand(nextExecutionTime(), source, oldValue, newValue));
}
public void notifyChanged(IEntity source, FeatureDescriptor fd, short oldValue, short newValue) {
if (isHistoryEnabled())
addCommand(new ShortChangeCommand(nextExecutionTime(), source, oldValue, newValue));
}
public void notifyChanged(IEntity source, FeatureDescriptor fd, String oldValue, String newValue) {
if (isHistoryEnabled())
addCommand(new StringChangeCommand(nextExecutionTime(), source, oldValue, newValue));
}
public void notifyChanged(IEntity source, FeatureDescriptor fd, Date oldValue, Date newValue) {
if (isHistoryEnabled())
addCommand(new DateChangeCommand(nextExecutionTime(), source, oldValue, newValue));
}
public void notifyChanged(IEntity source, FeatureDescriptor fd, EnumValue oldValue, EnumValue newValue) {
if (isHistoryEnabled())
addCommand(new EnumValueChangeCommand(nextExecutionTime(), source, oldValue, newValue));
}
public void notifyChanged(IEntity source, FeatureDescriptor fd, Object oldValue, Object newValue) {
if (isHistoryEnabled())
addCommand(new ObjectChangeCommand(nextExecutionTime(), source, oldValue, newValue));
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("History Manager (enabled=");
builder.append(isHistoryEnabled());
builder.append(" size=");
builder.append(getUndoSize());
builder.append('u');
builder.append(getRedoSize());
builder.append('r');
builder.append(" capacity=");
builder.append(getHistoryCapacity() == Integer.MAX_VALUE ? "*" : getHistoryCapacity());
builder.append(")\n[");
for (ICommand command : getUndoCommands()) {
builder.append("\n");
builder.append(command.toString());
}
builder.append("\n--");
for (ICommand command : getRedoCommands()) {
builder.append("\n");
builder.append(command.toString());
}
builder.append("\n]");
return builder.toString();
}
}