From 8857c90dc23bfcbfccacb89254c3eea914f59dde Mon Sep 17 00:00:00 2001 From: Eduardo Ramirez Date: Fri, 2 Feb 2024 16:28:29 -0800 Subject: [PATCH] feat: add support for going from HollowGenericObject/HollowRecod/ReadStateEngine to POJO (#662) * feat: add support for going from HollowGenericObject/HollowRecod/ReadStateEngine to POJO * handle nulls --- .../objectmapper/HollowListTypeMapper.java | 12 + .../objectmapper/HollowMapTypeMapper.java | 14 + .../objectmapper/HollowObjectMapper.java | 9 + .../objectmapper/HollowObjectTypeMapper.java | 239 +++++- .../objectmapper/HollowSetTypeMapper.java | 12 + .../write/objectmapper/HollowTypeMapper.java | 3 + ...lowObjectMapperHollowRecordParserTest.java | 787 ++++++++++++++++++ 7 files changed, 1075 insertions(+), 1 deletion(-) create mode 100644 hollow/src/test/java/com/netflix/hollow/core/write/objectmapper/HollowObjectMapperHollowRecordParserTest.java diff --git a/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowListTypeMapper.java b/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowListTypeMapper.java index da8083dd5b..3924894832 100644 --- a/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowListTypeMapper.java +++ b/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowListTypeMapper.java @@ -16,6 +16,8 @@ */ package com.netflix.hollow.core.write.objectmapper; +import com.netflix.hollow.api.objects.HollowRecord; +import com.netflix.hollow.api.objects.generic.GenericHollowList; import com.netflix.hollow.core.schema.HollowListSchema; import com.netflix.hollow.core.schema.HollowSchema; import com.netflix.hollow.core.util.IntList; @@ -113,6 +115,16 @@ private HollowListWriteRecord copyToWriteRecord(List l, FlatRecordWriter flat return rec; } + @Override + protected Object parseHollowRecord(HollowRecord record) { + GenericHollowList hollowList = (GenericHollowList) record; + List list = new ArrayList<>(); + for (HollowRecord element : hollowList) { + list.add(elementMapper.parseHollowRecord(element)); + } + return list; + } + @Override protected Object parseFlatRecord(HollowSchema recordSchema, FlatRecordReader reader, Map parsedObjects) { List collection = new ArrayList<>(); diff --git a/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowMapTypeMapper.java b/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowMapTypeMapper.java index 440a65bb4d..2e7e3c1203 100644 --- a/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowMapTypeMapper.java +++ b/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowMapTypeMapper.java @@ -16,6 +16,8 @@ */ package com.netflix.hollow.core.write.objectmapper; +import com.netflix.hollow.api.objects.HollowRecord; +import com.netflix.hollow.api.objects.generic.GenericHollowMap; import com.netflix.hollow.core.schema.HollowMapSchema; import com.netflix.hollow.core.schema.HollowSchema; import com.netflix.hollow.core.util.HollowObjectHashCodeFinder; @@ -124,6 +126,18 @@ private HollowMapWriteRecord copyToWriteRecord(Map m, FlatRecordWriter fla return rec; } + @Override + protected Object parseHollowRecord(HollowRecord record) { + GenericHollowMap hollowMap = (GenericHollowMap) record; + Map m = new HashMap<>(); + for (Map.Entry entry : hollowMap.entries()) { + Object key = keyMapper.parseHollowRecord(entry.getKey()); + Object value = valueMapper.parseHollowRecord(entry.getValue()); + m.put(key, value); + } + return m; + } + @Override protected Object parseFlatRecord(HollowSchema recordSchema, FlatRecordReader reader, Map parsedObjects) { Map collection = new HashMap<>(); diff --git a/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowObjectMapper.java b/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowObjectMapper.java index aeed0b8b98..7eed923dcc 100644 --- a/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowObjectMapper.java +++ b/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowObjectMapper.java @@ -16,6 +16,7 @@ */ package com.netflix.hollow.core.write.objectmapper; +import com.netflix.hollow.api.objects.HollowRecord; import com.netflix.hollow.core.schema.HollowSchema; import com.netflix.hollow.core.write.HollowWriteStateEngine; import com.netflix.hollow.core.write.objectmapper.flatrecords.FlatRecord; @@ -76,6 +77,14 @@ public int add(Object o) { HollowTypeMapper typeMapper = getTypeMapper(o.getClass(), null, null); return typeMapper.write(o); } + + public T readHollowRecord(HollowRecord record) { + HollowTypeMapper typeMapper = typeMappers.get(record.getSchema().getName()); + if (typeMapper == null) { + throw new IllegalArgumentException("No type mapper found for schema " + record.getSchema().getName()); + } + return (T) typeMapper.parseHollowRecord(record); + } public void writeFlat(Object o, FlatRecordWriter flatRecordWriter) { HollowTypeMapper typeMapper = getTypeMapper(o.getClass(), null, null); diff --git a/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowObjectTypeMapper.java b/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowObjectTypeMapper.java index 16475247b6..8cef2fad52 100644 --- a/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowObjectTypeMapper.java +++ b/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowObjectTypeMapper.java @@ -16,6 +16,8 @@ */ package com.netflix.hollow.core.write.objectmapper; +import com.netflix.hollow.api.objects.HollowRecord; +import com.netflix.hollow.api.objects.generic.GenericHollowObject; import com.netflix.hollow.core.index.key.PrimaryKey; import com.netflix.hollow.core.memory.HollowUnsafeHandle; import com.netflix.hollow.core.schema.HollowObjectSchema; @@ -194,6 +196,49 @@ private HollowObjectWriteRecord copyToWriteRecord(Object obj, FlatRecordWriter f return rec; } + @Override + protected Object parseHollowRecord(HollowRecord record) { + try { + GenericHollowObject hollowObject = (GenericHollowObject) record; + + HollowObjectSchema objectSchema = (HollowObjectSchema) record.getSchema(); + Object obj = null; + if (BOXED_WRAPPERS.contains(clazz)) { + // if `clazz` is a BoxedWrapper then by definition its OBJECT schema will have a single primitive + // field so find it in the HollowObject and ignore all other fields. + for (int i = 0; i < objectSchema.numFields(); i++) { + int posInPojoSchema = schema.getPosition(objectSchema.getFieldName(i)); + if (posInPojoSchema != -1) { + obj = mappedFields.get(posInPojoSchema).parseBoxedWrapper(hollowObject); + } + } + } else if (clazz.isEnum()) { + // if `clazz` is an enum, then we should expect to find a field called `_name` in the FlatRecord. + // There may be other fields if the producer enum contained custom properties, we ignore them + // here assuming the enum constructor will set them if needed. + for (int i = 0; i < objectSchema.numFields(); i++) { + String fieldName = objectSchema.getFieldName(i); + int posInPojoSchema = schema.getPosition(fieldName); + if (fieldName.equals(MappedFieldType.ENUM_NAME.getSpecialFieldName()) && posInPojoSchema != -1) { + obj = mappedFields.get(posInPojoSchema).parseBoxedWrapper(hollowObject); + } + } + } else { + obj = unsafe.allocateInstance(clazz); + for (int i = 0; i < objectSchema.numFields(); i++) { + int posInPojoSchema = schema.getPosition(objectSchema.getFieldName(i)); + if (posInPojoSchema != -1) { + mappedFields.get(posInPojoSchema).copy(hollowObject, obj); + } + } + } + + return obj; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + @Override protected Object parseFlatRecord(HollowSchema recordSchema, FlatRecordReader reader, Map parsedObjects) { try { @@ -527,7 +572,199 @@ public void copy(Object obj, HollowObjectWriteRecord rec, FlatRecordWriter flatR break; } } - + + public void copy(GenericHollowObject rec, Object pojo) { + switch(fieldType) { + case BOOLEAN: + unsafe.putBoolean(pojo, fieldOffset, rec.getBoolean(fieldName)); + break; + case INT: + int intValue = rec.getInt(fieldName); + if (intValue != Integer.MIN_VALUE) { + unsafe.putInt(pojo, fieldOffset, intValue); + } + break; + case SHORT: + int shortValue = rec.getInt(fieldName); + if (shortValue != Integer.MIN_VALUE) { + unsafe.putShort(pojo, fieldOffset, (short) shortValue); + } + break; + case BYTE: + int byteValue = rec.getInt(fieldName); + if (byteValue != Integer.MIN_VALUE) { + unsafe.putByte(pojo, fieldOffset, (byte) byteValue); + } + break; + case CHAR: + int charValue = rec.getInt(fieldName); + if (charValue != Integer.MIN_VALUE) { + unsafe.putChar(pojo, fieldOffset, (char) charValue); + } + break; + case LONG: + long longValue = rec.getLong(fieldName); + if (longValue != Long.MIN_VALUE) { + unsafe.putLong(pojo, fieldOffset, longValue); + } + break; + case DOUBLE: + double doubleValue = rec.getDouble(fieldName); + if (!Double.isNaN(doubleValue)) { + unsafe.putDouble(pojo, fieldOffset, doubleValue); + } + break; + case FLOAT: + float floatValue = rec.getFloat(fieldName); + if (!Float.isNaN(floatValue)) { + unsafe.putFloat(pojo, fieldOffset, floatValue); + } + break; + case STRING: + unsafe.putObject(pojo, fieldOffset, rec.getString(fieldName)); + break; + case BYTES: + unsafe.putObject(pojo, fieldOffset, rec.getBytes(fieldName)); + break; + case INLINED_BOOLEAN: + unsafe.putObject(pojo, fieldOffset, Boolean.valueOf(rec.getBoolean(fieldName))); + break; + case INLINED_INT: + int inlinedIntValue = rec.getInt(fieldName); + if (inlinedIntValue != Integer.MIN_VALUE) { + unsafe.putObject(pojo, fieldOffset, Integer.valueOf(inlinedIntValue)); + } + break; + case INLINED_SHORT: + int inlinedShortValue = rec.getInt(fieldName); + if (inlinedShortValue != Integer.MIN_VALUE) { + unsafe.putObject(pojo, fieldOffset, Short.valueOf((short) inlinedShortValue)); + } + break; + case INLINED_BYTE: + int inlinedByteValue = rec.getInt(fieldName); + if (inlinedByteValue != Integer.MIN_VALUE) { + unsafe.putObject(pojo, fieldOffset, Byte.valueOf((byte) inlinedByteValue)); + } + break; + case INLINED_CHAR: + int inlinedCharValue = rec.getInt(fieldName); + if (inlinedCharValue != Integer.MIN_VALUE) { + unsafe.putObject(pojo, fieldOffset, Character.valueOf((char) inlinedCharValue)); + } + break; + case INLINED_LONG: + long inlinedLongValue = rec.getLong(fieldName); + if (inlinedLongValue != Long.MIN_VALUE) { + unsafe.putObject(pojo, fieldOffset, Long.valueOf(inlinedLongValue)); + } + break; + case INLINED_DOUBLE: + double inlinedDoubleValue = rec.getDouble(fieldName); + if (!Double.isNaN(inlinedDoubleValue)) { + unsafe.putObject(pojo, fieldOffset, Double.valueOf(inlinedDoubleValue)); + } + break; + case INLINED_FLOAT: + float inlinedFloatValue = rec.getFloat(fieldName); + if (!Float.isNaN(inlinedFloatValue)) { + unsafe.putObject(pojo, fieldOffset, Float.valueOf(inlinedFloatValue)); + } + break; + case INLINED_STRING: + unsafe.putObject(pojo, fieldOffset, rec.getString(fieldName)); + break; + case DATE_TIME: + long dateValue = rec.getLong(fieldName); + if (dateValue != Long.MIN_VALUE) { + unsafe.putObject(pojo, fieldOffset, new Date(dateValue)); + } + break; + case ENUM_NAME: + String enumNameValue = rec.getString(fieldName); + if (enumNameValue != null) { + unsafe.putObject(pojo, fieldOffset, Enum.valueOf((Class) type, enumNameValue)); + } + break; + case REFERENCE: + HollowRecord fieldRecord = rec.getReferencedGenericRecord(fieldName); + if(fieldRecord != null) { + unsafe.putObject(pojo, fieldOffset, subTypeMapper.parseHollowRecord(fieldRecord)); + } + break; + default: + throw new IllegalArgumentException("Unexpected field type " + fieldType + " for field " + fieldName); + } + } + + private Object parseBoxedWrapper(GenericHollowObject record) { + switch (fieldType) { + case BOOLEAN: + return Boolean.valueOf(record.getBoolean(fieldName)); + case INT: + int intValue = record.getInt(fieldName); + if (intValue == Integer.MIN_VALUE) { + return null; + } + return Integer.valueOf(intValue); + case SHORT: + int shortValue = record.getInt(fieldName); + if (shortValue == Integer.MIN_VALUE) { + return null; + } + return Short.valueOf((short) shortValue); + case BYTE: + int byteValue = record.getInt(fieldName); + if (byteValue == Integer.MIN_VALUE) { + return null; + } + return Byte.valueOf((byte) byteValue); + case CHAR: + int charValue = record.getInt(fieldName); + if (charValue == Integer.MIN_VALUE) { + return null; + } + return Character.valueOf((char) charValue); + case LONG: + long longValue = record.getLong(fieldName); + if (longValue == Long.MIN_VALUE) { + return null; + } + return Long.valueOf(longValue); + case FLOAT: + float floatValue = record.getFloat(fieldName); + if (Float.isNaN(floatValue)) { + return null; + } + return Float.valueOf(floatValue); + case DOUBLE: + double doubleValue = record.getDouble(fieldName); + if (Double.isNaN(doubleValue)) { + return null; + } + return Double.valueOf(doubleValue); + case STRING: + return record.getString(fieldName); + case BYTES: + return record.getBytes(fieldName); + case ENUM_NAME: + String enumName = record.getString(fieldName); + if (enumName == null) { + return null; + } + return Enum.valueOf((Class) clazz, enumName); + case DATE_TIME: { + long dateValue = record.getLong(fieldName); + if (dateValue == Long.MIN_VALUE) { + return null; + } + return new Date(dateValue); + } + default: + throw new IllegalArgumentException("Unexpected field type " + fieldType + " for field " + fieldName); + } + } + private Object parseBoxedWrapper(FlatRecordReader reader) { switch (fieldType) { case BOOLEAN: { diff --git a/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowSetTypeMapper.java b/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowSetTypeMapper.java index c68ca6dc09..a6ad9694e3 100644 --- a/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowSetTypeMapper.java +++ b/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowSetTypeMapper.java @@ -16,6 +16,8 @@ */ package com.netflix.hollow.core.write.objectmapper; +import com.netflix.hollow.api.objects.HollowRecord; +import com.netflix.hollow.api.objects.generic.GenericHollowSet; import com.netflix.hollow.core.schema.HollowSchema; import com.netflix.hollow.core.schema.HollowSetSchema; import com.netflix.hollow.core.util.HollowObjectHashCodeFinder; @@ -104,6 +106,16 @@ private HollowSetWriteRecord copyToWriteRecord(Set s, FlatRecordWriter flatRe return rec; } + @Override + protected Object parseHollowRecord(HollowRecord record) { + GenericHollowSet hollowSet = (GenericHollowSet) record; + Set s = new HashSet<>(); + for (HollowRecord element : hollowSet) { + s.add(elementMapper.parseHollowRecord(element)); + } + return s; + } + @Override protected Object parseFlatRecord(HollowSchema recordSchema, FlatRecordReader reader, Map parsedObjects) { Set collection = new HashSet<>(); diff --git a/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowTypeMapper.java b/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowTypeMapper.java index afb81b5142..c6d8078995 100644 --- a/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowTypeMapper.java +++ b/hollow/src/main/java/com/netflix/hollow/core/write/objectmapper/HollowTypeMapper.java @@ -16,6 +16,7 @@ */ package com.netflix.hollow.core.write.objectmapper; +import com.netflix.hollow.api.objects.HollowRecord; import com.netflix.hollow.core.memory.ByteDataArray; import com.netflix.hollow.core.schema.HollowSchema; import com.netflix.hollow.core.write.HollowTypeWriteState; @@ -42,6 +43,8 @@ public abstract class HollowTypeMapper { protected abstract int write(Object obj); protected abstract int writeFlat(Object obj, FlatRecordWriter flatRecordWriter); + + protected abstract Object parseHollowRecord(HollowRecord record); protected abstract Object parseFlatRecord(HollowSchema schema, FlatRecordReader reader, Map parsedObjects); diff --git a/hollow/src/test/java/com/netflix/hollow/core/write/objectmapper/HollowObjectMapperHollowRecordParserTest.java b/hollow/src/test/java/com/netflix/hollow/core/write/objectmapper/HollowObjectMapperHollowRecordParserTest.java new file mode 100644 index 0000000000..2fb0a46f39 --- /dev/null +++ b/hollow/src/test/java/com/netflix/hollow/core/write/objectmapper/HollowObjectMapperHollowRecordParserTest.java @@ -0,0 +1,787 @@ +package com.netflix.hollow.core.write.objectmapper; + +import com.netflix.hollow.api.objects.generic.GenericHollowObject; +import com.netflix.hollow.core.read.engine.HollowReadStateEngine; +import com.netflix.hollow.core.util.StateEngineRoundTripper; +import com.netflix.hollow.core.write.HollowWriteStateEngine; +import com.netflix.hollow.test.HollowWriteStateEngineBuilder; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Base64; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +public class HollowObjectMapperHollowRecordParserTest { + private HollowObjectMapper mapper; + + @Before + public void setUp() { + mapper = new HollowObjectMapper(new HollowWriteStateEngine()); + mapper.initializeTypeState(TypeWithAllSimpleTypes.class); + mapper.initializeTypeState(InternalTypeA.class); + mapper.initializeTypeState(TypeWithCollections.class); + mapper.initializeTypeState(VersionedType2.class); + mapper.initializeTypeState(SpecialWrapperTypesTest.class); + } + + @Test + public void testSpecialWrapperTypes() { + SpecialWrapperTypesTest wrapperTypesTest = new SpecialWrapperTypesTest(); + wrapperTypesTest.id = 8797182L; + wrapperTypesTest.type = AnEnum.SOME_VALUE_C; + wrapperTypesTest.complexEnum = ComplexEnum.SOME_VALUE_A; + wrapperTypesTest.dateCreated = new Date(); + + HollowReadStateEngine stateEngine = createReadStateEngine(wrapperTypesTest); + GenericHollowObject obj = new GenericHollowObject(stateEngine, "SpecialWrapperTypesTest", 0); + SpecialWrapperTypesTest result = mapper.readHollowRecord(obj); + + Assert.assertEquals(wrapperTypesTest, result); + Assert.assertEquals(wrapperTypesTest.complexEnum.value, result.complexEnum.value); + Assert.assertEquals(wrapperTypesTest.complexEnum.anotherValue, result.complexEnum.anotherValue); + } + + @Test + public void testNullableSpecialWrapperTypes() { + SpecialWrapperTypesTest wrapperTypesTest = new SpecialWrapperTypesTest(); + wrapperTypesTest.id = 8797182L; + wrapperTypesTest.type = AnEnum.SOME_VALUE_C; + + HollowReadStateEngine stateEngine = createReadStateEngine(wrapperTypesTest); + GenericHollowObject obj = new GenericHollowObject(stateEngine, "SpecialWrapperTypesTest", 0); + SpecialWrapperTypesTest result = mapper.readHollowRecord(obj); + + Assert.assertEquals(wrapperTypesTest, result); + Assert.assertNull(wrapperTypesTest.complexEnum); + Assert.assertNull(wrapperTypesTest.dateCreated); + } + + @Test + public void testSimpleTypes() { + TypeWithAllSimpleTypes typeWithAllSimpleTypes = new TypeWithAllSimpleTypes(); + typeWithAllSimpleTypes.boxedIntegerField = 1; + typeWithAllSimpleTypes.boxedBooleanField = true; + typeWithAllSimpleTypes.boxedDoubleField = 1.0; + typeWithAllSimpleTypes.boxedFloatField = 1.0f; + typeWithAllSimpleTypes.boxedLongField = 1L; + typeWithAllSimpleTypes.boxedShortField = (short) 1; + typeWithAllSimpleTypes.boxedByteField = (byte) 1; + typeWithAllSimpleTypes.boxedCharField = 'a'; + typeWithAllSimpleTypes.primitiveIntegerField = 2; + typeWithAllSimpleTypes.primitiveBooleanField = false; + typeWithAllSimpleTypes.primitiveDoubleField = 2.0; + typeWithAllSimpleTypes.primitiveFloatField = 2.0f; + typeWithAllSimpleTypes.primitiveLongField = 2L; + typeWithAllSimpleTypes.primitiveShortField = (short) 2; + typeWithAllSimpleTypes.primitiveByteField = (byte) 2; + typeWithAllSimpleTypes.primitiveCharField = 'b'; + typeWithAllSimpleTypes.byteArrayField = new byte[]{1, 2, 3}; + typeWithAllSimpleTypes.stringField = "string"; + typeWithAllSimpleTypes.inlinedIntegerField = 3; + typeWithAllSimpleTypes.inlinedBooleanField = true; + typeWithAllSimpleTypes.inlinedDoubleField = 3.0; + typeWithAllSimpleTypes.inlinedFloatField = 3.0f; + typeWithAllSimpleTypes.inlinedLongField = 3L; + typeWithAllSimpleTypes.inlinedShortField = (short) 3; + typeWithAllSimpleTypes.inlinedByteField = (byte) 3; + typeWithAllSimpleTypes.inlinedCharField = 'c'; + typeWithAllSimpleTypes.inlinedStringField = "inlinedstring"; + typeWithAllSimpleTypes.namedIntegerField = 4; + typeWithAllSimpleTypes.namedBooleanField = false; + typeWithAllSimpleTypes.namedDoubleField = 4.0; + typeWithAllSimpleTypes.namedFloatField = 4.0f; + typeWithAllSimpleTypes.namedLongField = 4L; + typeWithAllSimpleTypes.namedShortField = (short) 4; + typeWithAllSimpleTypes.namedByteField = (byte) 4; + typeWithAllSimpleTypes.namedCharField = 'd'; + typeWithAllSimpleTypes.namedByteArrayField = new byte[]{2, 4, 6}; + typeWithAllSimpleTypes.namedStringField = "namedstring"; + typeWithAllSimpleTypes.internalTypeAField = new InternalTypeA(1, "name"); + typeWithAllSimpleTypes.internalTypeCField = new InternalTypeC("data"); + + HollowReadStateEngine stateEngine = createReadStateEngine(typeWithAllSimpleTypes); + GenericHollowObject obj = new GenericHollowObject(stateEngine, "TypeWithAllSimpleTypes", 0); + + TypeWithAllSimpleTypes result = mapper.readHollowRecord(obj); + Assert.assertEquals(typeWithAllSimpleTypes, result); + } + + @Test + public void testNullablesSimpleTypes() { + TypeWithAllSimpleTypes typeWithAllSimpleTypes = new TypeWithAllSimpleTypes(); + typeWithAllSimpleTypes.boxedIntegerField = 1; + typeWithAllSimpleTypes.boxedCharField = 'a'; + typeWithAllSimpleTypes.primitiveIntegerField = Integer.MIN_VALUE; + typeWithAllSimpleTypes.primitiveDoubleField = Double.NaN; + typeWithAllSimpleTypes.inlinedLongField = 4L; + + HollowReadStateEngine stateEngine = createReadStateEngine(typeWithAllSimpleTypes); + GenericHollowObject obj = new GenericHollowObject(stateEngine, "TypeWithAllSimpleTypes", 0); + + TypeWithAllSimpleTypes result = mapper.readHollowRecord(obj); + Assert.assertEquals(Integer.valueOf(1), result.boxedIntegerField); + Assert.assertEquals(Character.valueOf('a'), result.boxedCharField); + Assert.assertNull(result.boxedFloatField); + Assert.assertNull(result.boxedDoubleField); + Assert.assertNull(result.boxedLongField); + Assert.assertNull(result.boxedShortField); + Assert.assertNull(result.boxedByteField); + Assert.assertEquals(0, result.primitiveIntegerField); + Assert.assertEquals(0.0, result.primitiveDoubleField, 0); + Assert.assertEquals(0.0f, result.primitiveFloatField, 0); + Assert.assertEquals(0L, result.primitiveLongField); + Assert.assertEquals(0, result.primitiveShortField); + Assert.assertEquals(0, result.primitiveByteField); + Assert.assertEquals(false, result.inlinedBooleanField); + Assert.assertEquals(Long.valueOf(4L), result.inlinedLongField); + Assert.assertNull(result.inlinedIntegerField); + Assert.assertNull(result.inlinedDoubleField); + Assert.assertNull(result.inlinedFloatField); + Assert.assertNull(result.inlinedShortField); + Assert.assertNull(result.inlinedByteField); + Assert.assertNull(result.inlinedCharField); + Assert.assertNull(result.inlinedStringField); + } + + @Test + public void testCollections() { + TypeWithCollections type = new TypeWithCollections(); + type.id = 1; + type.stringList = Arrays.asList("a", "b", "c"); + type.stringSet = new HashSet<>(type.stringList); + type.integerStringMap = type.stringList.stream().collect( + Collectors.toMap( + s -> type.stringList.indexOf(s), + s -> s + ) + ); + type.internalTypeAList = Arrays.asList(new InternalTypeA(1), new InternalTypeA(2)); + type.internalTypeASet = new HashSet<>(type.internalTypeAList); + type.integerInternalTypeAMap = type.internalTypeAList.stream().collect( + Collectors.toMap( + b -> b.id, + b -> b + ) + ); + type.internalTypeAStringMap = type.internalTypeAList.stream().collect( + Collectors.toMap( + b -> b, + b -> b.name + ) + ); + type.multiTypeMap = new HashMap<>(); + type.multiTypeMap.put(new TypeA10(1, "1", 1L), new TypeC10(new byte[]{1, 2, 3})); + type.multiTypeMap.put(new TypeA10(2, "2", 2L), new TypeC10(new byte[]{4, 5, 6})); + type.multiTypeMap.put(new TypeA10(2, "2", 2L), new TypeC10(new byte[]{7, 8, 9})); + + HollowReadStateEngine stateEngine = createReadStateEngine(type); + GenericHollowObject obj = new GenericHollowObject(stateEngine, "TypeWithCollections", 0); + TypeWithCollections result = mapper.readHollowRecord(obj); + + Assert.assertEquals(type, result); + } + + @Test + public void testNullableCollections() { + TypeWithCollections type = new TypeWithCollections(); + type.id = 1; + type.stringList = Arrays.asList("a", "b", "c"); + type.integerStringMap = type.stringList.stream().collect( + Collectors.toMap( + s -> type.stringList.indexOf(s), + s -> s + ) + ); + + HollowReadStateEngine stateEngine = createReadStateEngine(type); + GenericHollowObject obj = new GenericHollowObject(stateEngine, "TypeWithCollections", 0); + TypeWithCollections result = mapper.readHollowRecord(obj); + + Assert.assertEquals(type, result); + Assert.assertNull(result.stringSet); + Assert.assertNull(result.internalTypeAList); + Assert.assertNull(result.internalTypeASet); + Assert.assertNull(result.integerInternalTypeAMap); + Assert.assertNull(result.internalTypeAStringMap); + Assert.assertNull(result.multiTypeMap); + } + + @Test + public void testMapFromVersionedTypes() { + HollowObjectMapper readerMapper = new HollowObjectMapper(new HollowWriteStateEngine()); + readerMapper.initializeTypeState(TypeWithAllSimpleTypes.class); + readerMapper.initializeTypeState(InternalTypeA.class); + readerMapper.initializeTypeState(TypeWithCollections.class); + readerMapper.initializeTypeState(VersionedType1.class); + + VersionedType2 versionedType2 = new VersionedType2(); + versionedType2.boxedIntegerField = 1; + versionedType2.internalTypeBField = new InternalTypeB(1); + versionedType2.charField = 'a'; + versionedType2.primitiveDoubleField = 1.0; + versionedType2.stringSet = new HashSet<>(Arrays.asList("a", "b", "c")); + + HollowReadStateEngine stateEngine = createReadStateEngine(versionedType2); + GenericHollowObject obj = new GenericHollowObject(stateEngine, "VersionedType", 0); + + VersionedType1 result = readerMapper.readHollowRecord(obj); + + Assert.assertEquals(null, result.stringField); // stringField is not present in VersionedType1 + Assert.assertEquals(versionedType2.boxedIntegerField, result.boxedIntegerField); + Assert.assertEquals(versionedType2.primitiveDoubleField, result.primitiveDoubleField, 0); + Assert.assertEquals(null, result.internalTypeAField); // internalTypeAField is not present in VersionedType1 + Assert.assertEquals(versionedType2.stringSet, result.stringSet); + } + + @Test + public void shouldMapNonPrimitiveWrapperToPrimitiveWrapperIfCommonFieldIsTheSame() { + TypeStateA1 typeStateA1 = new TypeStateA1(); + typeStateA1.id = 1; + typeStateA1.subValue = new SubValue(); + typeStateA1.subValue.value = "value"; + + HollowReadStateEngine stateEngine = createReadStateEngine(typeStateA1); + GenericHollowObject obj = new GenericHollowObject(stateEngine, "TypeStateA", 0); + + HollowObjectMapper readerMapper = new HollowObjectMapper(new HollowWriteStateEngine()); + readerMapper.initializeTypeState(TypeStateA2.class); + TypeStateA2 result = readerMapper.readHollowRecord(obj); + + Assert.assertEquals("value", result.subValue); + } + + @Test + public void shouldMapPrimitiveWrapperToNonPrimitiveWrapperIfCommonFieldIsTheSame() { + TypeStateA2 typeStateA2 = new TypeStateA2(); + typeStateA2.id = 1; + typeStateA2.subValue = "value"; + + HollowReadStateEngine stateEngine = createReadStateEngine(typeStateA2); + GenericHollowObject obj = new GenericHollowObject(stateEngine, "TypeStateA", 0); + + HollowObjectMapper readerMapper = new HollowObjectMapper(new HollowWriteStateEngine()); + readerMapper.initializeTypeState(TypeStateA1.class); + TypeStateA1 result = readerMapper.readHollowRecord(obj); + + Assert.assertEquals("value", result.subValue.value); + } + + private HollowReadStateEngine createReadStateEngine(Object... recs) { + HollowWriteStateEngine writeStateEngine = new HollowWriteStateEngineBuilder() + .add(recs) + .build(); + try { + return StateEngineRoundTripper.roundTripSnapshot(writeStateEngine); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @HollowPrimaryKey(fields={"boxedIntegerField", "stringField"}) + private static class TypeWithAllSimpleTypes { + Integer boxedIntegerField; + Boolean boxedBooleanField; + Double boxedDoubleField; + Float boxedFloatField; + Long boxedLongField; + Short boxedShortField; + Byte boxedByteField; + Character boxedCharField; + + int primitiveIntegerField; + boolean primitiveBooleanField; + double primitiveDoubleField; + float primitiveFloatField; + long primitiveLongField; + short primitiveShortField; + byte primitiveByteField; + char primitiveCharField; + byte[] byteArrayField; + String stringField; + + @HollowInline + Integer inlinedIntegerField; + @HollowInline + Boolean inlinedBooleanField; + @HollowInline + Double inlinedDoubleField; + @HollowInline + Float inlinedFloatField; + @HollowInline + Long inlinedLongField; + @HollowInline + Short inlinedShortField; + @HollowInline + Byte inlinedByteField; + @HollowInline + Character inlinedCharField; + @HollowInline + String inlinedStringField; + + @HollowTypeName(name = "NamedIntegerField") + Integer namedIntegerField; + @HollowTypeName(name = "NamedBooleanField") + Boolean namedBooleanField; + @HollowTypeName(name = "NamedDoubleField") + Double namedDoubleField; + @HollowTypeName(name = "NamedFloatField") + Float namedFloatField; + @HollowTypeName(name = "NamedLongField") + Long namedLongField; + @HollowTypeName(name = "NamedShortField") + Short namedShortField; + @HollowTypeName(name = "NamedByteField") + Byte namedByteField; + @HollowTypeName(name = "NamedCharField") + Character namedCharField; + @HollowTypeName(name = "NamedByteArrayField") + byte[] namedByteArrayField; + @HollowTypeName(name = "NamedStringField") + String namedStringField; + + InternalTypeA internalTypeAField; + InternalTypeC internalTypeCField; + + @Override + public boolean equals(Object o) { + if(o instanceof TypeWithAllSimpleTypes) { + TypeWithAllSimpleTypes other = (TypeWithAllSimpleTypes)o; + return Objects.equals(boxedIntegerField, other.boxedIntegerField) && + Objects.equals(boxedBooleanField, other.boxedBooleanField) && + Objects.equals(boxedDoubleField, other.boxedDoubleField) && + Objects.equals(boxedFloatField, other.boxedFloatField) && + Objects.equals(boxedLongField, other.boxedLongField) && + Objects.equals(boxedShortField, other.boxedShortField) && + Objects.equals(boxedByteField, other.boxedByteField) && + Objects.equals(boxedCharField, other.boxedCharField) && + primitiveIntegerField == other.primitiveIntegerField && + primitiveBooleanField == other.primitiveBooleanField && + primitiveDoubleField == other.primitiveDoubleField && + primitiveFloatField == other.primitiveFloatField && + primitiveLongField == other.primitiveLongField && + primitiveShortField == other.primitiveShortField && + primitiveByteField == other.primitiveByteField && + primitiveCharField == other.primitiveCharField && + Arrays.equals(byteArrayField, other.byteArrayField) && + Objects.equals(stringField, other.stringField) && + Objects.equals(inlinedIntegerField, other.inlinedIntegerField) && + Objects.equals(inlinedBooleanField, other.inlinedBooleanField) && + Objects.equals(inlinedDoubleField, other.inlinedDoubleField) && + Objects.equals(inlinedFloatField, other.inlinedFloatField) && + Objects.equals(inlinedLongField, other.inlinedLongField) && + Objects.equals(inlinedShortField, other.inlinedShortField) && + Objects.equals(inlinedByteField, other.inlinedByteField) && + Objects.equals(inlinedCharField, other.inlinedCharField) && + Objects.equals(inlinedStringField, other.inlinedStringField) && + Objects.equals(namedIntegerField, other.namedIntegerField) && + Objects.equals(namedBooleanField, other.namedBooleanField) && + Objects.equals(namedDoubleField, other.namedDoubleField) && + Objects.equals(namedFloatField, other.namedFloatField) && + Objects.equals(namedLongField, other.namedLongField) && + Objects.equals(namedShortField, other.namedShortField) && + Objects.equals(namedByteField, other.namedByteField) && + Objects.equals(namedCharField, other.namedCharField) && + Arrays.equals(namedByteArrayField, other.namedByteArrayField) && + Objects.equals(namedStringField, other.namedStringField) && + Objects.equals(internalTypeAField, other.internalTypeAField) && + Objects.equals(internalTypeCField, other.internalTypeCField); + } + return false; + } + + @Override + public String toString() { + return "TypeA{" + + "boxedIntegerField=" + boxedIntegerField + + ", boxedBooleanField=" + boxedBooleanField + + ", boxedDoubleField=" + boxedDoubleField + + ", boxedFloatField=" + boxedFloatField + + ", boxedLongField=" + boxedLongField + + ", boxedShortField=" + boxedShortField + + ", boxedByteField=" + boxedByteField + + ", boxedCharField=" + boxedCharField + + ", primitiveIntegerField=" + primitiveIntegerField + + ", primitiveBooleanField=" + primitiveBooleanField + + ", primitiveDoubleField=" + primitiveDoubleField + + ", primitiveFloatField=" + primitiveFloatField + + ", primitiveLongField=" + primitiveLongField + + ", primitiveShortField=" + primitiveShortField + + ", primitiveByteField=" + primitiveByteField + + ", primitiveCharField=" + primitiveCharField + + ", byteArrayField=" + Arrays.toString(byteArrayField) + + ", stringField='" + stringField + '\'' + + ", inlinedIntegerField=" + inlinedIntegerField + + ", inlinedBooleanField=" + inlinedBooleanField + + ", inlinedDoubleField=" + inlinedDoubleField + + ", inlinedFloatField=" + inlinedFloatField + + ", inlinedLongField=" + inlinedLongField + + ", inlinedShortField=" + inlinedShortField + + ", inlinedByteField=" + inlinedByteField + + ", inlinedCharField=" + inlinedCharField + + ", inlinedStringField=" + inlinedStringField + + ", namedIntegerField=" + namedIntegerField + + ", namedBooleanField=" + namedBooleanField + + ", namedDoubleField=" + namedDoubleField + + ", namedFloatField=" + namedFloatField + + ", namedLongField=" + namedLongField + + ", namedShortField=" + namedShortField + + ", namedByteField=" + namedByteField + + ", namedCharField=" + namedCharField + + ", namedByteArrayField=" + Arrays.toString(namedByteArrayField) + + ", namedStringField='" + namedStringField + '\'' + + ", internalTypeAField=" + internalTypeAField + + ", internalTypeCField=" + internalTypeCField + + '}'; + } + } + + @HollowPrimaryKey(fields={"id"}) + private static class TypeWithCollections { + int id; + List stringList; + Set stringSet; + Map integerStringMap; + List internalTypeAList; + Set internalTypeASet; + Map integerInternalTypeAMap; + Map internalTypeAStringMap; + @HollowHashKey(fields="a2") + public Map multiTypeMap; + + @Override + public boolean equals(Object o) { + if(o instanceof TypeWithCollections) { + TypeWithCollections other = (TypeWithCollections)o; + return id == other.id && + Objects.equals(stringList, other.stringList) && + Objects.equals(stringSet, other.stringSet) && + Objects.equals(integerStringMap, other.integerStringMap) && + Objects.equals(internalTypeAList, other.internalTypeAList) && + Objects.equals(internalTypeASet, other.internalTypeASet) && + Objects.equals(integerInternalTypeAMap, other.integerInternalTypeAMap) && + Objects.equals(internalTypeAStringMap, other.internalTypeAStringMap) && + Objects.equals(multiTypeMap, other.multiTypeMap); + } + return false; + } + + @Override + public String toString() { + return "TypeWithCollections{" + + "id=" + id + + ", stringList=" + stringList + + ", stringSet=" + stringSet + + ", integerStringMap=" + integerStringMap + + ", internalTypeAList=" + internalTypeAList + + ", internalTypeASet=" + internalTypeASet + + ", integerInternalTypeAMap=" + integerInternalTypeAMap + + ", internalTypeAStringMap=" + internalTypeAStringMap + + ", multiTypeMap=" + multiTypeMap + + '}'; + } + } + + @HollowTypeName(name = "VersionedType") + private static class VersionedType1 { + String stringField; + Integer boxedIntegerField; + double primitiveDoubleField; + InternalTypeA internalTypeAField; + Set stringSet; + + @Override + public boolean equals(Object o) { + if(o instanceof VersionedType1) { + VersionedType1 other = (VersionedType1)o; + return Objects.equals(stringField, other.stringField) && + Objects.equals(boxedIntegerField, other.boxedIntegerField) && + primitiveDoubleField == other.primitiveDoubleField && + Objects.equals(internalTypeAField, other.internalTypeAField) && + Objects.equals(stringSet, other.stringSet); + } + return false; + } + + @Override + public String toString() { + return "VersionedType1{" + + "stringField='" + stringField + '\'' + + ", boxedIntegerField=" + boxedIntegerField + + ", primitiveDoubleField=" + primitiveDoubleField + + ", internalTypeAField=" + internalTypeAField + + ", stringSet=" + stringSet + + '}'; + } + } + + @HollowTypeName(name = "VersionedType") + private static class VersionedType2 { + // No longer has the stringField + Integer boxedIntegerField; + double primitiveDoubleField; + // No longer has the typeBField + Set stringSet; + char charField; // Added a char field + InternalTypeB internalTypeBField; // Added a new type field + + @Override + public boolean equals(Object o) { + if(o instanceof VersionedType2) { + VersionedType2 other = (VersionedType2)o; + return Objects.equals(boxedIntegerField, other.boxedIntegerField) && + primitiveDoubleField == other.primitiveDoubleField && + Objects.equals(stringSet, other.stringSet) && + charField == other.charField && + Objects.equals(internalTypeBField, other.internalTypeBField); + } + return false; + } + + @Override + public String toString() { + return "VersionedType2{" + + "boxedIntegerField=" + boxedIntegerField + + ", primitiveDoubleField=" + primitiveDoubleField + + ", stringSet=" + stringSet + + ", charField=" + charField + + ", internalTypeBField=" + internalTypeBField + + '}'; + } + } + + private static class InternalTypeA { + Integer id; + String name; + + public InternalTypeA(Integer id) { + this(id, String.valueOf(id)); + } + + public InternalTypeA(Integer id, String name) { + this.id = id; + this.name = name; + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } + + @Override + public boolean equals(Object o) { + if(o instanceof InternalTypeA) { + InternalTypeA other = (InternalTypeA)o; + return id.equals(other.id) && name.equals(other.name); + } + return false; + } + + @Override + public String toString() { + return "InternalTypeA{" + + "id=" + id + + ", name='" + name + '\'' + + '}'; + } + } + + private static class InternalTypeB { + Integer id; + String name; + + public InternalTypeB(Integer id) { + this(id, String.valueOf(id)); + } + + public InternalTypeB(Integer id, String name) { + this.id = id; + this.name = name; + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } + + @Override + public boolean equals(Object o) { + if(o instanceof InternalTypeB) { + InternalTypeB other = (InternalTypeB)o; + return id.equals(other.id) && name.equals(other.name); + } + return false; + } + + @Override + public String toString() { + return "InternalTypeB{" + + "id=" + id + + ", name='" + name + '\'' + + '}'; + } + } + + public static class InternalTypeC { + @HollowInline + String data; + + public InternalTypeC(String data) { + this.data = data; + } + + @Override + public boolean equals(Object o) { + if(o instanceof InternalTypeC) { + InternalTypeC other = (InternalTypeC)o; + return data.equals(other.data); + } + return false; + } + + @Override + public String toString() { + return "InternalTypeC{" + + "data=" + data + + '}'; + } + } + + @HollowTypeName(name="TypeStateA") + @HollowPrimaryKey(fields="id") + public static class TypeStateA1 { + public int id; + public SubValue subValue; + } + + @HollowTypeName(name="TypeStateA") + @HollowPrimaryKey(fields="id") + public static class TypeStateA2 { + public int id; + @HollowTypeName(name="SubValue") + public String subValue; + } + + public static class SubValue { + @HollowInline + public String value; + @HollowInline + public String anotherValue; + } + + enum AnEnum { + SOME_VALUE_A, + SOME_VALUE_B, + SOME_VALUE_C, + } + + enum ComplexEnum { + SOME_VALUE_A("A", 1), + SOME_VALUE_B("B", 2), + SOME_VALUE_C("C", 3); + + final String value; + final int anotherValue; + + ComplexEnum(String value, int anotherValue) { + this.value = value; + this.anotherValue = anotherValue; + } + } + + @HollowTypeName(name = "SpecialWrapperTypesTest") + @HollowPrimaryKey(fields = {"id"}) + static class SpecialWrapperTypesTest { + long id; + @HollowTypeName(name = "AnEnum") + AnEnum type; + @HollowTypeName(name = "ComplexEnum") + ComplexEnum complexEnum; + Date dateCreated; + + @Override + public boolean equals(Object o) { + if (o instanceof SpecialWrapperTypesTest) { + SpecialWrapperTypesTest other = (SpecialWrapperTypesTest) o; + return Objects.equals(id, other.id) && + Objects.equals(type, other.type) && + Objects.equals(complexEnum, other.complexEnum) && + Objects.equals(dateCreated, other.dateCreated); + } + return false; + } + + @Override + public String toString() { + return "SpecialWrapperTypesTest{" + + "id=" + id + + ", type='" + type + '\'' + + ", complexEnum='" + complexEnum + '\'' + + ", dateCreated=" + dateCreated + + '}'; + } + } + + public static class TypeA10 { + public int a1; + @HollowInline + public String a2; + public long a3; + + public TypeA10(int a1, String a2, long a3) { + this.a1 = a1; + this.a2 = a2; + this.a3 = a3; + } + + @Override + public String toString() { + return "{" + a1 + "," + a2 + "," + a3 + "}"; + } + + @Override + public int hashCode() { + return Objects.hash(a1, a2, a3); + } + + @Override + public boolean equals(Object o) { + if(o instanceof TypeA10) { + TypeA10 other = (TypeA10)o; + return a1 == other.a1 && a2.equals(other.a2) && a3 == other.a3; + } + return false; + } + } + + public static class TypeC10 { + public byte[] c1; + + public TypeC10(byte[] c1) { + this.c1 = c1; + } + + @Override + public String toString() { + return Base64.getEncoder().encodeToString(c1); + } + + @Override + public boolean equals(Object o) { + if(o instanceof TypeC10) { + TypeC10 other = (TypeC10)o; + return Arrays.equals(c1, other.c1); + } + return false; + } + } +}