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 extends Bean> 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() {}
+ }
+
+}