+/*
+ * Sixth - System for data storage, computation, exploration and interaction.
+ * Copyright ©2012-2016, Svjatoslav Agejenko, svjatoslav@svjatoslav.eu
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of version 3 of the GNU Lesser General Public License
+ * or later as published by the Free Software Foundation.
+ */
+
+package eu.svjatoslav.sixth.data.store.file;
+
+import eu.svjatoslav.sixth.data.store.DataStore;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.List;
+
+/**
+ * DataStore backed by single filesystem file.
+ */
+public class FileDataStore implements DataStore {
+
+ final MetaData metaData = new MetaData(this);
+ final EntryAllocationTable entryAllocationTable = new EntryAllocationTable(this);
+ RandomAccessFile randomAccessFile;
+
+ public FileDataStore(final File backingFile) throws IOException {
+ if (backingFile.exists())
+ initializeFromExistingFile(backingFile);
+ else
+ initializeNewFile(backingFile);
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ metaData.writeFileHeader();
+ randomAccessFile.close();
+ }
+
+ @Override
+ public synchronized int createRecord(final byte[] value) throws IOException {
+
+ if (metaData.entriesTableNeedsIncreasing())
+ increaseEntriesTable();
+
+ final int newEntryId = entryAllocationTable.getNewUnusedEntryId();
+
+ final long currentLocation = metaData
+ .allocateStorageSpace(value.length);
+
+ final EntryRecord record = new EntryRecord(newEntryId, currentLocation,
+ value.length);
+
+ metaData.increaseUsedEntriesCount();
+
+ record.save(this);
+
+ randomAccessFile.seek(currentLocation);
+ randomAccessFile.write(value);
+
+ // update header to increase crash resilience
+ metaData.writeFileHeader();
+ return newEntryId;
+ }
+
+ @Override
+ public synchronized void deleteRecord(final int id) throws IOException {
+ final EntryRecord entryRecord = new EntryRecord(this, id);
+
+ if (!entryRecord.isUsed())
+ throw new RuntimeException("Record already does not exist!");
+
+ entryRecord.clear();
+ entryRecord.save(this);
+ metaData.decreaseUsedEntriesCount();
+
+ // update header to increase crash resilience
+ metaData.writeFileHeader();
+ }
+
+ public long getDefragmentationStartAddress(final int allowedFragmentationPercent,
+ final List<EntryRecord> allEntryRecords) {
+
+ final VacuumContext context = new VacuumContext(this,
+ metaData.allocateStorageSpace(0), allowedFragmentationPercent);
+
+ for (int i = allEntryRecords.size() - 1; i >= 0; i--) {
+ final EntryRecord entryRecord = allEntryRecords.get(i);
+ context.analyzeEntry(entryRecord);
+ }
+ context.analyzeStartOfDataArea();
+
+ return context.defragmentationStartAddress;
+ }
+
+ public void increaseEntriesTable() throws IOException {
+ final List<EntryRecord> allEntryRecords = entryAllocationTable
+ .loadAllEntryRecords();
+
+ final int newEntriesTableSize = metaData.getEntriesTableSize() * 2;
+
+ final long dataEvacuationTreshold = metaData
+ .getEntriesStorageAreaStart(newEntriesTableSize);
+
+ metaData.ensureMinimumCurrentLocation(dataEvacuationTreshold);
+
+ for (final EntryRecord record : allEntryRecords)
+ if (record.location < dataEvacuationTreshold) {
+
+ final long newEntryLocation = metaData
+ .allocateStorageSpace(record.length);
+
+ // read record content
+ final byte[] entryContent = new byte[record.length];
+
+ randomAccessFile.seek(record.location);
+ randomAccessFile.readFully(entryContent);
+
+ // write record content to new location
+ randomAccessFile.seek(newEntryLocation);
+ randomAccessFile.write(entryContent);
+
+ // update record header
+ record.location = newEntryLocation;
+ record.save(this);
+ }
+
+ entryAllocationTable.enlarge(newEntriesTableSize);
+ System.out.println("Entries table increased.");
+
+ // update header to increase crash resilience
+ metaData.writeFileHeader();
+ }
+
+ public void initializeFromExistingFile(final File backingFile)
+ throws IOException {
+ try {
+ randomAccessFile = new RandomAccessFile(backingFile, "rw");
+
+ metaData.readFileHeader();
+
+ } catch (final FileNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void initializeNewFile(final File backingFile) throws IOException {
+ try {
+ randomAccessFile = new RandomAccessFile(backingFile, "rw");
+ } catch (final FileNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+
+ metaData.initializeNewFile();
+
+ entryAllocationTable.initializeNewFile();
+ }
+
+ public int readInt(final long position) throws IOException {
+ randomAccessFile.seek(position);
+ return randomAccessFile.readInt();
+ }
+
+ public long readLong(final long position) throws IOException {
+ randomAccessFile.seek(position);
+ return randomAccessFile.readLong();
+ }
+
+ @Override
+ public synchronized byte[] readRecord(final int id) throws IOException {
+
+ final EntryRecord entryRecord = new EntryRecord(this, id);
+
+ if (!entryRecord.isUsed())
+ throw new RuntimeException("Entity record by id: " + id
+ + " does not exist.");
+
+ final byte[] result = new byte[entryRecord.length];
+
+ randomAccessFile.seek(entryRecord.location);
+ randomAccessFile.readFully(result);
+
+ return result;
+ }
+
+ public long relocateEntry(final EntryRecord record, final long newLocation)
+ throws IOException {
+
+ if (record.location != newLocation) {
+ System.out.println("Relocating record " + record.id + " from: "
+ + record.location + " to: " + newLocation);
+
+ final byte[] result = new byte[record.length];
+
+ randomAccessFile.seek(record.location);
+ randomAccessFile.readFully(result);
+
+ randomAccessFile.seek(newLocation);
+ randomAccessFile.write(result);
+
+ record.location = newLocation;
+ record.save(this);
+ }
+
+ return newLocation + record.length;
+ }
+
+ @Override
+ public synchronized void updateRecord(final int id, final byte[] value)
+ throws IOException {
+
+ final EntryRecord entryRecord = new EntryRecord(this, id);
+
+ if (!entryRecord.isUsed())
+ throw new RuntimeException("Entry record is empty!");
+
+ final int newDataSize = value.length;
+
+ if (entryRecord.length >= newDataSize) {
+ // update record in place
+ if (entryRecord.length != newDataSize) {
+ entryRecord.length = newDataSize;
+ entryRecord.save(this);
+ }
+ } else {
+ // save record to the new location
+ entryRecord.location = metaData.allocateStorageSpace(newDataSize);
+ entryRecord.length = newDataSize;
+ entryRecord.save(this);
+ }
+
+ randomAccessFile.seek(entryRecord.location);
+ randomAccessFile.write(value);
+
+ // update header to increase crash resilience
+ metaData.writeFileHeader();
+ }
+
+ /**
+ * Defragment empty space.
+ *
+ * @param allowedFragmentationPercent allowed maximum percentage of free space, relative to total
+ * used space.
+ * @throws IOException
+ */
+ public void vacuum(final int allowedFragmentationPercent) throws IOException {
+
+ final List<EntryRecord> allEntryRecords = entryAllocationTable
+ .loadAllEntryRecords();
+
+ final long defragmentationStart = getDefragmentationStartAddress(
+ allowedFragmentationPercent, allEntryRecords);
+
+ long nextFreeSpace = defragmentationStart;
+
+ for (final EntryRecord record : allEntryRecords) {
+ if (record.location < defragmentationStart)
+ continue;
+
+ nextFreeSpace = relocateEntry(record, nextFreeSpace);
+
+ }
+
+ metaData.setCurrentLocation(nextFreeSpace);
+
+ // update header to increase crash resilience
+ metaData.writeFileHeader();
+ }
+
+ public void writeInt(final long position, final int value)
+ throws IOException {
+ randomAccessFile.seek(position);
+ randomAccessFile.writeInt(value);
+ }
+
+ public void writeLong(final long position, final long value)
+ throws IOException {
+ randomAccessFile.seek(position);
+ randomAccessFile.writeLong(value);
+ }
+}