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