/**
* Copyright (C) 2007 Sly Technologies, Inc. This library 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 2.1 of the License, or (at your option) any later version. This
* library 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 this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package com.slytechs.utils.region;
import java.io.IOException;
import java.nio.ReadOnlyBufferException;
import java.util.Iterator;
import java.util.LinkedList;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.slytechs.utils.collection.Readonly;
import com.slytechs.utils.region.FlexRegionListener.FlexRegiontSupport;
/**
* @author Mark Bednarczyk
* @author Sly Technologies, Inc.
*/
public class FlexRegion<T> implements Iterable<RegionSegment<T>>, Changable {
public static final Log logger = LogFactory.getLog(FlexRegion.class);
private final boolean append;
private long changeId = System.currentTimeMillis();
protected Region<T> initialRegion;
protected long length = 0;
private final boolean readonly;
protected final LinkedList<RegionSegment<T>> segments = new LinkedList<RegionSegment<T>>();
private final FlexRegiontSupport<T> support = new FlexRegiontSupport<T>(this);
private boolean modifiedAtLeastOnce = false;
/**
* @param readonly
* @param append
*/
protected FlexRegion(final boolean readonly, final boolean append) {
this.readonly = readonly;
this.append = append;
}
/**
* @param readonly
* creates a readonly flex region. No mutable operations are
* supported and will throw an immediate Readonly exception if any
* are used.
* @param append
* TODO
* @param length
* @param data
*/
public FlexRegion(final boolean readonly, final boolean append,
final long length, final T data) {
this.readonly = readonly;
this.append = append;
if (logger.isTraceEnabled()) {
logger.trace("readonly=" + readonly + ", append=" + append + ", length="
+ length + ", data=" + data);
}
init(length, data);
}
/**
* @param o
* @return
* @see java.util.ArrayList#add(java.lang.Object)
*/
public boolean add(final FlexRegionListener<T> o) {
return this.support.add(o);
}
public final RegionSegment[] append(final long length, final T data) {
this.throwIfNoAppend();
if (length == 0) {
return null; // Nothing to do
}
this.modifiedAtLeastOnce = true;
this.support.fireAppend(length, data);
this.changeHappened();
// The last element of data is at (length - 1), so we append after the last
// byte
final long position = this.length;
final RegionSegment<T> newSegment = createSegment(position, length, data);
this.segments.addLast(newSegment);
this.length += length;
final RegionSegment[] newSegments = new RegionSegment[1];
newSegments[0] = newSegment;
this.support.fireLinkSegment(newSegments);
if (logger.isTraceEnabled()) {
logger.trace("length=" + length + ", data=" + data + " AFTER:"
+ toString());
}
return newSegments;
}
private final void changeGlobalStart(final int index, final long delta) {
final Iterator<RegionSegment<T>> i = this.segments.listIterator(index);
while (i.hasNext()) {
final RegionSegment<T> s = i.next();
s.addToStartGlobal(delta);
}
}
public final void changeHappened() {
if (this.changeId == Long.MAX_VALUE) {
this.changeId = Long.MIN_VALUE;
} else {
this.changeId++;
}
}
public final boolean checkBoundsGlobal(final long global) {
return (global >= 0) && (global < this.length);
}
public final RegionSegment[] clear() {
if (this.isModified() == false) {
return null; // Nothing to do
}
support.fireAbortAllChanges();
this.segments.clear();
final RegionSegment<T> s = createSegment(this.initialRegion, 0, 0,
this.initialRegion.getLength());
this.segments.add(s);
this.length = this.initialRegion.getLength();
this.changeHappened();
final RegionSegment[] newSegments = new RegionSegment[] { s };
support.fireLinkSegment(newSegments);
return newSegments;
}
/**
*
*/
public void close() {
/*
* We really don't need to do anything here, except may be print out a debug
* message.
*/
if (logger.isDebugEnabled() && modifiedAtLeastOnce
|| logger.isTraceEnabled()) {
logger.debug(" " + toString() + (modifiedAtLeastOnce ? "*Modified" : ""));
}
}
/**
* @param listener
* @return
* @see java.util.ArrayList#contains(java.lang.Object)
*/
public boolean contains(final FlexRegionListener<T> listener) {
return this.support.contains(listener);
}
/**
* @param i
* @return
*/
public final RegionHandle<T> createHandle(final long global) {
final RegionSegment<T> s = this.getSegment(global);
final long regional = s.mapGlobalToRegional(global);
final RegionHandle<T> h = new RegionHandle<T>(s, regional, this);
return h;
}
/**
* @param changable
* @param i
* @param length
* @param data
* @return
*/
protected RegionSegment<T> createSegment(Changable changable, long global,
long length, T data) {
final RegionSegment<T> segment = new RegionSegment<T>(this, 0, length, data);
return segment;
}
protected RegionSegment<T> createSegment(long global, long length, T data) {
final RegionSegment<T> segment = new RegionSegment<T>(this, global, length,
data);
return segment;
}
/**
* @param region
* @param endGlobal
* @param thirdStartRegional
* @param thirdLength
* @return
*/
protected RegionSegment<T> createSegment(Region<T> region, long global,
long regional, long length) {
final RegionSegment<T> segment = new RegionSegment<T>(region, global,
regional, length);
return segment;
}
public final RegionSegment[] flatten(final T data) {
return this.flatten(data, this.length);
}
public final RegionSegment[] flatten(final T data, final long length) {
if (logger.isDebugEnabled()) {
logger.debug("BEFORE:" + toString());
}
// final RegionSegment<T> initial = new RegionSegment<T>(this, 0, length,
// data);
final RegionSegment<T> initial = createSegment((Changable) this, 0, length,
data);
support.fireFlatten(data);
/*
* Set forward references for all segments. Each segment will use its
* current GLOBAL address as a REGIONAL address into the new "initial"
* segment.
*/
for (final RegionSegment<T> segment : this.segments) {
final Region<T> region = segment.getRegion();
if (region.getForward() != initial.getRegion()) {
region.setForward(initial.getRegion());
/*
* Tell each data element within each region that its underlying content
* should be set to readonly, since we do not want any more changes to
* the region's data. All changes should be propagated to the forwarded
* region and its data.
*/
final T d = region.getData();
if (d instanceof Readonly) {
((Readonly) d).setReadonly(true);
}
}
}
/*
* Remove all old segments
*/
this.segments.clear();
/*
* Now add the replacement "init" segment which is the entire updated file
*/
this.segments.add(initial);
this.initialRegion = initial.getRegion();
this.changeHappened();
final RegionSegment[] newSegments = new RegionSegment[] { initial };
support.fireLinkSegment(newSegments);
if (logger.isDebugEnabled()) {
logger.debug(" AFTER:" + toString());
}
return newSegments;
}
public final long getChangeId() {
return this.changeId;
}
public final T getData(final long global) {
final RegionSegment<T> s = this.getSegment(global);
return s.getData();
}
public final long getLength() {
return this.length;
}
/**
* @param index
* @return
* @throws IOException
*/
public Object getRegionalData(final int global) throws IOException {
final RegionSegment<T> segment = this.getSegment(global);
final long regional = segment.mapGlobalToRegional(global);
final T data = segment.getData();
if (data instanceof RegionDataGetter) {
final RegionDataGetter getter = (RegionDataGetter) data;
return getter.get(regional);
}
throw new UnsupportedOperationException(
"This operation is not supported by the data object. "
+ "The data object needs to implement the RegionDataGetter "
+ "interface.");
}
public final RegionSegment<T> getSegment(final long position) {
/*
* Optimize for 1 single segment list, no need to initiate an Iterator
* object
*/
final RegionSegment<T> first = this.segments.getFirst();
if (first.checkBoundsGlobal(position)) {
return first;
}
final int s = this.segments.size();
/*
* Optimize for very large number of segments in the list Last else does a
* direct search for list sizes under 1000
*/
if (s >= 1000000) {
return this.getSegment(position, 0, 1000000);
} else if (s >= 100000) {
return this.getSegment(position, 0, 100000);
} else if (s >= 10000) {
return this.getSegment(position, 0, 10000);
} else if (s >= 1000) {
return this.getSegment(position, 0, 1000);
} else {
return this.getSegment(position, 0);
}
}
public final RegionSegment<T> getSegment(final long global, final int start) {
int i = 0;
final Iterator<RegionSegment<T>> l = this.segments.listIterator(start);
while (l.hasNext()) {
final RegionSegment<T> ds = l.next();
if (ds.checkBoundsGlobal(global)) {
return ds;
}
i++;
}
throw new IndexOutOfBoundsException("Position out of bounds [" + global
+ "]");
}
private final RegionSegment<T> getSegment(final long position,
final int start, final int power) {
final int s = this.segments.size();
int l = start;
if (power == 100) {
return this.getSegment(position, start);
}
for (int i = start + power; i < s; i += power) {
if (position < this.segments.get(i).getStartGlobal()) {
break;
}
l = i;
}
return this.getSegment(position, l, power / 10);
}
public final int getSegmentCount() {
return this.segments.size();
}
private final int getSegmentIndex(final long position) {
/*
* Optimize for 1 single segment list, no need to initiate an Iterator
* object
*/
if ((this.segments.size() == 1)
&& this.segments.getFirst().checkBoundsGlobal(position)) {
return 0;
}
int i = 0;
for (final RegionSegment<T> ds : this.segments) {
if (ds.checkBoundsGlobal(position)) {
return i;
}
i++;
}
throw new IndexOutOfBoundsException("Position out of bounds [" + position
+ "]");
}
/**
* @return
*/
public Iterable<RegionSegment<T>> getSegmentIterable() {
return new Iterable<RegionSegment<T>>() {
public Iterator<RegionSegment<T>> iterator() {
return segments.iterator();
}
};
}
protected void init(final long length, final T data) {
this.length = length;
final RegionSegment<T> ds = new RegionSegment<T>(this, 0, length, data);
this.initialRegion = ds.getRegion();
this.segments.add(ds);
}
public final void insert(final long position, final long length, final T data) {
if (length == 0) {
return; // Nothing to do
}
/*
* Special case: append at the end - the position is actually outside any
* active region, therefore only if we are positioned just 1 byte past the
* end of the last active region, we do an append instead of an insert.
*/
if (position == this.length) {
this.append(length, data);
} else {
this.replace(position, 0, length, data);
}
}
/**
* @return
*/
public boolean isAppend() {
return this.append;
}
/*
* (non-Javadoc)
*
* @see com.slytechs.utils.region.Changable#isChanged(long)
*/
public final boolean isChanged(final long changeId) {
return this.changeId != changeId;
}
public final boolean isModified() {
return this.segments.size() != 1;
}
public final boolean isModifiedByAppendOnly() {
final RegionSegment<T> first = this.segments.getFirst();
return (this.segments.size() > 1)
&& (first.getRegion() == this.initialRegion)
&& (first.getLength() == this.initialRegion.getLength());
}
/**
* @return
*/
public boolean isReadonly() {
return this.readonly;
}
/*
* (non-Javadoc)
*
* @see java.lang.Iterable#iterator()
*/
public final Iterator<RegionSegment<T>> iterator() {
return this.segments.iterator();
}
/**
* Creates a new region which may contain different data and who is actively
* synched with this region. Any changes to this region will be propagated and
* translated via the supplied translator in the linked region.
*
* @param <C>
* new type prarameter of the new region
* @param translator
* translator which will translate various even properties from one
* region type <T> to linked region <C>.
* @return new region that is linked to this region
*/
public <C> FlexRegion<C> linkedRegion(final RegionTranslator<C, T> translator) {
final FlexRegion<C> linked = new LinkedFlexRegion<C, T>(this, translator);
return linked;
}
/**
* Removes the specified listener from the active listeners list. The listener
* will no longer be notified of any events.
*
* @param listener
* listener to remove
* @return true if found and removed, otherwise false
*/
public boolean remove(final FlexRegionListener<T> listener) {
return this.support.remove(listener);
}
public final void remove(final long position, final long length) {
if (length == 0) {
return; // Nothing to do
}
this.replace(position, length, 0, null);
}
/**
* @param start
* global start
* @param oldLength
* number of elements to be replaced
* @param newLength
* number of elements to replace the old segment
* @param data
* data associated with this replacement
* @return TODO
*/
public final RegionSegment[] replace(final long start, final long oldLength,
final long newLength, final T data) {
this.throwIfReadonly();
if ((oldLength == 0) && (newLength == 0)) {
return null; // Nothing to do
}
this.modifiedAtLeastOnce = true;
this.support.fireReplace(start, oldLength, newLength, data);
this.changeHappened();
final int i = this.getSegmentIndex(start);
final RegionSegment<T> first = this.segments.get(i);
if ((first.checkBoundsGlobal(start) == false)
|| (first.checkBoundsGlobal(start, oldLength) == false)) {
throw new IndexOutOfBoundsException("Replacement request [" + start
+ "] falls outside the segment's [" + first.toString()
+ "] boundaries");
}
final long startLocal = first.mapGlobalToLocal(start);
final long firstEndLocal = first.getEndLocal();
final long endLocal = startLocal + oldLength;
final RegionSegment[] newSegment;
if (startLocal == 0) {
newSegment = new RegionSegment[1];
newSegment[0] = this.replaceFront(first, start, oldLength, newLength,
data);
} else if (endLocal == firstEndLocal) {
newSegment = new RegionSegment[1];
newSegment[0] = this
.replaceBack(first, start, oldLength, newLength, data);
} else {
newSegment = this.replaceMiddle(first, i, start, oldLength, newLength,
data);
}
this.support.fireLinkSegment(newSegment);
if (logger.isTraceEnabled()) {
logger.trace("start" + start + ", old=" + oldLength + ", new="
+ newLength + ", data=" + data + " AFTER:" + toString());
}
return newSegment;
}
/**
* @param start
* @param oldLength
* @param newLength
* @param data
*/
private final RegionSegment<T> replaceBack(final RegionSegment<T> first,
final long start, final long oldLength, final long newLength, final T data) {
final long delta = newLength - oldLength;
final int i = this.segments.indexOf(first);
this.changeGlobalStart(i + 1, delta);
this.length += delta;
final long firstOldLength = first.getLength();
first.setLength(firstOldLength + delta);
if (newLength != 0) {
final RegionSegment<T> newSegment = createSegment(first.getEndGlobal(),
newLength, data);
this.segments.add(i + 1, newSegment);
return newSegment;
}
return null;
}
/**
* @param start
* @param replacementLength
* @param newLength
* @param data
*/
private final RegionSegment<T> replaceFront(final RegionSegment<T> first,
final long start, final long replacementLength, final long newLength,
final T data) {
final long firstOldLength = first.getLength();
final int i = this.segments.indexOf(first);
final long delta = newLength - replacementLength;
this.changeGlobalStart(i + 1, delta);
this.length += delta;
/*
* Check if this is a total remove
*/
if (newLength == 0) {
if (replacementLength == firstOldLength) {
/*
* Removing entire segment?
*/
this.segments.remove(first);
} else {
/*
* Removing only the front portion of the segment. So shrink length and
* push out the regional start. Local and global starts are not affected
*/
first.setLength(firstOldLength - replacementLength);
first.addToStartRegional(replacementLength);
}
} else {
/*
* Ok, we're replacing a portion of the first segment with a new region
*/
final RegionSegment<T> newSegment = createSegment(start, newLength, data);
/*
* Check if we are replacing the entire segment
*/
if ((newLength == replacementLength) && (first.getLength() == newLength)) {
first.setValid(false);
this.segments.remove(i);
this.segments.add(i, newSegment);
} else {
first.addToStartRegional(replacementLength);
first.addToStartGlobal(newLength);
first.setLength(firstOldLength - replacementLength);
this.segments.add(i, newSegment);
}
return newSegment;
}
return null;
}
/**
* @param start
* @param replacedLength
* @param newLength
* @param data
*/
private final RegionSegment[] replaceMiddle(final RegionSegment<T> first,
final int i, final long start, final long replacedLength,
final long newLength, final T data) {
final long firstOldLength = first.getLength();
final long firstNewLength = first.mapGlobalToRegional(start
- first.getStartRegional());
first.setLength(firstNewLength);
final long thirdStartRegional = first.getEndRegional() + replacedLength;
final long thirdLength = firstOldLength - firstNewLength - replacedLength;
final RegionSegment<T> second = createSegment(first.getEndGlobal(),
newLength, data);
final RegionSegment<T> third = createSegment(first.getRegion(), second
.getEndGlobal(), thirdStartRegional, thirdLength);
final long globalDelta = newLength - replacedLength;
this.length += globalDelta;
this.changeGlobalStart(i + 1, globalDelta);
final RegionSegment[] newSegments;
if (second.getLength() == 0) {
this.segments.add(i + 1, third);
second.getRegion().remove(second);
newSegments = new RegionSegment[1];
newSegments[0] = third;
} else {
this.segments.add(i + 1, second);
this.segments.add(i + 2, third);
newSegments = new RegionSegment[2];
newSegments[0] = second;
newSegments[1] = third;
}
return newSegments;
}
private final void throwIfNoAppend() {
if (this.append) {
return;
}
throw new ReadOnlyBufferException();
}
private final void throwIfReadonly() {
if (this.readonly == false) {
return;
}
throw new ReadOnlyBufferException();
}
/**
* <p>
* Prints the current FlexRegion and its segments. The output will have the
* following format:
*
* <pre>
* [[A.0 - A.1/l=2/g=0/r=0], [B.2 - B.8/l=7/g=2/r=1]]
* </pre>
*
* Note: A and B are user supplied T type parameters supplied to the region
* and the operations on the region.
* </p>
* <p>
* The output shows the entire FlexRegion enclosed in the outter []. It has 2
* segments, each enclosed in inner []. First segment, is associated with a
* region who's user data parameter was "A". The segments range is from
* abstract position 0 to 1, inclusive. The other paramters:
* <ul>
* <li>l - length or number of elements within the segment (2)
* <li>g - global position of the first element within the segment (0)
* <li>r - regional position of the first element within the segment (0)1
* </ul>
* The second segment shows the same parameters but with differnt values.
* Range is from 2 to 8, inclusive:
* <ul>
* <li>l - length or number of elements within the segment (72)
* <li>g - global position of the first element within the segment (2)
* <li>r - regional position of the first element within the segment (1)
* </ul>
* </p>
*/
@Override
public String toString() {
return this.segments.toString();
}
}