View Javadoc
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 }