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      /** 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 }