Skip to content

Commit 793b6aa

Browse files
authored
Merge pull request #745 from theigl/744-compatible-super-types
#744 CompatibleFieldSerializer and fields declared as super-types
2 parents 6230bf7 + 6e61f77 commit 793b6aa

File tree

4 files changed

+195
-2
lines changed

4 files changed

+195
-2
lines changed

src/com/esotericsoftware/kryo/serializers/CompatibleFieldSerializer.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.esotericsoftware.kryo.io.Output;
3131
import com.esotericsoftware.kryo.io.OutputChunked;
3232
import com.esotericsoftware.kryo.util.ObjectMap;
33+
import com.esotericsoftware.kryo.util.Util;
3334

3435
/** Serializes objects using direct field assignment, providing both forward and backward compatibility. This means fields can be
3536
* added or removed without invalidating previously serialized bytes. Renaming or changing the type of a field is not supported.
@@ -90,7 +91,7 @@ public void write (Kryo kryo, Output output, T object) {
9091
try {
9192
if (object != null) {
9293
Object value = cachedField.field.get(object);
93-
if (value != null) valueClass = cachedField.field.getType();
94+
if (value != null) valueClass = value.getClass();
9495
}
9596
} catch (IllegalAccessException ex) {
9697
}
@@ -162,7 +163,7 @@ public T read (Kryo kryo, Input input, Class<? extends T> type) {
162163
}
163164

164165
// Ensure the type in the data is compatible with the field type.
165-
if (cachedField.valueClass != null && !cachedField.valueClass.isAssignableFrom(valueClass)) {
166+
if (cachedField.valueClass != null && !Util.isAssignableTo(valueClass, cachedField.valueClass)) {
166167
String message = "Read type is incompatible with the field type: " + className(valueClass) + " -> "
167168
+ className(cachedField.valueClass) + " (" + getType().getName() + "#" + cachedField + ")";
168169
if (!chunked) throw new KryoException(message);

src/com/esotericsoftware/kryo/util/Util.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import com.esotericsoftware.kryo.util.Generics.GenericType;
2828

2929
import java.lang.reflect.Type;
30+
import java.util.HashMap;
31+
import java.util.Map;
3032

3133
/** A few utility methods, mostly for private use.
3234
* @author Nathan Sweet */
@@ -53,6 +55,18 @@ public class Util {
5355
// Maximum reasonable array length. See: https://stackoverflow.com/questions/3038392/do-java-arrays-have-a-maximum-size
5456
public static final int maxArraySize = Integer.MAX_VALUE - 8;
5557

58+
private static final Map<Class<?>, Class<?>> primitiveWrappers = new HashMap<>();
59+
static {
60+
primitiveWrappers.put(boolean.class, Boolean.class);
61+
primitiveWrappers.put(byte.class, Byte.class);
62+
primitiveWrappers.put(char.class, Character.class);
63+
primitiveWrappers.put(double.class, Double.class);
64+
primitiveWrappers.put(float.class, Float.class);
65+
primitiveWrappers.put(int.class, Integer.class);
66+
primitiveWrappers.put(long.class, Long.class);
67+
primitiveWrappers.put(short.class, Short.class);
68+
}
69+
5670
public static boolean isClassAvailable (String className) {
5771
try {
5872
Class.forName(className);
@@ -207,6 +221,18 @@ public static Class getElementClass (Class arrayClass) {
207221
return elementClass;
208222
}
209223

224+
public static boolean isAssignableTo (Class<?> from, Class<?> to) {
225+
if (to.isAssignableFrom(from)) return true;
226+
if (from.isPrimitive()) return isPrimitiveWrapperOf(to, from);
227+
if (to.isPrimitive()) return isPrimitiveWrapperOf(from, to);
228+
return false;
229+
}
230+
231+
private static boolean isPrimitiveWrapperOf (Class<?> targetClass, Class<?> primitive) {
232+
if (!primitive.isPrimitive()) throw new IllegalArgumentException("First argument has to be primitive type");
233+
return primitiveWrappers.get(primitive) == targetClass;
234+
}
235+
210236
public static boolean isAscii (String value) {
211237
for (int i = 0, n = value.length(); i < n; i++)
212238
if (value.charAt(i) > 127) return false;

test/com/esotericsoftware/kryo/serializers/CompatibleFieldSerializerTest.java

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,28 @@
2222
import static org.junit.Assert.*;
2323

2424
import com.esotericsoftware.kryo.Kryo;
25+
import com.esotericsoftware.kryo.KryoException;
2526
import com.esotericsoftware.kryo.KryoTestCase;
2627
import com.esotericsoftware.kryo.SerializerFactory.CompatibleFieldSerializerFactory;
2728

29+
import java.io.Serializable;
30+
import java.util.Arrays;
31+
import java.util.List;
32+
import java.util.Objects;
33+
2834
import org.apache.commons.lang.builder.EqualsBuilder;
35+
import org.junit.Rule;
2936
import org.junit.Test;
37+
import org.junit.rules.ExpectedException;
3038

3139
/** @author Nathan Sweet */
3240
public class CompatibleFieldSerializerTest extends KryoTestCase {
3341
{
3442
supportsCopy = true;
3543
}
3644

45+
@Rule public ExpectedException exceptionRule = ExpectedException.none();
46+
3747
@Test
3848
public void testCompatibleFieldSerializer () {
3949
testCompatibleFieldSerializer(83, false, false);
@@ -206,6 +216,55 @@ private void testRemovedField (int length, boolean references, boolean chunked)
206216
assertEquals(object1, object2);
207217
}
208218

219+
@Test
220+
public void testChangeFieldTypeWithChunkedEncodingEnabled () {
221+
testChangeFieldType(16, true);
222+
}
223+
224+
@Test
225+
public void testChangeFieldTypeWithChunkedEncodingDisabled () {
226+
exceptionRule.expect(KryoException.class);
227+
exceptionRule.expectMessage("Read type is incompatible with the field type: String -> Long");
228+
229+
testChangeFieldType(14, false);
230+
}
231+
232+
private void testChangeFieldType(int length, boolean chunked) {
233+
CompatibleFieldSerializer<AnotherClass> serializer = new CompatibleFieldSerializer<>(kryo, AnotherClass.class);
234+
serializer.getCompatibleFieldSerializerConfig().setChunkedEncoding(chunked);
235+
kryo.setReferences(false);
236+
kryo.register(AnotherClass.class, serializer);
237+
238+
roundTrip(length, new AnotherClass("Hacker"));
239+
240+
serializer.getField("value").setValueClass(Long.class);
241+
242+
final AnotherClass o = (AnotherClass)kryo.readClassAndObject(input);
243+
assertNull(o.value);
244+
}
245+
246+
@Test
247+
public void testChangePrimitiveAndWrapperFieldTypes () {
248+
testChangePrimitiveAndWrapperFieldTypes(26, true);
249+
testChangePrimitiveAndWrapperFieldTypes(22, false);
250+
}
251+
252+
private void testChangePrimitiveAndWrapperFieldTypes (int length, boolean chunked) {
253+
CompatibleFieldSerializer<ClassWithPrimitiveAndWrapper> serializer = new CompatibleFieldSerializer<>(kryo, ClassWithPrimitiveAndWrapper.class);
254+
serializer.getCompatibleFieldSerializerConfig().setChunkedEncoding(chunked);
255+
kryo.setReferences(false);
256+
kryo.register(ClassWithPrimitiveAndWrapper.class, serializer);
257+
258+
roundTrip(length, new ClassWithPrimitiveAndWrapper(1, 1L));
259+
260+
serializer.getField("primitive").setValueClass(Long.class);
261+
serializer.getField("wrapper").setValueClass(long.class);
262+
263+
ClassWithPrimitiveAndWrapper o = (ClassWithPrimitiveAndWrapper)kryo.readClassAndObject(input);
264+
assertEquals(1, o.primitive);
265+
assertEquals(1L, o.wrapper, 0);
266+
}
267+
209268
@Test
210269
public void testRemovedFieldFromClassWithManyFields () {
211270
testRemovedFieldFromClassWithManyFields(198, false, false, true);
@@ -379,6 +438,21 @@ private void testExtendedClass (int length, boolean references, boolean chunked)
379438
assertEquals(extendedObject, object2);
380439
}
381440

441+
@Test
442+
public void testClassWithSuperTypeFields() {
443+
kryo.setReferences(false);
444+
kryo.setRegistrationRequired(false);
445+
446+
CompatibleFieldSerializer<ClassWithSuperTypeFields> serializer = new CompatibleFieldSerializer<>(kryo,
447+
ClassWithSuperTypeFields.class);
448+
CompatibleFieldSerializer.CompatibleFieldSerializerConfig config = serializer.getCompatibleFieldSerializerConfig();
449+
config.setChunkedEncoding(true);
450+
config.setReadUnknownFieldData(true);
451+
kryo.register(ClassWithSuperTypeFields.class, serializer);
452+
453+
roundTrip(71, new ClassWithSuperTypeFields("foo", Arrays.asList("bar"), "baz"));
454+
}
455+
382456
public static class TestClass {
383457
public String text = "something";
384458
public int moo = 120;
@@ -436,6 +510,26 @@ public boolean equals (Object obj) {
436510

437511
public static class AnotherClass {
438512
String value;
513+
514+
public AnotherClass () {
515+
}
516+
517+
public AnotherClass (String value) {
518+
this.value = value;
519+
}
520+
521+
@Override
522+
public boolean equals (Object o) {
523+
if (this == o) return true;
524+
if (o == null || getClass() != o.getClass()) return false;
525+
final AnotherClass that = (AnotherClass)o;
526+
return Objects.equals(value, that.value);
527+
}
528+
529+
@Override
530+
public int hashCode () {
531+
return Objects.hash(value);
532+
}
439533
}
440534

441535
public static class ClassWithManyFields {
@@ -493,4 +587,59 @@ public boolean equals (Object obj) {
493587
return false;
494588
}
495589
}
590+
591+
public static class ClassWithPrimitiveAndWrapper {
592+
long primitive;
593+
Long wrapper;
594+
595+
public ClassWithPrimitiveAndWrapper () {
596+
}
597+
598+
public ClassWithPrimitiveAndWrapper (long primitive, Long wrapper) {
599+
this.primitive = primitive;
600+
this.wrapper = wrapper;
601+
}
602+
603+
@Override
604+
public boolean equals (Object o) {
605+
if (this == o) return true;
606+
if (o == null || getClass() != o.getClass()) return false;
607+
final ClassWithPrimitiveAndWrapper that = (ClassWithPrimitiveAndWrapper)o;
608+
return primitive == that.primitive && Objects.equals(wrapper, that.wrapper);
609+
}
610+
611+
@Override
612+
public int hashCode () {
613+
return Objects.hash(primitive, wrapper);
614+
}
615+
}
616+
617+
public static class ClassWithSuperTypeFields {
618+
private Object value;
619+
private Iterable<?> list;
620+
private Serializable serializable;
621+
622+
public ClassWithSuperTypeFields () {
623+
}
624+
625+
public ClassWithSuperTypeFields (Object value, List<?> list, Serializable serializable) {
626+
this.value = value;
627+
this.list = list;
628+
this.serializable = serializable;
629+
}
630+
631+
@Override
632+
public boolean equals (Object o) {
633+
if (this == o) return true;
634+
if (o == null || getClass() != o.getClass()) return false;
635+
ClassWithSuperTypeFields that = (ClassWithSuperTypeFields)o;
636+
return Objects.equals(value, that.value) && Objects.equals(list, that.list)
637+
&& Objects.equals(serializable, that.serializable);
638+
}
639+
640+
@Override
641+
public int hashCode () {
642+
return Objects.hash(value, list, serializable);
643+
}
644+
}
496645
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.esotericsoftware.kryo.util;
2+
3+
import junit.framework.TestCase;
4+
5+
public class UtilTest extends TestCase {
6+
7+
public void testIsAssignableTo() {
8+
assertTrue(Util.isAssignableTo(Long.class, long.class));
9+
assertTrue(Util.isAssignableTo(long.class, Long.class));
10+
assertTrue(Util.isAssignableTo(Long.class, Long.class));
11+
assertTrue(Util.isAssignableTo(long.class, long.class));
12+
13+
assertFalse(Util.isAssignableTo(String.class, Long.class));
14+
assertFalse(Util.isAssignableTo(String.class, long.class));
15+
}
16+
17+
}

0 commit comments

Comments
 (0)