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 /** For thread synchronization */
78 protected Object lock = new Object();
79
80 /**
81 * Instantiates a new buffered random access file with the specified IO mode and buffer size. This class offers up
82 * to 2+ orders of magnitude superior performance over {@link RandomAccessFile} when repeatedly reading or writing
83 * chunks of data at consecutive locations
84 *
85 * @param f the file
86 * @param mode the access mode, such as "rw" (see {@link RandomAccessFile} for more info).
87 * @param bufferSize the size of the buffer in bytes
88 *
89 * @throws IOException if there was an IO error getting the required access to the file.
90 */
91 @SuppressWarnings("resource")
92 BufferedFileIO(File f, String mode, int bufferSize) throws IOException {
93 this(new RandomFileIO(f, mode), bufferSize);
94 }
95
96 /**
97 * Instantiates a new buffered random access file with the provided RandomAccessFileIO and buffer size. This allows
98 * implementors to provide alternate RandomAccessFile-like implementations, such as network accessed (byte range
99 * request) files.
100 *
101 * @param f the RandomAccessFileIO implementation
102 * @param bufferSize the size of the buffer in bytes
103 */
104 BufferedFileIO(RandomAccessFileIO f, int bufferSize) {
105 file = f;
106 buf = new byte[bufferSize];
107 startOfBuf = 0;
108 offset = 0;
109 end = 0;
110 isModified = false;
111 writeAhead = false;
112 }
113
114 /**
115 * Sets a new position in the file for subsequent reading or writing.
116 *
117 * @param newPos the new byte offset from the beginning of the file. It may be beyond the current end of the
118 * file, for example for writing more data after some 'gap'.
119 *
120 * @throws IOException if the position is negative or cannot be set.
121 */
122 public final void seek(long newPos) throws IOException {
123 synchronized (lock) {
124 // Check that the new position is valid
125 if (newPos < 0) {
126 throw new IllegalArgumentException("seek at " + newPos);
127 }
128
129 if (newPos <= startOfBuf + end) {
130 // AK: Do not invoke checking length (costly) if not necessary
131 writeAhead = false;
132 } else {
133 writeAhead = newPos > length();
134 }
135
136 if (newPos < startOfBuf || newPos >= startOfBuf + end) {
137 // The new position is outside the currently buffered region.
138 // Write the current buffer back to the stream, and start anew.
139 flush();
140
141 // We'll start buffering at the new position next.
142 startOfBuf = newPos;
143 end = 0;
144 }
145
146 // Position within the new buffer...
147 offset = (int) (newPos - startOfBuf);
148
149 matchBufferPos();
150 }
151 }
152
153 /**
154 * Get the channel associated with this file. Note that this returns the channel of the associated RandomAccessFile.
155 * Note that since the BufferedFile buffers the I/O's to the underlying file, the offset of the channel may be
156 * different from the offset of the BufferedFile. This is different for a RandomAccessFile where the offsets are
157 * guaranteed to be the same.
158 *
159 * @return the file channel
160 */
161 public final FileChannel getChannel() {
162 return file.getChannel();
163 }
164
165 /**
166 * Get the file descriptor associated with this stream. Note that this returns the file descriptor of the associated
167 * RandomAccessFile.
168 *
169 * @return the file descriptor
170 *
171 * @throws IOException if the descriptor could not be accessed.
172 */
173 public final FileDescriptor getFD() throws IOException {
174 return file.getFD();
175 }
176
177 /**
178 * Gets the current read/write position in the file.
179 *
180 * @return the current byte offset from the beginning of the file.
181 */
182 public final long getFilePointer() {
183 synchronized (lock) {
184 return startOfBuf + offset;
185 }
186 }
187
188 /**
189 * Returns the number of bytes that can still be read from the file before the end is reached.
190 *
191 * @return the number of bytes left to read.
192 *
193 * @throws IOException if there was an IO error.
194 */
195 private long getRemaining() throws IOException {
196 synchronized (lock) {
197 long n = file.length() - getFilePointer();
198 return n > 0 ? n : 0L;
199 }
200 }
201
202 /**
203 * Checks if there is more that can be read from this file. If so, it ensures that the next byte is buffered.
204 *
205 * @return <code>true</code> if there is more that can be read from the file (and from the buffer!), or
206 * else <code>false</code>.
207 *
208 * @throws IOException if there was an IO error accessing the file.
209 */
210 private boolean makeAvailable() throws IOException {
211 synchronized (lock) {
212 if (offset < end) {
213 return true;
214 }
215
216 if (getRemaining() <= 0) {
217 return false;
218 }
219
220 // Buffer as much as we can.
221 seek(getFilePointer());
222 end = file.read(buf, 0, buf.length);
223
224 return end > 0;
225 }
226 }
227
228 /**
229 * Checks if there are the required number of bytes available to read from this file.
230 *
231 * @param need the number of bytes we need.
232 *
233 * @return <code>true</code> if the needed number of bytes can be read from the file or else
234 * <code>false</code>.
235 *
236 * @throws IOException if there was an IO error accessing the file.
237 */
238 public final boolean hasAvailable(int need) throws IOException {
239 synchronized (lock) {
240 if (end >= offset + need) {
241 return true;
242 }
243 return file.length() >= getFilePointer() + need;
244 }
245 }
246
247 /**
248 * Returns the current length of the file.
249 *
250 * @return the current length of the file.
251 *
252 * @throws IOException if the operation failed
253 */
254 public final long length() throws IOException {
255 synchronized (lock) {
256 // It's either the file's length or that of the end of the (yet) unsynched buffer...
257 if (end > 0) {
258 return Math.max(file.length(), startOfBuf + end);
259 }
260 return file.length();
261 }
262 }
263
264 /**
265 * Sets the length of the file. This method calls the method of the same name in {@link RandomAccessFileIO}.
266 *
267 * @param newLength The number of bytes at which the file is set.
268 *
269 * @throws IOException if the resizing of the underlying stream fails
270 */
271 public void setLength(long newLength) throws IOException {
272 synchronized (lock) {
273 // Check if we can change the length inside the current buffer.
274 final long bufEnd = startOfBuf + end;
275 if (newLength >= startOfBuf && newLength < bufEnd) {
276 // If truncated in the buffered region, then truncate the buffer also...
277 end = (int) (newLength - startOfBuf);
278 } else {
279 flush();
280 end = 0;
281 }
282
283 if (getFilePointer() > newLength) {
284 seek(newLength);
285 }
286
287 // Change the length of the file itself...
288 file.setLength(newLength);
289 }
290 }
291
292 /**
293 * Positions the file pointer to match the current buffer pointer.
294 *
295 * @throws IOException if there was an IO error
296 */
297 private void matchBufferPos() throws IOException {
298 synchronized (lock) {
299 file.position(getFilePointer());
300 }
301 }
302
303 /**
304 * Positions the buffer pointer to match the current file pointer.
305 *
306 * @throws IOException if there was an IO error
307 */
308 private void matchFilePos() throws IOException {
309 synchronized (lock) {
310 seek(file.position());
311 }
312 }
313
314 @Override
315 public void close() throws IOException {
316 synchronized (lock) {
317 flush();
318 file.close();
319 startOfBuf = 0;
320 offset = 0;
321 end = 0;
322 }
323 }
324
325 /**
326 * Moves the buffer to the current read/write position.
327 *
328 * @throws IOException if there was an IO error writing the prior data back to the file.
329 */
330 private void moveBuffer() throws IOException {
331 synchronized (lock) {
332 seek(getFilePointer());
333 }
334 }
335
336 @Override
337 public void flush() throws IOException {
338 synchronized (lock) {
339 if (!isModified) {
340 return;
341 }
342
343 // the buffer was modified locally, so we need to write it back to the stream
344 if (end > 0) {
345 file.position(startOfBuf);
346 file.write(buf, 0, end);
347 }
348 isModified = false;
349 }
350 }
351
352 @Override
353 public final void write(int b) throws IOException {
354 synchronized (lock) {
355
356 if (writeAhead) {
357 setLength(getFilePointer());
358 }
359
360 if (offset >= buf.length) {
361 // The buffer is full, it's time to write it back to stream, and start anew
362 moveBuffer();
363 }
364
365 isModified = true;
366 buf[offset++] = (byte) b;
367
368 if (offset > end) {
369 // We are writing at the end of the file, and we need to grow the buffer with the file...
370 end = offset;
371 }
372 }
373 }
374
375 @Override
376 public final int read() throws IOException {
377 synchronized (lock) {
378 if (!makeAvailable()) {
379 // By contract of read(), it returns -1 at th end of file.
380 return -1;
381 }
382
383 // Return the unsigned(!) byte.
384 return buf[offset++] & BYTE_MASK;
385 }
386 }
387
388 @Override
389 public final void write(byte[] b, int from, int len) throws IOException {
390 if (len <= 0) {
391 return;
392 }
393
394 synchronized (lock) {
395 if (writeAhead) {
396 setLength(getFilePointer());
397 }
398
399 if (len > 2 * buf.length) {
400 // Large direct write...
401 matchBufferPos();
402 file.write(b, from, len);
403 matchFilePos();
404 return;
405 }
406
407 while (len > 0) {
408 if (offset >= buf.length) {
409 // The buffer is full, it's time to write it back to stream, and start anew.
410 moveBuffer();
411 }
412
413 isModified = true;
414 int n = Math.min(len, buf.length - offset);
415 System.arraycopy(b, from, buf, offset, n);
416
417 offset += n;
418 from += n;
419 len -= n;
420
421 if (offset > end) {
422 // We are growing the file, so grow the buffer also...
423 end = offset;
424 }
425 }
426 }
427 }
428
429 @Override
430 public final int read(byte[] b, int from, int len) throws IOException {
431 if (len <= 0) {
432 // Nothing to do.
433 return 0;
434 }
435
436 synchronized (lock) {
437 if (len > 2 * buf.length) {
438 // Large direct read...
439 matchBufferPos();
440 int l = file.read(b, from, len);
441 matchFilePos();
442 return l;
443 }
444
445 int got = 0;
446
447 while (got < len) {
448 if (!makeAvailable()) {
449 return got > 0 ? got : -1;
450 }
451
452 int n = Math.min(len - got, end - offset);
453
454 System.arraycopy(buf, offset, b, from, n);
455
456 got += n;
457 offset += n;
458 from += n;
459 }
460
461 return got;
462 }
463 }
464
465 /**
466 * Reads bytes to completely fill the supplied buffer. If not enough bytes are avaialable in the file to fully fill
467 * the buffer, an {@link EOFException} will be thrown.
468 *
469 * @param b the buffer
470 *
471 * @throws EOFException if already at the end of file.
472 * @throws IOException if there was an IO error before the buffer could be fully populated.
473 */
474 public final void readFully(byte[] b) throws EOFException, IOException {
475 synchronized (lock) {
476 readFully(b, 0, b.length);
477 }
478 }
479
480 /**
481 * Reads bytes to fill the supplied buffer with the requested number of bytes from the given starting buffer index.
482 * If not enough bytes are avaialable in the file to deliver the reqauested number of bytes the buffer, an
483 * {@link EOFException} will be thrown.
484 *
485 * @param b the buffer
486 * @param off the buffer index at which to start reading data
487 * @param len the total number of bytes to read.
488 *
489 * @throws EOFException if already at the end of file.
490 * @throws IOException if there was an IO error before the requested number of bytes could all be read.
491 */
492 public void readFully(byte[] b, int off, int len) throws EOFException, IOException {
493 synchronized (lock) {
494 while (len > 0) {
495 int n = read(b, off, len);
496 if (n < 0) {
497 throw new EOFException();
498 }
499 off += n;
500 len -= n;
501 }
502 }
503 }
504
505 /**
506 * Same as {@link RandomAccessFile#readUTF()}.
507 *
508 * @return a string
509 *
510 * @throws IOException if there was an IO error while reading from the file.
511 */
512 public final String readUTF() throws IOException {
513 synchronized (lock) {
514 matchBufferPos();
515 String s = file.readUTF();
516 matchFilePos();
517 return s;
518 }
519 }
520
521 /**
522 * Same as {@link RandomAccessFile#writeUTF(String)}
523 *
524 * @param s a string
525 *
526 * @throws IOException if there was an IO error while writing to the file.
527 */
528 public final void writeUTF(String s) throws IOException {
529 synchronized (lock) {
530 matchBufferPos();
531 file.writeUTF(s);
532 matchFilePos();
533 }
534 }
535
536 /**
537 * Moves the file pointer by a number of bytes from its current position.
538 *
539 * @param n the number of byter to move. Negative values are allowed and result in moving the pointer
540 * backward.
541 *
542 * @return the actual number of bytes that the pointer moved, which may be fewer than requested if the
543 * file boundary was reached.
544 *
545 * @throws IOException if there was an IO error.
546 */
547 public final long skip(long n) throws IOException {
548 synchronized (lock) {
549 if (offset + n >= 0 && offset + n <= end) {
550 // Skip within the buffered region...
551 offset = (int) (offset + n);
552 return n;
553 }
554
555 long pos = getFilePointer();
556
557 n = Math.max(n, -pos);
558
559 seek(pos + n);
560 return n;
561 }
562 }
563
564 /**
565 * Read as many bytes into a byte array as possible. The number of bytes read may be fewer than the size of the
566 * array, for example because the end-of-file is reached during the read.
567 *
568 * @param b the byte buffer to fill with data from the file.
569 *
570 * @return the number of bytes actually read.
571 *
572 * @throws IOException if there was an IO error while reading, other than the end-of-file.
573 */
574 public final int read(byte[] b) throws IOException {
575 synchronized (lock) {
576 return read(b, 0, b.length);
577 }
578 }
579
580 /**
581 * Writes the contents of a byte array into the file.
582 *
583 * @param b the byte buffer to write into the file.
584 *
585 * @throws IOException if there was an IO error while writing to the file...
586 */
587 public final void write(byte[] b) throws IOException {
588 synchronized (lock) {
589 write(b, 0, b.length);
590 }
591 }
592
593 /**
594 * Default implementation of the RandomAccessFileIO interface.
595 */
596 static final class RandomFileIO extends RandomAccessFile implements RandomAccessFileIO {
597 RandomFileIO(File file, String mode) throws FileNotFoundException {
598 super(file, mode);
599 }
600
601 @Override
602 public long position() throws IOException {
603 return super.getFilePointer();
604 }
605
606 @Override
607 public void position(long n) throws IOException {
608 super.seek(n);
609 }
610 }
611 }