/* RepeatableInputStream.java
Purpose:
Description:
History:
Mar 12, 2008 12:03:53 PM , Created by jumperchen
Copyright (C) 2008 Potix Corporation. All Rights Reserved.
{{IS_RIGHT
This program is distributed under LGPL Version 2.1 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.io;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.URL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.lang.Library;
/**
* {@link RepeatableInputStream} adds functionality to another input stream,
* the ability to read repeatedly.
* By repeatable-read we mean, after {@link #close}, the next invocation of
* {@link #read} will re-open the input stream.
*
* <p>{@link RepeatableInputStream} actually creates a temporary space
* to buffer the content, so it can be re-opened again after closed.
* Notice that the temporary space (a.k.a., the buffered input stream)
* is never closed until garbage-collected.
*
* <p>If the content size of the given input stream is smaller than
* the value specified in the system property called
* "org.zkoss.io.memoryLimitSize", the content will be buffered in
* the memory. If the size exceeds, the content will be buffered in
* a temporary file. By default, it is 512KB.
* Note: the maximal value is {@link Integer#MAX_VALUE}
*
* <p>If the content size of the given input stream is larger than
* the value specified in the system property called
* "org.zkoss.io.bufferLimitSize", the content won't be buffered,
* and it means the read is not repeatable. By default, it is 20MB.
* Note: the maximal value is {@link Integer#MAX_VALUE}
*
* @author jumperchen
* @author tomyeh
* @since 3.0.4
*/
public class RepeatableInputStream extends InputStream implements Repeatable,
Serializable {
private static final Logger log = LoggerFactory.getLogger(RepeatableInputStream.class);
/*package*/ static final String BUFFER_LIMIT_SIZE = "org.zkoss.io.bufferLimitSize";
/*package*/ static final String MEMORY_LIMIT_SIZE = "org.zkoss.io.memoryLimitSize";
private transient InputStream _org;
private transient OutputStream _out;
private transient InputStream _in;
private transient File _f;
/** The content size. It is meaningful only if !_nobuf.
* Note: int is enough (since long makes no sense for buffering)
*/
private int _cntsz;
private final int _bufmaxsz, _memmaxsz;
private boolean _nobuf;
private RepeatableInputStream(InputStream is) {
_org = is;
_bufmaxsz = Library.getIntProperty(BUFFER_LIMIT_SIZE, 20 * 1024 * 1024);
_memmaxsz = Library.getIntProperty(MEMORY_LIMIT_SIZE, 512 * 1024);
}
/**
* Returns an input stream that can be read repeatedly, or null if the
* given input stream is null.
* Note: the returned input stream encapsulates the given input stream, rd
* (a.k.a., the buffered input stream) to adds the functionality to
* re-opens the input stream once {@link #close} is called.
*
* <p>By repeatable-read we mean, after {@link #close}, the next
* invocation of {@link #read} will re-open the input stream.
*
* <p>Use this method instead of instantiating {@link RepeatableInputStream}
* with the constructor.
*
* @see #getInstance(File)
*/
public static InputStream getInstance(InputStream is) {
if (is instanceof ByteArrayInputStream)
return new ResetableInputStream(is);
else if (is != null && !(is instanceof Repeatable))
return new RepeatableInputStream(is);
return is;
}
/**
* Returns an input stream of a file that can be read repeatedly.
* Note: it assumes the file is binary (rather than text).
*
* <p>By repeatable-read we mean, after {@link #close}, the next
* invocation of {@link #read} will re-open the input stream.
*
* <p>Note: it is efficient since we don't have to buffer the
* content of the file to make it repeatable-read.
*
* @exception IllegalArgumentException if file is null.
* @exception FileNotFoundException if file not found
* @see #getInstance(InputStream)
* @see #getInstance(String)
*/
public static InputStream getInstance(File file)
throws FileNotFoundException {
if (file == null)
throw new IllegalArgumentException("null");
if (!file.exists())
throw new FileNotFoundException(file.toString());
return new RepeatableFileInputStream(file);
}
/**
* Returns an input stream of a file that can be read repeatedly.
* Note: it assumes the file is binary (rather than text).
*
* <p>By repeatable-read we mean, after {@link #close}, the next
* invocation of {@link #read} will re-open the input stream.
*
* <p>Note: it is efficient since we don't have to buffer the
* content of the file to make it repeatable-read.
*
* @exception IllegalArgumentException if file is null.
* @see #getInstance(InputStream)
* @see #getInstance(File)
*/
public static InputStream getInstance(String filename)
throws FileNotFoundException {
return getInstance(new File(filename));
}
/**
* Returns an input stream of the resource of the given URL
* that can be read repeatedly.
* Note: it assumes the resource is binary (rather than text).
*
* <p>By repeatable-read we mean, after {@link #close}, the next
* invocation of {@link #read} will re-open the input stream.
*
* <p>Note: it is efficient since we don't have to buffer the
* content of the resource to make it repeatable-read.
*
* @exception IllegalArgumentException if url is null.
* @see #getInstance(InputStream)
* @see #getInstance(String)
*/
public static InputStream getInstance(URL url) {
if (url == null)
throw new IllegalArgumentException("null");
return new RepeatableURLInputStream(url);
}
private OutputStream getOutputStream() throws IOException {
if (_out == null)
return _nobuf ? null: (_out = new ByteArrayOutputStream());
//it is possible _membufsz <= 0, but OK to use memory first
if (_cntsz >= _bufmaxsz) { //too large to buffer
disableBuffering();
return null;
}
if (_f == null && _cntsz >= _memmaxsz) { //memory to file
try {
final File f =
new File(System.getProperty("java.io.tmpdir"), "zk");
if (!f.isDirectory())
f.mkdir();
_f = File.createTempFile("zk.io", ".zk.io", f);
final byte[] bs = ((ByteArrayOutputStream)_out).toByteArray();
_out = new BufferedOutputStream(new FileOutputStream(_f));
_out.write(bs);
} catch (Throwable ex) {
log.warn("Ignored: failed to buffer to a file, "+_f+"\nCause: "+ex.getMessage());
disableBuffering();
}
}
return _out;
}
private void disableBuffering() {
_nobuf = true;
if (_out != null) {
try {
_out.close();
} catch (Throwable ex) { //ignore
}
_out = null;
}
if (_f != null) {
try {
_f.delete();
} catch (Throwable ex) { //ignore
}
_f = null;
}
}
public int read() throws IOException {
if (_org != null) {
final int b = _org.read();
if (!_nobuf)
if (b >= 0) {
final OutputStream out = getOutputStream();
if (out != null) out.write(b);
++_cntsz;
}
return b;
} else {
if (_in == null)
_in = new BufferedInputStream(new FileInputStream(_f)); //_f must be non-null
return _in.read();
}
}
/** Closes the current access, and the next call of {@link #close}
* re-opens the buffered input stream.
*/
public void close() throws IOException {
_cntsz = 0;
if (_org != null) {
_org.close();
if (_out != null) {
try {
_out.close();
} catch (Throwable ex) {
log.warn("Ignored: failed to close the buffer.\nCause: "+ex.getMessage());
disableBuffering();
return;
}
if (_f == null)
_in = new ByteArrayInputStream(
((ByteArrayOutputStream)_out).toByteArray());
//we don't initialize _in if _f is not null
//to reduce memory use (after all, read might not be called)
_out = null;
_org = null;
}
} else if (_in != null) {
if (_f != null) {
_in.close();
_in = null;
} else {
_in.reset();
}
}
}
// -- Serializable --//
// NOTE: they must be declared as private
private synchronized void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
s.defaultWriteObject();
if (_org != null) {
// write to buffer
while(read() != -1);
}
close();
final byte[] data = new byte[_memmaxsz];
int read;
while ((read = read(data)) > 0) {
s.writeInt(read);
s.write(data, 0, read);
}
s.writeInt(0);
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
int readInt = s.readInt();
final byte[] data = new byte[_memmaxsz];
ByteArrayOutputStream out = new ByteArrayOutputStream();
while (readInt > 0) {
int read = s.read(data, 0, readInt);
out.write(data, 0, read);
readInt -= read;
if (readInt == 0) {
readInt = s.readInt();
}
}
_in = new ByteArrayInputStream(
((ByteArrayOutputStream) out).toByteArray());
}
//Object//
protected void finalize() throws Throwable {
disableBuffering();
if (_org != null)
_org.close();
if (_in != null) {
_in.close();
_in = null;
}
super.finalize();
}
private static class ResetableInputStream extends InputStream
implements Repeatable, Serializable {
private final InputStream _org;
ResetableInputStream(InputStream bais) {
_org = bais;
}
public int read() throws IOException {
return _org.read();
}
/** Closes the current access, and the next call of {@link #read}
* re-opens the buffered input stream.
*/
public void close() throws IOException {
_org.reset();
}
//Object//
protected void finalize() throws Throwable {
_org.close();
super.finalize();
}
}
private static class RepeatableFileInputStream extends InputStream
implements Repeatable, Serializable {
private final File _file;
private InputStream _in;
RepeatableFileInputStream(File file) {
_file = file;
}
public int read() throws IOException {
if (_in == null)
_in = new BufferedInputStream(new FileInputStream(_file));
return _in.read();
}
/** Closes the current access, and the next call of {@link #read}
* re-opens the buffered input stream.
*/
public void close() throws IOException {
if (_in != null) {
_in.close();
_in = null;
}
}
//Object//
protected void finalize() throws Throwable {
close();
super.finalize();
}
}
private static class RepeatableURLInputStream extends InputStream
implements Repeatable, Serializable {
private final URL _url;
private InputStream _in;
RepeatableURLInputStream(URL url) {
_url = url;
}
public int read() throws IOException {
if (_in == null) {
_in = _url.openStream();
if (_in == null) throw new FileNotFoundException(_url.toExternalForm());
_in = new BufferedInputStream(_in);
}
return _in.read();
}
/** Closes the current access, and the next call of {@link #read}
* re-opens the buffered input stream.
*/
public void close() throws IOException {
if (_in != null) {
_in.close();
_in = null;
}
}
//Object//
protected void finalize() throws Throwable {
close();
super.finalize();
}
}
}