blob: f3520934d3eafa8432ad42e33b39791275adfe8e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.tika.utils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Wraps an input stream, reading it only once, but making it available
* for rereading an arbitrary number of times. The stream's bytes are
* stored in memory up to a user specified maximum, and then stored in a
* temporary file which is deleted when this class' close() method is called.
*/
public class RereadableInputStream extends InputStream {
/**
* Input stream originally passed to the constructor.
*/
private InputStream originalInputStream;
/**
* The inputStream currently being used by this object to read contents;
* may be the original stream passed in, or a stream that reads
* the saved copy.
*/
private InputStream inputStream;
/**
* Maximum number of bytes that can be stored in memory before
* storage will be moved to a temporary file.
*/
private int maxBytesInMemory;
/**
* True when the original stream is being read; set to false when
* reading is set to use the stored data instead.
*/
private boolean firstPass = true;
/**
* Whether or not the stream's contents are being stored in a file
* as opposed to memory.
*/
private boolean bufferIsInFile;
/**
* The buffer used to store the stream's content; this storage is moved
* to a file when the stored data's size exceeds maxBytesInMemory.
*/
private byte[] byteBuffer;
/**
* The total number of bytes read from the original stream at the time.
*/
private int size;
/**
* File used to store the stream's contents; is null until the stored
* content's size exceeds maxBytesInMemory.
*/
private File storeFile;
/**
* OutputStream used to save the content of the input stream in a
* temporary file.
*/
private OutputStream storeOutputStream;
/**
* Specifies whether or not to read to the end of stream on first
* rewind. This defaults to true. If this is set to false,
* then the first time when rewind() is called, only those bytes
* already read from the original stream will be available from then on.
*/
private boolean readToEndOfStreamOnFirstRewind = true;
/**
* Specifies whether or not to close the original input stream
* when close() is called. Defaults to true.
*/
private boolean closeOriginalStreamOnClose = true;
// TODO: At some point it would be better to replace the current approach
// (specifying the above) with more automated behavior. The stream could
// keep the original stream open until EOF was reached. For example, if:
//
// the original stream is 10 bytes, and
// only 2 bytes are read on the first pass
// rewind() is called
// 5 bytes are read
//
// In this case, this instance gets the first 2 from its store,
// and the next 3 from the original stream, saving those additional 3
// bytes in the store. In this way, only the maximum number of bytes
// ever needed must be saved in the store; unused bytes are never read.
// The original stream is closed when EOF is reached, or when close()
// is called, whichever comes first. Using this approach eliminates
// the need to specify the flag (though makes implementation more complex).
/**
* Creates a rereadable input stream.
*
* @param inputStream stream containing the source of data
* @param maxBytesInMemory maximum number of bytes to use to store
* the stream's contents in memory before switching to disk; note that
* the instance will preallocate a byte array whose size is
* maxBytesInMemory. This byte array will be made available for
* garbage collection (i.e. its reference set to null) when the
* content size exceeds the array's size, when close() is called, or
* when there are no more references to the instance.
* @param readToEndOfStreamOnFirstRewind Specifies whether or not to
* read to the end of stream on first rewind. If this is set to false,
* then when rewind() is first called, only those bytes already read
* from the original stream will be available from then on.
*/
public RereadableInputStream(InputStream inputStream, int maxBytesInMemory,
boolean readToEndOfStreamOnFirstRewind,
boolean closeOriginalStreamOnClose) {
this.inputStream = inputStream;
this.originalInputStream = inputStream;
this.maxBytesInMemory = maxBytesInMemory;
byteBuffer = new byte[maxBytesInMemory];
this.readToEndOfStreamOnFirstRewind = readToEndOfStreamOnFirstRewind;
this.closeOriginalStreamOnClose = closeOriginalStreamOnClose;
}
/**
* Reads a byte from the stream, saving it in the store if it is being
* read from the original stream. Implements the abstract
* InputStream.read().
*
* @return the read byte, or -1 on end of stream.
* @throws IOException
*/
public int read() throws IOException {
int inputByte = inputStream.read();
if (firstPass) {
saveByte(inputByte);
}
return inputByte;
}
/**
* "Rewinds" the stream to the beginning for rereading.
* @throws IOException
*/
public void rewind() throws IOException {
if (firstPass && readToEndOfStreamOnFirstRewind) {
// Force read to end of stream to fill store with any
// remaining bytes from original stream.
while(read() != -1) {
// empty loop
}
}
closeStream();
if (storeOutputStream != null) {
storeOutputStream.close();
storeOutputStream = null;
}
firstPass = false;
boolean newStreamIsInMemory = (size < maxBytesInMemory);
inputStream = newStreamIsInMemory
? new ByteArrayInputStream(byteBuffer)
: new BufferedInputStream(new FileInputStream(storeFile));
}
/**
* Closes the input stream currently used for reading (may either be
* the original stream or a memory or file stream after the first pass).
*
* @throws IOException
*/
// Does anyone need/want for this to be public?
private void closeStream() throws IOException {
if (inputStream != null
&&
(inputStream != originalInputStream
|| closeOriginalStreamOnClose)) {
inputStream.close();
inputStream = null;
}
}
/**
* Closes the input stream and removes the temporary file if one was
* created.
*
* @throws IOException
*/
public void close() throws IOException {
closeStream();
super.close();
if (storeFile != null) {
storeFile.delete();
}
}
/**
* Returns the number of bytes read from the original stream.
*
* @return number of bytes read
*/
public int getSize() {
return size;
}
/**
* Saves the byte read from the original stream to the store.
*
* @param inputByte byte read from original stream
* @throws IOException
*/
private void saveByte(int inputByte) throws IOException {
if (!bufferIsInFile) {
boolean switchToFile = (size == (maxBytesInMemory));
if (switchToFile) {
storeFile = File.createTempFile("TIKA_streamstore_", ".tmp");
bufferIsInFile = true;
storeOutputStream = new BufferedOutputStream(
new FileOutputStream(storeFile));
storeOutputStream.write(byteBuffer, 0, size);
storeOutputStream.write(inputByte);
byteBuffer = null; // release for garbage collection
} else {
byteBuffer[size] = (byte) inputByte;
}
} else {
storeOutputStream.write(inputByte);
}
++size;
}
}