View Javadoc

1   /**
2    *
3    * Copyright 2006 The Apache Software Foundation
4    *
5    *  Licensed under the Apache License, Version 2.0 (the "License");
6    *  you may not use this file except in compliance with the License.
7    *  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   */
17  
18  package javax.mail.util;
19  
20  import java.io.BufferedInputStream;
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.RandomAccessFile;
25  
26  import javax.mail.internet.SharedInputStream;
27  
28  public class SharedFileInputStream extends BufferedInputStream implements SharedInputStream {
29  
30  
31      // This initial size isn't documented, but bufsize is 2048 after initialization for the
32      // Sun implementation.
33      private static final int DEFAULT_BUFFER_SIZE = 2048;
34  
35      // the shared file information, used to synchronize opens/closes of the base file.
36      private SharedFileSource source;
37  
38      /**
39       * The file offset that is the first byte in the read buffer.
40       */
41      protected long bufpos;
42  
43      /**
44       * The normal size of the read buffer.
45       */
46      protected int bufsize;
47  
48      /**
49       * The size of the file subset represented by this stream instance.
50       */
51      protected long datalen;
52  
53      /**
54       * The source of the file data.  This is shared across multiple
55       * instances.
56       */
57      protected RandomAccessFile in;
58  
59      /**
60       * The starting position of data represented by this stream relative
61       * to the start of the file data.  This stream instance represents
62       * data in the range start to (start + datalen - 1).
63       */
64      protected long start;
65  
66  
67      /**
68       * Construct a SharedFileInputStream from a file name, using the default buffer size.
69       *
70       * @param file   The name of the file.
71       *
72       * @exception IOException
73       */
74      public SharedFileInputStream(String file) throws IOException {
75          this(file, DEFAULT_BUFFER_SIZE);
76      }
77  
78  
79      /**
80       * Construct a SharedFileInputStream from a File object, using the default buffer size.
81       *
82       * @param file   The name of the file.
83       *
84       * @exception IOException
85       */
86      public SharedFileInputStream(File file) throws IOException {
87          this(file, DEFAULT_BUFFER_SIZE);
88      }
89  
90  
91      /**
92       * Construct a SharedFileInputStream from a file name, with a given initial buffer size.
93       *
94       * @param file       The name of the file.
95       * @param bufferSize The initial buffer size.
96       *
97       * @exception IOException
98       */
99      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