Sync with latest bnd code for testing purposes
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1354104 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/bundleplugin/src/main/java/aQute/lib/properties/BadLocationException.java b/bundleplugin/src/main/java/aQute/lib/properties/BadLocationException.java
new file mode 100644
index 0000000..2d9207d
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/properties/BadLocationException.java
@@ -0,0 +1,14 @@
+package aQute.lib.properties;
+
+public class BadLocationException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public BadLocationException() {
+ super();
+ }
+
+ public BadLocationException(String var0) {
+ super(var0);
+ }
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/properties/CopyOnWriteTextStore.java b/bundleplugin/src/main/java/aQute/lib/properties/CopyOnWriteTextStore.java
new file mode 100644
index 0000000..77bb9f6
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/properties/CopyOnWriteTextStore.java
@@ -0,0 +1,147 @@
+package aQute.lib.properties;
+
+/**
+ * Copy-on-write <code>ITextStore</code> wrapper.
+ * <p>
+ * This implementation uses an unmodifiable text store for the initial content.
+ * Upon first modification attempt, the unmodifiable store is replaced with a
+ * modifiable instance which must be supplied in the constructor.
+ * </p>
+ * <p>
+ * This class is not intended to be subclassed.
+ * </p>
+ *
+ * @since 3.2
+ * @noextend This class is not intended to be subclassed by clients.
+ */
+public class CopyOnWriteTextStore implements ITextStore {
+
+ /**
+ * An unmodifiable String based text store. It is not possible to modify the
+ * content other than using {@link #set}. Trying to {@link #replace} a text
+ * range will throw an <code>UnsupportedOperationException</code>.
+ */
+ private static class StringTextStore implements ITextStore {
+
+ /** Represents the content of this text store. */
+ private String fText = ""; //$NON-NLS-1$
+
+ /**
+ * Create an empty text store.
+ */
+ private StringTextStore() {
+ super();
+ }
+
+ /**
+ * Create a text store with initial content.
+ *
+ * @param text
+ * the initial content
+ */
+ private StringTextStore(String text) {
+ super();
+ set(text);
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ITextStore#get(int)
+ */
+ public char get(int offset) {
+ return fText.charAt(offset);
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ITextStore#get(int, int)
+ */
+ public String get(int offset, int length) {
+ return fText.substring(offset, offset + length);
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ITextStore#getLength()
+ */
+ public int getLength() {
+ return fText.length();
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ITextStore#replace(int, int,
+ * java.lang.String)
+ */
+ public void replace(int offset, int length, String text) {
+ // modification not supported
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ITextStore#set(java.lang.String)
+ */
+ public void set(String text) {
+ fText = text != null ? text : ""; //$NON-NLS-1$
+ }
+
+ }
+
+ /** The underlying "real" text store */
+ protected ITextStore fTextStore = new StringTextStore();
+
+ /** A modifiable <code>ITextStore</code> instance */
+ private final ITextStore fModifiableTextStore;
+
+ /**
+ * Creates an empty text store. The given text store will be used upon first
+ * modification attempt.
+ *
+ * @param modifiableTextStore
+ * a modifiable <code>ITextStore</code> instance, may not be
+ * <code>null</code>
+ */
+ public CopyOnWriteTextStore(ITextStore modifiableTextStore) {
+ fTextStore = new StringTextStore();
+ fModifiableTextStore = modifiableTextStore;
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ITextStore#get(int)
+ */
+ public char get(int offset) {
+ return fTextStore.get(offset);
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ITextStore#get(int, int)
+ */
+ public String get(int offset, int length) {
+ return fTextStore.get(offset, length);
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ITextStore#getLength()
+ */
+ public int getLength() {
+ return fTextStore.getLength();
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ITextStore#replace(int, int,
+ * java.lang.String)
+ */
+ public void replace(int offset, int length, String text) {
+ if (fTextStore != fModifiableTextStore) {
+ String content = fTextStore.get(0, fTextStore.getLength());
+ fTextStore = fModifiableTextStore;
+ fTextStore.set(content);
+ }
+ fTextStore.replace(offset, length, text);
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ITextStore#set(java.lang.String)
+ */
+ public void set(String text) {
+ fTextStore = new StringTextStore(text);
+ fModifiableTextStore.set(""); //$NON-NLS-1$
+ }
+
+}
\ No newline at end of file
diff --git a/bundleplugin/src/main/java/aQute/lib/properties/Document.java b/bundleplugin/src/main/java/aQute/lib/properties/Document.java
new file mode 100644
index 0000000..4df75a6
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/properties/Document.java
@@ -0,0 +1,59 @@
+package aQute.lib.properties;
+
+public class Document implements IDocument {
+
+ public final static String[] DELIMITERS = {
+ "\r", "\n", "\r\n"
+ };
+
+ private LineTracker lineTracker = new LineTracker();
+ private ITextStore textStore = new CopyOnWriteTextStore(new GapTextStore());
+
+ public Document(String text) {
+ setText(text);
+ }
+
+ public int getNumberOfLines() {
+ return lineTracker.getNumberOfLines();
+ }
+
+ public IRegion getLineInformation(int line) throws BadLocationException {
+ return lineTracker.getLineInformation(line);
+ }
+
+ public String get(int offset, int length) throws BadLocationException {
+ return textStore.get(offset, length);
+ }
+
+ public String getLineDelimiter(int line) throws BadLocationException {
+ return lineTracker.getLineDelimiter(line);
+ }
+
+ public int getLength() {
+ return textStore.getLength();
+ }
+
+ public void replace(int offset, int length, String text) throws BadLocationException {
+ textStore.replace(offset, length, text);
+ lineTracker.set(get());
+ }
+
+ public char getChar(int pos) {
+ return textStore.get(pos);
+ }
+
+ public void setText(String text) {
+ textStore.set(text);
+ lineTracker.set(text);
+ }
+
+ public String get() {
+ return textStore.get(0, textStore.getLength());
+ }
+
+ protected static class DelimiterInfo {
+ public int delimiterIndex;
+ public int delimiterLength;
+ public String delimiter;
+ }
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/properties/GapTextStore.java b/bundleplugin/src/main/java/aQute/lib/properties/GapTextStore.java
new file mode 100644
index 0000000..7c4a043
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/properties/GapTextStore.java
@@ -0,0 +1,419 @@
+package aQute.lib.properties;
+
+/**
+ * Implements a gap managing text store. The gap text store relies on the
+ * assumption that consecutive changes to a document are co-located. The start
+ * of the gap is always moved to the location of the last change.
+ * <p>
+ * <strong>Performance:</strong> Typing-style changes perform in constant time
+ * unless re-allocation becomes necessary. Generally, a change that does not
+ * cause re-allocation will cause at most one
+ * {@linkplain System#arraycopy(Object, int, Object, int, int) arraycopy}
+ * operation of a length of about <var>d</var>, where <var>d</var> is the
+ * distance from the previous change. Let <var>a(x)</var> be the algorithmic
+ * performance of an <code>arraycopy</code> operation of the length
+ * <var>x</var>, then such a change then performs in <i>O(a(x))</i>,
+ * {@linkplain #get(int, int) get(int, <var>length</var>)} performs in
+ * <i>O(a(length))</i>, {@link #get(int)} in <i>O(1)</i>.
+ * <p>
+ * How frequently the array needs re-allocation is controlled by the constructor
+ * parameters.
+ * </p>
+ * <p>
+ * This class is not intended to be subclassed.
+ * </p>
+ *
+ * @see CopyOnWriteTextStore for a copy-on-write text store wrapper
+ * @noextend This class is not intended to be subclassed by clients.
+ */
+public class GapTextStore implements ITextStore {
+ /**
+ * The minimum gap size allocated when re-allocation occurs.
+ *
+ * @since 3.3
+ */
+ private final int fMinGapSize;
+ /**
+ * The maximum gap size allocated when re-allocation occurs.
+ *
+ * @since 3.3
+ */
+ private final int fMaxGapSize;
+ /**
+ * The multiplier to compute the array size from the content length
+ * (1 <= fSizeMultiplier <= 2).
+ *
+ * @since 3.3
+ */
+ private final float fSizeMultiplier;
+
+ /** The store's content */
+ private char[] fContent = new char[0];
+ /** Starting index of the gap */
+ private int fGapStart = 0;
+ /** End index of the gap */
+ private int fGapEnd = 0;
+ /**
+ * The current high water mark. If a change would cause the gap to grow
+ * larger than this, the array is re-allocated.
+ *
+ * @since 3.3
+ */
+ private int fThreshold = 0;
+
+ /**
+ * Creates a new empty text store using the specified low and high
+ * watermarks.
+ *
+ * @param lowWatermark
+ * unused - at the lower bound, the array is only resized when
+ * the content does not fit
+ * @param highWatermark
+ * if the gap is ever larger than this, it will automatically be
+ * shrunken (>= 0)
+ * @deprecated use {@link GapTextStore#GapTextStore(int, int, float)}
+ * instead
+ */
+ public GapTextStore(int lowWatermark, int highWatermark) {
+ /*
+ * Legacy constructor. The API contract states that highWatermark is the
+ * upper bound for the gap size. Albeit this contract was not previously
+ * adhered to, it is now: The allocated gap size is fixed at half the
+ * highWatermark. Since the threshold is always twice the allocated gap
+ * size, the gap will never grow larger than highWatermark. Previously,
+ * the gap size was initialized to highWatermark, causing re-allocation
+ * if the content length shrunk right after allocation. The fixed gap
+ * size is now only half of the previous value, circumventing that
+ * problem (there was no API contract specifying the initial gap size).
+ * The previous implementation did not allow the gap size to become
+ * smaller than lowWatermark, which doesn't make any sense: that area of
+ * the gap was simply never ever used.
+ */
+ this(highWatermark / 2, highWatermark / 2, 0f);
+ }
+
+ /**
+ * Equivalent to {@linkplain GapTextStore#GapTextStore(int, int, float) new
+ * GapTextStore(256, 4096, 0.1f)}.
+ *
+ * @since 3.3
+ */
+ public GapTextStore() {
+ this(256, 4096, 0.1f);
+ }
+
+ /**
+ * Creates an empty text store that uses re-allocation thresholds relative
+ * to the content length. Re-allocation is controlled by the
+ * <em>gap factor</em>, which is the quotient of the gap size and the array
+ * size. Re-allocation occurs if a change causes the gap factor to go
+ * outside <code>[0, maxGapFactor]</code>. When re-allocation occurs,
+ * the array is sized such that the gap factor is
+ * <code>0.5 * maxGapFactor</code>. The gap size computed in this manner is
+ * bounded by the <code>minSize</code> and <code>maxSize</code> parameters.
+ * <p>
+ * A <code>maxGapFactor</code> of <code>0</code> creates a text store that
+ * never has a gap at all (if <code>minSize</code> is 0); a
+ * <code>maxGapFactor</code> of <code>1</code> creates a text store that
+ * doubles its size with every re-allocation and that never shrinks.
+ * </p>
+ * <p>
+ * The <code>minSize</code> and <code>maxSize</code> parameters are absolute
+ * bounds to the allocated gap size. Use <code>minSize</code> to avoid
+ * frequent re-allocation for small documents. Use <code>maxSize</code> to
+ * avoid a huge gap being allocated for large documents.
+ * </p>
+ *
+ * @param minSize
+ * the minimum gap size to allocate (>= 0; use 0 for no
+ * minimum)
+ * @param maxSize
+ * the maximum gap size to allocate (>= minSize; use
+ * {@link Integer#MAX_VALUE} for no maximum)
+ * @param maxGapFactor
+ * is the maximum fraction of the array that is occupied by the
+ * gap (
+ * <code>0 <= maxGapFactor <= 1</code>)
+ * @since 3.3
+ */
+ public GapTextStore(int minSize, int maxSize, float maxGapFactor) {
+ fMinGapSize = minSize;
+ fMaxGapSize = maxSize;
+ fSizeMultiplier = 1 / (1 - maxGapFactor / 2);
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ITextStore#get(int)
+ */
+ public final char get(int offset) {
+ if (offset < fGapStart)
+ return fContent[offset];
+
+ return fContent[offset + gapSize()];
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ITextStore#get(int, int)
+ */
+ public final String get(int offset, int length) {
+ if (fGapStart <= offset)
+ return new String(fContent, offset + gapSize(), length);
+
+ final int end = offset + length;
+
+ if (end <= fGapStart)
+ return new String(fContent, offset, length);
+
+ StringBuffer buf = new StringBuffer(length);
+ buf.append(fContent, offset, fGapStart - offset);
+ buf.append(fContent, fGapEnd, end - fGapStart);
+ return buf.toString();
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ITextStore#getLength()
+ */
+ public final int getLength() {
+ return fContent.length - gapSize();
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ITextStore#set(java.lang.String)
+ */
+ public final void set(String text) {
+ /*
+ * Moves the gap to the end of the content. There is no sensible
+ * prediction of where the next change will occur, but at least the next
+ * change will not trigger re-allocation. This is especially important
+ * when using the GapTextStore within a CopyOnWriteTextStore, where the
+ * GTS is only initialized right before a modification.
+ */
+ replace(0, getLength(), text);
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ITextStore#replace(int, int,
+ * java.lang.String)
+ */
+ public final void replace(int offset, int length, String text) {
+ if (text == null) {
+ adjustGap(offset, length, 0);
+ } else {
+ int textLength = text.length();
+ adjustGap(offset, length, textLength);
+ if (textLength != 0)
+ text.getChars(0, textLength, fContent, offset);
+ }
+ }
+
+ /**
+ * Moves the gap to <code>offset + add</code>, moving any content after
+ * <code>offset + remove</code> behind the gap. The gap size is kept between
+ * 0 and {@link #fThreshold}, leading to re-allocation if needed. The
+ * content between <code>offset</code> and <code>offset + add</code> is
+ * undefined after this operation.
+ *
+ * @param offset
+ * the offset at which a change happens
+ * @param remove
+ * the number of character which are removed or overwritten at
+ * <code>offset</code>
+ * @param add
+ * the number of character which are inserted or overwriting at
+ * <code>offset</code>
+ */
+ private void adjustGap(int offset, int remove, int add) {
+ final int oldGapSize = gapSize();
+ final int newGapSize = oldGapSize - add + remove;
+ final boolean reuseArray = 0 <= newGapSize && newGapSize <= fThreshold;
+
+ final int newGapStart = offset + add;
+ final int newGapEnd;
+
+ if (reuseArray)
+ newGapEnd = moveGap(offset, remove, oldGapSize, newGapSize, newGapStart);
+ else
+ newGapEnd = reallocate(offset, remove, oldGapSize, newGapSize, newGapStart);
+
+ fGapStart = newGapStart;
+ fGapEnd = newGapEnd;
+ }
+
+ /**
+ * Moves the gap to <code>newGapStart</code>.
+ *
+ * @param offset
+ * the change offset
+ * @param remove
+ * the number of removed / overwritten characters
+ * @param oldGapSize
+ * the old gap size
+ * @param newGapSize
+ * the gap size after the change
+ * @param newGapStart
+ * the offset in the array to move the gap to
+ * @return the new gap end
+ * @since 3.3
+ */
+ private int moveGap(int offset, int remove, int oldGapSize, int newGapSize, int newGapStart) {
+ /*
+ * No re-allocation necessary. The area between the change offset and
+ * gap can be copied in at most one operation. Don't copy parts that
+ * will be overwritten anyway.
+ */
+ final int newGapEnd = newGapStart + newGapSize;
+ if (offset < fGapStart) {
+ int afterRemove = offset + remove;
+ if (afterRemove < fGapStart) {
+ final int betweenSize = fGapStart - afterRemove;
+ arrayCopy(afterRemove, fContent, newGapEnd, betweenSize);
+ }
+ // otherwise, only the gap gets enlarged
+ } else {
+ final int offsetShifted = offset + oldGapSize;
+ final int betweenSize = offsetShifted - fGapEnd; // in the typing
+ // case,
+ // betweenSize
+ // is 0
+ arrayCopy(fGapEnd, fContent, fGapStart, betweenSize);
+ }
+ return newGapEnd;
+ }
+
+ /**
+ * Reallocates a new array and copies the data from the previous one.
+ *
+ * @param offset
+ * the change offset
+ * @param remove
+ * the number of removed / overwritten characters
+ * @param oldGapSize
+ * the old gap size
+ * @param newGapSize
+ * the gap size after the change if no re-allocation would occur
+ * (can be negative)
+ * @param newGapStart
+ * the offset in the array to move the gap to
+ * @return the new gap end
+ * @since 3.3
+ */
+ private int reallocate(int offset, int remove, final int oldGapSize, int newGapSize, final int newGapStart) {
+ // the new content length (without any gap)
+ final int newLength = fContent.length - newGapSize;
+ // the new array size based on the gap factor
+ int newArraySize = (int) (newLength * fSizeMultiplier);
+ newGapSize = newArraySize - newLength;
+
+ // bound the gap size within min/max
+ if (newGapSize < fMinGapSize) {
+ newGapSize = fMinGapSize;
+ newArraySize = newLength + newGapSize;
+ } else if (newGapSize > fMaxGapSize) {
+ newGapSize = fMaxGapSize;
+ newArraySize = newLength + newGapSize;
+ }
+
+ // the upper threshold is always twice the gapsize
+ fThreshold = newGapSize * 2;
+ final char[] newContent = allocate(newArraySize);
+ final int newGapEnd = newGapStart + newGapSize;
+
+ /*
+ * Re-allocation: The old content can be copied in at most 3 operations
+ * to the newly allocated array. Either one of change offset and the gap
+ * may come first. - unchanged area before the change offset / gap -
+ * area between the change offset and the gap (either one may be first)
+ * - rest area after the change offset / after the gap
+ */
+ if (offset < fGapStart) {
+ // change comes before gap
+ arrayCopy(0, newContent, 0, offset);
+ int afterRemove = offset + remove;
+ if (afterRemove < fGapStart) {
+ // removal is completely before the gap
+ final int betweenSize = fGapStart - afterRemove;
+ arrayCopy(afterRemove, newContent, newGapEnd, betweenSize);
+ final int restSize = fContent.length - fGapEnd;
+ arrayCopy(fGapEnd, newContent, newGapEnd + betweenSize, restSize);
+ } else {
+ // removal encompasses the gap
+ afterRemove += oldGapSize;
+ final int restSize = fContent.length - afterRemove;
+ arrayCopy(afterRemove, newContent, newGapEnd, restSize);
+ }
+ } else {
+ // gap comes before change
+ arrayCopy(0, newContent, 0, fGapStart);
+ final int offsetShifted = offset + oldGapSize;
+ final int betweenSize = offsetShifted - fGapEnd;
+ arrayCopy(fGapEnd, newContent, fGapStart, betweenSize);
+ final int afterRemove = offsetShifted + remove;
+ final int restSize = fContent.length - afterRemove;
+ arrayCopy(afterRemove, newContent, newGapEnd, restSize);
+ }
+
+ fContent = newContent;
+ return newGapEnd;
+ }
+
+ /**
+ * Allocates a new <code>char[size]</code>.
+ *
+ * @param size
+ * the length of the new array.
+ * @return a newly allocated char array
+ * @since 3.3
+ */
+ private char[] allocate(int size) {
+ return new char[size];
+ }
+
+ /*
+ * Executes System.arraycopy if length != 0. A length < 0 cannot happen ->
+ * don't hide coding errors by checking for negative lengths.
+ * @since 3.3
+ */
+ private void arrayCopy(int srcPos, char[] dest, int destPos, int length) {
+ if (length != 0)
+ System.arraycopy(fContent, srcPos, dest, destPos, length);
+ }
+
+ /**
+ * Returns the gap size.
+ *
+ * @return the gap size
+ * @since 3.3
+ */
+ private int gapSize() {
+ return fGapEnd - fGapStart;
+ }
+
+ /**
+ * Returns a copy of the content of this text store. For internal use only.
+ *
+ * @return a copy of the content of this text store
+ */
+ protected String getContentAsString() {
+ return new String(fContent);
+ }
+
+ /**
+ * Returns the start index of the gap managed by this text store. For
+ * internal use only.
+ *
+ * @return the start index of the gap managed by this text store
+ */
+ protected int getGapStartIndex() {
+ return fGapStart;
+ }
+
+ /**
+ * Returns the end index of the gap managed by this text store. For internal
+ * use only.
+ *
+ * @return the end index of the gap managed by this text store
+ */
+ protected int getGapEndIndex() {
+ return fGapEnd;
+ }
+}
\ No newline at end of file
diff --git a/bundleplugin/src/main/java/aQute/lib/properties/IDocument.java b/bundleplugin/src/main/java/aQute/lib/properties/IDocument.java
new file mode 100644
index 0000000..aba541f
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/properties/IDocument.java
@@ -0,0 +1,21 @@
+package aQute.lib.properties;
+
+public interface IDocument {
+
+ int getNumberOfLines();
+
+ IRegion getLineInformation(int lineNum) throws BadLocationException;
+
+ String get();
+
+ String get(int offset, int length) throws BadLocationException;
+
+ String getLineDelimiter(int line) throws BadLocationException;
+
+ int getLength();
+
+ void replace(int offset, int length, String data) throws BadLocationException;
+
+ char getChar(int offset) throws BadLocationException;
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/properties/IRegion.java b/bundleplugin/src/main/java/aQute/lib/properties/IRegion.java
new file mode 100644
index 0000000..70230e7
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/properties/IRegion.java
@@ -0,0 +1,8 @@
+package aQute.lib.properties;
+
+public interface IRegion {
+
+ int getLength();
+
+ int getOffset();
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/properties/ITextStore.java b/bundleplugin/src/main/java/aQute/lib/properties/ITextStore.java
new file mode 100644
index 0000000..f4cf2c7
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/properties/ITextStore.java
@@ -0,0 +1,65 @@
+package aQute.lib.properties;
+
+/**
+ * Interface for storing and managing text.
+ * <p>
+ * Provides access to the stored text and allows to manipulate it.
+ * </p>
+ * <p>
+ * Clients may implement this interface or use
+ * {@link org.eclipse.jface.text.GapTextStore} or
+ * {@link org.eclipse.jface.text.CopyOnWriteTextStore}.
+ * </p>
+ */
+public interface ITextStore {
+
+ /**
+ * Returns the character at the specified offset.
+ *
+ * @param offset
+ * the offset in this text store
+ * @return the character at this offset
+ */
+ char get(int offset);
+
+ /**
+ * Returns the text of the specified character range.
+ *
+ * @param offset
+ * the offset of the range
+ * @param length
+ * the length of the range
+ * @return the text of the range
+ */
+ String get(int offset, int length);
+
+ /**
+ * Returns number of characters stored in this text store.
+ *
+ * @return the number of characters stored in this text store
+ */
+ int getLength();
+
+ /**
+ * Replaces the specified character range with the given text.
+ * <code>replace(getLength(), 0, "some text")</code> is a valid call and
+ * appends text to the end of the text store.
+ *
+ * @param offset
+ * the offset of the range to be replaced
+ * @param length
+ * the number of characters to be replaced
+ * @param text
+ * the substitution text
+ */
+ void replace(int offset, int length, String text);
+
+ /**
+ * Replace the content of the text store with the given text. Convenience
+ * method for <code>replace(0, getLength(), text</code>.
+ *
+ * @param text
+ * the new content of the text store
+ */
+ void set(String text);
+}
\ No newline at end of file
diff --git a/bundleplugin/src/main/java/aQute/lib/properties/Line.java b/bundleplugin/src/main/java/aQute/lib/properties/Line.java
new file mode 100644
index 0000000..8d07ce1
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/properties/Line.java
@@ -0,0 +1,60 @@
+package aQute.lib.properties;
+
+/**
+ * Describes a line as a particular number of characters beginning at a
+ * particular offset, consisting of a particular number of characters, and being
+ * closed with a particular line delimiter.
+ */
+final class Line implements IRegion {
+
+ /** The offset of the line */
+ public int offset;
+ /** The length of the line */
+ public int length;
+ /** The delimiter of this line */
+ public final String delimiter;
+
+ /**
+ * Creates a new Line.
+ *
+ * @param offset
+ * the offset of the line
+ * @param end
+ * the last including character offset of the line
+ * @param delimiter
+ * the line's delimiter
+ */
+ public Line(int offset, int end, String delimiter) {
+ this.offset = offset;
+ this.length = (end - offset) + 1;
+ this.delimiter = delimiter;
+ }
+
+ /**
+ * Creates a new Line.
+ *
+ * @param offset
+ * the offset of the line
+ * @param length
+ * the length of the line
+ */
+ public Line(int offset, int length) {
+ this.offset = offset;
+ this.length = length;
+ this.delimiter = null;
+ }
+
+ /*
+ * @see org.eclipse.jface.text.IRegion#getOffset()
+ */
+ public int getOffset() {
+ return offset;
+ }
+
+ /*
+ * @see org.eclipse.jface.text.IRegion#getLength()
+ */
+ public int getLength() {
+ return length;
+ }
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/properties/LineTracker.java b/bundleplugin/src/main/java/aQute/lib/properties/LineTracker.java
new file mode 100644
index 0000000..a3c3adf
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/properties/LineTracker.java
@@ -0,0 +1,382 @@
+package aQute.lib.properties;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import aQute.lib.properties.Document.DelimiterInfo;
+
+public class LineTracker {
+
+ /** The line information */
+ private final List<Line> fLines = new ArrayList<Line>();
+ /** The length of the tracked text */
+ private int fTextLength;
+
+ /**
+ * Creates a new line tracker.
+ */
+ protected LineTracker() {}
+
+ /**
+ * Binary search for the line at a given offset.
+ *
+ * @param offset
+ * the offset whose line should be found
+ * @return the line of the offset
+ */
+ private int findLine(int offset) {
+
+ if (fLines.size() == 0)
+ return -1;
+
+ int left = 0;
+ int right = fLines.size() - 1;
+ int mid = 0;
+ Line line = null;
+
+ while (left < right) {
+
+ mid = (left + right) / 2;
+
+ line = fLines.get(mid);
+ if (offset < line.offset) {
+ if (left == mid)
+ right = left;
+ else
+ right = mid - 1;
+ } else if (offset > line.offset) {
+ if (right == mid)
+ left = right;
+ else
+ left = mid + 1;
+ } else if (offset == line.offset) {
+ left = right = mid;
+ }
+ }
+
+ line = fLines.get(left);
+ if (line.offset > offset)
+ --left;
+ return left;
+ }
+
+ /**
+ * Returns the number of lines covered by the specified text range.
+ *
+ * @param startLine
+ * the line where the text range starts
+ * @param offset
+ * the start offset of the text range
+ * @param length
+ * the length of the text range
+ * @return the number of lines covered by this text range
+ * @exception BadLocationException
+ * if range is undefined in this tracker
+ */
+ private int getNumberOfLines(int startLine, int offset, int length) throws BadLocationException {
+
+ if (length == 0)
+ return 1;
+
+ int target = offset + length;
+
+ Line l = fLines.get(startLine);
+
+ if (l.delimiter == null)
+ return 1;
+
+ if (l.offset + l.length > target)
+ return 1;
+
+ if (l.offset + l.length == target)
+ return 2;
+
+ return getLineNumberOfOffset(target) - startLine + 1;
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ILineTracker#getLineLength(int)
+ */
+ public final int getLineLength(int line) throws BadLocationException {
+ int lines = fLines.size();
+
+ if (line < 0 || line > lines)
+ throw new BadLocationException();
+
+ if (lines == 0 || lines == line)
+ return 0;
+
+ Line l = fLines.get(line);
+ return l.length;
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ILineTracker#getLineNumberOfOffset(int)
+ */
+ public final int getLineNumberOfOffset(int position) throws BadLocationException {
+ if (position < 0 || position > fTextLength)
+ throw new BadLocationException();
+
+ if (position == fTextLength) {
+
+ int lastLine = fLines.size() - 1;
+ if (lastLine < 0)
+ return 0;
+
+ Line l = fLines.get(lastLine);
+ return (l.delimiter != null ? lastLine + 1 : lastLine);
+ }
+
+ return findLine(position);
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ILineTracker#getLineInformationOfOffset(int)
+ */
+ public final IRegion getLineInformationOfOffset(int position) throws BadLocationException {
+ if (position > fTextLength)
+ throw new BadLocationException();
+
+ if (position == fTextLength) {
+ int size = fLines.size();
+ if (size == 0)
+ return new Region(0, 0);
+ Line l = fLines.get(size - 1);
+ return (l.delimiter != null ? new Line(fTextLength, 0) : new Line(fTextLength - l.length, l.length));
+ }
+
+ return getLineInformation(findLine(position));
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ILineTracker#getLineInformation(int)
+ */
+ public final IRegion getLineInformation(int line) throws BadLocationException {
+ int lines = fLines.size();
+
+ if (line < 0 || line > lines)
+ throw new BadLocationException();
+
+ if (lines == 0)
+ return new Line(0, 0);
+
+ if (line == lines) {
+ Line l = fLines.get(line - 1);
+ return new Line(l.offset + l.length, 0);
+ }
+
+ Line l = fLines.get(line);
+ return (l.delimiter != null ? new Line(l.offset, l.length - l.delimiter.length()) : l);
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ILineTracker#getLineOffset(int)
+ */
+ public final int getLineOffset(int line) throws BadLocationException {
+ int lines = fLines.size();
+
+ if (line < 0 || line > lines)
+ throw new BadLocationException();
+
+ if (lines == 0)
+ return 0;
+
+ if (line == lines) {
+ Line l = fLines.get(line - 1);
+ if (l.delimiter != null)
+ return l.offset + l.length;
+ throw new BadLocationException();
+ }
+
+ Line l = fLines.get(line);
+ return l.offset;
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ILineTracker#getNumberOfLines()
+ */
+ public final int getNumberOfLines() {
+ int lines = fLines.size();
+
+ if (lines == 0)
+ return 1;
+
+ Line l = fLines.get(lines - 1);
+ return (l.delimiter != null ? lines + 1 : lines);
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ILineTracker#getNumberOfLines(int, int)
+ */
+ public final int getNumberOfLines(int position, int length) throws BadLocationException {
+
+ if (position < 0 || position + length > fTextLength)
+ throw new BadLocationException();
+
+ if (length == 0) // optimization
+ return 1;
+
+ return getNumberOfLines(getLineNumberOfOffset(position), position, length);
+ }
+
+ /*
+ * @see
+ * org.eclipse.jface.text.ILineTracker#computeNumberOfLines(java.lang.String
+ * )
+ */
+ public final int computeNumberOfLines(String text) {
+ int count = 0;
+ int start = 0;
+ DelimiterInfo delimiterInfo = nextDelimiterInfo(text, start);
+ while (delimiterInfo != null && delimiterInfo.delimiterIndex > -1) {
+ ++count;
+ start = delimiterInfo.delimiterIndex + delimiterInfo.delimiterLength;
+ delimiterInfo = nextDelimiterInfo(text, start);
+ }
+ return count;
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ILineTracker#getLineDelimiter(int)
+ */
+ public final String getLineDelimiter(int line) throws BadLocationException {
+ int lines = fLines.size();
+
+ if (line < 0 || line > lines)
+ throw new BadLocationException();
+
+ if (lines == 0)
+ return null;
+
+ if (line == lines)
+ return null;
+
+ Line l = fLines.get(line);
+ return l.delimiter;
+ }
+
+ /**
+ * Returns the information about the first delimiter found in the given text
+ * starting at the given offset.
+ *
+ * @param text
+ * the text to be searched
+ * @param offset
+ * the offset in the given text
+ * @return the information of the first found delimiter or <code>null</code>
+ */
+ protected DelimiterInfo nextDelimiterInfo(String text, int offset) {
+
+ char ch;
+ int length = text.length();
+ for (int i = offset; i < length; i++) {
+
+ ch = text.charAt(i);
+ if (ch == '\r') {
+
+ if (i + 1 < length) {
+ if (text.charAt(i + 1) == '\n') {
+ DelimiterInfo fDelimiterInfo = new DelimiterInfo();
+ fDelimiterInfo.delimiter = Document.DELIMITERS[2];
+ fDelimiterInfo.delimiterIndex = i;
+ fDelimiterInfo.delimiterLength = 2;
+ return fDelimiterInfo;
+ }
+ }
+ DelimiterInfo fDelimiterInfo = new DelimiterInfo();
+ fDelimiterInfo.delimiter = Document.DELIMITERS[0];
+ fDelimiterInfo.delimiterIndex = i;
+ fDelimiterInfo.delimiterLength = 1;
+ return fDelimiterInfo;
+
+ } else if (ch == '\n') {
+ DelimiterInfo fDelimiterInfo = new DelimiterInfo();
+ fDelimiterInfo.delimiter = Document.DELIMITERS[1];
+ fDelimiterInfo.delimiterIndex = i;
+ fDelimiterInfo.delimiterLength = 1;
+ return fDelimiterInfo;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates the line structure for the given text. Newly created lines are
+ * inserted into the line structure starting at the given position. Returns
+ * the number of newly created lines.
+ *
+ * @param text
+ * the text for which to create a line structure
+ * @param insertPosition
+ * the position at which the newly created lines are inserted
+ * into the tracker's line structure
+ * @param offset
+ * the offset of all newly created lines
+ * @return the number of newly created lines
+ */
+ private int createLines(String text, int insertPosition, int offset) {
+
+ int count = 0;
+ int start = 0;
+ DelimiterInfo delimiterInfo = nextDelimiterInfo(text, 0);
+
+ while (delimiterInfo != null && delimiterInfo.delimiterIndex > -1) {
+
+ int index = delimiterInfo.delimiterIndex + (delimiterInfo.delimiterLength - 1);
+
+ if (insertPosition + count >= fLines.size())
+ fLines.add(new Line(offset + start, offset + index, delimiterInfo.delimiter));
+ else
+ fLines.add(insertPosition + count, new Line(offset + start, offset + index, delimiterInfo.delimiter));
+
+ ++count;
+ start = index + 1;
+ delimiterInfo = nextDelimiterInfo(text, start);
+ }
+
+ if (start < text.length()) {
+ if (insertPosition + count < fLines.size()) {
+ // there is a line below the current
+ Line l = fLines.get(insertPosition + count);
+ int delta = text.length() - start;
+ l.offset -= delta;
+ l.length += delta;
+ } else {
+ fLines.add(new Line(offset + start, offset + text.length() - 1, null));
+ ++count;
+ }
+ }
+
+ return count;
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ILineTracker#replace(int, int,
+ * java.lang.String)
+ */
+ public final void replace(@SuppressWarnings("unused") int position, @SuppressWarnings("unused") int length, @SuppressWarnings("unused") String text) throws BadLocationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * @see org.eclipse.jface.text.ILineTracker#set(java.lang.String)
+ */
+ public final void set(String text) {
+ fLines.clear();
+ if (text != null) {
+ fTextLength = text.length();
+ createLines(text, 0, 0);
+ }
+ }
+
+ /**
+ * Returns the internal data structure, a {@link List} of {@link Line}s.
+ * Used only by {@link TreeLineTracker#TreeLineTracker(ListLineTracker)}.
+ *
+ * @return the internal list of lines.
+ */
+ final List<Line> getLines() {
+ return fLines;
+ }
+}
\ No newline at end of file
diff --git a/bundleplugin/src/main/java/aQute/lib/properties/LineType.java b/bundleplugin/src/main/java/aQute/lib/properties/LineType.java
new file mode 100644
index 0000000..69eda5f
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/properties/LineType.java
@@ -0,0 +1,5 @@
+package aQute.lib.properties;
+
+public enum LineType {
+ blank, comment, entry, eof
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/properties/PropertiesLineReader.java b/bundleplugin/src/main/java/aQute/lib/properties/PropertiesLineReader.java
new file mode 100644
index 0000000..c3ea14b
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/properties/PropertiesLineReader.java
@@ -0,0 +1,125 @@
+package aQute.lib.properties;
+
+import static aQute.lib.properties.LineType.*;
+
+public class PropertiesLineReader {
+
+ private final IDocument document;
+ private final int lineCount;
+
+ private int lineNum = 0;
+
+ private IRegion lastRegion = null;
+ private String lastKey = null;
+ private String lastValue = null;
+
+ public PropertiesLineReader(IDocument document) {
+ this.document = document;
+ this.lineCount = document.getNumberOfLines();
+ }
+
+ public PropertiesLineReader(String data) {
+ this(new Document(data));
+ }
+
+ public LineType next() throws Exception {
+ int index = 0;
+ char[] chars = null;
+
+ StringBuilder keyData = new StringBuilder();
+ StringBuilder valueData = new StringBuilder();
+ StringBuilder currentBuffer = keyData;
+
+ boolean started = false;
+
+ mainLoop: while (true) {
+ if (chars == null)
+ chars = grabLine(false);
+ if (chars == null)
+ return eof;
+
+ if (index >= chars.length)
+ break;
+
+ char c = chars[index];
+ if (c == '\\') {
+ index++;
+ if (index == chars.length) {
+ chars = grabLine(true);
+ index = 0;
+ if (chars == null || chars.length == 0)
+ break; // The last line ended with a backslash
+ }
+ currentBuffer.append(chars[index]);
+ index++;
+ continue mainLoop;
+ }
+
+ if (c == '=' || c == ':')
+ currentBuffer = valueData;
+
+ if (!started && (c == '#' || c == '!'))
+ return comment;
+
+ if (Character.isWhitespace(c)) {
+ if (started) {
+ // whitespace ends the key
+ currentBuffer = valueData;
+ }
+ } else {
+ started = true;
+ currentBuffer.append(c);
+ }
+
+ index++;
+ }
+
+ if (!started)
+ return blank;
+
+ lastKey = keyData.toString();
+ return entry;
+ }
+
+ private char[] grabLine(boolean continued) throws BadLocationException {
+ if (lineNum >= lineCount) {
+ lastRegion = null;
+ return null;
+ }
+
+ IRegion lineInfo = document.getLineInformation(lineNum);
+ char[] chars = document.get(lineInfo.getOffset(), lineInfo.getLength()).toCharArray();
+
+ if (continued) {
+ int length = lastRegion.getLength();
+ length += document.getLineDelimiter(lineNum - 1).length();
+ length += lineInfo.getLength();
+ lastRegion = new Region(lastRegion.getOffset(), length);
+ } else {
+ lastRegion = lineInfo;
+ }
+
+ lineNum++;
+ return chars;
+ }
+
+ public IRegion region() {
+ if (lastRegion == null)
+ throw new IllegalStateException("Last region not available: either before start or after end of document.");
+ return lastRegion;
+ }
+
+ public String key() {
+ if (lastKey == null)
+ throw new IllegalStateException(
+ "Last key not available: either before state or after end of document, or last line type was not 'entry'.");
+ return lastKey;
+ }
+
+ public String value() {
+ if (lastValue == null)
+ throw new IllegalStateException(
+ "Last value not available: either before state or after end of document, or last line type was not 'entry'.");
+ return lastValue;
+ }
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/properties/Region.java b/bundleplugin/src/main/java/aQute/lib/properties/Region.java
new file mode 100644
index 0000000..abf567c
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/properties/Region.java
@@ -0,0 +1,20 @@
+package aQute.lib.properties;
+
+public class Region implements IRegion {
+
+ private final int length;
+ private final int offset;
+
+ public Region(int length, int offset) {
+ this.length = length;
+ this.offset = offset;
+ }
+
+ public int getLength() {
+ return length;
+ }
+
+ public int getOffset() {
+ return offset;
+ }
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/properties/packageinfo b/bundleplugin/src/main/java/aQute/lib/properties/packageinfo
new file mode 100644
index 0000000..a4f1546
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/properties/packageinfo
@@ -0,0 +1 @@
+version 1.0
\ No newline at end of file