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