diff --git a/dump/src/util/dump/ExternalizableBean.java b/dump/src/util/dump/ExternalizableBean.java index 536646a..f760748 100644 --- a/dump/src/util/dump/ExternalizableBean.java +++ b/dump/src/util/dump/ExternalizableBean.java @@ -225,26 +225,31 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc j++; } - FieldType ft; + FieldType inputFt, configuredFt; FieldAccessor f = null; Class defaultType = null; + externalize annotation = null; + if ( (fieldIndexes[j] & 0xff) == fieldIndex ) { - final FieldType fft = ft = fieldTypes[j]; + final FieldType fft = configuredFt = inputFt = fieldTypes[j]; f = fieldAccessors[j]; defaultType = defaultTypes[j]; - if ( fieldTypeId != ft._id ) { - if ( fieldTypeId == FieldType.EnumOld._id && ft._id == FieldType.Enum._id ) { - ft = FieldType.EnumOld; - } else if ( fieldTypeId == FieldType.EnumSetOld._id && ft._id == FieldType.EnumSet._id ) { - ft = FieldType.EnumSetOld; - } else if ( fieldTypeId == FieldType.SetOfStrings._id && ft._id == FieldType.Set._id ) { - ft = FieldType.SetOfStrings; - } else if ( fieldTypeId == FieldType.ListOfStrings._id && ft._id == FieldType.List._id ) { - ft = FieldType.ListOfStrings; - } else if ( fieldTypeId == FieldType.Set._id && ft._id == FieldType.SetOfStrings._id ) { - ft = FieldType.Set; - } else if ( fieldTypeId == FieldType.List._id && ft._id == FieldType.ListOfStrings._id ) { - ft = FieldType.List; + if ( fieldTypeId != inputFt._id ) { + if ( fieldTypeId == FieldType.EnumOld._id && inputFt._id == FieldType.Enum._id ) { + inputFt = FieldType.EnumOld; + } else if ( fieldTypeId == FieldType.EnumSetOld._id && inputFt._id == FieldType.EnumSet._id ) { + inputFt = FieldType.EnumSetOld; + } else if ( fieldTypeId == FieldType.SetOfStrings._id && inputFt._id == FieldType.Set._id ) { + inputFt = FieldType.SetOfStrings; + } else if ( fieldTypeId == FieldType.ListOfStrings._id && inputFt._id == FieldType.List._id ) { + inputFt = FieldType.ListOfStrings; + } else if ( fieldTypeId == FieldType.Set._id && inputFt._id == FieldType.SetOfStrings._id ) { + inputFt = FieldType.Set; + } else if ( fieldTypeId == FieldType.List._id && inputFt._id == FieldType.ListOfStrings._id ) { + inputFt = FieldType.List; + } else if ( isCompatible(FieldType.forId(fieldTypeId), fft) ) { + annotation = config._annotations[j]; + inputFt = FieldType.forId(fieldTypeId); } else if ( Boolean.TRUE.equals(CLASS_CHANGED_INCOMPATIBLY.computeIfAbsent(getClass(), clazz -> { LoggerFactory.getLogger(clazz).error("The field type of index " + fieldIndex + // " in " + clazz.getSimpleName() + // @@ -255,17 +260,17 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc return Boolean.TRUE; })) ) { // read it without exception, but ignore the data - ft = FieldType.forId(fieldTypeId); + inputFt = FieldType.forId(fieldTypeId); f = null; } } } else { // unknown field - ft = FieldType.forId(fieldTypeId); + configuredFt = inputFt = FieldType.forId(fieldTypeId); } - Objects.requireNonNull(ft, "Invalid field type " + (fieldTypeId & 0xff) + " for field index " + fieldIndex); + Objects.requireNonNull(inputFt, "Invalid field type " + (fieldTypeId & 0xff) + " for field index " + fieldIndex); - if ( ft.isLengthDynamic() ) { + if ( inputFt.isLengthDynamic() ) { int len = in.readInt(); if ( f == null ) { // unknown field, skip it skipFully(in, len); @@ -273,60 +278,92 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc } } - switch ( ft ) { + switch ( inputFt ) { case pInt: { int d = in.readInt(); if ( f != null ) { - f.setInt(this, d); + switch ( configuredFt ) { + case pInt -> f.setInt(this, d); + case Integer -> f.set(this, annotation.sparseBoxed() && d == annotation.pIntNullValue() ? null : d); + default -> throw new IllegalStateException(); + } } break; } case pBoolean: { boolean d = in.readBoolean(); if ( f != null ) { - f.setBoolean(this, d); + switch ( configuredFt ) { + case pBoolean -> f.setBoolean(this, d); + case Boolean -> f.set(this, annotation.sparseBoxed() && d == annotation.pBooleanNullValue() ? null : d); + default -> throw new IllegalStateException(); + } } break; } case pByte: { byte d = in.readByte(); if ( f != null ) { - f.setByte(this, d); + switch ( configuredFt ) { + case pByte -> f.setByte(this, d); + case Byte -> f.set(this, annotation.sparseBoxed() && d == annotation.pByteNullValue() ? null : d); + default -> throw new IllegalStateException(); + } } break; } case pChar: { char d = in.readChar(); if ( f != null ) { - f.setChar(this, d); + switch ( configuredFt ) { + case pChar -> f.setChar(this, d); + case Character -> f.set(this, annotation.sparseBoxed() && d == annotation.pCharNullValue() ? null : d); + default -> throw new IllegalStateException(); + } } break; } case pDouble: { double d = in.readDouble(); if ( f != null ) { - f.setDouble(this, d); + switch ( configuredFt ) { + case pDouble -> f.setDouble(this, d); + case Double -> f.set(this, annotation.sparseBoxed() && d == annotation.pDoubleNullValue() ? null : d); + default -> throw new IllegalStateException(); + } } break; } case pFloat: { float d = in.readFloat(); if ( f != null ) { - f.setFloat(this, d); + switch ( configuredFt ) { + case pFloat -> f.setFloat(this, d); + case Float -> f.set(this, annotation.sparseBoxed() && d == annotation.pFloatNullValue() ? null : d); + default -> throw new IllegalStateException(); + } } break; } case pLong: { long d = in.readLong(); if ( f != null ) { - f.setLong(this, d); + switch ( configuredFt ) { + case pLong -> f.setLong(this, d); + case Long -> f.set(this, annotation.sparseBoxed() && d == annotation.pLongNullValue() ? null : d); + default -> throw new IllegalStateException(); + } } break; } case pShort: { short d = in.readShort(); if ( f != null ) { - f.setShort(this, d); + switch ( configuredFt ) { + case pShort -> f.setShort(this, d); + case Short -> f.set(this, annotation.sparseBoxed() && d == annotation.pShortNullValue() ? null : d); + default -> throw new IllegalStateException(); + } } break; } @@ -365,56 +402,89 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc case Integer: { Integer d = readInteger(in); if ( f != null ) { - f.set(this, d); + switch ( configuredFt ) { + case pInt -> f.setInt(this, d == null ? annotation.pIntNullValue() : d); + case Integer -> f.set(this, d); + default -> throw new IllegalStateException(); + } } break; } case Boolean: { Boolean d = readBoolean(in); if ( f != null ) { - f.set(this, d); + switch ( configuredFt ) { + case pBoolean -> //noinspection SimplifiableConditionalExpression + f.setBoolean(this, d == null ? annotation.pBooleanNullValue() : false); + case Boolean -> f.set(this, d); + default -> throw new IllegalStateException(); + } } break; } case Byte: { Byte d = readByte(in); if ( f != null ) { - f.set(this, d); + switch ( configuredFt ) { + case pByte -> f.setByte(this, d == null ? annotation.pByteNullValue() : d); + case Byte -> f.set(this, d); + default -> throw new IllegalStateException(); + } } break; } case Character: { Character d = readCharacter(in); if ( f != null ) { - f.set(this, d); + switch ( configuredFt ) { + case pChar -> f.setChar(this, d == null ? annotation.pCharNullValue() : d); + case Character -> f.set(this, d); + default -> throw new IllegalStateException(); + } } break; } case Double: { Double d = readDouble(in); if ( f != null ) { - f.set(this, d); + switch ( configuredFt ) { + case pDouble -> f.setDouble(this, d == null ? annotation.pDoubleNullValue() : d); + case Double -> f.set(this, d); + default -> throw new IllegalStateException(); + } } break; } case Float: { Float d = readFloat(in); if ( f != null ) { - f.set(this, d); + switch ( configuredFt ) { + case pFloat -> f.setFloat(this, d == null ? annotation.pFloatNullValue() : d); + case Float -> f.set(this, d); + default -> throw new IllegalStateException(); + } } break; } case Long: { Long d = readLong(in); if ( f != null ) { - f.set(this, d); + switch ( configuredFt ) { + case pLong -> f.setLong(this, d == null ? annotation.pLongNullValue() : d); + case Long -> f.set(this, d); + default -> throw new IllegalStateException(); + } } break; } case Short: { Short d = readShort(in); if ( f != null ) { - f.set(this, d); + switch ( configuredFt ) { + case pShort -> f.setShort(this, d == null ? annotation.pShortNullValue() : d); + case Short -> f.set(this, d); + default -> throw new IllegalStateException(); + } } break; } @@ -1216,6 +1286,44 @@ default void writeExternal( ObjectOutput out ) throws IOException { } } + private boolean isCompatible( FieldType inputType, FieldType configuredType ) { + return switch ( inputType ) { + case Boolean, pBoolean -> switch ( configuredType ) { + case Boolean, pBoolean -> true; + default -> false; + }; + case Character, pChar -> switch ( configuredType ) { + case Character, pChar -> true; + default -> false; + }; + case Byte, pByte -> switch ( configuredType ) { + case Byte, pByte -> true; + default -> false; + }; + case Short, pShort -> switch ( configuredType ) { + case Short, pShort -> true; + default -> false; + }; + case Integer, pInt -> switch ( configuredType ) { + case Integer, pInt -> true; + default -> false; + }; + case Long, pLong -> switch ( configuredType ) { + case Long, pLong -> true; + default -> false; + }; + case Float, pFloat -> switch ( configuredType ) { + case Float, pFloat -> true; + default -> false; + }; + case Double, pDouble -> switch ( configuredType ) { + case Double, pDouble -> true; + default -> false; + }; + default -> false; + }; + } + /** * By adding this annotation to a class implementing ExternalizableBean, you can make certain, that the byte[] * created by externalizing has a size where size%sizeModulo==0, i.e. it is divisible by @@ -1275,6 +1383,51 @@ default void writeExternal( ObjectOutput out ) throws IOException { */ Class defaultType() default System.class; // System.class is just a placeholder for nothing, in order to make this argument optional + /** + * The value for primitive fields being migrated from boxed values, in case the latter reads null from the input. + */ + boolean pBooleanNullValue() default false; + + /** + * The value for primitive fields being migrated from boxed values, in case the latter reads null from the input. + */ + byte pByteNullValue() default 0; + + /** + * The value for primitive fields being migrated from boxed values, in case the latter reads null from the input. + */ + char pCharNullValue() default 0; + + /** + * The value for primitive fields being migrated from boxed values, in case the latter reads null from the input. + */ + double pDoubleNullValue() default 0.0; + + /** + * The value for primitive fields being migrated from boxed values, in case the latter reads null from the input. + */ + float pFloatNullValue() default 0.0f; + + /** + * The value for primitive fields being migrated from boxed values, in case the latter reads null from the input. + */ + int pIntNullValue() default 0; + + /** + * The value for primitive fields being migrated from boxed values, in case the latter reads null from the input. + */ + long pLongNullValue() default 0L; + + /** + * The value for primitive fields being migrated from boxed values, in case the latter reads null from the input. + */ + short pShortNullValue() default 0; + + /** + * Defines whether boxed fields are set to null whenever primitive input matches the pTypeNullValue + */ + boolean sparseBoxed() default true; + /** * Aka index. Must be unique. Convention is to start from 1. To guarantee compatibility between revisions of a bean, * you may never change the field type or any of the default*Types while reusing the same index specified with this parameter. diff --git a/dump/src/util/dump/ExternalizationHelper.java b/dump/src/util/dump/ExternalizationHelper.java index e228f49..03e3079 100644 --- a/dump/src/util/dump/ExternalizationHelper.java +++ b/dump/src/util/dump/ExternalizationHelper.java @@ -272,7 +272,6 @@ static Collection readCollectionContainer( ObjectInput in, Class defaultType, bo } return switch ( containerType ) { - default -> d; case UnmodifiableCollection -> Collections.unmodifiableCollection(d); case UnmodifiableList -> Collections.unmodifiableList((List)d); @@ -280,6 +279,7 @@ static Collection readCollectionContainer( ObjectInput in, Class defaultType, bo case ImmutableList -> List.copyOf(d); case ImmutableSet -> Set.copyOf(d); + default -> d; }; } @@ -1189,6 +1189,7 @@ private static boolean isSetter( Method m ) { Class _class; ClassLoader _classLoader; + externalize[] _annotations; FieldAccessor[] _fieldAccessors; byte[] _fieldIndexes; FieldType[] _fieldTypes; @@ -1218,6 +1219,7 @@ public ClassConfig( Class clientClass ) { Collections.sort(fieldInfos); + _annotations = new externalize[fieldInfos.size()]; _fieldAccessors = new FieldAccessor[fieldInfos.size()]; _fieldIndexes = new byte[fieldInfos.size()]; _fieldTypes = new FieldType[fieldInfos.size()]; @@ -1226,6 +1228,7 @@ public ClassConfig( Class clientClass ) { _defaultGenericTypes1 = new Class[fieldInfos.size()]; for ( int i = 0, length = fieldInfos.size(); i < length; i++ ) { FieldInfo fi = fieldInfos.get(i); + _annotations[i] = fi._annotation; _fieldAccessors[i] = fi._fieldAccessor; _fieldIndexes[i] = fi._fieldIndex; _fieldTypes[i] = fi._fieldType; @@ -1241,6 +1244,8 @@ public ClassConfig( Class clientClass ) { private void addFieldInfo( List fieldInfos, externalize annotation, FieldAccessor fieldAccessor, Class type, String fieldName ) { FieldInfo fi = new FieldInfo(); + fi._annotation = annotation; + fi._fieldAccessor = fieldAccessor; byte index = annotation.value(); @@ -1454,6 +1459,7 @@ private void initSizeModulo( List fieldInfos ) { static class FieldInfo implements Comparable { + externalize _annotation; FieldAccessor _fieldAccessor; FieldType _fieldType; byte _fieldIndex; diff --git a/dump/test/util/dump/externalization/BaseExternalizableBeanRoundtripTest.java b/dump/test/util/dump/externalization/BaseExternalizableBeanRoundtripTest.java index e16477c..c762a67 100644 --- a/dump/test/util/dump/externalization/BaseExternalizableBeanRoundtripTest.java +++ b/dump/test/util/dump/externalization/BaseExternalizableBeanRoundtripTest.java @@ -28,8 +28,12 @@ protected void thenBeansAreEqual() { @SuppressWarnings("unchecked") protected void whenBeanIsExternalizedAndRead() { + whenBeanIsExternalizedAndRead((Class)_beanToWrite.getClass()); + } + + protected void whenBeanIsExternalizedAndRead( Class beanClass ) { byte[] bytes = SingleTypeObjectOutputStream.writeSingleInstance(_beanToWrite); - _beanThatWasRead = SingleTypeObjectInputStream.readSingleInstance((Class)_beanToWrite.getClass(), bytes); + _beanThatWasRead = SingleTypeObjectInputStream.readSingleInstance(beanClass, bytes); } } diff --git a/dump/test/util/dump/externalization/BoxedPrimitiveMigrationTest.java b/dump/test/util/dump/externalization/BoxedPrimitiveMigrationTest.java new file mode 100644 index 0000000..3bb116e --- /dev/null +++ b/dump/test/util/dump/externalization/BoxedPrimitiveMigrationTest.java @@ -0,0 +1,81 @@ +package util.dump.externalization; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +import util.dump.ExternalizableBean; + + +public class BoxedPrimitiveMigrationTest extends BaseExternalizableBeanRoundtripTest { + + @Test + public void longBoxedToPrimitive() { + givenBean(new BoxedLong(7L)); + whenBeanIsExternalizedAndRead(PrimitiveLong.class); + assertThat(_beanThatWasRead).extracting("value").isEqualTo(7L); + } + + @Test + public void longBoxedToPrimitiveDefault() { + givenBean(new BoxedLong(null)); + whenBeanIsExternalizedAndRead(PrimitiveLongWithDefault.class); + assertThat(_beanThatWasRead).extracting("value").isEqualTo(Long.MIN_VALUE); + } + + @Test + public void longBoxedToPrimitiveNull() { + givenBean(new BoxedLong(null)); + whenBeanIsExternalizedAndRead(PrimitiveLong.class); + assertThat(_beanThatWasRead).extracting("value").isEqualTo(0L); + } + + @Test + public void longPrimitiveToBoxed() { + givenBean(new PrimitiveLong(11L)); + whenBeanIsExternalizedAndRead(BoxedLong.class); + assertThat(_beanThatWasRead).extracting("value").isEqualTo(11L); + } + + @Test + public void longPrimitiveToSparseBoxed() { + givenBean(new PrimitiveLong(13L)); + whenBeanIsExternalizedAndRead(BoxedLong.class); + assertThat(_beanThatWasRead).extracting("value").isNull(); + } + + public static final class BoxedLong implements ExternalizableBean { + + @externalize(value = 1, pLongNullValue = 13L) + Long value; + + public BoxedLong() {} + + public BoxedLong( Long value ) { + this.value = value; + } + } + + + public static final class PrimitiveLong implements ExternalizableBean { + + @externalize(1) + long value; + + public PrimitiveLong() {} + + public PrimitiveLong( long value ) { + this.value = value; + } + } + + + public static final class PrimitiveLongWithDefault implements ExternalizableBean { + + @externalize(value = 1, pLongNullValue = Long.MIN_VALUE) + long value; + + public PrimitiveLongWithDefault() {} + } + +}