/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jkiss.dbeaver.ui.editors.binary;
import org.jkiss.dbeaver.ui.editors.binary.BinaryContent.Range;
import org.jkiss.dbeaver.utils.ContentUtils;
import java.io.Closeable;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* Keeps track of actions performed on a BinaryContent so they can be undone and redone.
* Actions can be single or block deletes, inserts or overwrites.
* Consecutive single actions are merged into a block action if they are of the same type, their data
* is contiguous, and are performed with a time difference lower than MERGE_TIME.
* Block actions are sequences of BinaryContent.Range. Single actions are one range of size 1.
*
* @author Jordi
*/
public class ActionHistory {
static enum ActionType {
DELETE,
INSERT,
OVERWRITE
}
/**
* Waiting time before a single action is considered separate from the previous one.
* Current value is 1500 milliseconds.
*/
private static final int MERGE_TIME = 1500; // milliseconds
private BinaryContent.Range actionLastRange = null;
private BinaryContent content = null;
private List<Integer> deletedList = null; // of Integers
private boolean isBackspace = false;
private List<Object[]> actionList = null; // contains ArrayLists (from currentAction)
private int actionsIndex = 0;
private List<Range> currentAction = null; // contains Ranges
private ActionType currentActionType = null;
private long mergedSinglesTop = -1L;
private boolean mergingSingles = false;
private long previousTime = 0L;
private long newRangeLength = -1L;
private long newRangePosition = -1L;
/**
* Create new action history storage object
*/
ActionHistory(BinaryContent aContent)
{
if (aContent == null)
throw new NullPointerException("null content");
content = aContent;
actionList = new ArrayList<>();
}
private long actionExclusiveEnd()
{
long result = 0L;
if (currentAction != null && currentAction.size() > 0) {
BinaryContent.Range highest =
currentAction.get(currentAction.size() - 1);
result = highest.exclusiveEnd();
}
long newRangeExclusiveEnd = newRangePosition + newRangeLength;
if (newRangeExclusiveEnd > result)
result = newRangeExclusiveEnd;
return result;
}
private long actionPosition()
{
long result = -1L;
if (currentAction != null && currentAction.size() > 0) {
BinaryContent.Range lowest = currentAction.get(0);
result = lowest.position;
}
if (result < 0 || newRangePosition >= 0 && newRangePosition < result)
result = newRangePosition;
return result;
}
/**
* Adds a list of deleted integers to the current action. If possible, merges integerList with the list
* in the previous call to this method.
*
* @param position starting delete point
* @param integerList deleted integers
* @param isSingle used when integerList.size == 1 to tell whether it is a single or a piece of a block
* delete. When integerList.size() > 1 (a block delete for sure) isSingle is ignored.
*/
void addDeleted(long position, List<Integer> integerList, boolean isSingle)
{
if (integerList.size() > 1L || !isSingle) { // block delete
BinaryContent.Range range = newRangeFromIntegerList(position, integerList);
List<Range> oneElementList = new ArrayList<>();
oneElementList.add(range);
addLostRanges(oneElementList);
} else {
addLostByte(position, integerList.get(0));
}
previousTime = System.currentTimeMillis();
}
void addLostByte(long position, Integer integer)
{
if (deletedList == null)
deletedList = new ArrayList<>();
updateNewRange(position);
if (isBackspace) {
deletedList.add(0, integer);
} else { // delete(Del) or overwrite
deletedList.add(integer);
}
previousTime = System.currentTimeMillis();
}
void addLostRange(BinaryContent.Range aRange)
{
if (mergingSingles) {
if (mergedSinglesTop < 0L) {
mergedSinglesTop = aRange.exclusiveEnd();
// merging singles shifts aRange
} else if (currentActionType == ActionType.DELETE && !isBackspace) {
aRange.position = mergedSinglesTop++;
}
previousTime = System.currentTimeMillis();
}
mergeRange(aRange);
}
void addLostRanges(java.util.List<Range> ranges)
{
if (ranges == null)
return;
for (Range range : ranges) {
addLostRange(range);
}
}
void addRangeToCurrentAction(Range aRange)
{
if (actionPosition() <= aRange.position) { // they're == when ending an overwrite action
currentAction.add(aRange);
} else {
currentAction.add(0, aRange);
}
actionLastRange = aRange;
}
/**
* Adds an inserted range to a new action. Does not merge Ranges nor single actions.
*
* @param aRange the range being inserted
*/
void addInserted(BinaryContent.Range aRange)
{
currentAction.add(aRange);
endAction();
}
/**
* Tells whether a redo is possible
*
* @return true if something can be redone
*/
public boolean canRedo()
{
return actionsIndex < actionList.size() && currentAction == null;
}
/**
* Tells whether an undo is possible
*
* @return true if something can be undone
*/
public boolean canUndo()
{
return currentAction != null || actionsIndex > 0;
}
void dispose()
{
if (actionList != null) {
for (Object[] tuple : actionList) {
@SuppressWarnings("unchecked")
List<Range> ranges = (List<Range>) tuple[1];
disposeRanges(ranges);
}
actionList = null;
}
if (currentAction != null) {
disposeRanges(currentAction);
currentAction = null;
}
}
private void disposeRanges(java.util.List<Range> ranges)
{
if (ranges == null) {
return;
}
for (Range range : ranges) {
if (range.data instanceof Closeable) {
ContentUtils.close((Closeable) range.data);
}
}
}
/**
* Sets the last processed action as finished. Calling this method will prevent single action merging.
* Must be called after each block action.
*/
void endAction()
{
if (currentAction == null) return;
if (mergingSingles)
newRangeToCurrentAction();
Object[] tuple = {currentActionType, currentAction};
actionList.subList(actionsIndex, actionList.size()).clear();
actionList.add(tuple);
actionsIndex = actionList.size();
isBackspace = false;
currentActionType = null;
currentAction = null;
actionLastRange = null;
newRangePosition = -1L;
newRangeLength = -1L;
mergedSinglesTop = -1L;
}
/**
* User event: single/block delete/insert/overwrite. Called before any change has been done
*
* @param type
* @param position
*/
void eventPreModify(ActionType type, long position, boolean isSingle)
{
if (type != currentActionType ||
!isSingle ||
System.currentTimeMillis() - previousTime > MERGE_TIME ||
(type == ActionType.INSERT || type == ActionType.OVERWRITE) && actionExclusiveEnd() != position ||
type == ActionType.DELETE && actionPosition() != position && actionPosition() - 1L != position) {
startAction(type, isSingle);
} else {
isBackspace = actionPosition() > position;
}
if (isSingle && type == ActionType.INSERT) { // never calls addInserted...
updateNewRange(position);
previousTime = System.currentTimeMillis();
}
}
/**
* Closes all files for termination
*
* @see Object#finalize()
*/
@Override
protected void finalize() throws Throwable
{
dispose();
super.finalize();
}
private void mergeRange(BinaryContent.Range aRange)
{
if (actionLastRange == null || actionLastRange.data != aRange.data) {
newRangeToCurrentAction();
addRangeToCurrentAction(aRange);
} else {
if (actionLastRange.compareTo(aRange) > 0) {
actionLastRange.position -= aRange.length;
actionLastRange.dataOffset -= aRange.length;
newRangePosition = aRange.position;
}
actionLastRange.length += aRange.length;
}
if (currentActionType == ActionType.OVERWRITE && mergingSingles) {
if (newRangePosition < 0L) {
newRangePosition = aRange.position;
newRangeLength = 1L;
} else {
++newRangeLength;
}
}
}
private ByteBuffer newBufferFromIntegerList(List<Integer> integerList)
{
ByteBuffer store = ByteBuffer.allocate(integerList.size());
for (Integer anIntegerList : integerList) {
store.put(anIntegerList.byteValue());
}
store.position(0);
return store;
}
private BinaryContent.Range newRangeFromIntegerList(long position, List<Integer> integerList)
{
ByteBuffer store = newBufferFromIntegerList(integerList);
return new BinaryContent.Range(position, store, true);
}
private void newRangeToCurrentAction()
{
BinaryContent.Range newRange;
if (currentActionType == ActionType.DELETE) {
if (deletedList == null)
return;
newRange = newRangeFromIntegerList(newRangePosition, deletedList);
deletedList = null;
} else { // currentActionType == INSERT || currentActionType == OVERWRITE
if (newRangePosition < 0L)
return;
content.actionsOn(false);
content.commitChanges();
content.actionsOn(true);
newRange = (BinaryContent.Range) content.getRangeAt(newRangePosition).clone();
}
addRangeToCurrentAction(newRange);
}
/**
* Redoes last action on BinaryContent.
*/
Object[] redoAction()
{
if (!canRedo()) return null;
return actionList.get(actionsIndex++);
}
/**
* Starts the processing of a new action.
*
* @param type one of DELETE, INSERT or OVERWRITE
* @param isSingle whether the action is a single byte or more
*/
private void startAction(ActionType type, boolean isSingle)
{
endAction();
currentAction = new ArrayList<>();
currentActionType = type;
mergingSingles = isSingle;
}
public String toString()
{
return actionList.toString();
}
/**
* Undoes last action on BinaryContent.
*/
Object[] undoAction()
{
if (!canUndo()) return null;
endAction();
--actionsIndex;
return actionList.get(actionsIndex);
}
private void updateNewRange(long position)
{
if (newRangePosition < 0L) {
newRangePosition = position;
newRangeLength = 1L;
} else {
if (newRangePosition > position) { // Backspace (BS)
newRangePosition = position;
}
++newRangeLength;
}
}
}