Add ObjectSerialization class for advanced object serialization and deserialization with options for compression, Base64 encoding, and digest verification; include unit tests for various serialization scenarios
This commit is contained in:
15
pom.xml
15
pom.xml
@@ -11,6 +11,7 @@
|
|||||||
<maven.compiler.source>25</maven.compiler.source>
|
<maven.compiler.source>25</maven.compiler.source>
|
||||||
<maven.compiler.target>25</maven.compiler.target>
|
<maven.compiler.target>25</maven.compiler.target>
|
||||||
<spring-boot.version>4.0.1</spring-boot.version>
|
<spring-boot.version>4.0.1</spring-boot.version>
|
||||||
|
<zstd-jni.version>1.5.7-6</zstd-jni.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
@@ -30,13 +31,25 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter</artifactId>
|
<artifactId>spring-boot-starter</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.luben</groupId>
|
||||||
|
<artifactId>zstd-jni</artifactId>
|
||||||
|
<version>${zstd-jni.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-jackson</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tools.jackson.dataformat</groupId>
|
||||||
|
<artifactId>jackson-dataformat-yaml</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.awaitility</groupId>
|
<groupId>org.awaitility</groupId>
|
||||||
<artifactId>awaitility</artifactId>
|
<artifactId>awaitility</artifactId>
|
||||||
|
|||||||
135
src/main/java/de/extio/game_engine/util/ObjectSerialization.java
Normal file
135
src/main/java/de/extio/game_engine/util/ObjectSerialization.java
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package de.extio.game_engine.util;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.security.DigestInputStream;
|
||||||
|
import java.security.DigestOutputStream;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import com.github.luben.zstd.ZstdInputStream;
|
||||||
|
import com.github.luben.zstd.ZstdOutputStream;
|
||||||
|
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.dataformat.yaml.YAMLMapper;
|
||||||
|
|
||||||
|
public final class ObjectSerialization {
|
||||||
|
|
||||||
|
private static final ObjectMapper OBJECT_MAPPER;
|
||||||
|
|
||||||
|
static {
|
||||||
|
OBJECT_MAPPER = YAMLMapper.builder()
|
||||||
|
.findAndAddModules()
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] serialize(final Object o, final boolean compress, final boolean base64, final boolean digest, final byte[] digestSalt, final Consumer<byte[]> digestConsumer) {
|
||||||
|
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(1 << 16);
|
||||||
|
OutputStream base64OutputStream = null;
|
||||||
|
ZstdOutputStream zstdOutputStream = null;
|
||||||
|
DigestOutputStream digestOutputStream = null;
|
||||||
|
try {
|
||||||
|
OutputStream stream = byteArrayOutputStream;
|
||||||
|
if (base64) {
|
||||||
|
stream = base64OutputStream = Base64.getEncoder().wrap(stream);
|
||||||
|
}
|
||||||
|
if (compress) {
|
||||||
|
stream = zstdOutputStream = new ZstdOutputStream(stream);
|
||||||
|
}
|
||||||
|
if (digest) {
|
||||||
|
stream = digestOutputStream = new DigestOutputStream(stream, MessageDigest.getInstance("SHA-256"));
|
||||||
|
}
|
||||||
|
OBJECT_MAPPER.writeValue(stream, o);
|
||||||
|
}
|
||||||
|
catch (IOException | NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (digestOutputStream != null) {
|
||||||
|
try {
|
||||||
|
digestOutputStream.close();
|
||||||
|
}
|
||||||
|
catch (final IOException e) {
|
||||||
|
throw new RuntimeException(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
digestConsumer.accept(digestOutputStream.getMessageDigest().digest(digestSalt));
|
||||||
|
}
|
||||||
|
if (zstdOutputStream != null) {
|
||||||
|
try {
|
||||||
|
zstdOutputStream.close();
|
||||||
|
}
|
||||||
|
catch (final IOException e) {
|
||||||
|
throw new RuntimeException(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (base64OutputStream != null) {
|
||||||
|
try {
|
||||||
|
base64OutputStream.close();
|
||||||
|
}
|
||||||
|
catch (final IOException e) {
|
||||||
|
throw new RuntimeException(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return byteArrayOutputStream.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Object> T deserialize(final Class<T> clazz, final byte[] data, final boolean deserializeObjRefs, final boolean decompress, final boolean base64, final byte[] checkDigest, final byte[] digestSalt, final Consumer<Boolean> digestCheckConsumer) {
|
||||||
|
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
|
||||||
|
InputStream base64InputStream = null;
|
||||||
|
ZstdInputStream zstdInputStream = null;
|
||||||
|
DigestInputStream digestInputStream = null;
|
||||||
|
try {
|
||||||
|
InputStream stream = byteArrayInputStream;
|
||||||
|
if (base64) {
|
||||||
|
stream = base64InputStream = Base64.getDecoder().wrap(stream);
|
||||||
|
}
|
||||||
|
if (decompress) {
|
||||||
|
stream = zstdInputStream = new ZstdInputStream(stream);
|
||||||
|
}
|
||||||
|
if (checkDigest != null && checkDigest.length > 0) {
|
||||||
|
stream = digestInputStream = new DigestInputStream(stream, MessageDigest.getInstance("SHA-256"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return OBJECT_MAPPER.readValue(stream, clazz);
|
||||||
|
}
|
||||||
|
catch (NoSuchAlgorithmException | IOException e) {
|
||||||
|
throw new RuntimeException(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (base64InputStream != null) {
|
||||||
|
try {
|
||||||
|
base64InputStream.close();
|
||||||
|
}
|
||||||
|
catch (final IOException e) {
|
||||||
|
throw new RuntimeException(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (zstdInputStream != null) {
|
||||||
|
try {
|
||||||
|
zstdInputStream.close();
|
||||||
|
}
|
||||||
|
catch (final IOException e) {
|
||||||
|
throw new RuntimeException(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (digestInputStream != null) {
|
||||||
|
try {
|
||||||
|
digestInputStream.close();
|
||||||
|
digestCheckConsumer.accept(Arrays.equals(checkDigest, digestInputStream.getMessageDigest().digest(digestSalt)));
|
||||||
|
}
|
||||||
|
catch (final IOException e) {
|
||||||
|
throw new RuntimeException(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package de.extio.game_engine.util;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ObjectSerializationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSerializeDeserializeBasic() {
|
||||||
|
var serialization = new ObjectSerialization();
|
||||||
|
var original = new TestObject("Hello", 42, 3.14, List.of("tag1", "tag2"), List.of(new TestObject2("key", true), new TestObject2("key2", false)), Map.of("attr", 1, "attr2", 2));
|
||||||
|
|
||||||
|
var serialized = serialization.serialize(original, false, false, false, null, digest -> {
|
||||||
|
});
|
||||||
|
|
||||||
|
System.out.println("Serialized length: " + serialized.length);
|
||||||
|
System.out.println("" + new String(serialized));
|
||||||
|
assertNotNull(serialized);
|
||||||
|
assertTrue(serialized.length > 0);
|
||||||
|
|
||||||
|
var deserialized = serialization.deserialize(TestObject.class, serialized, false, false, false, null, null, result -> {
|
||||||
|
});
|
||||||
|
|
||||||
|
assertNotNull(deserialized);
|
||||||
|
assertEquals(original.name, deserialized.name);
|
||||||
|
assertEquals(original.id, deserialized.id);
|
||||||
|
assertEquals(original.value, deserialized.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSerializeDeserializeWithCompression() {
|
||||||
|
var serialization = new ObjectSerialization();
|
||||||
|
var original = new TestObject("Compressed", 100, 2.71, List.of("a", "b", "c"), List.of(), Map.of());
|
||||||
|
|
||||||
|
var serialized = serialization.serialize(original, true, false, false, null, digest -> {
|
||||||
|
});
|
||||||
|
|
||||||
|
assertNotNull(serialized);
|
||||||
|
assertTrue(serialized.length > 0);
|
||||||
|
|
||||||
|
var deserialized = serialization.deserialize(TestObject.class, serialized, false, true, false, null, null, result -> {
|
||||||
|
});
|
||||||
|
|
||||||
|
assertNotNull(deserialized);
|
||||||
|
assertEquals(original.name, deserialized.name);
|
||||||
|
assertEquals(original.id, deserialized.id);
|
||||||
|
assertEquals(original.value, deserialized.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSerializeDeserializeWithBase64() {
|
||||||
|
var serialization = new ObjectSerialization();
|
||||||
|
var original = new TestObject("Base64", 200, 1.41, List.of(), List.of(), Map.of());
|
||||||
|
|
||||||
|
var serialized = serialization.serialize(original, false, true, false, null, digest -> {
|
||||||
|
});
|
||||||
|
|
||||||
|
assertNotNull(serialized);
|
||||||
|
assertTrue(serialized.length > 0);
|
||||||
|
assertTrue(new String(serialized).matches("[A-Za-z0-9+/=]+"));
|
||||||
|
|
||||||
|
var deserialized = serialization.deserialize(TestObject.class, serialized, false, false, true, null, null, result -> {
|
||||||
|
});
|
||||||
|
|
||||||
|
assertNotNull(deserialized);
|
||||||
|
assertEquals(original.name, deserialized.name);
|
||||||
|
assertEquals(original.id, deserialized.id);
|
||||||
|
assertEquals(original.value, deserialized.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSerializeDeserializeWithDigest() {
|
||||||
|
var serialization = new ObjectSerialization();
|
||||||
|
var original = new TestObject("Digested", 300, 0.5, List.of("x"), List.of(), Map.of());
|
||||||
|
var digestSalt = "salt".getBytes();
|
||||||
|
var capturedDigest = new byte[32][];
|
||||||
|
|
||||||
|
var serialized = serialization.serialize(original, false, false, true, digestSalt, digest -> capturedDigest[0] = digest);
|
||||||
|
|
||||||
|
assertNotNull(serialized);
|
||||||
|
assertNotNull(capturedDigest[0]);
|
||||||
|
assertTrue(capturedDigest[0].length == 32);
|
||||||
|
|
||||||
|
var digestCheckPassed = new boolean[1];
|
||||||
|
var deserialized = serialization.deserialize(TestObject.class, serialized, false, false, false, capturedDigest[0], digestSalt, result -> digestCheckPassed[0] = result);
|
||||||
|
|
||||||
|
assertNotNull(deserialized);
|
||||||
|
assertTrue(digestCheckPassed[0]);
|
||||||
|
assertEquals(original.name, deserialized.name);
|
||||||
|
assertEquals(original.id, deserialized.id);
|
||||||
|
assertEquals(original.value, deserialized.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSerializeDeserializeWithCompressionAndBase64() {
|
||||||
|
var serialization = new ObjectSerialization();
|
||||||
|
var original = new TestObject("CompressedBase64", 400, 1.23, List.of("p", "q"), List.of(new TestObject2("nested", true)), Map.of("key", 99));
|
||||||
|
|
||||||
|
var serialized = serialization.serialize(original, true, true, false, null, digest -> {
|
||||||
|
});
|
||||||
|
|
||||||
|
assertNotNull(serialized);
|
||||||
|
assertTrue(serialized.length > 0);
|
||||||
|
assertTrue(new String(serialized).matches("[A-Za-z0-9+/=]+"));
|
||||||
|
|
||||||
|
var deserialized = serialization.deserialize(TestObject.class, serialized, false, true, true, null, null, result -> {
|
||||||
|
});
|
||||||
|
|
||||||
|
assertNotNull(deserialized);
|
||||||
|
assertEquals(original.name, deserialized.name);
|
||||||
|
assertEquals(original.id, deserialized.id);
|
||||||
|
assertEquals(original.value, deserialized.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSerializeDeserializeWithAllOptions() {
|
||||||
|
var serialization = new ObjectSerialization();
|
||||||
|
var original = new TestObject("AllOptions", 500, 9.99, List.of("all"), List.of(new TestObject2("a", false), new TestObject2("b", true)), Map.of("x", 1, "y", 2));
|
||||||
|
var digestSalt = "salt123".getBytes();
|
||||||
|
var capturedDigest = new byte[32][];
|
||||||
|
|
||||||
|
var serialized = serialization.serialize(original, true, true, true, digestSalt, digest -> capturedDigest[0] = digest);
|
||||||
|
|
||||||
|
assertNotNull(serialized);
|
||||||
|
assertNotNull(capturedDigest[0]);
|
||||||
|
assertTrue(new String(serialized).matches("[A-Za-z0-9+/=]+"));
|
||||||
|
|
||||||
|
var digestCheckPassed = new boolean[1];
|
||||||
|
var deserialized = serialization.deserialize(TestObject.class, serialized, false, true, true, capturedDigest[0], digestSalt, result -> digestCheckPassed[0] = result);
|
||||||
|
|
||||||
|
assertNotNull(deserialized);
|
||||||
|
assertTrue(digestCheckPassed[0]);
|
||||||
|
assertEquals(original.name, deserialized.name);
|
||||||
|
assertEquals(original.id, deserialized.id);
|
||||||
|
assertEquals(original.value, deserialized.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record TestObject(String name, int id, double value, List<String> tags, List<TestObject2> children, Map<String, Integer> attributes) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record TestObject2(String key, boolean flag) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user