Skip to content

Commit bc0d03c

Browse files
authored
Support timestamp extension in jackson-dataformat-msgpack (#677)
* Support JDK8 to handle Instant * Add TimestampExtensionModule * Add TimestampExtensionModuleTest * Add a few more tests * Use msgpack-core's serde for timestamp internally * Add a test for 96-bit format * Revert "Support JDK8 to handle Instant" This reverts commit 33cdf2d. * Take care of "No newline at end of file" * Fix format * Handle checkstyle error "Utility classes should not have a public or default constructor" * Clean up * Add some test for serializing * Add how to use TimestampExtensionModule in the doc
1 parent 64bf2e2 commit bc0d03c

File tree

3 files changed

+319
-1
lines changed

3 files changed

+319
-1
lines changed

msgpack-jackson/README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,27 @@ When you want to use non-String value as a key of Map, use `MessagePackKeySerial
240240
System.out.println(mapper.readValue(converted, Pojo.class)); // => Pojo{value=1234567890.98765432100}
241241
```
242242

243+
### Serialize and deserialize Instant instances as MessagePack extension type
244+
245+
`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.
246+
247+
```java
248+
ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory())
249+
.registerModule(TimestampExtensionModule.INSTANCE);
250+
Pojo pojo = new Pojo();
251+
// The type of `timestamp` variable is Instant
252+
pojo.timestamp = Instant.now();
253+
byte[] bytes = objectMapper.writeValueAsBytes(pojo);
254+
255+
// The Instant instance is serialized as MessagePack extension type (type: -1)
256+
257+
Pojo deserialized = objectMapper.readValue(bytes, Pojo.class);
258+
System.out.println(deserialized); // "2022-09-14T08:47:24.922Z"
259+
```
260+
243261
### Deserialize extension types with ExtensionTypeCustomDeserializers
244262

245-
`ExtensionTypeCustomDeserializers` helps you to deserialize extension types easily.
263+
`ExtensionTypeCustomDeserializers` helps you to deserialize your own custom extension types easily.
246264

247265
#### Deserialize extension type value directly
248266

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package org.msgpack.jackson.dataformat;
2+
3+
import com.fasterxml.jackson.core.JsonGenerator;
4+
import com.fasterxml.jackson.core.JsonParser;
5+
import com.fasterxml.jackson.databind.DeserializationContext;
6+
import com.fasterxml.jackson.databind.SerializerProvider;
7+
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
8+
import com.fasterxml.jackson.databind.module.SimpleModule;
9+
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
10+
import org.msgpack.core.ExtensionTypeHeader;
11+
import org.msgpack.core.MessagePack;
12+
import org.msgpack.core.MessagePacker;
13+
import org.msgpack.core.MessageUnpacker;
14+
15+
import java.io.ByteArrayOutputStream;
16+
import java.io.IOException;
17+
import java.time.Instant;
18+
19+
public class TimestampExtensionModule
20+
{
21+
public static final byte EXT_TYPE = -1;
22+
public static final SimpleModule INSTANCE = new SimpleModule("msgpack-ext-timestamp");
23+
24+
static {
25+
INSTANCE.addSerializer(Instant.class, new InstantSerializer(Instant.class));
26+
INSTANCE.addDeserializer(Instant.class, new InstantDeserializer(Instant.class));
27+
}
28+
29+
private static class InstantSerializer extends StdSerializer<Instant>
30+
{
31+
protected InstantSerializer(Class<Instant> t)
32+
{
33+
super(t);
34+
}
35+
36+
@Override
37+
public void serialize(Instant value, JsonGenerator gen, SerializerProvider provider)
38+
throws IOException
39+
{
40+
ByteArrayOutputStream os = new ByteArrayOutputStream();
41+
// MEMO: Reusing these MessagePacker and MessageUnpacker instances would improve the performance
42+
try (MessagePacker packer = MessagePack.newDefaultPacker(os)) {
43+
packer.packTimestamp(value);
44+
}
45+
try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(os.toByteArray())) {
46+
ExtensionTypeHeader header = unpacker.unpackExtensionTypeHeader();
47+
byte[] bytes = unpacker.readPayload(header.getLength());
48+
49+
MessagePackExtensionType extensionType = new MessagePackExtensionType(EXT_TYPE, bytes);
50+
gen.writeObject(extensionType);
51+
}
52+
}
53+
}
54+
55+
private static class InstantDeserializer extends StdDeserializer<Instant>
56+
{
57+
protected InstantDeserializer(Class<?> vc)
58+
{
59+
super(vc);
60+
}
61+
62+
@Override
63+
public Instant deserialize(JsonParser p, DeserializationContext ctxt)
64+
throws IOException
65+
{
66+
MessagePackExtensionType ext = p.readValueAs(MessagePackExtensionType.class);
67+
if (ext.getType() != EXT_TYPE) {
68+
throw new RuntimeException(
69+
String.format("Unexpected extension type (0x%X) for Instant object", ext.getType()));
70+
}
71+
72+
// MEMO: Reusing this MessageUnpacker instance would improve the performance
73+
try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(ext.getData())) {
74+
return unpacker.unpackTimestamp(new ExtensionTypeHeader(EXT_TYPE, ext.getData().length));
75+
}
76+
}
77+
}
78+
79+
private TimestampExtensionModule()
80+
{
81+
}
82+
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
//
2+
// MessagePack for Java
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
package org.msgpack.jackson.dataformat;
17+
18+
import com.fasterxml.jackson.databind.ObjectMapper;
19+
import org.junit.Before;
20+
import org.junit.Test;
21+
import org.msgpack.core.MessagePack;
22+
import org.msgpack.core.MessagePacker;
23+
import org.msgpack.core.MessageUnpacker;
24+
25+
import java.io.ByteArrayOutputStream;
26+
import java.io.IOException;
27+
import java.time.Instant;
28+
29+
import static org.junit.Assert.assertEquals;
30+
31+
public class TimestampExtensionModuleTest
32+
{
33+
private final ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory());
34+
private final SingleInstant singleInstant = new SingleInstant();
35+
private final TripleInstants tripleInstants = new TripleInstants();
36+
37+
private static class SingleInstant
38+
{
39+
public Instant instant;
40+
}
41+
42+
private static class TripleInstants
43+
{
44+
public Instant a;
45+
public Instant b;
46+
public Instant c;
47+
}
48+
49+
@Before
50+
public void setUp()
51+
throws Exception
52+
{
53+
objectMapper.registerModule(TimestampExtensionModule.INSTANCE);
54+
}
55+
56+
@Test
57+
public void testSingleInstantPojo()
58+
throws IOException
59+
{
60+
singleInstant.instant = Instant.now();
61+
byte[] bytes = objectMapper.writeValueAsBytes(singleInstant);
62+
SingleInstant deserialized = objectMapper.readValue(bytes, SingleInstant.class);
63+
assertEquals(singleInstant.instant, deserialized.instant);
64+
}
65+
66+
@Test
67+
public void testTripleInstantsPojo()
68+
throws IOException
69+
{
70+
Instant now = Instant.now();
71+
tripleInstants.a = now.minusSeconds(1);
72+
tripleInstants.b = now;
73+
tripleInstants.c = now.plusSeconds(1);
74+
byte[] bytes = objectMapper.writeValueAsBytes(tripleInstants);
75+
TripleInstants deserialized = objectMapper.readValue(bytes, TripleInstants.class);
76+
assertEquals(now.minusSeconds(1), deserialized.a);
77+
assertEquals(now, deserialized.b);
78+
assertEquals(now.plusSeconds(1), deserialized.c);
79+
}
80+
81+
@Test
82+
public void serialize32BitFormat()
83+
throws IOException
84+
{
85+
singleInstant.instant = Instant.ofEpochSecond(Instant.now().getEpochSecond());
86+
87+
byte[] bytes = objectMapper.writeValueAsBytes(singleInstant);
88+
89+
// Check the size of serialized data first
90+
try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) {
91+
unpacker.unpackMapHeader();
92+
assertEquals("instant", unpacker.unpackString());
93+
assertEquals(4, unpacker.unpackExtensionTypeHeader().getLength());
94+
}
95+
96+
try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) {
97+
unpacker.unpackMapHeader();
98+
unpacker.unpackString();
99+
assertEquals(singleInstant.instant, unpacker.unpackTimestamp());
100+
}
101+
}
102+
103+
@Test
104+
public void serialize64BitFormat()
105+
throws IOException
106+
{
107+
singleInstant.instant = Instant.ofEpochSecond(Instant.now().getEpochSecond(), 1234);
108+
109+
byte[] bytes = objectMapper.writeValueAsBytes(singleInstant);
110+
111+
// Check the size of serialized data first
112+
try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) {
113+
unpacker.unpackMapHeader();
114+
assertEquals("instant", unpacker.unpackString());
115+
assertEquals(8, unpacker.unpackExtensionTypeHeader().getLength());
116+
}
117+
118+
try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) {
119+
unpacker.unpackMapHeader();
120+
unpacker.unpackString();
121+
assertEquals(singleInstant.instant, unpacker.unpackTimestamp());
122+
}
123+
}
124+
125+
@Test
126+
public void serialize96BitFormat()
127+
throws IOException
128+
{
129+
singleInstant.instant = Instant.ofEpochSecond(19880866800L /* 2600-01-01 */, 1234);
130+
131+
byte[] bytes = objectMapper.writeValueAsBytes(singleInstant);
132+
133+
// Check the size of serialized data first
134+
try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) {
135+
unpacker.unpackMapHeader();
136+
assertEquals("instant", unpacker.unpackString());
137+
assertEquals(12, unpacker.unpackExtensionTypeHeader().getLength());
138+
}
139+
140+
try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) {
141+
unpacker.unpackMapHeader();
142+
unpacker.unpackString();
143+
assertEquals(singleInstant.instant, unpacker.unpackTimestamp());
144+
}
145+
}
146+
147+
@Test
148+
public void deserialize32BitFormat()
149+
throws IOException
150+
{
151+
Instant instant = Instant.ofEpochSecond(Instant.now().getEpochSecond());
152+
153+
ByteArrayOutputStream os = new ByteArrayOutputStream();
154+
try (MessagePacker packer = MessagePack.newDefaultPacker(os)) {
155+
packer.packMapHeader(1)
156+
.packString("instant")
157+
.packTimestamp(instant);
158+
}
159+
160+
byte[] bytes = os.toByteArray();
161+
try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) {
162+
unpacker.unpackMapHeader();
163+
unpacker.unpackString();
164+
assertEquals(4, unpacker.unpackExtensionTypeHeader().getLength());
165+
}
166+
167+
SingleInstant deserialized = objectMapper.readValue(bytes, SingleInstant.class);
168+
assertEquals(instant, deserialized.instant);
169+
}
170+
171+
@Test
172+
public void deserialize64BitFormat()
173+
throws IOException
174+
{
175+
Instant instant = Instant.ofEpochSecond(Instant.now().getEpochSecond(), 1234);
176+
177+
ByteArrayOutputStream os = new ByteArrayOutputStream();
178+
try (MessagePacker packer = MessagePack.newDefaultPacker(os)) {
179+
packer.packMapHeader(1)
180+
.packString("instant")
181+
.packTimestamp(instant);
182+
}
183+
184+
byte[] bytes = os.toByteArray();
185+
try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) {
186+
unpacker.unpackMapHeader();
187+
unpacker.unpackString();
188+
assertEquals(8, unpacker.unpackExtensionTypeHeader().getLength());
189+
}
190+
191+
SingleInstant deserialized = objectMapper.readValue(bytes, SingleInstant.class);
192+
assertEquals(instant, deserialized.instant);
193+
}
194+
195+
@Test
196+
public void deserialize96BitFormat()
197+
throws IOException
198+
{
199+
Instant instant = Instant.ofEpochSecond(19880866800L /* 2600-01-01 */, 1234);
200+
201+
ByteArrayOutputStream os = new ByteArrayOutputStream();
202+
try (MessagePacker packer = MessagePack.newDefaultPacker(os)) {
203+
packer.packMapHeader(1)
204+
.packString("instant")
205+
.packTimestamp(instant);
206+
}
207+
208+
byte[] bytes = os.toByteArray();
209+
try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) {
210+
unpacker.unpackMapHeader();
211+
unpacker.unpackString();
212+
assertEquals(12, unpacker.unpackExtensionTypeHeader().getLength());
213+
}
214+
215+
SingleInstant deserialized = objectMapper.readValue(bytes, SingleInstant.class);
216+
assertEquals(instant, deserialized.instant);
217+
}
218+
}

0 commit comments

Comments
 (0)