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