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