
com.gc.iotools.stream.is.RandomAccessInputStream Maven / Gradle / Ivy
Show all versions of easystream Show documentation
package com.gc.iotools.stream.is;
/*
* Copyright (c) 2008, 2015 Gabriele Contini. This source code is released
* under the BSD License.
*/
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import com.gc.iotools.stream.base.AbstractInputStreamWrapper;
import com.gc.iotools.stream.store.SeekableStore;
import com.gc.iotools.stream.store.Store;
import com.gc.iotools.stream.store.ThresholdStore;
/**
*
* A RandomAccessInputStream
adds functionality to another input
* stream-namely, the ability to buffer the input, allowing it to be read
* multiple times, and to support the mark
and reset
* methods.
*
*
* When the RandomAccessInputStream
is created, an internal
* {@linkplain Store} is created. As bytes from the stream are read or skipped,
* the internal store
is refilled as necessary from the source
* input stream. The implementation of store
can be changed to fit
* the application needs: cache on disk rather than in memory. The default
* store
implementation caches 64K in memory and then write the
* content on disk.
*
*
* It also adds the functionality of marking an InputStream
without
* specifying a mark length, thus allowing a reset
after an
* indefinite length of bytes has been read. Check the {@link #mark(int))}
* javadoc for details.
*
*
* Internally it uses a {@link RandomAccessFile} to cache and seek the data.
* Since it must be able to random seek it can't be (easily) buffered
* internally. External programs should wrap this class with a
* {@link BufferedInputStream} to improve performances (especially if
* int read()
method is called).
*
*
* @author gabriele.contini
* @see Store
* @since 1.2.0
* @version $Id: RandomAccessInputStream.java 527 2014-02-24 19:29:50Z
* $
*/
public class RandomAccessInputStream extends AbstractInputStreamWrapper {
/**
* Default size for passing from memory allocation to disk allocation for
* the buffer.
*/
public static final int DEFAULT_DISK_TRHESHOLD = 32768 * 2;
protected long markLimit = 0;
/**
* Position in the stream when the mark() was issued.
*/
protected long markPosition = 0;
/**
* Position of read cursor in the RandomAccessInputStream.
*/
protected long randomAccessIsPosition = 0;
/**
* Position of reading in the source stream.
*/
protected long sourcePosition = 0;
/**
* Store where data is kept.
*/
private Store store;
/**
*
* Constructor for RandomAccessInputStream.
*
*
* @param source
* The underlying input stream.
*/
public RandomAccessInputStream(final InputStream source) {
this(source, DEFAULT_DISK_TRHESHOLD);
}
/**
*
* Creates a RandomAccessInputStream
with the specified
* treshold, and saves its argument, the input stream source
,
* for later use.
*
*
* When data read under threshold size treshold
is kept into
* memory. Over this size it is placed on disk.
*
*
* @see ThresholdStore
* @param source
* The underlying input stream.
* @param threshold
* Maximum number of bytes to keep into memory.
*/
public RandomAccessInputStream(final InputStream source, final int threshold) {
super(source);
this.store = new ThresholdStore(threshold);
}
/**
*
* Constructor for RandomAccessInputStream.
*
*
* @param source
* The underlying input stream.
* @param store
* a {@link com.gc.iotools.stream.store.SeekableStore} object.
*/
public RandomAccessInputStream(final InputStream source,
final SeekableStore store) {
super(source);
if (store == null) {
throw new IllegalArgumentException("store can't be null.");
}
this.store = store;
}
/** {@inheritDoc} */
@Override
public int available() throws IOException {
return (int) Math.min(this.sourcePosition - this.randomAccessIsPosition
+ this.source.available(), Integer.MAX_VALUE);
}
/** {@inheritDoc} */
@Override
protected void closeOnce() throws IOException {
this.store.cleanup();
this.source.close();
}
/**
* Return the underlying store where the cache of data is kept.
*
* @return The underlying store that caches data.
*/
public Store getStore() {
return this.store;
}
/** {@inheritDoc} */
@Override
protected int innerRead(final byte[] b, final int off, final int len)
throws IOException {
int n;
if (this.sourcePosition == this.randomAccessIsPosition) {
// source and external same position so read from source.
n = super.source.read(b, off, len);
if (n > 0) {
this.sourcePosition += n;
this.randomAccessIsPosition += n;
this.store.put(b, off, n);
}
} else if (this.randomAccessIsPosition < this.sourcePosition) {
// resetIS has been called. Read from buffer;n
final int efflen = (int) Math.min(len, this.sourcePosition
- this.randomAccessIsPosition);
n = this.store.get(b, off, efflen);
if (n <= 0) {
throw new IllegalStateException(
"Problem reading from buffer. Expecting bytes ["
+ efflen + "] but buffer is empty.");
}
this.randomAccessIsPosition += n;
} else {
/*
* shouldn't be here. refactor throw exception
* randomAccessIsPosition > sourcePosition. A reset() was called on
* the StorageBufInputStream. just read from source don't buffer.
*/
// final int efflen = (int) Math.min(len,
// this.randomAccessIsPosition - this.sourcePosition);
// n = this.source.read(b, off, efflen);
// this.sourcePosition += Math.max(n, 0);
throw new IllegalStateException("randomAccessIsPosition["
+ this.randomAccessIsPosition + "] > sourcePosition["
+ this.sourcePosition + "]");
}
return n;
}
/**
* {@inheritDoc}
*
*
* Marks the current position in this input stream. A subsequent call to the
* {@linkplain #reset()} method repositions this stream at the last marked
* position so that subsequent reads re-read the same bytes.
*
*
* This method extends the original behavior of the class
* InputStream
allowing to use indefinite marking.
*
* readLimit> 0
The readLimit
arguments
* tells this input stream to allow that many bytes to be read before the
* mark position gets invalidated.
* readLimit == 0
Invalidate the all the current marks and
* clean up the temporary files.
* readLimit < 0
Set up an indefinite mark: reset can
* be invoked regardless on how many bytes have been read.
*
*
*
* @see RandomAccessInputStream#reset()
* @see java.io.InputStream#reset()
*/
@Override
public synchronized void mark(final int readLimit) {
this.markLimit = readLimit;
this.markPosition = this.randomAccessIsPosition;
}
/**
* {@inheritDoc}
*
* Overrides the markSupported()
method of the
* InputStream
class.
*
* @see InputStream#markSupported();
*/
@Override
public boolean markSupported() {
return true;
}
/**
* {@inheritDoc}
*
*
* Repositions this stream to the position at the time the mark
* method was last called on this input stream.
*
*
* After invoking mark
it can be invoked multiple times and it
* always reset the stream at the previously marked position.
*
*
* @exception IOException
* if this stream has not been marked or if the mark has been
* invalidated.
* @see RandomAccessInputStream#mark(int)
* @see java.io.InputStream#reset()
*/
@Override
public synchronized void reset() throws IOException {
if ((this.markLimit < 0)
|| (this.randomAccessIsPosition - this.markPosition <= this.markLimit)) {
this.randomAccessIsPosition = this.markPosition;
((SeekableStore) this.store).seek(this.markPosition);
} else {
throw new IOException("Reset to an invalid mark.");
}
}
/**
*
* Reposition the read pointer in the stream. Next read will start from the
* position passed as argument.
*
*
* @param position
* a long indicating the position in the stream we want to go.
* @throws java.io.IOException
* throw if any IOException occurs reading the underlying stream
* or if it is attempted a seek to a position greater to the
* effective number of bytes in the stream.
*/
public void seek(final long position) throws IOException {
if (position < 0) {
throw new IllegalArgumentException("Seek to negative position ["
+ position + "]");
}
if (!(this.store instanceof SeekableStore)) {
throw new IllegalStateException("Seek was called but the store["
+ this.store + "] is not an instance of ["
+ SeekableStore.class + "]");
}
final long startingPos = this.randomAccessIsPosition;
final long len = position - this.randomAccessIsPosition;
if (len > 0) {
final long n = skip(len);
if (n < len) {
throw new IOException("Requested seek to [" + position
+ "] but the stream is only ["
+ (this.randomAccessIsPosition)
+ "] bytes long. skipped[" + n + "] startingPos["
+ startingPos + "].");
}
} else if (len < 0) {
// if len==0 already at the right place. Do Nothing.
this.randomAccessIsPosition = position;
((SeekableStore) this.store).seek(position);
}
}
/**
*
* Setter for the field store
.
*
*
* @param store
* a {@link com.gc.iotools.stream.store.Store} object.
*/
public void setStore(final Store store) {
this.store = store;
}
/**
* {@inheritDoc}
*
* Provides a String representation of the state of the stream for debugging
* purposes.
*/
@Override
public String toString() {
return this.getClass().getSimpleName() + "[randomAccPos="
+ this.randomAccessIsPosition + ",srcPos="
+ this.sourcePosition + ", store=" + this.store + "]";
}
}