AbstractSeekableByteChannel.java

package emissary.core.channels;

import org.apache.commons.lang3.Validate;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.NonWritableChannelException;
import java.nio.channels.SeekableByteChannel;

/**
 * Core implementation of the {@link SeekableByteChannel} interface
 */
public abstract class AbstractSeekableByteChannel implements SeekableByteChannel {
    /**
     * Boolean describing whether the SeekableByteChannel is open or closed.
     */
    private boolean open = true;
    /**
     * The current position of the SeekableByteChannel.
     */
    private long position = 0;

    /**
     * Used during {@link #read(ByteBuffer)} to calculate resizing a ByteBuffer
     */
    private static final BigInteger bigIntMaxValue = BigInteger.valueOf(Integer.MAX_VALUE);

    /**
     * Create a new SBC
     */
    protected AbstractSeekableByteChannel() {}

    /**
     * Real close implementation
     * 
     * @throws IOException if an error occurs
     */
    protected abstract void closeImpl() throws IOException;

    /**
     * Real read implementation. The provided byteBuffer will be properly sized (limited) on the way in.
     * 
     * @param byteBuffer to read from the SBC into.
     * @return the number of bytes read
     * @throws IOException if an error occurs
     */
    protected abstract int readImpl(ByteBuffer byteBuffer) throws IOException;

    /**
     * Real size implementation
     * 
     * @return the size of the channel
     * @throws IOException if an error occurs
     */
    protected abstract long sizeImpl() throws IOException;

    /**
     * Close the channel and mark as such
     */
    @Override
    public final void close() throws IOException {
        if (open) {
            open = false;
            closeImpl();
        }
    }

    /**
     * Determine whether the channel is marked as open/closed
     * 
     * @return if the channel is open
     */
    @Override
    public final boolean isOpen() {
        return open;
    }

    /**
     * If the channel is open, return the current position
     * 
     * @return the current position if the channel is still open
     */
    @Override
    public final long position() throws IOException {
        checkOpen(open);
        return position;
    }

    /**
     * Set the position of the channel. Must be greater than -1, can be beyond the length of the channel.
     * 
     * @param position to set within the channel
     */
    @Override
    public final SeekableByteChannel position(final long position) throws IOException {
        checkOpen(open);
        Validate.isTrue(position >= 0, "Required: position >= 0");
        this.position = position;
        return this;
    }

    /**
     * Read bytes from the channel into the provided buffer, if the channel is still open.
     * 
     * Relies on the implementation provided by the extending class to actually carry out the read.
     * 
     * @param byteBuffer to read into
     * @throws IOException if an error occurs
     */
    @Override
    public final int read(final ByteBuffer byteBuffer) throws IOException {
        checkOpen(open);
        Validate.notNull(byteBuffer, "Required: byteBuffer != null");

        // If we're at the end of the file, early return
        if (!hasRemaining()) {
            return -1;
        }

        // Remaining bytes in this channel
        final long channelRemaining = remaining();
        // Remaining bytes in the provided buffer
        final int byteBufferRemaining = byteBuffer.remaining();
        // Store off the current limit in case we need to update it
        final int byteBufferLimit = byteBuffer.limit();

        // If the byte buffer has more bytes left than the channel, we want to right-size it for
        // implementations to be able to 'simply' just read into the byteBuffer.
        if (byteBufferRemaining > channelRemaining) {
            // Get the new limit in a safe way to avoid arithmetic exception issues
            final int newLimit = BigInteger.valueOf(channelRemaining).add(BigInteger.valueOf(byteBuffer.position()))
                    .min(bigIntMaxValue).intValue();
            // Update the limit of the byteBuffer temporarily whilst we carry out the read
            // This will be reset to the original limit before returning
            byteBuffer.limit(newLimit);
        }

        try {
            // Actually carry out the read, and keep how many bytes read for later return
            final int bytesRead = readImpl(byteBuffer);
            // Update position of channel
            position(position() + bytesRead);
            // Return the amount of bytes read from the channel
            return bytesRead;
        } finally {
            // Update limit of byteBuffer, which may have been reduced to ensure a safe read
            byteBuffer.limit(byteBufferLimit);
        }
    }

    /**
     * Whether this channel has any bytes remaining.
     * 
     * @return true if there are bytes remaining
     * @throws IOException if an error occurs
     */
    public final boolean hasRemaining() throws IOException {
        return remaining() > 0;
    }

    /**
     * The amount of bytes remaining in this channel (size - current position).
     * 
     * @return amount of bytes remaining
     * @throws IOException if an error occurs
     */
    public final long remaining() throws IOException {
        return size() - position();
    }

    /**
     * Return the size of the channel if the channel is still open.
     * 
     * This adheres to the {@link SeekableByteChannel} specification.
     * 
     * @throws IOException if an error occurs
     */
    @Override
    public final long size() throws IOException {
        checkOpen(open);
        return sizeImpl();
    }

    /**
     * Block truncation of the channel, keep it immutable. Will throw {@link NonWritableChannelException}
     * 
     * @param size to set the channel to
     * @throws IOException if an error occurs
     */
    @Override
    @SuppressWarnings("CheckedExceptionNotThrown")
    public final SeekableByteChannel truncate(final long size) throws IOException {
        throw new NonWritableChannelException();
    }

    /**
     * Block writing of the channel, keep it immutable. Will throw {@link NonWritableChannelException}
     * 
     * @param byteBuffer to write from
     */
    @Override
    @SuppressWarnings("CheckedExceptionNotThrown")
    public final int write(final ByteBuffer byteBuffer) throws IOException {
        throw new NonWritableChannelException();
    }

    /**
     * Validate the channel is still open, otherwise throw a {@link ClosedChannelException}
     * 
     * @param open if the channel is open or not
     * @throws IOException if an error occurs
     */
    private static void checkOpen(final boolean open) throws IOException {
        if (!open) {
            throw new ClosedChannelException();
        }
    }
}