Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor standard binary serialization #433

Merged
merged 1 commit into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/main/java/org/joda/beans/ser/bin/AbstractBinReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.joda.beans.MetaProperty;
import org.joda.beans.ser.JodaBeanSer;
import org.joda.beans.ser.SerIterable;
import org.joda.beans.ser.SerIteratorFactory;
import org.joda.beans.ser.SerOptional;
import org.joda.beans.ser.SerTypeMapper;

Expand Down Expand Up @@ -209,7 +210,12 @@ private Object parseObjectFromInput(
childIterable = settings.getIteratorFactory().createIterable(parentIterable);
}
if (childIterable == null) {
throw new IllegalArgumentException("Invalid binary data: Unable to create collection type");
// handle array types sent without a metatype
if (effectiveType.isArray()) {
childIterable = SerIteratorFactory.array(effectiveType.getComponentType());
} else {
throw new IllegalArgumentException("Invalid binary data: Unable to create collection type");
}
}
return parseIterable(typeByte, childIterable);
} else {
Expand Down Expand Up @@ -301,7 +307,7 @@ Object parseIterableArray(int typeByte, SerIterable iterable) throws Exception {
Object parseSimple(int typeByte, Class<?> type) throws Exception {
if (isString(typeByte)) {
var text = acceptString(typeByte);
if (type == String.class || type == Object.class) {
if (type == String.class || type.isAssignableFrom(String.class)) {
return text;
}
return settings.getConverter().convertFromString(type, text);
Expand Down
680 changes: 666 additions & 14 deletions src/main/java/org/joda/beans/ser/bin/JodaBeanStandardBinWriter.java

Large diffs are not rendered by default.

29 changes: 11 additions & 18 deletions src/main/java/org/joda/beans/ser/bin/MsgPackOutput.java
Original file line number Diff line number Diff line change
Expand Up @@ -201,36 +201,29 @@ void writeBytes(byte[] bytes) throws IOException {
* @throws IOException if an error occurs
*/
void writeString(String value) throws IOException {
var bytes = toUTF8(value);
// Java 21 performance testing showed manually converting to UTF-8 to be slower
var bytes = value.getBytes(UTF_8);
var size = bytes.length;
if (size < 32) {
output.writeByte(MIN_FIX_STR + size);
} else if (size < 256) {
output.writeByte(STR_8);
output.writeByte(size);
} else if (size < 65536) {
output.writeByte(STR_16);
output.writeShort(size);
} else {
output.writeByte(STR_32);
output.writeInt(size);
writeStringHeaderLarge(size);
}
output.write(bytes);
}

private byte[] toUTF8(String value) {
// inline common ASCII case for much better performance
var size = value.length();
var bytes = new byte[size];
for (var i = 0; i < size; i++) {
var ch = value.charAt(i);
if (ch < 128) {
bytes[i] = (byte) ch;
} else {
return value.getBytes(UTF_8);
}
// separate out larger strings, which may benefit hotspot
private void writeStringHeaderLarge(int size) throws IOException {
if (size < 65536) {
output.writeByte(STR_16);
output.writeShort(size);
} else {
output.writeByte(STR_32);
output.writeInt(size);
}
return bytes;
}

/**
Expand Down
131 changes: 114 additions & 17 deletions src/test/java/org/joda/beans/ser/bin/TestSerializeStandardBin.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@
import org.joda.beans.sample.JodaConvertBean;
import org.joda.beans.sample.JodaConvertWrapper;
import org.joda.beans.sample.Person;
import org.joda.beans.sample.SimpleJson;
import org.joda.beans.ser.JodaBeanSer;
import org.joda.beans.ser.SerTestHelper;
import org.joda.beans.test.BeanAssert;
import org.junit.jupiter.api.Test;

import com.google.common.collect.ImmutableList;
import com.google.common.io.Resources;

/**
Expand All @@ -58,7 +60,7 @@ public void test_writeAddress() throws IOException {

var bytes = JodaBeanSer.PRETTY.binWriter().write(bean);
// System.out.println(JodaBeanBinReader.visualize(bytes));
assertEqualsSerialization(bytes, "/org/joda/beans/ser/Address1.binstr");
assertEqualsSerialization(bytes, "/org/joda/beans/ser/Address2.binstr");

var parsed = (Address) JodaBeanSer.PRETTY.binReader().read(bytes);
BeanAssert.assertBeanEquals(bean, parsed);
Expand All @@ -69,18 +71,20 @@ public void test_writeImmAddress() throws IOException {
var bean = SerTestHelper.testImmAddress(false);
var bytes = JodaBeanSer.PRETTY.binWriter().write(bean);
// System.out.println(JodaBeanBinReader.visualize(bytes));
assertEqualsSerialization(bytes, "/org/joda/beans/ser/ImmAddress1.binstr");
assertEqualsSerialization(bytes, "/org/joda/beans/ser/ImmAddress2.binstr");

var parsed = (ImmAddress) JodaBeanSer.PRETTY.binReader().read(bytes);
BeanAssert.assertBeanEquals(bean, parsed);

// old format in /org/joda/beans/ser/ImmAddress1 is indirectly tested in test_readOldStringArrayWithMetaFormat()
}

@Test
public void test_writeImmOptional() throws IOException {
var bean = SerTestHelper.testImmOptional();
var bytes = JodaBeanSer.PRETTY.withIncludeDerived(true).binWriter().write(bean);
// System.out.println(JodaBeanBinReader.visualize(bytes));
assertEqualsSerialization(bytes, "/org/joda/beans/ser/ImmOptional1.binstr");
assertEqualsSerialization(bytes, "/org/joda/beans/ser/ImmOptional2.binstr");

var parsed = (ImmOptional) JodaBeanSer.PRETTY.binReader().read(bytes);
BeanAssert.assertBeanEquals(bean, parsed);
Expand All @@ -97,22 +101,26 @@ public void test_writeImmArrays() throws IOException {
new boolean[][] {{true, false}, {false}, {}});
var bytes = JodaBeanSer.PRETTY.binWriter().write(bean);
// System.out.println(JodaBeanBinReader.visualize(bytes));
assertEqualsSerialization(bytes, "/org/joda/beans/ser/ImmArrays1.binstr");
assertEqualsSerialization(bytes, "/org/joda/beans/ser/ImmArrays2.binstr");

var parsed = JodaBeanSer.PRETTY.binReader().read(bytes, ImmArrays.class);
BeanAssert.assertBeanEquals(bean, parsed);

// old format in /org/joda/beans/ser/ImmArrays1 is indirectly tested in test_readOldPrimitiveArrayFormat()
}

@Test
public void test_writeCollections() throws IOException {
var bean = SerTestHelper.testCollections();
var bytes = JodaBeanSer.PRETTY.binWriter().write(bean);
// System.out.println(JodaBeanBinReader.visualize(bytes));
assertEqualsSerialization(bytes, "/org/joda/beans/ser/Collections1.binstr");
assertEqualsSerialization(bytes, "/org/joda/beans/ser/Collections2.binstr");

@SuppressWarnings("unchecked")
var parsed = (ImmGuava<String>) JodaBeanSer.PRETTY.binReader().read(bytes);
BeanAssert.assertBeanEquals(bean, parsed);

// old format in /org/joda/beans/ser/Collections1 is indirectly tested in test_readOldListWithMetaFormat()
}

private void assertEqualsSerialization(byte[] actualBytes, String expectedResource) throws IOException {
Expand All @@ -126,7 +134,7 @@ private void assertEqualsSerialization(byte[] actualBytes, String expectedResour
@Test
public void test_writeJodaConvertInterface() {
var bean = SerTestHelper.testGenericInterfaces();

var bytes = JodaBeanSer.COMPACT.binWriter().write(bean);
// System.out.println(JodaBeanBinReader.visualize(bytes));

Expand Down Expand Up @@ -230,7 +238,7 @@ public void test_read_primitiveTypeChanged() throws IOException {
assertThat(parsed.getA()).isCloseTo(6, offset(1e-10));
assertThat(parsed.getB()).isCloseTo(5, offset(1e-10));
}

@Test
public void test_read_optionalTypeToDefaulted() throws IOException {
var baos = new ByteArrayOutputStream();
Expand All @@ -240,7 +248,7 @@ public void test_read_optionalTypeToDefaulted() throws IOException {
out.writeByte(MsgPack.MIN_FIX_MAP);
}
var bytes = baos.toByteArray();

var parsed = JodaBeanSer.COMPACT.binReader().read(bytes, ImmDefault.class);
assertThat(parsed.getValue()).isEqualTo("Defaulted");
}
Expand Down Expand Up @@ -297,6 +305,95 @@ public void test_readWriteJodaConvertBean() throws IOException {
BeanAssert.assertBeanEquals(bean, parsed);
}

//-------------------------------------------------------------------------
@Test
public void test_readOldPrimitiveArrayFormat() throws IOException {
// test format written by v2.12 can still be read
var baos = new ByteArrayOutputStream();
var out = new MsgPackOutput(baos);
out.writeArrayHeader(2);
out.writeInt(1);
out.writeMapHeader(2);
out.writeString("intArray");
out.writeString("1,3,2");
out.writeString("intArray2d");
out.writeArrayHeader(3);
out.writeString("1,2");
out.writeString("2");
out.writeString("");
var bytes = baos.toByteArray();

var expected = ImmArrays.builder()
.intArray(1, 3, 2)
.intArray2d(new int[][] {{1, 2}, {2}, {}})
.build();

var parsed = JodaBeanSer.COMPACT.binReader().read(bytes, ImmArrays.class);
BeanAssert.assertBeanEquals(expected, parsed);
}

@Test
public void test_readOldStringArrayWithMetaFormat() throws IOException {
// test format written by v2.12 can still be read
var baos = new ByteArrayOutputStream();
var out = new MsgPackOutput(baos);
out.writeArrayHeader(2);
out.writeInt(1);
out.writeMapHeader(1);
out.writeString("array2d");
out.writeMapHeader(1);
out.writeExtensionString(MsgPack.JODA_TYPE_META, "String[][]");
out.writeArrayHeader(3);
out.writeMapHeader(1);
out.writeExtensionString(MsgPack.JODA_TYPE_META, "String[]");
out.writeArrayHeader(1);
out.writeString("a");
out.writeMapHeader(1);
out.writeExtensionString(MsgPack.JODA_TYPE_META, "String[]");
out.writeArrayHeader(0);
out.writeMapHeader(1);
out.writeExtensionString(MsgPack.JODA_TYPE_META, "String[]");
out.writeArrayHeader(2);
out.writeString("b");
out.writeString("c");
var bytes = baos.toByteArray();

var expected = SimpleJson.builder()
.array2d(new String[][] {{"a"}, {}, {"b", "c"}})
.build();

var parsed = JodaBeanSer.COMPACT.binReader().read(bytes, SimpleJson.class);
BeanAssert.assertBeanEquals(expected, parsed);
}

@Test
public <T extends Comparable<T>> void test_readOldListWithMetaFormat() throws IOException {
// test format written by v2.12 can still be read (meta caused by List<Comparable> declaration)
var baos = new ByteArrayOutputStream();
var out = new MsgPackOutput(baos);
out.writeArrayHeader(2);
out.writeInt(1);
out.writeMapHeader(1);
out.writeString("list");
out.writeArrayHeader(2);
out.writeMapHeader(1);
out.writeExtensionString(MsgPack.JODA_TYPE_DATA, "String");
out.writeString("A");
out.writeMapHeader(1);
out.writeExtensionString(MsgPack.JODA_TYPE_DATA, "String");
out.writeString("B");
var bytes = baos.toByteArray();

@SuppressWarnings("unchecked")
var list = (ImmutableList<T>) ImmutableList.of("A", "B");
var expected = ImmGuava.<T>builder()
.list(list)
.build();

var parsed = JodaBeanSer.COMPACT.binReader().read(bytes, ImmGuava.class);
BeanAssert.assertBeanEquals(expected, parsed);
}

//-----------------------------------------------------------------------
@Test
public void test_read_nonStandard_JodaConvertWrapper_expanded() throws IOException {
Expand Down Expand Up @@ -357,7 +454,7 @@ public void test_read_invalidFormat_sizeOneArrayAtRoot() throws IOException {
}
var bytes = baos.toByteArray();
assertThatRuntimeException()
.isThrownBy(() -> JodaBeanSer.COMPACT.binReader().read(bytes, FlexiBean.class));
.isThrownBy(() -> JodaBeanSer.COMPACT.binReader().read(bytes, FlexiBean.class));
}

@Test
Expand All @@ -370,7 +467,7 @@ public void test_read_wrongVersion() throws IOException {
}
var bytes = baos.toByteArray();
assertThatRuntimeException()
.isThrownBy(() -> JodaBeanSer.COMPACT.binReader().read(bytes, FlexiBean.class));
.isThrownBy(() -> JodaBeanSer.COMPACT.binReader().read(bytes, FlexiBean.class));
}

@Test
Expand All @@ -395,7 +492,7 @@ public void test_read_rootTypeNotSpecified_Bean() throws IOException {
}
var bytes = baos.toByteArray();
assertThatRuntimeException()
.isThrownBy(() -> JodaBeanSer.COMPACT.binReader().read(bytes, Bean.class));
.isThrownBy(() -> JodaBeanSer.COMPACT.binReader().read(bytes, Bean.class));
}

@Test
Expand Down Expand Up @@ -430,7 +527,7 @@ public void test_read_rootTypeInvalid_Bean() throws IOException {
}
var bytes = baos.toByteArray();
assertThatRuntimeException()
.isThrownBy(() -> JodaBeanSer.COMPACT.binReader().read(bytes, Bean.class));
.isThrownBy(() -> JodaBeanSer.COMPACT.binReader().read(bytes, Bean.class));
}

@Test
Expand All @@ -448,7 +545,7 @@ public void test_read_rootTypeInvalid_incompatible() throws IOException {
}
var bytes = baos.toByteArray();
assertThatRuntimeException()
.isThrownBy(() -> JodaBeanSer.COMPACT.binReader().read(bytes, FlexiBean.class));
.isThrownBy(() -> JodaBeanSer.COMPACT.binReader().read(bytes, FlexiBean.class));
}

@Test
Expand All @@ -466,13 +563,13 @@ public void test_read_invalidFormat_noNilValueAfterType() throws IOException {
}
var bytes = baos.toByteArray();
assertThatRuntimeException()
.isThrownBy(() -> JodaBeanSer.COMPACT.binReader().read(bytes, Bean.class));
.isThrownBy(() -> JodaBeanSer.COMPACT.binReader().read(bytes, Bean.class));
}

@Test
public void test_read_byteArray_nullByteArray() {
assertThatRuntimeException()
.isThrownBy(() -> JodaBeanSer.COMPACT.binReader().read((byte[]) null, Company.class));
assertThatRuntimeException()
.isThrownBy(() -> JodaBeanSer.COMPACT.binReader().read((byte[]) null, Company.class));
}

@Test
Expand All @@ -481,7 +578,7 @@ public void test_write_nullKeyInMap() {
var bean = new Person();
bean.getOtherAddressMap().put(null, address);
assertThatRuntimeException()
.isThrownBy(() -> JodaBeanSer.COMPACT.binWriter().write(bean));
.isThrownBy(() -> JodaBeanSer.COMPACT.binWriter().write(bean));
}

}
Loading