/* * 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.code.NotNull; import org.jkiss.dbeaver.utils.ContentUtils; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.util.*; /** * A binary content provider. Content backed by files has no effect on memory footprint. Content * backed by memory buffers is limited by amount of memory. Notifies ModifyListeners when it has been * modified. * Keeps track of the positions where changes have been done. Files that back this content must not be * modified while the content is still in use. * * @author Jordi */ public class BinaryContent { /** * Used to notify changes in content */ public interface ModifyListener extends EventListener { /** * Notifies the listener that the content has just been changed */ void modified(); } /** * A subset of data contained in a ByteBuffer or a File */ final static class Range implements Comparable<Range>, Cloneable { long position = -1L; long length = -1L; long dataOffset = 0L; Object data = null; private boolean dirty = true; Range(long aPosition, long aLength) { position = aPosition; length = aLength; } Range(long aPosition, ByteBuffer aBuffer, boolean isDirty) { this(aPosition, aBuffer.remaining()); data = aBuffer; dirty = isDirty; } Range(long aPosition, File aFile, boolean isDirty) throws IOException { this(aPosition, aFile.length()); if (length < 0L) throw new IOException("File error"); data = new RandomAccessFile(aFile, "r"); dirty = isDirty; } @Override public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { throw new IllegalStateException(e); } } @Override public int compareTo(@NotNull Range other) { if (position < other.position && exclusiveEnd() <= other.position) return -1; if (other.position < position && other.exclusiveEnd() <= position) return 1; return 0; // overlap } public boolean equals(Object obj) { return obj instanceof Range && compareTo((Range)obj) == 0; } long exclusiveEnd() { return position + length; } public String toString() { return "Range {position:" + position + ", length:" + length + '}'; } } private static final long mappedFileBufferLength = 2048 * 1024; // for mapped file I/O private ActionHistory actions = null; // undo/redo actions history private ActionHistory actionsTemp = null; private boolean dirty = false; private long exclusiveEnd = -1L; private long lastUpperNibblePosition = -1L; private List<ModifyListener> listeners = null; private List<Integer> changeList = null; private boolean changesInserted = false; private long changesPosition = -1L; private TreeSet<Range> ranges = new TreeSet<>(); private Iterator<Range> tailTree = null; /** * Create new empty content. */ BinaryContent() { } /** * Create new content from a file * * @param aFile the backing content provider * @throws IOException when i/o problems occur. The content will be empty but valid */ BinaryContent(File aFile) throws IOException { this(); if (aFile == null || aFile.length() < 1L) return; ranges.add(new Range(0L, aFile, false)); } void actionsOn(boolean on) { if (on) { if (actions == null) actions = actionsTemp; } else { actionsTemp = actions; actions = null; } } /** * Add a listener to the list of listeners to be notified when there is a change in the content * * @param listener to be notified of the change */ public void addModifyListener(ModifyListener listener) { if (listeners == null) listeners = new ArrayList<>(); listeners.add(listener); } /** * Tells whether a redo is possible * * @return true if something can be redone */ public boolean canRedo() { return actions != null && actions.canRedo(); } /** * Tells whether an undo is possible * * @return true if something can be undone */ public boolean canUndo() { return actions != null && actions.canUndo(); } void commitChanges() { if (changeList == null) return; ByteBuffer store = ByteBuffer.allocate(changeList.size()); for (Integer myChange : changeList) { store.put(myChange.byteValue()); } store.position(0); changeList = null; if (changesInserted) insertRange(new Range(changesPosition, store, true)); else overwriteRange(new Range(changesPosition, store, true)); changesInserted = false; changesPosition = -1L; } /** * Deletes length bytes from the content at the given position * * @param position start deletion point * @param length number of bytes to delete */ public void delete(long position, long length) { if (position < 0 || position >= length() || length < 1L) return; dirty = true; if (length > length() - position) length = length() - position; if (actions != null) { lastUpperNibblePosition = -1L; actions.eventPreModify(ActionHistory.ActionType.DELETE, position, length == 1L); } if (changeList != null && changesInserted && changesPosition <= position && changesPosition + changeList.size() >= position + length) { int deleteStart = (int) (position - changesPosition); List<Integer> subList = changeList.subList(deleteStart, deleteStart + (int) length); if (actions != null) { actions.addDeleted(position, subList, length == 1L); if (length > 1) actions.endAction(); } if (length < changeList.size()) { subList.clear(); } else { // length == changeList.size() changeList = null; // splitAndShift(position, 0); // mark them as dirty } } else { commitChanges(); deleteAndShift(position, length); } notifyListeners(); } private void deleteAndShift(long start, long length) { deleteInternal(start, length); initSubtreeTraversing(start, 0L); shiftRemainingRanges(-length); } private void deleteInternal(long startPosition, long length) { if (length < 1L) return; initSubtreeTraversing(startPosition, length); if (!tailTree.hasNext()) return; java.util.List<Range> deleted = new ArrayList<>(); Range firstRange = tailTree.next(); Range secondRange = (Range) firstRange.clone(); // will be tail part of firstRange Range lastRange = null; if (firstRange.position < startPosition) { firstRange.length = startPosition - firstRange.position; secondRange.length = secondRange.exclusiveEnd() - startPosition; // actions secondRange.dataOffset += startPosition - secondRange.position; // actions secondRange.position = startPosition; // actions } else { // firstRange.position == startPosition tailTree.remove(); } long endSoFar = secondRange.exclusiveEnd(); boolean toBeAdded = false; if (endSoFar > exclusiveEnd) { lastRange = (Range) secondRange.clone(); toBeAdded = true; secondRange.length = exclusiveEnd - secondRange.position; // actions } deleted.add(secondRange); // actions if (endSoFar < exclusiveEnd) { while (tailTree.hasNext() && lastRange == null) { lastRange = tailTree.next(); if (lastRange.exclusiveEnd() <= exclusiveEnd) { tailTree.remove(); deleted.add(lastRange); // actions lastRange = null; } } if (lastRange != null && lastRange.position < exclusiveEnd) { // actions Range beforeLastRange = (Range) lastRange.clone(); beforeLastRange.length = exclusiveEnd - beforeLastRange.position; deleted.add(beforeLastRange); } } if (lastRange != null && lastRange.position < exclusiveEnd && lastRange.exclusiveEnd() > exclusiveEnd) { long delta = exclusiveEnd - lastRange.position; lastRange.position += delta; lastRange.length -= delta; lastRange.dataOffset += delta; if (toBeAdded) ranges.add(lastRange); } if (actions != null) actions.addLostRanges(deleted); } private long[] deleteRanges(List<Range> currentAction) { long[] result = new long[2]; result[0] = result[1] = currentAction.get(0).position; actionsOn(false); deleteAndShift(result[0], currentAction.get(currentAction.size() - 1).exclusiveEnd() - result[0]); actionsOn(true); return result; } /** * Closes all files before termination. After this call the object is no longer valid. Calling * dispose() is optional, but it will let use of files immediately in the operating system, instead of * having to wait until the object is garbage collected. Note: apparently due to a bug in the java * virtual machine combined with some dumb os, files won't be freed after this call. See * http://forum.java.sun.com/thread.jspa?forumID=4&threadID=158689 */ public void dispose() { if (ranges == null) return; for (Range value : ranges) { if (value.data instanceof Closeable) { ContentUtils.close((Closeable) value.data); } } if (actions != null) { actions.dispose(); actions = null; } ranges = null; listeners = null; } private int fillWithChanges(ByteBuffer dst, long position) { long relativePosition = position - changesPosition; int changesSize = changeList.size(); if (relativePosition < 0L || relativePosition >= changesSize) return 0; int remaining = dst.remaining(); int i = (int) relativePosition; for (; remaining > 0 && i < changesSize; ++i, --remaining) { dst.put(changeList.get(i).byteValue()); } return i - (int) relativePosition; } private int fillWithPartOfRange(ByteBuffer dst, Range sourceRange, long overlapBytes, int maxCopyLength) throws IOException { int dstInitialPosition = dst.position(); if (sourceRange.data instanceof ByteBuffer) { ByteBuffer src = (ByteBuffer) sourceRange.data; src.limit((int) (sourceRange.dataOffset + sourceRange.length)); src.position((int) (sourceRange.dataOffset + overlapBytes)); if (src.remaining() > dst.remaining() || src.remaining() > maxCopyLength) { src.limit(src.position() + Math.min(dst.remaining(), maxCopyLength)); } dst.put(src); } else if (sourceRange.data instanceof RandomAccessFile) { RandomAccessFile src = (RandomAccessFile) sourceRange.data; long start = sourceRange.dataOffset + overlapBytes; int length = (int) Math.min(sourceRange.length - overlapBytes, maxCopyLength); int limit = -1; if (dst.remaining() > length) { limit = dst.limit(); dst.limit(dst.position() + length); } src.getChannel().read(dst, start); if (limit > 0) dst.limit(limit); } return dst.position() - dstInitialPosition; } private void fillWithRange(ByteBuffer dst, Range sourceRange, long overlapBytes, long position, List<Long> rangesModified) throws IOException { long positionSoFar = position; if (position < changesPosition) { int added = fillWithPartOfRange(dst, sourceRange, overlapBytes, (int) Math.min(changesPosition - position, Integer.MAX_VALUE)); positionSoFar += added; overlapBytes += added; } int changesAdded = 0; long changesPosition = positionSoFar; if (changeList != null && positionSoFar >= this.changesPosition && positionSoFar < this.changesPosition + changeList.size() && overlapBytes < sourceRange.length) { changesAdded = fillWithChanges(dst, positionSoFar); if (changesInserted) positionSoFar += changesAdded; else overlapBytes += changesAdded; } positionSoFar += fillWithPartOfRange(dst, sourceRange, overlapBytes, Integer.MAX_VALUE); if (rangesModified != null) { if (sourceRange.dirty) { rangesModified.add(position); rangesModified.add(positionSoFar - position); } else if (changesAdded > 0) {//&& !changesInserted) { rangesModified.add(changesPosition); rangesModified.add((long)changesAdded); // } else if (changeList != null && changesPosition >= changesPosition && changesInserted && // positionSoFar - changesPosition > 0) { // rangesModified.add(Long.valueOf(changesPosition)); //rangesModified.add(Long.valueOf(positionSoFar - changesPosition)); } } } /** * Closes all files for termination * * @see Object#finalize() */ @Override protected void finalize() throws Throwable { dispose(); super.finalize(); } /** * Reads a sequence of bytes from this content into the given buffer, starting at the given position * * @param dst where to write the read result to * @param position starting read point * @return number of bytes read */ public int get(ByteBuffer dst, long position) throws IOException { return get(dst, null, position); } /** * Reads a sequence of bytes from this content into the given buffer, starting at the given position * * @param dst where to write the read result to * @param position starting read point * @return number of bytes read */ public int get(ByteBuffer dst, List<Long> rangesModified, long position) throws IOException { if (rangesModified != null) rangesModified.clear(); long positionShift = 0; int dstInitialRemaining = dst.remaining(); if (changeList != null && changesInserted && position > changesPosition) positionShift = (int) Math.min(changeList.size(), position - changesPosition); long positionSoFar = position - positionShift; initSubtreeTraversing(positionSoFar, dst.remaining()); Range partialRange; while (tailTree.hasNext() && (partialRange = tailTree.next()).position < exclusiveEnd) { fillWithRange(dst, partialRange, positionSoFar - partialRange.position, positionSoFar + positionShift, rangesModified); positionSoFar = partialRange.exclusiveEnd(); if (changeList != null && changesInserted && positionSoFar + positionShift > changesPosition) positionShift = changeList.size(); } if (dst.remaining() > 0 && changeList != null && positionSoFar + positionShift < changesPosition + changeList.size()) { int size = fillWithChanges(dst, positionSoFar + positionShift); if (rangesModified != null) { rangesModified.add(positionSoFar + positionShift); rangesModified.add((long)size); } } return dstInitialRemaining - dst.remaining(); } /** * Reads the sequence of all bytes from this content into the given file * * @return number of bytes read */ public long get(File destinationFile) throws IOException { return get(destinationFile, 0L, length()); } /** * Reads a sequence of bytes from this content into the given file * * @param start first byte in sequence * @param length number of bytes to read * @return number of bytes read */ public long get(File destinationFile, long start, long length) throws IOException { if (start < 0L || length < 0L || start + length > length()) return 0L; if (actions != null) actions.endAction(); commitChanges(); RandomAccessFile dst = new RandomAccessFile(destinationFile, "rws"); try { dst.setLength(length); FileChannel channel = dst.getChannel(); ByteBuffer buffer = null; for (long position = 0L; position < length; position += mappedFileBufferLength) { int partLength = (int) Math.min(mappedFileBufferLength, length - position); boolean bufferFromMap = true; try { buffer = channel.map(FileChannel.MapMode.READ_WRITE, position, partLength); } catch (IOException e) { // gcj 4.3.0 channel maps work differently than sun's: // gcj won't accept two calls to channel.map with a different position, sun does // gcj will happily accept maps of size bigger than available memory, sun won't // to access past the 2Gb barrier there is no choice but use plain ByteBuffers in gcj bufferFromMap = false; if (buffer == null) buffer = ByteBuffer.allocateDirect((int) mappedFileBufferLength); buffer.position(0); buffer.limit(partLength); } get(buffer, start + position); if (bufferFromMap) { ((MappedByteBuffer) buffer).force(); buffer = null; } else { buffer.position(0); buffer.limit(partLength); channel.write(buffer, position); } } channel.force(true); channel.close(); } finally { ContentUtils.close(dst); } return length; } /* * Does not check changeList */ private int getFromRanges(long position) throws IOException { int result = 0; Range range = getRangeAt(position); if (range != null) { Object value = range.data; if (value instanceof ByteBuffer) { ByteBuffer data = (ByteBuffer) value; data.limit(data.capacity()); data.position((int) range.dataOffset); result = data.get((int) (position - range.position)) & 0x0ff; } else if (value instanceof RandomAccessFile) { RandomAccessFile randomFile = (RandomAccessFile) value; randomFile.seek(position); result = randomFile.read(); } } return result; } Range getRangeAt(long position) { SortedSet<Range> subSet = ranges.tailSet(new Range(position, 1L)); if (subSet.isEmpty()) return null; return subSet.first(); } private Set<Range> initSubtreeTraversing(long position, long length) { Set<Range> result = ranges.tailSet(new Range(position, 1L)); tailTree = result.iterator(); exclusiveEnd = position + length; if (exclusiveEnd > length()) exclusiveEnd = length(); return result; } /** * Inserts a byte into this content at the given position * * @param source byte * @param position insert point */ public void insert(byte source, long position) throws IOException { if (position > length()) return; dirty = true; lastUpperNibblePosition = position; if (actions != null) actions.eventPreModify(ActionHistory.ActionType.INSERT, position, true); updateChanges(position, true); changeList.set((int) (position - changesPosition), source & 0x0ff); notifyListeners(); } /** * Inserts a sequence of bytes from the given buffer into this content, starting at the given position * and shifting the existing ones. * * @param source bytes. The buffer is not copied internally, changes after this call will result in * undefined behaviour. * @param position starting insert point */ public void insert(ByteBuffer source, long position) { if (source.remaining() < 1 || position > length()) return; dirty = true; lastUpperNibblePosition = -1L; if (actions != null) actions.eventPreModify(ActionHistory.ActionType.INSERT, position, false); commitChanges(); Range newRange = new Range(position, source, true); insertRange(newRange); if (actions != null) actions.addInserted((Range) newRange.clone()); notifyListeners(); } /** * Inserts a sequence of bytes from the given file into this content, starting at the given position * and shifting the existing ones. * * @param aFile The file is not copied internally, changes after this call will result in * undefined behaviour. * @param position starting insert point * @throws IOException when i/o problems occur. The content stays unchanged and valid */ public void insert(File aFile, long position) throws IOException { long fileLength = aFile.length(); if (fileLength < 1L || position > length()) return; Range newRange = new Range(position, aFile, true); dirty = true; lastUpperNibblePosition = -1L; if (actions != null) actions.eventPreModify(ActionHistory.ActionType.INSERT, position, false); commitChanges(); insertRange(newRange); if (actions != null) actions.addInserted((Range) newRange.clone()); notifyListeners(); } private void insertRange(Range newRange) { splitAndShift(newRange.position, newRange.length); ranges.add(newRange); } private long[] insertRanges(List<Range> ranges) { BinaryContent.Range firstRange = ranges.get(0); BinaryContent.Range lastRange = ranges.get(ranges.size() - 1); splitAndShift(firstRange.position, lastRange.exclusiveEnd() - firstRange.position); List<Range> cloned = new ArrayList<>(ranges.size()); for (Range range : ranges) cloned.add((Range)range.clone()); this.ranges.addAll(cloned); return new long[]{firstRange.position, lastRange.exclusiveEnd()}; } /** * Tells whether changes have been done to the original content * * @return true: the content has been modified */ public boolean isDirty() { return dirty; } /** * Number of bytes in content * * @return length of content in byte units */ public long length() { long result = 0L; if (ranges.size() > 0) { result = ranges.last().exclusiveEnd(); } if (changeList != null && changesInserted) { result += changeList.size(); } return result; } private void notifyListeners() { if (listeners == null) return; for (ModifyListener listener : listeners) { listener.modified(); } } /** * Writes a byte into this content at the given position * * @param source byte * @param position overwrite point */ public void overwrite(byte source, long position) throws IOException { overwrite(source, 0, 8, position); } /** * Writes a byte into this content at the given position with bit offset and length bits * Examples: * previous content 0000 0000, source 1111 1111, offset 0, length 8 -> resulting content 1111 1111 * previous content 0000 0000, source 1111 1111, offset 1, length 2 -> resulting content 0110 0000 * previous content 0000 0000, source stuv wxyz, offset 2, length 5 -> resulting content 00vw xyz0 * <P>When action history is on, considers the special case of user generated input(in hex) from a * keyboard, in which nibbles are input in different calls to this method: the lower nibble input is * not considered in action history so undoing/redoing takes effect on the whole byte.<P> * * @param source byte, interesting bits are to the right * @param offset bit offset (0 <= offset < 8) * @param length number of bits to copy * @param position overwrite point */ public void overwrite(byte source, int offset, int length, long position) throws IOException { if (offset < 0 || offset > 7 || length < 0 || position >= length()) return; dirty = true; if (actions != null) { if (lastUpperNibblePosition == position && offset == 4 && length == 4) actionsOn(false); else actions.eventPreModify(ActionHistory.ActionType.OVERWRITE, position, true); } if (length + offset > 8) length = 8 - offset; Range range = updateChanges(position, false); int previous = changeList.get((int) (position - changesPosition)); int mask = (0x0ff >>> offset) & (0x0ff << (8 - offset - length)); int newValue = previous & ~mask | (source << (8 - offset - length)) & mask; changeList.set((int) (position - changesPosition), newValue); if (actions != null) { if (range == null) actions.addLostByte(position, previous); else { Range clone = (Range) range.clone(); clone.position = position; clone.length = 1L; clone.dataOffset = range.dataOffset + position - range.position; // clone.dirty = true; actions.addLostRange(clone); } } actionsOn(true); lastUpperNibblePosition = actions != null && offset == 0 && length == 4 ? position : -1L; notifyListeners(); } /** * Writes a sequence of bytes from the given buffer into this content, starting at the given position * and overwriting the existing ones. * * @param source bytes. The buffer is not copied internally, changes after this call will result in * undefined behaviour. * @param position starting overwrite point */ public void overwrite(ByteBuffer source, long position) { if (source.remaining() > 0 && position < length()) overwriteInternal(new Range(position, source, true)); } /** * Writes a sequence of bytes from the given file into this content, starting at the given position * and overwriting the existing ones. Changes to the file after this call will result in undefined * behaviour. * * @param aFile with source bytes. * @param position starting overwrite point * @throws IOException when i/o problems occur. The content stays unchanged and valid */ public void overwrite(File aFile, long position) throws IOException { if (aFile.length() > 0L && position < length()) overwriteInternal(new Range(position, aFile, true)); } private void overwriteInternal(Range newRange) { dirty = true; lastUpperNibblePosition = -1L; if (actions != null) actions.eventPreModify(ActionHistory.ActionType.OVERWRITE, newRange.position, false); commitChanges(); overwriteRange(newRange); if (actions != null) actions.addRangeToCurrentAction((Range) newRange.clone()); notifyListeners(); } private void overwriteRange(Range aRange) { deleteInternal(aRange.position, aRange.length); ranges.add(aRange); } private long[] overwriteRanges(List<Range> ranges) { BinaryContent.Range firstRange = ranges.get(0); BinaryContent.Range lastRange = ranges.get(ranges.size() - 1); splitAndShift(firstRange.position, 0); splitAndShift(lastRange.exclusiveEnd(), 0); initSubtreeTraversing(firstRange.position, 0L); if (tailTree.hasNext()) { Range goingRange = tailTree.next(); while (goingRange != null && goingRange.exclusiveEnd() <= lastRange.exclusiveEnd()) { tailTree.remove(); goingRange = null; if (tailTree.hasNext()) goingRange = tailTree.next(); } } List<Range> cloned = new ArrayList<>(ranges.size()); for (Range range : ranges) cloned.add((Range)range.clone()); this.ranges.addAll(cloned); return new long[]{firstRange.position, lastRange.exclusiveEnd()}; } /** * Redoes last action on BinaryContent. Action history should be on: setActionHistory() * * @return 2 elements long array, first one the start point (inclusive) of finished undo operation, * second one the end point (exclusive). <code>null</code> if redo is not performed */ public long[] redo() { if (actions == null) return null; Object[] action = actions.redoAction(); if (action == null) return null; long[] result = null; @SuppressWarnings("unchecked") List<Range> currentAction = (List<Range>) action[1]; if (action[0] == ActionHistory.ActionType.DELETE) { result = deleteRanges(currentAction); } else if (action[0] == ActionHistory.ActionType.INSERT) { result = insertRanges(currentAction); } else if (action[0] == ActionHistory.ActionType.OVERWRITE) { // 0 to size - 1: overwritten ranges, last one: overwrite range int size = currentAction.size(); result = overwriteRanges(currentAction.subList(size - 1, size)); } notifyListeners(); return result; } /** * Sets action history on. After this call the content will remember past actions to undo and redo */ void setActionsHistory() { if (actions == null) { commitChanges(); actions = new ActionHistory(this); } } private void shiftRemainingRanges(long increment) { if (increment == 0L) return; while (tailTree.hasNext()) { Range currentRange = tailTree.next(); currentRange.position += increment; // currentRange.dirty = true; } } private void splitAndShift(long position, long increment) { initSubtreeTraversing(position, 0); if (!tailTree.hasNext()) return; Range firstRange = tailTree.next(); Range secondRange = null; if (firstRange.position < position) { secondRange = (Range) firstRange.clone(); // will be tail part of firstRange long delta = position - firstRange.position; firstRange.length = delta; secondRange.length -= delta; secondRange.dataOffset += delta; secondRange.position = secondRange.position + delta + increment; // secondRange.dirty |= increment != 0; } else { firstRange.position += increment; // firstRange.dirty |= increment != 0; } shiftRemainingRanges(increment); if (secondRange != null) ranges.add(secondRange); } /** * Lists the ranges that back this content */ public String toString() { StringBuilder result = new StringBuilder("BinaryContent: {length:").append(length()).append("}\n"); for (Range myRange : ranges) { result.append(myRange).append('\n'); } return result.toString(); } /** * Undoes last action on BinaryContent. Action history should be on: setActionHistory() * * @return 2 elements long array, first one the start point (inclusive) of finished undo operation, * second one the end point (exclusive). <code>null</code> if undo is not performed */ public long[] undo() { if (actions == null) return null; Object[] action = actions.undoAction(); if (action == null) return null; commitChanges(); long[] result = null; @SuppressWarnings("unchecked") List<Range> currentAction = (List<Range>) action[1]; if (action[0] == ActionHistory.ActionType.DELETE) { result = insertRanges(currentAction); } else if (action[0] == ActionHistory.ActionType.INSERT) { result = deleteRanges(currentAction); } else if (action[0] == ActionHistory.ActionType.OVERWRITE) { // 0 to size - 1: overwritten ranges, last one: overwrite range result = overwriteRanges(currentAction.subList(0, currentAction.size() - 1)); } notifyListeners(); return result; } private Range updateChanges(long position, boolean insert) throws IOException { Range result = null; if (changeList != null) { long lowerLimit = changesPosition; long upperLimit = changesPosition + changeList.size(); if (!insert && position >= lowerLimit && position < upperLimit) return null; // reuse without expanding if (!insert) --lowerLimit; if (insert == changesInserted && position >= lowerLimit && position <= upperLimit) { // reuse if (insert) { changeList.add((int) (position - changesPosition), 0); } else { result = getRangeAt(position); if (changesPosition > position) { changesPosition = position; changeList.add(0, getFromRanges(position)); } else if (changesPosition + changeList.size() <= position) { changeList.add(getFromRanges(position)); } } return result; } else { commitChanges(); } } changeList = new ArrayList<>(); changeList.add(getFromRanges(position)); changesInserted = insert; changesPosition = position; if (!insert) result = getRangeAt(position); return result; } }