1 /* 2 * #%L 3 * nom.tam FITS library 4 * %% 5 * Copyright (C) 2004 - 2024 nom-tam-fits 6 * %% 7 * This is free and unencumbered software released into the public domain. 8 * 9 * Anyone is free to copy, modify, publish, use, compile, sell, or 10 * distribute this software, either in source code form or as a compiled 11 * binary, for any purpose, commercial or non-commercial, and by any 12 * means. 13 * 14 * In jurisdictions that recognize copyright laws, the author or authors 15 * of this software dedicate any and all copyright interest in the 16 * software to the public domain. We make this dedication for the benefit 17 * of the public at large and to the detriment of our heirs and 18 * successors. We intend this dedication to be an overt act of 19 * relinquishment in perpetuity of all present and future rights to this 20 * software under copyright law. 21 * 22 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 23 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 24 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 25 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 26 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 27 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 * OTHER DEALINGS IN THE SOFTWARE. 29 * #L% 30 */ 31 32 package nom.tam.util; 33 34 import java.io.Closeable; 35 import java.io.EOFException; 36 import java.io.File; 37 import java.io.FileDescriptor; 38 import java.io.FileNotFoundException; 39 import java.io.Flushable; 40 import java.io.IOException; 41 import java.io.RandomAccessFile; 42 import java.nio.channels.FileChannel; 43 44 /** 45 * Basic file input/output with efficient internal buffering. 46 * 47 * @author Attila Kovacs 48 * 49 * @since 1.16 50 */ 51 class BufferedFileIO implements InputReader, OutputWriter, Flushable, Closeable { 52 53 /** Bit mask for a single byte */ 54 protected static final int BYTE_MASK = 0xFF; 55 56 /** The underlying unbuffered random access file IO */ 57 private final RandomAccessFileIO file; 58 59 /** The file position at which the buffer begins */ 60 private long startOfBuf; 61 62 /** The buffer */ 63 private final byte[] buf; 64 65 /** Pointer to the next byte to read/write */ 66 private int offset; 67 68 /** The last legal element in the buffer */ 69 private int end; 70 71 /** Whether the buffer has been modified locally, so it needs to be written back to stream before discarding. */ 72 private boolean isModified; 73 74 /** Whether the current position is beyond the current ennd-of-file */ 75 private boolean writeAhead; 76 77 /** 78 * Instantiates a new buffered random access file with the specified IO mode and buffer size. This class offers up 79 * to 2+ orders of magnitude superior performance over {@link RandomAccessFile} when repeatedly reading or writing 80 * chunks of data at consecutive locations 81 * 82 * @param f the file 83 * @param mode the access mode, such as "rw" (see {@link RandomAccessFile} for more info). 84 * @param bufferSize the size of the buffer in bytes 85 * 86 * @throws IOException if there was an IO error getting the required access to the file. 87 */ 88 @SuppressWarnings("resource") 89 BufferedFileIO(File f, String mode, int bufferSize) throws IOException { 90 this(new RandomFileIO(f, mode), bufferSize); 91 } 92 93 /** 94 * Instantiates a new buffered random access file with the provided RandomAccessFileIO and buffer size. This allows 95 * implementors to provide alternate RandomAccessFile-like implementations, such as network accessed (byte range 96 * request) files. 97 * 98 * @param f the RandomAccessFileIO implementation 99 * @param bufferSize the size of the buffer in bytes 100 */ 101 BufferedFileIO(RandomAccessFileIO f, int bufferSize) { 102 file = f; 103 buf = new byte[bufferSize]; 104 startOfBuf = 0; 105 offset = 0; 106 end = 0; 107 isModified = false; 108 writeAhead = false; 109 } 110 111 /** 112 * Sets a new position in the file for subsequent reading or writing. 113 * 114 * @param newPos the new byte offset from the beginning of the file. It may be beyond the current end of the 115 * file, for example for writing more data after some 'gap'. 116 * 117 * @throws IOException if the position is negative or cannot be set. 118 */ 119 public final synchronized void seek(long newPos) throws IOException { 120 // Check that the new position is valid 121 if (newPos < 0) { 122 throw new IllegalArgumentException("seek at " + newPos); 123 } 124 125 if (newPos <= startOfBuf + end) { 126 // AK: Do not invoke checking length (costly) if not necessary 127 writeAhead = false; 128 } else { 129 writeAhead = newPos > length(); 130 } 131 132 if (newPos < startOfBuf || newPos >= startOfBuf + end) { 133 // The new position is outside the currently buffered region. 134 // Write the current buffer back to the stream, and start anew. 135 flush(); 136 137 // We'll start buffering at the new position next. 138 startOfBuf = newPos; 139 end = 0; 140 } 141 142 // Position within the new buffer... 143 offset = (int) (newPos - startOfBuf); 144 145 matchBufferPos(); 146 } 147 148 /** 149 * Get the channel associated with this file. Note that this returns the channel of the associated RandomAccessFile. 150 * Note that since the BufferedFile buffers the I/O's to the underlying file, the offset of the channel may be 151 * different from the offset of the BufferedFile. This is different for a RandomAccessFile where the offsets are 152 * guaranteed to be the same. 153 * 154 * @return the file channel 155 */ 156 public final FileChannel getChannel() { 157 return file.getChannel(); 158 } 159 160 /** 161 * Get the file descriptor associated with this stream. Note that this returns the file descriptor of the associated 162 * RandomAccessFile. 163 * 164 * @return the file descriptor 165 * 166 * @throws IOException if the descriptor could not be accessed. 167 */ 168 public final FileDescriptor getFD() throws IOException { 169 return file.getFD(); 170 } 171 172 /** 173 * Gets the current read/write position in the file. 174 * 175 * @return the current byte offset from the beginning of the file. 176 */ 177 public final synchronized long getFilePointer() { 178 return startOfBuf + offset; 179 } 180 181 /** 182 * Returns the number of bytes that can still be read from the file before the end is reached. 183 * 184 * @return the number of bytes left to read. 185 * 186 * @throws IOException if there was an IO error. 187 */ 188 private synchronized long getRemaining() throws IOException { 189 long n = file.length() - getFilePointer(); 190 return n > 0 ? n : 0L; 191 } 192 193 /** 194 * Checks if there is more that can be read from this file. If so, it ensures that the next byte is buffered. 195 * 196 * @return <code>true</code> if there is more that can be read from the file (and from the buffer!), or 197 * else <code>false</code>. 198 * 199 * @throws IOException if there was an IO error accessing the file. 200 */ 201 private synchronized boolean makeAvailable() throws IOException { 202 if (offset < end) { 203 return true; 204 } 205 206 if (getRemaining() <= 0) { 207 return false; 208 } 209 210 // Buffer as much as we can. 211 seek(getFilePointer()); 212 end = file.read(buf, 0, buf.length); 213 214 return end > 0; 215 } 216 217 /** 218 * Checks if there are the required number of bytes available to read from this file. 219 * 220 * @param need the number of bytes we need. 221 * 222 * @return <code>true</code> if the needed number of bytes can be read from the file or else 223 * <code>false</code>. 224 * 225 * @throws IOException if there was an IO error accessing the file. 226 */ 227 public final synchronized boolean hasAvailable(int need) throws IOException { 228 if (end >= offset + need) { 229 return true; 230 } 231 return file.length() >= getFilePointer() + need; 232 } 233 234 /** 235 * Returns the current length of the file. 236 * 237 * @return the current length of the file. 238 * 239 * @throws IOException if the operation failed 240 */ 241 public final synchronized long length() throws IOException { 242 // It's either the file's length or that of the end of the (yet) unsynched buffer... 243 if (end > 0) { 244 return Math.max(file.length(), startOfBuf + end); 245 } 246 return file.length(); 247 } 248 249 /** 250 * Sets the length of the file. This method calls the method of the same name in {@link RandomAccessFileIO}. 251 * 252 * @param newLength The number of bytes at which the file is set. 253 * 254 * @throws IOException if the resizing of the underlying stream fails 255 */ 256 public synchronized void setLength(long newLength) throws IOException { 257 // Check if we can change the length inside the current buffer. 258 final long bufEnd = startOfBuf + end; 259 if (newLength >= startOfBuf && newLength < bufEnd) { 260 // If truncated in the buffered region, then truncate the buffer also... 261 end = (int) (newLength - startOfBuf); 262 } else { 263 flush(); 264 end = 0; 265 } 266 267 if (getFilePointer() > newLength) { 268 seek(newLength); 269 } 270 271 // Change the length of the file itself... 272 file.setLength(newLength); 273 } 274 275 /** 276 * Positions the file pointer to match the current buffer pointer. 277 * 278 * @throws IOException if there was an IO error 279 */ 280 private synchronized void matchBufferPos() throws IOException { 281 file.position(getFilePointer()); 282 } 283 284 /** 285 * Positions the buffer pointer to match the current file pointer. 286 * 287 * @throws IOException if there was an IO error 288 */ 289 private synchronized void matchFilePos() throws IOException { 290 seek(file.position()); 291 } 292 293 @Override 294 public synchronized void close() throws IOException { 295 flush(); 296 file.close(); 297 startOfBuf = 0; 298 offset = 0; 299 end = 0; 300 } 301 302 /** 303 * Moves the buffer to the current read/write position. 304 * 305 * @throws IOException if there was an IO error writing the prior data back to the file. 306 */ 307 private synchronized void moveBuffer() throws IOException { 308 seek(getFilePointer()); 309 } 310 311 @Override 312 public synchronized void flush() throws IOException { 313 if (!isModified) { 314 return; 315 } 316 317 // the buffer was modified locally, so we need to write it back to the stream 318 if (end > 0) { 319 file.position(startOfBuf); 320 file.write(buf, 0, end); 321 } 322 isModified = false; 323 } 324 325 @Override 326 public final synchronized void write(int b) throws IOException { 327 if (writeAhead) { 328 setLength(getFilePointer()); 329 } 330 331 if (offset >= buf.length) { 332 // The buffer is full, it's time to write it back to stream, and start anew 333 moveBuffer(); 334 } 335 336 isModified = true; 337 buf[offset++] = (byte) b; 338 339 if (offset > end) { 340 // We are writing at the end of the file, and we need to grow the buffer with the file... 341 end = offset; 342 } 343 } 344 345 @Override 346 public final synchronized int read() throws IOException { 347 if (!makeAvailable()) { 348 // By contract of read(), it returns -1 at th end of file. 349 return -1; 350 } 351 352 // Return the unsigned(!) byte. 353 return buf[offset++] & BYTE_MASK; 354 } 355 356 @Override 357 public final synchronized void write(byte[] b, int from, int len) throws IOException { 358 if (len <= 0) { 359 return; 360 } 361 362 if (writeAhead) { 363 setLength(getFilePointer()); 364 } 365 366 if (len > 2 * buf.length) { 367 // Large direct write... 368 matchBufferPos(); 369 file.write(b, from, len); 370 matchFilePos(); 371 return; 372 } 373 374 while (len > 0) { 375 if (offset >= buf.length) { 376 // The buffer is full, it's time to write it back to stream, and start anew. 377 moveBuffer(); 378 } 379 380 isModified = true; 381 int n = Math.min(len, buf.length - offset); 382 System.arraycopy(b, from, buf, offset, n); 383 384 offset += n; 385 from += n; 386 len -= n; 387 388 if (offset > end) { 389 // We are growing the file, so grow the buffer also... 390 end = offset; 391 } 392 } 393 } 394 395 @Override 396 public final synchronized int read(byte[] b, int from, int len) throws IOException { 397 if (len <= 0) { 398 // Nothing to do. 399 return 0; 400 } 401 402 if (len > 2 * buf.length) { 403 // Large direct read... 404 matchBufferPos(); 405 int l = file.read(b, from, len); 406 matchFilePos(); 407 return l; 408 } 409 410 int got = 0; 411 412 while (got < len) { 413 if (!makeAvailable()) { 414 return got > 0 ? got : -1; 415 } 416 417 int n = Math.min(len - got, end - offset); 418 419 System.arraycopy(buf, offset, b, from, n); 420 421 got += n; 422 offset += n; 423 from += n; 424 } 425 426 return got; 427 } 428 429 /** 430 * Reads bytes to completely fill the supplied buffer. If not enough bytes are avaialable in the file to fully fill 431 * the buffer, an {@link EOFException} will be thrown. 432 * 433 * @param b the buffer 434 * 435 * @throws EOFException if already at the end of file. 436 * @throws IOException if there was an IO error before the buffer could be fully populated. 437 */ 438 public final synchronized void readFully(byte[] b) throws EOFException, IOException { 439 readFully(b, 0, b.length); 440 } 441 442 /** 443 * Reads bytes to fill the supplied buffer with the requested number of bytes from the given starting buffer index. 444 * If not enough bytes are avaialable in the file to deliver the reqauested number of bytes the buffer, an 445 * {@link EOFException} will be thrown. 446 * 447 * @param b the buffer 448 * @param off the buffer index at which to start reading data 449 * @param len the total number of bytes to read. 450 * 451 * @throws EOFException if already at the end of file. 452 * @throws IOException if there was an IO error before the requested number of bytes could all be read. 453 */ 454 public synchronized void readFully(byte[] b, int off, int len) throws EOFException, IOException { 455 while (len > 0) { 456 int n = read(b, off, len); 457 if (n < 0) { 458 throw new EOFException(); 459 } 460 off += n; 461 len -= n; 462 } 463 } 464 465 /** 466 * Same as {@link RandomAccessFile#readUTF()}. 467 * 468 * @return a string 469 * 470 * @throws IOException if there was an IO error while reading from the file. 471 */ 472 public final synchronized String readUTF() throws IOException { 473 matchBufferPos(); 474 String s = file.readUTF(); 475 matchFilePos(); 476 return s; 477 } 478 479 /** 480 * Same as {@link RandomAccessFile#writeUTF(String)} 481 * 482 * @param s a string 483 * 484 * @throws IOException if there was an IO error while writing to the file. 485 */ 486 public final synchronized void writeUTF(String s) throws IOException { 487 matchBufferPos(); 488 file.writeUTF(s); 489 matchFilePos(); 490 } 491 492 /** 493 * Moves the file pointer by a number of bytes from its current position. 494 * 495 * @param n the number of byter to move. Negative values are allowed and result in moving the pointer 496 * backward. 497 * 498 * @return the actual number of bytes that the pointer moved, which may be fewer than requested if the 499 * file boundary was reached. 500 * 501 * @throws IOException if there was an IO error. 502 */ 503 public final synchronized long skip(long n) throws IOException { 504 if (offset + n >= 0 && offset + n <= end) { 505 // Skip within the buffered region... 506 offset = (int) (offset + n); 507 return n; 508 } 509 510 long pos = getFilePointer(); 511 512 n = Math.max(n, -pos); 513 514 seek(pos + n); 515 return n; 516 } 517 518 /** 519 * Read as many bytes into a byte array as possible. The number of bytes read may be fewer than the size of the 520 * array, for example because the end-of-file is reached during the read. 521 * 522 * @param b the byte buffer to fill with data from the file. 523 * 524 * @return the number of bytes actually read. 525 * 526 * @throws IOException if there was an IO error while reading, other than the end-of-file. 527 */ 528 public final synchronized int read(byte[] b) throws IOException { 529 return read(b, 0, b.length); 530 } 531 532 /** 533 * Writes the contents of a byte array into the file. 534 * 535 * @param b the byte buffer to write into the file. 536 * 537 * @throws IOException if there was an IO error while writing to the file... 538 */ 539 public final synchronized void write(byte[] b) throws IOException { 540 write(b, 0, b.length); 541 } 542 543 /** 544 * Default implementation of the RandomAccessFileIO interface. 545 */ 546 static final class RandomFileIO extends RandomAccessFile implements RandomAccessFileIO { 547 RandomFileIO(File file, String mode) throws FileNotFoundException { 548 super(file, mode); 549 } 550 551 @Override 552 public long position() throws IOException { 553 return super.getFilePointer(); 554 } 555 556 @Override 557 public void position(long n) throws IOException { 558 super.seek(n); 559 } 560 } 561 }