package it.cavallium.strangedb.database; import com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryo.io.Input; import com.esotericsoftware.kryo.io.Output; import it.cavallium.strangedb.annotations.*; 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.longs.LongList; import it.unimi.dsi.fastutil.shorts.ShortArrayList; import org.apache.commons.lang3.reflect.FieldUtils; import it.cavallium.strangedb.EnhancedObject; 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.util.*; import java.util.function.Supplier; public class DatabaseObjectsIO implements IObjectsIO { private final IDatabaseTools databaseTools; private final DatabaseReferencesIO referencesIO; private final Object accessLock = new Object(); private final DatabaseDataInitializer dataInitializer; private Kryo kryo = new Kryo(); public DatabaseObjectsIO(IDatabaseTools databaseTools, DatabaseReferencesIO referencesIO) { this.databaseTools = databaseTools; this.referencesIO = referencesIO; this.dataInitializer = new DatabaseDataInitializer(this); 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 serializedVersion = Byte.toUnsignedInt(buffer.get()); 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(); } long nativeFieldsDataReference = buffer.getLong(); return preloadEnhancedObject(objectType, serializedVersion, nativeFieldsDataReference, fieldRefs, methodRefs); } } @SuppressWarnings("unchecked") 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); default: throw new NullPointerException("Unknown data type"); } } void setData(long reference, DbDataType propertyType, T loadedPropertyValue) throws IOException { switch (propertyType) { case OBJECT: setObject(reference, loadedPropertyValue); break; case REFERENCES_LIST: setReferencesList(reference, (LongArrayList) loadedPropertyValue); break; case ENHANCED_OBJECT: setEnhancedObject(reference, (EnhancedObject) loadedPropertyValue); break; } } void setPrimitives(T enhancedObject, long reference, DbPrimitiveType[] types, Field[] fields) throws IOException { ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES * fields.length); try { for (int i = 0; i < fields.length; i++) { Field field = fields[i]; DbPrimitiveType type = types[i]; if (field == null) { buffer.putLong(0); } else { switch (type) { case BOOLEAN: buffer.putLong(field.getBoolean(enhancedObject) ? 1 : 0); break; case BYTE: buffer.putLong(field.getByte(enhancedObject)); break; case SHORT: buffer.putLong(field.getShort(enhancedObject)); break; case CHAR: buffer.putLong(field.getChar(enhancedObject)); break; case INTEGER: buffer.putLong(field.getInt(enhancedObject)); break; case LONG: buffer.putLong(field.getLong(enhancedObject)); break; case FLOAT: buffer.putLong(Float.floatToRawIntBits(field.getFloat(enhancedObject))); break; case DOUBLE: buffer.putLong(Double.doubleToRawLongBits(field.getDouble(enhancedObject))); break; } } } } catch (IllegalArgumentException | IllegalAccessException e) { throw new IOException(e); } buffer.flip(); referencesIO.writeToReference(reference, buffer.limit(), buffer); } @Override public void setEnhancedObject(long reference, T value) throws IOException { synchronized (accessLock) { if (value != null) { EnhancedObjectFullInfo objectFullInfo = value.getAllInfo(); if (objectFullInfo == null || objectFullInfo.getFieldReferences() == null || objectFullInfo.getPropertyReferences() == null) { throw new NullPointerException( "An EnhancedObject has been initialized using the empty constructor!"); } final long[] fieldReferences = objectFullInfo.getFieldReferences(); final DbDataType[] fieldTypes = objectFullInfo.getFieldTypes(); final Field[] fields = objectFullInfo.getFields(); final long nativeFieldDataReference = objectFullInfo.getPrimitiveFieldDataReference(); final DbPrimitiveType[] nativeFieldTypes = objectFullInfo.getPrimitiveFieldTypes(); final Field[] nativeFields = objectFullInfo.getPrimitiveFields(); final long[] propertyReferences = objectFullInfo.getPropertyReferences(); final DbDataType[] propertyTypes = objectFullInfo.getPropertyTypes(); final Object[] propertyValues = objectFullInfo.getLoadedPropertyValues(); final int totalSize = Byte.BYTES + Integer.BYTES * 2 + fieldReferences.length * Long.BYTES + propertyReferences.length * Long.BYTES + Long.BYTES; ByteBuffer buffer = ByteBuffer.allocate(totalSize); buffer.put((byte) objectFullInfo.getVersion()); buffer.putInt(fieldReferences.length); buffer.putInt(propertyReferences.length); for (int i = 0; i < fieldReferences.length; i++) { buffer.putLong(fieldReferences[i]); } for (int i = 0; i < propertyReferences.length; i++) { buffer.putLong(propertyReferences[i]); } buffer.putLong(nativeFieldDataReference); buffer.flip(); for (int i = 0; i < fieldReferences.length; i++) { if (fields[i] != null) { try { setData(fieldReferences[i], fieldTypes[i], fields[i].get(value)); } catch (IllegalAccessException e) { throw new IOException(e); } } } for (int i = 0; i < propertyReferences.length; i++) { if (propertyValues[i] != null) { setData(propertyReferences[i], propertyTypes[i], propertyValues[i]); } } setPrimitives(value, nativeFieldDataReference, nativeFieldTypes, nativeFields); 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 LongList loadPrimitiveData(long reference) throws IOException { synchronized (accessLock) { ByteBuffer buffer = referencesIO.readFromReference(reference); if (buffer.limit() == 0) { return null; } int size = buffer.limit() / Long.BYTES; LongArrayList result = new LongArrayList(size); for (int i = 0; i < size; i++) { result.add(buffer.getLong()); } return result; } } @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 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); } 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; } 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 preloadEnhancedObjectPrimitiveFields(T obj, long nativeFieldsDataReference) throws IOException { // Declare the variables needed to get the biggest field Id Field[] unorderedFields = getPrimitiveFields(obj); // Find the biggest field Id int biggestFieldId = getBiggestPrimitiveFieldId(unorderedFields); // Declare the other variables Field[] fields = new Field[biggestFieldId + 1]; DbPrimitiveType[] orderedFieldTypes = new DbPrimitiveType[biggestFieldId + 1]; // Load all fields metadata for (Field field : unorderedFields) { DbPrimitiveField fieldAnnotation = field.getAnnotation(DbPrimitiveField.class); int fieldId = fieldAnnotation.id(); DbPrimitiveType fieldType = fieldAnnotation.type(); fields[fieldId] = field; orderedFieldTypes[fieldId] = fieldType; } // Load fields data ByteBuffer buffer = referencesIO.readFromReference(nativeFieldsDataReference); // Load fields try { for (int id = 0; id < fields.length; id++) { Field field = fields[id]; DbPrimitiveType type = orderedFieldTypes[id]; switch (type) { case BOOLEAN: FieldUtils.writeField(field, obj, (boolean) (buffer.getLong() % 2 == 1), true); break; case BYTE: FieldUtils.writeField(field, obj, (byte) buffer.getLong(), true); break; case SHORT: FieldUtils.writeField(field, obj, (short) buffer.getLong(), true); break; case CHAR: FieldUtils.writeField(field, obj, (char) buffer.getLong(), true); break; case INTEGER: FieldUtils.writeField(field, obj, (int) buffer.getLong(), true); break; case LONG: FieldUtils.writeField(field, obj, (long) buffer.getLong(), true); break; case FLOAT: FieldUtils.writeField(field, obj, Float.intBitsToFloat((int) buffer.getLong()), true); break; case DOUBLE: FieldUtils.writeField(field, obj, Double.longBitsToDouble(buffer.getLong()), true); break; } } } catch (IllegalArgumentException | IllegalAccessException e) { throw new IOException(e); } // Set fields metadata obj.setPrimitiveFields(fields, orderedFieldTypes, nativeFieldsDataReference); } 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); } 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); } } Field[] getFields(T obj) { return FieldUtils.getFieldsWithAnnotation(obj.getClass(), DbField.class); } Field[] getPrimitiveFields(T obj) { return FieldUtils.getFieldsWithAnnotation(obj.getClass(), DbPrimitiveField.class); } 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; } int getBiggestPrimitiveFieldId(Field[] unorderedFields) { int biggestFieldId = -1; for (Field field : unorderedFields) { DbPrimitiveField fieldAnnotation = field.getAnnotation(DbPrimitiveField.class); int propertyId = fieldAnnotation.id(); if (propertyId > biggestFieldId) { biggestFieldId = propertyId; } } return biggestFieldId; } private T toInstance(Class type) throws IOException { try { T obj = type.getConstructor().newInstance(); obj.setDatabaseTools(databaseTools); 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, int serializedVersion, long nativeFieldsRef, long[] fieldRefs, long[] methodRefs) throws IOException { // Instantiate the class to an object T obj = toInstance(objectType); // Check the serialized version DbClass dbClass = objectType.getAnnotation(DbClass.class); int classVersion = 0; if (dbClass != null) { classVersion = dbClass.version(); } if (classVersion > serializedVersion) { DatabaseEnhancedObjectUpgrader enhancedObjectUpgrader = new DatabaseEnhancedObjectUpgrader(this, fieldRefs, methodRefs, nativeFieldsRef); dataInitializer.initializeDbObject(obj); obj.onUpgrade(serializedVersion, enhancedObjectUpgrader); } else if (classVersion < serializedVersion) { throw new IllegalStateException( "The serialized class is more recent than the current version of that class!"); } else { preloadEnhancedObjectPrimitiveFields(obj, nativeFieldsRef); 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; } @Override public IDataInitializer getDataInitializer() { return dataInitializer; } }