001/* 002 * BioJava development code 003 * 004 * This code may be freely distributed and modified under the 005 * terms of the GNU Lesser General Public Licence. This should 006 * be distributed with the code. If you do not have a copy, 007 * see: 008 * 009 * http://www.gnu.org/copyleft/lesser.html 010 * 011 * Copyright for this code is held jointly by the individual 012 * authors. These should be listed in @author doc comments. 013 * 014 * For more information on the BioJava project and its aims, 015 * or to join the biojava-l mailing list, visit the home page 016 * at: 017 * 018 * http://www.biojava.org/ 019 * 020 */ 021 022package org.biojava.utils; 023 024import java.io.File; 025import java.io.IOException; 026import java.io.RandomAccessFile; 027import java.util.AbstractList; 028import java.util.Comparator; 029import java.util.Iterator; 030 031/** 032 * <code>FileAsList</code> creates a writable <code>List</code> 033 * implementation backed by a random access file. There is a 034 * restriction on the record length that the string representation of 035 * that integer may not be longer than 4 bytes. This is because a 036 * fixed 4 byte leader is used to encode the record length in the 037 * file. 038 * 039 * @author Matthew Pocock 040 * @author Keith James 041 * @author Greg Cox 042 */ 043 044// fixme: throughout this class, we are raising assertions for things that 045// are legitimiate exceptions. This needs re-factoring. 046public abstract class FileAsList 047 extends 048 AbstractList 049 implements 050 Commitable 051{ 052 private static final int LEADER = 4; 053 054 private RandomAccessFile mappedFile; 055 private int commitedRecords; 056 private int lastIndx = -1; 057 private Object lastRec; 058 private byte[] buffer; 059 private int sizeCache = -1; 060 061 /** 062 * Creates a new <code>FileAsList</code> and corresponding backing 063 * file. 064 * 065 * @param mappedFile a <code>File</code> used to back the 066 * list. This file must not already exist. 067 * @param recordLength an <code>int</code> byte record length. 068 * 069 * @exception IOException if an error occurs. 070 */ 071 public FileAsList(File mappedFile, int recordLength) 072 throws IOException { 073 if(mappedFile.exists()) { 074 throw new IOException("Can't create file as it already exists: " + mappedFile); 075 } 076 mappedFile.createNewFile(); 077 this.mappedFile = new RandomAccessFile(mappedFile, "rw"); 078 buffer = new byte[recordLength]; 079 this.mappedFile.seek(0L); 080 byte[] rl = String.valueOf(recordLength).getBytes(); 081 if(rl.length > LEADER) { 082 throw new IOException("Length of record too long"); // FIXME: ugg 083 } 084 for(int i = 0; i < rl.length; i++) { 085 this.mappedFile.write(rl[i]); 086 } 087 for(int i = rl.length; i < LEADER; i++) { 088 this.mappedFile.write(' '); 089 } 090 091 this.mappedFile.close(); 092 } 093 094 /** 095 * Creates a new <code>FileAsList</code> instance from an existing 096 * backing file. 097 * 098 * @param mappedFile a <code>File</code> used to back the 099 * list. This file must already exist. 100 * @param mutable true if this list should support edits, false otherwise 101 * 102 * @exception IOException if an error occurs. 103 */ 104 public FileAsList(File mappedFile, boolean mutable) 105 throws IOException { 106 if(!mappedFile.exists()) { 107 throw new IOException("Can't load mapped list as the file does not exist: " + mappedFile); 108 } 109 110 if(mutable) { 111 this.mappedFile = new RandomAccessFile(mappedFile, "rw"); 112 } else { 113 this.mappedFile = new RandomAccessFile(mappedFile, "r"); 114 } 115 StringBuffer sbuff = new StringBuffer(); 116 this.mappedFile.seek(0L); 117 for(int i = 0; i < Math.min(LEADER, mappedFile.length()); i++) { 118 char c = (char) this.mappedFile.readByte(); 119 sbuff.append(c); 120 } 121 122 buffer = new byte[Integer.parseInt(sbuff.substring(0).trim())]; 123 } 124 125 /** 126 * <code>rawGet</code> reads the record at the specified index as 127 * a raw byte array. 128 * 129 * @param indx an <code>int</code> list index. 130 * 131 * @return a <code>byte []</code> array containing the raw record 132 * data. 133 */ 134 public byte[] rawGet(int indx) { 135 if(indx < 0 || indx >= size()) { 136 throw new IndexOutOfBoundsException("Can't access element: " + indx + " of " + size()); 137 } 138 139 if(indx != lastIndx) { 140 long offset = fixOffset(indx * buffer.length); 141 try { 142 mappedFile.seek(offset); 143 mappedFile.readFully(buffer); 144 } catch (IOException ioe) { 145 throw new AssertionFailure("Failed to seek for record", ioe); 146 } 147 } 148 149 return buffer; 150 } 151 152 public Object get(int indx) { 153 if(indx == lastIndx) { 154 return lastRec; 155 } 156 157 byte[] buffer = rawGet(indx); 158 159 lastRec = parseRecord(buffer); 160 lastIndx = indx; 161 162 return lastRec; 163 } 164 165 public int size() { 166 if(sizeCache < 0) { 167 try { 168 sizeCache = (int) (unFixOffset(mappedFile.length()) / (long) buffer.length); 169 } catch (IOException ioe) { 170 throw new AssertionFailure("Can't read file length", ioe); 171 } 172 }; 173 174 return sizeCache; 175 } 176 177 public boolean add(Object o) { 178 sizeCache = -1; 179 180 try { 181 generateRecord(buffer, o); 182 } catch (IOException e) { 183 throw new AssertionFailure("Failed to write index", e); 184 } 185 186 try { 187 mappedFile.seek(mappedFile.length()); 188 mappedFile.write(buffer); 189 } catch (IOException ioe) { 190 throw new AssertionFailure("Failed to write index", ioe); 191 } 192 193 return true; 194 } 195 196 /** 197 * This always returns null, not the previous object. 198 */ 199 public Object set(int indx, Object o) { 200 try { 201 generateRecord(buffer, o); 202 } catch (IOException e) { 203 throw new AssertionFailure("Failed to write index", e); 204 } 205 206 try { 207 mappedFile.seek(fixOffset(indx * buffer.length)); 208 mappedFile.write(buffer); 209 } catch (IOException ioe) { 210 throw new AssertionFailure("Failed to write index", ioe); 211 } 212 213 return null; 214 } 215 216 public void clear() { 217 try { 218 mappedFile.setLength(fixOffset(0)); 219 } catch (IOException ioe) { 220 throw new AssertionFailure("Could not truncate list", ioe); 221 } 222 commitedRecords = 0; 223 } 224 225 public void commit() { 226 commitedRecords = this.size(); 227 } 228 229 public void rollback() { 230 try { 231 mappedFile.setLength(fixOffset((long) commitedRecords * (long) buffer.length)); 232 } catch (Throwable t) { 233 throw new AssertionFailure( 234 "Could not roll back. " 235 + "The index store will be in an inconsistent state " 236 + "and should be discarded. File: " 237 + mappedFile, 238 t ); 239 } 240 } 241 242 private long fixOffset(long offset) { 243 return offset + (long) LEADER; 244 } 245 246 private long unFixOffset(long offset) { 247 return offset - (long) LEADER; 248 } 249 250 protected abstract Object parseRecord(byte[] buffer); 251 252 protected abstract void generateRecord(byte[] buffer, Object item) 253 throws IOException; 254 255 public abstract Comparator getComparator(); 256 257 public Iterator iterator() { 258 return new Iterator() { 259 int i = 0; 260 261 public Object next() { 262 return get(i++); 263 } 264 265 public boolean hasNext() { 266 return i < size(); 267 } 268 269 public void remove() {} 270 }; 271 } 272}