001    /*
002     * This file is part of muCommander, http://www.mucommander.com
003     * Copyright (C) 2002-2008 Maxence Bernard
004     *
005     * muCommander is free software; you can redistribute it and/or modify
006     * it under the terms of the GNU General Public License as published by
007     * the Free Software Foundation; either version 3 of the License, or
008     * (at your option) any later version.
009     *
010     * muCommander is distributed in the hope that it will be useful,
011     * but WITHOUT ANY WARRANTY; without even the implied warranty of
012     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013     * GNU General Public License for more details.
014     *
015     * You should have received a copy of the GNU General Public License
016     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017     */
018    
019    package com.mucommander.io;
020    
021    import java.io.IOException;
022    
023    /**
024     * <code>BlockRandomInputStream</code> is a specialized-yet-still-abstract <code>RandomAccessInputStream</code> that
025     * is geared towards resources that are read block by block, either because of a particular constrain or for performance
026     * reasons. This class typically comes in handy for network resources such as HTTP which have to request a block range
027     * for reading the resource.
028     *
029     * <p>Seeking inside the file is implemented transparently by reading a block starting at the seek offset.
030     * If {@link #seek(long)} is called with an offset that is within the current block, no read occurs.
031     * The block size should be carefully chosen as it affects seek performance and thus overall performance greatly:
032     * the larger the block size, the more data is fetched when seeking outside the current block and consequently the
033     * longer it takes to reposition the stream. On the other hand, a larger block size will yield better performance when
034     * reading the resource sequentially, as it lessens the overhead of requesting a particular block.</p>
035     *
036     * @author Maxence Bernard
037     */
038    public abstract class BlockRandomInputStream extends RandomAccessInputStream {
039    
040        /** Block size, i.e. length of the {@link #block} array */
041        protected final int blockSize;
042    
043        /** Contains the current file block. Data may end before the array does. */
044        private final byte block[];
045    
046        /** Current offset within the block array to the next byte to return */
047        private int blockOff;
048    
049        /** Length of the current block */
050        private int blockLen;
051    
052        /** Global offset within the file */
053        private long offset;
054    
055    
056        /**
057         * Creates a new <code>BlockRandomInputStream</code> using the specified block size.
058         *
059         * <p>The block size should be carefully chosen as it affects seek performance and thus overall performance greatly:
060         * the larger the block size, the more data is fetched when seeking outside the current block and consequently the
061         * longer it takes to reposition the stream. On the other hand, a larger block size will yield better performance
062         * when reading the resource sequentially, as it lessens the overhead of requesting a particular block.</p>
063         *
064         * @param blockSize controls the amount of data requested when reading a block
065         */
066        protected BlockRandomInputStream(int blockSize) {
067            this.blockSize = blockSize;
068            block = new byte[blockSize];
069        }
070    
071        /**
072         * Returns <code>true</code> if the end of file has been reached.
073         *
074         * @return true if the end of file has been reached.
075         * @throws IOException if an I/O error occurred
076         */
077        private boolean eofReached() throws IOException {
078            return offset>=getLength();
079        }
080    
081        /**
082         * Checks if the current buffered block has been read completely (i.e. no more data is available) and if it has,
083         * calls {@link #readBlock(long, byte[], int)} to fetch the next block.
084         *
085         * @throws IOException if an I/O error occurred
086         */
087        private void checkBuffer() throws IOException {
088            if(blockOff >= blockLen)      // True initially
089                readBlock();
090        }
091    
092        /**
093         * Calls {@link #readBlock(long, byte[], int)} to read a block of up to <code>blockSize</code>, less if the
094         * the end of file is near.
095         *
096         * @throws IOException if an I/O error occurred
097         */
098        private void readBlock() throws IOException {
099            int len = Math.min((int)(getLength()-offset), blockSize);
100            // update len with the number of bytes actually read
101            len = readBlock(offset, block, len);
102    
103            // Note: these fields won't be updated if an I/O error occurs
104            this.blockOff = 0;
105            this.blockLen = len;
106        }
107    
108    
109        ////////////////////////////////////////////
110        // RandomAccessInputStream implementation //
111        ////////////////////////////////////////////
112    
113        public int read() throws IOException {
114            if(eofReached())
115                return -1;
116    
117            checkBuffer();
118    
119            int ret = block[blockOff];
120    
121            blockOff++;
122            offset ++;
123    
124            return ret;
125        }
126    
127        public int read(byte b[], int off, int len) throws IOException {
128            if(len==0)
129                return 0;
130    
131            if(eofReached())
132                return -1;
133    
134            checkBuffer();
135    
136            int nbBytes = Math.min(len, blockLen - blockOff);
137            System.arraycopy(block, blockOff, b, off, nbBytes);
138    
139            blockOff += nbBytes;
140            offset += nbBytes;
141    
142            return nbBytes;
143        }
144    
145        public long getOffset() throws IOException {
146            return offset;
147        }
148    
149        public void seek(long newOffset) throws IOException {
150            // If the new offset is within the current buffer's range, simply reposition the offsets
151            if(newOffset>=offset && newOffset<offset+ blockLen) {
152                blockOff += (int)(newOffset-offset);
153                offset = newOffset;
154            }
155            // If not, retrieve a block of data starting at the new offset and fill the buffer with it
156            else {
157                offset = newOffset;
158                readBlock();
159            }
160        }
161    
162    
163        ///////////////////////
164        // Abstract methods //
165        ///////////////////////
166    
167        /**
168         * Reads a block, that spawns from <code>fileOffset</code> to <code>fileOffset+blockLen</code>, an returns
169         * the number of bytes that could be read, normally <code>blockLen</code> but can be less.
170         *
171         * <p>Note that <code>blockLen</code> may be smaller than {@link #blockSize} if the end of file is near, to prevent
172         * <code>EOF</code> from being reached. In other words, <code>fileOffset+blockLen</code> should theorically not
173         * exceed the file's length, but this could happen in the unlikely event that the file just shrinked after
174         * {@link #getLength()} was last called. So this method's implementation should handle the case where
175         * <code>EOF</code> is reached prematurely and return the number of bytes that were actually read.</p>
176         *
177         * @param fileOffset global file offset that marks the beginning of the block
178         * @param block the array to fill with data, starting at 0
179         * @param blockLen number of bytes to read
180         * @return the number of bytes that were actually read, normally blockLen unless
181         * @throws IOException if an I/O error occurred
182         */
183        protected abstract int readBlock(long fileOffset, byte block[], int blockLen) throws IOException;
184    }