zcommon/src/main/java/org/zkoss/io/RepeatableInputStream.java

Summary

Maintainability
D
1 day
Test Coverage
/* 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 java.nio.file.Files;

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() {
        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, {}\nCause: {}", _f, 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 {
                Files.delete(_f.toPath());
            } catch (Throwable ex) { //ignore
            }
            _f = null;
        }
    }

    public int read() throws IOException {
        if (_org != null) {
            final int b = _org.read();
            if (!_nobuf && (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();
        }
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        if (_org != null) {
            final int realLen = _org.read(b, off, len);
            if (!_nobuf)
                if (realLen >= 0) {
                    final OutputStream out = getOutputStream();
                    if (out != null) out.write(b, off, realLen);
                    _cntsz += realLen;
                }
            return realLen;
        } else {
            if (_in == null)
                _in = new BufferedInputStream(new FileInputStream(_f)); //_f must be non-null

            return _in.read(b, off, len);
        }
    }

    /** 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 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(out.toByteArray());
    }

    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();
        }

        @Override
        public int read(byte[] b, int off, int len) throws IOException {
            return _org.read(b, off, len);
        }

        /** Closes the current access, and the next call of {@link #read}
         * re-opens the buffered input stream.
         */
        @Override
        public void close() throws IOException {
            _org.reset();
        }
    }
    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();
        }

        @Override
        public int read(byte[] b, int off, int len) throws IOException {
            if (_in == null)
                _in = new BufferedInputStream(new FileInputStream(_file));
            return _in.read(b, off, len);
        }

        /** Closes the current access, and the next call of {@link #read}
         * re-opens the buffered input stream.
         */
        @Override
        public void close() throws IOException {
            if (_in != null) {
                _in.close();
                _in = null;
            }
        }
    }
    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();
                _in = new BufferedInputStream(_in);
            }
            return _in.read();
        }

        @Override
        public int read(byte[] b, int off, int len) throws IOException {
            if (_in == null) {
                _in = _url.openStream();
                _in = new BufferedInputStream(_in);
            }
            return _in.read(b, off, len);
        }

        /** Closes the current access, and the next call of {@link #read}
         * re-opens the buffered input stream.
         */
        @Override
        public void close() throws IOException {
            if (_in != null) {
                _in.close();
                _in = null;
            }
        }
    }
}