All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javax.mail.util.SharedFileInputStream Maven / Gradle / Ivy

There is a newer version: 2.0.31
Show newest version
/*
 * 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 javax.mail.util;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;

import javax.mail.internet.SharedInputStream;

public class SharedFileInputStream extends BufferedInputStream implements SharedInputStream {


    // This initial size isn't documented, but bufsize is 2048 after initialization for the
    // Sun implementation.
    private static final int DEFAULT_BUFFER_SIZE = 2048;

    // the shared file information, used to synchronize opens/closes of the base file.
    private SharedFileSource source;

    /**
     * The file offset that is the first byte in the read buffer.
     */
    protected long bufpos;

    /**
     * The normal size of the read buffer.
     */
    protected int bufsize;

    /**
     * The size of the file subset represented by this stream instance.
     */
    protected long datalen;

    /**
     * The source of the file data.  This is shared across multiple
     * instances.
     */
    protected RandomAccessFile in;

    /**
     * The starting position of data represented by this stream relative
     * to the start of the file data.  This stream instance represents
     * data in the range start to (start + datalen - 1).
     */
    protected long start;


    /**
     * Construct a SharedFileInputStream from a file name, using the default buffer size.
     *
     * @param file   The name of the file.
     *
     * @exception IOException
     */
    public SharedFileInputStream(String file) throws IOException {
        this(file, DEFAULT_BUFFER_SIZE);
    }


    /**
     * Construct a SharedFileInputStream from a File object, using the default buffer size.
     *
     * @param file   The name of the file.
     *
     * @exception IOException
     */
    public SharedFileInputStream(File file) throws IOException {
        this(file, DEFAULT_BUFFER_SIZE);
    }


    /**
     * Construct a SharedFileInputStream from a file name, with a given initial buffer size.
     *
     * @param file       The name of the file.
     * @param bufferSize The initial buffer size.
     *
     * @exception IOException
     */
    public SharedFileInputStream(String file, int bufferSize) throws IOException {
        // I'm not sure this is correct or not.  The SharedFileInputStream spec requires this
        // be a subclass of BufferedInputStream.  The BufferedInputStream constructor takes a stream,
        // which we're not really working from at this point.  Using null seems to work so far.
        super(null);
        init(new File(file), bufferSize);
    }


    /**
     * Construct a SharedFileInputStream from a File object, with a given initial buffer size.
     *
     * @param file   The name of the file.
     * @param bufferSize The initial buffer size.
     *
     * @exception IOException
     */
    public SharedFileInputStream(File file, int bufferSize) throws IOException {
        // I'm not sure this is correct or not.  The SharedFileInputStream spec requires this
        // be a subclass of BufferedInputStream.  The BufferedInputStream constructor takes a stream,
        // which we're not really working from at this point.  Using null seems to work so far.
        super(null);
        init(file, bufferSize);
    }


    /**
     * Private constructor used to spawn off a shared instance
     * of this stream.
     *
     * @param source  The internal class object that manages the shared resources of
     *                the stream.
     * @param start   The starting offset relative to the beginning of the file.
     * @param len     The length of file data in this shared instance.
     * @param bufsize The initial buffer size (same as the spawning parent.
     */
    private SharedFileInputStream(SharedFileSource source, long start, long len, int bufsize) {
        super(null);
        this.source = source;
        in = source.open();
        this.start = start;
        bufpos = start;
        datalen = len;
        this.bufsize = bufsize;
        buf = new byte[bufsize];
        // other fields such as pos and count initialized by the super class constructor.
    }


    /**
     * Shared initializtion routine for the constructors.
     *
     * @param file       The file we're accessing.
     * @param bufferSize The initial buffer size to use.
     *
     * @exception IOException
     */
    private void init(File file, int bufferSize) throws IOException {
        if (bufferSize <= 0) {
            throw new IllegalArgumentException("Buffer size must be positive");
        }
        // create a random access file for accessing the data, then create an object that's used to share
        // instances of the same stream.
        source = new SharedFileSource(file);
        // we're opening the first one.
        in = source.open();
        // this represents the entire file, for now.
        start = 0;
        // use the current file length for the bounds
        datalen = in.length();
        // now create our buffer version
        bufsize = bufferSize;
        bufpos = 0;
        // NB:  this is using the super class protected variable.
        buf = new byte[bufferSize];
    }


    /**
     * Check to see if we need to read more data into our buffer.
     *
     * @return False if there's not valid data in the buffer (generally means
     *         an EOF condition).
     * @exception IOException
     */
    private boolean checkFill() throws IOException {
        // if we have data in the buffer currently, just return
        if (pos < count) {
            return true;
        }

        // ugh, extending BufferedInputStream also means supporting mark positions.  That complicates everything.
        // life is so much easier if marks are not used....
        if (markpos < 0) {
            // reset back to the buffer position
            pos = 0;
            // this will be the new position within the file once we're read some data.
            bufpos += count;
        }
        else {
            // we have marks to worry about....damn.
            // if we have room in the buffer to read more data, then we will.  Otherwise, we need to see
            // if it's possible to shift the data in the buffer or extend the buffer (up to the mark limit).
            if (pos >= buf.length) {
                // the mark position is not at the beginning of the buffer, so just shuffle the bytes, leaving
                // us room to read more data.
                if (markpos > 0) {
                    // this is the size of the data we need to keep.
                    int validSize = pos - markpos;
                    // perform the shift operation.
                    System.arraycopy(buf, markpos, buf, 0, validSize);
                    // now adjust the positional markers for this shift.
                    pos = validSize;
                    bufpos += markpos;
                    markpos = 0;
                }
                // the mark is at the beginning, and we've used up the buffer.  See if we're allowed to
                // extend this.
                else if (buf.length < marklimit) {
                    // try to double this, but throttle to the mark limit
                    int newSize = Math.min(buf.length * 2, marklimit);

                    byte[] newBuffer = new byte[newSize];
                    System.arraycopy(buf, 0, newBuffer, 0, buf.length);

                    // replace the old buffer.  Note that all other positional markers remain the same here.
                    buf = newBuffer;
                }
                // we've got further than allowed, so invalidate the mark, and just reset the buffer
                else {
                    markpos = -1;
                    pos = 0;
                    bufpos += count;
                }
            }
        }

        // if we're past our designated end, force an eof.
        if (bufpos + pos >= start + datalen) {
            // make sure we zero the count out, otherwise we'll reuse this data 
            // if called again. 
            count = pos; 
            return false;
        }

        // seek to the read location start.  Note this is a shared file, so this assumes all of the methods
        // doing buffer fills will be synchronized.
        int fillLength = buf.length - pos;

        // we might be working with a subset of the file data, so normal eof processing might not apply.
        // we need to limit how much we read to the data length.
        if (bufpos - start + pos + fillLength > datalen) {
            fillLength = (int)(datalen - (bufpos - start + pos));
        }

        // finally, try to read more data into the buffer.
        fillLength = source.read(bufpos + pos, buf, pos, fillLength);

        // we weren't able to read anything, count this as an eof failure.
        if (fillLength <= 0) {
            // make sure we zero the count out, otherwise we'll reuse this data 
            // if called again. 
            count = pos; 
            return false;
        }

        // set the new buffer count
        count = fillLength + pos;

        // we have data in the buffer.
        return true;
    }


    /**
     * Return the number of bytes available for reading without
     * blocking for a long period.
     *
     * @return For this stream, this is the number of bytes between the
     *         current read position and the indicated end of the file.
     * @exception IOException
     */
    public synchronized int available() throws IOException {
        checkOpen();

        // this is backed by a file, which doesn't really block.  We can return all the way to the
        // marked data end, if necessary
        long endMarker = start + datalen;
        return (int)(endMarker - (bufpos + pos));
    }


    /**
     * Return the current read position of the stream.
     *
     * @return The current position relative to the beginning of the stream.
     *         This is not the position relative to the start of the file, since
     *         the stream starting position may be other than the beginning.
     */
    public long getPosition() {
        checkOpenRuntime();

        return bufpos + pos - start;
    }


    /**
     * Mark the current position for retracing.
     *
     * @param readlimit The limit for the distance the read position can move from
     *                  the mark position before the mark is reset.
     */
    public synchronized void mark(int readlimit) {
        checkOpenRuntime();
        marklimit = readlimit;
        markpos = pos;
    }


    /**
     * Read a single byte of data from the input stream.
     *
     * @return The read byte.  Returns -1 if an eof condition has been hit.
     * @exception IOException
     */
    public synchronized int read() throws IOException {
        checkOpen();

        // check to see if we can fill more data
        if (!checkFill()) {
            return -1;
        }

        // return the current byte...anded to prevent sign extension.
        return buf[pos++] & 0xff;
    }


    /**
     * Read multiple bytes of data and place them directly into
     * a byte-array buffer.
     *
     * @param buffer The target buffer.
     * @param offset The offset within the buffer to place the data.
     * @param length The length to attempt to read.
     *
     * @return The number of bytes actually read.  Returns -1 for an EOF
     *         condition.
     * @exception IOException
     */
    public synchronized int read(byte buffer[], int offset, int length) throws IOException {
        checkOpen();

        // asked to read nothing?  That's what we'll do.
        if (length == 0) {
            return 0;
        }


        int returnCount = 0;
        while (length > 0) {
            // check to see if we can/must fill more data
            if (!checkFill()) {
                // we've hit the end, but if we've read data, then return that.
                if (returnCount > 0) {
                    return returnCount;
                }
                // trun eof.
                return -1;
            }

            int available = count - pos;
            int given = Math.min(available, length);

            System.arraycopy(buf, pos, buffer, offset, given);

            // now adjust all of our positions and counters
            pos += given;
            length -= given;
            returnCount += given;
            offset += given;
        }
        // return the accumulated count.
        return returnCount;
    }


    /**
     * Skip the read pointer ahead a given number of bytes.
     *
     * @param n      The number of bytes to skip.
     *
     * @return The number of bytes actually skipped.
     * @exception IOException
     */
    public synchronized long skip(long n) throws IOException {
        checkOpen();

        // nothing to skip, so don't skip
        if (n <= 0) {
            return 0;
        }

        // see if we need to fill more data, and potentially shift the mark positions
        if (!checkFill()) {
            return 0;
        }

        long available = count - pos;

        // the skipped contract allows skipping within the current buffer bounds, so cap it there.
        long skipped = available < n ? available : n;
        pos += skipped;
        return skipped;
    }

    /**
     * Reset the mark position.
     *
     * @exception IOException
     */
    public synchronized void reset() throws IOException {
        checkOpen();
        if (markpos < 0) {
            throw new IOException("Resetting to invalid mark position");
        }
        // if we have a markpos, it will still be in the buffer bounds.
        pos = markpos;
    }


    /**
     * Indicates the mark() operation is supported.
     *
     * @return Always returns true.
     */
    public boolean markSupported() {
        return true;
    }


    /**
     * Close the stream.  This does not close the source file until
     * the last shared instance is closed.
     *
     * @exception IOException
     */
    public void close() throws IOException {
        // already closed?  This is not an error
        if (in == null) {
            return;
        }

        try {
            // perform a close on the source version.
            source.close();
        } finally {
            in = null;
        }
    }


    /**
     * Create a new stream from this stream, using the given
     * start offset and length.
     *
     * @param offset The offset relative to the start of this stream instance.
     * @param end    The end offset of the substream.  If -1, the end of the parent stream is used.
     *
     * @return A new SharedFileInputStream object sharing the same source
     *         input file.
     */
    public InputStream newStream(long offset, long end) {
        checkOpenRuntime();

        if (offset < 0) {
            throw new IllegalArgumentException("Start position is less than 0");
        }
        // the default end position is the datalen of the one we're spawning from.
        if (end == -1) {
            end = datalen;
        }

        // create a new one using the private constructor
        return new SharedFileInputStream(source, start + (int)offset, (int)(end - offset), bufsize);
    }


    /**
     * Check if the file is open and throw an IOException if not.
     *
     * @exception IOException
     */
    private void checkOpen() throws IOException {
        if (in == null) {
            throw new IOException("Stream has been closed");
        }
    }


    /**
     * Check if the file is open and throw an IOException if not.  This version is
     * used because several API methods are not defined as throwing IOException, so
     * checkOpen() can't be used.  The Sun implementation just throws RuntimeExceptions
     * in those methods, hence 2 versions.
     *
     * @exception RuntimeException
     */
    private void checkOpenRuntime() {
        if (in == null) {
            throw new RuntimeException("Stream has been closed");
        }
    }


    /**
     * Internal class used to manage resources shared between the
     * ShareFileInputStream instances.
     */
    class SharedFileSource {
        // the file source
        public RandomAccessFile source;
        // the shared instance count for this file (open instances)
        public int instanceCount = 0;

        public SharedFileSource(File file) throws IOException {
            source = new RandomAccessFile(file, "r");
        }

        /**
         * Open the shared stream to keep track of open instances.
         */
        public synchronized RandomAccessFile open() {
            instanceCount++;
            return source;
        }

        /**
         * Process a close request for this stream.  If there are multiple
         * instances using this underlying stream, the stream will not
         * be closed.
         *
         * @exception IOException
         */
        public synchronized void close() throws IOException {
            if (instanceCount > 0) {
                instanceCount--;
                // if the last open instance, close the real source file.
                if (instanceCount == 0) {
                    source.close();
                }
            }
        }

        /**
         * Read a buffer of data from the shared file.
         *
         * @param position The position to read from.
         * @param buf      The target buffer for storing the read data.
         * @param offset   The starting offset within the buffer.
         * @param length   The length to attempt to read.
         *
         * @return The number of bytes actually read.
         * @exception IOException
         */
        public synchronized int read(long position, byte[] buf, int offset, int length) throws IOException {
            // seek to the read location start.  Note this is a shared file, so this assumes all of the methods
            // doing buffer fills will be synchronized.
            source.seek(position);
            return source.read(buf, offset, length);
        }


        /**
         * Ensure the stream is closed when this shared object is finalized.
         *
         * @exception Throwable
         */
        protected void finalize() throws Throwable {
            super.finalize();
            if (instanceCount > 0) {
                source.close();
            }
        }
    }
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy