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