fixed maven repository URL
[sixth-data.git] / src / main / java / eu / svjatoslav / sixth / data / store / file / FileDataStore.java
1 /*
2  * Sixth Data. Author: Svjatoslav Agejenko. 
3  * This project is released under Creative Commons Zero (CC0) license.
4  *
5 */
6
7 package eu.svjatoslav.sixth.data.store.file;
8
9 import eu.svjatoslav.sixth.data.store.DataStore;
10
11 import java.io.File;
12 import java.io.FileNotFoundException;
13 import java.io.IOException;
14 import java.io.RandomAccessFile;
15 import java.util.List;
16
17 /**
18  * DataStore backed by single filesystem file.
19  */
20 public class FileDataStore implements DataStore {
21
22     final MetaData metaData = new MetaData(this);
23     final EntryAllocationTable entryAllocationTable = new EntryAllocationTable(this);
24     RandomAccessFile randomAccessFile;
25
26     public FileDataStore(final File backingFile) throws IOException {
27         if (backingFile.exists())
28             initializeFromExistingFile(backingFile);
29         else
30             initializeNewFile(backingFile);
31     }
32
33     @Override
34     public synchronized void close() throws IOException {
35         metaData.writeFileHeader();
36         randomAccessFile.close();
37     }
38
39     @Override
40     public synchronized int createRecord(final byte[] value) throws IOException {
41
42         if (metaData.entriesTableNeedsIncreasing())
43             increaseEntriesTable();
44
45         final int newEntryId = entryAllocationTable.getNewUnusedEntryId();
46
47         final long currentLocation = metaData
48                 .allocateStorageSpace(value.length);
49
50         final EntryRecord record = new EntryRecord(newEntryId, currentLocation,
51                 value.length);
52
53         metaData.increaseUsedEntriesCount();
54
55         record.save(this);
56
57         randomAccessFile.seek(currentLocation);
58         randomAccessFile.write(value);
59
60         // update header to increase crash resilience
61         metaData.writeFileHeader();
62         return newEntryId;
63     }
64
65     @Override
66     public synchronized void deleteRecord(final int id) throws IOException {
67         final EntryRecord entryRecord = new EntryRecord(this, id);
68
69         if (!entryRecord.isUsed())
70             throw new RuntimeException("Record already does not exist!");
71
72         entryRecord.clear();
73         entryRecord.save(this);
74         metaData.decreaseUsedEntriesCount();
75
76         // update header to increase crash resilience
77         metaData.writeFileHeader();
78     }
79
80     public long getDefragmentationStartAddress(final int allowedFragmentationPercent,
81                                                final List<EntryRecord> allEntryRecords) {
82
83         final VacuumContext context = new VacuumContext(this,
84                 metaData.allocateStorageSpace(0), allowedFragmentationPercent);
85
86         for (int i = allEntryRecords.size() - 1; i >= 0; i--) {
87             final EntryRecord entryRecord = allEntryRecords.get(i);
88             context.analyzeEntry(entryRecord);
89         }
90         context.analyzeStartOfDataArea();
91
92         return context.defragmentationStartAddress;
93     }
94
95     public void increaseEntriesTable() throws IOException {
96         final List<EntryRecord> allEntryRecords = entryAllocationTable
97                 .loadAllEntryRecords();
98
99         final int newEntriesTableSize = metaData.getEntriesTableSize() * 2;
100
101         final long dataEvacuationTreshold = metaData
102                 .getEntriesStorageAreaStart(newEntriesTableSize);
103
104         metaData.ensureMinimumCurrentLocation(dataEvacuationTreshold);
105
106         for (final EntryRecord record : allEntryRecords)
107             if (record.location < dataEvacuationTreshold) {
108
109                 final long newEntryLocation = metaData
110                         .allocateStorageSpace(record.length);
111
112                 // read record content
113                 final byte[] entryContent = new byte[record.length];
114
115                 randomAccessFile.seek(record.location);
116                 randomAccessFile.readFully(entryContent);
117
118                 // write record content to new location
119                 randomAccessFile.seek(newEntryLocation);
120                 randomAccessFile.write(entryContent);
121
122                 // update record header
123                 record.location = newEntryLocation;
124                 record.save(this);
125             }
126
127         entryAllocationTable.enlarge(newEntriesTableSize);
128         System.out.println("Entries table increased.");
129
130         // update header to increase crash resilience
131         metaData.writeFileHeader();
132     }
133
134     public void initializeFromExistingFile(final File backingFile)
135             throws IOException {
136         try {
137             randomAccessFile = new RandomAccessFile(backingFile, "rw");
138
139             metaData.readFileHeader();
140
141         } catch (final FileNotFoundException e) {
142             throw new RuntimeException(e);
143         }
144     }
145
146     public void initializeNewFile(final File backingFile) throws IOException {
147         try {
148             randomAccessFile = new RandomAccessFile(backingFile, "rw");
149         } catch (final FileNotFoundException e) {
150             throw new RuntimeException(e);
151         }
152
153         metaData.initializeNewFile();
154
155         entryAllocationTable.initializeNewFile();
156     }
157
158     public int readInt(final long position) throws IOException {
159         randomAccessFile.seek(position);
160         return randomAccessFile.readInt();
161     }
162
163     public long readLong(final long position) throws IOException {
164         randomAccessFile.seek(position);
165         return randomAccessFile.readLong();
166     }
167
168     @Override
169     public synchronized byte[] readRecord(final int id) throws IOException {
170
171         final EntryRecord entryRecord = new EntryRecord(this, id);
172
173         if (!entryRecord.isUsed())
174             throw new RuntimeException("Entity record by id: " + id
175                     + " does not exist.");
176
177         final byte[] result = new byte[entryRecord.length];
178
179         randomAccessFile.seek(entryRecord.location);
180         randomAccessFile.readFully(result);
181
182         return result;
183     }
184
185     public long relocateEntry(final EntryRecord record, final long newLocation)
186             throws IOException {
187
188         if (record.location != newLocation) {
189             System.out.println("Relocating record " + record.id + " from: "
190                     + record.location + " to: " + newLocation);
191
192             final byte[] result = new byte[record.length];
193
194             randomAccessFile.seek(record.location);
195             randomAccessFile.readFully(result);
196
197             randomAccessFile.seek(newLocation);
198             randomAccessFile.write(result);
199
200             record.location = newLocation;
201             record.save(this);
202         }
203
204         return newLocation + record.length;
205     }
206
207     @Override
208     public synchronized void updateRecord(final int id, final byte[] value)
209             throws IOException {
210
211         final EntryRecord entryRecord = new EntryRecord(this, id);
212
213         if (!entryRecord.isUsed())
214             throw new RuntimeException("Entry record is empty!");
215
216         final int newDataSize = value.length;
217
218         if (entryRecord.length >= newDataSize) {
219             // update record in place
220             if (entryRecord.length != newDataSize) {
221                 entryRecord.length = newDataSize;
222                 entryRecord.save(this);
223             }
224         } else {
225             // save record to the new location
226             entryRecord.location = metaData.allocateStorageSpace(newDataSize);
227             entryRecord.length = newDataSize;
228             entryRecord.save(this);
229         }
230
231         randomAccessFile.seek(entryRecord.location);
232         randomAccessFile.write(value);
233
234         // update header to increase crash resilience
235         metaData.writeFileHeader();
236     }
237
238     /**
239      * Defragment empty space.
240      *
241      * @param allowedFragmentationPercent allowed maximum percentage of free space, relative to total
242      *                                    used space.
243      * @throws IOException
244      */
245     public void vacuum(final int allowedFragmentationPercent) throws IOException {
246
247         final List<EntryRecord> allEntryRecords = entryAllocationTable
248                 .loadAllEntryRecords();
249
250         final long defragmentationStart = getDefragmentationStartAddress(
251                 allowedFragmentationPercent, allEntryRecords);
252
253         long nextFreeSpace = defragmentationStart;
254
255         for (final EntryRecord record : allEntryRecords) {
256             if (record.location < defragmentationStart)
257                 continue;
258
259             nextFreeSpace = relocateEntry(record, nextFreeSpace);
260
261         }
262
263         metaData.setCurrentLocation(nextFreeSpace);
264
265         // update header to increase crash resilience
266         metaData.writeFileHeader();
267     }
268
269     public void writeInt(final long position, final int value)
270             throws IOException {
271         randomAccessFile.seek(position);
272         randomAccessFile.writeInt(value);
273     }
274
275     public void writeLong(final long position, final long value)
276             throws IOException {
277         randomAccessFile.seek(position);
278         randomAccessFile.writeLong(value);
279     }
280 }