/*******************************************************************************
* Copyright (c) 2004, 2008 John Krasnay and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* John Krasnay - initial API and implementation
*******************************************************************************/
package net.sf.vex.dom;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* Implementation of the <code>Content</code> interface that manages
* changes efficiently. Implements a buffer that keeps its free space
* (the "gap") at the location of the last change. Insertions at the
* start of the gap require no other chars to be moved so long as the
* insertion is smaller than the gap. Deletions that end of the gap
* are also very efficent. Furthermore, changes near the gap require
* relatively few characters to be moved.
*/
public class GapContent implements Content {
private char[] content;
private int gapStart;
private int gapEnd;
private final Map positions = new HashMap();
/**
* Class constructor.
*
* @param initialCapacity initial capacity of the content.
*/
public GapContent(int initialCapacity) {
assertPositive(initialCapacity);
this.content = new char[initialCapacity];
this.gapStart = 0;
this.gapEnd = initialCapacity;
}
/**
* Creates a new Position object at the given initial offset.
*
* @param offset initial offset of the position
*/
public Position createPosition(int offset) {
assertOffset(offset, 0, this.getLength());
Position pos = new GapContentPosition(offset);
this.positions.put(pos, pos);
return pos;
}
/**
* Insert a string into the content.
*
* @param offset Offset at which to insert the string.
* @param s String to insert.
*/
public void insertString(int offset, String s) {
assertOffset(offset, 0, this.getLength());
if (s.length() > (this.gapEnd - this.gapStart)) {
this.expandContent(this.getLength() + s.length());
}
//
// Optimization: no need to update positions if we're inserting
// after existing content (offset == this.getLength()) and if
// we don't have to move the gap to do it (offset == gapStart).
//
// This significantly improves document load speed.
//
boolean atEnd = (offset == this.getLength() && offset == gapStart);
this.moveGap(offset);
s.getChars(0, s.length(), this.content, offset);
this.gapStart += s.length();
if (!atEnd) {
//
// Update positions
//
for (Iterator i = this.positions.keySet().iterator(); i.hasNext(); ) {
GapContentPosition pos = (GapContentPosition) i.next();
if (pos.getOffset() >= offset) {
pos.setOffset(pos.getOffset() + s.length());
}
}
}
}
/**
* Deletes the given range of characters.
*
* @param offset Offset from which characters should be deleted.
* @param length Number of characters to delete.
*/
public void remove(int offset, int length) {
assertOffset(offset, 0, this.getLength() - length);
assertPositive(length);
this.moveGap(offset + length);
this.gapStart -= length;
for (Iterator i = this.positions.keySet().iterator(); i.hasNext(); ) {
GapContentPosition pos = (GapContentPosition) i.next();
if (pos.getOffset() >= offset + length) {
pos.setOffset(pos.getOffset() - length);
} else if (pos.getOffset() >= offset) {
pos.setOffset(offset);
}
}
}
/**
* Gets a substring of the content.
*
* @param offset Offset at which the string begins.
* @param length Number of characters to return.
*/
public String getString(int offset, int length) {
assertOffset(offset, 0, this.getLength() - length);
assertPositive(length);
if (offset + length <= this.gapStart) {
return new String(this.content,
offset,
length);
} else if (offset >= this.gapStart) {
return new String(this.content,
offset - this.gapStart + this.gapEnd,
length);
} else {
StringBuffer sb = new StringBuffer(length);
sb.append(this.content,
offset,
this.gapStart - offset);
sb.append(this.content,
this.gapEnd,
offset + length - this.gapStart);
return sb.toString();
}
}
/**
* Return the length of the content.
*/
public int getLength() {
return this.content.length - (this.gapEnd - this.gapStart);
}
//====================================================== PRIVATE
private static final int GROWTH_SLOWDOWN_SIZE = 100000;
private static final int GROWTH_RATE_FAST = 2;
private static final float GROWTH_RATE_SLOW = 1.1f;
/**
* Implementation of the Position interface.
*/
private static class GapContentPosition implements Position {
private int offset;
public GapContentPosition(int offset) {
this.offset = offset;
}
public int getOffset() {
return this.offset;
}
public void setOffset(int offset) {
this.offset = offset;
}
public String toString() {
return Integer.toString(this.offset);
}
}
/**
* Assert that the given offset is within the given range,
* throwing IllegalArgumentException if not.
*/
private static void assertOffset(int offset, int min, int max) {
if (offset < min || offset > max) {
throw new IllegalArgumentException("Bad offset " + offset +
"must be between " + min +
" and " + max);
}
}
/**
* Assert that the given value is zero or positive.
* throwing IllegalArgumentException if not.
*/
private static void assertPositive(int value) {
if (value < 0) {
throw new IllegalArgumentException("Value should be zero or positive, but it was " + value);
}
}
/**
* Expand the content array to fit at least the given length.
*/
private void expandContent(int newLength) {
// grow quickly when small, slower when large
int newCapacity;
if (newLength < GROWTH_SLOWDOWN_SIZE) {
newCapacity = Math.max((int) (newLength * GROWTH_RATE_FAST), 32);
} else {
newCapacity = (int)(newLength * GROWTH_RATE_SLOW);
}
char[] newContent = new char[newCapacity];
System.arraycopy(this.content, 0,
newContent, 0,
this.gapStart);
int tailLength = this.content.length - this.gapEnd;
System.arraycopy(this.content, this.gapEnd,
newContent, newCapacity - tailLength,
tailLength);
this.content = newContent;
this.gapEnd = newCapacity - tailLength;
}
/**
* Move the gap to the given offset.
*/
private void moveGap(int offset) {
assertOffset(offset, 0, this.getLength());
if (offset <= this.gapStart) {
int length = this.gapStart - offset;
System.arraycopy(this.content, offset,
this.content, this.gapEnd - length,
length);
this.gapStart -= length;
this.gapEnd -= length;
} else {
int length = offset - this.gapStart;
System.arraycopy(this.content, this.gapEnd,
this.content, this.gapStart,
length);
this.gapStart += length;
this.gapEnd += length;
}
}
}