001    /**
002     *
003     * Copyright 2006 The Apache Software Foundation
004     *
005     *  Licensed under the Apache License, Version 2.0 (the "License");
006     *  you may not use this file except in compliance with the License.
007     *  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     */
017    
018    package javax.mail.util;
019    
020    import java.io.BufferedInputStream;
021    import java.io.File;
022    import java.io.IOException;
023    import java.io.InputStream;
024    import java.io.RandomAccessFile;
025    
026    import javax.mail.internet.SharedInputStream;
027    
028    public class SharedFileInputStream extends BufferedInputStream implements SharedInputStream {
029    
030    
031        // This initial size isn't documented, but bufsize is 2048 after initialization for the
032        // Sun implementation.
033        private static final int DEFAULT_BUFFER_SIZE = 2048;
034    
035        // the shared file information, used to synchronize opens/closes of the base file.
036        private SharedFileSource source;
037    
038        /**
039         * The file offset that is the first byte in the read buffer.
040         */
041        protected long bufpos;
042    
043        /**
044         * The normal size of the read buffer.
045         */
046        protected int bufsize;
047    
048        /**
049         * The size of the file subset represented by this stream instance.
050         */
051        protected long datalen;
052    
053        /**
054         * The source of the file data.  This is shared across multiple
055         * instances.
056         */
057        protected RandomAccessFile in;
058    
059        /**
060         * The starting position of data represented by this stream relative
061         * to the start of the file data.  This stream instance represents
062         * data in the range start to (start + datalen - 1).
063         */
064        protected long start;
065    
066    
067        /**
068         * Construct a SharedFileInputStream from a file name, using the default buffer size.
069         *
070         * @param file   The name of the file.
071         *
072         * @exception IOException
073         */
074        public SharedFileInputStream(String file) throws IOException {
075            this(file, DEFAULT_BUFFER_SIZE);
076        }
077    
078    
079        /**
080         * Construct a SharedFileInputStream from a File object, using the default buffer size.
081         *
082         * @param file   The name of the file.
083         *
084         * @exception IOException
085         */
086        public SharedFileInputStream(File file) throws IOException {
087            this(file, DEFAULT_BUFFER_SIZE);
088        }
089    
090    
091        /**
092         * Construct a SharedFileInputStream from a file name, with a given initial buffer size.
093         *
094         * @param file       The name of the file.
095         * @param bufferSize The initial buffer size.
096         *
097         * @exception IOException
098         */
099        public SharedFileInputStream(String file, int bufferSize) throws IOException {
100            // I'm not sure this is correct or not.  The SharedFileInputStream spec requires this
101            // be a subclass of BufferedInputStream.  The BufferedInputStream constructor takes a stream,
102            // which we're not really working from at this point.  Using null seems to work so far.
103            super(null);
104            init(new File(file), bufferSize);
105        }
106    
107    
108        /**
109         * Construct a SharedFileInputStream from a File object, with a given initial buffer size.
110         *
111         * @param file   The name of the file.
112         * @param bufferSize The initial buffer size.
113         *
114         * @exception IOException
115         */
116        public SharedFileInputStream(File file, int bufferSize) throws IOException {
117            // I'm not sure this is correct or not.  The SharedFileInputStream spec requires this
118            // be a subclass of BufferedInputStream.  The BufferedInputStream constructor takes a stream,
119            // which we're not really working from at this point.  Using null seems to work so far.
120            super(null);
121            init(file, bufferSize);
122        }
123    
124    
125        /**
126         * Private constructor used to spawn off a shared instance
127         * of this stream.
128         *
129         * @param source  The internal class object that manages the shared resources of
130         *                the stream.
131         * @param start   The starting offset relative to the beginning of the file.
132         * @param len     The length of file data in this shared instance.
133         * @param bufsize The initial buffer size (same as the spawning parent.
134         */
135        private SharedFileInputStream(SharedFileSource source, long start, long len, int bufsize) {
136            super(null);
137            this.source = source;
138            in = source.open();
139            this.start = start;
140            bufpos = start;
141            datalen = len;
142            this.bufsize = bufsize;
143            buf = new byte[bufsize];
144            // other fields such as pos and count initialized by the super class constructor.
145        }
146    
147    
148        /**
149         * Shared initializtion routine for the constructors.
150         *
151         * @param file       The file we're accessing.
152         * @param bufferSize The initial buffer size to use.
153         *
154         * @exception IOException
155         */
156        private void init(File file, int bufferSize) throws IOException {
157            if (bufferSize <= 0) {
158                throw new IllegalArgumentException("Buffer size must be positive");
159            }
160            // create a random access file for accessing the data, then create an object that's used to share
161            // instances of the same stream.
162            source = new SharedFileSource(file);
163            // we're opening the first one.
164            in = source.open();
165            // this represents the entire file, for now.
166            start = 0;
167            // use the current file length for the bounds
168            datalen = in.length();
169            // now create our buffer version
170            bufsize = bufferSize;
171            bufpos = 0;
172            // NB:  this is using the super class protected variable.
173            buf = new byte[bufferSize];
174        }
175    
176    
177        /**
178         * Check to see if we need to read more data into our buffer.
179         *
180         * @return False if there's not valid data in the buffer (generally means
181         *         an EOF condition).
182         * @exception IOException
183         */
184        private boolean checkFill() throws IOException {
185            // if we have data in the buffer currently, just return
186            if (pos < count) {
187                return true;
188            }
189    
190            // ugh, extending BufferedInputStream also means supporting mark positions.  That complicates everything.
191            // life is so much easier if marks are not used....
192            if (markpos < 0) {
193                // reset back to the buffer position
194                pos = 0;
195                // this will be the new position within the file once we're read some data.
196                bufpos += count;
197            }
198            else {
199    
200    
201                // we have marks to worry about....damn.
202                // if we have room in the buffer to read more data, then we will.  Otherwise, we need to see
203                // if it's possible to shift the data in the buffer or extend the buffer (up to the mark limit).
204                if (pos >= buf.length) {
205                    // the mark position is not at the beginning of the buffer, so just shuffle the bytes, leaving
206                    // us room to read more data.
207                    if (markpos > 0) {
208                        // this is the size of the data we need to keep.
209                        int validSize = pos - markpos;
210                        // perform the shift operation.
211                        System.arraycopy(buf, markpos, buf, 0, validSize);
212                        // now adjust the positional markers for this shift.
213                        pos = validSize;
214                        bufpos += markpos;
215                        markpos = 0;
216                    }
217                    // the mark is at the beginning, and we've used up the buffer.  See if we're allowed to
218                    // extend this.
219                    else if (buf.length < marklimit) {
220                        // try to double this, but throttle to the mark limit
221                        int newSize = Math.min(buf.length * 2, marklimit);
222    
223                        byte[] newBuffer = new byte[newSize];
224                        System.arraycopy(buf, 0, newBuffer, 0, buf.length);
225    
226                        // replace the old buffer.  Note that all other positional markers remain the same here.
227                        buf = newBuffer;
228                    }
229                    // we've got further than allowed, so invalidate the mark, and just reset the buffer
230                    else {
231                        markpos = -1;
232                        pos = 0;
233                        bufpos += count;
234                    }
235                }
236            }
237    
238            // if we're past our designated end, force an eof.
239            if (bufpos + pos >= start + datalen) {
240                return false;
241            }
242    
243            // seek to the read location start.  Note this is a shared file, so this assumes all of the methods
244            // doing buffer fills will be synchronized.
245            int fillLength = buf.length - pos;
246    
247            // we might be working with a subset of the file data, so normal eof processing might not apply.
248            // we need to limit how much we read to the data length.
249            if (bufpos - start + pos + fillLength > datalen) {
250                fillLength = (int)(datalen - (bufpos - start + pos));
251            }
252    
253            // finally, try to read more data into the buffer.
254            fillLength = source.read(bufpos + pos, buf, pos, fillLength);
255    
256            // we weren't able to read anything, count this as an eof failure.
257            if (fillLength <= 0) {
258                return false;
259            }
260    
261            // set the new buffer count
262            count = fillLength + pos;
263    
264            // we have data in the buffer.
265            return true;
266        }
267    
268    
269        /**
270         * Return the number of bytes available for reading without
271         * blocking for a long period.
272         *
273         * @return For this stream, this is the number of bytes between the
274         *         current read position and the indicated end of the file.
275         * @exception IOException
276         */
277        public synchronized int available() throws IOException {
278            checkOpen();
279    
280            // this is backed by a file, which doesn't really block.  We can return all the way to the
281            // marked data end, if necessary
282            long endMarker = start + datalen;
283            return (int)(endMarker - (bufpos + pos));
284        }
285    
286    
287        /**
288         * Return the current read position of the stream.
289         *
290         * @return The current position relative to the beginning of the stream.
291         *         This is not the position relative to the start of the file, since
292         *         the stream starting position may be other than the beginning.
293         */
294        public long getPosition() {
295            checkOpenRuntime();
296    
297            return bufpos + pos - start;
298        }
299    
300    
301        /**
302         * Mark the current position for retracing.
303         *
304         * @param readlimit The limit for the distance the read position can move from
305         *                  the mark position before the mark is reset.
306         */
307        public synchronized void mark(int readlimit) {
308            checkOpenRuntime();
309            marklimit = readlimit;
310            markpos = pos;
311        }
312    
313    
314        /**
315         * Read a single byte of data from the input stream.
316         *
317         * @return The read byte.  Returns -1 if an eof condition has been hit.
318         * @exception IOException
319         */
320        public synchronized int read() throws IOException {
321            checkOpen();
322    
323            // check to see if we can fill more data
324            if (!checkFill()) {
325                return -1;
326            }
327    
328            // return the current byte...anded to prevent sign extension.
329            return buf[pos++] & 0xff;
330        }
331    
332    
333        /**
334         * Read multiple bytes of data and place them directly into
335         * a byte-array buffer.
336         *
337         * @param buffer The target buffer.
338         * @param offset The offset within the buffer to place the data.
339         * @param length The length to attempt to read.
340         *
341         * @return The number of bytes actually read.  Returns -1 for an EOF
342         *         condition.
343         * @exception IOException
344         */
345        public synchronized int read(byte buffer[], int offset, int length) throws IOException {
346            checkOpen();
347    
348            // asked to read nothing?  That's what we'll do.
349            if (length == 0) {
350                return 0;
351            }
352    
353    
354            int returnCount = 0;
355            while (length > 0) {
356                // check to see if we can/must fill more data
357                if (!checkFill()) {
358                    // we've hit the end, but if we've read data, then return that.
359                    if (returnCount > 0) {
360                        return returnCount;
361                    }
362                    // trun eof.
363                    return -1;
364                }
365    
366                int available = count - pos;
367                int given = Math.min(available, length);
368    
369                System.arraycopy(buf, pos, buffer, offset, given);
370    
371                // now adjust all of our positions and counters
372                pos += given;
373                length -= given;
374                returnCount += given;
375                offset += given;
376            }
377            // return the accumulated count.
378            return returnCount;
379        }
380    
381    
382        /**
383         * Skip the read pointer ahead a given number of bytes.
384         *
385         * @param n      The number of bytes to skip.
386         *
387         * @return The number of bytes actually skipped.
388         * @exception IOException
389         */
390        public synchronized long skip(long n) throws IOException {
391            checkOpen();
392    
393            // nothing to skip, so don't skip
394            if (n <= 0) {
395                return 0;
396            }
397    
398            // see if we need to fill more data, and potentially shift the mark positions
399            if (!checkFill()) {
400                return 0;
401            }
402    
403            long available = count - pos;
404    
405            // the skipped contract allows skipping within the current buffer bounds, so cap it there.
406            long skipped = available < n ? available : n;
407            pos += skipped;
408            return skipped;
409        }
410    
411        /**
412         * Reset the mark position.
413         *
414         * @exception IOException
415         */
416        public synchronized void reset() throws IOException {
417            checkOpen();
418            if (markpos < 0) {
419                throw new IOException("Resetting to invalid mark position");
420            }
421            // if we have a markpos, it will still be in the buffer bounds.
422            pos = markpos;
423        }
424    
425    
426        /**
427         * Indicates the mark() operation is supported.
428         *
429         * @return Always returns true.
430         */
431        public boolean markSupported() {
432            return true;
433        }
434    
435    
436        /**
437         * Close the stream.  This does not close the source file until
438         * the last shared instance is closed.
439         *
440         * @exception IOException
441         */
442        public void close() throws IOException {
443            // already closed?  This is not an error
444            if (in == null) {
445                return;
446            }
447    
448            try {
449                // perform a close on the source version.
450                source.close();
451            } finally {
452                in = null;
453            }
454        }
455    
456    
457        /**
458         * Create a new stream from this stream, using the given
459         * start offset and length.
460         *
461         * @param offset The offset relative to the start of this stream instance.
462         * @param end    The end offset of the substream.  If -1, the end of the parent stream is used.
463         *
464         * @return A new SharedFileInputStream object sharing the same source
465         *         input file.
466         */
467        public InputStream newStream(long offset, long end) {
468            checkOpenRuntime();
469    
470            if (offset < 0) {
471                throw new IllegalArgumentException("Start position is less than 0");
472            }
473            // the default end position is the datalen of the one we're spawning from.
474            if (end == -1) {
475                end = datalen;
476            }
477    
478            // create a new one using the private constructor
479            return new SharedFileInputStream(source, start + (int)offset, (int)(end - offset), bufsize);
480        }
481    
482    
483        /**
484         * Check if the file is open and throw an IOException if not.
485         *
486         * @exception IOException
487         */
488        private void checkOpen() throws IOException {
489            if (in == null) {
490                throw new IOException("Stream has been closed");
491            }
492        }
493    
494    
495        /**
496         * Check if the file is open and throw an IOException if not.  This version is
497         * used because several API methods are not defined as throwing IOException, so
498         * checkOpen() can't be used.  The Sun implementation just throws RuntimeExceptions
499         * in those methods, hence 2 versions.
500         *
501         * @exception RuntimeException
502         */
503        private void checkOpenRuntime() {
504            if (in == null) {
505                throw new RuntimeException("Stream has been closed");
506            }
507        }
508    
509    
510        /**
511         * Internal class used to manage resources shared between the
512         * ShareFileInputStream instances.
513         */
514        class SharedFileSource {
515            // the file source
516            public RandomAccessFile source;
517            // the shared instance count for this file (open instances)
518            public int instanceCount = 0;
519    
520            public SharedFileSource(File file) throws IOException {
521                source = new RandomAccessFile(file, "r");
522            }
523    
524            /**
525             * Open the shared stream to keep track of open instances.
526             */
527            public synchronized RandomAccessFile open() {
528                instanceCount++;
529                return source;
530            }
531    
532            /**
533             * Process a close request for this stream.  If there are multiple
534             * instances using this underlying stream, the stream will not
535             * be closed.
536             *
537             * @exception IOException
538             */
539            public synchronized void close() throws IOException {
540                if (instanceCount > 0) {
541                    instanceCount--;
542                    // if the last open instance, close the real source file.
543                    if (instanceCount == 0) {
544                        source.close();
545                    }
546                }
547            }
548    
549            /**
550             * Read a buffer of data from the shared file.
551             *
552             * @param position The position to read from.
553             * @param buf      The target buffer for storing the read data.
554             * @param offset   The starting offset within the buffer.
555             * @param length   The length to attempt to read.
556             *
557             * @return The number of bytes actually read.
558             * @exception IOException
559             */
560            public synchronized int read(long position, byte[] buf, int offset, int length) throws IOException {
561                // seek to the read location start.  Note this is a shared file, so this assumes all of the methods
562                // doing buffer fills will be synchronized.
563                source.seek(position);
564                return source.read(buf, offset, length);
565            }
566    
567    
568            /**
569             * Ensure the stream is closed when this shared object is finalized.
570             *
571             * @exception Throwable
572             */
573            protected void finalize() throws Throwable {
574                super.finalize();
575                if (instanceCount > 0) {
576                    source.close();
577                }
578            }
579        }
580    }
581