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