/* XXL: The eXtensible and fleXible Library for data processing Copyright (C) 2000-2011 Prof. Dr. Bernhard Seeger Head of the Database Research Group Department of Mathematics and Computer Science University of Marburg Germany This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; If not, see <http://www.gnu.org/licenses/>. http://code.google.com/p/xxl/ */ package xxl.core.io; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import xxl.core.functions.AbstractFunction; import xxl.core.functions.Function; import xxl.core.util.WrappingRuntimeException; /** * This class provides a buffered random access file that reads and writes * whole blocks of buffered bytes to and from the random access file. When * reading bytes from the file, the buffer is checked first, whether it * contains the desired bytes. When the desired bytes are not buffered, * whole blocks containing the desired bytes are read into the buffer. * When writing bytes to the file, the bytes are written to the buffered * blocks (that are read from the file into the buffer when needed). So * every read and write operation works actually on the buffer and does * only access the file, when the desired bytes are not buffered. Every * time a block is flushed out of the buffer, the according bytes in the * random access file are updated.<p> * * Example usage (1). * <pre> * // catch IOExceptions * * try { * * // create a new buffered random access file with ... * * BufferedRandomAccessFile file = new BufferedRandomAccessFile( * * // a new file * * new File("file.dat"), * * // read and write access * * "rw", * * // an LRU buffer with 5 slots * * new LRUBuffer(5), * * // a block size of 20 bytes * * 20 * ); * * // write data to the data output stream * * file.writeInt(200); * file.writeUTF("Some data!"); * file.writeUTF("More data!"); * file.writeBoolean(true); * file.writeUTF("Another bundle of data!"); * file.writeUTF("The last bundle of data!"); * * // set the file pointer to the first string * * file.seek(4); * * // print the data of the data input stream * * System.out.println(file.readUTF()); * System.out.println(file.readUTF()); * System.out.println(file.readBoolean()); * System.out.println(file.readUTF()); * System.out.println(file.readUTF()); * * // set the file pointer to the beginning of the file * * file.seek(0); * * // print the first integer * * System.out.println(file.readInt()); * * // close the open streams * * file.close(); * } * catch (IOException ioe) { * System.out.println("An I/O error occured."); * } * </pre> * * @see File * @see Function * @see IOException * @see RandomAccessFile * @see WrappingRuntimeException */ public class BufferedRandomAccessFile extends RandomAccessFile { /** * The buffer that is used for storing the blocks of buffered bytes. */ protected Buffer buffer; /** * The size of the blocks that are used for storing the buffered bytes * of this random access file. In other words, every block can be used * for buffering <tt>blockSize</tt> bytes of this random access file. */ protected int blockSize; /** * The file pointer is a kind of cursor, or index into the random * access file; input operations read bytes starting at the file * pointer and advance the file pointer past the bytes read. If the * random access file is in read/write mode, output operations write * bytes starting at the file pointer and advance the file pointer * past the bytes written. */ protected long filePointer; /** * The length of the buffered random access file. */ protected long length; /** * The block of buffered bytes that is accessed at last. This byte * array contains the bytes of the random access file that are read or * written at last. */ protected byte [] block = null; /** * The id of the actual block in the buffer. The <tt>blockId</tt> is * determined by the offset of the block's bytes in the random access * file and <tt>blockSize</tt>, i.e. the first <tt>blockSize</tt> * bytes of the file will be buffered in the block with <tt>blockId * 0</tt>, the next <tt>blockSize</tt> bytes int hte block with * <tt>blockSize 1</tt> and so on. */ protected long blockId = 0; /** * A simple counter for counting I/Os. It is increased every time a * block is read from the file into the buffer or the other way round. * It can be used for comparing the efficiency of buffered and * unbuffered files or of buffers with different displacement * strategies. */ public final IOCounter counter; /** * Constructs a new buffered random access file stream to read from, * and optionally to write to, the file specified by the <tt>File</tt> * argument. <br> * The mode argument must either be equal to <tt>"r"</tt> or * <tt>"rw"</tt>, indicating that the file is to be opened for input * only or for both input and output, respectively. The write methods * on this object will always throw an <tt>IOException</tt> if the * file is opened with a mode of <tt>"r"</tt>. If the mode is * <tt>"rw"</tt> and the file does not exist, then an attempt is made * to create it. An <tt>IOException</tt> is thrown if the file * argument refers to a directory.<br> * The specified buffer is used for buffering the bytes of the random * access file. The buffered bytes are stored in blocks with the * specified size.<br> * This constructor is equivalent to th call of * <code>BufferedRandomAccessFile(file, mode, buffer, blockSize, new IOCounter())</code>. * * @param file the file object. * @param mode the access mode. * @param buffer the buffer that is used for buffering the file. * @param blockSize the size of a block of buffered bytes. * @throws IllegalArgumentException if the mode argument is not equal * to <tt>"r"</tt> or to <tt>"rw"</tt>. * @throws java.io.FileNotFoundException if the file exists but is a * directory rather than a regular file, or cannot be opened * or created for any other reason. * @throws IOException if an I/O error occurs. * @throws SecurityException if a security manager exists and its * checkRead method denies read access to the file or the mode * is <tt>"rw"</tt> and the security manager's checkWrite * method denies write access to the file. */ public BufferedRandomAccessFile (File file, String mode, Buffer buffer, int blockSize) throws IOException { this(file, mode, buffer, blockSize, new IOCounter()); } /** * Creates a random access file stream to read from, and optionally to * write to, a file with the specified name. <br> * The mode argument must either be equal to <tt>"r"</tt> or * <tt>"rw"</tt>, indicating that the file is to be opened for input * only or for both input and output, respectively. The write methods * on this object will always throw an <tt>IOException</tt> if the * file is opened with a mode of <tt>"r"</tt>. If the mode is * <tt>"rw"</tt> and the file does not exist, then an attempt is made * to create it. An <tt>IOException</tt> is thrown if the name * argument refers to a directory.<br> * The specified buffer is used for buffering the bytes of the random * access file. The buffered bytes are stored in blocks with the * specified size. * * @param name the system-dependent filename. * @param mode the access mode. * @param buffer the buffer that is used for buffering the file. * @param blockSize the size of a block of buffered bytes. * @throws IllegalArgumentException if the mode argument is not equal * to <tt>"r"</tt> or to <tt>"rw"</tt>. * @throws java.io.FileNotFoundException if the file exists but is a * directory rather than a regular file, or cannot be opened * or created for any other reason. * @throws IOException if an I/O error occurs. * @throws SecurityException if a security manager exists and its * checkRead method denies read access to the file or the mode * is <tt>"rw"</tt> and the security manager's checkWrite * method denies write access to the file. */ public BufferedRandomAccessFile (String name, String mode, Buffer buffer, int blockSize) throws IOException { this(name, mode, buffer, blockSize, new IOCounter()); } /** * Constructs a new buffered random access file stream to read from, * and optionally to write to, the file specified by the <tt>File</tt> * argument. <br> * The mode argument must either be equal to <tt>"r"</tt> or * <tt>"rw"</tt>, indicating that the file is to be opened for input * only or for both input and output, respectively. The write methods * on this object will always throw an <tt>IOException</tt> if the * file is opened with a mode of <tt>"r"</tt>. If the mode is * <tt>"rw"</tt> and the file does not exist, then an attempt is made * to create it. An <tt>IOException</tt> is thrown if the file * argument refers to a directory.<br> * The specified buffer is used for buffering the bytes of the random * access file. The buffered bytes are stored in blocks with the * specified size. The specified I/O counter will be continued in * order to count the I/Os caused by the buffered random access file. * * @param file the file object. * @param mode the access mode. * @param buffer the buffer that is used for buffering the file. * @param blockSize the size of a block of buffered bytes. * @param counter an I/O counter to be continued. * @throws IllegalArgumentException if the mode argument is not equal * to <tt>"r"</tt> or to <tt>"rw"</tt>. * @throws java.io.FileNotFoundException if the file exists but is a * directory rather than a regular file, or cannot be opened * or created for any other reason. * @throws IOException if an I/O error occurs. * @throws SecurityException if a security manager exists and its * checkRead method denies read access to the file or the mode * is <tt>"rw"</tt> and the security manager's checkWrite * method denies write access to the file. */ public BufferedRandomAccessFile (File file, String mode, Buffer buffer, int blockSize, IOCounter counter) throws IOException { super(file, mode); this.buffer = buffer; this.blockSize = blockSize; this.length = super.length(); this.filePointer = super.getFilePointer(); this.counter = counter; } /** * Creates a random access file stream to read from, and optionally to * write to, a file with the specified name. <br> * The mode argument must either be equal to <tt>"r"</tt> or * <tt>"rw"</tt>, indicating that the file is to be opened for input * only or for both input and output, respectively. The write methods * on this object will always throw an <tt>IOException</tt> if the * file is opened with a mode of <tt>"r"</tt>. If the mode is * <tt>"rw"</tt> and the file does not exist, then an attempt is made * to create it. An <tt>IOException</tt> is thrown if the name * argument refers to a directory.<br> * The specified buffer is used for buffering the bytes of the random * access file. The buffered bytes are stored in blocks with the * specified size. The specified I/O counter will be continued in * order to count the I/Os caused by the buffered random access file. * * @param name the system-dependent filename. * @param mode the access mode. * @param buffer the buffer that is used for buffering the file. * @param blockSize the size of a block of buffered bytes. * @param counter an I/O counter to be continued. * @throws IllegalArgumentException if the mode argument is not equal * to <tt>"r"</tt> or to <tt>"rw"</tt>. * @throws java.io.FileNotFoundException if the file exists but is a * directory rather than a regular file, or cannot be opened * or created for any other reason. * @throws IOException if an I/O error occurs. * @throws SecurityException if a security manager exists and its * checkRead method denies read access to the file or the mode * is <tt>"rw"</tt> and the security manager's checkWrite * method denies write access to the file. */ public BufferedRandomAccessFile (String name, String mode, Buffer buffer, int blockSize, IOCounter counter) throws IOException { super(name, mode); this.buffer = buffer; this.blockSize = blockSize; this.length = super.length(); this.filePointer = super.getFilePointer(); this.counter = counter; } /** * Closes this buffered random access file stream and releases any * system resources associated with the stream. A closed buffered * random access file cannot perform input or output operations and * cannot be reopened.<br> * This implementation flushes the buffer first. Thereafter all * buffered blocks are removed out of the buffer and actual block is * set to <tt>null</tt>. At last the random access file is closed. * * @throws IOException if an I/O error occurs. */ public void close () throws IOException { try { buffer.flushAll(this); buffer.removeAll(this); block = null; } catch (WrappingRuntimeException wre) { if (wre.throwable instanceof IOException) throw (IOException)wre.throwable; else throw wre; } super.close(); } /** * Returns the current offset in this file. * * @return the offset from the beginning of the file, in bytes, at * which the next read or write occurs. * @throws IOException if an I/O error occurs. */ public long getFilePointer () throws IOException { return filePointer; } /** * Returns the length of this file. * * @return the length of this file, measured in bytes. * @throws IOException if an I/O error occurs. */ public long length () throws IOException { return length; } /** * Sets <tt>block</tt> to the block of buffered bytes that contains * the byte the file pointer points to. When this block is not * buffered, the actual block is unfixed and a new block containing * the desired bytes is inserted into the buffer. * * @param readBlock determines whether to read the block from file * or just to provide an empty block frame. * @throws IOException if an I/O error occurs. */ protected void getBlock (boolean readBlock) throws IOException { if (block!=null && filePointer/blockSize!=blockId) { buffer.unfix(this, new Long(blockId)); block = null; } if (block==null) { blockId = filePointer/blockSize; try { block = readBlock? (byte[])buffer.get(this, new Long(blockId), new AbstractFunction() { public Object invoke (Object id) { return readBlock(id); } }, false ): new byte[blockSize]; } catch (WrappingRuntimeException wre) { if (wre.throwable instanceof IOException) throw (IOException)wre.throwable; else throw wre; } } } /** * Reads the block of bytes with the given id and returns it. The id * and <tt>blockSize</tt> determines the block of bytes to read. The * id <tt>0</tt> identifies the first <tt>blockSize</tt> bytes of the * file, the id <tt>1</tt> identifies the second <tt>blockSize</tt> * bytes and so on. After reading the block of bytes, the I/O counter * is increased and the byte array containing this block is returned. * @param id The id of the block (type Long). * @return The block as a byte array. */ protected byte [] readBlock (Object id) { try { block = new byte[blockSize]; super.seek(((Long)id).longValue()*blockSize); super.read(block); counter.readIO++; return block; } catch (IOException ie) { throw new WrappingRuntimeException(ie); } } /** * Reads up to <tt>len</tt> bytes of data from the buffered file into * an array of bytes. This method blocks until at least one byte of * input is available.<br> * Although <tt>BufferedRandomAccessFile</tt> is not a subclass of * <tt>InputStream</tt>, this method behaves in the exactly the same * way as the <tt>InputStream.read(byte[], int, int)</tt> method of * <tt>InputStream</tt>. * * @param array the byte array into which the data is read. * @param off the start offset of the data. * @param len the maximum number of bytes read. * @return the total number of bytes read into the array, or * <tt>-1</tt> if there is no more data because the end of the * file has been reached. * @throws IOException if an I/O error occurs. */ public int read (byte [] array, int off, int len) throws IOException { int n = 0; int b; while (len-->0 && (b = read())>=0) array[off+n++] = (byte)b; return n==0? -1: n; } /** * Reads up to <tt>array.length</tt> bytes of data from the buffered * file into an array of bytes. This method blocks until at least one * byte of input is available.<br> * Although <tt>BufferedRandomAccessFile</tt> is not a subclass of * <tt>InputStream</tt>, this method behaves in the exactly the same * way as the <tt>InputStream.read(byte[])</tt> method of * <tt>InputStream</tt>. * * @param array the byte array into which the data is read. * @return the total number of bytes read into the array, or * <tt>-1</tt> if there is no more data because the end of * this file has been reached. * @throws IOException if an I/O error occurs. */ public int read (byte [] array) throws IOException { return read(array, 0, array.length); } /** * Reads a byte of data from this file. The byte is returned as an * <tt>int</tt> value in the range 0 to 255 <tt>(0x00-0x0ff)</tt>. * This method blocks if no input is yet available. <br> * This implementation sets <tt>block</tt> to the block that contains * the desired byte by calling the getBlock method. Thereafter the * desired byte is read and returned.<br> * Although <tt>BufferedRandomAccessFile</tt> is not a subclass of * <tt>InputStream</tt>, this method behaves in exactly the same way * as the <tt>InputStream.read()</tt> method of <tt>InputStream</tt>. * * @return the next byte of data, or <tt>-1</tt> if the end of the * file has been reached. * @throws IOException if an I/O error occurs. Not thrown if * end-of-file has been reached. */ public int read () throws IOException { if (filePointer>=length) return -1; getBlock(true); return block[(int)(filePointer++%blockSize)]&255; } /** * Sets the file-pointer offset, measured from the beginning of this * file, at which the next read or write occurs. The offset may be set * beyond the end of the file. Setting the offset beyond the end of * the file does not change the file length. The file length will * change only by writing after the offset has been set beyond the end * of the file. * * @param pos the offset position, measured in bytes from the * beginning of the file, at which to set the file pointer. * @throws IOException if <tt>pos</tt> is less than <tt>0</tt> or if * an I/O error occurs. */ public void seek (long pos) throws IOException { filePointer = pos; } /** * Sets the length of this file. <br> * If the present length of the file as returned by the * <tt>length</tt> method is greater than the <tt>newLength</tt> * argument then the file will be truncated. In this case, if the file * offset as returned by the <tt>getFilePointer</tt> method is greater * then <tt>newLength</tt> then after this method returns the offset * will be equal to <tt>newLength</tt>. Also every block storing only * buffered bytes of the truncated part of the file is removed out of * the buffer.<br> * If the present length of the file as returned by the * <tt>length</tt> method is smaller than the <tt>newLength</tt> * argument then the file will be extended. In this case, the contents * of the extended portion of the file are not defined. * * @param newLength the desired length of the file. * @throws IOException if an I/O error occurs. */ public void setLength (long newLength) throws IOException { super.setLength(newLength); if (newLength<length) { if (blockId*blockSize>=newLength) block = null; for (long id = (length-1)/blockSize; id>(newLength-1)/blockSize; id--) buffer.remove(this, new Long(id)); } length = newLength; if (filePointer>length) filePointer = length; } /** * Overwrites the block of bytes with the given id with the specified * object (byte array). The id and <tt>blockSize</tt> determines the * block of bytes to overwrite. The id <tt>0</tt> identifies the first * <tt>blockSize</tt> bytes of the file, the id <tt>1</tt> identifies * the second <tt>blockSize</tt> bytes and so on. After writing the * block of bytes, the I/O counter is increased. * @param id position inside the file as Long. * @param object byte array to be written. */ protected void writeBlock (Object id, Object object) { try { super.seek(((Long)id).longValue()*blockSize); super.write((byte[])object, 0, Math.min(blockSize, (int)(length-((Long)id).longValue()*blockSize))); counter.writeIO++; } catch (IOException ie) { throw new WrappingRuntimeException(ie); } } /** * Writes <tt>len</tt> bytes from the specified byte array starting at * offset <tt>off</tt> to this buffered file. * * @param array the data. * @param off the start offset in the data. * @param len the number of bytes to write. * @throws IOException if an I/O error occurs. */ public void write (byte [] array, int off, int len) throws IOException { while (len>0) { int piece = Math.min(len, (int)(blockSize-filePointer%blockSize)); getBlock(piece<blockSize); System.arraycopy(array, off, block, (int)(filePointer%blockSize), piece); try { buffer.update(this, new Long(blockId), block, new AbstractFunction() { public Object invoke (Object id, Object object) { writeBlock(id, object); return null; } }, false ); } catch (WrappingRuntimeException wre) { if (wre.throwable instanceof IOException) throw (IOException)wre.throwable; else throw wre; } off += piece; len -= piece; filePointer += piece; if (length<filePointer) length = filePointer; } } /** * Writes <tt>b.length</tt> bytes from the specified byte array to * this buffered file, starting at the current file pointer. * * @param array the data. * @throws IOException if an I/O error occurs. */ public void write (byte [] array) throws IOException { write(array, 0, array.length); } /** * Writes the specified byte to this buffered file. The write starts * at the current file pointer.<br> * This implementation sets <tt>block</tt> to the block that contains * the desired byte by calling the getBlock method. Thereafter the * flush function of the buffered block is updated by a function that * writes the block of bytes to the random access file, when it is * invoked. At last the desired byte is written to the actual block. * * @param b the byte to be written. * @throws IOException if an I/O error occurs. */ public void write (int b) throws IOException { write(new byte[] {(byte)b}); } }