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 }