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                // 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                // make sure we zero the count out, otherwise we'll reuse this data 
241                // if called again. 
242                count = pos; 
243                return false;
244            }
245    
246            // seek to the read location start.  Note this is a shared file, so this assumes all of the methods
247            // doing buffer fills will be synchronized.
248            int fillLength = buf.length - pos;
249    
250            // we might be working with a subset of the file data, so normal eof processing might not apply.
251            // we need to limit how much we read to the data length.
252            if (bufpos - start + pos + fillLength > datalen) {
253                fillLength = (int)(datalen - (bufpos - start + pos));
254            }
255    
256            // finally, try to read more data into the buffer.
257            fillLength = source.read(bufpos + pos, buf, pos, fillLength);
258    
259            // we weren't able to read anything, count this as an eof failure.
260            if (fillLength <= 0) {
261                // make sure we zero the count out, otherwise we'll reuse this data 
262                // if called again. 
263                count = pos; 
264                return false;
265            }
266    
267            // set the new buffer count
268            count = fillLength + pos;
269    
270            // we have data in the buffer.
271            return true;
272        }
273    
274    
275        /**
276         * Return the number of bytes available for reading without
277         * blocking for a long period.
278         *
279         * @return For this stream, this is the number of bytes between the
280         *         current read position and the indicated end of the file.
281         * @exception IOException
282         */
283        public synchronized int available() throws IOException {
284            checkOpen();
285    
286            // this is backed by a file, which doesn't really block.  We can return all the way to the
287            // marked data end, if necessary
288            long endMarker = start + datalen;
289            return (int)(endMarker - (bufpos + pos));
290        }
291    
292    
293        /**
294         * Return the current read position of the stream.
295         *
296         * @return The current position relative to the beginning of the stream.
297         *         This is not the position relative to the start of the file, since
298         *         the stream starting position may be other than the beginning.
299         */
300        public long getPosition() {
301            checkOpenRuntime();
302    
303            return bufpos + pos - start;
304        }
305    
306    
307        /**
308         * Mark the current position for retracing.
309         *
310         * @param readlimit The limit for the distance the read position can move from
311         *                  the mark position before the mark is reset.
312         */
313        public synchronized void mark(int readlimit) {
314            checkOpenRuntime();
315            marklimit = readlimit;
316            markpos = pos;
317        }
318    
319    
320        /**
321         * Read a single byte of data from the input stream.
322         *
323         * @return The read byte.  Returns -1 if an eof condition has been hit.
324         * @exception IOException
325         */
326        public synchronized int read() throws IOException {
327            checkOpen();
328    
329            // check to see if we can fill more data
330            if (!checkFill()) {
331                return -1;
332            }
333    
334            // return the current byte...anded to prevent sign extension.
335            return buf[pos++] & 0xff;
336        }
337    
338    
339        /**
340         * Read multiple bytes of data and place them directly into
341         * a byte-array buffer.
342         *
343         * @param buffer The target buffer.
344         * @param offset The offset within the buffer to place the data.
345         * @param length The length to attempt to read.
346         *
347         * @return The number of bytes actually read.  Returns -1 for an EOF
348         *         condition.
349         * @exception IOException
350         */
351        public synchronized int read(byte buffer[], int offset, int length) throws IOException {
352            checkOpen();
353    
354            // asked to read nothing?  That's what we'll do.
355            if (length == 0) {
356                return 0;
357            }
358    
359    
360            int returnCount = 0;
361            while (length > 0) {
362                // check to see if we can/must fill more data
363                if (!checkFill()) {
364                    // we've hit the end, but if we've read data, then return that.
365                    if (returnCount > 0) {
366                        return returnCount;
367                    }
368                    // trun eof.
369                    return -1;
370                }
371    
372                int available = count - pos;
373                int given = Math.min(available, length);
374    
375                System.arraycopy(buf, pos, buffer, offset, given);
376    
377                // now adjust all of our positions and counters
378                pos += given;
379                length -= given;
380                returnCount += given;
381                offset += given;
382            }
383            // return the accumulated count.
384            return returnCount;
385        }
386    
387    
388        /**
389         * Skip the read pointer ahead a given number of bytes.
390         *
391         * @param n      The number of bytes to skip.
392         *
393         * @return The number of bytes actually skipped.
394         * @exception IOException
395         */
396        public synchronized long skip(long n) throws IOException {
397            checkOpen();
398    
399            // nothing to skip, so don't skip
400            if (n <= 0) {
401                return 0;
402            }
403    
404            // see if we need to fill more data, and potentially shift the mark positions
405            if (!checkFill()) {
406                return 0;
407            }
408    
409            long available = count - pos;
410    
411            // the skipped contract allows skipping within the current buffer bounds, so cap it there.
412            long skipped = available < n ? available : n;
413            pos += skipped;
414            return skipped;
415        }
416    
417        /**
418         * Reset the mark position.
419         *
420         * @exception IOException
421         */
422        public synchronized void reset() throws IOException {
423            checkOpen();
424            if (markpos < 0) {
425                throw new IOException("Resetting to invalid mark position");
426            }
427            // if we have a markpos, it will still be in the buffer bounds.
428            pos = markpos;
429        }
430    
431    
432        /**
433         * Indicates the mark() operation is supported.
434         *
435         * @return Always returns true.
436         */
437        public boolean markSupported() {
438            return true;
439        }
440    
441    
442        /**
443         * Close the stream.  This does not close the source file until
444         * the last shared instance is closed.
445         *
446         * @exception IOException
447         */
448        public void close() throws IOException {
449            // already closed?  This is not an error
450            if (in == null) {
451                return;
452            }
453    
454            try {
455                // perform a close on the source version.
456                source.close();
457            } finally {
458                in = null;
459            }
460        }
461    
462    
463        /**
464         * Create a new stream from this stream, using the given
465         * start offset and length.
466         *
467         * @param offset The offset relative to the start of this stream instance.
468         * @param end    The end offset of the substream.  If -1, the end of the parent stream is used.
469         *
470         * @return A new SharedFileInputStream object sharing the same source
471         *         input file.
472         */
473        public InputStream newStream(long offset, long end) {
474            checkOpenRuntime();
475    
476            if (offset < 0) {
477                throw new IllegalArgumentException("Start position is less than 0");
478            }
479            // the default end position is the datalen of the one we're spawning from.
480            if (end == -1) {
481                end = datalen;
482            }
483    
484            // create a new one using the private constructor
485            return new SharedFileInputStream(source, start + (int)offset, (int)(end - offset), bufsize);
486        }
487    
488    
489        /**
490         * Check if the file is open and throw an IOException if not.
491         *
492         * @exception IOException
493         */
494        private void checkOpen() throws IOException {
495            if (in == null) {
496                throw new IOException("Stream has been closed");
497            }
498        }
499    
500    
501        /**
502         * Check if the file is open and throw an IOException if not.  This version is
503         * used because several API methods are not defined as throwing IOException, so
504         * checkOpen() can't be used.  The Sun implementation just throws RuntimeExceptions
505         * in those methods, hence 2 versions.
506         *
507         * @exception RuntimeException
508         */
509        private void checkOpenRuntime() {
510            if (in == null) {
511                throw new RuntimeException("Stream has been closed");
512            }
513        }
514    
515    
516        /**
517         * Internal class used to manage resources shared between the
518         * ShareFileInputStream instances.
519         */
520        class SharedFileSource {
521            // the file source
522            public RandomAccessFile source;
523            // the shared instance count for this file (open instances)
524            public int instanceCount = 0;
525    
526            public SharedFileSource(File file) throws IOException {
527                source = new RandomAccessFile(file, "r");
528            }
529    
530            /**
531             * Open the shared stream to keep track of open instances.
532             */
533            public synchronized RandomAccessFile open() {
534                instanceCount++;
535                return source;
536            }
537    
538            /**
539             * Process a close request for this stream.  If there are multiple
540             * instances using this underlying stream, the stream will not
541             * be closed.
542             *
543             * @exception IOException
544             */
545            public synchronized void close() throws IOException {
546                if (instanceCount > 0) {
547                    instanceCount--;
548                    // if the last open instance, close the real source file.
549                    if (instanceCount == 0) {
550                        source.close();
551                    }
552                }
553            }
554    
555            /**
556             * Read a buffer of data from the shared file.
557             *
558             * @param position The position to read from.
559             * @param buf      The target buffer for storing the read data.
560             * @param offset   The starting offset within the buffer.
561             * @param length   The length to attempt to read.
562             *
563             * @return The number of bytes actually read.
564             * @exception IOException
565             */
566            public synchronized int read(long position, byte[] buf, int offset, int length) throws IOException {
567                // seek to the read location start.  Note this is a shared file, so this assumes all of the methods
568                // doing buffer fills will be synchronized.
569                source.seek(position);
570                return source.read(buf, offset, length);
571            }
572    
573    
574            /**
575             * Ensure the stream is closed when this shared object is finalized.
576             *
577             * @exception Throwable
578             */
579            protected void finalize() throws Throwable {
580                super.finalize();
581                if (instanceCount > 0) {
582                    source.close();
583                }
584            }
585        }
586    }
587