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