diff --git a/msgpack-jackson/README.md b/msgpack-jackson/README.md index d3c77986..7e859781 100644 --- a/msgpack-jackson/README.md +++ b/msgpack-jackson/README.md @@ -240,9 +240,27 @@ When you want to use non-String value as a key of Map, use `MessagePackKeySerial System.out.println(mapper.readValue(converted, Pojo.class)); // => Pojo{value=1234567890.98765432100} ``` +### Serialize and deserialize Instant instances as MessagePack extension type + +`timestamp` extension type is defined in MessagePack as type:-1. Registering `TimestampExtensionModule.INSTANCE` module enables automatic serialization and deserialization of java.time.Instant to/from the MessagePack extension type. + +```java + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()) + .registerModule(TimestampExtensionModule.INSTANCE); + Pojo pojo = new Pojo(); + // The type of `timestamp` variable is Instant + pojo.timestamp = Instant.now(); + byte[] bytes = objectMapper.writeValueAsBytes(pojo); + + // The Instant instance is serialized as MessagePack extension type (type: -1) + + Pojo deserialized = objectMapper.readValue(bytes, Pojo.class); + System.out.println(deserialized); // "2022-09-14T08:47:24.922Z" +``` + ### Deserialize extension types with ExtensionTypeCustomDeserializers -`ExtensionTypeCustomDeserializers` helps you to deserialize extension types easily. +`ExtensionTypeCustomDeserializers` helps you to deserialize your own custom extension types easily. #### Deserialize extension type value directly diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java new file mode 100755 index 00000000..2216d276 --- /dev/null +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java @@ -0,0 +1,82 @@ +package org.msgpack.jackson.dataformat; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import org.msgpack.core.ExtensionTypeHeader; +import org.msgpack.core.MessagePack; +import org.msgpack.core.MessagePacker; +import org.msgpack.core.MessageUnpacker; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.Instant; + +public class TimestampExtensionModule +{ + public static final byte EXT_TYPE = -1; + public static final SimpleModule INSTANCE = new SimpleModule("msgpack-ext-timestamp"); + + static { + INSTANCE.addSerializer(Instant.class, new InstantSerializer(Instant.class)); + INSTANCE.addDeserializer(Instant.class, new InstantDeserializer(Instant.class)); + } + + private static class InstantSerializer extends StdSerializer + { + protected InstantSerializer(Class t) + { + super(t); + } + + @Override + public void serialize(Instant value, JsonGenerator gen, SerializerProvider provider) + throws IOException + { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + // MEMO: Reusing these MessagePacker and MessageUnpacker instances would improve the performance + try (MessagePacker packer = MessagePack.newDefaultPacker(os)) { + packer.packTimestamp(value); + } + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(os.toByteArray())) { + ExtensionTypeHeader header = unpacker.unpackExtensionTypeHeader(); + byte[] bytes = unpacker.readPayload(header.getLength()); + + MessagePackExtensionType extensionType = new MessagePackExtensionType(EXT_TYPE, bytes); + gen.writeObject(extensionType); + } + } + } + + private static class InstantDeserializer extends StdDeserializer + { + protected InstantDeserializer(Class vc) + { + super(vc); + } + + @Override + public Instant deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException + { + MessagePackExtensionType ext = p.readValueAs(MessagePackExtensionType.class); + if (ext.getType() != EXT_TYPE) { + throw new RuntimeException( + String.format("Unexpected extension type (0x%X) for Instant object", ext.getType())); + } + + // MEMO: Reusing this MessageUnpacker instance would improve the performance + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(ext.getData())) { + return unpacker.unpackTimestamp(new ExtensionTypeHeader(EXT_TYPE, ext.getData().length)); + } + } + } + + private TimestampExtensionModule() + { + } +} diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java new file mode 100755 index 00000000..05851dbc --- /dev/null +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java @@ -0,0 +1,218 @@ +// +// MessagePack for Java +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +package org.msgpack.jackson.dataformat; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Before; +import org.junit.Test; +import org.msgpack.core.MessagePack; +import org.msgpack.core.MessagePacker; +import org.msgpack.core.MessageUnpacker; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.Instant; + +import static org.junit.Assert.assertEquals; + +public class TimestampExtensionModuleTest +{ + private final ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + private final SingleInstant singleInstant = new SingleInstant(); + private final TripleInstants tripleInstants = new TripleInstants(); + + private static class SingleInstant + { + public Instant instant; + } + + private static class TripleInstants + { + public Instant a; + public Instant b; + public Instant c; + } + + @Before + public void setUp() + throws Exception + { + objectMapper.registerModule(TimestampExtensionModule.INSTANCE); + } + + @Test + public void testSingleInstantPojo() + throws IOException + { + singleInstant.instant = Instant.now(); + byte[] bytes = objectMapper.writeValueAsBytes(singleInstant); + SingleInstant deserialized = objectMapper.readValue(bytes, SingleInstant.class); + assertEquals(singleInstant.instant, deserialized.instant); + } + + @Test + public void testTripleInstantsPojo() + throws IOException + { + Instant now = Instant.now(); + tripleInstants.a = now.minusSeconds(1); + tripleInstants.b = now; + tripleInstants.c = now.plusSeconds(1); + byte[] bytes = objectMapper.writeValueAsBytes(tripleInstants); + TripleInstants deserialized = objectMapper.readValue(bytes, TripleInstants.class); + assertEquals(now.minusSeconds(1), deserialized.a); + assertEquals(now, deserialized.b); + assertEquals(now.plusSeconds(1), deserialized.c); + } + + @Test + public void serialize32BitFormat() + throws IOException + { + singleInstant.instant = Instant.ofEpochSecond(Instant.now().getEpochSecond()); + + byte[] bytes = objectMapper.writeValueAsBytes(singleInstant); + + // Check the size of serialized data first + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { + unpacker.unpackMapHeader(); + assertEquals("instant", unpacker.unpackString()); + assertEquals(4, unpacker.unpackExtensionTypeHeader().getLength()); + } + + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { + unpacker.unpackMapHeader(); + unpacker.unpackString(); + assertEquals(singleInstant.instant, unpacker.unpackTimestamp()); + } + } + + @Test + public void serialize64BitFormat() + throws IOException + { + singleInstant.instant = Instant.ofEpochSecond(Instant.now().getEpochSecond(), 1234); + + byte[] bytes = objectMapper.writeValueAsBytes(singleInstant); + + // Check the size of serialized data first + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { + unpacker.unpackMapHeader(); + assertEquals("instant", unpacker.unpackString()); + assertEquals(8, unpacker.unpackExtensionTypeHeader().getLength()); + } + + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { + unpacker.unpackMapHeader(); + unpacker.unpackString(); + assertEquals(singleInstant.instant, unpacker.unpackTimestamp()); + } + } + + @Test + public void serialize96BitFormat() + throws IOException + { + singleInstant.instant = Instant.ofEpochSecond(19880866800L /* 2600-01-01 */, 1234); + + byte[] bytes = objectMapper.writeValueAsBytes(singleInstant); + + // Check the size of serialized data first + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { + unpacker.unpackMapHeader(); + assertEquals("instant", unpacker.unpackString()); + assertEquals(12, unpacker.unpackExtensionTypeHeader().getLength()); + } + + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { + unpacker.unpackMapHeader(); + unpacker.unpackString(); + assertEquals(singleInstant.instant, unpacker.unpackTimestamp()); + } + } + + @Test + public void deserialize32BitFormat() + throws IOException + { + Instant instant = Instant.ofEpochSecond(Instant.now().getEpochSecond()); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(os)) { + packer.packMapHeader(1) + .packString("instant") + .packTimestamp(instant); + } + + byte[] bytes = os.toByteArray(); + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { + unpacker.unpackMapHeader(); + unpacker.unpackString(); + assertEquals(4, unpacker.unpackExtensionTypeHeader().getLength()); + } + + SingleInstant deserialized = objectMapper.readValue(bytes, SingleInstant.class); + assertEquals(instant, deserialized.instant); + } + + @Test + public void deserialize64BitFormat() + throws IOException + { + Instant instant = Instant.ofEpochSecond(Instant.now().getEpochSecond(), 1234); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(os)) { + packer.packMapHeader(1) + .packString("instant") + .packTimestamp(instant); + } + + byte[] bytes = os.toByteArray(); + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { + unpacker.unpackMapHeader(); + unpacker.unpackString(); + assertEquals(8, unpacker.unpackExtensionTypeHeader().getLength()); + } + + SingleInstant deserialized = objectMapper.readValue(bytes, SingleInstant.class); + assertEquals(instant, deserialized.instant); + } + + @Test + public void deserialize96BitFormat() + throws IOException + { + Instant instant = Instant.ofEpochSecond(19880866800L /* 2600-01-01 */, 1234); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(os)) { + packer.packMapHeader(1) + .packString("instant") + .packTimestamp(instant); + } + + byte[] bytes = os.toByteArray(); + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { + unpacker.unpackMapHeader(); + unpacker.unpackString(); + assertEquals(12, unpacker.unpackExtensionTypeHeader().getLength()); + } + + SingleInstant deserialized = objectMapper.readValue(bytes, SingleInstant.class); + assertEquals(instant, deserialized.instant); + } +}