/* Copyright (c) 2012, Adam Retter All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Adam Retter Consulting nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Adam Retter BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.exist.util.io; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; /** * Implementation of an Input Stream Filter that extends any InputStream with * mark() and reset() capabilities by caching the read data for later * re-reading. * * NOTE - Only supports reading data up to 2GB as the cache index uses an 'int' * index * * @version 1.1 * * @author Adam Retter <adam.retter@googlemail.com> * @author Tobi Krebs <tobi.krebs AT gmail.com> */ public class CachingFilterInputStream extends FilterInputStream { //TODO what about if the underlying stream supports marking //then we could just use its capabilities? private final FilterInputStreamCache cache; private int srcOffset = 0; private int mark = 0; /** * Constructor which uses an existing Cache from a CachingFilterInputStream, * if inputStream is a CachingFilterInputStream. * * @param inputStream */ public CachingFilterInputStream(InputStream inputStream) throws InstantiationException { super(null); if (inputStream instanceof CachingFilterInputStream) { this.cache = ((CachingFilterInputStream) inputStream).getCache(); } else { throw new InstantiationException("Only CachingFilterInputStream are supported as InputStream"); } } public CachingFilterInputStream(final FilterInputStreamCache cache) { super(null); this.cache = cache; } /** * Gets the cache implementation */ private FilterInputStreamCache getCache() { return cache; } @Override public int available() throws IOException { return getCache().available() - srcOffset; } @Override public synchronized void mark(final int readLimit) { mark = srcOffset; } @Override public boolean markSupported() { return true; } @Override public synchronized void reset() throws IOException { srcOffset = mark; } @Override public int read() throws IOException { if (getCache().isSrcClosed()) { throw new IOException(FilterInputStreamCache.INPUTSTREAM_CLOSED); } //Read from cache if (useCache()) { final int data = getCache().get(srcOffset++); return data; } else { final int data = getCache().read(); if(data == FileFilterInputStreamCache.END_OF_STREAM) { return FilterInputStreamCache.END_OF_STREAM; } srcOffset++; return data; } } @Override public int read(final byte[] b) throws IOException { return read(b, 0, b.length); } @Override public int read(final byte[] b, final int off, final int len) throws IOException { if (getCache().isSrcClosed()) { throw new IOException(FilterInputStreamCache.INPUTSTREAM_CLOSED); } if (useCache()) { //copy data from the cache int actualLen = (len > getCache().getLength() - this.srcOffset ? getCache().getLength() - this.srcOffset : len); getCache().copyTo(this.srcOffset, b, off, actualLen); this.srcOffset += actualLen; //if the requested bytes were more than what is present in the cache, then also read from the src if (actualLen < len) { int srcLen = getCache().read(b, off + actualLen, len - actualLen); //have we reached the end of the stream? if (srcLen == FilterInputStreamCache.END_OF_STREAM) { return actualLen; } //increase srcOffset due to the read opertaion above srcOffset += srcLen; actualLen += srcLen; } return actualLen; } else { int actualLen = getCache().read(b, off, len); //have we reached the end of the stream? if (actualLen == FilterInputStreamCache.END_OF_STREAM) { return actualLen; } //increase srcOffset due to read operation above srcOffset += actualLen; return actualLen; } } /** * Closes the src InputStream and empties the cache */ @Override public void close() throws IOException { if(!getCache().isSrcClosed()) { getCache().close(); } } /** * We cant actually skip as we need to read so that we can cache the data, * however apart from the potentially increased I/O and Memory, the end * result is the same */ @Override public long skip(final long len) throws IOException { if (getCache().isSrcClosed()) { throw new IOException(FilterInputStreamCache.INPUTSTREAM_CLOSED); } else if (len < 1) { return 0; } if (useCache()) { //skip data from the cache long actualLen = (len > getCache().getLength() - this.srcOffset ? getCache().getLength() - this.srcOffset : len); //if the requested bytes were more than what is present in the cache, then also read from the src if (actualLen < len) { final byte skipped[] = new byte[(int) (len - actualLen)]; int srcLen = getCache().read(skipped); //have we reached the end of the stream? if (srcLen == FilterInputStreamCache.END_OF_STREAM) { return actualLen; } //increase srcOffset due to the read operation above srcOffset += srcLen; actualLen += srcLen; } return actualLen; } else { final byte skipped[] = new byte[(int) len]; //TODO could overflow int actualLen = getCache().read(skipped); //increase srcOffset due to read operation above srcOffset += actualLen; return actualLen; } } private boolean useCache() { //If cache hasRead and srcOffset is still in cache useCache return getCache().getSrcOffset() > 0 && getCache().getLength() > srcOffset; } public void register(InputStream inputStream) { getCache().register(inputStream); } public void deregister(InputStream inputStream) { getCache().deregister(inputStream); } }