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

Summary

Maintainability
D
1 day
Test Coverage
/* RepeatableReader.java

    Purpose:
        
    Description:
        
    History:
        Fri Mar 14 11:47:38     2008, Created by tomyeh

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.CharArrayReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;
import java.nio.file.Files;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.lang.Library;

/**
 * {@link RepeatableReader} adds functionality to another reader,
 * the ability to read repeatedly.
 * By repeatable-read we mean, after {@link #close}, the next invocation of
 * {@link #read} will re-open the reader.
 *
 * <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 reader)
 * is never closed until garbage-collected.
 *
 * <p>If the content size of the given reader 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 reader 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 tomyeh
 * @since 3.0.4
 */
public class RepeatableReader extends Reader implements Repeatable, Serializable {
    private static final Logger log = LoggerFactory.getLogger(RepeatableReader.class);

    private Reader _org;
    private Writer _out;
    private Reader _in;
    private 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 RepeatableReader(Reader is) {
        _org = is;
        _bufmaxsz = Library.getIntProperty(
            RepeatableInputStream.BUFFER_LIMIT_SIZE, 20 * 1024 * 1024);
        _memmaxsz = Library.getIntProperty(
            RepeatableInputStream.MEMORY_LIMIT_SIZE, 512 * 1024);
    }

    /**
     * Returns a reader that can be read repeatedly, or null if the given
     * reader is null.
     * Note: the returned reader encapsulates the given reader, rd
     * (a.k.a., the buffered reader) to adds the functionality to
     * re-opens the reader once {@link #close} is called.
     *
     * <p>By repeatable-read we mean, after {@link #close}, the next
     * invocation of {@link #read} will re-open the reader.
     *
     * <p>Use this method instead of instantiating {@link RepeatableReader}
     * with the constructor.
     *
     * @see #getInstance(File)
     */
    public static Reader getInstance(Reader rd) {
        if ((rd instanceof CharArrayReader) || (rd instanceof StringReader))
            return new ResetableReader(rd);
        else if (rd != null && !(rd instanceof Repeatable))
            return new RepeatableReader(rd);
        return rd;
    }
    /**
     * Returns a reader to read a file that can be read repeatedly.
     * Note: it assumes the file is text (rather than binary).
     *
     * <p>By repeatable-read we mean, after {@link #close}, the next
     * invocation of {@link #read} will re-open the reader.
     *
     * <p>Note: it is efficient since we don't have to buffer the
     * content of the file to make it repeatable-read.
     *
     * @param charset the charset. If null, "UTF-8" is assumed.
     * @exception IllegalArgumentException if file is null.
     * @see #getInstance(Reader)
     * @see #getInstance(String, String)
     * @since 3.0.8
     */
    public static Reader getInstance(File file, String charset)
    throws FileNotFoundException {
        if (file == null)
            throw new IllegalArgumentException("null");
        if (!file.exists())
            throw new FileNotFoundException(file.toString());
        return new RepeatableFileReader(file, charset);
    }
    /**
     * Returns a reader to read a file, encoded in UTF-8,
     * that can be read repeatedly.
     * Note: it assumes the file is text (rather than binary).
     *
     * <p>By repeatable-read we mean, after {@link #close}, the next
     * invocation of {@link #read} will re-open the reader.
     *
     * <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(Reader)
     * @see #getInstance(String)
     */
    public static Reader getInstance(File file)
    throws FileNotFoundException {
        return getInstance(file, "UTF-8");
    }
    /**
     * Returns a reader to read a file that can be read repeatedly.
     * Note: it assumes the file is text (rather than binary).
     *
     * <p>By repeatable-read we mean, after {@link #close}, the next
     * invocation of {@link #read} will re-open the reader.
     *
     * <p>Note: it is efficient since we don't have to buffer the
     * content of the file to make it repeatable-read.
     *
     * @param filename the file name
     * @param charset the charset. If null, "UTF-8" is assumed.
     * @exception IllegalArgumentException if file is null.
     * @exception FileNotFoundException if file is not found.
     * @see #getInstance(Reader)
     * @see #getInstance(File, String)
     * @since 3.0.8
     */
    public static Reader getInstance(String filename, String charset)
    throws FileNotFoundException {
        return getInstance(new File(filename));
    }
    /**
     * Returns a reader to read a file, encoded in UTF-8,
     * that can be read repeatedly.
     * Note: it assumes the file is text (rather than binary).
     *
     * <p>By repeatable-read we mean, after {@link #close}, the next
     * invocation of {@link #read} will re-open the reader.
     *
     * <p>Note: it is efficient since we don't have to buffer the
     * content of the file to make it repeatable-read.
     *
     * @param filename the file name
     * @exception IllegalArgumentException if file is null.
     * @exception FileNotFoundException if file is not found.
     * @see #getInstance(Reader)
     * @see #getInstance(File)
     */
    public static Reader getInstance(String filename)
    throws FileNotFoundException {
        return getInstance(new File(filename), "UTF-8");
    }
    /**
     * Returns a reader to read the resource of the specified URL.
     * The reader can be read repeatedly.
     * Note: it assumes the resource is text (rather than binary).
     *
     * <p>By repeatable-read we mean, after {@link #close}, the next
     * invocation of {@link #read} will re-open the reader.
     *
     * <p>Note: it is efficient since we don't have to buffer the
     * content of the file to make it repeatable-read.
     *
     * @param charset the charset. If null, "UTF-8" is assumed.
     * @exception IllegalArgumentException if file is null.
     * @see #getInstance(Reader)
     * @see #getInstance(String, String)
     * @since 3.0.8
     */
    public static Reader getInstance(URL url, String charset) {
        if (url == null)
            throw new IllegalArgumentException("null");
        return new RepeatableURLReader(url, charset);
    }
    /**
     * Returns a reader to read the resource of the specified URL,
     * encoded in UTF-8.
     * The reader can be read repeatedly.
     * Note: it assumes the resource is text (rather than binary).
     *
     * <p>By repeatable-read we mean, after {@link #close}, the next
     * invocation of {@link #read} will re-open the reader.
     *
     * <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(Reader)
     * @see #getInstance(String)
     */
    public static Reader getInstance(URL url) {
        return getInstance(url, "UTF-8");
    }

    private Writer getWriter() throws IOException {
        if (_out == null)
            return _nobuf ? null: (_out = new StringWriter());
                //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 String cnt = ((StringWriter)_out).toString();
                _out = new FileWriter(_f, "UTF-8");
                _out.write(cnt);
            } 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 {
                Files.delete(_f.toPath());
            } catch (Throwable ex) { //ignore
            }
            _f = null;
        }
    }

    public int read(char cbuf[], int off, int len) throws IOException {
        if (_org != null) {
            final int cnt = _org.read(cbuf, off, len);
            if (!_nobuf)
                if (cnt >= 0) {
                    final Writer out = getWriter();
                    if (out != null) out.write(cbuf, off, cnt);
                    _cntsz += cnt;
                }
            return cnt;
        } else {
            if (_in == null)
                _in = new FileReader(_f, "UTF-8"); //_f must be non-null

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

    /** Closes the current access, and the next call of {@link #read}
     * re-opens the buffered reader.
     */
    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 StringReader(
                        ((StringWriter)_out).toString());
                    //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();
            }
        }
    }

    //Object//
    protected void finalize() throws Throwable {
        disableBuffering();
        if (_org != null)
            _org.close();
        if (_in != null) {
            _in.close();
            _in = null;
        }
        super.finalize();
    }

    private static class ResetableReader extends Reader implements Repeatable,
            Serializable {
        private final Reader _org;
        ResetableReader(Reader bais) {
            _org = bais;
        }

        public int read(char cbuf[], int off, int len) throws IOException {
            return _org.read(cbuf, off, len);
        }
        /** Closes the current access and the next call of {@link #read}
         * re-opens the buffered reader.
         */
        public void close() throws IOException {
            _org.reset();
        }

        //Object//
        protected void finalize() throws Throwable {
            _org.close();
            super.finalize();
        }
    }
    private static class RepeatableFileReader extends Reader implements Repeatable,
            Serializable {
        private final File _file;
        private Reader _in;
        private final String _charset;

        RepeatableFileReader(File file, String charset) {
            _file = file;
            _charset = charset != null ? charset: "UTF-8";
        }

        public int read(char cbuf[], int off, int len) throws IOException {
            if (_in == null)
                _in = new FileReader(_file, _charset);
            return _in.read(cbuf, off, len);
        }
        /** Closes the current access and the next call of {@link #read}
         * re-opens the buffered reader.
         */
        public void close() throws IOException {
            if (_in != null) {
                _in.close();
                _in = null;
            }
        }

        //Object//
        protected void finalize() throws Throwable {
            close();
            super.finalize();
        }
    }
    private static class RepeatableURLReader extends Reader implements Repeatable,
            Serializable {
        private final URL _url;
        private Reader _in;
        private final String _charset;

        RepeatableURLReader(URL url, String charset) {
            _url = url;
            _charset = charset != null ? charset: "UTF-8";
        }

        public int read(char cbuf[], int off, int len) throws IOException {
            if (_in == null)
                _in = new URLReader(_url, _charset);
            return _in.read(cbuf, off, len);
        }
        /** Closes the current access and the next call of {@link #read}
         * re-opens the buffered reader.
         */
        public void close() throws IOException {
            if (_in != null) {
                _in.close();
                _in = null;
            }
        }

        //Object//
        protected void finalize() throws Throwable {
            close();
            super.finalize();
        }
    }
}