com.slytechs.utils.region.FlexRegion.java Source code

Java tutorial

Introduction

Here is the source code for com.slytechs.utils.region.FlexRegion.java

Source

/**
 * 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();
    }
}