View Javadoc

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