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}