package org.warp.cowdb; import com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryo.io.Input; import com.esotericsoftware.kryo.io.Output; import it.unimi.dsi.fastutil.booleans.BooleanArrayList; import it.unimi.dsi.fastutil.bytes.ByteArrayList; import it.unimi.dsi.fastutil.chars.CharArrayList; import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.shorts.ShortArrayList; import org.apache.commons.lang3.reflect.FieldUtils; import org.warp.jcwdb.ann.*; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.*; import java.util.function.Supplier; import static org.warp.cowdb.IBlocksMetadata.EMPTY_BLOCK_ID; public class Database implements IDatabase { private final DatabaseFileIO fileIO; private final DatabaseBlocksIO blocksIO; private final DatabaseBlocksMetadata blocksMetadata; private final DatabaseReferencesIO referencesIO; private final DatabaseReferencesMetadata referencesMetadata; private final DatabaseObjectsIO objectsIO; private final DatabaseDataInitializer dataInitializer; private EnhancedObject loadedRootObject; public Database(Path dataFile, Path blocksMetaFile, Path referencesMetaFile) throws IOException { if (Files.notExists(dataFile)) { Files.createFile(dataFile); } if (Files.notExists(blocksMetaFile)) { Files.createFile(blocksMetaFile); } if (Files.notExists(referencesMetaFile)) { Files.createFile(referencesMetaFile); } this.fileIO = new DatabaseFileIO(dataFile); this.blocksMetadata = new DatabaseBlocksMetadata(blocksMetaFile); this.blocksIO = new DatabaseBlocksIO(fileIO, blocksMetadata); this.referencesMetadata = new DatabaseReferencesMetadata(referencesMetaFile); this.referencesIO = new DatabaseReferencesIO(blocksIO, referencesMetadata); this.objectsIO = new DatabaseObjectsIO(this, referencesIO); this.dataInitializer = new DatabaseDataInitializer(objectsIO); } @Override public IDataInitializer getDataInitializer() { return dataInitializer; } @Override public IObjectsIO getObjectsIO() { return objectsIO; } @Override public void close() throws IOException { this.objectsIO.setEnhancedObject(0, loadedRootObject); this.referencesMetadata.close(); this.blocksMetadata.close(); this.fileIO.close(); } public T loadRoot(Class type) throws IOException { return loadRoot(type, () -> { T obj = objectsIO.instantiateEnhancedObject(type); dataInitializer.initializeDBObject(obj); return obj; }); } public T loadRoot(Class type, SupplierWithIO ifAbsent) throws IOException { if (loadedRootObject != null) { throw new RuntimeException("Root already set!"); } T root; if (referencesMetadata.firstFreeReference > 0) { root = objectsIO.loadEnhancedObject(0, type); } else { if (objectsIO.newNullObject() != 0) { throw new IOException("Can't allocate root!"); } else { root = ifAbsent.getWithIO(); objectsIO.setEnhancedObject(0, root); } } loadedRootObject = root; return root; } protected void registerClass(Class type, int id) { this.objectsIO.registerClass(type, id); } public static class DatabaseDataInitializer implements IDataInitializer { private final DatabaseObjectsIO objectsIO; public DatabaseDataInitializer(DatabaseObjectsIO objectsIO) { this.objectsIO = objectsIO; } @Override public void initializeDBObject(EnhancedObject obj) throws IOException { initializeDBObjectFields(obj); initializeDBObjectProperties(obj); obj.initialize(); } private void initializeDBObjectFields(EnhancedObject obj) throws IOException { // Declare the variables needed to get the biggest field Id Field[] unorderedFields = objectsIO.getFields(obj); // Find the biggest field Id int biggestFieldId = objectsIO.getBiggestFieldId(unorderedFields); // Allocate new UIDs long[] fieldUIDs = objectsIO.allocateNewUIDs(biggestFieldId + 1); // Declare the other variables Field[] fields = new Field[biggestFieldId + 1]; DBDataType[] orderedFieldTypes = new DBDataType[biggestFieldId + 1]; // Load all fields metadata and load them for (Field field : unorderedFields) { DBField fieldAnnotation = field.getAnnotation(DBField.class); int fieldId = fieldAnnotation.id(); DBDataType fieldType = fieldAnnotation.type(); objectsIO.loadField(obj, field, fieldType, fieldUIDs[fieldId]); fields[fieldId] = field; orderedFieldTypes[fieldId] = fieldType; } // Set fields metadata obj.setFields(fields, orderedFieldTypes, fieldUIDs); } private void initializeDBObjectProperties(EnhancedObject obj) throws IOException { // Declare the variables needed to get the biggest property Id Method[] unorderedPropertyGetters = obj.getPropertyGetters(); Method[] unorderedPropertySetters = obj.getPropertySetters(); // Find the biggest property Id int biggestGetter = objectsIO.getBiggestPropertyGetterId(unorderedPropertyGetters); int biggestSetter = objectsIO.getBiggestPropertySetterId(unorderedPropertySetters); int biggestPropertyId = biggestGetter > biggestSetter ? biggestGetter : biggestSetter; // Allocate new UIDs long[] propertyUIDs = objectsIO.allocateNewUIDs(biggestPropertyId + 1); for (Method property : unorderedPropertySetters) { DBPropertySetter fieldAnnotation = property.getAnnotation(DBPropertySetter.class); int propertyId = fieldAnnotation.id(); if (propertyId > biggestPropertyId) { biggestPropertyId = propertyId; } } // Declare the other variables DBDataType[] propertyTypes = new DBDataType[biggestPropertyId + 1]; Method[] propertyGetters = new Method[biggestPropertyId + 1]; Method[] propertySetters = new Method[biggestPropertyId + 1]; Map setterMethods = new LinkedHashMap<>(); Map getterMethods = new LinkedHashMap<>(); // Load the properties metadata for (Method property : unorderedPropertyGetters) { DBPropertyGetter propertyAnnotation = property.getAnnotation(DBPropertyGetter.class); int propertyId = propertyAnnotation.id(); DBDataType propertyType = propertyAnnotation.type(); propertyTypes[propertyId] = propertyType; propertyGetters[propertyId] = property; getterMethods.put(property.getName(), propertyAnnotation); } for (Method property : unorderedPropertySetters) { DBPropertySetter propertyAnnotation = property.getAnnotation(DBPropertySetter.class); int propertyId = propertyAnnotation.id(); DBDataType propertyType = propertyAnnotation.type(); propertyTypes[propertyId] = propertyType; propertySetters[propertyId] = property; setterMethods.put(property.getName(), propertyAnnotation); } // Set properties metadata obj.setProperties(propertyGetters, propertySetters, propertyTypes, propertyUIDs, setterMethods, getterMethods); } } public static class DatabaseObjectsIO implements IObjectsIO { private final IDatabase database; private final DatabaseReferencesIO referencesIO; private final Object accessLock = new Object(); private Kryo kryo = new Kryo(); private DatabaseObjectsIO(IDatabase database, DatabaseReferencesIO referencesIO) { this.database = database; this.referencesIO = referencesIO; kryo.setRegistrationRequired(false); int id = -90; registerClass(boolean[].class, id++); registerClass(byte[].class, id++); registerClass(short[].class, id++); registerClass(char[].class, id++); registerClass(int[].class, id++); registerClass(long[].class, id++); registerClass(Boolean[].class, id++); registerClass(Byte[].class, id++); registerClass(Short[].class, id++); registerClass(Character[].class, id++); registerClass(Integer[].class, id++); registerClass(Long[].class, id++); registerClass(String.class, id++); registerClass(String[].class, id++); registerClass(Boolean.class, id++); registerClass(Byte.class, id++); registerClass(Short.class, id++); registerClass(Character.class, id++); registerClass(Integer.class, id++); registerClass(Class.class, id++); registerClass(Object.class, id++); registerClass(Object[].class, id++); registerClass(Long.class, id++); registerClass(String.class, id++); registerClass(String[].class, id++); registerClass(boolean[][].class, id++); registerClass(byte[][].class, id++); registerClass(short[][].class, id++); registerClass(char[][].class, id++); registerClass(int[][].class, id++); registerClass(long[][].class, id++); registerClass(String[][].class, id++); registerClass(List.class, id++); registerClass(ArrayList.class, id++); registerClass(LinkedList.class, id++); registerClass(Set.class, id++); registerClass(HashSet.class, id++); registerClass(LinkedHashSet.class, id++); registerClass(Map.class, id++); registerClass(HashMap.class, id++); registerClass(LinkedHashMap.class, id++); registerClass(TreeMap.class, id++); registerClass(BooleanArrayList.class, id++); registerClass(ByteArrayList.class, id++); registerClass(ShortArrayList.class, id++); registerClass(CharArrayList.class, id++); registerClass(IntArrayList.class, id++); registerClass(LongArrayList.class, id++); registerClass(TreeSet.class, id++); registerClass(SortedSet.class, id++); registerClass(SortedMap.class, id++); } @Override public T loadEnhancedObject(long reference, Class objectType) throws IOException { synchronized (accessLock) { ByteBuffer buffer = referencesIO.readFromReference(reference); if (buffer.limit() == 0) { return null; } int fieldsCount = buffer.getInt(); int methodsCount = buffer.getInt(); long[] fieldRefs = new long[fieldsCount]; long[] methodRefs = new long[methodsCount]; for (int i = 0; i < fieldsCount; i++) { fieldRefs[i] = buffer.getLong(); } for (int i = 0; i < methodsCount; i++) { methodRefs[i] = buffer.getLong(); } return preloadEnhancedObject(objectType, fieldRefs, methodRefs); } } @SuppressWarnings("unchecked") private Object loadData(DBDataType propertyType, long dataReference, Supplier> returnType) throws IOException { switch (propertyType) { case ENHANCED_OBJECT: return loadEnhancedObject(dataReference, (Class) returnType.get()); case OBJECT: return loadObject(dataReference); case REFERENCES_LIST: return loadReferencesList(dataReference); case BOOLEAN: return loadBoolean(dataReference); case BYTE: return loadByte(dataReference); case SHORT: return loadShort(dataReference); case CHAR: return loadChar(dataReference); case INTEGER: return loadInt(dataReference); case LONG: return loadLong(dataReference); default: throw new NullPointerException("Unknown data type"); } } private void setData(long reference, DBDataType propertyType, T loadedPropertyValue) throws IOException { switch (propertyType) { case BOOLEAN: setBoolean(reference, loadedPropertyValue != null && (boolean) loadedPropertyValue); break; case BYTE: setByte(reference, loadedPropertyValue == null ? 0 : (byte) loadedPropertyValue); break; case SHORT: setShort(reference, loadedPropertyValue == null ? 0 : (short) loadedPropertyValue); break; case CHAR: setChar(reference, loadedPropertyValue == null ? 0 : (char) loadedPropertyValue); break; case INTEGER: setInt(reference, loadedPropertyValue == null ? 0 : (int) loadedPropertyValue); break; case LONG: setLong(reference, loadedPropertyValue == null ? 0 : (long) loadedPropertyValue); break; case OBJECT: setObject(reference, loadedPropertyValue); break; case REFERENCES_LIST: setReferencesList(reference, (LongArrayList) loadedPropertyValue); break; case ENHANCED_OBJECT: setEnhancedObject(reference, (EnhancedObject) loadedPropertyValue); break; } } @Override public void setEnhancedObject(long reference, T value) throws IOException { synchronized (accessLock) { if (value != null) { EnhancedObjectFullInfo objectFullInfo = value.getAllInfo(); int totalSize = Integer.BYTES * 2 + objectFullInfo.getFieldReferences().length * Long.BYTES + objectFullInfo.getPropertyReferences().length * Long.BYTES; ByteBuffer buffer = ByteBuffer.allocate(totalSize); buffer.putInt(objectFullInfo.getFieldReferences().length); buffer.putInt(objectFullInfo.getPropertyReferences().length); for (int i = 0; i < objectFullInfo.getFieldReferences().length; i++) { buffer.putLong(objectFullInfo.getFieldReferences()[i]); } for (int i = 0; i < objectFullInfo.getPropertyReferences().length; i++) { buffer.putLong(objectFullInfo.getPropertyReferences()[i]); } for (int i = 0; i < objectFullInfo.getFieldReferences().length; i++) { try { setData(objectFullInfo.getFieldReferences()[i], objectFullInfo.getFieldTypes()[i], objectFullInfo.getFields()[i].get(value)); } catch (IllegalAccessException e) { throw new IOException(e); } } for (int i = 0; i < objectFullInfo.getPropertyReferences().length; i++) { setData(objectFullInfo.getPropertyReferences()[i], objectFullInfo.getPropertyTypes()[i], objectFullInfo.getLoadedPropertyValues()[i]); } buffer.flip(); referencesIO.writeToReference(reference, totalSize, buffer); } else { referencesIO.writeToReference(reference, 0, null); } } } @SuppressWarnings("unchecked") @Override public T loadObject(long reference) throws IOException { synchronized (accessLock) { ByteBuffer buffer = referencesIO.readFromReference(reference); if (buffer.limit() == 0) { return null; } buffer.rewind(); byte[] data = buffer.array(); return (T) kryo.readClassAndObject(new Input(data)); } } @Override public LongArrayList loadReferencesList(long reference) throws IOException { synchronized (accessLock) { ByteBuffer buffer = referencesIO.readFromReference(reference); if (buffer.limit() == 0) { return null; } int itemsCount = buffer.getInt(); LongArrayList arrayList = new LongArrayList(); for (int i = 0; i < itemsCount; i++) { arrayList.add(buffer.getLong()); } return arrayList; } } @Override public boolean loadBoolean(long reference) throws IOException { synchronized (accessLock) { ByteBuffer buffer = referencesIO.readFromReference(reference); if (buffer.limit() == 0) { return false; } return buffer.get() == 1; } } @Override public byte loadByte(long reference) throws IOException { synchronized (accessLock) { ByteBuffer buffer = referencesIO.readFromReference(reference); if (buffer.limit() == 0) { return 0; } return buffer.get(); } } @Override public short loadShort(long reference) throws IOException { synchronized (accessLock) { ByteBuffer buffer = referencesIO.readFromReference(reference); if (buffer.limit() == 0) { return 0; } return buffer.getShort(); } } @Override public char loadChar(long reference) throws IOException { synchronized (accessLock) { ByteBuffer buffer = referencesIO.readFromReference(reference); if (buffer.limit() == 0) { return 0; } return buffer.getChar(); } } @Override public int loadInt(long reference) throws IOException { synchronized (accessLock) { ByteBuffer buffer = referencesIO.readFromReference(reference); if (buffer.limit() == 0) { return 0; } return buffer.getInt(); } } @Override public long loadLong(long reference) throws IOException { synchronized (accessLock) { ByteBuffer buffer = referencesIO.readFromReference(reference); if (buffer.limit() == 0) { return 0; } return buffer.getLong(); } } @Override public void setObject(long reference, T value) throws IOException { synchronized (accessLock) { if (value != null) { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); Output output = new Output(outputStream); kryo.writeClassAndObject(output, value); output.flush(); byte[] data = outputStream.toByteArray(); ByteBuffer dataByteBuffer = ByteBuffer.wrap(data); referencesIO.writeToReference(reference, data.length, dataByteBuffer); } else { referencesIO.writeToReference(reference, 0, null); } } } @Override public void setReferencesList(long reference, LongArrayList value) throws IOException { synchronized (accessLock) { if (value != null) { int items = value.size(); ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES * items + Integer.BYTES); buffer.putInt(items); for (int i = 0; i < items; i++) { buffer.putLong(value.getLong(i)); } buffer.flip(); referencesIO.writeToReference(reference, buffer.limit(), buffer); } else { referencesIO.writeToReference(reference, 0, null); } } } @Override public void setBoolean(long reference, boolean value) throws IOException { synchronized (accessLock) { ByteBuffer buffer = ByteBuffer.allocate(Byte.BYTES); buffer.put(value ? (byte) 1 : (byte) 0); buffer.flip(); referencesIO.writeToReference(reference, Byte.BYTES, buffer); } } @Override public void setByte(long reference, byte value) throws IOException { synchronized (accessLock) { ByteBuffer buffer = ByteBuffer.allocate(Byte.BYTES); buffer.put(value); buffer.flip(); referencesIO.writeToReference(reference, Byte.BYTES, buffer); } } @Override public void setShort(long reference, short value) throws IOException { synchronized (accessLock) { ByteBuffer buffer = ByteBuffer.allocate(Short.BYTES); buffer.putShort(value); buffer.flip(); referencesIO.writeToReference(reference, Short.BYTES, buffer); } } @Override public void setChar(long reference, char value) throws IOException { synchronized (accessLock) { ByteBuffer buffer = ByteBuffer.allocate(Character.BYTES); buffer.putChar(value); buffer.flip(); referencesIO.writeToReference(reference, Character.BYTES, buffer); } } @Override public void setInt(long reference, int value) throws IOException { synchronized (accessLock) { ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES); buffer.putInt(value); buffer.flip(); referencesIO.writeToReference(reference, Integer.BYTES, buffer); } } @Override public void setLong(long reference, long value) throws IOException { synchronized (accessLock) { ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES); buffer.putLong(value); buffer.flip(); referencesIO.writeToReference(reference, Long.BYTES, buffer); } } @Override public long newNullObject() throws IOException { synchronized (accessLock) { return referencesIO.allocateReference(); } } @Override public void loadProperty(EnhancedObject obj, int propertyId, Method property, DBDataType propertyType, long propertyUID) throws IOException { synchronized (accessLock) { obj.setProperty(propertyId, loadData(propertyType, propertyUID, property::getReturnType)); } } @Override public void registerClass(Class type, int id) { if (id < -100) { throw new IllegalArgumentException(); } kryo.register(type, 100 + id); } private void preloadEnhancedObjectProperties(T obj, long[] propertyReferences) { // Declare the variables needed to get the biggest property Id Method[] unorderedPropertyGetters = obj.getPropertyGetters(); Method[] unorderedPropertySetters = obj.getPropertySetters(); // Find the biggest property Id int biggestGetter = getBiggestPropertyGetterId(unorderedPropertyGetters); int biggestSetter = getBiggestPropertySetterId(unorderedPropertySetters); int biggestPropertyId = biggestGetter > biggestSetter ? biggestGetter : biggestSetter; for (Method property : unorderedPropertySetters) { DBPropertySetter fieldAnnotation = property.getAnnotation(DBPropertySetter.class); int propertyId = fieldAnnotation.id(); if (propertyId > biggestPropertyId) { biggestPropertyId = propertyId; } } // Declare the other variables DBDataType[] propertyTypes = new DBDataType[biggestPropertyId + 1]; Method[] propertyGetters = new Method[biggestPropertyId + 1]; Method[] propertySetters = new Method[biggestPropertyId + 1]; Map setterMethods = new LinkedHashMap<>(); Map getterMethods = new LinkedHashMap<>(); // Load the properties metadata for (Method property : unorderedPropertyGetters) { DBPropertyGetter propertyAnnotation = property.getAnnotation(DBPropertyGetter.class); int propertyId = propertyAnnotation.id(); DBDataType propertyType = propertyAnnotation.type(); propertyTypes[propertyId] = propertyType; propertyGetters[propertyId] = property; getterMethods.put(property.getName(), propertyAnnotation); } for (Method property : unorderedPropertySetters) { DBPropertySetter propertyAnnotation = property.getAnnotation(DBPropertySetter.class); int propertyId = propertyAnnotation.id(); DBDataType propertyType = propertyAnnotation.type(); propertyTypes[propertyId] = propertyType; propertySetters[propertyId] = property; setterMethods.put(property.getName(), propertyAnnotation); } // Set properties metadata obj.setProperties(propertyGetters, propertySetters, propertyTypes, propertyReferences, setterMethods, getterMethods); } private int getBiggestPropertyGetterId(Method[] unorderedPropertyGetters) { int biggestPropertyId = -1; for (Method property : unorderedPropertyGetters) { DBPropertyGetter fieldAnnotation = property.getAnnotation(DBPropertyGetter.class); int propertyId = fieldAnnotation.id(); if (propertyId > biggestPropertyId) { biggestPropertyId = propertyId; } } return biggestPropertyId; } private int getBiggestPropertySetterId(Method[] unorderedPropertySetters) { int biggestPropertyId = -1; for (Method property : unorderedPropertySetters) { DBPropertySetter fieldAnnotation = property.getAnnotation(DBPropertySetter.class); int propertyId = fieldAnnotation.id(); if (propertyId > biggestPropertyId) { biggestPropertyId = propertyId; } } return biggestPropertyId; } private void preloadEnhancedObjectFields(T obj, long[] fieldReferences) throws IOException { // Declare the variables needed to get the biggest field Id Field[] unorderedFields = getFields(obj); // Find the biggest field Id int biggestFieldId = getBiggestFieldId(unorderedFields); // Declare the other variables Field[] fields = new Field[biggestFieldId + 1]; DBDataType[] orderedFieldTypes = new DBDataType[biggestFieldId + 1]; // Load all fields metadata and load them for (Field field : unorderedFields) { DBField fieldAnnotation = field.getAnnotation(DBField.class); int fieldId = fieldAnnotation.id(); DBDataType fieldType = fieldAnnotation.type(); loadField(obj, field, fieldType, fieldReferences[fieldId]); fields[fieldId] = field; orderedFieldTypes[fieldId] = fieldType; } // Set fields metadata obj.setFields(fields, orderedFieldTypes, fieldReferences); } private void loadField(T obj, Field field, DBDataType fieldType, long fieldReference) throws IOException { Object data = loadData(fieldType, fieldReference, field::getType); try { if (fieldType == DBDataType.OBJECT && data != null) { if (!field.getType().isInstance(data)) { throw new IOException("There is an attempt to load an object of type " + data.getClass() + " into a field of type " + field.getType()); } } FieldUtils.writeField(field, obj, data, true); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } private Field[] getFields(T obj) { return FieldUtils.getFieldsWithAnnotation(obj.getClass(), DBField.class); } private int getBiggestFieldId(Field[] unorderedFields) { int biggestFieldId = -1; for (Field field : unorderedFields) { DBField fieldAnnotation = field.getAnnotation(DBField.class); int propertyId = fieldAnnotation.id(); if (propertyId > biggestFieldId) { biggestFieldId = propertyId; } } return biggestFieldId; } private T instantiateEnhancedObject(Class type) throws IOException { try { T obj = type.getConstructor().newInstance(); obj.database = database; return obj; } catch (NoSuchMethodException e) { throw new IOException("You must declare a public empty constructor in class " + type + ": public " + type.getSimpleName() + "()", e); } catch (IllegalAccessException | InvocationTargetException | InstantiationException e) { throw new IOException(e); } } private T preloadEnhancedObject(Class objectType, long[] fieldRefs, long[] methodRefs) throws IOException { T obj = instantiateEnhancedObject(objectType); preloadEnhancedObjectFields(obj, fieldRefs); preloadEnhancedObjectProperties(obj, methodRefs); return obj; } public long[] allocateNewUIDs(int quantity) throws IOException { long[] ids = new long[quantity]; for (int i = 0; i < quantity; i++) { ids[i] = newNullObject(); } return ids; } } public static class DatabaseReferencesIO implements IReferencesIO { private final DatabaseBlocksIO blocksIO; private final DatabaseReferencesMetadata referencesMetadata; public DatabaseReferencesIO(DatabaseBlocksIO blocksIO, DatabaseReferencesMetadata referencesMetadata) { this.blocksIO = blocksIO; this.referencesMetadata = referencesMetadata; } @Override public long allocateReference() throws IOException { return referencesMetadata.newReference(EMPTY_BLOCK_ID); } @Override public long allocateReference(int size, ByteBuffer data) throws IOException { long blockId = (size == 0) ? EMPTY_BLOCK_ID : blocksIO.newBlock(size, data); return referencesMetadata.newReference(blockId); } @Override public void writeToReference(long reference, int size, ByteBuffer data) throws IOException { long blockId = (size == 0) ? EMPTY_BLOCK_ID : blocksIO.newBlock(size, data); referencesMetadata.editReference(reference, blockId); } @Override public ByteBuffer readFromReference(long reference) throws IOException { long blockId = referencesMetadata.getReference(reference); return blocksIO.readBlock(blockId); } } public static class DatabaseReferencesMetadata implements IReferencesMetadata { private final SeekableByteChannel metaFileChannel; private final int REF_META_BYTES_COUNT = Long.BYTES; private long firstFreeReference; private DatabaseReferencesMetadata(Path refMetaFile) throws IOException { metaFileChannel = Files.newByteChannel(refMetaFile, StandardOpenOption.READ, StandardOpenOption.WRITE); firstFreeReference = metaFileChannel.size() / REF_META_BYTES_COUNT; } @Override public long getReference(long reference) throws IOException { ByteBuffer buffer = ByteBuffer.allocate(REF_META_BYTES_COUNT); if (reference >= firstFreeReference) { return EMPTY_BLOCK_ID; } SeekableByteChannel currentFileChannel = metaFileChannel.position(reference * REF_META_BYTES_COUNT); currentFileChannel.read(buffer); buffer.flip(); long block = buffer.getLong(); if (buffer.limit() == 0 || block == 0xFFFFFFFFFFFFFFFFL) { return EMPTY_BLOCK_ID; } return block; } @Override public long newReference(long blockId) throws IOException { long newReference = firstFreeReference++; editReference(newReference, blockId); return newReference; } @Override public void editReference(long reference, long blockId) throws IOException { ByteBuffer data = ByteBuffer.allocate(REF_META_BYTES_COUNT); data.putLong(blockId); SeekableByteChannel currentFileChannel = metaFileChannel.position(reference * REF_META_BYTES_COUNT); data.flip(); currentFileChannel.write(data); } @Override public void close() throws IOException { metaFileChannel.close(); } } public static class DatabaseBlocksIO implements IBlocksIO { private final DatabaseFileIO fileIO; private final IBlocksMetadata blocksMetadata; private DatabaseBlocksIO(DatabaseFileIO fileIO, IBlocksMetadata blocksMetadata) { this.fileIO = fileIO; this.blocksMetadata = blocksMetadata; } @Override public long newBlock(int size, ByteBuffer data) throws IOException { long index = fileIO.writeAtEnd(size, data); return blocksMetadata.newBlock(index, size); } @Override public ByteBuffer readBlock(long blockId) throws IOException { if (blockId == EMPTY_BLOCK_ID) { return ByteBuffer.wrap(new byte[0]); } BlockInfo blockInfo = blocksMetadata.getBlockInfo(blockId); return fileIO.readAt(blockInfo.getIndex(), blockInfo.getSize()); } @Override public void close() { } } public static class DatabaseBlocksMetadata implements IBlocksMetadata { private final SeekableByteChannel metaFileChannel; private final int BLOCK_META_BYTES_COUNT = Long.BYTES + Integer.BYTES; private long firstFreeBlock; private DatabaseBlocksMetadata(Path metaFile) throws IOException { metaFileChannel = Files.newByteChannel(metaFile, StandardOpenOption.READ, StandardOpenOption.WRITE); firstFreeBlock = metaFileChannel.size() / BLOCK_META_BYTES_COUNT; } @Override public BlockInfo getBlockInfo(long blockId) throws IOException { if (blockId == EMPTY_BLOCK_ID) { return EMPTY_BLOCK_INFO; } ByteBuffer buffer = ByteBuffer.allocate(BLOCK_META_BYTES_COUNT); metaFileChannel.position(blockId * BLOCK_META_BYTES_COUNT).read(buffer); buffer.flip(); long index = buffer.getLong(); int size = buffer.getInt(); return new BlockInfo(index, size); } @Override public long newBlock(long index, int size) throws IOException { long newBlockId = firstFreeBlock++; ByteBuffer data = ByteBuffer.allocate(BLOCK_META_BYTES_COUNT); data.putLong(index); data.putInt(size); data.flip(); metaFileChannel.position(newBlockId * BLOCK_META_BYTES_COUNT).write(data); return newBlockId; } @Override public void close() throws IOException { metaFileChannel.close(); } } public static class DatabaseFileIO implements IFileIO { private final SeekableByteChannel dataFileChannel; private final Object dataAccessLock = new Object(); private long firstFreeIndex; private DatabaseFileIO(Path dataFile) throws IOException { synchronized (dataAccessLock) { dataFileChannel = Files.newByteChannel(dataFile, StandardOpenOption.READ, StandardOpenOption.WRITE); firstFreeIndex = dataFileChannel.size(); } } @Override public ByteBuffer readAt(long index, int length) throws IOException { ByteBuffer dataBuffer = ByteBuffer.allocate(length); dataFileChannel.position(index).read(dataBuffer); dataBuffer.flip(); return dataBuffer; } @Override public void writeAt(long index, int length, ByteBuffer data) throws IOException { synchronized (dataAccessLock) { if (data.position() != 0) { throw new IOException("You didn't flip the ByteBuffer!"); } if (firstFreeIndex < index + length) { firstFreeIndex = index + length; } dataFileChannel.position(index).write(data); } } @Override public long writeAtEnd(int length, ByteBuffer data) throws IOException { synchronized (dataAccessLock) { long index = firstFreeIndex; firstFreeIndex += length; writeAt(index, length, data); return index; } } @Override public void close() throws IOException { synchronized (dataAccessLock) { dataFileChannel.close(); } } } }