Skip to content

Commit

Permalink
Merge pull request EsotericSoftware#362 from magro/java8-support
Browse files Browse the repository at this point in the history
Add java8 support and serializer for java.util.Optional
  • Loading branch information
magro committed Feb 27, 2016
2 parents 7006429 + 0130c42 commit 785cd88
Show file tree
Hide file tree
Showing 16 changed files with 214 additions and 22 deletions.
23 changes: 23 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,29 @@
</plugins>
</build>
</profile>
<profile>
<id>until-java8</id>
<activation>
<!-- Use exclusive 1.8 range instead of inclusive 1.7, because an upper bound ",1.7]" is likely not to include most releases of 1.7,
since they will have an additional "patch" release such as _05 that is not taken into consideration in the above range.
See also http://maven.apache.org/guides/introduction/introduction-to-profiles.html#Details_on_profile_activation -->
<jdk>[1.5,1.8)</jdk>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19</version>
<configuration>
<excludes>
<exclude>**/java8/*Test.java</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>

<repositories>
Expand Down
4 changes: 3 additions & 1 deletion src/com/esotericsoftware/kryo/Kryo.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import java.util.TreeMap;
import java.util.TreeSet;

import com.esotericsoftware.kryo.serializers.java8.OptionalSerializer;
import org.objenesis.instantiator.ObjectInstantiator;
import org.objenesis.strategy.InstantiatorStrategy;
import org.objenesis.strategy.SerializingInstantiatorStrategy;
Expand Down Expand Up @@ -115,7 +116,7 @@ public class Kryo {
static private final int NO_REF = -2;

private SerializerFactory defaultSerializer = new ReflectionSerializerFactory(FieldSerializer.class);
private final ArrayList<DefaultSerializerEntry> defaultSerializers = new ArrayList(32);
private final ArrayList<DefaultSerializerEntry> defaultSerializers = new ArrayList(33);
private final int lowPriorityDefaultSerializerCount;

private final ClassResolver classResolver;
Expand Down Expand Up @@ -210,6 +211,7 @@ public Kryo (ClassResolver classResolver, ReferenceResolver referenceResolver, S
addDefaultSerializer(TimeZone.class, TimeZoneSerializer.class);
addDefaultSerializer(Calendar.class, CalendarSerializer.class);
addDefaultSerializer(Locale.class, LocaleSerializer.class);
OptionalSerializer.addDefaultSerializer(this);
lowPriorityDefaultSerializerCount = defaultSerializers.size();

// Primitives and string. Primitive wrappers automatically use the same registration as primitives.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.esotericsoftware.kryo.serializers.java8;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.Serializer;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;

import java.util.Optional;

import static com.esotericsoftware.minlog.Log.DEBUG;
import static com.esotericsoftware.minlog.Log.debug;

/**
* Serializer for {@link Optional}.
*/
public class OptionalSerializer extends Serializer<Optional> {

private static final boolean OPTIONAL_SUPPORTED;

static {
boolean supported = false;
try {
Class.forName("java.util.Optional");
supported = true;
} catch (Exception e) {
if (DEBUG) {
debug("Class 'java.util.Optional' not found, 'OptionalSerializer' won't be registered as default serializer.");
}
}
OPTIONAL_SUPPORTED = supported;
}

{
setAcceptsNull(false);
}

@Override
@SuppressWarnings("unchecked")
public void write(Kryo kryo, Output output, Optional object) {
Object nullable = object.isPresent() ? object.get() : null;
kryo.writeClassAndObject(output, nullable);
}

@Override
public Optional read(Kryo kryo, Input input, Class<Optional> type) {
return Optional.ofNullable(kryo.readClassAndObject(input));
}

@Override
public Optional copy(Kryo kryo, Optional original) {
if(original.isPresent()) {
return Optional.of(kryo.copy(original.get()));
}
return original;
}

public static void addDefaultSerializer(Kryo kryo) {
if(OPTIONAL_SUPPORTED)
kryo.addDefaultSerializer(Optional.class, OptionalSerializer.class);
}
}
85 changes: 64 additions & 21 deletions test/com/esotericsoftware/kryo/SerializationCompatTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package com.esotericsoftware.kryo;

import com.esotericsoftware.kryo.SerializationCompatTestData.TestData;
import com.esotericsoftware.kryo.SerializationCompatTestData.TestDataJava8;
import com.esotericsoftware.kryo.io.*;
import com.esotericsoftware.minlog.Log;
import org.objenesis.strategy.StdInstantiatorStrategy;
Expand All @@ -30,6 +31,7 @@
import java.io.FileOutputStream;
import java.lang.reflect.Field;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;

Expand Down Expand Up @@ -69,6 +71,14 @@
public class SerializationCompatTest extends KryoTestCase {

private static final String ENDIANNESS = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN ? "le" : "be";
private static final int JAVA_VERSION = Integer.parseInt(System.getProperty("java.version").split("\\.")[1]);
private static final int EXPECTED_DEFAULT_SERIALIZER_COUNT = JAVA_VERSION < 8 ? 33 : 34;
private static final List<TestDataDescription<?>> TEST_DATAS = new ArrayList<TestDataDescription<?>>();

static {
TEST_DATAS.add(new TestDataDescription<TestData>("3.0.0", new TestData(), 1646, 1754));
if(JAVA_VERSION >= 8) TEST_DATAS.add(new TestDataDescription<TestDataJava8>("3.1.0", new TestDataJava8(), 1656, 1764));
};

@Override
protected void setUp() throws Exception {
Expand All @@ -94,11 +104,11 @@ public void testDefaultSerializers() throws Exception {
" type of the new default serializer.\n" +
"After that's done, you must create new versions of 'test/resources/data*'" +
" because the new TestData instance will no longer be equals the formerly written/serialized one.",
33, defaultSerializers.size());
EXPECTED_DEFAULT_SERIALIZER_COUNT, defaultSerializers.size());
}

public void testStandard () throws Exception {
runTest(
runTests(
"standard",
new Function1<File, Input>() {
public Input apply(File file) throws FileNotFoundException {
Expand All @@ -114,7 +124,7 @@ public Output apply(File file) throws Exception {
}

public void testByteBuffer () throws Exception {
runTest(
runTests(
"bytebuffer",
new Function1<File, Input>() {
public Input apply(File file) throws FileNotFoundException {
Expand All @@ -130,7 +140,7 @@ public Output apply(File file) throws Exception {
}

public void testFast () throws Exception {
runTest(
runTests(
"fast",
new Function1<File, Input>() {
public Input apply(File file) throws FileNotFoundException {
Expand All @@ -146,7 +156,7 @@ public Output apply(File file) throws Exception {
}

public void testUnsafe () throws Exception {
runTest(
runTests(
"unsafe-" + ENDIANNESS,
new Function1<File, Input>() {
public Input apply(File file) throws FileNotFoundException {
Expand All @@ -162,7 +172,7 @@ public Output apply(File file) throws Exception {
}

public void testUnsafeMemory () throws Exception {
runTest(
runTests(
"unsafeMemory-" + ENDIANNESS,
new Function1<File, Input>() {
public Input apply(File file) throws FileNotFoundException {
Expand All @@ -177,40 +187,51 @@ public Output apply(File file) throws Exception {
);
}

private void runTest(String variant, Function1<File, Input> inputFactory, Function1<File, Output> outputFactory) throws Exception {
File file = new File("test/resources/data-"+ variant +".ser");
private void runTests(String variant, Function1<File, Input> inputFactory, Function1<File, Output> outputFactory) throws Exception {
for(TestDataDescription description : TEST_DATAS) {
runTest(description, variant, inputFactory, outputFactory);
}
}

private void runTest(TestDataDescription description, String variant, Function1<File, Input> inputFactory, Function1<File, Output> outputFactory) throws Exception {
File file = new File("test/resources/"+ description.classSimpleName() +"-"+ variant +".ser");
file.getParentFile().mkdirs();

if(file.exists()) {
Log.info("Reading and testing data with mode '"+ variant +"' from file " + file.getAbsolutePath());
Log.info("Reading and testing "+ description.classSimpleName() +" with mode '"+ variant +"' from file " + file.getAbsolutePath());
Input in = inputFactory.apply(file);
readAndRunTest(in);
readAndRunTest(description, in);
in.close();
}
else {
Log.info("Testing and writing data with mode '"+ variant +"' to file " + file.getAbsolutePath());
Log.info("Testing and writing "+ description.classSimpleName() +" with mode '"+ variant +"' to file " + file.getAbsolutePath());
Output out = outputFactory.apply(file);
runTestAndWrite(out);
out.close();
try {
runTestAndWrite(description, out);
out.close();
} catch (Exception e) {
// if anything failed (e.g. the initial test), we should delete the file as it may be empty or corruped
out.close();
file.delete();
}
}
}

private void readAndRunTest (Input in) throws FileNotFoundException {
TestData actual = kryo.readObject(in, TestData.class);
roundTrip(1646, 1754, actual);
private void readAndRunTest (TestDataDescription<?> description, Input in) throws FileNotFoundException {
TestData actual = kryo.readObject(in, description.testDataClass());
roundTrip(description.length, description.unsafeLength, actual);
try {
assertReflectionEquals(actual, new TestData());
assertReflectionEquals(actual, description.testData);
} catch(AssertionError e) {
Log.info("Serialization format is broken, please check " + getClass().getSimpleName() + "'s class doc to see" +
" what this means and how to proceed.");
throw e;
}
}

private void runTestAndWrite (Output out) throws FileNotFoundException {
TestData testData = new TestData();
roundTrip(1646, 1754, testData);
kryo.writeObject(out, testData);
private void runTestAndWrite (TestDataDescription<?> description, Output out) throws FileNotFoundException {
roundTrip(description.length, description.unsafeLength, description.testData);
kryo.writeObject(out, description.testData);
}

@Override
Expand All @@ -225,5 +246,27 @@ protected void doAssertEquals( final Object one, final Object another ) {
private interface Function1<A, B> {
B apply(A input) throws Exception;
}

private static class TestDataDescription<T extends TestData> {
private final String kryoVersion;
private final T testData;
private final int length;
private final int unsafeLength;
TestDataDescription(String kryoVersion, T testData, int length, int unsafeLength) {
this.kryoVersion = kryoVersion;
this.testData = testData;
this.length = length;
this.unsafeLength = unsafeLength;
}

Class<T> testDataClass() {
return (Class<T>) testData.getClass();
}

String classSimpleName() {
return testData.getClass().getSimpleName();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@
/** Testdata for serialization compatibility check. */
class SerializationCompatTestData {

static class TestDataJava8 extends TestData {
private Optional<String> _optionalString;

TestDataJava8() {
_optionalString = Optional.of("foo");
}
}

public static class TestData implements Serializable {

private boolean _boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.esotericsoftware.kryo.serializers.java8;

import com.esotericsoftware.kryo.KryoTestCase;

import java.util.Objects;
import java.util.Optional;

public class OptionalSerializerTest extends KryoTestCase {

{
supportsCopy = true;
}

@Override
protected void setUp() throws Exception {
super.setUp();
kryo.register(Optional.class);
kryo.register(TestClass.class);
}

public void testNull() {
roundTrip(2, 2, new TestClass(null));
}

public void testEmpty() {
roundTrip(3, 3, new TestClass(Optional.<String>empty()));
}

public void testPresent() {
roundTrip(6, 6, new TestClass(Optional.of("foo")));
}

static class TestClass {
Optional<String> maybe;
public TestClass() {}
public TestClass(Optional<String> maybe) {
this.maybe = maybe;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestClass testClass = (TestClass) o;
return Objects.equals(maybe, testClass.maybe);

}

@Override
public int hashCode() {
return Objects.hashCode(maybe);
}
}

}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file added test/resources/TestDataJava8-bytebuffer.ser
Binary file not shown.
Binary file added test/resources/TestDataJava8-fast.ser
Binary file not shown.
Binary file added test/resources/TestDataJava8-standard.ser
Binary file not shown.
Binary file added test/resources/TestDataJava8-unsafe-le.ser
Binary file not shown.
Binary file added test/resources/TestDataJava8-unsafeMemory-le.ser
Binary file not shown.

0 comments on commit 785cd88

Please sign in to comment.