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