From 177a988a01920c78b6b5ca97f55098097657b8a9 Mon Sep 17 00:00:00 2001 From: Christian Trefzer Date: Tue, 1 Oct 2024 15:08:39 +0200 Subject: [PATCH 1/2] TECH: allow on-the-fly migration of primitive vs. boxed fields --- dump/src/util/dump/ExternalizableBean.java | 217 +++++++++++++++--- dump/src/util/dump/ExternalizationHelper.java | 8 +- .../BaseExternalizableBeanRoundtripTest.java | 6 +- .../BoxedPrimitiveMigrationTest.java | 74 ++++++ 4 files changed, 267 insertions(+), 38 deletions(-) create mode 100644 dump/test/util/dump/externalization/BoxedPrimitiveMigrationTest.java diff --git a/dump/src/util/dump/ExternalizableBean.java b/dump/src/util/dump/ExternalizableBean.java index 536646a..c9f7e4c 100644 --- a/dump/src/util/dump/ExternalizableBean.java +++ b/dump/src/util/dump/ExternalizableBean.java @@ -225,26 +225,28 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc j++; } - FieldType ft; + FieldType inputFt, configuredFt; FieldAccessor f = null; Class defaultType = 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) ) { + 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 +257,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 +275,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, 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, 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, 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, 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, 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, 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, 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, d); + default -> throw new IllegalStateException(); + } } break; } @@ -365,56 +399,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 ? config._annotations[j].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 ? config._annotations[j].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 ? config._annotations[j].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 ? config._annotations[j].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 ? config._annotations[j].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 ? config._annotations[j].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 ? config._annotations[j].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 ? config._annotations[j].pShortNullValue() : d); + case Short -> f.set(this, d); + default -> throw new IllegalStateException(); + } } break; } @@ -1216,6 +1283,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 +1380,46 @@ 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; + /** * 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..d87b196 --- /dev/null +++ b/dump/test/util/dump/externalization/BoxedPrimitiveMigrationTest.java @@ -0,0 +1,74 @@ +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); + } + + public static final class BoxedLong implements ExternalizableBean { + + @externalize(1) + 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() {} + } + +} From 014cf4af770ce517faedbb210dcd9dcca2622fa8 Mon Sep 17 00:00:00 2001 From: Christian Trefzer Date: Tue, 1 Oct 2024 15:27:22 +0200 Subject: [PATCH 2/2] TECH: allow migration to sparse boxed fields --- dump/src/util/dump/ExternalizableBean.java | 40 +++++++++++-------- .../BoxedPrimitiveMigrationTest.java | 9 ++++- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/dump/src/util/dump/ExternalizableBean.java b/dump/src/util/dump/ExternalizableBean.java index c9f7e4c..f760748 100644 --- a/dump/src/util/dump/ExternalizableBean.java +++ b/dump/src/util/dump/ExternalizableBean.java @@ -228,6 +228,8 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc FieldType inputFt, configuredFt; FieldAccessor f = null; Class defaultType = null; + externalize annotation = null; + if ( (fieldIndexes[j] & 0xff) == fieldIndex ) { final FieldType fft = configuredFt = inputFt = fieldTypes[j]; f = fieldAccessors[j]; @@ -246,6 +248,7 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc } 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 + // @@ -281,7 +284,7 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc if ( f != null ) { switch ( configuredFt ) { case pInt -> f.setInt(this, d); - case Integer -> f.set(this, d); + case Integer -> f.set(this, annotation.sparseBoxed() && d == annotation.pIntNullValue() ? null : d); default -> throw new IllegalStateException(); } } @@ -292,7 +295,7 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc if ( f != null ) { switch ( configuredFt ) { case pBoolean -> f.setBoolean(this, d); - case Boolean -> f.set(this, d); + case Boolean -> f.set(this, annotation.sparseBoxed() && d == annotation.pBooleanNullValue() ? null : d); default -> throw new IllegalStateException(); } } @@ -303,7 +306,7 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc if ( f != null ) { switch ( configuredFt ) { case pByte -> f.setByte(this, d); - case Byte -> f.set(this, d); + case Byte -> f.set(this, annotation.sparseBoxed() && d == annotation.pByteNullValue() ? null : d); default -> throw new IllegalStateException(); } } @@ -314,7 +317,7 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc if ( f != null ) { switch ( configuredFt ) { case pChar -> f.setChar(this, d); - case Character -> f.set(this, d); + case Character -> f.set(this, annotation.sparseBoxed() && d == annotation.pCharNullValue() ? null : d); default -> throw new IllegalStateException(); } } @@ -325,7 +328,7 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc if ( f != null ) { switch ( configuredFt ) { case pDouble -> f.setDouble(this, d); - case Double -> f.set(this, d); + case Double -> f.set(this, annotation.sparseBoxed() && d == annotation.pDoubleNullValue() ? null : d); default -> throw new IllegalStateException(); } } @@ -336,7 +339,7 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc if ( f != null ) { switch ( configuredFt ) { case pFloat -> f.setFloat(this, d); - case Float -> f.set(this, d); + case Float -> f.set(this, annotation.sparseBoxed() && d == annotation.pFloatNullValue() ? null : d); default -> throw new IllegalStateException(); } } @@ -347,7 +350,7 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc if ( f != null ) { switch ( configuredFt ) { case pLong -> f.setLong(this, d); - case Long -> f.set(this, d); + case Long -> f.set(this, annotation.sparseBoxed() && d == annotation.pLongNullValue() ? null : d); default -> throw new IllegalStateException(); } } @@ -358,7 +361,7 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc if ( f != null ) { switch ( configuredFt ) { case pShort -> f.setShort(this, d); - case Short -> f.set(this, d); + case Short -> f.set(this, annotation.sparseBoxed() && d == annotation.pShortNullValue() ? null : d); default -> throw new IllegalStateException(); } } @@ -400,7 +403,7 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc Integer d = readInteger(in); if ( f != null ) { switch ( configuredFt ) { - case pInt -> f.setInt(this, d == null ? config._annotations[j].pIntNullValue() : d); + case pInt -> f.setInt(this, d == null ? annotation.pIntNullValue() : d); case Integer -> f.set(this, d); default -> throw new IllegalStateException(); } @@ -412,7 +415,7 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc if ( f != null ) { switch ( configuredFt ) { case pBoolean -> //noinspection SimplifiableConditionalExpression - f.setBoolean(this, d == null ? config._annotations[j].pBooleanNullValue() : false); + f.setBoolean(this, d == null ? annotation.pBooleanNullValue() : false); case Boolean -> f.set(this, d); default -> throw new IllegalStateException(); } @@ -423,7 +426,7 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc Byte d = readByte(in); if ( f != null ) { switch ( configuredFt ) { - case pByte -> f.setByte(this, d == null ? config._annotations[j].pByteNullValue() : d); + case pByte -> f.setByte(this, d == null ? annotation.pByteNullValue() : d); case Byte -> f.set(this, d); default -> throw new IllegalStateException(); } @@ -434,7 +437,7 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc Character d = readCharacter(in); if ( f != null ) { switch ( configuredFt ) { - case pChar -> f.setChar(this, d == null ? config._annotations[j].pCharNullValue() : d); + case pChar -> f.setChar(this, d == null ? annotation.pCharNullValue() : d); case Character -> f.set(this, d); default -> throw new IllegalStateException(); } @@ -445,7 +448,7 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc Double d = readDouble(in); if ( f != null ) { switch ( configuredFt ) { - case pDouble -> f.setDouble(this, d == null ? config._annotations[j].pDoubleNullValue() : d); + case pDouble -> f.setDouble(this, d == null ? annotation.pDoubleNullValue() : d); case Double -> f.set(this, d); default -> throw new IllegalStateException(); } @@ -456,7 +459,7 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc Float d = readFloat(in); if ( f != null ) { switch ( configuredFt ) { - case pFloat -> f.setFloat(this, d == null ? config._annotations[j].pFloatNullValue() : d); + case pFloat -> f.setFloat(this, d == null ? annotation.pFloatNullValue() : d); case Float -> f.set(this, d); default -> throw new IllegalStateException(); } @@ -467,7 +470,7 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc Long d = readLong(in); if ( f != null ) { switch ( configuredFt ) { - case pLong -> f.setLong(this, d == null ? config._annotations[j].pLongNullValue() : d); + case pLong -> f.setLong(this, d == null ? annotation.pLongNullValue() : d); case Long -> f.set(this, d); default -> throw new IllegalStateException(); } @@ -478,7 +481,7 @@ default void readExternal( ObjectInput in ) throws IOException, ClassNotFoundExc Short d = readShort(in); if ( f != null ) { switch ( configuredFt ) { - case pShort -> f.setShort(this, d == null ? config._annotations[j].pShortNullValue() : d); + case pShort -> f.setShort(this, d == null ? annotation.pShortNullValue() : d); case Short -> f.set(this, d); default -> throw new IllegalStateException(); } @@ -1420,6 +1423,11 @@ private boolean isCompatible( FieldType inputType, FieldType configuredType ) { */ 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/test/util/dump/externalization/BoxedPrimitiveMigrationTest.java b/dump/test/util/dump/externalization/BoxedPrimitiveMigrationTest.java index d87b196..3bb116e 100644 --- a/dump/test/util/dump/externalization/BoxedPrimitiveMigrationTest.java +++ b/dump/test/util/dump/externalization/BoxedPrimitiveMigrationTest.java @@ -37,9 +37,16 @@ public void longPrimitiveToBoxed() { 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(1) + @externalize(value = 1, pLongNullValue = 13L) Long value; public BoxedLong() {}