/* * BioJava development code * * This code may be freely distributed and modified under the * terms of the GNU Lesser General Public Licence. This should * be distributed with the code. If you do not have a copy, * see: * * http://www.gnu.org/copyleft/lesser.html * * Copyright for this code is held jointly by the individual * authors. These should be listed in @author doc comments. * * For more information on the BioJava project and its aims, * or to join the biojava-l mailing list, visit the home page * at: * * http://www.biojava.org/ * */ package org.biojava.utils; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.util.AbstractList; import java.util.Comparator; import java.util.Iterator; /** * FileAsList creates a writable List * implementation backed by a random access file. There is a * restriction on the record length that the string representation of * that integer may not be longer than 4 bytes. This is because a * fixed 4 byte leader is used to encode the record length in the * file. * * @author Matthew Pocock * @author Keith James * @author Greg Cox */ // fixme: throughout this class, we are raising assertions for things that // are legitimiate exceptions. This needs re-factoring. public abstract class FileAsList extends AbstractList implements Commitable { private static final int LEADER = 4; private RandomAccessFile mappedFile; private int commitedRecords; private int lastIndx = -1; private Object lastRec; private byte[] buffer; private int sizeCache = -1; /** * Creates a new FileAsList and corresponding backing * file. * * @param mappedFile a File used to back the * list. This file must not already exist. * @param recordLength an int byte record length. * * @exception IOException if an error occurs. */ public FileAsList(File mappedFile, int recordLength) throws IOException { if(mappedFile.exists()) { throw new IOException("Can't create file as it already exists: " + mappedFile); } mappedFile.createNewFile(); this.mappedFile = new RandomAccessFile(mappedFile, "rw"); buffer = new byte[recordLength]; this.mappedFile.seek(0L); byte[] rl = String.valueOf(recordLength).getBytes(); if(rl.length > LEADER) { throw new IOException("Length of record too long"); // FIXME: ugg } for(int i = 0; i < rl.length; i++) { this.mappedFile.write(rl[i]); } for(int i = rl.length; i < LEADER; i++) { this.mappedFile.write(' '); } this.mappedFile.close(); } /** * Creates a new FileAsList instance from an existing * backing file. * * @param mappedFile a File used to back the * list. This file must already exist. * @param mutable true if this list should support edits, false otherwise * * @exception IOException if an error occurs. */ public FileAsList(File mappedFile, boolean mutable) throws IOException { if(!mappedFile.exists()) { throw new IOException("Can't load mapped list as the file does not exist: " + mappedFile); } if(mutable) { this.mappedFile = new RandomAccessFile(mappedFile, "rw"); } else { this.mappedFile = new RandomAccessFile(mappedFile, "r"); } StringBuffer sbuff = new StringBuffer(); this.mappedFile.seek(0L); for(int i = 0; i < Math.min(LEADER, mappedFile.length()); i++) { char c = (char) this.mappedFile.readByte(); sbuff.append(c); } buffer = new byte[Integer.parseInt(sbuff.substring(0).trim())]; } /** * rawGet reads the record at the specified index as * a raw byte array. * * @param indx an int list index. * * @return a byte [] array containing the raw record * data. */ public byte[] rawGet(int indx) { if(indx < 0 || indx >= size()) { throw new IndexOutOfBoundsException("Can't access element: " + indx + " of " + size()); } if(indx != lastIndx) { long offset = fixOffset(indx * buffer.length); try { mappedFile.seek(offset); mappedFile.readFully(buffer); } catch (IOException ioe) { throw new AssertionFailure("Failed to seek for record", ioe); } } return buffer; } public Object get(int indx) { if(indx == lastIndx) { return lastRec; } byte[] buffer = rawGet(indx); lastRec = parseRecord(buffer); lastIndx = indx; return lastRec; } public int size() { if(sizeCache < 0) { try { sizeCache = (int) (unFixOffset(mappedFile.length()) / (long) buffer.length); } catch (IOException ioe) { throw new AssertionFailure("Can't read file length", ioe); } }; return sizeCache; } public boolean add(Object o) { sizeCache = -1; try { generateRecord(buffer, o); } catch (IOException e) { throw new AssertionFailure("Failed to write index", e); } try { mappedFile.seek(mappedFile.length()); mappedFile.write(buffer); } catch (IOException ioe) { throw new AssertionFailure("Failed to write index", ioe); } return true; } /** * This always returns null, not the previous object. */ public Object set(int indx, Object o) { try { generateRecord(buffer, o); } catch (IOException e) { throw new AssertionFailure("Failed to write index", e); } try { mappedFile.seek(fixOffset(indx * buffer.length)); mappedFile.write(buffer); } catch (IOException ioe) { throw new AssertionFailure("Failed to write index", ioe); } return null; } public void clear() { try { mappedFile.setLength(fixOffset(0)); } catch (IOException ioe) { throw new AssertionFailure("Could not truncate list", ioe); } commitedRecords = 0; } public void commit() { commitedRecords = this.size(); } public void rollback() { try { mappedFile.setLength(fixOffset((long) commitedRecords * (long) buffer.length)); } catch (Throwable t) { throw new AssertionFailure( "Could not roll back. " + "The index store will be in an inconsistent state " + "and should be discarded. File: " + mappedFile, t ); } } private long fixOffset(long offset) { return offset + (long) LEADER; } private long unFixOffset(long offset) { return offset - (long) LEADER; } protected abstract Object parseRecord(byte[] buffer); protected abstract void generateRecord(byte[] buffer, Object item) throws IOException; public abstract Comparator getComparator(); public Iterator iterator() { return new Iterator() { int i = 0; public Object next() { return get(i++); } public boolean hasNext() { return i < size(); } public void remove() {} }; } }