From 9b818d39b144e6f68495dd7c07967381a0c31bdc Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Sun, 31 May 2026 21:25:10 +0900 Subject: [PATCH 1/7] Add msgpack-jackson3 module for Jackson 3.x support --- .github/workflows/CI.yml | 15 +- .github/workflows/release.yml | 12 +- build.sbt | 42 +- msgpack-jackson3/README.md | 480 ++++++ .../dataformat/benchmark/BenchmarkState.java | 47 + .../benchmark/MsgpackReadBenchmark.java | 52 + .../benchmark/MsgpackWriteBenchmark.java | 58 + .../dataformat/benchmark/NopOutputStream.java | 41 + .../benchmark/WriteUTF8StringBenchmark.java | 86 + .../dataformat/benchmark/model/Image.java | 36 + .../benchmark/model/MediaContent.java | 44 + .../dataformat/benchmark/model/MediaItem.java | 43 + .../benchmark/model/MediaItems.java | 48 + .../dataformat/benchmark/model/Player.java | 21 + .../dataformat/benchmark/model/Size.java | 21 + .../ExtensionTypeCustomDeserializers.java | 56 + .../jackson/dataformat/JsonArrayFormat.java | 47 + .../dataformat/MessagePackExtensionType.java | 99 ++ .../dataformat/MessagePackFactory.java | 255 +++ .../dataformat/MessagePackFactoryBuilder.java | 114 ++ .../dataformat/MessagePackGenerator.java | 1041 +++++++++++ .../dataformat/MessagePackKeySerializer.java | 35 + .../jackson/dataformat/MessagePackMapper.java | 120 ++ .../jackson/dataformat/MessagePackParser.java | 801 +++++++++ .../dataformat/MessagePackReadContext.java | 213 +++ .../MessagePackSerializedString.java | 142 ++ .../MessagePackSerializerFactory.java | 44 + .../dataformat/MessagePackWriteContext.java | 139 ++ .../jackson/dataformat/PackageVersion.java | 30 + .../dataformat/TimestampExtensionModule.java | 107 ++ .../org/msgpack/jackson/dataformat/Tuple.java | 38 + .../ExampleOfTypeInformationSerDe.java | 171 ++ .../MessagePackDataformatForFieldIdTest.java | 135 ++ .../MessagePackDataformatForPojoTest.java | 150 ++ .../MessagePackDataformatTestBase.java | 289 ++++ .../dataformat/MessagePackFactoryTest.java | 198 +++ .../dataformat/MessagePackGeneratorTest.java | 1446 ++++++++++++++++ .../dataformat/MessagePackMapperTest.java | 117 ++ .../dataformat/MessagePackParserTest.java | 1524 +++++++++++++++++ .../MessagePackWriteContextTest.java | 230 +++ .../TimestampExtensionModuleTest.java | 217 +++ project/plugins.sbt | 1 + 42 files changed, 8799 insertions(+), 6 deletions(-) create mode 100644 msgpack-jackson3/README.md create mode 100644 msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/BenchmarkState.java create mode 100644 msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackReadBenchmark.java create mode 100644 msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackWriteBenchmark.java create mode 100644 msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/NopOutputStream.java create mode 100644 msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/WriteUTF8StringBenchmark.java create mode 100644 msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Image.java create mode 100644 msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaContent.java create mode 100644 msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItem.java create mode 100644 msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItems.java create mode 100644 msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Player.java create mode 100644 msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Size.java create mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java create mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java create mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java create mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java create mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactoryBuilder.java create mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java create mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java create mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java create mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java create mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java create mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java create mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java create mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackWriteContext.java create mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/PackageVersion.java create mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java create mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/Tuple.java create mode 100644 msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java create mode 100644 msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java create mode 100644 msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java create mode 100644 msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java create mode 100644 msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java create mode 100644 msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java create mode 100644 msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java create mode 100644 msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java create mode 100644 msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackWriteContextTest.java create mode 100644 msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 26c11bfb..941e53ec 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -28,6 +28,7 @@ jobs: - 'project/build.properties' - 'msgpack-core/**' - 'msgpack-jackson/**' + - 'msgpack-jackson3/**' docs: - '**.md' - '**.txt' @@ -65,6 +66,16 @@ jobs: key: ${{ runner.os }}-jdk${{ matrix.java }}-${{ hashFiles('**/*.sbt') }} restore-keys: ${{ runner.os }}-jdk${{ matrix.java }}- - name: Test - run: ./sbt test + run: | + if [[ ${{ matrix.java }} -lt 17 ]]; then + ./sbt msgpack-core/test msgpack-jackson/test + else + ./sbt test + fi - name: Universal Buffer Test - run: ./sbt test -J-Dmsgpack.universal-buffer=true \ No newline at end of file + run: | + if [[ ${{ matrix.java }} -lt 17 ]]; then + ./sbt msgpack-core/test msgpack-jackson/test -J-Dmsgpack.universal-buffer=true + else + ./sbt test -J-Dmsgpack.universal-buffer=true + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0cec5029..3fb5753e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,17 @@ jobs: env: PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} run: | - ./sbt publishSigned + ./sbt msgpack-core/publishSigned msgpack-jackson/publishSigned + # msgpack-jackson3 requires JDK 17+ + - uses: actions/setup-java@v5 + with: + java-version: 17 + distribution: temurin + - name: Build bundle for msgpack-jackson3 + env: + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + run: | + ./sbt msgpack-jackson3/publishSigned - name: Release to Sonatype env: SONATYPE_USERNAME: '${{ secrets.SONATYPE_USERNAME }}' diff --git a/build.sbt b/build.sbt index 36d6f05e..0f3c280e 100644 --- a/build.sbt +++ b/build.sbt @@ -96,10 +96,18 @@ val buildSettings = Seq[Setting[?]]( Test / compile := ((Test / compile) dependsOn (Test / jcheckStyle)).value ) -val junitJupiter = "org.junit.jupiter" % "junit-jupiter" % "5.14.4" % "test" -val junitVintage = "org.junit.vintage" % "junit-vintage-engine" % "5.14.4" % "test" +val junitJupiter = "org.junit.jupiter" % "junit-jupiter" % "5.14.4" % "test" +val junitVintage = "org.junit.vintage" % "junit-vintage-engine" % "5.14.4" % "test" +val junitInterface = "com.github.sbt" % "junit-interface" % "0.13.3" % "test" // Project settings +val isJava17Plus: Boolean = { + val v = sys.props.getOrElse("java.specification.version", "1.8") + // getOrElse(false): non-numeric versions (e.g. early-access "17-ea") fail safe + // by not compiling the jackson3 module rather than making an optimistic guess. + if (v.startsWith("1.")) false else scala.util.Try(v.toInt >= 17).getOrElse(false) +} + lazy val root = Project(id = "msgpack-java", base = file(".")) .settings( buildSettings, @@ -108,7 +116,10 @@ lazy val root = Project(id = "msgpack-java", base = file(".")) publish := {}, publishLocal := {} ) - .aggregate(msgpackCore, msgpackJackson) + .aggregate( + Seq[ProjectReference](msgpackCore, msgpackJackson) ++ + (if (isJava17Plus) Seq[ProjectReference](msgpackJackson3) else Nil): _* + ) lazy val msgpackCore = Project(id = "msgpack-core", base = file("msgpack-core")) .enablePlugins(SbtOsgi) @@ -170,3 +181,28 @@ lazy val msgpackJackson = Project(id = "msgpack-jackson", base = file("msgpack-j testOptions += Tests.Argument(TestFrameworks.JUnit, "-v") ) .dependsOn(msgpackCore) + +lazy val msgpackJackson3 = Project(id = "msgpack-jackson3", base = file("msgpack-jackson3")) + .enablePlugins(SbtOsgi, JmhPlugin) + .settings( + buildSettings, + name := "jackson-dataformat-msgpack-jackson3", + description := "Jackson 3.x extension that adds support for MessagePack", + OsgiKeys.bundleSymbolicName := "org.msgpack.msgpack-jackson3", + OsgiKeys.exportPackage := Seq("org.msgpack.jackson", "org.msgpack.jackson.dataformat"), + OsgiKeys.importPackage := Seq("!android.os", "!sun.*"), + Test / fork := true, + javacOptions := Seq("--release", "17"), + doc / javacOptions := Seq("--release", "17", "-Xdoclint:none"), + libraryDependencies ++= + Seq( + "tools.jackson.core" % "jackson-databind" % "3.1.2", + junitInterface + ), + testOptions += Tests.Argument(TestFrameworks.JUnit, "-v"), + Jmh / javaOptions ++= Seq( + "--add-opens=java.base/java.nio=ALL-UNNAMED", + "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED" + ) + ) + .dependsOn(msgpackCore) diff --git a/msgpack-jackson3/README.md b/msgpack-jackson3/README.md new file mode 100644 index 00000000..9e149e4d --- /dev/null +++ b/msgpack-jackson3/README.md @@ -0,0 +1,480 @@ +# jackson-dataformat-msgpack-jackson3 + +This Jackson 3.x extension library is a component to easily read and write [MessagePack](http://msgpack.org/) encoded data through jackson-databind API. + +It extends standard Jackson streaming API (`JsonFactory`, `JsonParser`, `JsonGenerator`), and as such works seamlessly with all the higher level data abstractions (data binding, tree model, and pluggable extensions). + +**Requirements:** Java 17+ and Jackson 3.x. For the Jackson 2.x compatible version, see [`msgpack-jackson`](../msgpack-jackson/). + +**Note on imports:** Jackson 3 moved its core and databind packages from `com.fasterxml.jackson` to `tools.jackson`. User-facing annotations (`@JsonProperty`, `@JsonFormat`, etc.) remain in `com.fasterxml.jackson.annotation` for backward compatibility. + +## Install + +### Maven + +```xml + + org.msgpack + jackson-dataformat-msgpack-jackson3 + (version) + +``` + +### Sbt + +```scala +libraryDependencies += "org.msgpack" % "jackson-dataformat-msgpack-jackson3" % "(version)" +``` + +### Gradle + +```groovy +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.msgpack:jackson-dataformat-msgpack-jackson3:(version)' +} +``` + +## Basic usage + +### Serialization/Deserialization of POJO + +Only thing you need to do is to instantiate `MessagePackFactory` and pass it to the constructor of `tools.jackson.databind.ObjectMapper`. And then, you can use it for MessagePack format data in the same way as jackson-databind. + +```java +// Instantiate ObjectMapper for MessagePack +ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + +// Serialize a Java object to byte array +ExamplePojo pojo = new ExamplePojo("komamitsu"); +byte[] bytes = objectMapper.writeValueAsBytes(pojo); + +// Deserialize the byte array to a Java object +ExamplePojo deserialized = objectMapper.readValue(bytes, ExamplePojo.class); +System.out.println(deserialized.getName()); // => komamitsu +``` + +Or more easily: + +```java +ObjectMapper objectMapper = new MessagePackMapper(); +``` + +We strongly recommend calling `MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString()` if you serialize and/or deserialize BigInteger/BigDecimal values. See [Serialize and deserialize BigDecimal as str type internally in MessagePack format](#serialize-and-deserialize-bigdecimal-as-str-type-internally-in-messagepack-format) for details. + +```java +ObjectMapper objectMapper = MessagePackMapper.builder().handleBigIntegerAndBigDecimalAsString().build(); +``` + +### Serialization/Deserialization of List + +```java +// Instantiate ObjectMapper for MessagePack +ObjectMapper objectMapper = new MessagePackMapper(); + +// Serialize a List to byte array +List list = new ArrayList<>(); +list.add("Foo"); +list.add("Bar"); +list.add(42); +byte[] bytes = objectMapper.writeValueAsBytes(list); + +// Deserialize the byte array to a List +List deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); +System.out.println(deserialized); // => [Foo, Bar, 42] +``` + +### Serialization/Deserialization of Map + +```java +// Instantiate ObjectMapper for MessagePack +ObjectMapper objectMapper = new MessagePackMapper(); + +// Serialize a Map to byte array +Map map = new HashMap<>(); +map.put("name", "komamitsu"); +map.put("age", 42); +byte[] bytes = objectMapper.writeValueAsBytes(map); + +// Deserialize the byte array to a Map +Map deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); +System.out.println(deserialized); // => {name=komamitsu, age=42} +``` + +### Example of Serialization/Deserialization over multiple languages + +Java + +```java +// Serialize +Map obj = new HashMap(); +obj.put("foo", "hello"); +obj.put("bar", "world"); +byte[] bs = objectMapper.writeValueAsBytes(obj); +// bs => [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111, +// -93, 98, 97, 114, -91, 119, 111, 114, 108, 100] +``` + +Ruby + +```ruby +require 'msgpack' + +# Deserialize +xs = [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111, + -93, 98, 97, 114, -91, 119, 111, 114, 108, 100] +MessagePack.unpack(xs.pack("C*")) +# => {"foo"=>"hello", "bar"=>"world"} + +# Serialize +["zero", 1, 2.0, nil].to_msgpack.unpack('C*') +# => [148, 164, 122, 101, 114, 111, 1, 203, 64, 0, 0, 0, 0, 0, 0, 0, 192] +``` + +Java + +```java +// Deserialize +bs = new byte[] {(byte) 148, (byte) 164, 122, 101, 114, 111, 1, + (byte) 203, 64, 0, 0, 0, 0, 0, 0, 0, (byte) 192}; +TypeReference> typeReference = new TypeReference>(){}; +List xs = objectMapper.readValue(bs, typeReference); +// xs => [zero, 1, 2.0, null] +``` + +## Advanced usage + +### Serialize multiple values without closing an output stream + +`tools.jackson.databind.ObjectMapper` closes an output stream by default after it writes a value. If you want to serialize multiple values in a row without closing an output stream, disable `StreamWriteFeature.AUTO_CLOSE_TARGET`. + +```java +OutputStream out = new FileOutputStream(tempFile); +ObjectMapper objectMapper = MessagePackMapper.builder() + .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) + .build(); + +objectMapper.writeValue(out, 1); +objectMapper.writeValue(out, "two"); +objectMapper.writeValue(out, 3.14); +out.close(); + +MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(new FileInputStream(tempFile)); +System.out.println(unpacker.unpackInt()); // => 1 +System.out.println(unpacker.unpackString()); // => two +System.out.println(unpacker.unpackFloat()); // => 3.14 +``` + +### Deserialize multiple values without closing an input stream + +`tools.jackson.databind.ObjectMapper` closes an input stream by default after it reads a value. If you want to deserialize multiple values in a row without closing an input stream, disable `StreamReadFeature.AUTO_CLOSE_SOURCE`. + +```java +MessagePacker packer = MessagePack.newDefaultPacker(new FileOutputStream(tempFile)); +packer.packInt(42); +packer.packString("Hello"); +packer.close(); + +FileInputStream in = new FileInputStream(tempFile); +ObjectMapper objectMapper = MessagePackMapper.builder() + .disable(StreamReadFeature.AUTO_CLOSE_SOURCE) + .build(); +System.out.println(objectMapper.readValue(in, Integer.class)); +System.out.println(objectMapper.readValue(in, String.class)); +in.close(); +``` + +### Serialize not using str8 type + +Old msgpack-java (e.g 0.6.7) doesn't support MessagePack str8 type. When your application needs to communicate with such an old MessagePack library, you can disable the data type like this: + +```java +MessagePack.PackerConfig config = new MessagePack.PackerConfig().withStr8FormatSupport(false); +ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory(config)); +// This string is serialized as bin8 type +byte[] resultWithoutStr8Format = objectMapper.writeValueAsBytes(str8LengthString); +``` + +### Serialize using non-String as a key of Map + +When you want to use non-String value as a key of Map, use `MessagePackKeySerializer` for key serialization. + +```java +@JsonSerialize(keyUsing = MessagePackKeySerializer.class) +private Map intMap = new HashMap<>(); + + : + +intMap.put(42, "Hello"); + +ObjectMapper objectMapper = new MessagePackMapper(); +byte[] bytes = objectMapper.writeValueAsBytes(intMap); + +Map deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); +System.out.println(deserialized); // => {42=Hello} +``` + +### Serialize and deserialize BigDecimal as str type internally in MessagePack format + +`jackson-dataformat-msgpack-jackson3` represents BigDecimal values as float type in MessagePack format by default for backward compatibility. But the default behavior could fail when handling too large value for `double` type. So we strongly recommend calling `MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString()` to internally handle BigDecimal values as String. + +```java +ObjectMapper objectMapper = MessagePackMapper.builder().handleBigIntegerAndBigDecimalAsString().build(); + +Pojo obj = new Pojo(); +// This value is too large to be serialized as double +obj.value = new BigDecimal("1234567890.98765432100"); + +byte[] converted = objectMapper.writeValueAsBytes(obj); + +System.out.println(objectMapper.readValue(converted, Pojo.class)); // => Pojo{value=1234567890.98765432100} +``` + +`MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString()` is equivalent to the following configuration. + +```java +ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); +objectMapper.configOverride(BigInteger.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); +objectMapper.configOverride(BigDecimal.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); +``` + +### 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 = MessagePackMapper.builder() + .addModule(TimestampExtensionModule.INSTANCE) + .build(); +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 your own custom extension types easily. + +#### Deserialize extension type value directly + +```java +// In this application, extension type 59 is used for byte[] +byte[] bytes; +{ + // This ObjectMapper is just for temporary serialization + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + MessagePacker packer = MessagePack.newDefaultPacker(outputStream); + + packer.packExtensionTypeHeader((byte) 59, hexspeak.length); + packer.addPayload(hexspeak); + packer.close(); + + bytes = outputStream.toByteArray(); +} + +// Register the type and a deserializer to ExtensionTypeCustomDeserializers +ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); +extTypeCustomDesers.addCustomDeser((byte) 59, data -> { + if (Arrays.equals(data, + new byte[] {(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE})) { + return "Java"; + } + return "Not Java"; +}); + +ObjectMapper objectMapper = new ObjectMapper( + new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)); + +System.out.println(objectMapper.readValue(bytes, Object.class)); + // => Java +``` + +#### Use extension type as Map key + +```java +static class TripleBytesPojo +{ + public byte first; + public byte second; + public byte third; + + public TripleBytesPojo(byte first, byte second, byte third) + { + this.first = first; + this.second = second; + this.third = third; + } + + @Override + public boolean equals(Object o) + { + : + } + + @Override + public int hashCode() + { + : + } + + @Override + public String toString() + { + // This key format is used when serialized as map key + return String.format("%d-%d-%d", first, second, third); + } + + static class KeyDeserializer + extends tools.jackson.databind.KeyDeserializer + { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) + { + String[] values = key.split("-"); + return new TripleBytesPojo(Byte.parseByte(values[0]), Byte.parseByte(values[1]), Byte.parseByte(values[2])); + } + } + + static TripleBytesPojo deserialize(byte[] bytes) + { + return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]); + } +} + +: + +byte extTypeCode = 42; + +ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); +extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() +{ + @Override + public Object deserialize(byte[] value) + throws IOException + { + return TripleBytesPojo.deserialize(value); + } +}); + +SimpleModule module = new SimpleModule(); +module.addKeyDeserializer(TripleBytesPojo.class, new TripleBytesPojo.KeyDeserializer()); +ObjectMapper objectMapper = MessagePackMapper.builder( + new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) + .addModule(module) + .build(); + +Map deserializedMap = + objectMapper.readValue(serializedData, + new TypeReference>() {}); +``` + +#### Use extension type as Map value + +```java +static class TripleBytesPojo +{ + public byte first; + public byte second; + public byte third; + + public TripleBytesPojo(byte first, byte second, byte third) + { + this.first = first; + this.second = second; + this.third = third; + } + + static class Deserializer + extends StdDeserializer + { + protected Deserializer() + { + super(TripleBytesPojo.class); + } + + @Override + public TripleBytesPojo deserialize(JsonParser p, DeserializationContext ctxt) + { + return TripleBytesPojo.deserialize(p.getBinaryValue()); + } + } + + static TripleBytesPojo deserialize(byte[] bytes) + { + return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]); + } +} + +: + +byte extTypeCode = 42; + +ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); +extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() +{ + @Override + public Object deserialize(byte[] value) + throws IOException + { + return TripleBytesPojo.deserialize(value); + } +}); + +SimpleModule module = new SimpleModule(); +module.addDeserializer(TripleBytesPojo.class, new TripleBytesPojo.Deserializer()); +ObjectMapper objectMapper = MessagePackMapper.builder( + new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) + .addModule(module) + .build(); + +Map deserializedMap = + objectMapper.readValue(serializedData, + new TypeReference>() {}); +``` + +### Serialize a nested object that also serializes + +When you serialize an object that has a nested object also serializing with ObjectMapper and MessagePackFactory like the following code, it throws NullPointerException since the nested MessagePackFactory modifies a shared state stored in ThreadLocal. + +```java +@Test +public void testNestedSerialization() throws Exception +{ + ObjectMapper objectMapper = new MessagePackMapper(); + objectMapper.writeValueAsBytes(new OuterClass()); +} + +public class OuterClass +{ + public String getInner() throws JacksonException + { + ObjectMapper m = new MessagePackMapper(); + m.writeValueAsBytes(new InnerClass()); + return "EFG"; + } +} + +public class InnerClass +{ + public String getName() + { + return "ABC"; + } +} +``` + +There are a few options to fix this issue, but they introduce performance degradations while this usage is a corner case. A workaround that doesn't affect performance is to call `MessagePackFactory#setReuseResourceInGenerator(false)`. It might be inconvenient to call the API for users, but it's a reasonable tradeoff with performance for now. + +```java +ObjectMapper objectMapper = new ObjectMapper( + new MessagePackFactory().setReuseResourceInGenerator(false)); +``` diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/BenchmarkState.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/BenchmarkState.java new file mode 100644 index 00000000..8e47ceaa --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/BenchmarkState.java @@ -0,0 +1,47 @@ +// +// 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.benchmark; + +import org.msgpack.jackson.dataformat.MessagePackFactory; +import org.msgpack.jackson.dataformat.MessagePackMapper; +import org.msgpack.jackson.dataformat.benchmark.model.MediaItem; +import org.msgpack.jackson.dataformat.benchmark.model.MediaItems; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +@State(Scope.Thread) +public class BenchmarkState +{ + public final ObjectMapper msgpackMapper = MessagePackMapper.builder(new MessagePackFactory()).build(); + public final ObjectMapper jsonMapper = JsonMapper.builder().build(); + + public final byte[] msgpackBytes; + public final byte[] jsonBytes; + + public BenchmarkState() + { + try { + MediaItem item = MediaItems.stdMediaItem(); + msgpackBytes = msgpackMapper.writeValueAsBytes(item); + jsonBytes = jsonMapper.writeValueAsBytes(item); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackReadBenchmark.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackReadBenchmark.java new file mode 100644 index 00000000..f7860bc5 --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackReadBenchmark.java @@ -0,0 +1,52 @@ +// +// 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.benchmark; + +import org.msgpack.jackson.dataformat.benchmark.model.MediaItem; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +@Fork(2) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +public class MsgpackReadBenchmark +{ + private final BenchmarkState state = new BenchmarkState(); + + @Benchmark + public Object readPojoMsgpack() throws Exception + { + return state.msgpackMapper.readValue(state.msgpackBytes, MediaItem.class); + } + + @Benchmark + public Object readPojoJson() throws Exception + { + return state.jsonMapper.readValue(state.jsonBytes, MediaItem.class); + } +} diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackWriteBenchmark.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackWriteBenchmark.java new file mode 100644 index 00000000..9ed2bfa6 --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackWriteBenchmark.java @@ -0,0 +1,58 @@ +// +// 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.benchmark; + +import org.msgpack.jackson.dataformat.benchmark.model.MediaItem; +import org.msgpack.jackson.dataformat.benchmark.model.MediaItems; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +@Fork(2) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +public class MsgpackWriteBenchmark +{ + private final BenchmarkState state = new BenchmarkState(); + private final MediaItem item = MediaItems.stdMediaItem(); + + @Benchmark + public int writePojoMsgpack() throws Exception + { + NopOutputStream out = new NopOutputStream(); + state.msgpackMapper.writeValue(out, item); + return out.size(); + } + + @Benchmark + public int writePojoJson() throws Exception + { + NopOutputStream out = new NopOutputStream(); + state.jsonMapper.writeValue(out, item); + return out.size(); + } +} diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/NopOutputStream.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/NopOutputStream.java new file mode 100644 index 00000000..983a27e2 --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/NopOutputStream.java @@ -0,0 +1,41 @@ +// +// 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.benchmark; + +import java.io.OutputStream; + +public class NopOutputStream + extends OutputStream +{ + private int size; + + @Override + public void write(int b) + { + size++; + } + + @Override + public void write(byte[] b, int off, int len) + { + size += len; + } + + public int size() + { + return size; + } +} diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/WriteUTF8StringBenchmark.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/WriteUTF8StringBenchmark.java new file mode 100644 index 00000000..764a0efb --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/WriteUTF8StringBenchmark.java @@ -0,0 +1,86 @@ +// +// 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.benchmark; + +import org.msgpack.jackson.dataformat.MessagePackFactory; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.ObjectWriteContext; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; + +/** + * Compares writeUTF8String throughput for RawUtf8String (current) vs new String(bytes, UTF_8). + * Run this benchmark twice: once with the current implementation, once after replacing + * writeByteArrayTextValue to use new String(text, offset, len, StandardCharsets.UTF_8). + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +@Fork(2) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +public class WriteUTF8StringBenchmark +{ + private static final int WRITES_PER_INVOCATION = 500; + + private final MessagePackFactory factory = new MessagePackFactory(); + + // ASCII-only: compact-string fast path applies for new String(bytes, UTF_8) + private final byte[] asciiBytes = + "Hello, World! This is a typical ASCII field value.".getBytes(StandardCharsets.UTF_8); + + // Non-ASCII: multi-byte UTF-8, compact-string fast path does NOT apply + private final byte[] nonAsciiBytes = + "東京は日本の首都です。This mixes CJK and ASCII.".getBytes(StandardCharsets.UTF_8); + + @Benchmark + public int writeUTF8StringAscii() throws Exception + { + NopOutputStream out = new NopOutputStream(); + try (JsonGenerator gen = factory.createGenerator(ObjectWriteContext.empty(), out)) { + gen.writeStartArray(); + for (int i = 0; i < WRITES_PER_INVOCATION; i++) { + gen.writeUTF8String(asciiBytes, 0, asciiBytes.length); + } + gen.writeEndArray(); + } + return out.size(); + } + + @Benchmark + public int writeUTF8StringNonAscii() throws Exception + { + NopOutputStream out = new NopOutputStream(); + try (JsonGenerator gen = factory.createGenerator(ObjectWriteContext.empty(), out)) { + gen.writeStartArray(); + for (int i = 0; i < WRITES_PER_INVOCATION; i++) { + gen.writeUTF8String(nonAsciiBytes, 0, nonAsciiBytes.length); + } + gen.writeEndArray(); + } + return out.size(); + } +} diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Image.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Image.java new file mode 100644 index 00000000..607d813c --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Image.java @@ -0,0 +1,36 @@ +// +// 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.benchmark.model; + +public class Image +{ + public String uri; + public String title; + public int width; + public int height; + public Size size; + + public Image() {} + + public Image(String uri, String title, int width, int height, Size size) + { + this.uri = uri; + this.title = title; + this.width = width; + this.height = height; + this.size = size; + } +} diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaContent.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaContent.java new file mode 100644 index 00000000..9af96caa --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaContent.java @@ -0,0 +1,44 @@ +// +// 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.benchmark.model; + +import java.util.ArrayList; +import java.util.List; + +public class MediaContent +{ + public String uri; + public String title; + public int width; + public int height; + public String format; + public long duration; + public long size; + public int bitrate; + public List persons; + public Player player; + public String copyright; + + public MediaContent() {} + + public void addPerson(String person) + { + if (persons == null) { + persons = new ArrayList<>(); + } + persons.add(person); + } +} diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItem.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItem.java new file mode 100644 index 00000000..0bed7a69 --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItem.java @@ -0,0 +1,43 @@ +// +// 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.benchmark.model; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.util.ArrayList; +import java.util.List; + +@JsonPropertyOrder({"content", "images"}) +public class MediaItem +{ + public MediaContent content; + public List images; + + public MediaItem() {} + + public MediaItem(MediaContent content) + { + this.content = content; + } + + public void addImage(Image image) + { + if (images == null) { + images = new ArrayList<>(); + } + images.add(image); + } +} diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItems.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItems.java new file mode 100644 index 00000000..dd77700f --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItems.java @@ -0,0 +1,48 @@ +// +// 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.benchmark.model; + +public class MediaItems +{ + private static final MediaItem STD_MEDIA_ITEM; + + static { + MediaContent content = new MediaContent(); + content.uri = "http://javaone.com/keynote.mpg"; + content.title = "Javaone Keynote"; + content.width = 640; + content.height = 480; + content.format = "video/mpg4"; + content.duration = 18000000L; + content.size = 58982400L; + content.bitrate = 262144; + content.player = Player.JAVA; + content.copyright = "None"; + content.addPerson("Bill Gates"); + content.addPerson("Steve Jobs"); + + MediaItem item = new MediaItem(content); + item.addImage(new Image("http://javaone.com/keynote_large.jpg", "Javaone Keynote", 1024, 768, Size.LARGE)); + item.addImage(new Image("http://javaone.com/keynote_small.jpg", "Javaone Keynote", 320, 240, Size.SMALL)); + + STD_MEDIA_ITEM = item; + } + + public static MediaItem stdMediaItem() + { + return STD_MEDIA_ITEM; + } +} diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Player.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Player.java new file mode 100644 index 00000000..82740c66 --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Player.java @@ -0,0 +1,21 @@ +// +// 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.benchmark.model; + +public enum Player +{ + JAVA, FLASH +} diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Size.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Size.java new file mode 100644 index 00000000..36e56489 --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Size.java @@ -0,0 +1,21 @@ +// +// 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.benchmark.model; + +public enum Size +{ + SMALL, LARGE +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java new file mode 100644 index 00000000..aa587975 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java @@ -0,0 +1,56 @@ +// +// 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 java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class ExtensionTypeCustomDeserializers +{ + private Map deserTable = new ConcurrentHashMap<>(); + + public ExtensionTypeCustomDeserializers() + { + } + + public ExtensionTypeCustomDeserializers(ExtensionTypeCustomDeserializers src) + { + this(); + this.deserTable.putAll(src.deserTable); + } + + public void addCustomDeser(byte type, final Deser deser) + { + deserTable.put(type, deser); + } + + public Deser getDeser(byte type) + { + return deserTable.get(type); + } + + public void clearEntries() + { + deserTable.clear(); + } + + public interface Deser + { + Object deserialize(byte[] data) + throws IOException; + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java new file mode 100644 index 00000000..1b1e048b --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java @@ -0,0 +1,47 @@ +// +// 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 tools.jackson.databind.cfg.MapperConfig; +import tools.jackson.databind.introspect.Annotated; +import tools.jackson.databind.introspect.JacksonAnnotationIntrospector; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import static com.fasterxml.jackson.annotation.JsonFormat.Shape.ARRAY; + +/** + * Provides the ability of serializing POJOs without their schema. + * Similar to @JsonFormat annotation with JsonFormat.Shape.ARRAY, but in a programmatic + * way. + * + * This also provides same behavior as msgpack-java 0.6.x serialization api. + */ +public class JsonArrayFormat extends JacksonAnnotationIntrospector +{ + private static final JsonFormat.Value ARRAY_FORMAT = new JsonFormat.Value().withShape(ARRAY); + + @Override + public JsonFormat.Value findFormat(MapperConfig config, Annotated ann) + { + JsonFormat.Value precedenceFormat = super.findFormat(config, ann); + if (precedenceFormat != null) { + return precedenceFormat; + } + + return ARRAY_FORMAT; + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java new file mode 100644 index 00000000..1b62b80e --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java @@ -0,0 +1,99 @@ +// +// 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 tools.jackson.core.JsonGenerator; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.annotation.JsonSerialize; +import tools.jackson.databind.ser.std.StdSerializer; + +import java.util.Arrays; +import java.util.Objects; + +@JsonSerialize(using = MessagePackExtensionType.Serializer.class) +public class MessagePackExtensionType +{ + private final byte type; + private final byte[] data; + + public MessagePackExtensionType(byte type, byte[] data) + { + this.type = type; + this.data = Objects.requireNonNull(data, "data"); + } + + public byte getType() + { + return type; + } + + public byte[] getData() + { + return data; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (!(o instanceof MessagePackExtensionType)) { + return false; + } + + MessagePackExtensionType that = (MessagePackExtensionType) o; + + if (type != that.type) { + return false; + } + return Arrays.equals(data, that.data); + } + + @Override + public int hashCode() + { + int result = type; + result = 31 * result + Arrays.hashCode(data); + return result; + } + + @Override + public String toString() + { + return "MessagePackExtensionType(type=" + type + ", data.length=" + data.length + ")"; + } + + public static class Serializer extends StdSerializer + { + public Serializer() + { + super(MessagePackExtensionType.class); + } + + @Override + public void serialize(MessagePackExtensionType value, JsonGenerator gen, SerializationContext serializers) + { + if (gen instanceof MessagePackGenerator) { + MessagePackGenerator msgpackGenerator = (MessagePackGenerator) gen; + msgpackGenerator.writeExtensionType(value); + } + else { + throw new IllegalStateException("'gen' is expected to be MessagePackGenerator but it's " + gen.getClass()); + } + } + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java new file mode 100644 index 00000000..e6b0bf01 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java @@ -0,0 +1,255 @@ +// +// 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 tools.jackson.core.ErrorReportConfiguration; +import tools.jackson.core.FormatFeature; +import tools.jackson.core.FormatSchema; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.ObjectReadContext; +import tools.jackson.core.ObjectWriteContext; +import tools.jackson.core.StreamReadConstraints; +import tools.jackson.core.StreamWriteConstraints; +import tools.jackson.core.TSFBuilder; +import tools.jackson.core.TokenStreamFactory; +import tools.jackson.core.Version; +import tools.jackson.core.base.BinaryTSFactory; +import tools.jackson.core.io.IOContext; +import org.msgpack.core.MessagePack; +import org.msgpack.core.annotations.VisibleForTesting; + +import org.msgpack.core.buffer.ArrayBufferInput; +import org.msgpack.core.buffer.InputStreamBufferInput; +import org.msgpack.core.buffer.MessageBufferInput; + +import java.io.DataInput; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class MessagePackFactory + extends BinaryTSFactory + implements java.io.Serializable +{ + private static final long serialVersionUID = 2578263992015504348L; + + private final MessagePack.PackerConfig packerConfig; + private boolean reuseResourceInGenerator = true; + private boolean reuseResourceInParser = true; + private boolean supportIntegerKeys = false; + private ExtensionTypeCustomDeserializers extTypeCustomDesers; + + public MessagePackFactory() + { + this(MessagePack.DEFAULT_PACKER_CONFIG); + } + + public MessagePackFactory(MessagePack.PackerConfig packerConfig) + { + super(StreamReadConstraints.defaults(), StreamWriteConstraints.defaults(), + ErrorReportConfiguration.defaults(), 0, 0); + this.packerConfig = packerConfig; + } + + public MessagePackFactory(MessagePackFactory src) + { + super(src); + this.packerConfig = src.packerConfig.clone(); + this.reuseResourceInGenerator = src.reuseResourceInGenerator; + this.reuseResourceInParser = src.reuseResourceInParser; + this.supportIntegerKeys = src.supportIntegerKeys; + if (src.extTypeCustomDesers != null) { + this.extTypeCustomDesers = new ExtensionTypeCustomDeserializers(src.extTypeCustomDesers); + } + } + + protected MessagePackFactory(MessagePackFactoryBuilder b) + { + super(b); + this.packerConfig = b.packerConfig().clone(); + this.reuseResourceInGenerator = b.reuseResourceInGenerator(); + this.reuseResourceInParser = b.reuseResourceInParser(); + this.supportIntegerKeys = b.supportIntegerKeys(); + this.extTypeCustomDesers = b.extTypeCustomDesers(); + } + + public MessagePackFactory setReuseResourceInGenerator(boolean reuseResourceInGenerator) + { + this.reuseResourceInGenerator = reuseResourceInGenerator; + return this; + } + + public MessagePackFactory setReuseResourceInParser(boolean reuseResourceInParser) + { + this.reuseResourceInParser = reuseResourceInParser; + return this; + } + + public MessagePackFactory setSupportIntegerKeys(boolean supportIntegerKeys) + { + this.supportIntegerKeys = supportIntegerKeys; + return this; + } + + public MessagePackFactory setExtTypeCustomDesers(ExtensionTypeCustomDeserializers extTypeCustomDesers) + { + this.extTypeCustomDesers = extTypeCustomDesers; + return this; + } + + @Override + protected JsonParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt, + InputStream in) throws JacksonException + { + try { + MessagePackParser parser = new MessagePackParser(readCtxt, ioCtxt, + readCtxt.getStreamReadFeatures(_streamReadFeatures), + new InputStreamBufferInput(in), in, reuseResourceInParser); + if (extTypeCustomDesers != null) { + parser.setExtensionTypeCustomDeserializers(extTypeCustomDesers); + } + return parser; + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + } + + @Override + protected JsonParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt, + byte[] data, int offset, int len) throws JacksonException + { + try { + MessageBufferInput input = new ArrayBufferInput(data, offset, len); + MessagePackParser parser = new MessagePackParser(readCtxt, ioCtxt, + readCtxt.getStreamReadFeatures(_streamReadFeatures), input, data, reuseResourceInParser); + if (extTypeCustomDesers != null) { + parser.setExtensionTypeCustomDeserializers(extTypeCustomDesers); + } + return parser; + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + } + + @Override + protected JsonParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt, + DataInput input) throws JacksonException + { + return _unsupported(); + } + + @Override + protected JsonGenerator _createGenerator(ObjectWriteContext writeCtxt, IOContext ioCtxt, + OutputStream out) throws JacksonException + { + try { + return new MessagePackGenerator(writeCtxt, ioCtxt, + writeCtxt.getStreamWriteFeatures(_streamWriteFeatures), + out, packerConfig, reuseResourceInGenerator, supportIntegerKeys); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + } + + @Override + public TokenStreamFactory copy() + { + return new MessagePackFactory(this); + } + + @Override + public TokenStreamFactory snapshot() + { + return copy(); + } + + @Override + public TSFBuilder rebuild() + { + return new MessagePackFactoryBuilder(this); + } + + @Override + public Version version() + { + return PackageVersion.VERSION; + } + + @VisibleForTesting + MessagePack.PackerConfig getPackerConfig() + { + return packerConfig; + } + + @VisibleForTesting + boolean isReuseResourceInGenerator() + { + return reuseResourceInGenerator; + } + + @VisibleForTesting + boolean isReuseResourceInParser() + { + return reuseResourceInParser; + } + + @VisibleForTesting + boolean isSupportIntegerKeys() + { + return supportIntegerKeys; + } + + @VisibleForTesting + ExtensionTypeCustomDeserializers getExtTypeCustomDesers() + { + return extTypeCustomDesers; + } + + @Override + public String getFormatName() + { + return "msgpack"; + } + + @Override + public boolean canParseAsync() + { + return false; + } + + @Override + public boolean canUseSchema(FormatSchema schema) + { + return false; + } + + @Override + public Class getFormatReadFeatureType() + { + return null; + } + + @Override + public Class getFormatWriteFeatureType() + { + return null; + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactoryBuilder.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactoryBuilder.java new file mode 100644 index 00000000..59f80991 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactoryBuilder.java @@ -0,0 +1,114 @@ +// +// 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 tools.jackson.core.ErrorReportConfiguration; +import tools.jackson.core.StreamReadConstraints; +import tools.jackson.core.StreamWriteConstraints; +import tools.jackson.core.base.DecorableTSFactory; +import org.msgpack.core.MessagePack; + +public class MessagePackFactoryBuilder + extends DecorableTSFactory.DecorableTSFBuilder +{ + private MessagePack.PackerConfig packerConfig; + private boolean reuseResourceInGenerator; + private boolean reuseResourceInParser; + private boolean supportIntegerKeys; + private ExtensionTypeCustomDeserializers extTypeCustomDesers; + + public MessagePackFactoryBuilder() + { + super(StreamReadConstraints.defaults(), StreamWriteConstraints.defaults(), + ErrorReportConfiguration.defaults(), 0, 0); + this.packerConfig = MessagePack.DEFAULT_PACKER_CONFIG; + this.reuseResourceInGenerator = true; + this.reuseResourceInParser = true; + this.supportIntegerKeys = false; + } + + public MessagePackFactoryBuilder(MessagePackFactory base) + { + super(base); + this.packerConfig = base.getPackerConfig().clone(); + this.reuseResourceInGenerator = base.isReuseResourceInGenerator(); + this.reuseResourceInParser = base.isReuseResourceInParser(); + this.supportIntegerKeys = base.isSupportIntegerKeys(); + ExtensionTypeCustomDeserializers srcDesers = base.getExtTypeCustomDesers(); + this.extTypeCustomDesers = srcDesers == null ? null : new ExtensionTypeCustomDeserializers(srcDesers); + } + + public MessagePackFactoryBuilder packerConfig(MessagePack.PackerConfig config) + { + this.packerConfig = config; + return this; + } + + public MessagePackFactoryBuilder reuseResourceInGenerator(boolean v) + { + this.reuseResourceInGenerator = v; + return this; + } + + public MessagePackFactoryBuilder reuseResourceInParser(boolean v) + { + this.reuseResourceInParser = v; + return this; + } + + public MessagePackFactoryBuilder supportIntegerKeys(boolean v) + { + this.supportIntegerKeys = v; + return this; + } + + public MessagePackFactoryBuilder extTypeCustomDesers(ExtensionTypeCustomDeserializers desers) + { + this.extTypeCustomDesers = desers; + return this; + } + + public MessagePack.PackerConfig packerConfig() + { + return packerConfig; + } + + public boolean reuseResourceInGenerator() + { + return reuseResourceInGenerator; + } + + public boolean reuseResourceInParser() + { + return reuseResourceInParser; + } + + public boolean supportIntegerKeys() + { + return supportIntegerKeys; + } + + public ExtensionTypeCustomDeserializers extTypeCustomDesers() + { + return extTypeCustomDesers; + } + + @Override + public MessagePackFactory build() + { + return new MessagePackFactory(this); + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java new file mode 100644 index 00000000..cc6e9137 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java @@ -0,0 +1,1041 @@ +// +// 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 tools.jackson.core.Base64Variant; +import tools.jackson.core.JacksonException; +import tools.jackson.core.util.JacksonFeatureSet; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.ObjectWriteContext; +import tools.jackson.core.SerializableString; +import tools.jackson.core.StreamWriteCapability; +import tools.jackson.core.StreamWriteFeature; +import tools.jackson.core.json.DupDetector; +import tools.jackson.core.TokenStreamContext; +import tools.jackson.core.base.GeneratorBase; +import tools.jackson.core.io.IOContext; +import org.msgpack.core.MessagePack; +import org.msgpack.core.MessagePacker; +import org.msgpack.core.buffer.MessageBufferOutput; +import org.msgpack.core.buffer.OutputStreamBufferOutput; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.Reader; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +public class MessagePackGenerator + extends GeneratorBase +{ + private static final int IN_ROOT = 0; + private static final int IN_OBJECT = 1; + private static final int IN_ARRAY = 2; + private final MessagePacker messagePacker; + // Retained heap per idle thread: ~8 KB (OutputStreamBufferOutput + internal MessageBuffer). + // Negligible compared to Jackson's own per-thread buffer retention. + private static final ThreadLocal messageBufferOutputHolder = new ThreadLocal<>(); + private final OutputStream output; + private final MessagePack.PackerConfig packerConfig; + private final boolean supportIntegerKeys; + + private int currentParentElementIndex = -1; + private int currentState = IN_ROOT; + private final List nodes; + private boolean isElementsClosed = false; + private MessagePackWriteContext writeContext; + private final boolean ownsThreadLocalBuffer; + + private static final class RawUtf8String + { + public final byte[] bytes; + public final int offset; + public final int len; + + public RawUtf8String(byte[] bytes, int offset, int len) + { + this.bytes = bytes; + this.offset = offset; + this.len = len; + } + } + + private abstract static class Node + { + // Root containers have -1. + final int parentIndex; + + public Node(int parentIndex) + { + this.parentIndex = parentIndex; + } + + abstract void incrementChildCount(); + + abstract int currentStateAsParent(); + } + + private abstract static class NodeContainer extends Node + { + // Only for containers. + int childCount; + + public NodeContainer(int parentIndex) + { + super(parentIndex); + } + + @Override + void incrementChildCount() + { + childCount++; + } + } + + private static final class NodeArray extends NodeContainer + { + public NodeArray(int parentIndex) + { + super(parentIndex); + } + + @Override + int currentStateAsParent() + { + return IN_ARRAY; + } + } + + private static final class NodeObject extends NodeContainer + { + public NodeObject(int parentIndex) + { + super(parentIndex); + } + + @Override + int currentStateAsParent() + { + return IN_OBJECT; + } + } + + private static final class NodeEntryInArray extends Node + { + final Object value; + + public NodeEntryInArray(int parentIndex, Object value) + { + super(parentIndex); + this.value = value; + } + + @Override + void incrementChildCount() + { + throw new UnsupportedOperationException(); + } + + @Override + int currentStateAsParent() + { + throw new UnsupportedOperationException(); + } + } + + private static final class NodeEntryInObject extends Node + { + final Object key; + // Lazily initialized. + Object value; + + public NodeEntryInObject(int parentIndex, Object key) + { + super(parentIndex); + this.key = key; + } + + @Override + void incrementChildCount() + { + assert value instanceof NodeContainer; + ((NodeContainer) value).childCount++; + } + + @Override + int currentStateAsParent() + { + if (value instanceof NodeObject) { + return IN_OBJECT; + } + else if (value instanceof NodeArray) { + return IN_ARRAY; + } + else { + throw new AssertionError(); + } + } + } + + // Internal constructor for nested serialization. + private MessagePackGenerator( + ObjectWriteContext writeCtxt, + IOContext ioCtxt, + int streamWriteFeatures, + OutputStream out, + MessagePack.PackerConfig packerConfig, + boolean supportIntegerKeys) + { + super(writeCtxt, ioCtxt, streamWriteFeatures); + this.output = out; + this.messagePacker = packerConfig.newPacker(out); + this.packerConfig = packerConfig; + this.nodes = new ArrayList<>(); + this.supportIntegerKeys = supportIntegerKeys; + this.writeContext = MessagePackWriteContext.createRootContext( + StreamWriteFeature.STRICT_DUPLICATE_DETECTION.enabledIn(streamWriteFeatures) + ? DupDetector.rootDetector(this) : null); + this.ownsThreadLocalBuffer = false; + } + + public MessagePackGenerator( + ObjectWriteContext writeCtxt, + IOContext ioCtxt, + int streamWriteFeatures, + OutputStream out, + MessagePack.PackerConfig packerConfig, + boolean reuseResourceInGenerator, + boolean supportIntegerKeys) + throws IOException + { + super(writeCtxt, ioCtxt, streamWriteFeatures); + this.output = out; + this.messagePacker = packerConfig.newPacker(getMessageBufferOutputForOutputStream(out, reuseResourceInGenerator)); + this.packerConfig = packerConfig; + this.nodes = new ArrayList<>(); + this.supportIntegerKeys = supportIntegerKeys; + this.writeContext = MessagePackWriteContext.createRootContext( + StreamWriteFeature.STRICT_DUPLICATE_DETECTION.enabledIn(streamWriteFeatures) + ? DupDetector.rootDetector(this) : null); + this.ownsThreadLocalBuffer = reuseResourceInGenerator; + } + + private MessageBufferOutput getMessageBufferOutputForOutputStream( + OutputStream out, + boolean reuseResourceInGenerator) + throws IOException + { + OutputStreamBufferOutput messageBufferOutput; + if (reuseResourceInGenerator) { + messageBufferOutput = messageBufferOutputHolder.get(); + if (messageBufferOutput == null) { + messageBufferOutput = new OutputStreamBufferOutput(out); + messageBufferOutputHolder.set(messageBufferOutput); + } + else { + messageBufferOutput.reset(out); + } + } + else { + messageBufferOutput = new OutputStreamBufferOutput(out); + } + return messageBufferOutput; + } + + private String currentStateStr() + { + switch (currentState) { + case IN_OBJECT: + return "IN_OBJECT"; + case IN_ARRAY: + return "IN_ARRAY"; + default: + return "IN_ROOT"; + } + } + + @Override + public JsonGenerator writeStartArray() throws JacksonException + { + return writeStartArray(null); + } + + @Override + public JsonGenerator writeStartArray(Object currentValue) throws JacksonException + { + return writeStartArray(currentValue, -1); + } + + // size is ignored: element count is determined at flush time from the actual child nodes. + // When size >= 0, it could in principle be used to skip buffering and write the array + // header immediately, but Jackson does not guarantee it — dynamic filters (Views, + // @JsonFilter) evaluate entries incrementally and will pass -1 even for known-size + // collections. Backends must handle both cases (Jackson author confirmed, see #841). + @Override + public JsonGenerator writeStartArray(Object currentValue, int size) throws JacksonException + { + _verifyValueWrite("start an array"); + writeContext = writeContext.createChildArrayContext(currentValue); + if (currentState == IN_OBJECT) { + Node node = nodes.get(nodes.size() - 1); + assert node instanceof NodeEntryInObject; + NodeEntryInObject nodeEntryInObject = (NodeEntryInObject) node; + nodeEntryInObject.value = new NodeArray(currentParentElementIndex); + } + else { + if (isElementsClosed) { + flush(); + } + nodes.add(new NodeArray(currentParentElementIndex)); + } + currentParentElementIndex = nodes.size() - 1; + currentState = IN_ARRAY; + return this; + } + + @Override + public JsonGenerator writeEndArray() throws JacksonException + { + if (currentState != IN_ARRAY) { + _reportError("Current context not an array but " + currentStateStr()); + } + endCurrentContainer(); + return this; + } + + @Override + public JsonGenerator writeStartObject() throws JacksonException + { + return writeStartObject(null); + } + + @Override + public JsonGenerator writeStartObject(Object currentValue) throws JacksonException + { + return writeStartObject(currentValue, -1); + } + + // size is ignored: same reasoning as writeStartArray(Object, int) above. + @Override + public JsonGenerator writeStartObject(Object forValue, int size) throws JacksonException + { + _verifyValueWrite("start an object"); + writeContext = writeContext.createChildObjectContext(forValue); + if (currentState == IN_OBJECT) { + Node node = nodes.get(nodes.size() - 1); + assert node instanceof NodeEntryInObject; + NodeEntryInObject nodeEntryInObject = (NodeEntryInObject) node; + nodeEntryInObject.value = new NodeObject(currentParentElementIndex); + } + else { + if (isElementsClosed) { + flush(); + } + nodes.add(new NodeObject(currentParentElementIndex)); + } + currentParentElementIndex = nodes.size() - 1; + currentState = IN_OBJECT; + return this; + } + + @Override + public JsonGenerator writeEndObject() throws JacksonException + { + if (currentState != IN_OBJECT) { + _reportError("Current context not an object but " + currentStateStr()); + } + if (writeContext.isExpectingValue()) { + _reportError("Cannot close Object, property name written but no value"); + } + endCurrentContainer(); + return this; + } + + private void endCurrentContainer() + { + writeContext = writeContext.getParent(); + Node parent = nodes.get(currentParentElementIndex); + if (parent.parentIndex == -1) { + isElementsClosed = true; + currentParentElementIndex = parent.parentIndex; + currentState = IN_ROOT; + return; + } + + currentParentElementIndex = parent.parentIndex; + Node currentParent = nodes.get(currentParentElementIndex); + currentParent.incrementChildCount(); + currentState = currentParent.currentStateAsParent(); + } + + private void packNonContainer(Object v) + throws IOException + { + MessagePacker messagePacker = getMessagePacker(); + if (v instanceof String) { + messagePacker.packString((String) v); + } + else if (v instanceof RawUtf8String) { + RawUtf8String raw = (RawUtf8String) v; + messagePacker.packRawStringHeader(raw.len); + messagePacker.writePayload(raw.bytes, raw.offset, raw.len); + } + else if (v instanceof Integer) { + messagePacker.packInt((Integer) v); + } + else if (v == null) { + messagePacker.packNil(); + } + else if (v instanceof Float) { + messagePacker.packFloat((Float) v); + } + else if (v instanceof Long) { + messagePacker.packLong((Long) v); + } + else if (v instanceof Double) { + messagePacker.packDouble((Double) v); + } + else if (v instanceof BigInteger) { + messagePacker.packBigInteger((BigInteger) v); + } + else if (v instanceof BigDecimal) { + packBigDecimal((BigDecimal) v); + } + else if (v instanceof Boolean) { + messagePacker.packBoolean((Boolean) v); + } + else if (v instanceof ByteBuffer) { + ByteBuffer bb = (ByteBuffer) v; + int len = bb.remaining(); + if (bb.hasArray() && !bb.isReadOnly()) { + messagePacker.packBinaryHeader(len); + messagePacker.writePayload(bb.array(), bb.arrayOffset() + bb.position(), len); + } + else { + byte[] data = new byte[len]; + bb.duplicate().get(data); + messagePacker.packBinaryHeader(len); + messagePacker.addPayload(data); + } + } + else if (v instanceof MessagePackExtensionType) { + MessagePackExtensionType extensionType = (MessagePackExtensionType) v; + byte[] extData = extensionType.getData(); + messagePacker.packExtensionTypeHeader(extensionType.getType(), extData.length); + messagePacker.writePayload(extData); + } + else { + messagePacker.flush(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (MessagePackGenerator messagePackGenerator = new MessagePackGenerator( + objectWriteContext(), _ioContext, _streamWriteFeatures, + outputStream, packerConfig, supportIntegerKeys)) { + objectWriteContext().writeValue(messagePackGenerator, v); + } + output.write(outputStream.toByteArray()); + } + } + + private void packBigDecimal(BigDecimal decimal) + throws IOException + { + MessagePacker messagePacker = getMessagePacker(); + boolean failedToPackAsBI = false; + try { + //Check to see if this BigDecimal can be converted to BigInteger + BigInteger integer = decimal.toBigIntegerExact(); + messagePacker.packBigInteger(integer); + } + catch (ArithmeticException | IllegalArgumentException e) { + failedToPackAsBI = true; + } + + if (failedToPackAsBI) { + double doubleValue = decimal.doubleValue(); + //Check to make sure this BigDecimal can be represented as a double + if (Double.isInfinite(doubleValue) || decimal.compareTo(BigDecimal.valueOf(doubleValue)) != 0) { + throw new IllegalArgumentException("MessagePack cannot serialize a BigDecimal that can't be represented as double. " + decimal); + } + messagePacker.packDouble(doubleValue); + } + } + + private void packObject(NodeObject container) + throws IOException + { + MessagePacker messagePacker = getMessagePacker(); + messagePacker.packMapHeader(container.childCount); + } + + private void packArray(NodeArray container) + throws IOException + { + MessagePacker messagePacker = getMessagePacker(); + messagePacker.packArrayHeader(container.childCount); + } + + private void addKeyNode(Object key) + { + if (currentState != IN_OBJECT) { + _reportError("Can not write a property name, expecting a value"); + } + nodes.add(new NodeEntryInObject(currentParentElementIndex, key)); + } + + private void addValueNode(Object value) throws IOException + { + if (!writeContext.writeValue()) { + _reportError("Cannot write value: expecting a property name in Object context"); + } + switch (currentState) { + case IN_OBJECT: { + Node node = nodes.get(nodes.size() - 1); + assert node instanceof NodeEntryInObject; + NodeEntryInObject nodeEntryInObject = (NodeEntryInObject) node; + nodeEntryInObject.value = value; + nodes.get(node.parentIndex).incrementChildCount(); + break; + } + case IN_ARRAY: { + Node node = new NodeEntryInArray(currentParentElementIndex, value); + nodes.add(node); + nodes.get(node.parentIndex).incrementChildCount(); + break; + } + default: + // Flush any buffered root container before packing a root scalar, + // otherwise the scalar would be emitted before the container. + if (isElementsClosed) { + flush(); + } + packNonContainer(value); + flushMessagePacker(); + break; + } + } + + private void writeCharArrayTextValue(char[] text, int offset, int len) throws IOException + { + addValueNode(new String(text, offset, len)); + } + + private void writeByteArrayTextValue(byte[] text, int offset, int len) throws IOException + { + addValueNode(new RawUtf8String(text, offset, len)); + } + + @Override + public JsonGenerator writePropertyId(long id) throws JacksonException + { + if (this.supportIntegerKeys) { + if (!writeContext.writeName(String.valueOf(id))) { + _reportError("Can not write a property id, expecting a value"); + } + addKeyNode(id); + } + else { + writeName(String.valueOf(id)); + } + return this; + } + + @Override + public JacksonFeatureSet streamWriteCapabilities() + { + return DEFAULT_BINARY_WRITE_CAPABILITIES; + } + + @Override + public JsonGenerator writeName(String name) throws JacksonException + { + if (!writeContext.writeName(name)) { + _reportError("Can not write a property name, expecting a value"); + } + addKeyNode(name); + return this; + } + + @Override + public JsonGenerator writeName(SerializableString name) throws JacksonException + { + if (name instanceof MessagePackSerializedString) { + if (!writeContext.writeName(name.getValue())) { + _reportError("Can not write a property name, expecting a value"); + } + addKeyNode(((MessagePackSerializedString) name).getRawValue()); + } + else { + writeName(name.getValue()); + } + return this; + } + + @Override + public JsonGenerator writeString(String text) throws JacksonException + { + try { + addValueNode(text); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeString(char[] text, int offset, int len) throws JacksonException + { + try { + writeCharArrayTextValue(text, offset, len); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeString(Reader reader, int len) throws JacksonException + { + try { + long remaining = len < 0 ? Long.MAX_VALUE : len; + // Cap chunk size: len is a caller hint and can be arbitrarily large. + // Pre-allocating new StringBuilder(len) would reserve len*2 bytes upfront, + // which is an OOM risk for large inputs. The StringBuilder grows as needed. + int chunkSize = (int) Math.min(remaining, 8192); + StringBuilder sb = new StringBuilder(chunkSize); + char[] tmpBuf = new char[chunkSize]; + while (remaining > 0) { + int read = reader.read(tmpBuf, 0, (int) Math.min(remaining, tmpBuf.length)); + if (read < 0) { + break; + } + sb.append(tmpBuf, 0, read); + remaining -= read; + } + addValueNode(sb.toString()); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeString(SerializableString text) throws JacksonException + { + return writeString(text.getValue()); + } + + @Override + public JsonGenerator writeRawUTF8String(byte[] text, int offset, int length) throws JacksonException + { + try { + writeByteArrayTextValue(text, offset, length); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeUTF8String(byte[] text, int offset, int length) throws JacksonException + { + try { + writeByteArrayTextValue(text, offset, length); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeRaw(String text) throws JacksonException + { + try { + addValueNode(text); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeRaw(String text, int offset, int len) throws JacksonException + { + try { + addValueNode(text.substring(offset, offset + len)); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeRaw(char[] text, int offset, int len) throws JacksonException + { + try { + writeCharArrayTextValue(text, offset, len); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeRaw(char c) throws JacksonException + { + try { + writeCharArrayTextValue(new char[] { c }, 0, 1); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeBinary(Base64Variant b64variant, byte[] data, int offset, int len) throws JacksonException + { + try { + addValueNode(ByteBuffer.wrap(data, offset, len)); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeNumber(short v) throws JacksonException + { + return writeNumber((int) v); + } + + @Override + public JsonGenerator writeNumber(int v) throws JacksonException + { + try { + addValueNode(v); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeNumber(long v) throws JacksonException + { + try { + addValueNode(v); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeNumber(BigInteger v) throws JacksonException + { + try { + addValueNode(v); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeNumber(double d) throws JacksonException + { + try { + addValueNode(d); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeNumber(float f) throws JacksonException + { + try { + addValueNode(f); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeNumber(BigDecimal dec) throws JacksonException + { + try { + addValueNode(dec); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeNumber(String encodedValue) throws JacksonException + { + // There is a room to improve this API's performance while the implementation is robust. + // If users can use other MessagePackGenerator#writeNumber APIs that accept + // proper numeric types not String, it's better to use the other APIs instead. + try { + try { + long l = Long.parseLong(encodedValue); + addValueNode(l); + return this; + } + catch (NumberFormatException ignored) { + } + + try { + BigInteger bi = new BigInteger(encodedValue); + addValueNode(bi); + return this; + } + catch (NumberFormatException ignored) { + } + + try { + BigDecimal bd = new BigDecimal(encodedValue); + double d = bd.doubleValue(); + + // Check if the double can perfectly represent the exact decimal value. + // isInfinite guard: values like "1e309" overflow double to Infinity; keep as BigDecimal. + if (!Double.isInfinite(d) && bd.compareTo(new BigDecimal(String.valueOf(d))) == 0) { + // It's a safe ordinary floating-point number. + addValueNode(d); + } + else { + // It has more precision than a double can handle, or overflows double range. + addValueNode(bd); + } + return this; + } + catch (NumberFormatException e) { + // Fall back for NaN, Infinity, -Infinity which BigDecimal rejects. + try { + double d = Double.parseDouble(encodedValue); + addValueNode(d); + return this; + } + catch (NumberFormatException ignored) { + } + } + + throw new NumberFormatException(encodedValue); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + } + + @Override + public JsonGenerator writeBoolean(boolean state) throws JacksonException + { + try { + addValueNode(state); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeNull() throws JacksonException + { + try { + addValueNode(null); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + public void writeExtensionType(MessagePackExtensionType extensionType) + { + try { + addValueNode(extensionType); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + } + + @Override + public void close() throws JacksonException + { + if (!_closed) { + try { + flush(); + } + finally { + super.close(); + } + } + } + + @Override + public void flush() throws JacksonException + { + if (!isElementsClosed) { + // The whole elements are not closed yet. + return; + } + + try { + for (int i = 0; i < nodes.size(); i++) { + Node node = nodes.get(i); + if (node instanceof NodeEntryInObject) { + NodeEntryInObject nodeEntry = (NodeEntryInObject) node; + packNonContainer(nodeEntry.key); + if (nodeEntry.value instanceof NodeObject) { + packObject((NodeObject) nodeEntry.value); + } + else if (nodeEntry.value instanceof NodeArray) { + packArray((NodeArray) nodeEntry.value); + } + else { + packNonContainer(nodeEntry.value); + } + } + else if (node instanceof NodeObject) { + packObject((NodeObject) node); + } + else if (node instanceof NodeEntryInArray) { + packNonContainer(((NodeEntryInArray) node).value); + } + else if (node instanceof NodeArray) { + packArray((NodeArray) node); + } + else { + throw new AssertionError(); + } + } + flushMessagePacker(); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + nodes.clear(); + isElementsClosed = false; + } + + private void flushMessagePacker() + throws IOException + { + getMessagePacker().flush(); + } + + @Override + public tools.jackson.core.Version version() + { + return PackageVersion.VERSION; + } + + @Override + public TokenStreamContext streamWriteContext() + { + return writeContext; + } + + @Override + public Object streamWriteOutputTarget() + { + return output; + } + + @Override + public int streamWriteOutputBuffered() + { + return -1; + } + + @Override + public Object currentValue() + { + return writeContext.currentValue(); + } + + @Override + public void assignCurrentValue(Object v) + { + writeContext.assignCurrentValue(v); + } + + @Override + protected void _closeInput() throws IOException + { + if (StreamWriteFeature.AUTO_CLOSE_TARGET.enabledIn(_streamWriteFeatures)) { + messagePacker.close(); + } + } + + @Override + protected void _releaseBuffers() + { + // No null check on get(): generators are single-threaded by contract so this + // ThreadLocal is always set on the calling thread. A null here would indicate + // cross-thread misuse; letting it NPE surfaces that bug immediately. + if (ownsThreadLocalBuffer) { + OutputStreamBufferOutput buf = messageBufferOutputHolder.get(); + if (buf != null) { + try { + buf.reset(null); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + } + } + } + + @Override + protected void _verifyValueWrite(String typeMsg) throws JacksonException + { + if (!writeContext.writeValue()) { + _reportError("Cannot " + typeMsg + ", expecting a property name"); + } + } + + private MessagePacker getMessagePacker() + { + return messagePacker; + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java new file mode 100644 index 00000000..836a905d --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java @@ -0,0 +1,35 @@ +// +// 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 tools.jackson.core.JsonGenerator; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ser.std.StdSerializer; + +public class MessagePackKeySerializer + extends StdSerializer +{ + public MessagePackKeySerializer() + { + super(Object.class); + } + + @Override + public void serialize(Object value, JsonGenerator jgen, SerializationContext provider) + { + jgen.writeName(new MessagePackSerializedString(value)); + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java new file mode 100644 index 00000000..674b2733 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java @@ -0,0 +1,120 @@ +// +// 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.annotation.JsonFormat; + +import tools.jackson.core.Version; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.cfg.MapperBuilder; +import tools.jackson.databind.cfg.MapperBuilderState; + +import java.math.BigDecimal; +import java.math.BigInteger; + +public class MessagePackMapper extends ObjectMapper +{ + private static final long serialVersionUID = 3L; + + public static class Builder extends MapperBuilder + { + public Builder(MessagePackFactory f) + { + super(f); + } + + protected Builder(StateImpl state) + { + super(state); + } + + @Override + public MessagePackMapper build() + { + return new MessagePackMapper(this); + } + + public Builder handleBigIntegerAsString() + { + return withConfigOverride(BigInteger.class, + o -> o.setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING))); + } + + public Builder handleBigDecimalAsString() + { + return withConfigOverride(BigDecimal.class, + o -> o.setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING))); + } + + public Builder handleBigIntegerAndBigDecimalAsString() + { + return handleBigIntegerAsString().handleBigDecimalAsString(); + } + + @Override + protected MapperBuilderState _saveState() + { + return new StateImpl(this); + } + + protected static class StateImpl extends MapperBuilderState + { + private static final long serialVersionUID = 3L; + + public StateImpl(Builder src) + { + super(src); + } + + @Override + protected Object readResolve() + { + return new Builder(this).build(); + } + } + } + + public MessagePackMapper() + { + this(new Builder(new MessagePackFactory())); + } + + public MessagePackMapper(MessagePackFactory f) + { + this(new Builder(f)); + } + + protected MessagePackMapper(Builder builder) + { + super(builder); + } + + public static Builder builder() + { + return new Builder(new MessagePackFactory()); + } + + public static Builder builder(MessagePackFactory f) + { + return new Builder(f); + } + + @Override + public Version version() + { + return PackageVersion.VERSION; + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java new file mode 100644 index 00000000..98fb9c88 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java @@ -0,0 +1,801 @@ +// +// 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 tools.jackson.core.Base64Variant; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonToken; +import tools.jackson.core.ObjectReadContext; +import tools.jackson.core.StreamReadFeature; +import tools.jackson.core.TokenStreamContext; +import tools.jackson.core.TokenStreamLocation; +import tools.jackson.core.Version; +import tools.jackson.core.base.ParserMinimalBase; +import tools.jackson.core.exc.UnexpectedEndOfInputException; +import tools.jackson.core.io.IOContext; +import tools.jackson.core.json.DupDetector; +import org.msgpack.core.ExtensionTypeHeader; +import org.msgpack.core.MessageFormat; +import org.msgpack.core.MessagePack; +import org.msgpack.core.MessageUnpacker; +import org.msgpack.core.buffer.MessageBufferInput; +import org.msgpack.value.ValueType; + +import java.io.IOException; + +import java.math.BigDecimal; +import java.math.BigInteger; + +public class MessagePackParser + extends ParserMinimalBase +{ + // Retained heap per idle thread: ~0.2 KB (MessageUnpacker with cleared input buffer). + // Negligible compared to Jackson's own per-thread buffer retention. + private static final ThreadLocal> messageUnpackerHolder = new ThreadLocal<>(); + private final MessageUnpacker messageUnpacker; + + private static final BigInteger LONG_MIN = BigInteger.valueOf(Long.MIN_VALUE); + private static final BigInteger LONG_MAX = BigInteger.valueOf(Long.MAX_VALUE); + + private MessagePackReadContext streamReadContext; + + private boolean isClosed; + private long tokenPosition; + private long currentPosition; + private final IOContext ioContext; + private ExtensionTypeCustomDeserializers extTypeCustomDesers; + private final boolean ownsThreadLocalUnpacker; + + private enum Type + { + INT, LONG, DOUBLE, STRING, BYTES, BOOL, BIG_INT, EXT, NULL + } + private Type type; + private int intValue; + private long longValue; + private double doubleValue; + private boolean booleanValue; + private byte[] bytesValue; + private String stringValue; + private BigInteger biValue; + private MessagePackExtensionType extensionTypeValue; + + MessagePackParser(ObjectReadContext readCtxt, + IOContext ioCtxt, + int streamReadFeatures, + MessageBufferInput input, + Object src, + boolean reuseResourceInParser) + throws IOException + { + super(readCtxt, ioCtxt, streamReadFeatures); + + ioContext = ioCtxt; + DupDetector dups = StreamReadFeature.STRICT_DUPLICATE_DETECTION.enabledIn(streamReadFeatures) + ? DupDetector.rootDetector(this) : null; + streamReadContext = MessagePackReadContext.createRootContext(dups); + if (!reuseResourceInParser) { + messageUnpacker = MessagePack.newDefaultUnpacker(input); + ownsThreadLocalUnpacker = false; + return; + } + + Tuple messageUnpackerTuple = messageUnpackerHolder.get(); + if (messageUnpackerTuple == null) { + messageUnpacker = MessagePack.newDefaultUnpacker(input); + } + else { + // Considering to reuse InputStream with StreamReadFeature.AUTO_CLOSE_SOURCE, + // MessagePackParser needs to use the MessageUnpacker that has the same InputStream + // since it has buffer which has loaded the InputStream data ahead. + // However, it needs to call MessageUnpacker#reset when the source is different from the previous one. + Object cachedSrc = messageUnpackerTuple.first(); + if (StreamReadFeature.AUTO_CLOSE_SOURCE.enabledIn(streamReadFeatures) || cachedSrc != src || src instanceof byte[]) { + // reset() replaces the internal MessageBufferInput and clears the unpacker's + // internal read buffer to EMPTY_BUFFER. The old ArrayBufferInput becomes + // unreachable here (we discard the return value), so its byte[] is GC-eligible. + messageUnpackerTuple.second().reset(input); + } + messageUnpacker = messageUnpackerTuple.second(); + } + messageUnpackerHolder.set(new Tuple<>(src, messageUnpacker)); + ownsThreadLocalUnpacker = true; + } + + public void setExtensionTypeCustomDeserializers(ExtensionTypeCustomDeserializers extTypeCustomDesers) + { + this.extTypeCustomDesers = extTypeCustomDesers; + } + + @Override + public Version version() + { + return PackageVersion.VERSION; + } + + private String unpackString(MessageUnpacker messageUnpacker) throws IOException + { + return messageUnpacker.unpackString(); + } + + @Override + public JsonToken nextToken() throws JacksonException + { + try { + return _nextToken(); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + } + + private JsonToken _nextToken() throws IOException + { + type = null; + tokenPosition = messageUnpacker.getTotalReadBytes(); + + boolean isObjectValueSet = streamReadContext.inObject() && _currToken != JsonToken.PROPERTY_NAME; + if (isObjectValueSet) { + if (!streamReadContext.expectMoreValues()) { + streamReadContext = streamReadContext.getParent(); + return _updateToken(JsonToken.END_OBJECT); + } + } + else if (streamReadContext.inArray()) { + if (!streamReadContext.expectMoreValues()) { + streamReadContext = streamReadContext.getParent(); + return _updateToken(JsonToken.END_ARRAY); + } + } + + if (!messageUnpacker.hasNext()) { + if (streamReadContext.inRoot()) { + return null; + } + throw new UnexpectedEndOfInputException(this, null, null); + } + + MessageFormat format = messageUnpacker.getNextFormat(); + ValueType valueType = format.getValueType(); + + JsonToken nextToken; + switch (valueType) { + case STRING: + type = Type.STRING; + stringValue = unpackString(messageUnpacker); + _streamReadConstraints.validateStringLength(stringValue.length()); + if (isObjectValueSet) { + streamReadContext.setCurrentName(stringValue); + nextToken = JsonToken.PROPERTY_NAME; + } + else { + nextToken = JsonToken.VALUE_STRING; + } + break; + case INTEGER: + Object v; + switch (format) { + case UINT64: + BigInteger bi = messageUnpacker.unpackBigInteger(); + if (0 <= bi.compareTo(LONG_MIN) && bi.compareTo(LONG_MAX) <= 0) { + type = Type.LONG; + longValue = bi.longValue(); + v = longValue; + } + else { + type = Type.BIG_INT; + biValue = bi; + v = biValue; + } + break; + default: + long l = messageUnpacker.unpackLong(); + if (Integer.MIN_VALUE <= l && l <= Integer.MAX_VALUE) { + type = Type.INT; + intValue = (int) l; + v = intValue; + } + else { + type = Type.LONG; + longValue = l; + v = longValue; + } + break; + } + + if (isObjectValueSet) { + streamReadContext.setCurrentName(String.valueOf(v)); + nextToken = JsonToken.PROPERTY_NAME; + } + else { + nextToken = JsonToken.VALUE_NUMBER_INT; + } + break; + case NIL: + type = Type.NULL; + messageUnpacker.unpackNil(); + if (isObjectValueSet) { + streamReadContext.setCurrentName(null); + nextToken = JsonToken.PROPERTY_NAME; + } + else { + nextToken = JsonToken.VALUE_NULL; + } + break; + case BOOLEAN: + boolean b = messageUnpacker.unpackBoolean(); + type = Type.BOOL; + booleanValue = b; + if (isObjectValueSet) { + streamReadContext.setCurrentName(Boolean.toString(b)); + nextToken = JsonToken.PROPERTY_NAME; + } + else { + nextToken = b ? JsonToken.VALUE_TRUE : JsonToken.VALUE_FALSE; + } + break; + case FLOAT: + type = Type.DOUBLE; + doubleValue = messageUnpacker.unpackDouble(); + if (isObjectValueSet) { + streamReadContext.setCurrentName(String.valueOf(doubleValue)); + nextToken = JsonToken.PROPERTY_NAME; + } + else { + nextToken = JsonToken.VALUE_NUMBER_FLOAT; + } + break; + case BINARY: + type = Type.BYTES; + int len = messageUnpacker.unpackBinaryHeader(); + _streamReadConstraints.validateStringLength(len); + bytesValue = messageUnpacker.readPayload(len); + if (isObjectValueSet) { + streamReadContext.setCurrentName(new String(bytesValue, MessagePack.UTF8)); + nextToken = JsonToken.PROPERTY_NAME; + } + else { + nextToken = JsonToken.VALUE_EMBEDDED_OBJECT; + } + break; + case ARRAY: + nextToken = JsonToken.START_ARRAY; + streamReadContext = streamReadContext.createChildArrayContext(messageUnpacker.unpackArrayHeader()); + _streamReadConstraints.validateNestingDepth(streamReadContext.getNestingDepth()); + break; + case MAP: + nextToken = JsonToken.START_OBJECT; + streamReadContext = streamReadContext.createChildObjectContext(messageUnpacker.unpackMapHeader()); + _streamReadConstraints.validateNestingDepth(streamReadContext.getNestingDepth()); + break; + case EXTENSION: + type = Type.EXT; + ExtensionTypeHeader header = messageUnpacker.unpackExtensionTypeHeader(); + _streamReadConstraints.validateStringLength(header.getLength()); + extensionTypeValue = new MessagePackExtensionType(header.getType(), messageUnpacker.readPayload(header.getLength())); + if (isObjectValueSet) { + streamReadContext.setCurrentName(deserializedExtensionTypeValue().toString()); + nextToken = JsonToken.PROPERTY_NAME; + } + else { + nextToken = JsonToken.VALUE_EMBEDDED_OBJECT; + } + break; + default: + nextToken = _reportError("Unexpected MessagePack format type: " + valueType); + } + currentPosition = messageUnpacker.getTotalReadBytes(); + + _updateToken(nextToken); + + return nextToken; + } + + @Override + protected void _handleEOF() + { + } + + @Override + public String getString() + { + if (type == null) { + return _currToken == null ? null : _currToken.asString(); + } + switch (type) { + case STRING: + return stringValue; + case BYTES: + return new String(bytesValue, MessagePack.UTF8); + case INT: + return String.valueOf(intValue); + case LONG: + return String.valueOf(longValue); + case DOUBLE: + return String.valueOf(doubleValue); + case BOOL: + return Boolean.toString(booleanValue); + case BIG_INT: + return String.valueOf(biValue); + case EXT: + try { + return deserializedExtensionTypeValue().toString(); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + case NULL: + return "null"; + default: + return _reportError("Unexpected MessagePack value type: " + type); + } + } + + @Override + public char[] getStringCharacters() + { + return getString().toCharArray(); + } + + @Override + public boolean hasStringCharacters() + { + return false; + } + + @Override + public int getStringLength() + { + return getString().length(); + } + + @Override + public int getStringOffset() + { + return 0; + } + + @Override + public byte[] getBinaryValue(Base64Variant b64variant) + { + if (type == null) { + return _reportError("Current token (" + _currToken + ") not of binary type"); + } + switch (type) { + case BYTES: + return bytesValue; + case STRING: + return stringValue.getBytes(MessagePack.UTF8); + case EXT: + return extensionTypeValue.getData(); + case INT: + case LONG: + case DOUBLE: + case BOOL: + case BIG_INT: + case NULL: + return _reportError("Current token (" + _currToken + ") not of binary type"); + default: + return _reportError("Unexpected MessagePack value type: " + type); + } + } + + @Override + public Number getNumberValue() + { + if (type == null) { + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + } + switch (type) { + case INT: + return intValue; + case LONG: + return longValue; + case DOUBLE: + return doubleValue; + case BIG_INT: + return biValue; + case NULL: + case BOOL: + case STRING: + case BYTES: + case EXT: + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + default: + return _reportError("Unexpected MessagePack value type: " + type); + } + } + + @Override + public int getIntValue() + { + if (type == null) { + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + } + switch (type) { + case INT: + return intValue; + case LONG: + if (longValue < Integer.MIN_VALUE || longValue > Integer.MAX_VALUE) { + return _reportError("Numeric value (" + longValue + ") out of range for `int`"); + } + return (int) longValue; + case DOUBLE: + if (!Double.isFinite(doubleValue)) { + return _reportError("Cannot convert non-finite double (" + doubleValue + ") to `int`"); + } + if (doubleValue < Integer.MIN_VALUE || doubleValue > Integer.MAX_VALUE) { + return _reportError("Numeric value (" + doubleValue + ") out of range for `int`"); + } + return (int) doubleValue; + case BIG_INT: + try { + return biValue.intValueExact(); + } + catch (ArithmeticException e) { + return _reportError("Numeric value (" + biValue + ") out of range for `int`"); + } + case NULL: + case BOOL: + case STRING: + case BYTES: + case EXT: + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + default: + return _reportError("Unexpected MessagePack value type: " + type); + } + } + + @Override + public long getLongValue() + { + if (type == null) { + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + } + switch (type) { + case INT: + return intValue; + case LONG: + return longValue; + case DOUBLE: + if (!Double.isFinite(doubleValue)) { + return _reportError("Cannot convert non-finite double (" + doubleValue + ") to `long`"); + } + // (double)Long.MAX_VALUE rounds up to 2^63; use >= to reject 2^63 itself. + if (doubleValue < Long.MIN_VALUE || doubleValue >= (double) Long.MAX_VALUE) { + return _reportError("Numeric value (" + doubleValue + ") out of range for `long`"); + } + return (long) doubleValue; + case BIG_INT: + try { + return biValue.longValueExact(); + } + catch (ArithmeticException e) { + return _reportError("Numeric value (" + biValue + ") out of range for `long`"); + } + case NULL: + case BOOL: + case STRING: + case BYTES: + case EXT: + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + default: + return _reportError("Unexpected MessagePack value type: " + type); + } + } + + @Override + public BigInteger getBigIntegerValue() + { + if (type == null) { + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + } + switch (type) { + case INT: + return BigInteger.valueOf(intValue); + case LONG: + return BigInteger.valueOf(longValue); + case DOUBLE: + if (!Double.isFinite(doubleValue)) { + return _reportError("Cannot convert non-finite double (" + doubleValue + ") to BigInteger"); + } + return BigDecimal.valueOf(doubleValue).toBigInteger(); // truncates fractional part + case BIG_INT: + return biValue; + case NULL: + case BOOL: + case STRING: + case BYTES: + case EXT: + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + default: + return _reportError("Unexpected MessagePack value type: " + type); + } + } + + @Override + public float getFloatValue() + { + // No bounds/range check: a finite double or large BigInteger may overflow to + // Float.POSITIVE_INFINITY. This is intentional — same as ParserBase and CBORParser. + if (type == null) { + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + } + switch (type) { + case INT: + return (float) intValue; + case LONG: + return (float) longValue; + case DOUBLE: + return (float) doubleValue; + case BIG_INT: + return biValue.floatValue(); + case NULL: + case BOOL: + case STRING: + case BYTES: + case EXT: + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + default: + return _reportError("Unexpected MessagePack value type: " + type); + } + } + + @Override + public double getDoubleValue() + { + // No bounds/range check: large BigInteger may overflow to Double.POSITIVE_INFINITY, + // and large long values may lose precision. Intentional — same as ParserBase. + if (type == null) { + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + } + switch (type) { + case INT: + return intValue; + case LONG: + return (double) longValue; + case DOUBLE: + return doubleValue; + case BIG_INT: + return biValue.doubleValue(); + case NULL: + case BOOL: + case STRING: + case BYTES: + case EXT: + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + default: + return _reportError("Unexpected MessagePack value type: " + type); + } + } + + @Override + public BigDecimal getDecimalValue() + { + if (type == null) { + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + } + switch (type) { + case INT: + return BigDecimal.valueOf(intValue); + case LONG: + return BigDecimal.valueOf(longValue); + case DOUBLE: + if (!Double.isFinite(doubleValue)) { + return _reportError("Cannot convert non-finite double (" + doubleValue + ") to BigDecimal"); + } + return BigDecimal.valueOf(doubleValue); + case BIG_INT: + return new BigDecimal(biValue); + case NULL: + case BOOL: + case STRING: + case BYTES: + case EXT: + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + default: + return _reportError("Unexpected MessagePack value type: " + type); + } + } + + private Object deserializedExtensionTypeValue() + throws IOException + { + if (extTypeCustomDesers != null) { + ExtensionTypeCustomDeserializers.Deser deser = extTypeCustomDesers.getDeser(extensionTypeValue.getType()); + if (deser != null) { + return deser.deserialize(extensionTypeValue.getData()); + } + } + return extensionTypeValue; + } + + @Override + public Object getEmbeddedObject() + { + if (type == null) { + return _reportError("Current token (" + _currToken + ") not of embeddable type"); + } + switch (type) { + case BYTES: + return bytesValue; + case EXT: + try { + return deserializedExtensionTypeValue(); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + case INT: + case LONG: + case DOUBLE: + case BOOL: + case BIG_INT: + case STRING: + case NULL: + return _reportError("Current token (" + _currToken + ") not of embeddable type"); + default: + return _reportError("Unexpected MessagePack value type: " + type); + } + } + + @Override + public NumberType getNumberType() + { + if (type == null) { + return null; + } + switch (type) { + case INT: + return NumberType.INT; + case LONG: + return NumberType.LONG; + case DOUBLE: + return NumberType.DOUBLE; + case BIG_INT: + return NumberType.BIG_INTEGER; + case NULL: + case BOOL: + case STRING: + case BYTES: + case EXT: + return null; + default: + return _reportError("Unexpected MessagePack value type: " + type); + } + } + + @Override + protected void _closeInput() throws IOException + { + if (StreamReadFeature.AUTO_CLOSE_SOURCE.enabledIn(_streamReadFeatures)) { + messageUnpacker.close(); + } + if (ownsThreadLocalUnpacker) { + Tuple tuple = messageUnpackerHolder.get(); + if (tuple != null) { + if (tuple.first() instanceof byte[]) { + // close() calls ArrayBufferInput.close() which sets buffer = null, + // releasing the byte[] payload reference held by the unpacker's input. + // The unpacker itself is kept alive for reuse on the next parse. + tuple.second().close(); + messageUnpackerHolder.set(new Tuple<>(null, tuple.second())); + } + else if (StreamReadFeature.AUTO_CLOSE_SOURCE.enabledIn(_streamReadFeatures)) { + // Stream is already closed above; release the reference so it doesn't + // linger on the thread until the next parse. + messageUnpackerHolder.set(new Tuple<>(null, tuple.second())); + } + // else: InputStream with AUTO_CLOSE_SOURCE disabled — keep the reference + // so the next parse on the same thread can detect same-stream reuse and + // avoid resetting the unpacker (which would discard its read-ahead buffer). + } + } + } + + @Override + protected void _releaseBuffers() + { + } + + @Override + public boolean isClosed() + { + return isClosed; + } + + @Override + public void close() + { + if (isClosed) { + return; + } + // Parsers are single-threaded by contract; close() is expected on the same + // thread that created the parser. Cross-thread close is not a supported use case. + try { + _closeInput(); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + finally { + isClosed = true; + } + } + + @Override + public TokenStreamContext streamReadContext() + { + return streamReadContext; + } + + @Override + public TokenStreamLocation currentTokenLocation() + { + // columnNr repurposed as byte offset; truncates for inputs > 2 GB + return new TokenStreamLocation(ioContext.contentReference(), tokenPosition, -1, (int) tokenPosition); + } + + @Override + public TokenStreamLocation currentLocation() + { + // columnNr repurposed as byte offset; truncates for inputs > 2 GB + return new TokenStreamLocation(ioContext.contentReference(), currentPosition, -1, (int) currentPosition); + } + + @Override + public String currentName() + { + // Simple, but need to look for START_OBJECT/ARRAY's "off-by-one" thing: + if (_currToken == JsonToken.START_OBJECT || _currToken == JsonToken.START_ARRAY) { + MessagePackReadContext parent = streamReadContext.getParent(); + return parent.currentName(); + } + return streamReadContext.currentName(); + } + + @Override + public Object streamReadInputSource() + { + return ioContext.contentReference().getRawContent(); + } + + @Override + public Object currentValue() + { + return streamReadContext.currentValue(); + } + + @Override + public void assignCurrentValue(Object v) + { + streamReadContext.assignCurrentValue(v); + } + + @Override + public boolean isNaN() + { + if (type == Type.DOUBLE) { + return Double.isNaN(doubleValue) || Double.isInfinite(doubleValue); + } + return false; + } + + public boolean isCurrentFieldId() + { + return this.type == Type.INT || this.type == Type.LONG; + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java new file mode 100644 index 00000000..ea830626 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java @@ -0,0 +1,213 @@ +// +// 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 tools.jackson.core.TokenStreamContext; +import tools.jackson.core.TokenStreamLocation; +import tools.jackson.core.exc.StreamReadException; +import tools.jackson.core.io.ContentReference; +import tools.jackson.core.json.DupDetector; + +/** + * Replacement of {@link tools.jackson.core.json.JsonReadContext} + * to support features needed by MessagePack format. + */ +public final class MessagePackReadContext + extends TokenStreamContext +{ + protected final MessagePackReadContext parent; + + protected final DupDetector dups; + + /** + * For fixed-size Arrays, Objects, this indicates expected number of entries. + */ + protected int expEntryCount; + + protected String currentName; + + protected Object currentValue; + + protected MessagePackReadContext child = null; + + public MessagePackReadContext(MessagePackReadContext parent, DupDetector dups, + int type, int expEntryCount) + { + super(); + this.parent = parent; + this.dups = dups; + _type = type; + this.expEntryCount = expEntryCount; + _index = -1; + _nestingDepth = parent == null ? 0 : parent._nestingDepth + 1; + } + + protected void reset(int type, int expEntryCount) + { + _type = type; + this.expEntryCount = expEntryCount; + _index = -1; + currentName = null; + currentValue = null; + if (dups != null) { + dups.reset(); + } + } + + @Override + public Object currentValue() + { + return currentValue; + } + + @Override + public void assignCurrentValue(Object v) + { + currentValue = v; + } + + public static MessagePackReadContext createRootContext(DupDetector dups) + { + return new MessagePackReadContext(null, dups, TYPE_ROOT, -1); + } + + public MessagePackReadContext createChildArrayContext(int expEntryCount) + { + MessagePackReadContext ctxt = child; + if (ctxt == null) { + ctxt = new MessagePackReadContext(this, + (dups == null) ? null : dups.child(), + TYPE_ARRAY, expEntryCount); + child = ctxt; + } + else { + ctxt.reset(TYPE_ARRAY, expEntryCount); + } + return ctxt; + } + + public MessagePackReadContext createChildObjectContext(int expEntryCount) + { + MessagePackReadContext ctxt = child; + if (ctxt == null) { + ctxt = new MessagePackReadContext(this, + (dups == null) ? null : dups.child(), + TYPE_OBJECT, expEntryCount); + child = ctxt; + return ctxt; + } + ctxt.reset(TYPE_OBJECT, expEntryCount); + return ctxt; + } + + @Override + public String currentName() + { + return currentName; + } + + @Override + public MessagePackReadContext getParent() + { + return parent; + } + + public boolean hasExpectedLength() + { + return (expEntryCount >= 0); + } + + public int getExpectedLength() + { + return expEntryCount; + } + + public boolean isEmpty() + { + return expEntryCount == 0; + } + + public int getRemainingExpectedLength() + { + int diff = expEntryCount - _index; + return Math.max(0, diff); + } + + public boolean acceptsBreakMarker() + { + return (expEntryCount < 0) && _type != TYPE_ROOT; + } + + public boolean expectMoreValues() + { + if (++_index == expEntryCount) { + return false; + } + return true; + } + + public TokenStreamLocation startLocation(ContentReference srcRef) + { + return new TokenStreamLocation(srcRef, 1L, -1, -1); + } + + public void setCurrentName(String name) + { + currentName = name; + if (dups != null) { + _checkDup(dups, name); + } + } + + private void _checkDup(DupDetector dd, String name) + { + // Null names (nil map keys) cannot be duplicate-checked via DupDetector + // because DupDetector.isDup(null) NPEs when a prior non-null name exists. + if (name != null && dd.isDup(name)) { + throw new StreamReadException(null, + "Duplicate field '" + name + "'", dd.findLocation()); + } + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(64); + switch (_type) { + case TYPE_ROOT: + sb.append("/"); + break; + case TYPE_ARRAY: + sb.append('['); + sb.append(getCurrentIndex()); + sb.append(']'); + break; + case TYPE_OBJECT: + sb.append('{'); + if (currentName != null) { + sb.append('"'); + sb.append(currentName.replace("\\", "\\\\").replace("\"", "\\\"")); + sb.append('"'); + } + else { + sb.append('?'); + } + sb.append('}'); + break; + } + return sb.toString(); + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java new file mode 100644 index 00000000..8bff3000 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java @@ -0,0 +1,142 @@ +// +// 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 tools.jackson.core.SerializableString; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +public class MessagePackSerializedString + implements SerializableString +{ + private static final Charset UTF8 = StandardCharsets.UTF_8; + private final Object value; + + public MessagePackSerializedString(Object value) + { + this.value = value; + } + + @Override + public String getValue() + { + return value == null ? null : value.toString(); + } + + @Override + public int charLength() + { + String v = getValue(); + return v == null ? 0 : v.length(); + } + + @Override + public char[] asQuotedChars() + { + String v = getValue(); + return v == null ? new char[0] : v.toCharArray(); + } + + @Override + public byte[] asUnquotedUTF8() + { + String v = getValue(); + return v == null ? new byte[0] : v.getBytes(UTF8); + } + + @Override + public byte[] asQuotedUTF8() + { + return asUnquotedUTF8(); + } + + @Override + public int appendQuotedUTF8(byte[] bytes, int i) + { + return appendUnquotedUTF8(bytes, i); + } + + @Override + public int appendQuoted(char[] chars, int i) + { + return appendUnquoted(chars, i); + } + + @Override + public int appendUnquotedUTF8(byte[] bytes, int i) + { + byte[] utf8 = asUnquotedUTF8(); + if (utf8.length > bytes.length - i) { + return -1; + } + System.arraycopy(utf8, 0, bytes, i, utf8.length); + return utf8.length; + } + + @Override + public int appendUnquoted(char[] chars, int i) + { + String v = getValue(); + if (v == null) { + return 0; + } + if (v.length() > chars.length - i) { + return -1; + } + v.getChars(0, v.length(), chars, i); + return v.length(); + } + + @Override + public int writeQuotedUTF8(OutputStream outputStream) throws IOException + { + return writeUnquotedUTF8(outputStream); + } + + @Override + public int writeUnquotedUTF8(OutputStream outputStream) throws IOException + { + byte[] utf8 = asUnquotedUTF8(); + outputStream.write(utf8); + return utf8.length; + } + + @Override + public int putQuotedUTF8(ByteBuffer byteBuffer) + { + return putUnquotedUTF8(byteBuffer); + } + + @Override + public int putUnquotedUTF8(ByteBuffer byteBuffer) + { + byte[] utf8 = asUnquotedUTF8(); + if (utf8.length > byteBuffer.remaining()) { + return -1; + } + byteBuffer.put(utf8); + return utf8.length; + } + + public Object getRawValue() + { + return value; + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java new file mode 100644 index 00000000..0aa72293 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java @@ -0,0 +1,44 @@ +// +// 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 tools.jackson.databind.JavaType; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.cfg.SerializerFactoryConfig; +import tools.jackson.databind.ser.BeanSerializerFactory; + +public class MessagePackSerializerFactory + extends BeanSerializerFactory +{ + public MessagePackSerializerFactory() + { + super(null); + } + + public MessagePackSerializerFactory(SerializerFactoryConfig config) + { + super(config); + } + + private static final MessagePackKeySerializer KEY_SERIALIZER = new MessagePackKeySerializer(); + + @Override + public ValueSerializer createKeySerializer(SerializationContext ctxt, JavaType keyType) + { + return KEY_SERIALIZER; + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackWriteContext.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackWriteContext.java new file mode 100644 index 00000000..99d7e2ac --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackWriteContext.java @@ -0,0 +1,139 @@ +// +// 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 tools.jackson.core.TokenStreamContext; +import tools.jackson.core.exc.StreamWriteException; +import tools.jackson.core.json.DupDetector; + +class MessagePackWriteContext extends TokenStreamContext +{ + private final MessagePackWriteContext parent; + private MessagePackWriteContext childToRecycle; + private DupDetector dups; + private String currentName; + private Object currentValue; + // For TYPE_OBJECT: true after writeName (expecting value), false after writeValue (expecting name) + private boolean gotName; + + private MessagePackWriteContext(int type, MessagePackWriteContext parent, DupDetector dups) + { + super(type, -1); + this.parent = parent; + this.dups = dups; + } + + private MessagePackWriteContext reset(int type, Object value) + { + _type = type; + _index = -1; + gotName = false; + currentName = null; + currentValue = value; + if (dups != null) { + dups.reset(); + } + return this; + } + + static MessagePackWriteContext createRootContext(DupDetector dups) + { + return new MessagePackWriteContext(TYPE_ROOT, null, dups); + } + + MessagePackWriteContext createChildArrayContext(Object value) + { + MessagePackWriteContext ctx = childToRecycle; + if (ctx == null) { + ctx = new MessagePackWriteContext(TYPE_ARRAY, this, dups == null ? null : dups.child()); + childToRecycle = ctx; + } + return ctx.reset(TYPE_ARRAY, value); + } + + MessagePackWriteContext createChildObjectContext(Object value) + { + MessagePackWriteContext ctx = childToRecycle; + if (ctx == null) { + ctx = new MessagePackWriteContext(TYPE_OBJECT, this, dups == null ? null : dups.child()); + childToRecycle = ctx; + } + return ctx.reset(TYPE_OBJECT, value); + } + + @Override + public MessagePackWriteContext getParent() + { + return parent; + } + + boolean isExpectingValue() + { + return _type == TYPE_OBJECT && gotName; + } + + boolean writeValue() + { + if (_type == TYPE_OBJECT) { + if (!gotName) { + return false; + } + gotName = false; + } + ++_index; + return true; + } + + boolean writeName(String name) throws StreamWriteException + { + if (_type != TYPE_OBJECT || gotName) { + return false; + } + currentName = name; + gotName = true; + if (dups != null) { + _checkDup(name); + } + return true; + } + + private void _checkDup(String name) throws StreamWriteException + { + // Null names (nil map keys) cannot be duplicate-checked via DupDetector + // because DupDetector.isDup(null) NPEs when a prior non-null name exists. + if (name != null && dups.isDup(name)) { + throw new StreamWriteException(null, "Duplicate Object property \"" + name + "\""); + } + } + + @Override + public String currentName() + { + return currentName; + } + + @Override + public Object currentValue() + { + return currentValue; + } + + @Override + public void assignCurrentValue(Object v) + { + currentValue = v; + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/PackageVersion.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/PackageVersion.java new file mode 100644 index 00000000..255ce1d6 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/PackageVersion.java @@ -0,0 +1,30 @@ +// 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 tools.jackson.core.Version; +import tools.jackson.core.Versioned; + +public class PackageVersion + implements Versioned +{ + public static final Version VERSION = new Version(0, 9, 12, null, "org.msgpack", "jackson-dataformat-msgpack-jackson3"); + + @Override + public Version version() + { + return VERSION; + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java new file mode 100644 index 00000000..a1df18e4 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java @@ -0,0 +1,107 @@ +// +// 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 tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.deser.std.StdDeserializer; +import tools.jackson.databind.module.SimpleModule; +import tools.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, SerializationContext provider) + { + try { + // Per-call allocation is a known limitation carried from the v2 module. + // Manually encoding the timestamp bytes would avoid it but duplicates + // msgpack-core's timestamp logic. Tracked as a future optimization. + ByteArrayOutputStream os = new ByteArrayOutputStream(); + 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.writePOJO(extensionType); + } + } + catch (IOException e) { + throw _wrapIOFailure(provider, e); + } + } + } + + private static class InstantDeserializer extends StdDeserializer + { + protected InstantDeserializer(Class vc) + { + super(vc); + } + + @Override + public Instant deserialize(JsonParser p, DeserializationContext ctxt) + { + try { + // Per-call allocation is a known limitation — see serialize() above. + MessagePackExtensionType ext = p.readValueAs(MessagePackExtensionType.class); + if (ext.getType() != EXT_TYPE) { + ctxt.reportInputMismatch(Instant.class, + "Unexpected extension type (0x%X) for Instant object", ext.getType() & 0xFF); + return null; // unreachable + } + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(ext.getData())) { + return unpacker.unpackTimestamp(new ExtensionTypeHeader(EXT_TYPE, ext.getData().length)); + } + } + catch (IOException e) { + throw _wrapIOFailure(ctxt, e); + } + } + } + + private TimestampExtensionModule() + { + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/Tuple.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/Tuple.java new file mode 100644 index 00000000..b0f720d8 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/Tuple.java @@ -0,0 +1,38 @@ +// +// 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; + +public class Tuple +{ + private final F first; + private final S second; + + public Tuple(F first, S second) + { + this.first = first; + this.second = second; + } + + public F first() + { + return first; + } + + public S second() + { + return second; + } +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java new file mode 100644 index 00000000..6a194a8c --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java @@ -0,0 +1,171 @@ +// +// 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 tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JacksonException; +import tools.jackson.core.TreeNode; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class ExampleOfTypeInformationSerDe + extends MessagePackDataformatTestBase +{ + static class A + { + private List list = new ArrayList(); + + public List getList() + { + return list; + } + + public void setList(List list) + { + this.list = list; + } + } + + static class B + { + private String str; + + public String getStr() + { + return str; + } + + public void setStr(String str) + { + this.str = str; + } + } + + @JsonSerialize(using = ObjectContainerSerializer.class) + @JsonDeserialize(using = ObjectContainerDeserializer.class) + static class ObjectContainer + { + private final Map objects; + + public ObjectContainer(Map objects) + { + this.objects = objects; + } + + public Map getObjects() + { + return objects; + } + } + + static class ObjectContainerSerializer + extends ValueSerializer + { + @Override + public void serialize(ObjectContainer value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException + { + gen.writeStartObject(); + HashMap metadata = new HashMap(); + for (Map.Entry entry : value.getObjects().entrySet()) { + metadata.put(entry.getKey(), entry.getValue().getClass().getName()); + } + gen.writePOJOProperty("__metadata", metadata); + gen.writePOJOProperty("objects", value.getObjects()); + gen.writeEndObject(); + } + } + + static class ObjectContainerDeserializer + extends ValueDeserializer + { + @Override + public ObjectContainer deserialize(JsonParser p, DeserializationContext ctxt) + throws JacksonException + { + ObjectContainer objectContainer = new ObjectContainer(new HashMap()); + TreeNode treeNode = p.readValueAsTree(); + + Map metadata = treeNode.get("__metadata") + .traverse(p.objectReadContext()) + .readValueAs(new TypeReference>() {}); + TreeNode dataMapTree = treeNode.get("objects"); + for (Map.Entry entry : metadata.entrySet()) { + try { + Object o = dataMapTree.get(entry.getKey()) + .traverse(p.objectReadContext()) + .readValueAs(Class.forName(entry.getValue())); + objectContainer.getObjects().put(entry.getKey(), o); + } + catch (ClassNotFoundException e) { + throw new RuntimeException("Failed to deserialize: " + entry, e); + } + } + + return objectContainer; + } + } + + @Test + public void test() + throws IOException + { + ObjectContainer objectContainer = new ObjectContainer(new HashMap()); + { + A a = new A(); + a.setList(Arrays.asList("first", "second", "third")); + objectContainer.getObjects().put("a", a); + + B b = new B(); + b.setStr("hello world"); + objectContainer.getObjects().put("b", b); + + Double pi = 3.14; + objectContainer.getObjects().put("pi", pi); + } + + ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory()); + byte[] bytes = objectMapper.writeValueAsBytes(objectContainer); + ObjectContainer restored = objectMapper.readValue(bytes, ObjectContainer.class); + + { + assertEquals(3, restored.getObjects().size()); + A a = (A) restored.getObjects().get("a"); + assertArrayEquals(new String[] {"first", "second", "third"}, a.getList().toArray()); + B b = (B) restored.getObjects().get("b"); + assertEquals("hello world", b.getStr()); + assertEquals(3.14, restored.getObjects().get("pi")); + } + } +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java new file mode 100644 index 00000000..23983a39 --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java @@ -0,0 +1,135 @@ +// +// 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 tools.jackson.core.JsonParser; +import tools.jackson.core.JacksonException; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.KeyDeserializer; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.deser.NullValueProvider; +import tools.jackson.databind.deser.jdk.JDKValueInstantiators; +import tools.jackson.databind.deser.jdk.MapDeserializer; +import tools.jackson.databind.jsontype.TypeDeserializer; +import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.type.TypeFactory; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.LinkedHashMap; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class MessagePackDataformatForFieldIdTest +{ + static class MessagePackMapDeserializer extends MapDeserializer + { + public static KeyDeserializer keyDeserializer = new KeyDeserializer() + { + @Override + public Object deserializeKey(String s, DeserializationContext deserializationContext) + throws JacksonException + { + JsonParser parser = deserializationContext.getParser(); + if (parser instanceof MessagePackParser) { + MessagePackParser p = (MessagePackParser) parser; + if (p.isCurrentFieldId()) { + return Integer.valueOf(s); + } + } + return s; + } + }; + + public MessagePackMapDeserializer() + { + super( + TypeFactory.createDefaultInstance().constructMapType(Map.class, Object.class, Object.class), + JDKValueInstantiators.findStdValueInstantiator(null, LinkedHashMap.class), + keyDeserializer, null, null); + } + + public MessagePackMapDeserializer(MapDeserializer src, KeyDeserializer keyDeser, + ValueDeserializer valueDeser, TypeDeserializer valueTypeDeser, NullValueProvider nuller, + Set ignorable, Set includable) + { + super(src, keyDeser, valueDeser, valueTypeDeser, nuller, ignorable, includable); + } + + @Override + protected MapDeserializer withResolved(KeyDeserializer keyDeser, TypeDeserializer valueTypeDeser, + ValueDeserializer valueDeser, NullValueProvider nuller, Set ignorable, + Set includable) + { + return new MessagePackMapDeserializer(this, keyDeser, (ValueDeserializer) valueDeser, valueTypeDeser, + nuller, ignorable, includable); + } + } + + @Test + public void testMixedKeys() + throws IOException + { + ObjectMapper mapper = MessagePackMapper.builder( + new MessagePackFactory() + .setSupportIntegerKeys(true) + ) + .addModule(new SimpleModule() + .addDeserializer(Map.class, new MessagePackMapDeserializer())) + .build(); + + Map map = new HashMap<>(); + map.put(1, "one"); + map.put("2", "two"); + + byte[] bytes = mapper.writeValueAsBytes(map); + Map deserializedInit = mapper.readValue(bytes, new TypeReference>() {}); + + Map expected = new HashMap<>(map); + Map actual = new HashMap<>(deserializedInit); + + assertEquals(expected, actual); + } + + @Test + public void testMixedKeysBackwardsCompatible() + throws IOException + { + ObjectMapper mapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule() + .addDeserializer(Map.class, new MessagePackMapDeserializer())) + .build(); + + Map map = new HashMap<>(); + map.put(1, "one"); + map.put("2", "two"); + + byte[] bytes = mapper.writeValueAsBytes(map); + Map deserializedInit = mapper.readValue(bytes, new TypeReference>() {}); + + Map expected = new HashMap<>(); + expected.put("1", "one"); + expected.put("2", "two"); + Map actual = new HashMap<>(deserializedInit); + + assertEquals(expected, actual); + } +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java new file mode 100644 index 00000000..c8083189 --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java @@ -0,0 +1,150 @@ +// +// 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 tools.jackson.databind.ObjectMapper; +import org.junit.Test; + +import java.io.IOException; +import java.nio.charset.Charset; + +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.containsString; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertArrayEquals; +import static org.hamcrest.MatcherAssert.assertThat; + +public class MessagePackDataformatForPojoTest + extends MessagePackDataformatTestBase +{ + @Test + public void testNormal() + throws IOException + { + byte[] bytes = objectMapper.writeValueAsBytes(normalPojo); + NormalPojo value = objectMapper.readValue(bytes, NormalPojo.class); + assertEquals(normalPojo.s, value.getS()); + assertEquals(normalPojo.bool, value.bool); + assertEquals(normalPojo.i, value.i); + assertEquals(normalPojo.l, value.l); + assertEquals(normalPojo.f, value.f, 0.000001f); + assertEquals(normalPojo.d, value.d, 0.000001f); + assertArrayEquals(normalPojo.b, value.b); + assertEquals(normalPojo.bi, value.bi); + assertEquals(normalPojo.suit, value.suit); + assertEquals(normalPojo.sMultibyte, value.sMultibyte); + } + + @Test + public void testNestedList() + throws IOException + { + byte[] bytes = objectMapper.writeValueAsBytes(nestedListPojo); + NestedListPojo value = objectMapper.readValue(bytes, NestedListPojo.class); + assertEquals(nestedListPojo.s, value.s); + assertArrayEquals(nestedListPojo.strs.toArray(), value.strs.toArray()); + } + + @Test + public void testNestedListComplex1() + throws IOException + { + byte[] bytes = objectMapper.writeValueAsBytes(nestedListComplexPojo1); + NestedListComplexPojo1 value = objectMapper.readValue(bytes, NestedListComplexPojo1.class); + assertEquals(nestedListComplexPojo1.s, value.s); + assertEquals(1, nestedListComplexPojo1.foos.size()); + assertEquals(nestedListComplexPojo1.foos.get(0).t, value.foos.get(0).t); + } + + @Test + public void testNestedListComplex2() + throws IOException + { + byte[] bytes = objectMapper.writeValueAsBytes(nestedListComplexPojo2); + NestedListComplexPojo2 value = objectMapper.readValue(bytes, NestedListComplexPojo2.class); + assertEquals(nestedListComplexPojo2.s, value.s); + assertEquals(2, nestedListComplexPojo2.foos.size()); + assertEquals(nestedListComplexPojo2.foos.get(0).t, value.foos.get(0).t); + assertEquals(nestedListComplexPojo2.foos.get(1).t, value.foos.get(1).t); + } + + @Test + public void testStrings() + throws IOException + { + byte[] bytes = objectMapper.writeValueAsBytes(stringPojo); + StringPojo value = objectMapper.readValue(bytes, StringPojo.class); + assertEquals(stringPojo.shortSingleByte, value.shortSingleByte); + assertEquals(stringPojo.longSingleByte, value.longSingleByte); + assertEquals(stringPojo.shortMultiByte, value.shortMultiByte); + assertEquals(stringPojo.longMultiByte, value.longMultiByte); + } + + @Test + public void testUsingCustomConstructor() + throws IOException + { + UsingCustomConstructorPojo orig = new UsingCustomConstructorPojo("komamitsu", 55); + byte[] bytes = objectMapper.writeValueAsBytes(orig); + UsingCustomConstructorPojo value = objectMapper.readValue(bytes, UsingCustomConstructorPojo.class); + assertEquals("komamitsu", value.name); + assertEquals(55, value.age); + } + + @Test + public void testIgnoringProperties() + throws IOException + { + IgnoringPropertiesPojo orig = new IgnoringPropertiesPojo(); + orig.internal = "internal"; + orig.external = "external"; + orig.setCode(1234); + byte[] bytes = objectMapper.writeValueAsBytes(orig); + IgnoringPropertiesPojo value = objectMapper.readValue(bytes, IgnoringPropertiesPojo.class); + assertEquals(0, value.getCode()); + assertEquals(null, value.internal); + assertEquals("external", value.external); + } + + @Test + public void testChangingPropertyNames() + throws IOException + { + ChangingPropertyNamesPojo orig = new ChangingPropertyNamesPojo(); + orig.setTheName("komamitsu"); + byte[] bytes = objectMapper.writeValueAsBytes(orig); + ChangingPropertyNamesPojo value = objectMapper.readValue(bytes, ChangingPropertyNamesPojo.class); + assertEquals("komamitsu", value.getTheName()); + } + + @Test + public void testSerializationWithoutSchema() + throws IOException + { + ObjectMapper objectMapper = MessagePackMapper.builder(factory) + .annotationIntrospector(new JsonArrayFormat()) + .build(); + byte[] bytes = objectMapper.writeValueAsBytes(complexPojo); + String schema = new String(bytes, Charset.forName("UTF-8")); + assertThat(schema, not(containsString("name"))); + ComplexPojo value = objectMapper.readValue(bytes, ComplexPojo.class); + assertEquals("komamitsu", value.name); + assertEquals(20, value.age); + assertArrayEquals(complexPojo.values.toArray(), value.values.toArray()); + assertEquals(complexPojo.grades.get("math"), value.grades.get("math")); + } +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java new file mode 100644 index 00000000..8a7f1e52 --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java @@ -0,0 +1,289 @@ +// +// 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.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import tools.jackson.databind.ObjectMapper; +import org.junit.After; +import org.junit.Before; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Collections; + +public class MessagePackDataformatTestBase +{ + protected MessagePackFactory factory; + protected ByteArrayOutputStream out; + protected ByteArrayInputStream in; + protected ObjectMapper objectMapper; + protected NormalPojo normalPojo; + protected NestedListPojo nestedListPojo; + protected NestedListComplexPojo1 nestedListComplexPojo1; + protected NestedListComplexPojo2 nestedListComplexPojo2; + protected StringPojo stringPojo; + protected TinyPojo tinyPojo; + protected ComplexPojo complexPojo; + + @Before + public void setup() + { + factory = new MessagePackFactory(); + objectMapper = new MessagePackMapper(factory); + out = new ByteArrayOutputStream(); + in = new ByteArrayInputStream(new byte[4096]); + + normalPojo = new NormalPojo(); + normalPojo.setS("komamitsu"); + normalPojo.bool = true; + normalPojo.i = Integer.MAX_VALUE; + normalPojo.l = Long.MIN_VALUE; + normalPojo.f = Float.MIN_VALUE; + normalPojo.d = Double.MAX_VALUE; + normalPojo.b = new byte[] {0x01, 0x02, (byte) 0xFE, (byte) 0xFF}; + normalPojo.bi = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE); + normalPojo.suit = Suit.HEART; + normalPojo.sMultibyte = "text文字"; + + nestedListPojo = new NestedListPojo(); + nestedListPojo.s = "a string"; + nestedListPojo.strs = Arrays.asList("string#1", "string#2", "string#3"); + + tinyPojo = new TinyPojo(); + tinyPojo.t = "t string"; + + nestedListComplexPojo1 = new NestedListComplexPojo1(); + nestedListComplexPojo1.s = "a string"; + nestedListComplexPojo1.foos = new ArrayList<>(); + nestedListComplexPojo1.foos.add(tinyPojo); + + nestedListComplexPojo2 = new NestedListComplexPojo2(); + nestedListComplexPojo2.foos = new ArrayList<>(); + nestedListComplexPojo2.foos.add(tinyPojo); + nestedListComplexPojo2.foos.add(tinyPojo); + nestedListComplexPojo2.s = "another string"; + + stringPojo = new StringPojo(); + stringPojo.shortSingleByte = "hello"; + stringPojo.longSingleByte = "helloworldhelloworldhelloworldhelloworldhelloworldhelloworldhelloworldhelloworld"; + stringPojo.shortMultiByte = "こんにちは"; + stringPojo.longMultiByte = "こんにちは、世界!!こんにちは、世界!!こんにちは、世界!!こんにちは、世界!!こんにちは、世界!!"; + + complexPojo = new ComplexPojo(); + complexPojo.name = "komamitsu"; + complexPojo.age = 20; + complexPojo.grades = Collections.singletonMap("math", 97); + complexPojo.values = Arrays.asList("one", "two", "three"); + } + + @After + public void teardown() + { + if (in != null) { + try { + in.close(); + } + catch (IOException e) { + e.printStackTrace(); + } + } + + if (out != null) { + try { + out.close(); + } + catch (IOException e) { + e.printStackTrace(); + } + } + } + + public enum Suit + { + SPADE, HEART, DIAMOND, CLUB; + } + + public static class NestedListPojo + { + public String s; + public List strs; + } + + public static class ComplexPojo + { + public String name; + public int age; + public List values; + public Map grades; + } + + public static class TinyPojo + { + public String t; + } + + public static class NestedListComplexPojo1 + { + public String s; + public List foos; + } + + public static class NestedListComplexPojo2 + { + public List foos; + public String s; + } + + public static class StringPojo + { + public String shortSingleByte; + public String longSingleByte; + public String shortMultiByte; + public String longMultiByte; + } + + public static class NormalPojo + { + String s; + public boolean bool; + public int i; + public long l; + public Float f; + public Double d; + public byte[] b; + public BigInteger bi; + public Suit suit; + public String sMultibyte; + + public String getS() + { + return s; + } + + public void setS(String s) + { + this.s = s; + } + } + + public static class BinKeyPojo + { + public byte[] b; + public String s; + } + + public static class UsingCustomConstructorPojo + { + final String name; + final int age; + + public UsingCustomConstructorPojo(@JsonProperty("name") String name, @JsonProperty("age") int age) + { + this.name = name; + this.age = age; + } + + public String getName() + { + return name; + } + + public int getAge() + { + return age; + } + } + + @JsonIgnoreProperties({"foo", "bar"}) + public static class IgnoringPropertiesPojo + { + int code; + + @JsonIgnore + public String internal; + + public String external; + + @JsonIgnore + public void setCode(int c) + { + code = c; + } + + public int getCode() + { + return code; + } + } + + public static class ChangingPropertyNamesPojo + { + String name; + + @JsonProperty("name") + public String getTheName() + { + return name; + } + + public void setTheName(String n) + { + name = n; + } + } + + protected interface FileSetup + { + void setup(File f) + throws Exception; + } + + protected File createTempFile() + throws Exception + { + return createTempFile(null); + } + + protected File createTempFile(FileSetup fileSetup) + throws Exception + { + File tempFile = File.createTempFile("test", "msgpack"); + tempFile.deleteOnExit(); + if (fileSetup != null) { + fileSetup.setup(tempFile); + } + return tempFile; + } + + protected OutputStream createTempFileOutputStream() + throws IOException + { + File tempFile = File.createTempFile("test", "msgpack"); + tempFile.deleteOnExit(); + return new FileOutputStream(tempFile); + } +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java new file mode 100644 index 00000000..25ef0fe2 --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java @@ -0,0 +1,198 @@ +// +// 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 tools.jackson.core.JsonEncoding; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.TSFBuilder; +import tools.jackson.core.TokenStreamFactory; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import org.junit.Test; +import org.msgpack.core.MessagePack; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.junit.Assert.assertEquals; +import static org.hamcrest.MatcherAssert.assertThat; + +public class MessagePackFactoryTest + extends MessagePackDataformatTestBase +{ + @Test + public void testCreateGenerator() + throws IOException + { + JsonEncoding enc = JsonEncoding.UTF8; + JsonGenerator generator = factory.createGenerator(out, enc); + assertEquals(MessagePackGenerator.class, generator.getClass()); + } + + @Test + public void testCreateParser() + throws IOException + { + JsonParser parser = factory.createParser(in); + assertEquals(MessagePackParser.class, parser.getClass()); + } + + @Test + public void testCopyWithDefaultConfig() + throws IOException + { + MessagePackFactory messagePackFactory = new MessagePackFactory(); + ObjectMapper objectMapper = new MessagePackMapper(messagePackFactory); + + // Use the original ObjectMapper in advance + { + byte[] bytes = objectMapper.writeValueAsBytes(1234); + assertThat(objectMapper.readValue(bytes, Integer.class), is(1234)); + } + + // Copy the factory + TokenStreamFactory copiedFactory = messagePackFactory.copy(); + assertThat(copiedFactory, is(instanceOf(MessagePackFactory.class))); + MessagePackFactory copiedMessagePackFactory = (MessagePackFactory) copiedFactory; + + assertThat(copiedMessagePackFactory.getPackerConfig().isStr8FormatSupport(), is(true)); + assertThat(copiedMessagePackFactory.getExtTypeCustomDesers(), is(nullValue())); + + // Check the copied factory works fine + ObjectMapper copiedObjectMapper = new MessagePackMapper(copiedMessagePackFactory); + Map map = new HashMap<>(); + map.put("one", 1); + Map deserialized = copiedObjectMapper + .readValue(objectMapper.writeValueAsBytes(map), new TypeReference>() {}); + assertThat(deserialized.size(), is(1)); + assertThat(deserialized.get("one"), is(1)); + } + + @Test + public void testRebuildWithDefaultConfig() + throws IOException + { + MessagePackFactory messagePackFactory = new MessagePackFactory(); + TSFBuilder builder = messagePackFactory.rebuild(); + assertThat(builder, is(instanceOf(MessagePackFactoryBuilder.class))); + + MessagePackFactory rebuilt = (MessagePackFactory) builder.build(); + assertThat(rebuilt, is(not(sameInstance(messagePackFactory)))); + assertThat(rebuilt.getPackerConfig().isStr8FormatSupport(), is(true)); + assertThat(rebuilt.getExtTypeCustomDesers(), is(nullValue())); + + ObjectMapper rebuiltObjectMapper = new MessagePackMapper(rebuilt); + byte[] bytes = rebuiltObjectMapper.writeValueAsBytes(42); + assertThat(rebuiltObjectMapper.readValue(bytes, Integer.class), is(42)); + } + + @Test + public void testRebuildWithAdvancedConfig() + throws IOException + { + ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); + extTypeCustomDesers.addCustomDeser((byte) 42, + new ExtensionTypeCustomDeserializers.Deser() + { + @Override + public Object deserialize(byte[] data) + throws IOException + { + TinyPojo pojo = new TinyPojo(); + pojo.t = new String(data); + return pojo; + } + } + ); + MessagePack.PackerConfig packerConfig = new MessagePack.PackerConfig().withStr8FormatSupport(false); + MessagePackFactory messagePackFactory = new MessagePackFactory(packerConfig); + messagePackFactory.setExtTypeCustomDesers(extTypeCustomDesers); + + MessagePackFactory rebuilt = (MessagePackFactory) messagePackFactory.rebuild().build(); + assertThat(rebuilt, is(not(sameInstance(messagePackFactory)))); + assertThat(rebuilt.getPackerConfig().isStr8FormatSupport(), is(false)); + assertThat(rebuilt.getExtTypeCustomDesers().getDeser((byte) 42), is(notNullValue())); + assertThat(rebuilt.getExtTypeCustomDesers().getDeser((byte) 43), is(nullValue())); + } + + @Test + public void testSnapshotReturnsNewInstance() + { + MessagePackFactory messagePackFactory = new MessagePackFactory(); + TokenStreamFactory snapshot = messagePackFactory.snapshot(); + assertThat(snapshot, is(not(sameInstance(messagePackFactory)))); + assertThat(snapshot, is(instanceOf(MessagePackFactory.class))); + } + + @Test + public void testCopyWithAdvancedConfig() + throws IOException + { + ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); + extTypeCustomDesers.addCustomDeser((byte) 42, + new ExtensionTypeCustomDeserializers.Deser() + { + @Override + public Object deserialize(byte[] data) + throws IOException + { + TinyPojo pojo = new TinyPojo(); + pojo.t = new String(data); + return pojo; + } + } + ); + + MessagePack.PackerConfig msgpackPackerConfig = new MessagePack.PackerConfig().withStr8FormatSupport(false); + + MessagePackFactory messagePackFactory = new MessagePackFactory(msgpackPackerConfig); + messagePackFactory.setExtTypeCustomDesers(extTypeCustomDesers); + + ObjectMapper objectMapper = new MessagePackMapper(messagePackFactory); + + // Use the original ObjectMapper in advance + { + byte[] bytes = objectMapper.writeValueAsBytes(1234); + assertThat(objectMapper.readValue(bytes, Integer.class), is(1234)); + } + + // Copy the factory + TokenStreamFactory copiedFactory = messagePackFactory.copy(); + assertThat(copiedFactory, is(instanceOf(MessagePackFactory.class))); + MessagePackFactory copiedMessagePackFactory = (MessagePackFactory) copiedFactory; + + assertThat(copiedMessagePackFactory.getPackerConfig().isStr8FormatSupport(), is(false)); + assertThat(copiedMessagePackFactory.getExtTypeCustomDesers().getDeser((byte) 42), is(notNullValue())); + assertThat(copiedMessagePackFactory.getExtTypeCustomDesers().getDeser((byte) 43), is(nullValue())); + + // Check the copied factory works fine + ObjectMapper copiedObjectMapper = new MessagePackMapper(copiedMessagePackFactory); + Map map = new HashMap<>(); + map.put("one", 1); + Map deserialized = copiedObjectMapper + .readValue(objectMapper.writeValueAsBytes(map), new TypeReference>() {}); + assertThat(deserialized.size(), is(1)); + assertThat(deserialized.get("one"), is(1)); + } +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java new file mode 100644 index 00000000..2d593846 --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java @@ -0,0 +1,1446 @@ +// +// 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.annotation.JsonProperty; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.core.JsonEncoding; +import tools.jackson.core.JacksonException; +import tools.jackson.core.ObjectReadContext; +import tools.jackson.core.ObjectWriteContext; +import tools.jackson.core.StreamWriteFeature; +import tools.jackson.core.TokenStreamContext; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.annotation.JsonSerialize; +import tools.jackson.databind.module.SimpleModule; +import org.junit.Test; +import org.msgpack.core.ExtensionTypeHeader; +import org.msgpack.core.MessagePack; +import org.msgpack.core.MessageUnpacker; +import org.msgpack.core.buffer.ArrayBufferInput; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; +import static org.hamcrest.MatcherAssert.assertThat; + +public class MessagePackGeneratorTest + extends MessagePackDataformatTestBase +{ + @Test + public void testGeneratorShouldWriteObject() + throws IOException + { + Map hashMap = new HashMap(); + // #1 + hashMap.put("str", "komamitsu"); + // #2 + hashMap.put("boolean", true); + // #3 + hashMap.put("int", Integer.MAX_VALUE); + // #4 + hashMap.put("long", Long.MIN_VALUE); + // #5 + hashMap.put("float", 3.14159f); + // #6 + hashMap.put("double", 3.14159d); + // #7 + hashMap.put("bin", new byte[] {0x00, 0x01, (byte) 0xFE, (byte) 0xFF}); + // #8 + Map childObj = new HashMap(); + childObj.put("co_str", "child#0"); + childObj.put("co_int", 12345); + hashMap.put("childObj", childObj); + // #9 + List childArray = new ArrayList(); + childArray.add("child#1"); + childArray.add(1.23f); + hashMap.put("childArray", childArray); + // #10 + byte[] hello = "hello".getBytes("UTF-8"); + hashMap.put("ext", new MessagePackExtensionType((byte) 17, hello)); + + long bitmap = 0; + byte[] bytes = objectMapper.writeValueAsBytes(hashMap); + MessageUnpacker messageUnpacker = MessagePack.newDefaultUnpacker(new ArrayBufferInput(bytes)); + assertEquals(hashMap.size(), messageUnpacker.unpackMapHeader()); + for (int i = 0; i < hashMap.size(); i++) { + String key = messageUnpacker.unpackString(); + if (key.equals("str")) { + // #1 + assertEquals("komamitsu", messageUnpacker.unpackString()); + bitmap |= 0x1 << 0; + } + else if (key.equals("boolean")) { + // #2 + assertTrue(messageUnpacker.unpackBoolean()); + bitmap |= 0x1 << 1; + } + else if (key.equals("int")) { + // #3 + assertEquals(Integer.MAX_VALUE, messageUnpacker.unpackInt()); + bitmap |= 0x1 << 2; + } + else if (key.equals("long")) { + // #4 + assertEquals(Long.MIN_VALUE, messageUnpacker.unpackLong()); + bitmap |= 0x1 << 3; + } + else if (key.equals("float")) { + // #5 + assertEquals(3.14159f, messageUnpacker.unpackFloat(), 0.01f); + bitmap |= 0x1 << 4; + } + else if (key.equals("double")) { + // #6 + assertEquals(3.14159d, messageUnpacker.unpackDouble(), 0.01f); + bitmap |= 0x1 << 5; + } + else if (key.equals("bin")) { + // #7 + assertEquals(4, messageUnpacker.unpackBinaryHeader()); + assertEquals((byte) 0x00, messageUnpacker.unpackByte()); + assertEquals((byte) 0x01, messageUnpacker.unpackByte()); + assertEquals((byte) 0xFE, messageUnpacker.unpackByte()); + assertEquals((byte) 0xFF, messageUnpacker.unpackByte()); + bitmap |= 0x1 << 6; + } + else if (key.equals("childObj")) { + // #8 + assertEquals(2, messageUnpacker.unpackMapHeader()); + for (int j = 0; j < 2; j++) { + String childKey = messageUnpacker.unpackString(); + if (childKey.equals("co_str")) { + assertEquals("child#0", messageUnpacker.unpackString()); + bitmap |= 0x1 << 7; + } + else if (childKey.equals("co_int")) { + assertEquals(12345, messageUnpacker.unpackInt()); + bitmap |= 0x1 << 8; + } + else { + assertTrue(false); + } + } + } + else if (key.equals("childArray")) { + // #9 + assertEquals(2, messageUnpacker.unpackArrayHeader()); + assertEquals("child#1", messageUnpacker.unpackString()); + assertEquals(1.23f, messageUnpacker.unpackFloat(), 0.01f); + bitmap |= 0x1 << 9; + } + else if (key.equals("ext")) { + // #10 + ExtensionTypeHeader header = messageUnpacker.unpackExtensionTypeHeader(); + assertEquals(17, header.getType()); + assertEquals(5, header.getLength()); + ByteBuffer payload = ByteBuffer.allocate(header.getLength()); + payload.flip(); + payload.limit(payload.capacity()); + messageUnpacker.readPayload(payload); + payload.flip(); + assertArrayEquals("hello".getBytes(), payload.array()); + bitmap |= 0x1 << 10; + } + else { + assertTrue(false); + } + } + assertEquals(0x07FF, bitmap); + } + + @Test + public void testGeneratorShouldWriteArray() + throws IOException + { + List array = new ArrayList(); + // #1 + array.add("komamitsu"); + // #2 + array.add(Integer.MAX_VALUE); + // #3 + array.add(Long.MIN_VALUE); + // #4 + array.add(3.14159f); + // #5 + array.add(3.14159d); + // #6 + Map childObject = new HashMap(); + childObject.put("str", "foobar"); + childObject.put("num", 123456); + array.add(childObject); + // #7 + array.add(false); + + long bitmap = 0; + byte[] bytes = objectMapper.writeValueAsBytes(array); + MessageUnpacker messageUnpacker = MessagePack.newDefaultUnpacker(new ArrayBufferInput(bytes)); + assertEquals(array.size(), messageUnpacker.unpackArrayHeader()); + // #1 + assertEquals("komamitsu", messageUnpacker.unpackString()); + // #2 + assertEquals(Integer.MAX_VALUE, messageUnpacker.unpackInt()); + // #3 + assertEquals(Long.MIN_VALUE, messageUnpacker.unpackLong()); + // #4 + assertEquals(3.14159f, messageUnpacker.unpackFloat(), 0.01f); + // #5 + assertEquals(3.14159d, messageUnpacker.unpackDouble(), 0.01f); + // #6 + assertEquals(2, messageUnpacker.unpackMapHeader()); + for (int i = 0; i < childObject.size(); i++) { + String key = messageUnpacker.unpackString(); + if (key.equals("str")) { + assertEquals("foobar", messageUnpacker.unpackString()); + bitmap |= 0x1 << 0; + } + else if (key.equals("num")) { + assertEquals(123456, messageUnpacker.unpackInt()); + bitmap |= 0x1 << 1; + } + else { + assertTrue(false); + } + } + assertEquals(0x3, bitmap); + // #7 + assertEquals(false, messageUnpacker.unpackBoolean()); + } + + @Test + public void testMessagePackGeneratorDirectly() + throws Exception + { + MessagePackFactory messagePackFactory = new MessagePackFactory(); + File tempFile = createTempFile(); + + JsonGenerator generator = messagePackFactory.createGenerator(ObjectWriteContext.empty(), tempFile, JsonEncoding.UTF8); + assertTrue(generator instanceof MessagePackGenerator); + generator.writeStartArray(); + generator.writeNumber(0); + generator.writeString("one"); + generator.writeNumber(2.0f); + generator.writeEndArray(); + generator.flush(); + generator.flush(); // intentional + generator.close(); + + FileInputStream fileInputStream = new FileInputStream(tempFile); + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(fileInputStream); + assertEquals(3, unpacker.unpackArrayHeader()); + assertEquals(0, unpacker.unpackInt()); + assertEquals("one", unpacker.unpackString()); + assertEquals(2.0f, unpacker.unpackFloat(), 0.001f); + assertFalse(unpacker.hasNext()); + } + + @Test + public void testWritePrimitives() + throws Exception + { + MessagePackFactory messagePackFactory = new MessagePackFactory(); + File tempFile = createTempFile(); + + JsonGenerator generator = messagePackFactory.createGenerator(ObjectWriteContext.empty(), tempFile, JsonEncoding.UTF8); + assertTrue(generator instanceof MessagePackGenerator); + generator.writeNumber(0); + generator.writeString("one"); + generator.writeNumber(2.0f); + generator.writeString("三"); + generator.writeString("444④"); + generator.flush(); + generator.close(); + + FileInputStream fileInputStream = new FileInputStream(tempFile); + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(fileInputStream); + assertEquals(0, unpacker.unpackInt()); + assertEquals("one", unpacker.unpackString()); + assertEquals(2.0f, unpacker.unpackFloat(), 0.001f); + assertEquals("三", unpacker.unpackString()); + assertEquals("444④", unpacker.unpackString()); + assertFalse(unpacker.hasNext()); + } + + @Test + public void testBigDecimal() + throws IOException + { + ObjectMapper mapper = new MessagePackMapper(new MessagePackFactory()); + + { + double d0 = 1.23456789; + double d1 = 1.23450000000000000000006789; + String d2 = "12.30"; + String d3 = "0.00001"; + List bigDecimals = Arrays.asList( + BigDecimal.valueOf(d0), + BigDecimal.valueOf(d1), + new BigDecimal(d2), + new BigDecimal(d3), + BigDecimal.valueOf(Double.MIN_VALUE), + BigDecimal.valueOf(Double.MAX_VALUE), + BigDecimal.valueOf(Double.MIN_NORMAL) + ); + + byte[] bytes = mapper.writeValueAsBytes(bigDecimals); + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes); + + assertEquals(bigDecimals.size(), unpacker.unpackArrayHeader()); + assertEquals(d0, unpacker.unpackDouble(), 0.000000000000001); + assertEquals(d1, unpacker.unpackDouble(), 0.000000000000001); + assertEquals(Double.valueOf(d2), unpacker.unpackDouble(), 0.000000000000001); + assertEquals(Double.valueOf(d3), unpacker.unpackDouble(), 0.000000000000001); + assertEquals(Double.MIN_VALUE, unpacker.unpackDouble(), 0.000000000000001); + assertEquals(Double.MAX_VALUE, unpacker.unpackDouble(), 0.000000000000001); + assertEquals(Double.MIN_NORMAL, unpacker.unpackDouble(), 0.000000000000001); + } + + { + BigDecimal decimal = new BigDecimal("1234.567890123456789012345678901234567890"); + List bigDecimals = Arrays.asList( + decimal + ); + + try { + mapper.writeValueAsBytes(bigDecimals); + assertTrue(false); + } + catch (IllegalArgumentException e) { + assertTrue(true); + } + } + } + + @Test + public void testBigDecimalCompareTo() + throws IOException + { + ObjectMapper mapper = new MessagePackMapper(new MessagePackFactory()); + + // BigDecimal with trailing zeros is representable as double — must not throw + BigDecimal trailingZeros = new BigDecimal("1.50"); + byte[] bytes = mapper.writeValueAsBytes(trailingZeros); + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes); + assertEquals(1.5, unpacker.unpackDouble(), 0.0); + + // BigDecimal with precision beyond double range must throw + BigDecimal tooHighPrecision = new BigDecimal("1.00000000000000000000000000000000000001"); + try { + mapper.writeValueAsBytes(tooHighPrecision); + assertTrue(false); + } + catch (IllegalArgumentException e) { + assertTrue(true); + } + } + + @Test + public void testEnableFeatureAutoCloseTarget() + throws IOException + { + OutputStream out = createTempFileOutputStream(); + ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory()); + List integers = Arrays.asList(1); + objectMapper.writeValue(out, integers); + assertThrows(JacksonException.class, () -> { + objectMapper.writeValue(out, integers); + }); + } + + @Test + public void testDisableFeatureAutoCloseTarget() + throws Exception + { + File tempFile = createTempFile(); + OutputStream out = new FileOutputStream(tempFile); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) + .build(); + List integers = Arrays.asList(1); + objectMapper.writeValue(out, integers); + objectMapper.writeValue(out, integers); + out.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(new FileInputStream(tempFile)); + assertEquals(1, unpacker.unpackArrayHeader()); + assertEquals(1, unpacker.unpackInt()); + assertEquals(1, unpacker.unpackArrayHeader()); + assertEquals(1, unpacker.unpackInt()); + } + + @Test + public void testWritePrimitiveObjectViaObjectMapper() + throws Exception + { + File tempFile = createTempFile(); + try (OutputStream out = Files.newOutputStream(tempFile.toPath())) { + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) + .build(); + objectMapper.writeValue(out, 1); + objectMapper.writeValue(out, "two"); + objectMapper.writeValue(out, 3.14); + objectMapper.writeValue(out, Arrays.asList(4)); + objectMapper.writeValue(out, 5L); + } + + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(new FileInputStream(tempFile))) { + assertEquals(1, unpacker.unpackInt()); + assertEquals("two", unpacker.unpackString()); + assertEquals(3.14, unpacker.unpackFloat(), 0.0001); + assertEquals(1, unpacker.unpackArrayHeader()); + assertEquals(4, unpacker.unpackInt()); + assertEquals(5, unpacker.unpackLong()); + } + } + + @Test + public void testInMultiThreads() + throws Exception + { + int threadCount = 8; + final int loopCount = 4000; + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + final ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) + .build(); + final List buffers = new ArrayList(threadCount); + List> results = new ArrayList>(); + + for (int ti = 0; ti < threadCount; ti++) { + buffers.add(new ByteArrayOutputStream()); + final int threadIndex = ti; + results.add(executorService.submit(new Callable() + { + @Override + public Exception call() + throws Exception + { + try { + for (int i = 0; i < loopCount; i++) { + objectMapper.writeValue(buffers.get(threadIndex), threadIndex); + } + return null; + } + catch (Exception e) { + return e; + } + } + })); + } + + for (int ti = 0; ti < threadCount; ti++) { + Future exceptionFuture = results.get(ti); + Exception exception = exceptionFuture.get(20, TimeUnit.SECONDS); + if (exception != null) { + throw exception; + } + else { + try (ByteArrayOutputStream outputStream = buffers.get(ti); + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(outputStream.toByteArray())) { + for (int i = 0; i < loopCount; i++) { + assertEquals(ti, unpacker.unpackInt()); + } + } + } + } + } + + @Test + public void testDisableStr8Support() + throws Exception + { + String str8LengthString = new String(new char[32]).replace("\0", "a"); + + ObjectMapper defaultMapper = new MessagePackMapper(new MessagePackFactory()); + byte[] resultWithStr8Format = defaultMapper.writeValueAsBytes(str8LengthString); + assertEquals(resultWithStr8Format[0], MessagePack.Code.STR8); + + MessagePack.PackerConfig config = new MessagePack.PackerConfig().withStr8FormatSupport(false); + ObjectMapper mapperWithConfig = new MessagePackMapper(new MessagePackFactory(config)); + byte[] resultWithoutStr8Format = mapperWithConfig.writeValueAsBytes(str8LengthString); + assertNotEquals(resultWithoutStr8Format[0], MessagePack.Code.STR8); + } + + interface NonStringKeyMapHolder + { + Map getIntMap(); + + void setIntMap(Map intMap); + + Map getLongMap(); + + void setLongMap(Map longMap); + + Map getFloatMap(); + + void setFloatMap(Map floatMap); + + Map getDoubleMap(); + + void setDoubleMap(Map doubleMap); + + Map getBigIntMap(); + + void setBigIntMap(Map doubleMap); + } + + public static class NonStringKeyMapHolderWithAnnotation + implements NonStringKeyMapHolder + { + @JsonSerialize(keyUsing = MessagePackKeySerializer.class) + private Map intMap = new HashMap(); + + @JsonSerialize(keyUsing = MessagePackKeySerializer.class) + private Map longMap = new HashMap(); + + @JsonSerialize(keyUsing = MessagePackKeySerializer.class) + private Map floatMap = new HashMap(); + + @JsonSerialize(keyUsing = MessagePackKeySerializer.class) + private Map doubleMap = new HashMap(); + + @JsonSerialize(keyUsing = MessagePackKeySerializer.class) + private Map bigIntMap = new HashMap(); + + @Override + public Map getIntMap() + { + return intMap; + } + + @Override + public void setIntMap(Map intMap) + { + this.intMap = intMap; + } + + @Override + public Map getLongMap() + { + return longMap; + } + + @Override + public void setLongMap(Map longMap) + { + this.longMap = longMap; + } + + @Override + public Map getFloatMap() + { + return floatMap; + } + + @Override + public void setFloatMap(Map floatMap) + { + this.floatMap = floatMap; + } + + @Override + public Map getDoubleMap() + { + return doubleMap; + } + + @Override + public void setDoubleMap(Map doubleMap) + { + this.doubleMap = doubleMap; + } + + @Override + public Map getBigIntMap() + { + return bigIntMap; + } + + @Override + public void setBigIntMap(Map bigIntMap) + { + this.bigIntMap = bigIntMap; + } + } + + public static class NonStringKeyMapHolderWithoutAnnotation + implements NonStringKeyMapHolder + { + private Map intMap = new HashMap(); + + private Map longMap = new HashMap(); + + private Map floatMap = new HashMap(); + + private Map doubleMap = new HashMap(); + + private Map bigIntMap = new HashMap(); + + @Override + public Map getIntMap() + { + return intMap; + } + + @Override + public void setIntMap(Map intMap) + { + this.intMap = intMap; + } + + @Override + public Map getLongMap() + { + return longMap; + } + + @Override + public void setLongMap(Map longMap) + { + this.longMap = longMap; + } + + @Override + public Map getFloatMap() + { + return floatMap; + } + + @Override + public void setFloatMap(Map floatMap) + { + this.floatMap = floatMap; + } + + @Override + public Map getDoubleMap() + { + return doubleMap; + } + + @Override + public void setDoubleMap(Map doubleMap) + { + this.doubleMap = doubleMap; + } + + @Override + public Map getBigIntMap() + { + return bigIntMap; + } + + @Override + public void setBigIntMap(Map bigIntMap) + { + this.bigIntMap = bigIntMap; + } + } + + @Test + @SuppressWarnings("unchecked") + public void testNonStringKey() + throws Exception + { + for (Class clazz : + Arrays.asList( + NonStringKeyMapHolderWithAnnotation.class, + NonStringKeyMapHolderWithoutAnnotation.class)) { + NonStringKeyMapHolder mapHolder = clazz.getConstructor().newInstance(); + mapHolder.getIntMap().put(Integer.MAX_VALUE, "i"); + mapHolder.getLongMap().put(Long.MIN_VALUE, "l"); + mapHolder.getFloatMap().put(Float.MAX_VALUE, "f"); + mapHolder.getDoubleMap().put(Double.MIN_VALUE, "d"); + mapHolder.getBigIntMap().put(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE), "bi"); + + ObjectMapper objectMapper; + if (mapHolder instanceof NonStringKeyMapHolderWithoutAnnotation) { + SimpleModule mod = new SimpleModule("test"); + mod.addKeySerializer(Object.class, new MessagePackKeySerializer()); + objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(mod) + .build(); + } + else { + objectMapper = new MessagePackMapper(new MessagePackFactory()); + } + + byte[] bytes = objectMapper.writeValueAsBytes(mapHolder); + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes); + assertEquals(5, unpacker.unpackMapHeader()); + for (int i = 0; i < 5; i++) { + String keyName = unpacker.unpackString(); + assertThat(unpacker.unpackMapHeader(), is(1)); + if (keyName.equals("intMap")) { + assertThat(unpacker.unpackInt(), is(Integer.MAX_VALUE)); + assertThat(unpacker.unpackString(), is("i")); + } + else if (keyName.equals("longMap")) { + assertThat(unpacker.unpackLong(), is(Long.MIN_VALUE)); + assertThat(unpacker.unpackString(), is("l")); + } + else if (keyName.equals("floatMap")) { + assertThat(unpacker.unpackFloat(), is(Float.MAX_VALUE)); + assertThat(unpacker.unpackString(), is("f")); + } + else if (keyName.equals("doubleMap")) { + assertThat(unpacker.unpackDouble(), is(Double.MIN_VALUE)); + assertThat(unpacker.unpackString(), is("d")); + } + else if (keyName.equals("bigIntMap")) { + assertThat(unpacker.unpackBigInteger(), is(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE))); + assertThat(unpacker.unpackString(), is("bi")); + } + else { + fail("Unexpected key name: " + keyName); + } + } + } + } + + @Test + public void testComplexTypeKey() + throws IOException + { + HashMap map = new HashMap(); + TinyPojo pojo = new TinyPojo(); + pojo.t = "foo"; + map.put(pojo, 42); + + SimpleModule mod = new SimpleModule("test"); + mod.addKeySerializer(TinyPojo.class, new MessagePackKeySerializer()); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(mod) + .build(); + byte[] bytes = objectMapper.writeValueAsBytes(map); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes); + assertThat(unpacker.unpackMapHeader(), is(1)); + assertThat(unpacker.unpackMapHeader(), is(1)); + assertThat(unpacker.unpackString(), is("t")); + assertThat(unpacker.unpackString(), is("foo")); + assertThat(unpacker.unpackInt(), is(42)); + } + + @Test + public void testComplexTypeKeyWithV06Format() + throws IOException + { + HashMap map = new HashMap(); + TinyPojo pojo = new TinyPojo(); + pojo.t = "foo"; + map.put(pojo, 42); + + SimpleModule mod = new SimpleModule("test"); + mod.addKeySerializer(TinyPojo.class, new MessagePackKeySerializer()); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .annotationIntrospector(new JsonArrayFormat()) + .addModule(mod) + .build(); + byte[] bytes = objectMapper.writeValueAsBytes(map); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes); + assertThat(unpacker.unpackMapHeader(), is(1)); + assertThat(unpacker.unpackArrayHeader(), is(1)); + assertThat(unpacker.unpackString(), is("foo")); + assertThat(unpacker.unpackInt(), is(42)); + } + + public static class IntegerSerializerStoringAsString + extends ValueSerializer + { + @Override + public void serialize(Integer value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException + { + gen.writeNumber(String.valueOf(value)); + } + } + + @Test + public void serializeStringAsInteger() + throws IOException + { + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule().addSerializer(Integer.class, new IntegerSerializerStoringAsString())) + .build(); + + assertThat( + MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(Integer.MAX_VALUE)).unpackInt(), + is(Integer.MAX_VALUE)); + } + + public static class LongSerializerStoringAsString + extends ValueSerializer + { + @Override + public void serialize(Long value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException + { + gen.writeNumber(String.valueOf(value)); + } + } + + @Test + public void serializeStringAsLong() + throws IOException + { + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule().addSerializer(Long.class, new LongSerializerStoringAsString())) + .build(); + + assertThat( + MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(Long.MIN_VALUE)).unpackLong(), + is(Long.MIN_VALUE)); + } + + public static class FloatSerializerStoringAsString + extends ValueSerializer + { + @Override + public void serialize(Float value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException + { + gen.writeNumber(String.valueOf(value)); + } + } + + @Test + public void serializeStringAsFloat() + throws IOException + { + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule().addSerializer(Float.class, new FloatSerializerStoringAsString())) + .build(); + + assertThat( + MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(Float.MAX_VALUE)).unpackFloat(), + is(Float.MAX_VALUE)); + } + + public static class DoubleSerializerStoringAsString + extends ValueSerializer + { + @Override + public void serialize(Double value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException + { + gen.writeNumber(String.valueOf(value)); + } + } + + @Test + public void serializeStringAsDouble() + throws IOException + { + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule().addSerializer(Double.class, new DoubleSerializerStoringAsString())) + .build(); + + assertThat( + MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(Double.MIN_VALUE)).unpackDouble(), + is(Double.MIN_VALUE)); + } + + public static class BigDecimalSerializerStoringAsString + extends ValueSerializer + { + @Override + public void serialize(BigDecimal value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException + { + gen.writeNumber(String.valueOf(value)); + } + } + + @Test + public void serializeStringAsBigDecimal() + throws IOException + { + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule().addSerializer(BigDecimal.class, new BigDecimalSerializerStoringAsString())) + .build(); + + BigDecimal bd = BigDecimal.valueOf(Long.MAX_VALUE).add(BigDecimal.ONE); + assertThat( + MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(bd)).unpackBigInteger(), + is(bd.toBigIntegerExact())); + } + + public static class BigIntegerSerializerStoringAsString + extends ValueSerializer + { + @Override + public void serialize(BigInteger value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException + { + gen.writeNumber(String.valueOf(value)); + } + } + + @Test + public void serializeStringAsBigInteger() + throws IOException + { + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule().addSerializer(BigInteger.class, new BigIntegerSerializerStoringAsString())) + .build(); + + BigInteger bi = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE); + assertThat( + MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(bi)).unpackBigInteger(), + is(bi)); + } + + @Test + public void testNestedSerialization() throws Exception + { + ObjectMapper objectMapper = new MessagePackMapper( + new MessagePackFactory().setReuseResourceInGenerator(false)); + OuterClass outerClass = objectMapper.readValue( + objectMapper.writeValueAsBytes(new OuterClass("Foo")), + OuterClass.class); + assertEquals("Foo", outerClass.getName()); + } + + static class OuterClass + { + private final String name; + + public OuterClass(@JsonProperty("name") String name) + { + this.name = name; + } + + public String getName() + { + ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory()); + InnerClass innerClass = objectMapper.readValue( + objectMapper.writeValueAsBytes(new InnerClass("Bar")), + InnerClass.class); + assertEquals("Bar", innerClass.getName()); + + return name; + } + } + + static class InnerClass + { + private final String name; + + public InnerClass(@JsonProperty("name") String name) + { + this.name = name; + } + + public String getName() + { + return name; + } + } + + @Test + public void testIsClosedAfterClose() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + assertFalse(generator.isClosed()); + generator.writeStartArray(); + generator.writeEndArray(); + generator.close(); + assertTrue(generator.isClosed()); + } + + @Test + public void testGeneratorReusableAfterRootContainerClose() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeNumber(1); + generator.writeEndArray(); + generator.flush(); + + // Write a second root value; currentState must have reset to IN_ROOT + generator.writeStartObject(); + generator.writeName("k"); + generator.writeNumber(2); + generator.writeEndObject(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + assertEquals(1, unpacker.unpackArrayHeader()); + assertEquals(1, unpacker.unpackInt()); + assertEquals(1, unpacker.unpackMapHeader()); + assertEquals("k", unpacker.unpackString()); + assertEquals(2, unpacker.unpackInt()); + } + + @Test + public void testWriteRawStringWithOffset() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeRaw("XXhelloXX", 2, 5); // "hello" + generator.writeEndArray(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + unpacker.unpackArrayHeader(); + assertEquals("hello", unpacker.unpackString()); + } + + @Test + public void testWriteStringCharArrayWithOffset() + throws IOException + { + // Padding chars before/after the actual content to test non-zero offset + char[] buf = new char[] {'X', 'X', 'h', 'e', 'l', 'l', 'o', 'X'}; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeString(buf, 2, 5); // "hello" + generator.writeEndArray(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + unpacker.unpackArrayHeader(); + assertEquals("hello", unpacker.unpackString()); + } + + @Test + public void testWriteStringCharArrayWithOffsetNonAscii() + throws IOException + { + // Non-ASCII to exercise the non-fast-path in getBytesIfAscii + char[] buf = new char[] {'X', '三', '四', '五', 'X'}; // 三四五 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeString(buf, 1, 3); + generator.writeEndArray(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + unpacker.unpackArrayHeader(); + assertEquals("三四五", unpacker.unpackString()); + } + + @Test + public void testWriteUTF8StringWithOffset() + throws IOException + { + // Padding bytes before/after to test non-zero offset in writeUTF8String + byte[] buf = new byte[] {'X', 'X', 'h', 'i', 'X'}; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeUTF8String(buf, 2, 2); // "hi" + generator.writeEndArray(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + unpacker.unpackArrayHeader(); + assertEquals("hi", unpacker.unpackString()); + } + + @Test + public void testWriteBinaryWithOffset() + throws IOException + { + byte[] data = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04}; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeBinary(data, 1, 3); // bytes 0x01, 0x02, 0x03 + generator.writeEndArray(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + unpacker.unpackArrayHeader(); + byte[] result = unpacker.readPayload(unpacker.unpackBinaryHeader()); + assertArrayEquals(new byte[] {0x01, 0x02, 0x03}, result); + } + + @Test + public void testWriteBinaryByteBufferWithOffset() + throws IOException + { + byte[] data = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04}; + ByteBuffer bb = ByteBuffer.wrap(data, 1, 3); // position=1, limit=4, remaining=3 + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + ObjectMapper mapper = new MessagePackMapper(factory); + mapper.writeValue(generator, bb); + generator.writeEndArray(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + unpacker.unpackArrayHeader(); + byte[] result = unpacker.readPayload(unpacker.unpackBinaryHeader()); + assertArrayEquals(new byte[] {0x01, 0x02, 0x03}, result); + } + + @Test + public void testStreamWriteContext() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + + TokenStreamContext ctx = generator.streamWriteContext(); + assertNotEquals(null, ctx); + assertTrue(ctx.inRoot()); + + generator.writeStartArray(); + ctx = generator.streamWriteContext(); + assertTrue(ctx.inArray()); + + generator.writeStartObject(); + ctx = generator.streamWriteContext(); + assertTrue(ctx.inObject()); + + generator.writeName("k"); + assertEquals("k", ctx.currentName()); + + generator.writeNumber(1); + generator.writeEndObject(); + ctx = generator.streamWriteContext(); + assertTrue(ctx.inArray()); + + generator.writeEndArray(); + ctx = generator.streamWriteContext(); + assertTrue(ctx.inRoot()); + + generator.close(); + } + + @Test + public void testCurrentValue() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + + Object pojo = new Object(); + generator.writeStartObject(pojo); + assertEquals(pojo, generator.currentValue()); + generator.writeName("k"); + generator.writeNumber(1); + generator.writeEndObject(); + generator.close(); + } + + @Test + public void testVersion() + { + assertNotEquals(null, factory.version()); + assertEquals("org.msgpack", factory.version().getGroupId()); + assertEquals("jackson-dataformat-msgpack-jackson3", factory.version().getArtifactId()); + } + + @Test + public void testSerializedStringMethods() throws IOException + { + MessagePackSerializedString s = new MessagePackSerializedString("hello"); + + byte[] utf8Target = new byte[10]; + int written = s.appendUnquotedUTF8(utf8Target, 2); + assertEquals(5, written); + assertArrayEquals(new byte[] {'h', 'e', 'l', 'l', 'o'}, Arrays.copyOfRange(utf8Target, 2, 7)); + + char[] charTarget = new char[10]; + written = s.appendUnquoted(charTarget, 3); + assertEquals(5, written); + assertEquals("hello", new String(charTarget, 3, 5)); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + written = s.writeUnquotedUTF8(baos); + assertEquals(5, written); + assertArrayEquals("hello".getBytes(java.nio.charset.StandardCharsets.UTF_8), baos.toByteArray()); + } + + // Regression: addValueNode must call writeContext.writeValue() so that the Jackson write + // context resets _gotPropertyId after each value. Without it, the second writeName() in the + // same object finds _gotPropertyId still set from the first writeName() and returns false + // without updating currentName(), leaving streamWriteContext().currentName() stale. + @Test + public void testWriteContextCurrentNameIsUpdatedForEveryProperty() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartObject(); + + generator.writeName("alpha"); + generator.writeNumber(1); + + generator.writeName("beta"); + assertEquals("beta", generator.streamWriteContext().currentName()); + generator.writeNumber(2); + + generator.writeEndObject(); + generator.close(); + } + + // Regression: writePropertyId() must call writeContext.writeName() when supportIntegerKeys + // is true. Without it, streamWriteContext() never learns a name was written, so currentName() + // returns null and any downstream code relying on context state (e.g. duplicate-name + // detection, error messages) sees wrong state. + @Test + public void testWritePropertyIdUpdatesWriteContext() + throws IOException + { + MessagePackFactory intKeyFactory = new MessagePackFactory().setSupportIntegerKeys(true); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = intKeyFactory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartObject(); + generator.writePropertyId(42L); + assertEquals("42", generator.streamWriteContext().currentName()); + generator.writeString("value"); + generator.writeEndObject(); + generator.close(); + } + + @Test + public void testNullSerializedStringKeyDoesNotThrowNpe() + throws IOException + { + // writeName(MessagePackSerializedString(null)) calls getValue() → null.toString() → NPE. + // A null key should be serialized as msgpack nil, not crash. + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = new MessagePackFactory().createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartObject(); + generator.writeName(new MessagePackSerializedString(null)); + generator.writeNumber(42); + generator.writeEndObject(); + generator.close(); + + // Verify the null key round-trips as PROPERTY_NAME with null current name + try (JsonParser parser = + new MessagePackFactory().createParser(ObjectReadContext.empty(), baos.toByteArray())) { + assertEquals(JsonToken.START_OBJECT, parser.nextToken()); + assertEquals(JsonToken.PROPERTY_NAME, parser.nextToken()); + assertNull(parser.currentName()); + assertEquals(JsonToken.VALUE_NUMBER_INT, parser.nextToken()); + assertEquals(42, parser.getIntValue()); + assertEquals(JsonToken.END_OBJECT, parser.nextToken()); + } + } + + @Test + public void testFlushMidWriteOnSecondRootContainerDoesNotCorruptState() + throws IOException + { + // After the first root container closes, isElementsClosed=true. + // Opening a second root container does not reset this flag, so a + // flush() call while the second container is still open will pack + // the incomplete node tree and wipe nodes[], corrupting subsequent writes. + MessagePackFactory factory = new MessagePackFactory(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + + generator.writeStartArray(); + generator.writeNumber(1); + generator.writeEndArray(); + + generator.writeStartArray(); // second root — isElementsClosed still true + generator.flush(); // must NOT pack the incomplete second array + generator.writeNumber(2); + generator.writeEndArray(); + + generator.close(); + + ObjectMapper mapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .build(); + try (JsonParser parser = + new MessagePackFactory().createParser(ObjectReadContext.empty(), baos.toByteArray())) { + List first = mapper.readValue(parser, new TypeReference>() {}); + assertEquals(Collections.singletonList(1), first); + List second = mapper.readValue(parser, new TypeReference>() {}); + assertEquals(Collections.singletonList(2), second); + } + } + + @Test + public void testRootScalarAfterClosedRootContainerPreservesOrder() + throws IOException + { + // A root scalar written after a closed root container must be emitted AFTER + // the container, not before. Without a fix, addValueNode() packs the scalar + // immediately (default branch) while the container stays buffered, reversing order. + MessagePackFactory factory = new MessagePackFactory(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeNumber(1); + generator.writeEndArray(); + generator.writeNumber(2); // root scalar — must come AFTER the array + generator.close(); + + ObjectMapper mapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .build(); + try (JsonParser parser = + new MessagePackFactory().createParser(ObjectReadContext.empty(), baos.toByteArray())) { + List list = mapper.readValue(parser, new TypeReference>() {}); + assertEquals(Collections.singletonList(1), list); + int scalar = mapper.readValue(parser, Integer.class); + assertEquals(2, scalar); + } + } + + // Bug: writeNumber(String) is missing "return this" after addValueNode(d) in the + // NaN/Infinity fallback branch. addValueNode() advances the write-context state + // and (for root-level scalars) writes bytes to the output stream before the + // method falls through to "throw new NumberFormatException(encodedValue)". + @Test + public void testWriteNumberStringNanAndInfinity() throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + // Avoid try-with-resources: if writeNumber throws, the implicit close + // flushes a half-written node list and masks the original failure. + JsonGenerator gen = factory.createGenerator(ObjectWriteContext.empty(), baos); + gen.writeStartArray(); + gen.writeNumber("NaN"); // Bug: NFE thrown after state mutation + gen.writeNumber("Infinity"); + gen.writeNumber("-Infinity"); + gen.writeEndArray(); + gen.close(); + + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), baos.toByteArray())) { + assertEquals(JsonToken.START_ARRAY, p.nextToken()); + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + assertTrue(Double.isNaN(p.getDoubleValue())); + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + assertEquals(Double.POSITIVE_INFINITY, p.getDoubleValue(), 0); + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + assertEquals(Double.NEGATIVE_INFINITY, p.getDoubleValue(), 0); + assertEquals(JsonToken.END_ARRAY, p.nextToken()); + } + } + + // Bug: same DupDetector NPE as the read-path bug, on the write side. + // writeName(SerializableString) calls writeContext.writeName(name.getValue()) + // where MessagePackSerializedString(null).getValue() == null. With + // STRICT_DUPLICATE_DETECTION enabled and a prior non-null key already seen, + // DupDetector.isDup(null) reaches name.equals(_firstName) → NPE. + @Test + public void testNullKeyWithWriteDupDetectionDoesNotNPE() throws IOException + { + MessagePackFactory f = new MessagePackFactoryBuilder() + .enable(StreamWriteFeature.STRICT_DUPLICATE_DETECTION) + .build(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (JsonGenerator gen = f.createGenerator(ObjectWriteContext.empty(), baos)) { + gen.writeStartObject(); + gen.writeName("foo"); + gen.writeNumber(1); + // MessagePackSerializedString(null).getValue() == null + gen.writeName(new MessagePackSerializedString(null)); // Bug: NPE here + gen.writeNumber(2); + gen.writeEndObject(); + } + } + + // Bug: MessagePackSerializedString.charLength() calls getValue().length() + // unconditionally; getValue() returns null when value is null → NPE. + @Test + public void testSerializedStringNullValueCharLengthDoesNotNPE() + { + MessagePackSerializedString s = new MessagePackSerializedString(null); + // Bug: null.length() → NullPointerException + assertEquals(0, s.charLength()); + } + + @Test + public void testMultipleRootContainersWithoutFlush() + throws IOException + { + // Writing two consecutive root-level containers on the same generator without + // an intervening flush() must not throw IndexOutOfBoundsException. + // The second root container sits at node index 1, so the root check + // "currentParentElementIndex == 0" incorrectly falls through to the + // nested-container path and calls nodes.get(-1). + MessagePackFactory factory = new MessagePackFactory(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeNumber(1); + generator.writeEndArray(); + // second root container — no flush() in between + generator.writeStartObject(); + generator.writeStringProperty("key", "value"); + generator.writeEndObject(); + generator.close(); + + // Verify both values were written correctly by reading from a shared parser + ObjectMapper mapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .build(); + try (JsonParser parser = + new MessagePackFactory().createParser(ObjectReadContext.empty(), baos.toByteArray())) { + List list = mapper.readValue(parser, new TypeReference>() {}); + assertEquals(Collections.singletonList(1), list); + Map map = mapper.readValue(parser, new TypeReference>() {}); + assertEquals(Collections.singletonMap("key", "value"), map); + } + } +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java new file mode 100644 index 00000000..776a1109 --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java @@ -0,0 +1,117 @@ +// +// 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 tools.jackson.core.JacksonException; +import org.junit.Test; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class MessagePackMapperTest +{ + static class PojoWithBigInteger + { + public BigInteger value; + } + + static class PojoWithBigDecimal + { + public BigDecimal value; + } + + private void shouldFailToHandleBigInteger(MessagePackMapper messagePackMapper) throws JacksonException + { + PojoWithBigInteger obj = new PojoWithBigInteger(); + obj.value = BigInteger.valueOf(Long.MAX_VALUE).multiply(BigInteger.valueOf(10)); + + try { + messagePackMapper.writeValueAsBytes(obj); + fail(); + } + catch (IllegalArgumentException e) { + // Expected + } + } + + private void shouldSuccessToHandleBigInteger(MessagePackMapper messagePackMapper) throws IOException + { + PojoWithBigInteger obj = new PojoWithBigInteger(); + obj.value = BigInteger.valueOf(Long.MAX_VALUE).multiply(BigInteger.valueOf(10)); + + byte[] converted = messagePackMapper.writeValueAsBytes(obj); + + PojoWithBigInteger deserialized = messagePackMapper.readValue(converted, PojoWithBigInteger.class); + assertEquals(obj.value, deserialized.value); + } + + private void shouldFailToHandleBigDecimal(MessagePackMapper messagePackMapper) throws JacksonException + { + PojoWithBigDecimal obj = new PojoWithBigDecimal(); + obj.value = new BigDecimal("1234567890.98765432100"); + + try { + messagePackMapper.writeValueAsBytes(obj); + fail(); + } + catch (IllegalArgumentException e) { + // Expected + } + } + + private void shouldSuccessToHandleBigDecimal(MessagePackMapper messagePackMapper) throws IOException + { + PojoWithBigDecimal obj = new PojoWithBigDecimal(); + obj.value = new BigDecimal("1234567890.98765432100"); + + byte[] converted = messagePackMapper.writeValueAsBytes(obj); + + PojoWithBigDecimal deserialized = messagePackMapper.readValue(converted, PojoWithBigDecimal.class); + assertEquals(obj.value, deserialized.value); + } + + @Test + public void handleBigIntegerAsString() throws IOException + { + shouldFailToHandleBigInteger(new MessagePackMapper()); + shouldSuccessToHandleBigInteger(MessagePackMapper.builder() + .handleBigIntegerAsString() + .build()); + } + + @Test + public void handleBigDecimalAsString() throws IOException + { + shouldFailToHandleBigDecimal(new MessagePackMapper()); + shouldSuccessToHandleBigDecimal(MessagePackMapper.builder() + .handleBigDecimalAsString() + .build()); + } + + @Test + public void handleBigIntegerAndBigDecimalAsString() throws IOException + { + MessagePackMapper messagePackMapper = MessagePackMapper.builder() + .handleBigIntegerAndBigDecimalAsString() + .build(); + shouldSuccessToHandleBigInteger(messagePackMapper); + shouldSuccessToHandleBigDecimal(messagePackMapper); + } +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java new file mode 100644 index 00000000..7b0ceee6 --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java @@ -0,0 +1,1524 @@ +// +// 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 tools.jackson.core.JsonParser; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonToken; +import tools.jackson.core.ObjectReadContext; +import tools.jackson.core.StreamReadFeature; +import tools.jackson.core.exc.UnexpectedEndOfInputException; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.KeyDeserializer; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.deser.std.StdDeserializer; +import tools.jackson.databind.module.SimpleModule; +import org.junit.Test; +import org.msgpack.core.MessagePack; +import org.msgpack.core.MessagePacker; +import org.msgpack.value.ExtensionValue; +import org.msgpack.value.MapValue; +import org.msgpack.value.ValueFactory; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.Serializable; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; + +public class MessagePackParserTest + extends MessagePackDataformatTestBase +{ + @Test + public void testParserShouldReadObject() + throws IOException + { + MessagePacker packer = MessagePack.newDefaultPacker(out); + packer.packMapHeader(9); + // #1 + packer.packString("str"); + packer.packString("foobar"); + // #2 + packer.packString("int"); + packer.packInt(Integer.MIN_VALUE); + // #3 + packer.packString("map"); + { + packer.packMapHeader(2); + packer.packString("child_str"); + packer.packString("bla bla bla"); + packer.packString("child_int"); + packer.packInt(Integer.MAX_VALUE); + } + // #4 + packer.packString("double"); + packer.packDouble(Double.MAX_VALUE); + // #5 + packer.packString("long"); + packer.packLong(Long.MIN_VALUE); + // #6 + packer.packString("bi"); + BigInteger bigInteger = new BigInteger(Long.toString(Long.MAX_VALUE)); + packer.packBigInteger(bigInteger.add(BigInteger.ONE)); + // #7 + packer.packString("array"); + { + packer.packArrayHeader(3); + packer.packFloat(Float.MIN_VALUE); + packer.packNil(); + packer.packString("array_child_str"); + } + // #8 + packer.packString("bool"); + packer.packBoolean(false); + // #9 + byte[] extPayload = {-80, -50, -25, -114, -25, 16, 60, 68}; + packer.packString("ext"); + packer.packExtensionTypeHeader((byte) 0, extPayload.length); + packer.writePayload(extPayload); + + packer.flush(); + + byte[] bytes = out.toByteArray(); + + TypeReference> typeReference = new TypeReference>() {}; + Map object = objectMapper.readValue(bytes, typeReference); + assertEquals(9, object.keySet().size()); + + int bitmap = 0; + for (Map.Entry entry : object.entrySet()) { + String k = entry.getKey(); + Object v = entry.getValue(); + if (k.equals("str")) { + // #1 + bitmap |= 1 << 0; + assertEquals("foobar", v); + } + else if (k.equals("int")) { + // #2 + bitmap |= 1 << 1; + assertEquals(Integer.MIN_VALUE, v); + } + else if (k.equals("map")) { + // #3 + bitmap |= 1 << 2; + @SuppressWarnings("unchecked") + Map child = (Map) v; + assertEquals(2, child.keySet().size()); + for (Map.Entry childEntry : child.entrySet()) { + String ck = childEntry.getKey(); + Object cv = childEntry.getValue(); + if (ck.equals("child_str")) { + bitmap |= 1 << 3; + assertEquals("bla bla bla", cv); + } + else if (ck.equals("child_int")) { + bitmap |= 1 << 4; + assertEquals(Integer.MAX_VALUE, cv); + } + } + } + else if (k.equals("double")) { + // #4 + bitmap |= 1 << 5; + assertEquals(Double.MAX_VALUE, (Double) v, 0.0001f); + } + else if (k.equals("long")) { + // #5 + bitmap |= 1 << 6; + assertEquals(Long.MIN_VALUE, v); + } + else if (k.equals("bi")) { + // #6 + bitmap |= 1 << 7; + BigInteger bi = new BigInteger(Long.toString(Long.MAX_VALUE)); + assertEquals(bi.add(BigInteger.ONE), v); + } + else if (k.equals("array")) { + // #7 + bitmap |= 1 << 8; + @SuppressWarnings("unchecked") + List expected = Arrays.asList((double) Float.MIN_VALUE, null, "array_child_str"); + assertEquals(expected, v); + } + else if (k.equals("bool")) { + // #8 + bitmap |= 1 << 9; + assertEquals(false, v); + } + else if (k.equals("ext")) { + // #9 + bitmap |= 1 << 10; + MessagePackExtensionType extensionType = (MessagePackExtensionType) v; + assertEquals(0, extensionType.getType()); + assertArrayEquals(extPayload, extensionType.getData()); + } + } + assertEquals(0x7FF, bitmap); + } + + @Test + public void testParserShouldReadArray() + throws IOException + { + MessagePacker packer = MessagePack.newDefaultPacker(out); + packer.packArrayHeader(11); + // #1 + packer.packArrayHeader(3); + { + packer.packLong(Long.MAX_VALUE); + packer.packNil(); + packer.packString("FOO BAR"); + } + // #2 + packer.packString("str"); + // #3 + packer.packInt(Integer.MAX_VALUE); + // #4 + packer.packLong(Long.MIN_VALUE); + // #5 + packer.packFloat(Float.MAX_VALUE); + // #6 + packer.packDouble(Double.MIN_VALUE); + // #7 + BigInteger bi = new BigInteger(Long.toString(Long.MAX_VALUE)); + bi = bi.add(BigInteger.ONE); + packer.packBigInteger(bi); + // #8 + byte[] bytes = new byte[] {(byte) 0xFF, (byte) 0xFE, 0x01, 0x00}; + packer.packBinaryHeader(bytes.length); + packer.writePayload(bytes); + // #9 + packer.packMapHeader(2); + { + packer.packString("child_map_name"); + packer.packString("komamitsu"); + packer.packString("child_map_age"); + packer.packInt(42); + } + // #10 + packer.packBoolean(true); + // #11 + byte[] extPayload = {-80, -50, -25, -114, -25, 16, 60, 68}; + packer.packExtensionTypeHeader((byte) -1, extPayload.length); + packer.writePayload(extPayload); + + packer.flush(); + + bytes = out.toByteArray(); + + TypeReference> typeReference = new TypeReference>() {}; + List array = objectMapper.readValue(bytes, typeReference); + assertEquals(11, array.size()); + int i = 0; + // #1 + @SuppressWarnings("unchecked") + List childArray = (List) array.get(i++); + { + int j = 0; + assertEquals(Long.MAX_VALUE, childArray.get(j++)); + assertEquals(null, childArray.get(j++)); + assertEquals("FOO BAR", childArray.get(j++)); + } + // #2 + assertEquals("str", array.get(i++)); + // #3 + assertEquals(Integer.MAX_VALUE, array.get(i++)); + // #4 + assertEquals(Long.MIN_VALUE, array.get(i++)); + // #5 + assertEquals(Float.MAX_VALUE, (Double) array.get(i++), 0.001f); + // #6 + assertEquals(Double.MIN_VALUE, (Double) array.get(i++), 0.001f); + // #7 + assertEquals(bi, array.get(i++)); + // #8 + byte[] bs = (byte[]) array.get(i++); + assertEquals(4, bs.length); + assertEquals((byte) 0xFF, bs[0]); + assertEquals((byte) 0xFE, bs[1]); + assertEquals((byte) 0x01, bs[2]); + assertEquals((byte) 0x00, bs[3]); + // #9 + @SuppressWarnings("unchecked") + Map childMap = (Map) array.get(i++); + { + assertEquals(2, childMap.keySet().size()); + for (Map.Entry entry : childMap.entrySet()) { + String k = entry.getKey(); + Object v = entry.getValue(); + if (k.equals("child_map_name")) { + assertEquals("komamitsu", v); + } + else if (k.equals("child_map_age")) { + assertEquals(42, v); + } + } + } + // #10 + assertEquals(true, array.get(i++)); + // #11 + MessagePackExtensionType extensionType = (MessagePackExtensionType) array.get(i++); + assertEquals(-1, extensionType.getType()); + assertArrayEquals(extPayload, extensionType.getData()); + } + + @Test + public void testMessagePackParserDirectly() + throws IOException + { + MessagePackFactory factory = new MessagePackFactory(); + File tempFile = File.createTempFile("msgpackTest", "msgpack"); + tempFile.deleteOnExit(); + + FileOutputStream fileOutputStream = new FileOutputStream(tempFile); + MessagePacker packer = MessagePack.newDefaultPacker(fileOutputStream); + packer.packMapHeader(2); + packer.packString("zero"); + packer.packInt(0); + packer.packString("one"); + packer.packFloat(1.0f); + packer.close(); + + JsonParser parser = factory.createParser(tempFile); + assertTrue(parser instanceof MessagePackParser); + + JsonToken jsonToken = parser.nextToken(); + assertEquals(JsonToken.START_OBJECT, jsonToken); + assertEquals(-1, parser.currentTokenLocation().getLineNr()); + assertEquals(0, parser.currentTokenLocation().getColumnNr()); + assertEquals(-1, parser.currentLocation().getLineNr()); + assertEquals(1, parser.currentLocation().getColumnNr()); + + jsonToken = parser.nextToken(); + assertEquals(JsonToken.PROPERTY_NAME, jsonToken); + assertEquals("zero", parser.currentName()); + assertEquals(1, parser.currentTokenLocation().getColumnNr()); + assertEquals(6, parser.currentLocation().getColumnNr()); + + jsonToken = parser.nextToken(); + assertEquals(JsonToken.VALUE_NUMBER_INT, jsonToken); + assertEquals(0, parser.getIntValue()); + assertEquals(6, parser.currentTokenLocation().getColumnNr()); + assertEquals(7, parser.currentLocation().getColumnNr()); + + jsonToken = parser.nextToken(); + assertEquals(JsonToken.PROPERTY_NAME, jsonToken); + assertEquals("one", parser.currentName()); + assertEquals(7, parser.currentTokenLocation().getColumnNr()); + assertEquals(11, parser.currentLocation().getColumnNr()); + + jsonToken = parser.nextToken(); + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, jsonToken); + assertEquals(1.0f, parser.getIntValue(), 0.001f); + assertEquals(11, parser.currentTokenLocation().getColumnNr()); + assertEquals(16, parser.currentLocation().getColumnNr()); + + jsonToken = parser.nextToken(); + assertEquals(JsonToken.END_OBJECT, jsonToken); + assertEquals(-1, parser.currentTokenLocation().getLineNr()); + assertEquals(16, parser.currentTokenLocation().getColumnNr()); + assertEquals(-1, parser.currentLocation().getLineNr()); + assertEquals(16, parser.currentLocation().getColumnNr()); + + parser.close(); + parser.close(); // Intentional + } + + @Test + public void testReadPrimitives() + throws Exception + { + MessagePackFactory factory = new MessagePackFactory(); + File tempFile = createTempFile(); + + FileOutputStream out = new FileOutputStream(tempFile); + MessagePacker packer = MessagePack.newDefaultPacker(out); + packer.packString("foo"); + packer.packDouble(3.14); + packer.packInt(Integer.MIN_VALUE); + packer.packLong(Long.MAX_VALUE); + packer.packBigInteger(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE)); + byte[] bytes = {0x00, 0x11, 0x22}; + packer.packBinaryHeader(bytes.length); + packer.writePayload(bytes); + packer.close(); + + JsonParser parser = factory.createParser(new FileInputStream(tempFile)); + assertEquals(JsonToken.VALUE_STRING, parser.nextToken()); + assertEquals("foo", parser.getString()); + + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, parser.nextToken()); + assertEquals(3.14, parser.getDoubleValue(), 0.0001); + assertEquals("3.14", parser.getString()); + + assertEquals(JsonToken.VALUE_NUMBER_INT, parser.nextToken()); + assertEquals(Integer.MIN_VALUE, parser.getIntValue()); + assertEquals(Integer.MIN_VALUE, parser.getLongValue()); + assertEquals("-2147483648", parser.getString()); + + assertEquals(JsonToken.VALUE_NUMBER_INT, parser.nextToken()); + assertEquals(Long.MAX_VALUE, parser.getLongValue()); + assertEquals("9223372036854775807", parser.getString()); + + assertEquals(JsonToken.VALUE_NUMBER_INT, parser.nextToken()); + assertEquals(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE), parser.getBigIntegerValue()); + assertEquals("9223372036854775808", parser.getString()); + + assertEquals(JsonToken.VALUE_EMBEDDED_OBJECT, parser.nextToken()); + assertEquals(bytes.length, parser.getBinaryValue().length); + assertEquals(bytes[0], parser.getBinaryValue()[0]); + assertEquals(bytes[1], parser.getBinaryValue()[1]); + assertEquals(bytes[2], parser.getBinaryValue()[2]); + } + + @Test + public void testBigDecimal() + throws IOException + { + double d0 = 1.23456789; + double d1 = 1.23450000000000000000006789; + MessagePacker packer = MessagePack.newDefaultPacker(out); + packer.packArrayHeader(5); + packer.packDouble(d0); + packer.packDouble(d1); + packer.packDouble(Double.MIN_VALUE); + packer.packDouble(Double.MAX_VALUE); + packer.packDouble(Double.MIN_NORMAL); + packer.flush(); + + ObjectMapper mapper = MessagePackMapper.builder(new MessagePackFactory()) + .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS) + .build(); + List objects = mapper.readValue(out.toByteArray(), new TypeReference>() {}); + assertEquals(5, objects.size()); + int idx = 0; + assertEquals(BigDecimal.valueOf(d0), objects.get(idx++)); + assertEquals(BigDecimal.valueOf(d1), objects.get(idx++)); + assertEquals(BigDecimal.valueOf(Double.MIN_VALUE), objects.get(idx++)); + assertEquals(BigDecimal.valueOf(Double.MAX_VALUE), objects.get(idx++)); + assertEquals(BigDecimal.valueOf(Double.MIN_NORMAL), objects.get(idx++)); + } + + private File createTestFile() + throws Exception + { + File tempFile = createTempFile(new FileSetup() + { + @Override + public void setup(File f) + throws IOException + { + MessagePack.newDefaultPacker(new FileOutputStream(f)) + .packArrayHeader(1).packInt(1) + .packArrayHeader(1).packInt(1) + .close(); + } + }); + return tempFile; + } + + @Test + public void testEnableFeatureAutoCloseSource() + throws Exception + { + File tempFile = createTestFile(); + MessagePackFactory factory = new MessagePackFactory(); + FileInputStream in = new FileInputStream(tempFile); + ObjectMapper objectMapper = MessagePackMapper.builder(factory) + .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .build(); + objectMapper.readValue(in, new TypeReference>() {}); + assertThrows(JacksonException.class, () -> { + objectMapper.readValue(in, new TypeReference>() {}); + }); + } + + @Test + public void testDisableFeatureAutoCloseSource() + throws Exception + { + File tempFile = createTestFile(); + FileInputStream in = new FileInputStream(tempFile); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(tools.jackson.core.StreamReadFeature.AUTO_CLOSE_SOURCE) + .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .build(); + objectMapper.readValue(in, new TypeReference>() {}); + objectMapper.readValue(in, new TypeReference>() {}); + } + + @Test + public void testParseBigDecimal() + throws IOException + { + ArrayList list = new ArrayList(); + list.add(new BigDecimal(Long.MAX_VALUE)); + ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory()); + byte[] bytes = objectMapper.writeValueAsBytes(list); + + ArrayList result = objectMapper.readValue( + bytes, new TypeReference>() {}); + assertEquals(list, result); + } + + @Test + public void testReadPrimitiveObjectViaObjectMapper() + throws Exception + { + File tempFile = createTempFile(); + FileOutputStream out = new FileOutputStream(tempFile); + + MessagePacker packer = MessagePack.newDefaultPacker(out); + packer.packString("foo"); + packer.packLong(Long.MAX_VALUE); + packer.packDouble(3.14); + byte[] bytes = {0x00, 0x11, 0x22}; + packer.packBinaryHeader(bytes.length); + packer.writePayload(bytes); + packer.close(); + + FileInputStream in = new FileInputStream(tempFile); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(tools.jackson.core.StreamReadFeature.AUTO_CLOSE_SOURCE) + .disable(tools.jackson.databind.DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .build(); + assertEquals("foo", objectMapper.readValue(in, new TypeReference() {})); + long l = objectMapper.readValue(in, new TypeReference() {}); + assertEquals(Long.MAX_VALUE, l); + double d = objectMapper.readValue(in, new TypeReference() {}); + assertEquals(3.14, d, 0.001); + byte[] bs = objectMapper.readValue(in, new TypeReference() {}); + assertEquals(bytes.length, bs.length); + assertEquals(bytes[0], bs[0]); + assertEquals(bytes[1], bs[1]); + assertEquals(bytes[2], bs[2]); + } + + @Test + public void testBinaryKey() + throws Exception + { + File tempFile = createTempFile(); + FileOutputStream out = new FileOutputStream(tempFile); + MessagePacker packer = MessagePack.newDefaultPacker(out); + packer.packMapHeader(2); + packer.packString("foo"); + packer.packDouble(3.14); + byte[] bytes = "bar".getBytes(); + packer.packBinaryHeader(bytes.length); + packer.writePayload(bytes); + packer.packLong(42); + packer.close(); + + ObjectMapper mapper = new MessagePackMapper(new MessagePackFactory()); + Map object = mapper.readValue(new FileInputStream(tempFile), new TypeReference>() {}); + assertEquals(2, object.size()); + assertEquals(3.14, object.get("foo")); + assertEquals(42, object.get("bar")); + } + + @Test + public void testBinaryKeyInNestedObject() + throws Exception + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + MessagePacker packer = MessagePack.newDefaultPacker(out); + packer.packArrayHeader(2); + packer.packMapHeader(1); + byte[] bytes = "bar".getBytes(); + packer.packBinaryHeader(bytes.length); + packer.writePayload(bytes); + packer.packInt(12); + packer.packInt(1); + packer.close(); + + ObjectMapper mapper = new MessagePackMapper(new MessagePackFactory()); + List objects = mapper.readValue(out.toByteArray(), new TypeReference>() {}); + assertEquals(2, objects.size()); + @SuppressWarnings(value = "unchecked") + Map map = (Map) objects.get(0); + assertEquals(1, map.size()); + assertEquals(12, map.get("bar")); + assertEquals(1, objects.get(1)); + } + + @Test + public void testByteArrayKey() + throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + MessagePacker messagePacker = MessagePack.newDefaultPacker(out).packMapHeader(2); + byte[] k0 = new byte[] {0}; + byte[] k1 = new byte[] {1}; + messagePacker.packBinaryHeader(1).writePayload(k0).packInt(10); + messagePacker.packBinaryHeader(1).writePayload(k1).packInt(11); + messagePacker.close(); + + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule() + .addKeyDeserializer(byte[].class, new KeyDeserializer() + { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) + throws JacksonException + { + return key.getBytes(); + } + })) + .build(); + + Map map = objectMapper.readValue( + out.toByteArray(), new TypeReference>() {}); + assertEquals(2, map.size()); + for (Map.Entry entry : map.entrySet()) { + if (Arrays.equals(entry.getKey(), k0)) { + assertEquals((Integer) 10, entry.getValue()); + } + else if (Arrays.equals(entry.getKey(), k1)) { + assertEquals((Integer) 11, entry.getValue()); + } + } + } + + @Test + public void testIntegerKey() + throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + MessagePacker messagePacker = MessagePack.newDefaultPacker(out).packMapHeader(2); + for (int i = 0; i < 2; i++) { + messagePacker.packInt(i).packInt(i + 10); + } + messagePacker.close(); + + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule() + .addKeyDeserializer(Integer.class, new KeyDeserializer() + { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) + throws JacksonException + { + return Integer.valueOf(key); + } + })) + .build(); + + Map map = objectMapper.readValue( + out.toByteArray(), new TypeReference>() {}); + assertEquals(2, map.size()); + assertEquals((Integer) 10, map.get(0)); + assertEquals((Integer) 11, map.get(1)); + } + + @Test + public void testFloatKey() + throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + MessagePacker messagePacker = MessagePack.newDefaultPacker(out).packMapHeader(2); + for (int i = 0; i < 2; i++) { + messagePacker.packFloat(i).packInt(i + 10); + } + messagePacker.close(); + + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule() + .addKeyDeserializer(Float.class, new KeyDeserializer() + { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) + throws JacksonException + { + return Float.valueOf(key); + } + })) + .build(); + + Map map = objectMapper.readValue( + out.toByteArray(), new TypeReference>() {}); + assertEquals(2, map.size()); + assertEquals((Integer) 10, map.get(0f)); + assertEquals((Integer) 11, map.get(1f)); + } + + @Test + public void testBooleanKey() + throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + MessagePacker messagePacker = MessagePack.newDefaultPacker(out).packMapHeader(2); + messagePacker.packBoolean(true).packInt(10); + messagePacker.packBoolean(false).packInt(11); + messagePacker.close(); + + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule() + .addKeyDeserializer(Boolean.class, new KeyDeserializer() + { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) + throws JacksonException + { + return Boolean.valueOf(key); + } + })) + .build(); + + Map map = objectMapper.readValue( + out.toByteArray(), new TypeReference>() {}); + assertEquals(2, map.size()); + assertEquals((Integer) 10, map.get(true)); + assertEquals((Integer) 11, map.get(false)); + } + + @Test + public void testNilKey() + throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + MessagePacker packer = MessagePack.newDefaultPacker(out).packMapHeader(1); + packer.packNil(); + packer.packInt(42); + packer.close(); + + JsonParser parser = new MessagePackMapper().createParser(out.toByteArray()); + assertEquals(JsonToken.START_OBJECT, parser.nextToken()); + assertEquals(JsonToken.PROPERTY_NAME, parser.nextToken()); + assertNull(parser.currentName()); + assertEquals(JsonToken.VALUE_NUMBER_INT, parser.nextToken()); + assertEquals(42, parser.getIntValue()); + assertEquals(JsonToken.END_OBJECT, parser.nextToken()); + parser.close(); + } + + @Test + public void extensionTypeCustomDeserializers() + throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + MessagePacker packer = MessagePack.newDefaultPacker(out); + packer.packArrayHeader(3); + // 0: Integer + packer.packInt(42); + // 1: String + packer.packString("foo bar"); + // 2: ExtensionType + { + packer.packExtensionTypeHeader((byte) 31, 4); + packer.addPayload(new byte[] {(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE}); + } + packer.close(); + + ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); + extTypeCustomDesers.addCustomDeser((byte) 31, new ExtensionTypeCustomDeserializers.Deser() { + @Override + public Object deserialize(byte[] data) + throws IOException + { + if (Arrays.equals(data, new byte[] {(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE})) { + return "Java"; + } + return "Not Java"; + } + } + ); + ObjectMapper objectMapper = + new MessagePackMapper(new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)); + + List values = objectMapper.readValue(new ByteArrayInputStream(out.toByteArray()), new TypeReference>() {}); + assertThat(values.size(), is(3)); + assertThat((Integer) values.get(0), is(42)); + assertThat((String) values.get(1), is("foo bar")); + assertThat((String) values.get(2), is("Java")); + } + + static class TripleBytesPojo + { + public byte first; + public byte second; + public byte third; + + public TripleBytesPojo(byte first, byte second, byte third) + { + this.first = first; + this.second = second; + this.third = third; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (!(o instanceof TripleBytesPojo)) { + return false; + } + + TripleBytesPojo that = (TripleBytesPojo) o; + + if (first != that.first) { + return false; + } + if (second != that.second) { + return false; + } + return third == that.third; + } + + @Override + public int hashCode() + { + int result = first; + result = 31 * result + (int) second; + result = 31 * result + (int) third; + return result; + } + + @Override + public String toString() + { + return String.format("%d-%d-%d", first, second, third); + } + + static class Deserializer + extends StdDeserializer + { + protected Deserializer() + { + super(TripleBytesPojo.class); + } + + @Override + public TripleBytesPojo deserialize(JsonParser p, DeserializationContext ctxt) + throws JacksonException + { + return TripleBytesPojo.deserialize(p.getBinaryValue()); + } + } + + static class TripleBytesKeyDeserializer + extends KeyDeserializer + { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) + throws JacksonException + { + String[] values = key.split("-"); + return new TripleBytesPojo( + Byte.parseByte(values[0]), + Byte.parseByte(values[1]), + Byte.parseByte(values[2])); + } + } + + static byte[] serialize(TripleBytesPojo obj) + { + return new byte[] { obj.first, obj.second, obj.third }; + } + + static TripleBytesPojo deserialize(byte[] bytes) + { + return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]); + } + } + + @Test + public void extensionTypeWithPojoInMap() + throws IOException + { + byte extTypeCode = 42; + + ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); + extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() + { + @Override + public Object deserialize(byte[] value) + throws IOException + { + return TripleBytesPojo.deserialize(value); + } + }); + + SimpleModule module = new SimpleModule(); + module.addDeserializer(TripleBytesPojo.class, new TripleBytesPojo.Deserializer()); + module.addKeyDeserializer(TripleBytesPojo.class, new TripleBytesPojo.TripleBytesKeyDeserializer()); + ObjectMapper objectMapper = MessagePackMapper.builder( + new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) + .addModule(module) + .build(); + + // Prepare serialized data + Map originalMap = new HashMap<>(); + byte[] serializedData; + { + ValueFactory.MapBuilder mapBuilder = ValueFactory.newMapBuilder(); + for (int i = 0; i < 4; i++) { + TripleBytesPojo keyObj = new TripleBytesPojo((byte) i, (byte) (i + 1), (byte) (i + 2)); + TripleBytesPojo valueObj = new TripleBytesPojo((byte) (i * 2), (byte) (i * 3), (byte) (i * 4)); + ExtensionValue k = ValueFactory.newExtension(extTypeCode, TripleBytesPojo.serialize(keyObj)); + ExtensionValue v = ValueFactory.newExtension(extTypeCode, TripleBytesPojo.serialize(valueObj)); + mapBuilder.put(k, v); + originalMap.put(keyObj, valueObj); + } + ByteArrayOutputStream output = new ByteArrayOutputStream(); + MessagePacker packer = MessagePack.newDefaultPacker(output); + MapValue mapValue = mapBuilder.build(); + mapValue.writeTo(packer); + packer.close(); + + serializedData = output.toByteArray(); + } + + Map deserializedMap = objectMapper.readValue(serializedData, + new TypeReference>() {}); + + assertEquals(originalMap.size(), deserializedMap.size()); + for (Map.Entry entry : originalMap.entrySet()) { + assertEquals(entry.getValue(), deserializedMap.get(entry.getKey())); + } + } + + @Test + public void extensionTypeWithUuidInMap() + throws IOException + { + byte extTypeCode = 42; + + ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); + extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() + { + @Override + public Object deserialize(byte[] value) + throws IOException + { + return UUID.fromString(new String(value)); + } + }); + + ObjectMapper objectMapper = + new MessagePackMapper(new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)); + + // Prepare serialized data + Map originalMap = new HashMap<>(); + byte[] serializedData; + { + ValueFactory.MapBuilder mapBuilder = ValueFactory.newMapBuilder(); + for (int i = 0; i < 4; i++) { + UUID keyObj = UUID.randomUUID(); + UUID valueObj = UUID.randomUUID(); + ExtensionValue k = ValueFactory.newExtension(extTypeCode, keyObj.toString().getBytes()); + ExtensionValue v = ValueFactory.newExtension(extTypeCode, valueObj.toString().getBytes()); + mapBuilder.put(k, v); + originalMap.put(keyObj, valueObj); + } + ByteArrayOutputStream output = new ByteArrayOutputStream(); + MessagePacker packer = MessagePack.newDefaultPacker(output); + MapValue mapValue = mapBuilder.build(); + mapValue.writeTo(packer); + packer.close(); + + serializedData = output.toByteArray(); + } + + Map deserializedMap = objectMapper.readValue(serializedData, + new TypeReference>() {}); + + assertEquals(originalMap.size(), deserializedMap.size()); + for (Map.Entry entry : originalMap.entrySet()) { + assertEquals(entry.getValue(), deserializedMap.get(entry.getKey())); + } + } + + @Test + public void parserShouldReadStrAsBin() + throws IOException + { + MessagePacker packer = MessagePack.newDefaultPacker(out); + packer.packMapHeader(2); + // #1 + packer.packString("s"); + packer.packString("foo"); + // #2 + packer.packString("b"); + packer.packString("bar"); + + packer.flush(); + + byte[] bytes = out.toByteArray(); + + BinKeyPojo binKeyPojo = objectMapper.readValue(bytes, BinKeyPojo.class); + assertEquals("foo", binKeyPojo.s); + assertArrayEquals("bar".getBytes(), binKeyPojo.b); + } + + @Test + public void deserializeStringAsInteger() + throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + MessagePack.newDefaultPacker(out).packString(String.valueOf(Integer.MAX_VALUE)).close(); + + Integer v = objectMapper.readValue(out.toByteArray(), Integer.class); + assertThat(v, is(Integer.MAX_VALUE)); + } + + @Test + public void deserializeStringAsLong() + throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + MessagePack.newDefaultPacker(out).packString(String.valueOf(Long.MIN_VALUE)).close(); + + Long v = objectMapper.readValue(out.toByteArray(), Long.class); + assertThat(v, is(Long.MIN_VALUE)); + } + + @Test + public void deserializeStringAsFloat() + throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + MessagePack.newDefaultPacker(out).packString(String.valueOf(Float.MAX_VALUE)).close(); + + Float v = objectMapper.readValue(out.toByteArray(), Float.class); + assertThat(v, is(Float.MAX_VALUE)); + } + + @Test + public void deserializeStringAsDouble() + throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + MessagePack.newDefaultPacker(out).packString(String.valueOf(Double.MIN_VALUE)).close(); + + Double v = objectMapper.readValue(out.toByteArray(), Double.class); + assertThat(v, is(Double.MIN_VALUE)); + } + + @Test + public void deserializeStringAsBigInteger() + throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + BigInteger bi = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE); + MessagePack.newDefaultPacker(out).packString(bi.toString()).close(); + + BigInteger v = objectMapper.readValue(out.toByteArray(), BigInteger.class); + assertThat(v, is(bi)); + } + + @Test + public void deserializeStringAsBigDecimal() + throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + BigDecimal bd = BigDecimal.valueOf(Double.MAX_VALUE); + MessagePack.newDefaultPacker(out).packString(bd.toString()).close(); + + BigDecimal v = objectMapper.readValue(out.toByteArray(), BigDecimal.class); + assertThat(v, is(bd)); + } + + @Test + public void handleMissingItemInArray() + throws IOException + { + MessagePacker packer = MessagePack.newDefaultPacker(out); + packer.packArrayHeader(3); + packer.packString("one"); + packer.packString("two"); + packer.close(); + + assertThrows(UnexpectedEndOfInputException.class, () -> { + objectMapper.readValue(out.toByteArray(), new TypeReference>() {}); + }); + } + + @Test + public void handleMissingKeyValueInMap() + { + MessagePacker packer = MessagePack.newDefaultPacker(out); + try { + packer.packMapHeader(3); + packer.packString("one"); + packer.packInt(1); + packer.packString("two"); + packer.packInt(2); + packer.close(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + + assertThrows(JacksonException.class, () -> { + objectMapper.readValue(out.toByteArray(), new TypeReference>() {}); + }); + } + + @Test + public void handleMissingValueInMap() + { + MessagePacker packer = MessagePack.newDefaultPacker(out); + try { + packer.packMapHeader(3); + packer.packString("one"); + packer.packInt(1); + packer.packString("two"); + packer.packInt(2); + packer.packString("three"); + packer.close(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + + assertThrows(JacksonException.class, () -> { + objectMapper.readValue(out.toByteArray(), new TypeReference>() {}); + }); + } + + @Test + public void testByteArrayThreadLocalClearedAfterClose() + throws IOException + { + ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory()); + + byte[] bytes = objectMapper.writeValueAsBytes(Arrays.asList(1, 2, 3)); + + // Parse once; this caches the byte array in the ThreadLocal + objectMapper.readValue(bytes, new TypeReference>() {}); + + // Parse again with the same byte array instance and AUTO_CLOSE_SOURCE enabled + // (default). The byte array reference should have been cleared from the + // ThreadLocal on close, so the second parse resets the unpacker and starts + // from the beginning rather than continuing from the end. + List result = objectMapper.readValue(bytes, new TypeReference>() {}); + assertEquals(Arrays.asList(1, 2, 3), result); + } + + @Test + public void testByteArrayReuseResetsUnpackerWhenAutoCloseSourceDisabled() + throws IOException + { + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(tools.jackson.core.StreamReadFeature.AUTO_CLOSE_SOURCE) + .build(); + + byte[] bytes = objectMapper.writeValueAsBytes(Arrays.asList(1, 2, 3)); + + // First parse succeeds + List first = objectMapper.readValue(bytes, new TypeReference>() {}); + assertEquals(Arrays.asList(1, 2, 3), first); + + // Second parse with the same byte[] instance and AUTO_CLOSE_SOURCE disabled. + // The byte[] source always triggers an unpacker reset (|| src instanceof byte[]), + // so the second parse succeeds and returns the correct result. + List second = objectMapper.readValue(bytes, new TypeReference>() {}); + assertEquals(Arrays.asList(1, 2, 3), second); + } + + @Test + public void testInputStreamSequentialReadsWithAutoCloseSourceDisabled() + throws IOException + { + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(tools.jackson.core.StreamReadFeature.AUTO_CLOSE_SOURCE) + .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .build(); + + // Two values packed sequentially into a single stream + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(objectMapper.writeValueAsBytes(Arrays.asList(1, 2, 3))); + out.write(objectMapper.writeValueAsBytes(Arrays.asList(4, 5, 6))); + ByteArrayInputStream stream = new ByteArrayInputStream(out.toByteArray()); + + // First parse reads the first value; unpacker may read ahead into the second value + List first = objectMapper.readValue(stream, new TypeReference>() {}); + assertEquals(Arrays.asList(1, 2, 3), first); + + // Second parse must read the second value from the same stream. + // If the source was incorrectly cleared from the ThreadLocal on close(), + // the unpacker's read-ahead buffer is dismissed and the second value is lost. + List second = objectMapper.readValue(stream, new TypeReference>() {}); + assertEquals(Arrays.asList(4, 5, 6), second); + } + + @Test + public void testGetStringOnNullToken() throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packNil(); + } + MessagePackFactory factory = new MessagePackFactory(); + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { + assertEquals(JsonToken.VALUE_NULL, p.nextToken()); + assertEquals("null", p.getString()); + } + } + + @Test + public void testGetStringOnBoolToken() throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packBoolean(true); + packer.packBoolean(false); + } + MessagePackFactory factory = new MessagePackFactory(); + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { + assertEquals(JsonToken.VALUE_TRUE, p.nextToken()); + assertEquals("true", p.getString()); + assertEquals(JsonToken.VALUE_FALSE, p.nextToken()); + assertEquals("false", p.getString()); + } + } + + @Test + public void testNumericAccessorsOnNullTokenThrow() throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packNil(); + } + byte[] bytes = out.toByteArray(); + MessagePackFactory factory = new MessagePackFactory(); + + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getIntValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getLongValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getDoubleValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getFloatValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getBigIntegerValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getDecimalValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getNumberValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + } + + @Test + public void testNumericAccessorsOnBoolTokenThrow() throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packBoolean(true); + } + byte[] bytes = out.toByteArray(); + MessagePackFactory factory = new MessagePackFactory(); + + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getIntValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getLongValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getDoubleValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + } + + @Test + public void testGetNumberTypeOnNonNumericTokens() throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packNil(); + packer.packBoolean(true); + packer.packString("hello"); + } + MessagePackFactory factory = new MessagePackFactory(); + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { + assertEquals(JsonToken.VALUE_NULL, p.nextToken()); + assertNull(p.getNumberType()); + assertEquals(JsonToken.VALUE_TRUE, p.nextToken()); + assertNull(p.getNumberType()); + assertEquals(JsonToken.VALUE_STRING, p.nextToken()); + assertNull(p.getNumberType()); + } + } + + @Test + public void testGetIntValueFromOutOfRangeDoubleThrows() throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packDouble(1e30); + } + MessagePackFactory factory = new MessagePackFactory(); + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + try { + p.getIntValue(); + fail("expected exception for out-of-range double"); + } + catch (JacksonException ignored) { } + } + } + + @Test + public void testGetLongValueFromOutOfRangeDoubleThrows() throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packDouble(1e30); + } + MessagePackFactory factory = new MessagePackFactory(); + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + try { + p.getLongValue(); + fail("expected exception for out-of-range double"); + } + catch (JacksonException ignored) { } + } + } + + @Test + public void testGetLongValueFromDoubleThatEqualsLongMaxValueRoundedUpThrows() throws IOException + { + // (double) Long.MAX_VALUE rounds up to 2^63, which exceeds Long.MAX_VALUE. + // The check `doubleValue > Long.MAX_VALUE` misses this value and silently + // saturates the cast to Long.MAX_VALUE instead of throwing. + double twoTo63 = 9.223372036854776E18; // exact double for 2^63 + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packDouble(twoTo63); + } + MessagePackFactory factory = new MessagePackFactory(); + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + try { + p.getLongValue(); + fail("expected exception for double value 2^63 which exceeds Long.MAX_VALUE"); + } + catch (JacksonException ignored) { } + } + } + + @Test + public void testNumericAccessorsOnStructuralTokenThrow() throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packMapHeader(0); + } + byte[] bytes = out.toByteArray(); + MessagePackFactory factory = new MessagePackFactory(); + + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + assertEquals(JsonToken.START_OBJECT, p.nextToken()); + try { + p.getIntValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + try { + p.getLongValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + try { + p.getDoubleValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + try { + p.getFloatValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + try { + p.getBigIntegerValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + try { + p.getDecimalValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + try { + p.getNumberValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + assertNull(p.getNumberType()); + } + } + + @Test + public void testGetIntValueFromFractionalDoubleTruncates() throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packDouble(3.7); + } + MessagePackFactory factory = new MessagePackFactory(); + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + assertEquals(3, p.getIntValue()); + } + } + + // Bug: MessagePackParser.close() has no re-entrancy guard; the base-class + // "if (_closed) { return; }" is bypassed because close() is fully overridden + // without calling super. A second call re-enters _closeInput(), which closes + // the underlying InputStream a second time. + @Test + public void testParserCloseIsIdempotent() throws IOException + { + byte[] bytes = objectMapper.writeValueAsBytes(42); + final int[] closeCount = {0}; + InputStream trackingStream = new ByteArrayInputStream(bytes) { + @Override + public void close() throws IOException + { + closeCount[0]++; + super.close(); + } + }; + + // reuseResourceInParser=false keeps ThreadLocal out of the picture so the + // test isolates the re-entrancy guard in close() itself. + MessagePackFactory nonReuseFactory = + new MessagePackFactory().setReuseResourceInParser(false); + JsonParser parser = + nonReuseFactory.createParser(ObjectReadContext.empty(), trackingStream); + parser.nextToken(); + parser.close(); // first close + parser.close(); // Bug: calls _closeInput() again — stream closed twice + + assertEquals("Stream should be closed exactly once", 1, closeCount[0]); + } + + // Bug: DupDetector.isDup(null) throws NullPointerException when a nil map key + // follows a non-null key and STRICT_DUPLICATE_DETECTION is enabled. + // The fault: name.equals(_firstName) where name is null and _firstName holds + // the prior non-null key string → NullPointerException. + @Test + public void testNilMapKeyWithDupDetectionDoesNotNPE() throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(baos)) { + packer.packMapHeader(2); + packer.packString("foo"); + packer.packInt(1); + packer.packNil(); // nil key — triggers DupDetector.isDup(null) + packer.packInt(2); + } + + MessagePackFactory f = new MessagePackFactoryBuilder() + .enable(StreamReadFeature.STRICT_DUPLICATE_DETECTION) + .build(); + + try (JsonParser p = f.createParser(ObjectReadContext.empty(), baos.toByteArray())) { + assertEquals(JsonToken.START_OBJECT, p.nextToken()); + assertEquals(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("foo", p.currentName()); + assertEquals(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals(JsonToken.PROPERTY_NAME, p.nextToken()); // Bug: NPE here + assertNull(p.currentName()); + assertEquals(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals(JsonToken.END_OBJECT, p.nextToken()); + } + } +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackWriteContextTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackWriteContextTest.java new file mode 100644 index 00000000..1f542fc7 --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackWriteContextTest.java @@ -0,0 +1,230 @@ +// +// 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 tools.jackson.core.JsonGenerator; +import tools.jackson.core.ObjectWriteContext; +import tools.jackson.core.TokenStreamContext; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class MessagePackWriteContextTest +{ + private final MessagePackFactory factory = new MessagePackFactory(); + + @Test + public void testRootContext() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (JsonGenerator gen = factory.createGenerator(ObjectWriteContext.empty(), baos)) { + TokenStreamContext ctx = gen.streamWriteContext(); + assertTrue(ctx.inRoot()); + assertFalse(ctx.inArray()); + assertFalse(ctx.inObject()); + assertNull(ctx.currentName()); + assertEquals(0, ctx.getCurrentIndex()); + assertEquals(0, ctx.getEntryCount()); + assertNull(ctx.getParent()); + gen.writeNumber(1); + } + } + + @Test + public void testArrayContext() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (JsonGenerator gen = factory.createGenerator(ObjectWriteContext.empty(), baos)) { + gen.writeStartArray(); + TokenStreamContext ctx = gen.streamWriteContext(); + + assertFalse(ctx.inRoot()); + assertTrue(ctx.inArray()); + assertFalse(ctx.inObject()); + assertNull(ctx.currentName()); + assertEquals(0, ctx.getCurrentIndex()); + assertEquals(0, ctx.getEntryCount()); + assertNotNull(ctx.getParent()); + assertTrue(ctx.getParent().inRoot()); + + gen.writeNumber(1); + assertEquals(0, ctx.getCurrentIndex()); + assertEquals(1, ctx.getEntryCount()); + + gen.writeNumber(2); + assertEquals(1, ctx.getCurrentIndex()); + assertEquals(2, ctx.getEntryCount()); + + gen.writeNumber(3); + assertEquals(2, ctx.getCurrentIndex()); + assertEquals(3, ctx.getEntryCount()); + + gen.writeEndArray(); + assertTrue(gen.streamWriteContext().inRoot()); + } + } + + @Test + public void testObjectContext() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (JsonGenerator gen = factory.createGenerator(ObjectWriteContext.empty(), baos)) { + gen.writeStartObject(); + TokenStreamContext ctx = gen.streamWriteContext(); + + assertFalse(ctx.inRoot()); + assertFalse(ctx.inArray()); + assertTrue(ctx.inObject()); + assertNull(ctx.currentName()); + assertEquals(0, ctx.getCurrentIndex()); + assertEquals(0, ctx.getEntryCount()); + + gen.writeName("first"); + assertEquals("first", ctx.currentName()); + assertEquals(0, ctx.getCurrentIndex()); + assertEquals(0, ctx.getEntryCount()); + + gen.writeNumber(1); + assertEquals("first", ctx.currentName()); + assertEquals(0, ctx.getCurrentIndex()); + assertEquals(1, ctx.getEntryCount()); + + gen.writeName("second"); + assertEquals("second", ctx.currentName()); + assertEquals(0, ctx.getCurrentIndex()); + assertEquals(1, ctx.getEntryCount()); + + gen.writeString("value"); + assertEquals(1, ctx.getCurrentIndex()); + assertEquals(2, ctx.getEntryCount()); + + gen.writeEndObject(); + assertTrue(gen.streamWriteContext().inRoot()); + } + } + + @Test + public void testNestedObjectInArray() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (JsonGenerator gen = factory.createGenerator(ObjectWriteContext.empty(), baos)) { + gen.writeStartArray(); + TokenStreamContext arrayCtx = gen.streamWriteContext(); + assertTrue(arrayCtx.inArray()); + + gen.writeStartObject(); + TokenStreamContext objectCtx = gen.streamWriteContext(); + assertTrue(objectCtx.inObject()); + assertSame(arrayCtx, objectCtx.getParent()); + + gen.writeName("key"); + assertEquals("key", objectCtx.currentName()); + assertEquals("key", gen.streamWriteContext().currentName()); + + gen.writeNumber(42); + gen.writeEndObject(); + + assertSame(arrayCtx, gen.streamWriteContext()); + assertEquals(0, arrayCtx.getCurrentIndex()); + assertEquals(1, arrayCtx.getEntryCount()); + + gen.writeEndArray(); + } + } + + @Test + public void testCurrentValue() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (JsonGenerator gen = factory.createGenerator(ObjectWriteContext.empty(), baos)) { + Object pojo = new Object(); + gen.writeStartObject(pojo); + TokenStreamContext ctx = gen.streamWriteContext(); + assertEquals(pojo, ctx.currentValue()); + + Object inner = new Object(); + gen.writeName("arr"); + gen.writeStartArray(inner); + assertEquals(inner, gen.streamWriteContext().currentValue()); + gen.writeEndArray(); + + gen.writeEndObject(); + } + } + + @Test + public void testChildContextIsReused() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (JsonGenerator gen = factory.createGenerator(ObjectWriteContext.empty(), baos)) { + gen.writeStartArray(); + + gen.writeStartObject(); + TokenStreamContext first = gen.streamWriteContext(); + gen.writeName("k"); + gen.writeNumber(1); + gen.writeEndObject(); + + gen.writeStartObject(); + TokenStreamContext second = gen.streamWriteContext(); + assertSame(first, second); + assertNull(second.currentName()); + assertEquals(0, second.getCurrentIndex()); + assertEquals(0, second.getEntryCount()); + + gen.writeName("k2"); + gen.writeNumber(2); + gen.writeEndObject(); + + gen.writeEndArray(); + } + } + + @Test + public void testAssignCurrentValue() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (JsonGenerator gen = factory.createGenerator(ObjectWriteContext.empty(), baos)) { + gen.writeStartObject(); + TokenStreamContext ctx = gen.streamWriteContext(); + assertNull(ctx.currentValue()); + + Object v = new Object(); + gen.assignCurrentValue(v); + assertSame(v, ctx.currentValue()); + assertSame(v, gen.currentValue()); + + gen.writeName("k"); + gen.writeNumber(1); + gen.writeEndObject(); + } + } +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java new file mode 100644 index 00000000..36f5c97f --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java @@ -0,0 +1,217 @@ +// +// 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 tools.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 ObjectMapper objectMapper; + 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 = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(TimestampExtensionModule.INSTANCE) + .build(); + } + + @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); + + 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); + + 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); + + 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); + } +} diff --git a/project/plugins.sbt b/project/plugins.sbt index 2d12cde5..28f255ba 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,5 +6,6 @@ addSbtPlugin("org.xerial.sbt" % "sbt-jcheckstyle" % "0.2.1") addSbtPlugin("com.github.sbt" % "sbt-osgi" % "0.10.0") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.1") addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.1") +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") scalacOptions ++= Seq("-deprecation", "-feature") From 531d3d0e31a1ae8a9ffd78e762e08fb920e35512 Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Wed, 8 Jul 2026 09:19:19 +0900 Subject: [PATCH 2/7] Document Jackson 2/3 artifact mapping in top-level README Address PR #987 feedback asking for a clear artifact-name mapping between jackson-dataformat-msgpack (Jackson 2) and jackson-dataformat-msgpack-jackson3 (Jackson 3) to reduce upgrade confusion. --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f23af0b7..e76da08d 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,16 @@ For using DirectByteBuffer (off-heap memory access methods) in JDK17, you need t ### Integration with Jackson ObjectMapper (jackson-databind) -msgpack-java supports serialization and deserialization of Java objects through [jackson-databind](https://github.com/FasterXML/jackson-databind). -For details, see [msgpack-jackson/README.md](https://github.com/msgpack/msgpack-java/blob/develop/msgpack-jackson/README.md). The template-based serialization mechanism used in v06 is deprecated. +msgpack-java supports serialization and deserialization of Java objects through [jackson-databind](https://github.com/FasterXML/jackson-databind). The template-based serialization mechanism used in v06 is deprecated. + +Two artifacts are published depending on which Jackson major version your project uses: + +| Jackson version | Artifact | Requirements | +| --- | --- | --- | +| Jackson 2.x | [`jackson-dataformat-msgpack`](https://github.com/msgpack/msgpack-java/blob/develop/msgpack-jackson/README.md) | Java 8+ | +| Jackson 3.x | [`jackson-dataformat-msgpack-jackson3`](https://github.com/msgpack/msgpack-java/blob/develop/msgpack-jackson3/README.md) | Java 17+ | + +Use `jackson-dataformat-msgpack` if your project still depends on Jackson 2.x, or `jackson-dataformat-msgpack-jackson3` if you've upgraded to Jackson 3.x. See each module's README for install instructions and usage details. - [Release Notes](https://github.com/msgpack/msgpack-java/blob/develop/RELEASE_NOTES.md) From 4f6851aec6b4172d42aa91bcc50c5bc26e1e4b0b Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Wed, 8 Jul 2026 09:37:45 +0900 Subject: [PATCH 3/7] Bump sbt-jmh to 0.4.8 for sbt 2 compatibility main migrated the build to sbt 2 (feb87efc) without sbt-jmh, since sbt-jmh 0.4.7 has no sbt-2-compatible artifact. This branch's msgpack-jackson3 JMH benchmarks depend on sbt-jmh, so after merging main the plugin fails to resolve. 0.4.8 publishes an sbt_2_3 build. --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index ddf498f3..b92983e7 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,6 +6,6 @@ addSbtPlugin("org.xerial.sbt" % "sbt-jcheckstyle" % "0.3.0") addSbtPlugin("com.github.sbt" % "sbt-osgi" % "0.11.0-RC1") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.1") addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.1") -addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8") scalacOptions ++= Seq("-deprecation", "-feature") From ac9612205d2f97252ab0b530bb181b6ee19c70eb Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Tue, 28 Jul 2026 13:28:21 -0700 Subject: [PATCH 4/7] Move msgpack-jackson (Jackson 2) to msgpack-jackson2, msgpack-jackson3 to msgpack-jackson Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_014ZZUaHusAjVx6SNu4sA42s --- msgpack-jackson/README.md | 598 +++++----- .../dataformat/benchmark/BenchmarkState.java | 0 .../benchmark/MsgpackReadBenchmark.java | 0 .../benchmark/MsgpackWriteBenchmark.java | 0 .../dataformat/benchmark/NopOutputStream.java | 0 .../benchmark/WriteUTF8StringBenchmark.java | 0 .../dataformat/benchmark/model/Image.java | 0 .../benchmark/model/MediaContent.java | 0 .../dataformat/benchmark/model/MediaItem.java | 0 .../benchmark/model/MediaItems.java | 0 .../dataformat/benchmark/model/Player.java | 0 .../dataformat/benchmark/model/Size.java | 0 .../jackson/dataformat/JsonArrayFormat.java | 46 +- .../dataformat/MessagePackExtensionType.java | 43 +- .../dataformat/MessagePackFactory.java | 172 ++- .../dataformat/MessagePackFactoryBuilder.java | 0 .../dataformat/MessagePackGenerator.java | 699 +++++++---- .../dataformat/MessagePackKeySerializer.java | 13 +- .../jackson/dataformat/MessagePackMapper.java | 89 +- .../jackson/dataformat/MessagePackParser.java | 489 +++++--- .../dataformat/MessagePackReadContext.java | 124 +- .../MessagePackSerializedString.java | 56 +- .../MessagePackSerializerFactory.java | 31 +- .../dataformat/MessagePackWriteContext.java | 0 .../jackson/dataformat/PackageVersion.java | 0 .../dataformat/TimestampExtensionModule.java | 81 +- .../org/msgpack/jackson/dataformat/Tuple.java | 3 - .../ExampleOfTypeInformationSerDe.java | 48 +- .../MessagePackDataformatForFieldIdTest.java | 55 +- .../MessagePackDataformatForPojoTest.java | 19 +- .../MessagePackDataformatTestBase.java | 10 +- .../dataformat/MessagePackFactoryTest.java | 200 ++-- .../dataformat/MessagePackGeneratorTest.java | 671 +++++++++-- .../dataformat/MessagePackMapperTest.java | 24 +- .../dataformat/MessagePackParserTest.java | 690 ++++++++--- .../MessagePackWriteContextTest.java | 0 .../TimestampExtensionModuleTest.java | 19 +- msgpack-jackson2/README.md | 492 ++++++++ .../ExtensionTypeCustomDeserializers.java | 0 .../msgpack/jackson/dataformat/JavaInfo.java | 0 .../jackson/dataformat/JsonArrayFormat.java | 35 + .../dataformat/MessagePackExtensionType.java | 43 +- .../dataformat/MessagePackFactory.java | 187 +++ .../dataformat/MessagePackGenerator.java | 830 +++++++++++++ .../dataformat/MessagePackKeySerializer.java | 13 +- .../jackson/dataformat/MessagePackMapper.java | 73 ++ .../jackson/dataformat/MessagePackParser.java | 650 ++++++++++ .../dataformat/MessagePackReadContext.java | 124 +- .../MessagePackSerializedString.java | 56 +- .../MessagePackSerializerFactory.java | 59 + .../dataformat/TimestampExtensionModule.java | 82 ++ .../org/msgpack/jackson/dataformat/Tuple.java | 3 + .../ExampleOfTypeInformationSerDe.java | 48 +- .../MessagePackDataformatForFieldIdTest.java | 55 +- .../MessagePackDataformatForPojoTest.java | 19 +- .../MessagePackDataformatTestBase.java | 10 +- .../dataformat/MessagePackFactoryTest.java | 162 +++ .../dataformat/MessagePackGeneratorTest.java | 671 ++--------- .../dataformat/MessagePackMapperTest.java | 24 +- .../dataformat/MessagePackParserTest.java | 690 +++-------- .../TimestampExtensionModuleTest.java | 19 +- .../dataformat/benchmark/Benchmarker.java | 0 ...gePackDataformatHugeDataBenchmarkTest.java | 0 ...essagePackDataformatPojoBenchmarkTest.java | 0 msgpack-jackson3/README.md | 480 -------- .../jackson/dataformat/JsonArrayFormat.java | 47 - .../dataformat/MessagePackFactory.java | 255 ---- .../dataformat/MessagePackGenerator.java | 1041 ----------------- .../jackson/dataformat/MessagePackMapper.java | 120 -- .../jackson/dataformat/MessagePackParser.java | 801 ------------- .../MessagePackSerializerFactory.java | 44 - .../dataformat/TimestampExtensionModule.java | 107 -- .../dataformat/MessagePackFactoryTest.java | 198 ---- 73 files changed, 5809 insertions(+), 5809 deletions(-) rename {msgpack-jackson3 => msgpack-jackson}/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/BenchmarkState.java (100%) rename {msgpack-jackson3 => msgpack-jackson}/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackReadBenchmark.java (100%) rename {msgpack-jackson3 => msgpack-jackson}/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackWriteBenchmark.java (100%) rename {msgpack-jackson3 => msgpack-jackson}/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/NopOutputStream.java (100%) rename {msgpack-jackson3 => msgpack-jackson}/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/WriteUTF8StringBenchmark.java (100%) rename {msgpack-jackson3 => msgpack-jackson}/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Image.java (100%) rename {msgpack-jackson3 => msgpack-jackson}/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaContent.java (100%) rename {msgpack-jackson3 => msgpack-jackson}/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItem.java (100%) rename {msgpack-jackson3 => msgpack-jackson}/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItems.java (100%) rename {msgpack-jackson3 => msgpack-jackson}/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Player.java (100%) rename {msgpack-jackson3 => msgpack-jackson}/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Size.java (100%) rename {msgpack-jackson3 => msgpack-jackson}/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactoryBuilder.java (100%) rename {msgpack-jackson3 => msgpack-jackson}/src/main/java/org/msgpack/jackson/dataformat/MessagePackWriteContext.java (100%) rename {msgpack-jackson3 => msgpack-jackson}/src/main/java/org/msgpack/jackson/dataformat/PackageVersion.java (100%) mode change 100755 => 100644 msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java rename {msgpack-jackson3 => msgpack-jackson}/src/test/java/org/msgpack/jackson/dataformat/MessagePackWriteContextTest.java (100%) mode change 100755 => 100644 msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java create mode 100644 msgpack-jackson2/README.md rename {msgpack-jackson3 => msgpack-jackson2}/src/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java (100%) rename {msgpack-jackson => msgpack-jackson2}/src/main/java/org/msgpack/jackson/dataformat/JavaInfo.java (100%) create mode 100644 msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java rename {msgpack-jackson3 => msgpack-jackson2}/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java (53%) create mode 100644 msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java create mode 100644 msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java rename {msgpack-jackson3 => msgpack-jackson2}/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java (73%) create mode 100644 msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java create mode 100644 msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java rename {msgpack-jackson3 => msgpack-jackson2}/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java (55%) rename {msgpack-jackson3 => msgpack-jackson2}/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java (56%) create mode 100644 msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java create mode 100755 msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java rename {msgpack-jackson3 => msgpack-jackson2}/src/main/java/org/msgpack/jackson/dataformat/Tuple.java (95%) rename {msgpack-jackson3 => msgpack-jackson2}/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java (76%) rename {msgpack-jackson3 => msgpack-jackson2}/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java (70%) rename {msgpack-jackson3 => msgpack-jackson2}/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java (89%) rename {msgpack-jackson3 => msgpack-jackson2}/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java (94%) create mode 100644 msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java rename {msgpack-jackson3 => msgpack-jackson2}/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java (53%) rename {msgpack-jackson3 => msgpack-jackson2}/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java (82%) rename {msgpack-jackson3 => msgpack-jackson2}/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java (59%) rename {msgpack-jackson3 => msgpack-jackson2}/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java (93%) mode change 100644 => 100755 rename {msgpack-jackson => msgpack-jackson2}/src/test/java/org/msgpack/jackson/dataformat/benchmark/Benchmarker.java (100%) rename {msgpack-jackson => msgpack-jackson2}/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java (100%) rename {msgpack-jackson => msgpack-jackson2}/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java (100%) delete mode 100644 msgpack-jackson3/README.md delete mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java delete mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java delete mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java delete mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java delete mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java delete mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java delete mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java delete mode 100644 msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java diff --git a/msgpack-jackson/README.md b/msgpack-jackson/README.md index 0156453e..9e149e4d 100644 --- a/msgpack-jackson/README.md +++ b/msgpack-jackson/README.md @@ -1,214 +1,201 @@ -# jackson-dataformat-msgpack +# jackson-dataformat-msgpack-jackson3 -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.msgpack/jackson-dataformat-msgpack/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.msgpack/jackson-dataformat-msgpack/) -[![Javadoc](https://www.javadoc.io/badge/org.msgpack/jackson-dataformat-msgpack.svg)](https://www.javadoc.io/doc/org.msgpack/jackson-dataformat-msgpack) +This Jackson 3.x extension library is a component to easily read and write [MessagePack](http://msgpack.org/) encoded data through jackson-databind API. -This Jackson extension library is a component to easily read and write [MessagePack](http://msgpack.org/) encoded data through jackson-databind API. +It extends standard Jackson streaming API (`JsonFactory`, `JsonParser`, `JsonGenerator`), and as such works seamlessly with all the higher level data abstractions (data binding, tree model, and pluggable extensions). -It extends standard Jackson streaming API (`JsonFactory`, `JsonParser`, `JsonGenerator`), and as such works seamlessly with all the higher level data abstractions (data binding, tree model, and pluggable extensions). For the details of Jackson-annotations, please see https://github.com/FasterXML/jackson-annotations. +**Requirements:** Java 17+ and Jackson 3.x. For the Jackson 2.x compatible version, see [`msgpack-jackson`](../msgpack-jackson/). -This library isn't compatible with msgpack-java v0.6 or earlier by default in serialization/deserialization of POJO. See **Advanced usage** below for details. +**Note on imports:** Jackson 3 moved its core and databind packages from `com.fasterxml.jackson` to `tools.jackson`. User-facing annotations (`@JsonProperty`, `@JsonFormat`, etc.) remain in `com.fasterxml.jackson.annotation` for backward compatibility. ## Install ### Maven -``` +```xml org.msgpack - jackson-dataformat-msgpack + jackson-dataformat-msgpack-jackson3 (version) ``` ### Sbt -``` -libraryDependencies += "org.msgpack" % "jackson-dataformat-msgpack" % "(version)" +```scala +libraryDependencies += "org.msgpack" % "jackson-dataformat-msgpack-jackson3" % "(version)" ``` ### Gradle -``` + +```groovy repositories { mavenCentral() } dependencies { - compile 'org.msgpack:jackson-dataformat-msgpack:(version)' + implementation 'org.msgpack:jackson-dataformat-msgpack-jackson3:(version)' } ``` - ## Basic usage ### Serialization/Deserialization of POJO -Only thing you need to do is to instantiate `MessagePackFactory` and pass it to the constructor of `com.fasterxml.jackson.databind.ObjectMapper`. And then, you can use it for MessagePack format data in the same way as jackson-databind. +Only thing you need to do is to instantiate `MessagePackFactory` and pass it to the constructor of `tools.jackson.databind.ObjectMapper`. And then, you can use it for MessagePack format data in the same way as jackson-databind. ```java - // Instantiate ObjectMapper for MessagePack - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - - // Serialize a Java object to byte array - ExamplePojo pojo = new ExamplePojo("komamitsu"); - byte[] bytes = objectMapper.writeValueAsBytes(pojo); - - // Deserialize the byte array to a Java object - ExamplePojo deserialized = objectMapper.readValue(bytes, ExamplePojo.class); - System.out.println(deserialized.getName()); // => komamitsu +// Instantiate ObjectMapper for MessagePack +ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + +// Serialize a Java object to byte array +ExamplePojo pojo = new ExamplePojo("komamitsu"); +byte[] bytes = objectMapper.writeValueAsBytes(pojo); + +// Deserialize the byte array to a Java object +ExamplePojo deserialized = objectMapper.readValue(bytes, ExamplePojo.class); +System.out.println(deserialized.getName()); // => komamitsu ``` Or more easily: ```java - ObjectMapper objectMapper = new MessagePackMapper(); +ObjectMapper objectMapper = new MessagePackMapper(); ``` -We strongly recommend to call `MessagePackMapper#handleBigIntegerAndBigDecimalAsString()` if you serialize and/or deserialize BigInteger/BigDecimal values. See [Serialize and deserialize BigDecimal as str type internally in MessagePack format](#serialize-and-deserialize-bigdecimal-as-str-type-internally-in-messagepack-format) for details. +We strongly recommend calling `MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString()` if you serialize and/or deserialize BigInteger/BigDecimal values. See [Serialize and deserialize BigDecimal as str type internally in MessagePack format](#serialize-and-deserialize-bigdecimal-as-str-type-internally-in-messagepack-format) for details. ```java - ObjectMapper objectMapper = new MessagePackMapper().handleBigIntegerAndBigDecimalAsString(); +ObjectMapper objectMapper = MessagePackMapper.builder().handleBigIntegerAndBigDecimalAsString().build(); ``` ### Serialization/Deserialization of List ```java - // Instantiate ObjectMapper for MessagePack - ObjectMapper objectMapper = new MessagePackMapper(); - - // Serialize a List to byte array - List list = new ArrayList<>(); - list.add("Foo"); - list.add("Bar"); - list.add(42); - byte[] bytes = objectMapper.writeValueAsBytes(list); - - // Deserialize the byte array to a List - List deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); - System.out.println(deserialized); // => [Foo, Bar, 42] +// Instantiate ObjectMapper for MessagePack +ObjectMapper objectMapper = new MessagePackMapper(); + +// Serialize a List to byte array +List list = new ArrayList<>(); +list.add("Foo"); +list.add("Bar"); +list.add(42); +byte[] bytes = objectMapper.writeValueAsBytes(list); + +// Deserialize the byte array to a List +List deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); +System.out.println(deserialized); // => [Foo, Bar, 42] ``` ### Serialization/Deserialization of Map ```java - // Instantiate ObjectMapper for MessagePack - ObjectMapper objectMapper = MessagePackMapper(); - - // Serialize a Map to byte array - Map map = new HashMap<>(); - map.put("name", "komamitsu"); - map.put("age", 42); - byte[] bytes = objectMapper.writeValueAsBytes(map); - - // Deserialize the byte array to a Map - Map deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); - System.out.println(deserialized); // => {name=komamitsu, age=42} - - ``` +// Instantiate ObjectMapper for MessagePack +ObjectMapper objectMapper = new MessagePackMapper(); + +// Serialize a Map to byte array +Map map = new HashMap<>(); +map.put("name", "komamitsu"); +map.put("age", 42); +byte[] bytes = objectMapper.writeValueAsBytes(map); + +// Deserialize the byte array to a Map +Map deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); +System.out.println(deserialized); // => {name=komamitsu, age=42} +``` ### Example of Serialization/Deserialization over multiple languages Java ```java - // Serialize - Map obj = new HashMap(); - obj.put("foo", "hello"); - obj.put("bar", "world"); - byte[] bs = objectMapper.writeValueAsBytes(obj); - // bs => [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111, - // -93, 98, 97, 114, -91, 119, 111, 114, 108, 100] +// Serialize +Map obj = new HashMap(); +obj.put("foo", "hello"); +obj.put("bar", "world"); +byte[] bs = objectMapper.writeValueAsBytes(obj); +// bs => [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111, +// -93, 98, 97, 114, -91, 119, 111, 114, 108, 100] ``` Ruby ```ruby - require 'msgpack' +require 'msgpack' - # Deserialize - xs = [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111, - -93, 98, 97, 114, -91, 119, 111, 114, 108, 100] - MessagePack.unpack(xs.pack("C*")) - # => {"foo"=>"hello", "bar"=>"world"} +# Deserialize +xs = [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111, + -93, 98, 97, 114, -91, 119, 111, 114, 108, 100] +MessagePack.unpack(xs.pack("C*")) +# => {"foo"=>"hello", "bar"=>"world"} - # Serialize - ["zero", 1, 2.0, nil].to_msgpack.unpack('C*') - # => [148, 164, 122, 101, 114, 111, 1, 203, 64, 0, 0, 0, 0, 0, 0, 0, 192] +# Serialize +["zero", 1, 2.0, nil].to_msgpack.unpack('C*') +# => [148, 164, 122, 101, 114, 111, 1, 203, 64, 0, 0, 0, 0, 0, 0, 0, 192] ``` Java ```java - // Deserialize - bs = new byte[] {(byte) 148, (byte) 164, 122, 101, 114, 111, 1, - (byte) 203, 64, 0, 0, 0, 0, 0, 0, 0, (byte) 192}; - TypeReference> typeReference = new TypeReference>(){}; - List xs = objectMapper.readValue(bs, typeReference); - // xs => [zero, 1, 2.0, null] +// Deserialize +bs = new byte[] {(byte) 148, (byte) 164, 122, 101, 114, 111, 1, + (byte) 203, 64, 0, 0, 0, 0, 0, 0, 0, (byte) 192}; +TypeReference> typeReference = new TypeReference>(){}; +List xs = objectMapper.readValue(bs, typeReference); +// xs => [zero, 1, 2.0, null] ``` ## Advanced usage -### Serialize/Deserialize POJO as MessagePack array type to keep compatibility with msgpack-java:0.6 - -In msgpack-java:0.6 or earlier, a POJO was serliazed and deserialized as an array of values in MessagePack format. The order of values depended on an internal order of Java class's variables and it was a naive way and caused some issues since Java class's variables order isn't guaranteed over Java implementations. - -On the other hand, jackson-databind serializes and deserializes a POJO as a key-value object. So this `jackson-dataformat-msgpack` also handles POJOs in the same way. As a result, it isn't compatible with msgpack-java:0.6 or earlier in serialization and deserialization of POJOs. - -But if you want to make this library handle POJOs in the same way as msgpack-java:0.6 or earlier, you can use `JsonArrayFormat` like this: - -```java - ObjectMapper objectMapper = new MessagePackMapper(); - objectMapper.setAnnotationIntrospector(new JsonArrayFormat()); -``` - ### Serialize multiple values without closing an output stream -`com.fasterxml.jackson.databind.ObjectMapper` closes an output stream by default after it writes a value. If you want to serialize multiple values in a row without closing an output stream, set `JsonGenerator.Feature.AUTO_CLOSE_TARGET` to false. +`tools.jackson.databind.ObjectMapper` closes an output stream by default after it writes a value. If you want to serialize multiple values in a row without closing an output stream, disable `StreamWriteFeature.AUTO_CLOSE_TARGET`. ```java - OutputStream out = new FileOutputStream(tempFile); - ObjectMapper objectMapper = new MessagePackMapper(); - objectMapper.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false); - - objectMapper.writeValue(out, 1); - objectMapper.writeValue(out, "two"); - objectMapper.writeValue(out, 3.14); - out.close(); - - MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(new FileInputStream(tempFile)); - System.out.println(unpacker.unpackInt()); // => 1 - System.out.println(unpacker.unpackString()); // => two - System.out.println(unpacker.unpackFloat()); // => 3.14 +OutputStream out = new FileOutputStream(tempFile); +ObjectMapper objectMapper = MessagePackMapper.builder() + .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) + .build(); + +objectMapper.writeValue(out, 1); +objectMapper.writeValue(out, "two"); +objectMapper.writeValue(out, 3.14); +out.close(); + +MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(new FileInputStream(tempFile)); +System.out.println(unpacker.unpackInt()); // => 1 +System.out.println(unpacker.unpackString()); // => two +System.out.println(unpacker.unpackFloat()); // => 3.14 ``` ### Deserialize multiple values without closing an input stream -`com.fasterxml.jackson.databind.ObjectMapper` closes an input stream by default after it reads a value. If you want to deserialize multiple values in a row without closing an output stream, set `JsonParser.Feature.AUTO_CLOSE_SOURCE` to false. +`tools.jackson.databind.ObjectMapper` closes an input stream by default after it reads a value. If you want to deserialize multiple values in a row without closing an input stream, disable `StreamReadFeature.AUTO_CLOSE_SOURCE`. ```java - MessagePacker packer = MessagePack.newDefaultPacker(new FileOutputStream(tempFile)); - packer.packInt(42); - packer.packString("Hello"); - packer.close(); - - FileInputStream in = new FileInputStream(tempFile); - ObjectMapper objectMapper = new MessagePackMapper(); - objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false); - System.out.println(objectMapper.readValue(in, Integer.class)); - System.out.println(objectMapper.readValue(in, String.class)); - in.close(); +MessagePacker packer = MessagePack.newDefaultPacker(new FileOutputStream(tempFile)); +packer.packInt(42); +packer.packString("Hello"); +packer.close(); + +FileInputStream in = new FileInputStream(tempFile); +ObjectMapper objectMapper = MessagePackMapper.builder() + .disable(StreamReadFeature.AUTO_CLOSE_SOURCE) + .build(); +System.out.println(objectMapper.readValue(in, Integer.class)); +System.out.println(objectMapper.readValue(in, String.class)); +in.close(); ``` ### Serialize not using str8 type -Old msgpack-java (e.g 0.6.7) doesn't support MessagePack str8 type. When your application needs to comunicate with such an old MessagePack library, you can disable the data type like this: +Old msgpack-java (e.g 0.6.7) doesn't support MessagePack str8 type. When your application needs to communicate with such an old MessagePack library, you can disable the data type like this: ```java - MessagePack.PackerConfig config = new MessagePack.PackerConfig().withStr8FormatSupport(false); - ObjectMapper mapperWithConfig = new MessagePackMapper(new MessagePackFactory(config)); - // This string is serialized as bin8 type - byte[] resultWithoutStr8Format = mapperWithConfig.writeValueAsBytes(str8LengthString); +MessagePack.PackerConfig config = new MessagePack.PackerConfig().withStr8FormatSupport(false); +ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory(config)); +// This string is serialized as bin8 type +byte[] resultWithoutStr8Format = objectMapper.writeValueAsBytes(str8LengthString); ``` ### Serialize using non-String as a key of Map @@ -216,60 +203,61 @@ Old msgpack-java (e.g 0.6.7) doesn't support MessagePack str8 type. When your ap When you want to use non-String value as a key of Map, use `MessagePackKeySerializer` for key serialization. ```java - @JsonSerialize(keyUsing = MessagePackKeySerializer.class) - private Map intMap = new HashMap<>(); +@JsonSerialize(keyUsing = MessagePackKeySerializer.class) +private Map intMap = new HashMap<>(); - : + : - intMap.put(42, "Hello"); +intMap.put(42, "Hello"); - ObjectMapper objectMapper = new MessagePackMapper(); - byte[] bytes = objectMapper.writeValueAsBytes(intMap); +ObjectMapper objectMapper = new MessagePackMapper(); +byte[] bytes = objectMapper.writeValueAsBytes(intMap); - Map deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); - System.out.println(deserialized); // => {42=Hello} +Map deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); +System.out.println(deserialized); // => {42=Hello} ``` ### Serialize and deserialize BigDecimal as str type internally in MessagePack format -`jackson-dataformat-msgpack` represents BigDecimal values as float type in MessagePack format by default for backward compatibility. But the default behavior could fail when handling too large value for `double` type. So we strongly recommend to call `MessagePackMapper#handleBigIntegerAndBigDecimalAsString()` to internally handle BigDecimal values as String. +`jackson-dataformat-msgpack-jackson3` represents BigDecimal values as float type in MessagePack format by default for backward compatibility. But the default behavior could fail when handling too large value for `double` type. So we strongly recommend calling `MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString()` to internally handle BigDecimal values as String. ```java - ObjectMapper objectMapper = new MessagePackMapper().handleBigIntegerAndBigDecimalAsString(); +ObjectMapper objectMapper = MessagePackMapper.builder().handleBigIntegerAndBigDecimalAsString().build(); - Pojo obj = new Pojo(); - // This value is too large to be serialized as double - obj.value = new BigDecimal("1234567890.98765432100"); +Pojo obj = new Pojo(); +// This value is too large to be serialized as double +obj.value = new BigDecimal("1234567890.98765432100"); - byte[] converted = objectMapper.writeValueAsBytes(obj); +byte[] converted = objectMapper.writeValueAsBytes(obj); - System.out.println(objectMapper.readValue(converted, Pojo.class)); // => Pojo{value=1234567890.98765432100} +System.out.println(objectMapper.readValue(converted, Pojo.class)); // => Pojo{value=1234567890.98765432100} ``` -`MessagePackMapper#handleBigIntegerAndDecimalAsString()` is equivalent to the following configuration. + +`MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString()` is equivalent to the following configuration. ```java - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - objectMapper.configOverride(BigInteger.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); - objectMapper.configOverride(BigDecimal.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); +ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); +objectMapper.configOverride(BigInteger.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); +objectMapper.configOverride(BigDecimal.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); ``` - ### 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. +`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 MessagePackMapper() - .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" +ObjectMapper objectMapper = MessagePackMapper.builder() + .addModule(TimestampExtensionModule.INSTANCE) + .build(); +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 @@ -279,178 +267,178 @@ When you want to use non-String value as a key of Map, use `MessagePackKeySerial #### Deserialize extension type value directly ```java - // In this application, extension type 59 is used for byte[] - byte[] bytes; - { - // This ObjectMapper is just for temporary serialization - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - MessagePacker packer = MessagePack.newDefaultPacker(outputStream); +// In this application, extension type 59 is used for byte[] +byte[] bytes; +{ + // This ObjectMapper is just for temporary serialization + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + MessagePacker packer = MessagePack.newDefaultPacker(outputStream); + + packer.packExtensionTypeHeader((byte) 59, hexspeak.length); + packer.addPayload(hexspeak); + packer.close(); + + bytes = outputStream.toByteArray(); +} - packer.packExtensionTypeHeader((byte) 59, hexspeak.length); - packer.addPayload(hexspeak); - packer.close(); +// Register the type and a deserializer to ExtensionTypeCustomDeserializers +ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); +extTypeCustomDesers.addCustomDeser((byte) 59, data -> { + if (Arrays.equals(data, + new byte[] {(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE})) { + return "Java"; + } + return "Not Java"; +}); - bytes = outputStream.toByteArray(); - } +ObjectMapper objectMapper = new ObjectMapper( + new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)); - // Register the type and a deserializer to ExtensionTypeCustomDeserializers - ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); - extTypeCustomDesers.addCustomDeser((byte) 59, data -> { - if (Arrays.equals(data, - new byte[] {(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE})) { - return "Java"; - } - return "Not Java"; - }); - - ObjectMapper objectMapper = new ObjectMapper( - new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)); - - System.out.println(objectMapper.readValue(bytes, Object.class)); - // => Java +System.out.println(objectMapper.readValue(bytes, Object.class)); + // => Java ``` #### Use extension type as Map key ```java - static class TripleBytesPojo +static class TripleBytesPojo +{ + public byte first; + public byte second; + public byte third; + + public TripleBytesPojo(byte first, byte second, byte third) { - public byte first; - public byte second; - public byte third; + this.first = first; + this.second = second; + this.third = third; + } - public TripleBytesPojo(byte first, byte second, byte third) - { - this.first = first; - this.second = second; - this.third = third; - } + @Override + public boolean equals(Object o) + { + : + } - @Override - public boolean equals(Object o) - { - : - } + @Override + public int hashCode() + { + : + } - @Override - public int hashCode() - { - : - } + @Override + public String toString() + { + // This key format is used when serialized as map key + return String.format("%d-%d-%d", first, second, third); + } + static class KeyDeserializer + extends tools.jackson.databind.KeyDeserializer + { @Override - public String toString() - { - // This key format is used when serialized as map key - return String.format("%d-%d-%d", first, second, third); - } - - static class KeyDeserializer - extends com.fasterxml.jackson.databind.KeyDeserializer + public Object deserializeKey(String key, DeserializationContext ctxt) { - @Override - public Object deserializeKey(String key, DeserializationContext ctxt) - throws IOException - { - String[] values = key.split("-"); - return new TripleBytesPojo(Byte.parseByte(values[0]), Byte.parseByte(values[1]), Byte.parseByte(values[2])); - } + String[] values = key.split("-"); + return new TripleBytesPojo(Byte.parseByte(values[0]), Byte.parseByte(values[1]), Byte.parseByte(values[2])); } + } - static TripleBytesPojo deserialize(byte[] bytes) - { - return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]); - } + static TripleBytesPojo deserialize(byte[] bytes) + { + return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]); } +} - : +: - byte extTypeCode = 42; +byte extTypeCode = 42; - ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); - extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() +ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); +extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() +{ + @Override + public Object deserialize(byte[] value) + throws IOException { - @Override - public Object deserialize(byte[] value) - throws IOException - { - return TripleBytesPojo.deserialize(value); - } - }); - - SimpleModule module = new SimpleModule(); - module.addKeyDeserializer(TripleBytesPojo.class, new TripleBytesPojo.KeyDeserializer()); - ObjectMapper objectMapper = new ObjectMapper( - new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) - .registerModule(module); - - Map deserializedMap = - objectMapper.readValue(serializedData, - new TypeReference>() {}); + return TripleBytesPojo.deserialize(value); + } +}); + +SimpleModule module = new SimpleModule(); +module.addKeyDeserializer(TripleBytesPojo.class, new TripleBytesPojo.KeyDeserializer()); +ObjectMapper objectMapper = MessagePackMapper.builder( + new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) + .addModule(module) + .build(); + +Map deserializedMap = + objectMapper.readValue(serializedData, + new TypeReference>() {}); ``` #### Use extension type as Map value ```java - static class TripleBytesPojo +static class TripleBytesPojo +{ + public byte first; + public byte second; + public byte third; + + public TripleBytesPojo(byte first, byte second, byte third) { - public byte first; - public byte second; - public byte third; + this.first = first; + this.second = second; + this.third = third; + } - public TripleBytesPojo(byte first, byte second, byte third) + static class Deserializer + extends StdDeserializer + { + protected Deserializer() { - this.first = first; - this.second = second; - this.third = third; + super(TripleBytesPojo.class); } - static class Deserializer - extends StdDeserializer + @Override + public TripleBytesPojo deserialize(JsonParser p, DeserializationContext ctxt) { - protected Deserializer() - { - super(TripleBytesPojo.class); - } - - @Override - public TripleBytesPojo deserialize(JsonParser p, DeserializationContext ctxt) - throws IOException, JsonProcessingException - { - return TripleBytesPojo.deserialize(p.getBinaryValue()); - } + return TripleBytesPojo.deserialize(p.getBinaryValue()); } + } - static TripleBytesPojo deserialize(byte[] bytes) - { - return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]); - } + static TripleBytesPojo deserialize(byte[] bytes) + { + return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]); } +} - : +: - byte extTypeCode = 42; +byte extTypeCode = 42; - ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); - extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() +ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); +extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() +{ + @Override + public Object deserialize(byte[] value) + throws IOException { - @Override - public Object deserialize(byte[] value) - throws IOException - { - return TripleBytesPojo.deserialize(value); - } - }); - - SimpleModule module = new SimpleModule(); - module.addDeserializer(TripleBytesPojo.class, new TripleBytesPojo.Deserializer()); - ObjectMapper objectMapper = new ObjectMapper( - new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) - .registerModule(module); - - Map deserializedMap = - objectMapper.readValue(serializedData, - new TypeReference>() {}); + return TripleBytesPojo.deserialize(value); + } +}); + +SimpleModule module = new SimpleModule(); +module.addDeserializer(TripleBytesPojo.class, new TripleBytesPojo.Deserializer()); +ObjectMapper objectMapper = MessagePackMapper.builder( + new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) + .addModule(module) + .build(); + +Map deserializedMap = + objectMapper.readValue(serializedData, + new TypeReference>() {}); ``` ### Serialize a nested object that also serializes @@ -458,35 +446,35 @@ When you want to use non-String value as a key of Map, use `MessagePackKeySerial When you serialize an object that has a nested object also serializing with ObjectMapper and MessagePackFactory like the following code, it throws NullPointerException since the nested MessagePackFactory modifies a shared state stored in ThreadLocal. ```java - @Test - public void testNestedSerialization() throws Exception - { - ObjectMapper objectMapper = new MessagePackMapper(); - objectMapper.writeValueAsBytes(new OuterClass()); - } +@Test +public void testNestedSerialization() throws Exception +{ + ObjectMapper objectMapper = new MessagePackMapper(); + objectMapper.writeValueAsBytes(new OuterClass()); +} - public class OuterClass +public class OuterClass +{ + public String getInner() throws JacksonException { - public String getInner() throws JsonProcessingException - { - ObjectMapper m = new MessagePackMapper(); - m.writeValueAsBytes(new InnerClass()); - return "EFG"; - } + ObjectMapper m = new MessagePackMapper(); + m.writeValueAsBytes(new InnerClass()); + return "EFG"; } +} - public class InnerClass +public class InnerClass +{ + public String getName() { - public String getName() - { - return "ABC"; - } + return "ABC"; } +} ``` -There are a few options to fix this issue, but they introduce performance degredations while this usage is a corner case. A workaround that doesn't affect performance is to call `MessagePackFactory#setReuseResourceInGenerator(false)`. It might be inconvenient to call the API for users, but it's a reasonable tradeoff with performance for now. +There are a few options to fix this issue, but they introduce performance degradations while this usage is a corner case. A workaround that doesn't affect performance is to call `MessagePackFactory#setReuseResourceInGenerator(false)`. It might be inconvenient to call the API for users, but it's a reasonable tradeoff with performance for now. ```java - ObjectMapper objectMapper = new ObjectMapper( - new MessagePackFactory().setReuseResourceInGenerator(false)); +ObjectMapper objectMapper = new ObjectMapper( + new MessagePackFactory().setReuseResourceInGenerator(false)); ``` diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/BenchmarkState.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/BenchmarkState.java similarity index 100% rename from msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/BenchmarkState.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/BenchmarkState.java diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackReadBenchmark.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackReadBenchmark.java similarity index 100% rename from msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackReadBenchmark.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackReadBenchmark.java diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackWriteBenchmark.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackWriteBenchmark.java similarity index 100% rename from msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackWriteBenchmark.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackWriteBenchmark.java diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/NopOutputStream.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/NopOutputStream.java similarity index 100% rename from msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/NopOutputStream.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/NopOutputStream.java diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/WriteUTF8StringBenchmark.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/WriteUTF8StringBenchmark.java similarity index 100% rename from msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/WriteUTF8StringBenchmark.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/WriteUTF8StringBenchmark.java diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Image.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Image.java similarity index 100% rename from msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Image.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Image.java diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaContent.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaContent.java similarity index 100% rename from msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaContent.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaContent.java diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItem.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItem.java similarity index 100% rename from msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItem.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItem.java diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItems.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItems.java similarity index 100% rename from msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItems.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItems.java diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Player.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Player.java similarity index 100% rename from msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Player.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Player.java diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Size.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Size.java similarity index 100% rename from msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Size.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Size.java diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java index 39155030..1b1e048b 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java @@ -1,8 +1,25 @@ +// +// 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 tools.jackson.databind.cfg.MapperConfig; +import tools.jackson.databind.introspect.Annotated; +import tools.jackson.databind.introspect.JacksonAnnotationIntrospector; + import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.databind.introspect.Annotated; -import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; import static com.fasterxml.jackson.annotation.JsonFormat.Shape.ARRAY; @@ -15,21 +32,16 @@ */ public class JsonArrayFormat extends JacksonAnnotationIntrospector { - private static final JsonFormat.Value ARRAY_FORMAT = new JsonFormat.Value().withShape(ARRAY); + private static final JsonFormat.Value ARRAY_FORMAT = new JsonFormat.Value().withShape(ARRAY); - /** - * Defines array format for serialized entities with ObjectMapper, without actually - * including the schema - */ - @Override - public JsonFormat.Value findFormat(Annotated ann) - { - // If the entity contains JsonFormat annotation, give it higher priority. - JsonFormat.Value precedenceFormat = super.findFormat(ann); - if (precedenceFormat != null) { - return precedenceFormat; - } + @Override + public JsonFormat.Value findFormat(MapperConfig config, Annotated ann) + { + JsonFormat.Value precedenceFormat = super.findFormat(config, ann); + if (precedenceFormat != null) { + return precedenceFormat; + } - return ARRAY_FORMAT; - } + return ARRAY_FORMAT; + } } diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java index e11c2cd0..1b62b80e 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java @@ -1,12 +1,27 @@ +// +// 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.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import tools.jackson.core.JsonGenerator; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.annotation.JsonSerialize; +import tools.jackson.databind.ser.std.StdSerializer; -import java.io.IOException; import java.util.Arrays; +import java.util.Objects; @JsonSerialize(using = MessagePackExtensionType.Serializer.class) public class MessagePackExtensionType @@ -17,7 +32,7 @@ public class MessagePackExtensionType public MessagePackExtensionType(byte type, byte[] data) { this.type = type; - this.data = data; + this.data = Objects.requireNonNull(data, "data"); } public byte getType() @@ -56,11 +71,21 @@ public int hashCode() return result; } - public static class Serializer extends JsonSerializer + @Override + public String toString() + { + return "MessagePackExtensionType(type=" + type + ", data.length=" + data.length + ")"; + } + + public static class Serializer extends StdSerializer { + public Serializer() + { + super(MessagePackExtensionType.class); + } + @Override - public void serialize(MessagePackExtensionType value, JsonGenerator gen, SerializerProvider serializers) - throws IOException + public void serialize(MessagePackExtensionType value, JsonGenerator gen, SerializationContext serializers) { if (gen instanceof MessagePackGenerator) { MessagePackGenerator msgpackGenerator = (MessagePackGenerator) gen; diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java index 865c0cf4..e6b0bf01 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java @@ -15,27 +15,38 @@ // package org.msgpack.jackson.dataformat; -import com.fasterxml.jackson.core.JsonEncoding; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.io.ContentReference; -import com.fasterxml.jackson.core.io.IOContext; +import tools.jackson.core.ErrorReportConfiguration; +import tools.jackson.core.FormatFeature; +import tools.jackson.core.FormatSchema; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.ObjectReadContext; +import tools.jackson.core.ObjectWriteContext; +import tools.jackson.core.StreamReadConstraints; +import tools.jackson.core.StreamWriteConstraints; +import tools.jackson.core.TSFBuilder; +import tools.jackson.core.TokenStreamFactory; +import tools.jackson.core.Version; +import tools.jackson.core.base.BinaryTSFactory; +import tools.jackson.core.io.IOContext; import org.msgpack.core.MessagePack; import org.msgpack.core.annotations.VisibleForTesting; -import java.io.File; -import java.io.FileOutputStream; +import org.msgpack.core.buffer.ArrayBufferInput; +import org.msgpack.core.buffer.InputStreamBufferInput; +import org.msgpack.core.buffer.MessageBufferInput; + +import java.io.DataInput; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.Writer; -import java.util.Arrays; public class MessagePackFactory - extends JsonFactory + extends BinaryTSFactory + implements java.io.Serializable { - private static final long serialVersionUID = 2578263992015504347L; + private static final long serialVersionUID = 2578263992015504348L; private final MessagePack.PackerConfig packerConfig; private boolean reuseResourceInGenerator = true; @@ -50,20 +61,33 @@ public MessagePackFactory() public MessagePackFactory(MessagePack.PackerConfig packerConfig) { + super(StreamReadConstraints.defaults(), StreamWriteConstraints.defaults(), + ErrorReportConfiguration.defaults(), 0, 0); this.packerConfig = packerConfig; } public MessagePackFactory(MessagePackFactory src) { - super(src, null); + super(src); this.packerConfig = src.packerConfig.clone(); this.reuseResourceInGenerator = src.reuseResourceInGenerator; this.reuseResourceInParser = src.reuseResourceInParser; + this.supportIntegerKeys = src.supportIntegerKeys; if (src.extTypeCustomDesers != null) { this.extTypeCustomDesers = new ExtensionTypeCustomDeserializers(src.extTypeCustomDesers); } } + protected MessagePackFactory(MessagePackFactoryBuilder b) + { + super(b); + this.packerConfig = b.packerConfig().clone(); + this.reuseResourceInGenerator = b.reuseResourceInGenerator(); + this.reuseResourceInParser = b.reuseResourceInParser(); + this.supportIntegerKeys = b.supportIntegerKeys(); + this.extTypeCustomDesers = b.extTypeCustomDesers(); + } + public MessagePackFactory setReuseResourceInGenerator(boolean reuseResourceInGenerator) { this.reuseResourceInGenerator = reuseResourceInGenerator; @@ -89,70 +113,84 @@ public MessagePackFactory setExtTypeCustomDesers(ExtensionTypeCustomDeserializer } @Override - public JsonGenerator createGenerator(OutputStream out, JsonEncoding enc) - throws IOException - { - return new MessagePackGenerator(_generatorFeatures, _objectCodec, out, packerConfig, reuseResourceInGenerator, supportIntegerKeys); + protected JsonParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt, + InputStream in) throws JacksonException + { + try { + MessagePackParser parser = new MessagePackParser(readCtxt, ioCtxt, + readCtxt.getStreamReadFeatures(_streamReadFeatures), + new InputStreamBufferInput(in), in, reuseResourceInParser); + if (extTypeCustomDesers != null) { + parser.setExtensionTypeCustomDeserializers(extTypeCustomDesers); + } + return parser; + } + catch (IOException e) { + throw _wrapIOFailure(e); + } } @Override - public JsonGenerator createGenerator(File f, JsonEncoding enc) - throws IOException - { - return createGenerator(new FileOutputStream(f), enc); + protected JsonParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt, + byte[] data, int offset, int len) throws JacksonException + { + try { + MessageBufferInput input = new ArrayBufferInput(data, offset, len); + MessagePackParser parser = new MessagePackParser(readCtxt, ioCtxt, + readCtxt.getStreamReadFeatures(_streamReadFeatures), input, data, reuseResourceInParser); + if (extTypeCustomDesers != null) { + parser.setExtensionTypeCustomDeserializers(extTypeCustomDesers); + } + return parser; + } + catch (IOException e) { + throw _wrapIOFailure(e); + } } @Override - public JsonGenerator createGenerator(Writer w) + protected JsonParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt, + DataInput input) throws JacksonException { - throw new UnsupportedOperationException(); + return _unsupported(); } @Override - public JsonParser createParser(byte[] data) - throws IOException + protected JsonGenerator _createGenerator(ObjectWriteContext writeCtxt, IOContext ioCtxt, + OutputStream out) throws JacksonException { - IOContext ioContext = _createContext(ContentReference.rawReference(data), false); - return _createParser(data, 0, data.length, ioContext); + try { + return new MessagePackGenerator(writeCtxt, ioCtxt, + writeCtxt.getStreamWriteFeatures(_streamWriteFeatures), + out, packerConfig, reuseResourceInGenerator, supportIntegerKeys); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } } @Override - public JsonParser createParser(InputStream in) - throws IOException + public TokenStreamFactory copy() { - IOContext ioContext = _createContext(ContentReference.rawReference(in), false); - return _createParser(in, ioContext); + return new MessagePackFactory(this); } @Override - protected MessagePackParser _createParser(InputStream in, IOContext ctxt) - throws IOException + public TokenStreamFactory snapshot() { - MessagePackParser parser = new MessagePackParser(ctxt, _parserFeatures, _objectCodec, in, reuseResourceInParser); - if (extTypeCustomDesers != null) { - parser.setExtensionTypeCustomDeserializers(extTypeCustomDesers); - } - return parser; + return copy(); } @Override - protected JsonParser _createParser(byte[] data, int offset, int len, IOContext ctxt) - throws IOException + public TSFBuilder rebuild() { - if (offset != 0 || len != data.length) { - data = Arrays.copyOfRange(data, offset, offset + len); - } - MessagePackParser parser = new MessagePackParser(ctxt, _parserFeatures, _objectCodec, data, reuseResourceInParser); - if (extTypeCustomDesers != null) { - parser.setExtensionTypeCustomDeserializers(extTypeCustomDesers); - } - return parser; + return new MessagePackFactoryBuilder(this); } @Override - public JsonFactory copy() + public Version version() { - return new MessagePackFactory(this); + return PackageVersion.VERSION; } @VisibleForTesting @@ -161,12 +199,24 @@ MessagePack.PackerConfig getPackerConfig() return packerConfig; } + @VisibleForTesting + boolean isReuseResourceInGenerator() + { + return reuseResourceInGenerator; + } + @VisibleForTesting boolean isReuseResourceInParser() { return reuseResourceInParser; } + @VisibleForTesting + boolean isSupportIntegerKeys() + { + return supportIntegerKeys; + } + @VisibleForTesting ExtensionTypeCustomDeserializers getExtTypeCustomDesers() { @@ -180,8 +230,26 @@ public String getFormatName() } @Override - public boolean canHandleBinaryNatively() + public boolean canParseAsync() + { + return false; + } + + @Override + public boolean canUseSchema(FormatSchema schema) + { + return false; + } + + @Override + public Class getFormatReadFeatureType() + { + return null; + } + + @Override + public Class getFormatWriteFeatureType() { - return true; + return null; } } diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactoryBuilder.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactoryBuilder.java similarity index 100% rename from msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactoryBuilder.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactoryBuilder.java diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java index 3dde5604..cc6e9137 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java @@ -15,42 +15,43 @@ // package org.msgpack.jackson.dataformat; -import com.fasterxml.jackson.core.Base64Variant; -import com.fasterxml.jackson.core.ObjectCodec; -import com.fasterxml.jackson.core.SerializableString; -import com.fasterxml.jackson.core.base.GeneratorBase; -import com.fasterxml.jackson.core.io.ContentReference; -import com.fasterxml.jackson.core.io.IOContext; -import com.fasterxml.jackson.core.io.SerializedString; -import com.fasterxml.jackson.core.json.JsonWriteContext; -import com.fasterxml.jackson.core.util.BufferRecycler; +import tools.jackson.core.Base64Variant; +import tools.jackson.core.JacksonException; +import tools.jackson.core.util.JacksonFeatureSet; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.ObjectWriteContext; +import tools.jackson.core.SerializableString; +import tools.jackson.core.StreamWriteCapability; +import tools.jackson.core.StreamWriteFeature; +import tools.jackson.core.json.DupDetector; +import tools.jackson.core.TokenStreamContext; +import tools.jackson.core.base.GeneratorBase; +import tools.jackson.core.io.IOContext; import org.msgpack.core.MessagePack; import org.msgpack.core.MessagePacker; -import org.msgpack.core.annotations.Nullable; import org.msgpack.core.buffer.MessageBufferOutput; import org.msgpack.core.buffer.OutputStreamBufferOutput; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.io.Reader; + import java.math.BigDecimal; import java.math.BigInteger; import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; -import static org.msgpack.jackson.dataformat.JavaInfo.STRING_VALUE_FIELD_IS_CHARS; - public class MessagePackGenerator extends GeneratorBase { - private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; private static final int IN_ROOT = 0; private static final int IN_OBJECT = 1; private static final int IN_ARRAY = 2; private final MessagePacker messagePacker; + // Retained heap per idle thread: ~8 KB (OutputStreamBufferOutput + internal MessageBuffer). + // Negligible compared to Jackson's own per-thread buffer retention. private static final ThreadLocal messageBufferOutputHolder = new ThreadLocal<>(); private final OutputStream output; private final MessagePack.PackerConfig packerConfig; @@ -60,14 +61,20 @@ public class MessagePackGenerator private int currentState = IN_ROOT; private final List nodes; private boolean isElementsClosed = false; + private MessagePackWriteContext writeContext; + private final boolean ownsThreadLocalBuffer; - private static final class AsciiCharString + private static final class RawUtf8String { public final byte[] bytes; + public final int offset; + public final int len; - public AsciiCharString(byte[] bytes) + public RawUtf8String(byte[] bytes, int offset, int len) { this.bytes = bytes; + this.offset = offset; + this.len = len; } } @@ -188,39 +195,47 @@ else if (value instanceof NodeArray) { } } - // This is an internal constructor for nested serialization. - @SuppressWarnings("deprecation") + // Internal constructor for nested serialization. private MessagePackGenerator( - int features, - ObjectCodec codec, + ObjectWriteContext writeCtxt, + IOContext ioCtxt, + int streamWriteFeatures, OutputStream out, MessagePack.PackerConfig packerConfig, boolean supportIntegerKeys) { - super(features, codec, new IOContext(new BufferRecycler(), ContentReference.rawReference(out), false), JsonWriteContext.createRootContext(null)); + super(writeCtxt, ioCtxt, streamWriteFeatures); this.output = out; this.messagePacker = packerConfig.newPacker(out); this.packerConfig = packerConfig; this.nodes = new ArrayList<>(); this.supportIntegerKeys = supportIntegerKeys; + this.writeContext = MessagePackWriteContext.createRootContext( + StreamWriteFeature.STRICT_DUPLICATE_DETECTION.enabledIn(streamWriteFeatures) + ? DupDetector.rootDetector(this) : null); + this.ownsThreadLocalBuffer = false; } - @SuppressWarnings("deprecation") public MessagePackGenerator( - int features, - ObjectCodec codec, + ObjectWriteContext writeCtxt, + IOContext ioCtxt, + int streamWriteFeatures, OutputStream out, MessagePack.PackerConfig packerConfig, boolean reuseResourceInGenerator, boolean supportIntegerKeys) throws IOException { - super(features, codec, new IOContext(new BufferRecycler(), ContentReference.rawReference(out), false), JsonWriteContext.createRootContext(null)); + super(writeCtxt, ioCtxt, streamWriteFeatures); this.output = out; this.messagePacker = packerConfig.newPacker(getMessageBufferOutputForOutputStream(out, reuseResourceInGenerator)); this.packerConfig = packerConfig; this.nodes = new ArrayList<>(); this.supportIntegerKeys = supportIntegerKeys; + this.writeContext = MessagePackWriteContext.createRootContext( + StreamWriteFeature.STRICT_DUPLICATE_DETECTION.enabledIn(streamWriteFeatures) + ? DupDetector.rootDetector(this) : null); + this.ownsThreadLocalBuffer = reuseResourceInGenerator; } private MessageBufferOutput getMessageBufferOutputForOutputStream( @@ -258,8 +273,27 @@ private String currentStateStr() } @Override - public void writeStartArray() + public JsonGenerator writeStartArray() throws JacksonException + { + return writeStartArray(null); + } + + @Override + public JsonGenerator writeStartArray(Object currentValue) throws JacksonException { + return writeStartArray(currentValue, -1); + } + + // size is ignored: element count is determined at flush time from the actual child nodes. + // When size >= 0, it could in principle be used to skip buffering and write the array + // header immediately, but Jackson does not guarantee it — dynamic filters (Views, + // @JsonFilter) evaluate entries incrementally and will pass -1 even for known-size + // collections. Backends must handle both cases (Jackson author confirmed, see #841). + @Override + public JsonGenerator writeStartArray(Object currentValue, int size) throws JacksonException + { + _verifyValueWrite("start an array"); + writeContext = writeContext.createChildArrayContext(currentValue); if (currentState == IN_OBJECT) { Node node = nodes.get(nodes.size() - 1); assert node instanceof NodeEntryInObject; @@ -267,25 +301,44 @@ public void writeStartArray() nodeEntryInObject.value = new NodeArray(currentParentElementIndex); } else { + if (isElementsClosed) { + flush(); + } nodes.add(new NodeArray(currentParentElementIndex)); } currentParentElementIndex = nodes.size() - 1; currentState = IN_ARRAY; + return this; } @Override - public void writeEndArray() - throws IOException + public JsonGenerator writeEndArray() throws JacksonException { if (currentState != IN_ARRAY) { _reportError("Current context not an array but " + currentStateStr()); } endCurrentContainer(); + return this; + } + + @Override + public JsonGenerator writeStartObject() throws JacksonException + { + return writeStartObject(null); + } + + @Override + public JsonGenerator writeStartObject(Object currentValue) throws JacksonException + { + return writeStartObject(currentValue, -1); } + // size is ignored: same reasoning as writeStartArray(Object, int) above. @Override - public void writeStartObject() + public JsonGenerator writeStartObject(Object forValue, int size) throws JacksonException { + _verifyValueWrite("start an object"); + writeContext = writeContext.createChildObjectContext(forValue); if (currentState == IN_OBJECT) { Node node = nodes.get(nodes.size() - 1); assert node instanceof NodeEntryInObject; @@ -293,33 +346,41 @@ public void writeStartObject() nodeEntryInObject.value = new NodeObject(currentParentElementIndex); } else { + if (isElementsClosed) { + flush(); + } nodes.add(new NodeObject(currentParentElementIndex)); } currentParentElementIndex = nodes.size() - 1; currentState = IN_OBJECT; + return this; } @Override - public void writeEndObject() - throws IOException + public JsonGenerator writeEndObject() throws JacksonException { if (currentState != IN_OBJECT) { _reportError("Current context not an object but " + currentStateStr()); } + if (writeContext.isExpectingValue()) { + _reportError("Cannot close Object, property name written but no value"); + } endCurrentContainer(); + return this; } private void endCurrentContainer() { + writeContext = writeContext.getParent(); Node parent = nodes.get(currentParentElementIndex); - if (currentParentElementIndex == 0) { + if (parent.parentIndex == -1) { isElementsClosed = true; currentParentElementIndex = parent.parentIndex; + currentState = IN_ROOT; return; } currentParentElementIndex = parent.parentIndex; - assert currentParentElementIndex >= 0; Node currentParent = nodes.get(currentParentElementIndex); currentParent.incrementChildCount(); currentState = currentParent.currentStateAsParent(); @@ -332,10 +393,10 @@ private void packNonContainer(Object v) if (v instanceof String) { messagePacker.packString((String) v); } - else if (v instanceof AsciiCharString) { - byte[] bytes = ((AsciiCharString) v).bytes; - messagePacker.packRawStringHeader(bytes.length); - messagePacker.writePayload(bytes); + else if (v instanceof RawUtf8String) { + RawUtf8String raw = (RawUtf8String) v; + messagePacker.packRawStringHeader(raw.len); + messagePacker.writePayload(raw.bytes, raw.offset, raw.len); } else if (v instanceof Integer) { messagePacker.packInt((Integer) v); @@ -364,13 +425,13 @@ else if (v instanceof Boolean) { else if (v instanceof ByteBuffer) { ByteBuffer bb = (ByteBuffer) v; int len = bb.remaining(); - if (bb.hasArray()) { + if (bb.hasArray() && !bb.isReadOnly()) { messagePacker.packBinaryHeader(len); - messagePacker.writePayload(bb.array(), bb.arrayOffset(), len); + messagePacker.writePayload(bb.array(), bb.arrayOffset() + bb.position(), len); } else { byte[] data = new byte[len]; - bb.get(data); + bb.duplicate().get(data); messagePacker.packBinaryHeader(len); messagePacker.addPayload(data); } @@ -384,8 +445,11 @@ else if (v instanceof MessagePackExtensionType) { else { messagePacker.flush(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - MessagePackGenerator messagePackGenerator = new MessagePackGenerator(getFeatureMask(), getCodec(), outputStream, packerConfig, supportIntegerKeys); - getCodec().writeValue(messagePackGenerator, v); + try (MessagePackGenerator messagePackGenerator = new MessagePackGenerator( + objectWriteContext(), _ioContext, _streamWriteFeatures, + outputStream, packerConfig, supportIntegerKeys)) { + objectWriteContext().writeValue(messagePackGenerator, v); + } output.write(outputStream.toByteArray()); } } @@ -407,8 +471,7 @@ private void packBigDecimal(BigDecimal decimal) if (failedToPackAsBI) { double doubleValue = decimal.doubleValue(); //Check to make sure this BigDecimal can be represented as a double - if (!decimal.stripTrailingZeros().toEngineeringString().equals( - BigDecimal.valueOf(doubleValue).stripTrailingZeros().toEngineeringString())) { + if (Double.isInfinite(doubleValue) || decimal.compareTo(BigDecimal.valueOf(doubleValue)) != 0) { throw new IllegalArgumentException("MessagePack cannot serialize a BigDecimal that can't be represented as double. " + decimal); } messagePacker.packDouble(doubleValue); @@ -432,14 +495,16 @@ private void packArray(NodeArray container) private void addKeyNode(Object key) { if (currentState != IN_OBJECT) { - throw new IllegalStateException(); + _reportError("Can not write a property name, expecting a value"); } - Node node = new NodeEntryInObject(currentParentElementIndex, key); - nodes.add(node); + nodes.add(new NodeEntryInObject(currentParentElementIndex, key)); } private void addValueNode(Object value) throws IOException { + if (!writeContext.writeValue()) { + _reportError("Cannot write value: expecting a property name in Object context"); + } switch (currentState) { case IN_OBJECT: { Node node = nodes.get(nodes.size() - 1); @@ -456,345 +521,438 @@ private void addValueNode(Object value) throws IOException break; } default: + // Flush any buffered root container before packing a root scalar, + // otherwise the scalar would be emitted before the container. + if (isElementsClosed) { + flush(); + } packNonContainer(value); flushMessagePacker(); break; } } - @Nullable - private byte[] getBytesIfAscii(char[] chars, int offset, int len) - { - byte[] bytes = new byte[len]; - for (int i = offset; i < offset + len; i++) { - char c = chars[i]; - if (c >= 0x80) { - return null; - } - bytes[i] = (byte) c; - } - return bytes; - } - - private boolean areAllAsciiBytes(byte[] bytes, int offset, int len) - { - for (int i = offset; i < offset + len; i++) { - if ((bytes[i] & 0x80) != 0) { - return false; - } - } - return true; - } - - private void writeCharArrayTextKey(char[] text, int offset, int len) - { - byte[] bytes = getBytesIfAscii(text, offset, len); - if (bytes != null) { - addKeyNode(new AsciiCharString(bytes)); - return; - } - addKeyNode(new String(text, offset, len)); - } - private void writeCharArrayTextValue(char[] text, int offset, int len) throws IOException { - byte[] bytes = getBytesIfAscii(text, offset, len); - if (bytes != null) { - addValueNode(new AsciiCharString(bytes)); - return; - } addValueNode(new String(text, offset, len)); } private void writeByteArrayTextValue(byte[] text, int offset, int len) throws IOException { - if (areAllAsciiBytes(text, offset, len)) { - addValueNode(new AsciiCharString(text)); - return; - } - addValueNode(new String(text, offset, len, DEFAULT_CHARSET)); - } - - private void writeByteArrayTextKey(byte[] text, int offset, int len) throws IOException - { - if (areAllAsciiBytes(text, offset, len)) { - addValueNode(new AsciiCharString(text)); - return; - } - addValueNode(new String(text, offset, len, DEFAULT_CHARSET)); + addValueNode(new RawUtf8String(text, offset, len)); } @Override - public void writeFieldId(long id) throws IOException + public JsonGenerator writePropertyId(long id) throws JacksonException { if (this.supportIntegerKeys) { + if (!writeContext.writeName(String.valueOf(id))) { + _reportError("Can not write a property id, expecting a value"); + } addKeyNode(id); } else { - super.writeFieldId(id); + writeName(String.valueOf(id)); } + return this; } @Override - public void writeFieldName(String name) + public JacksonFeatureSet streamWriteCapabilities() { - if (STRING_VALUE_FIELD_IS_CHARS.get()) { - char[] chars = name.toCharArray(); - writeCharArrayTextKey(chars, 0, chars.length); - } - else { - addKeyNode(name); - } + return DEFAULT_BINARY_WRITE_CAPABILITIES; } @Override - public void writeFieldName(SerializableString name) + public JsonGenerator writeName(String name) throws JacksonException { - if (name instanceof SerializedString) { - writeFieldName(name.getValue()); + if (!writeContext.writeName(name)) { + _reportError("Can not write a property name, expecting a value"); } - else if (name instanceof MessagePackSerializedString) { + addKeyNode(name); + return this; + } + + @Override + public JsonGenerator writeName(SerializableString name) throws JacksonException + { + if (name instanceof MessagePackSerializedString) { + if (!writeContext.writeName(name.getValue())) { + _reportError("Can not write a property name, expecting a value"); + } addKeyNode(((MessagePackSerializedString) name).getRawValue()); } else { - throw new IllegalArgumentException("Unsupported key: " + name); + writeName(name.getValue()); } + return this; } @Override - public void writeString(String text) - throws IOException + public JsonGenerator writeString(String text) throws JacksonException { - if (STRING_VALUE_FIELD_IS_CHARS.get()) { - char[] chars = text.toCharArray(); - writeCharArrayTextValue(chars, 0, chars.length); - } - else { + try { addValueNode(text); } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; } @Override - public void writeString(char[] text, int offset, int len) - throws IOException + public JsonGenerator writeString(char[] text, int offset, int len) throws JacksonException { - writeCharArrayTextValue(text, offset, len); + try { + writeCharArrayTextValue(text, offset, len); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; } @Override - public void writeRawUTF8String(byte[] text, int offset, int length) - throws IOException + public JsonGenerator writeString(Reader reader, int len) throws JacksonException { - writeByteArrayTextValue(text, offset, length); + try { + long remaining = len < 0 ? Long.MAX_VALUE : len; + // Cap chunk size: len is a caller hint and can be arbitrarily large. + // Pre-allocating new StringBuilder(len) would reserve len*2 bytes upfront, + // which is an OOM risk for large inputs. The StringBuilder grows as needed. + int chunkSize = (int) Math.min(remaining, 8192); + StringBuilder sb = new StringBuilder(chunkSize); + char[] tmpBuf = new char[chunkSize]; + while (remaining > 0) { + int read = reader.read(tmpBuf, 0, (int) Math.min(remaining, tmpBuf.length)); + if (read < 0) { + break; + } + sb.append(tmpBuf, 0, read); + remaining -= read; + } + addValueNode(sb.toString()); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; } @Override - public void writeUTF8String(byte[] text, int offset, int length) - throws IOException + public JsonGenerator writeString(SerializableString text) throws JacksonException { - writeByteArrayTextValue(text, offset, length); + return writeString(text.getValue()); } @Override - public void writeRaw(String text) - throws IOException + public JsonGenerator writeRawUTF8String(byte[] text, int offset, int length) throws JacksonException { - if (STRING_VALUE_FIELD_IS_CHARS.get()) { - char[] chars = text.toCharArray(); - writeCharArrayTextValue(chars, 0, chars.length); + try { + writeByteArrayTextValue(text, offset, length); } - else { - addValueNode(text); + catch (IOException e) { + throw _wrapIOFailure(e); } + return this; } @Override - public void writeRaw(String text, int offset, int len) - throws IOException + public JsonGenerator writeUTF8String(byte[] text, int offset, int length) throws JacksonException { - // TODO: There is room to optimize this. - char[] chars = text.toCharArray(); - writeCharArrayTextValue(chars, offset, len); + try { + writeByteArrayTextValue(text, offset, length); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; } @Override - public void writeRaw(char[] text, int offset, int len) - throws IOException + public JsonGenerator writeRaw(String text) throws JacksonException { - writeCharArrayTextValue(text, offset, len); + try { + addValueNode(text); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; } @Override - public void writeRaw(char c) - throws IOException + public JsonGenerator writeRaw(String text, int offset, int len) throws JacksonException { - writeCharArrayTextValue(new char[] { c }, 0, 1); + try { + addValueNode(text.substring(offset, offset + len)); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; } @Override - public void writeBinary(Base64Variant b64variant, byte[] data, int offset, int len) - throws IOException + public JsonGenerator writeRaw(char[] text, int offset, int len) throws JacksonException { - addValueNode(ByteBuffer.wrap(data, offset, len)); + try { + writeCharArrayTextValue(text, offset, len); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; } @Override - public void writeNumber(int v) - throws IOException + public JsonGenerator writeRaw(char c) throws JacksonException { - addValueNode(v); + try { + writeCharArrayTextValue(new char[] { c }, 0, 1); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; } @Override - public void writeNumber(long v) - throws IOException + public JsonGenerator writeBinary(Base64Variant b64variant, byte[] data, int offset, int len) throws JacksonException { - addValueNode(v); + try { + addValueNode(ByteBuffer.wrap(data, offset, len)); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; } @Override - public void writeNumber(BigInteger v) - throws IOException + public JsonGenerator writeNumber(short v) throws JacksonException { - addValueNode(v); + return writeNumber((int) v); } @Override - public void writeNumber(double d) - throws IOException + public JsonGenerator writeNumber(int v) throws JacksonException { - addValueNode(d); + try { + addValueNode(v); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; } @Override - public void writeNumber(float f) - throws IOException + public JsonGenerator writeNumber(long v) throws JacksonException { - addValueNode(f); + try { + addValueNode(v); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; } @Override - public void writeNumber(BigDecimal dec) - throws IOException + public JsonGenerator writeNumber(BigInteger v) throws JacksonException { - addValueNode(dec); + try { + addValueNode(v); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; } @Override - public void writeNumber(String encodedValue) - throws IOException, UnsupportedOperationException + public JsonGenerator writeNumber(double d) throws JacksonException { - // There is a room to improve this API's performance while the implementation is robust. - // If users can use other MessagePackGenerator#writeNumber APIs that accept - // proper numeric types not String, it's better to use the other APIs instead. try { - long l = Long.parseLong(encodedValue); - addValueNode(l); - return; + addValueNode(d); } - catch (NumberFormatException ignored) { + catch (IOException e) { + throw _wrapIOFailure(e); } + return this; + } + @Override + public JsonGenerator writeNumber(float f) throws JacksonException + { try { - double d = Double.parseDouble(encodedValue); - addValueNode(d); - return; + addValueNode(f); } - catch (NumberFormatException ignored) { + catch (IOException e) { + throw _wrapIOFailure(e); } + return this; + } + @Override + public JsonGenerator writeNumber(BigDecimal dec) throws JacksonException + { try { - BigInteger bi = new BigInteger(encodedValue); - addValueNode(bi); - return; + addValueNode(dec); } - catch (NumberFormatException ignored) { + catch (IOException e) { + throw _wrapIOFailure(e); } + return this; + } + @Override + public JsonGenerator writeNumber(String encodedValue) throws JacksonException + { + // There is a room to improve this API's performance while the implementation is robust. + // If users can use other MessagePackGenerator#writeNumber APIs that accept + // proper numeric types not String, it's better to use the other APIs instead. try { - BigDecimal bc = new BigDecimal(encodedValue); - addValueNode(bc); - return; + try { + long l = Long.parseLong(encodedValue); + addValueNode(l); + return this; + } + catch (NumberFormatException ignored) { + } + + try { + BigInteger bi = new BigInteger(encodedValue); + addValueNode(bi); + return this; + } + catch (NumberFormatException ignored) { + } + + try { + BigDecimal bd = new BigDecimal(encodedValue); + double d = bd.doubleValue(); + + // Check if the double can perfectly represent the exact decimal value. + // isInfinite guard: values like "1e309" overflow double to Infinity; keep as BigDecimal. + if (!Double.isInfinite(d) && bd.compareTo(new BigDecimal(String.valueOf(d))) == 0) { + // It's a safe ordinary floating-point number. + addValueNode(d); + } + else { + // It has more precision than a double can handle, or overflows double range. + addValueNode(bd); + } + return this; + } + catch (NumberFormatException e) { + // Fall back for NaN, Infinity, -Infinity which BigDecimal rejects. + try { + double d = Double.parseDouble(encodedValue); + addValueNode(d); + return this; + } + catch (NumberFormatException ignored) { + } + } + + throw new NumberFormatException(encodedValue); } - catch (NumberFormatException ignored) { + catch (IOException e) { + throw _wrapIOFailure(e); } - - throw new NumberFormatException(encodedValue); } @Override - public void writeBoolean(boolean state) - throws IOException + public JsonGenerator writeBoolean(boolean state) throws JacksonException { - addValueNode(state); + try { + addValueNode(state); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; } @Override - public void writeNull() - throws IOException + public JsonGenerator writeNull() throws JacksonException { - addValueNode(null); + try { + addValueNode(null); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; } public void writeExtensionType(MessagePackExtensionType extensionType) - throws IOException { - addValueNode(extensionType); + try { + addValueNode(extensionType); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } } @Override - public void close() - throws IOException + public void close() throws JacksonException { - try { - flush(); - } - finally { - if (isEnabled(Feature.AUTO_CLOSE_TARGET)) { - MessagePacker messagePacker = getMessagePacker(); - messagePacker.close(); + if (!_closed) { + try { + flush(); + } + finally { + super.close(); } } } @Override - public void flush() - throws IOException + public void flush() throws JacksonException { if (!isElementsClosed) { // The whole elements are not closed yet. return; } - for (int i = 0; i < nodes.size(); i++) { - Node node = nodes.get(i); - if (node instanceof NodeEntryInObject) { - NodeEntryInObject nodeEntry = (NodeEntryInObject) node; - packNonContainer(nodeEntry.key); - if (nodeEntry.value instanceof NodeObject) { - packObject((NodeObject) nodeEntry.value); + try { + for (int i = 0; i < nodes.size(); i++) { + Node node = nodes.get(i); + if (node instanceof NodeEntryInObject) { + NodeEntryInObject nodeEntry = (NodeEntryInObject) node; + packNonContainer(nodeEntry.key); + if (nodeEntry.value instanceof NodeObject) { + packObject((NodeObject) nodeEntry.value); + } + else if (nodeEntry.value instanceof NodeArray) { + packArray((NodeArray) nodeEntry.value); + } + else { + packNonContainer(nodeEntry.value); + } } - else if (nodeEntry.value instanceof NodeArray) { - packArray((NodeArray) nodeEntry.value); + else if (node instanceof NodeObject) { + packObject((NodeObject) node); + } + else if (node instanceof NodeEntryInArray) { + packNonContainer(((NodeEntryInArray) node).value); + } + else if (node instanceof NodeArray) { + packArray((NodeArray) node); } else { - packNonContainer(nodeEntry.value); + throw new AssertionError(); } } - else if (node instanceof NodeObject) { - packObject((NodeObject) node); - } - else if (node instanceof NodeEntryInArray) { - packNonContainer(((NodeEntryInArray) node).value); - } - else if (node instanceof NodeArray) { - packArray((NodeArray) node); - } - else { - throw new AssertionError(); - } + flushMessagePacker(); + } + catch (IOException e) { + throw _wrapIOFailure(e); } - flushMessagePacker(); nodes.clear(); isElementsClosed = false; } @@ -802,25 +960,78 @@ else if (node instanceof NodeArray) { private void flushMessagePacker() throws IOException { - MessagePacker messagePacker = getMessagePacker(); - messagePacker.flush(); + getMessagePacker().flush(); } @Override - protected void _releaseBuffers() + public tools.jackson.core.Version version() { - try { + return PackageVersion.VERSION; + } + + @Override + public TokenStreamContext streamWriteContext() + { + return writeContext; + } + + @Override + public Object streamWriteOutputTarget() + { + return output; + } + + @Override + public int streamWriteOutputBuffered() + { + return -1; + } + + @Override + public Object currentValue() + { + return writeContext.currentValue(); + } + + @Override + public void assignCurrentValue(Object v) + { + writeContext.assignCurrentValue(v); + } + + @Override + protected void _closeInput() throws IOException + { + if (StreamWriteFeature.AUTO_CLOSE_TARGET.enabledIn(_streamWriteFeatures)) { messagePacker.close(); } - catch (IOException e) { - throw new RuntimeException("Failed to close MessagePacker", e); + } + + @Override + protected void _releaseBuffers() + { + // No null check on get(): generators are single-threaded by contract so this + // ThreadLocal is always set on the calling thread. A null here would indicate + // cross-thread misuse; letting it NPE surfaces that bug immediately. + if (ownsThreadLocalBuffer) { + OutputStreamBufferOutput buf = messageBufferOutputHolder.get(); + if (buf != null) { + try { + buf.reset(null); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + } } } @Override - protected void _verifyValueWrite(String typeMsg) throws IOException + protected void _verifyValueWrite(String typeMsg) throws JacksonException { - // FIXME? + if (!writeContext.writeValue()) { + _reportError("Cannot " + typeMsg + ", expecting a property name"); + } } private MessagePacker getMessagePacker() diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java index 36fb235d..836a905d 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java @@ -15,11 +15,9 @@ // package org.msgpack.jackson.dataformat; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; - -import java.io.IOException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ser.std.StdSerializer; public class MessagePackKeySerializer extends StdSerializer @@ -30,9 +28,8 @@ public MessagePackKeySerializer() } @Override - public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) - throws IOException + public void serialize(Object value, JsonGenerator jgen, SerializationContext provider) { - jgen.writeFieldName(new MessagePackSerializedString(value)); + jgen.writeName(new MessagePackSerializedString(value)); } } diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java index 144c8d1a..674b2733 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java @@ -16,8 +16,11 @@ package org.msgpack.jackson.dataformat; import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.cfg.MapperBuilder; + +import tools.jackson.core.Version; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.cfg.MapperBuilder; +import tools.jackson.databind.cfg.MapperBuilderState; import java.math.BigDecimal; import java.math.BigInteger; @@ -28,46 +31,90 @@ public class MessagePackMapper extends ObjectMapper public static class Builder extends MapperBuilder { - public Builder(MessagePackMapper m) + public Builder(MessagePackFactory f) + { + super(f); + } + + protected Builder(StateImpl state) + { + super(state); + } + + @Override + public MessagePackMapper build() + { + return new MessagePackMapper(this); + } + + public Builder handleBigIntegerAsString() + { + return withConfigOverride(BigInteger.class, + o -> o.setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING))); + } + + public Builder handleBigDecimalAsString() + { + return withConfigOverride(BigDecimal.class, + o -> o.setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING))); + } + + public Builder handleBigIntegerAndBigDecimalAsString() { - super(m); + return handleBigIntegerAsString().handleBigDecimalAsString(); + } + + @Override + protected MapperBuilderState _saveState() + { + return new StateImpl(this); + } + + protected static class StateImpl extends MapperBuilderState + { + private static final long serialVersionUID = 3L; + + public StateImpl(Builder src) + { + super(src); + } + + @Override + protected Object readResolve() + { + return new Builder(this).build(); + } } } public MessagePackMapper() { - this(new MessagePackFactory()); + this(new Builder(new MessagePackFactory())); } public MessagePackMapper(MessagePackFactory f) { - super(f); + this(new Builder(f)); } - public MessagePackMapper handleBigIntegerAsString() + protected MessagePackMapper(Builder builder) { - configOverride(BigInteger.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); - return this; + super(builder); } - public MessagePackMapper handleBigDecimalAsString() - { - configOverride(BigDecimal.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); - return this; - } - - public MessagePackMapper handleBigIntegerAndBigDecimalAsString() + public static Builder builder() { - return handleBigIntegerAsString().handleBigDecimalAsString(); + return new Builder(new MessagePackFactory()); } - public static Builder builder() + public static Builder builder(MessagePackFactory f) { - return new Builder(new MessagePackMapper()); + return new Builder(f); } - public static Builder builder(MessagePackFactory f) + @Override + public Version version() { - return new Builder(new MessagePackMapper(f)); + return PackageVersion.VERSION; } } diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java index 72aeed20..98fb9c88 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java @@ -15,44 +15,41 @@ // package org.msgpack.jackson.dataformat; -import com.fasterxml.jackson.core.Base64Variant; -import com.fasterxml.jackson.core.JsonLocation; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonStreamContext; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.core.ObjectCodec; -import com.fasterxml.jackson.core.Version; -import com.fasterxml.jackson.core.base.ParserMinimalBase; -import com.fasterxml.jackson.core.io.IOContext; -import com.fasterxml.jackson.core.io.JsonEOFException; -import com.fasterxml.jackson.core.json.DupDetector; +import tools.jackson.core.Base64Variant; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonToken; +import tools.jackson.core.ObjectReadContext; +import tools.jackson.core.StreamReadFeature; +import tools.jackson.core.TokenStreamContext; +import tools.jackson.core.TokenStreamLocation; +import tools.jackson.core.Version; +import tools.jackson.core.base.ParserMinimalBase; +import tools.jackson.core.exc.UnexpectedEndOfInputException; +import tools.jackson.core.io.IOContext; +import tools.jackson.core.json.DupDetector; import org.msgpack.core.ExtensionTypeHeader; import org.msgpack.core.MessageFormat; import org.msgpack.core.MessagePack; import org.msgpack.core.MessageUnpacker; -import org.msgpack.core.buffer.ArrayBufferInput; -import org.msgpack.core.buffer.InputStreamBufferInput; import org.msgpack.core.buffer.MessageBufferInput; import org.msgpack.value.ValueType; import java.io.IOException; -import java.io.InputStream; + import java.math.BigDecimal; import java.math.BigInteger; -import java.nio.charset.StandardCharsets; - -import static org.msgpack.jackson.dataformat.JavaInfo.STRING_VALUE_FIELD_IS_CHARS; public class MessagePackParser extends ParserMinimalBase { + // Retained heap per idle thread: ~0.2 KB (MessageUnpacker with cleared input buffer). + // Negligible compared to Jackson's own per-thread buffer retention. private static final ThreadLocal> messageUnpackerHolder = new ThreadLocal<>(); private final MessageUnpacker messageUnpacker; private static final BigInteger LONG_MIN = BigInteger.valueOf(Long.MIN_VALUE); private static final BigInteger LONG_MAX = BigInteger.valueOf(Long.MAX_VALUE); - private ObjectCodec codec; private MessagePackReadContext streamReadContext; private boolean isClosed; @@ -60,61 +57,39 @@ public class MessagePackParser private long currentPosition; private final IOContext ioContext; private ExtensionTypeCustomDeserializers extTypeCustomDesers; - private final byte[] tempBytes = new byte[64]; - private final char[] tempChars = new char[64]; + private final boolean ownsThreadLocalUnpacker; private enum Type { - INT, LONG, DOUBLE, STRING, BYTES, BIG_INT, EXT + INT, LONG, DOUBLE, STRING, BYTES, BOOL, BIG_INT, EXT, NULL } private Type type; private int intValue; private long longValue; private double doubleValue; + private boolean booleanValue; private byte[] bytesValue; private String stringValue; private BigInteger biValue; private MessagePackExtensionType extensionTypeValue; - public MessagePackParser( - IOContext ctxt, - int features, - ObjectCodec objectCodec, - InputStream in, - boolean reuseResourceInParser) - throws IOException - { - this(ctxt, features, new InputStreamBufferInput(in), objectCodec, in, reuseResourceInParser); - } - - public MessagePackParser( - IOContext ctxt, - int features, - ObjectCodec objectCodec, - byte[] bytes, - boolean reuseResourceInParser) - throws IOException - { - this(ctxt, features, new ArrayBufferInput(bytes), objectCodec, bytes, reuseResourceInParser); - } - - private MessagePackParser(IOContext ctxt, - int features, + MessagePackParser(ObjectReadContext readCtxt, + IOContext ioCtxt, + int streamReadFeatures, MessageBufferInput input, - ObjectCodec objectCodec, Object src, boolean reuseResourceInParser) throws IOException { - super(features, ctxt.streamReadConstraints()); + super(readCtxt, ioCtxt, streamReadFeatures); - this.codec = objectCodec; - ioContext = ctxt; - DupDetector dups = Feature.STRICT_DUPLICATE_DETECTION.enabledIn(features) + ioContext = ioCtxt; + DupDetector dups = StreamReadFeature.STRICT_DUPLICATE_DETECTION.enabledIn(streamReadFeatures) ? DupDetector.rootDetector(this) : null; streamReadContext = MessagePackReadContext.createRootContext(dups); if (!reuseResourceInParser) { messageUnpacker = MessagePack.newDefaultUnpacker(input); + ownsThreadLocalUnpacker = false; return; } @@ -123,16 +98,21 @@ private MessagePackParser(IOContext ctxt, messageUnpacker = MessagePack.newDefaultUnpacker(input); } else { - // Considering to reuse InputStream with JsonParser.Feature.AUTO_CLOSE_SOURCE, + // Considering to reuse InputStream with StreamReadFeature.AUTO_CLOSE_SOURCE, // MessagePackParser needs to use the MessageUnpacker that has the same InputStream // since it has buffer which has loaded the InputStream data ahead. // However, it needs to call MessageUnpacker#reset when the source is different from the previous one. - if (isEnabled(JsonParser.Feature.AUTO_CLOSE_SOURCE) || messageUnpackerTuple.first() != src) { + Object cachedSrc = messageUnpackerTuple.first(); + if (StreamReadFeature.AUTO_CLOSE_SOURCE.enabledIn(streamReadFeatures) || cachedSrc != src || src instanceof byte[]) { + // reset() replaces the internal MessageBufferInput and clears the unpacker's + // internal read buffer to EMPTY_BUFFER. The old ArrayBufferInput becomes + // unreachable here (we discard the return value), so its byte[] is GC-eligible. messageUnpackerTuple.second().reset(input); } messageUnpacker = messageUnpackerTuple.second(); } messageUnpackerHolder.set(new Tuple<>(src, messageUnpacker)); + ownsThreadLocalUnpacker = true; } public void setExtensionTypeCustomDeserializers(ExtensionTypeCustomDeserializers extTypeCustomDesers) @@ -141,54 +121,33 @@ public void setExtensionTypeCustomDeserializers(ExtensionTypeCustomDeserializers } @Override - public ObjectCodec getCodec() + public Version version() { - return codec; + return PackageVersion.VERSION; } - @Override - public void setCodec(ObjectCodec c) + private String unpackString(MessageUnpacker messageUnpacker) throws IOException { - codec = c; + return messageUnpacker.unpackString(); } @Override - public Version version() + public JsonToken nextToken() throws JacksonException { - return null; - } - - private String unpackString(MessageUnpacker messageUnpacker) throws IOException - { - int strLen = messageUnpacker.unpackRawStringHeader(); - if (strLen <= tempBytes.length) { - messageUnpacker.readPayload(tempBytes, 0, strLen); - if (STRING_VALUE_FIELD_IS_CHARS.get()) { - for (int i = 0; i < strLen; i++) { - byte b = tempBytes[i]; - if ((0x80 & b) != 0) { - return new String(tempBytes, 0, strLen, StandardCharsets.UTF_8); - } - tempChars[i] = (char) b; - } - return new String(tempChars, 0, strLen); - } - else { - return new String(tempBytes, 0, strLen); - } + try { + return _nextToken(); } - else { - byte[] bytes = messageUnpacker.readPayload(strLen); - return new String(bytes, 0, strLen, StandardCharsets.UTF_8); + catch (IOException e) { + throw _wrapIOFailure(e); } } - @Override - public JsonToken nextToken() throws IOException + private JsonToken _nextToken() throws IOException { + type = null; tokenPosition = messageUnpacker.getTotalReadBytes(); - boolean isObjectValueSet = streamReadContext.inObject() && _currToken != JsonToken.FIELD_NAME; + boolean isObjectValueSet = streamReadContext.inObject() && _currToken != JsonToken.PROPERTY_NAME; if (isObjectValueSet) { if (!streamReadContext.expectMoreValues()) { streamReadContext = streamReadContext.getParent(); @@ -203,7 +162,10 @@ else if (streamReadContext.inArray()) { } if (!messageUnpacker.hasNext()) { - throw new JsonEOFException(this, null, "Unexpected EOF"); + if (streamReadContext.inRoot()) { + return null; + } + throw new UnexpectedEndOfInputException(this, null, null); } MessageFormat format = messageUnpacker.getNextFormat(); @@ -214,9 +176,10 @@ else if (streamReadContext.inArray()) { case STRING: type = Type.STRING; stringValue = unpackString(messageUnpacker); + _streamReadConstraints.validateStringLength(stringValue.length()); if (isObjectValueSet) { streamReadContext.setCurrentName(stringValue); - nextToken = JsonToken.FIELD_NAME; + nextToken = JsonToken.PROPERTY_NAME; } else { nextToken = JsonToken.VALUE_STRING; @@ -255,21 +218,30 @@ else if (streamReadContext.inArray()) { if (isObjectValueSet) { streamReadContext.setCurrentName(String.valueOf(v)); - nextToken = JsonToken.FIELD_NAME; + nextToken = JsonToken.PROPERTY_NAME; } else { nextToken = JsonToken.VALUE_NUMBER_INT; } break; case NIL: + type = Type.NULL; messageUnpacker.unpackNil(); - nextToken = JsonToken.VALUE_NULL; + if (isObjectValueSet) { + streamReadContext.setCurrentName(null); + nextToken = JsonToken.PROPERTY_NAME; + } + else { + nextToken = JsonToken.VALUE_NULL; + } break; case BOOLEAN: boolean b = messageUnpacker.unpackBoolean(); + type = Type.BOOL; + booleanValue = b; if (isObjectValueSet) { streamReadContext.setCurrentName(Boolean.toString(b)); - nextToken = JsonToken.FIELD_NAME; + nextToken = JsonToken.PROPERTY_NAME; } else { nextToken = b ? JsonToken.VALUE_TRUE : JsonToken.VALUE_FALSE; @@ -280,7 +252,7 @@ else if (streamReadContext.inArray()) { doubleValue = messageUnpacker.unpackDouble(); if (isObjectValueSet) { streamReadContext.setCurrentName(String.valueOf(doubleValue)); - nextToken = JsonToken.FIELD_NAME; + nextToken = JsonToken.PROPERTY_NAME; } else { nextToken = JsonToken.VALUE_NUMBER_FLOAT; @@ -289,10 +261,11 @@ else if (streamReadContext.inArray()) { case BINARY: type = Type.BYTES; int len = messageUnpacker.unpackBinaryHeader(); + _streamReadConstraints.validateStringLength(len); bytesValue = messageUnpacker.readPayload(len); if (isObjectValueSet) { streamReadContext.setCurrentName(new String(bytesValue, MessagePack.UTF8)); - nextToken = JsonToken.FIELD_NAME; + nextToken = JsonToken.PROPERTY_NAME; } else { nextToken = JsonToken.VALUE_EMBEDDED_OBJECT; @@ -301,25 +274,28 @@ else if (streamReadContext.inArray()) { case ARRAY: nextToken = JsonToken.START_ARRAY; streamReadContext = streamReadContext.createChildArrayContext(messageUnpacker.unpackArrayHeader()); + _streamReadConstraints.validateNestingDepth(streamReadContext.getNestingDepth()); break; case MAP: nextToken = JsonToken.START_OBJECT; streamReadContext = streamReadContext.createChildObjectContext(messageUnpacker.unpackMapHeader()); + _streamReadConstraints.validateNestingDepth(streamReadContext.getNestingDepth()); break; case EXTENSION: type = Type.EXT; ExtensionTypeHeader header = messageUnpacker.unpackExtensionTypeHeader(); + _streamReadConstraints.validateStringLength(header.getLength()); extensionTypeValue = new MessagePackExtensionType(header.getType(), messageUnpacker.readPayload(header.getLength())); if (isObjectValueSet) { streamReadContext.setCurrentName(deserializedExtensionTypeValue().toString()); - nextToken = JsonToken.FIELD_NAME; + nextToken = JsonToken.PROPERTY_NAME; } else { nextToken = JsonToken.VALUE_EMBEDDED_OBJECT; } break; default: - throw new IllegalStateException("Shouldn't reach here"); + nextToken = _reportError("Unexpected MessagePack format type: " + valueType); } currentPosition = messageUnpacker.getTotalReadBytes(); @@ -334,8 +310,11 @@ protected void _handleEOF() } @Override - public String getText() throws IOException + public String getString() { + if (type == null) { + return _currToken == null ? null : _currToken.asString(); + } switch (type) { case STRING: return stringValue; @@ -347,35 +326,44 @@ public String getText() throws IOException return String.valueOf(longValue); case DOUBLE: return String.valueOf(doubleValue); + case BOOL: + return Boolean.toString(booleanValue); case BIG_INT: return String.valueOf(biValue); case EXT: - return deserializedExtensionTypeValue().toString(); + try { + return deserializedExtensionTypeValue().toString(); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + case NULL: + return "null"; default: - throw new IllegalStateException("Invalid type=" + type); + return _reportError("Unexpected MessagePack value type: " + type); } } @Override - public char[] getTextCharacters() throws IOException + public char[] getStringCharacters() { - return getText().toCharArray(); + return getString().toCharArray(); } @Override - public boolean hasTextCharacters() + public boolean hasStringCharacters() { return false; } @Override - public int getTextLength() throws IOException + public int getStringLength() { - return getText().length(); + return getString().length(); } @Override - public int getTextOffset() + public int getStringOffset() { return 0; } @@ -383,6 +371,9 @@ public int getTextOffset() @Override public byte[] getBinaryValue(Base64Variant b64variant) { + if (type == null) { + return _reportError("Current token (" + _currToken + ") not of binary type"); + } switch (type) { case BYTES: return bytesValue; @@ -390,14 +381,24 @@ public byte[] getBinaryValue(Base64Variant b64variant) return stringValue.getBytes(MessagePack.UTF8); case EXT: return extensionTypeValue.getData(); + case INT: + case LONG: + case DOUBLE: + case BOOL: + case BIG_INT: + case NULL: + return _reportError("Current token (" + _currToken + ") not of binary type"); default: - throw new IllegalStateException("Invalid type=" + type); + return _reportError("Unexpected MessagePack value type: " + type); } } @Override public Number getNumberValue() { + if (type == null) { + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + } switch (type) { case INT: return intValue; @@ -407,65 +408,132 @@ public Number getNumberValue() return doubleValue; case BIG_INT: return biValue; + case NULL: + case BOOL: + case STRING: + case BYTES: + case EXT: + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); default: - throw new IllegalStateException("Invalid type=" + type); + return _reportError("Unexpected MessagePack value type: " + type); } } @Override public int getIntValue() { + if (type == null) { + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + } switch (type) { case INT: return intValue; case LONG: + if (longValue < Integer.MIN_VALUE || longValue > Integer.MAX_VALUE) { + return _reportError("Numeric value (" + longValue + ") out of range for `int`"); + } return (int) longValue; case DOUBLE: + if (!Double.isFinite(doubleValue)) { + return _reportError("Cannot convert non-finite double (" + doubleValue + ") to `int`"); + } + if (doubleValue < Integer.MIN_VALUE || doubleValue > Integer.MAX_VALUE) { + return _reportError("Numeric value (" + doubleValue + ") out of range for `int`"); + } return (int) doubleValue; case BIG_INT: - return biValue.intValue(); + try { + return biValue.intValueExact(); + } + catch (ArithmeticException e) { + return _reportError("Numeric value (" + biValue + ") out of range for `int`"); + } + case NULL: + case BOOL: + case STRING: + case BYTES: + case EXT: + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); default: - throw new IllegalStateException("Invalid type=" + type); + return _reportError("Unexpected MessagePack value type: " + type); } } @Override public long getLongValue() { + if (type == null) { + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + } switch (type) { case INT: return intValue; case LONG: return longValue; case DOUBLE: + if (!Double.isFinite(doubleValue)) { + return _reportError("Cannot convert non-finite double (" + doubleValue + ") to `long`"); + } + // (double)Long.MAX_VALUE rounds up to 2^63; use >= to reject 2^63 itself. + if (doubleValue < Long.MIN_VALUE || doubleValue >= (double) Long.MAX_VALUE) { + return _reportError("Numeric value (" + doubleValue + ") out of range for `long`"); + } return (long) doubleValue; case BIG_INT: - return biValue.longValue(); + try { + return biValue.longValueExact(); + } + catch (ArithmeticException e) { + return _reportError("Numeric value (" + biValue + ") out of range for `long`"); + } + case NULL: + case BOOL: + case STRING: + case BYTES: + case EXT: + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); default: - throw new IllegalStateException("Invalid type=" + type); + return _reportError("Unexpected MessagePack value type: " + type); } } @Override public BigInteger getBigIntegerValue() { + if (type == null) { + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + } switch (type) { case INT: return BigInteger.valueOf(intValue); case LONG: return BigInteger.valueOf(longValue); case DOUBLE: - return BigInteger.valueOf((long) doubleValue); + if (!Double.isFinite(doubleValue)) { + return _reportError("Cannot convert non-finite double (" + doubleValue + ") to BigInteger"); + } + return BigDecimal.valueOf(doubleValue).toBigInteger(); // truncates fractional part case BIG_INT: return biValue; + case NULL: + case BOOL: + case STRING: + case BYTES: + case EXT: + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); default: - throw new IllegalStateException("Invalid type=" + type); + return _reportError("Unexpected MessagePack value type: " + type); } } @Override public float getFloatValue() { + // No bounds/range check: a finite double or large BigInteger may overflow to + // Float.POSITIVE_INFINITY. This is intentional — same as ParserBase and CBORParser. + if (type == null) { + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + } switch (type) { case INT: return (float) intValue; @@ -475,42 +543,71 @@ public float getFloatValue() return (float) doubleValue; case BIG_INT: return biValue.floatValue(); + case NULL: + case BOOL: + case STRING: + case BYTES: + case EXT: + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); default: - throw new IllegalStateException("Invalid type=" + type); + return _reportError("Unexpected MessagePack value type: " + type); } } @Override public double getDoubleValue() { - switch (type) { - case INT: - return intValue; + // No bounds/range check: large BigInteger may overflow to Double.POSITIVE_INFINITY, + // and large long values may lose precision. Intentional — same as ParserBase. + if (type == null) { + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + } + switch (type) { + case INT: + return intValue; case LONG: return (double) longValue; case DOUBLE: return doubleValue; case BIG_INT: return biValue.doubleValue(); + case NULL: + case BOOL: + case STRING: + case BYTES: + case EXT: + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); default: - throw new IllegalStateException("Invalid type=" + type); + return _reportError("Unexpected MessagePack value type: " + type); } } @Override public BigDecimal getDecimalValue() { - switch (type) { - case INT: - return BigDecimal.valueOf(intValue); + if (type == null) { + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); + } + switch (type) { + case INT: + return BigDecimal.valueOf(intValue); case LONG: return BigDecimal.valueOf(longValue); case DOUBLE: - return BigDecimal.valueOf(doubleValue); + if (!Double.isFinite(doubleValue)) { + return _reportError("Cannot convert non-finite double (" + doubleValue + ") to BigDecimal"); + } + return BigDecimal.valueOf(doubleValue); case BIG_INT: return new BigDecimal(biValue); + case NULL: + case BOOL: + case STRING: + case BYTES: + case EXT: + return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); default: - throw new IllegalStateException("Invalid type=" + type); + return _reportError("Unexpected MessagePack value type: " + type); } } @@ -527,21 +624,40 @@ private Object deserializedExtensionTypeValue() } @Override - public Object getEmbeddedObject() throws IOException + public Object getEmbeddedObject() { + if (type == null) { + return _reportError("Current token (" + _currToken + ") not of embeddable type"); + } switch (type) { case BYTES: return bytesValue; case EXT: - return deserializedExtensionTypeValue(); + try { + return deserializedExtensionTypeValue(); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + case INT: + case LONG: + case DOUBLE: + case BOOL: + case BIG_INT: + case STRING: + case NULL: + return _reportError("Current token (" + _currToken + ") not of embeddable type"); default: - throw new IllegalStateException("Invalid type=" + type); + return _reportError("Unexpected MessagePack value type: " + type); } } @Override public NumberType getNumberType() { + if (type == null) { + return null; + } switch (type) { case INT: return NumberType.INT; @@ -551,100 +667,135 @@ public NumberType getNumberType() return NumberType.DOUBLE; case BIG_INT: return NumberType.BIG_INTEGER; + case NULL: + case BOOL: + case STRING: + case BYTES: + case EXT: + return null; default: - throw new IllegalStateException("Invalid type=" + type); + return _reportError("Unexpected MessagePack value type: " + type); } } @Override - public void close() - throws IOException + protected void _closeInput() throws IOException { - try { - if (isEnabled(JsonParser.Feature.AUTO_CLOSE_SOURCE)) { - messageUnpacker.close(); - } + if (StreamReadFeature.AUTO_CLOSE_SOURCE.enabledIn(_streamReadFeatures)) { + messageUnpacker.close(); } - finally { - isClosed = true; + if (ownsThreadLocalUnpacker) { + Tuple tuple = messageUnpackerHolder.get(); + if (tuple != null) { + if (tuple.first() instanceof byte[]) { + // close() calls ArrayBufferInput.close() which sets buffer = null, + // releasing the byte[] payload reference held by the unpacker's input. + // The unpacker itself is kept alive for reuse on the next parse. + tuple.second().close(); + messageUnpackerHolder.set(new Tuple<>(null, tuple.second())); + } + else if (StreamReadFeature.AUTO_CLOSE_SOURCE.enabledIn(_streamReadFeatures)) { + // Stream is already closed above; release the reference so it doesn't + // linger on the thread until the next parse. + messageUnpackerHolder.set(new Tuple<>(null, tuple.second())); + } + // else: InputStream with AUTO_CLOSE_SOURCE disabled — keep the reference + // so the next parse on the same thread can detect same-stream reuse and + // avoid resetting the unpacker (which would discard its read-ahead buffer). + } } } @Override - public boolean isClosed() + protected void _releaseBuffers() { - return isClosed; } @Override - public JsonStreamContext getParsingContext() + public boolean isClosed() { - return streamReadContext; + return isClosed; } @Override - public JsonLocation currentTokenLocation() + public void close() { - return new JsonLocation(ioContext.contentReference(), tokenPosition, -1, -1); + if (isClosed) { + return; + } + // Parsers are single-threaded by contract; close() is expected on the same + // thread that created the parser. Cross-thread close is not a supported use case. + try { + _closeInput(); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + finally { + isClosed = true; + } } @Override - public JsonLocation currentLocation() + public TokenStreamContext streamReadContext() { - return new JsonLocation(ioContext.contentReference(), currentPosition, -1, -1); + return streamReadContext; } @Override - @Deprecated - public JsonLocation getTokenLocation() + public TokenStreamLocation currentTokenLocation() { - return currentTokenLocation(); + // columnNr repurposed as byte offset; truncates for inputs > 2 GB + return new TokenStreamLocation(ioContext.contentReference(), tokenPosition, -1, (int) tokenPosition); } @Override - @Deprecated - public JsonLocation getCurrentLocation() + public TokenStreamLocation currentLocation() { - return currentLocation(); + // columnNr repurposed as byte offset; truncates for inputs > 2 GB + return new TokenStreamLocation(ioContext.contentReference(), currentPosition, -1, (int) currentPosition); } @Override - public void overrideCurrentName(String name) + public String currentName() { // Simple, but need to look for START_OBJECT/ARRAY's "off-by-one" thing: - MessagePackReadContext ctxt = streamReadContext; if (_currToken == JsonToken.START_OBJECT || _currToken == JsonToken.START_ARRAY) { - ctxt = ctxt.getParent(); - } - // Unfortunate, but since we did not expose exceptions, need to wrap - try { - ctxt.setCurrentName(name); - } - catch (IOException e) { - throw new IllegalStateException(e); + MessagePackReadContext parent = streamReadContext.getParent(); + return parent.currentName(); } + return streamReadContext.currentName(); } @Override - public String currentName() + public Object streamReadInputSource() { - if (_currToken == JsonToken.START_OBJECT || _currToken == JsonToken.START_ARRAY) { - MessagePackReadContext parent = streamReadContext.getParent(); - return parent.getCurrentName(); - } - return streamReadContext.getCurrentName(); + return ioContext.contentReference().getRawContent(); } - public boolean isCurrentFieldId() + @Override + public Object currentValue() { - return this.type == Type.INT || this.type == Type.LONG; + return streamReadContext.currentValue(); } @Override - @Deprecated - public String getCurrentName() - throws IOException + public void assignCurrentValue(Object v) + { + streamReadContext.assignCurrentValue(v); + } + + @Override + public boolean isNaN() { - return currentName(); + if (type == Type.DOUBLE) { + return Double.isNaN(doubleValue) || Double.isInfinite(doubleValue); + } + return false; + } + + public boolean isCurrentFieldId() + { + return this.type == Type.INT || this.type == Type.LONG; } } diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java index 403f1b36..ea830626 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java @@ -1,27 +1,35 @@ +// +// 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.core.JsonLocation; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.JsonStreamContext; -import com.fasterxml.jackson.core.io.CharTypes; -import com.fasterxml.jackson.core.io.ContentReference; -import com.fasterxml.jackson.core.json.DupDetector; +import tools.jackson.core.TokenStreamContext; +import tools.jackson.core.TokenStreamLocation; +import tools.jackson.core.exc.StreamReadException; +import tools.jackson.core.io.ContentReference; +import tools.jackson.core.json.DupDetector; /** - * Replacement of {@link com.fasterxml.jackson.core.json.JsonReadContext} + * Replacement of {@link tools.jackson.core.json.JsonReadContext} * to support features needed by MessagePack format. */ public final class MessagePackReadContext - extends JsonStreamContext + extends TokenStreamContext { - /** - * Parent context for this context; null for root context. - */ protected final MessagePackReadContext parent; - // // // Optional duplicate detection - protected final DupDetector dups; /** @@ -29,26 +37,12 @@ public final class MessagePackReadContext */ protected int expEntryCount; - // // // Location information (minus source reference) - protected String currentName; protected Object currentValue; - /* - /********************************************************** - /* Simple instance reuse slots - /********************************************************** - */ - protected MessagePackReadContext child = null; - /* - /********************************************************** - /* Instance construction, reuse - /********************************************************** - */ - public MessagePackReadContext(MessagePackReadContext parent, DupDetector dups, int type, int expEntryCount) { @@ -74,19 +68,17 @@ protected void reset(int type, int expEntryCount) } @Override - public Object getCurrentValue() + public Object currentValue() { return currentValue; } @Override - public void setCurrentValue(Object v) + public void assignCurrentValue(Object v) { currentValue = v; } - // // // Factory methods - public static MessagePackReadContext createRootContext(DupDetector dups) { return new MessagePackReadContext(null, dups, TYPE_ROOT, -1); @@ -121,14 +113,8 @@ public MessagePackReadContext createChildObjectContext(int expEntryCount) return ctxt; } - /* - /********************************************************** - /* Abstract method implementation - /********************************************************** - */ - @Override - public String getCurrentName() + public String currentName() { return currentName; } @@ -139,12 +125,6 @@ public MessagePackReadContext getParent() return parent; } - /* - /********************************************************** - /* Extended API - /********************************************************** - */ - public boolean hasExpectedLength() { return (expEntryCount >= 0); @@ -163,7 +143,6 @@ public boolean isEmpty() public int getRemainingExpectedLength() { int diff = expEntryCount - _index; - // Negative values would occur when expected count is -1 return Math.max(0, diff); } @@ -172,15 +151,6 @@ public boolean acceptsBreakMarker() return (expEntryCount < 0) && _type != TYPE_ROOT; } - /** - * Method called to increment the current entry count (Object property, Array - * element or Root value) for this context level - * and then see if more entries are accepted. - * The only case where more entries are NOT expected is for fixed-count - * Objects and Arrays that just reached the entry count. - *

- * Note that since the entry count is updated this is a state-changing method. - */ public boolean expectMoreValues() { if (++_index == expEntryCount) { @@ -189,30 +159,12 @@ public boolean expectMoreValues() return true; } - /** - * @return Location pointing to the point where the context - * start marker was found - */ - @Override - public JsonLocation startLocation(ContentReference srcRef) - { - return new JsonLocation(srcRef, 1L, -1, -1); - } - - @Override - @Deprecated // since 2.13 - public JsonLocation getStartLocation(Object rawSrc) + public TokenStreamLocation startLocation(ContentReference srcRef) { - return startLocation(ContentReference.rawReference(rawSrc)); + return new TokenStreamLocation(srcRef, 1L, -1, -1); } - /* - /********************************************************** - /* State changes - /********************************************************** - */ - - public void setCurrentName(String name) throws JsonProcessingException + public void setCurrentName(String name) { currentName = name; if (dups != null) { @@ -220,24 +172,16 @@ public void setCurrentName(String name) throws JsonProcessingException } } - private void _checkDup(DupDetector dd, String name) throws JsonProcessingException + private void _checkDup(DupDetector dd, String name) { - if (dd.isDup(name)) { - throw new JsonParseException(null, + // Null names (nil map keys) cannot be duplicate-checked via DupDetector + // because DupDetector.isDup(null) NPEs when a prior non-null name exists. + if (name != null && dd.isDup(name)) { + throw new StreamReadException(null, "Duplicate field '" + name + "'", dd.findLocation()); } } - /* - /********************************************************** - /* Overridden standard methods - /********************************************************** - */ - - /** - * Overridden to provide developer readable "JsonPath" representation - * of the context. - */ @Override public String toString() { @@ -255,7 +199,7 @@ public String toString() sb.append('{'); if (currentName != null) { sb.append('"'); - CharTypes.appendQuoted(sb, currentName); + sb.append(currentName.replace("\\", "\\\\").replace("\"", "\\\"")); sb.append('"'); } else { diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java index 72ed5d8d..8bff3000 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java @@ -15,8 +15,9 @@ // package org.msgpack.jackson.dataformat; -import com.fasterxml.jackson.core.SerializableString; +import tools.jackson.core.SerializableString; +import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -36,79 +37,102 @@ public MessagePackSerializedString(Object value) @Override public String getValue() { - return value.toString(); + return value == null ? null : value.toString(); } @Override public int charLength() { - return getValue().length(); + String v = getValue(); + return v == null ? 0 : v.length(); } @Override public char[] asQuotedChars() { - return getValue().toCharArray(); + String v = getValue(); + return v == null ? new char[0] : v.toCharArray(); } @Override public byte[] asUnquotedUTF8() { - return getValue().getBytes(UTF8); + String v = getValue(); + return v == null ? new byte[0] : v.getBytes(UTF8); } @Override public byte[] asQuotedUTF8() { - return ("\"" + getValue() + "\"").getBytes(UTF8); + return asUnquotedUTF8(); } @Override public int appendQuotedUTF8(byte[] bytes, int i) { - return 0; + return appendUnquotedUTF8(bytes, i); } @Override public int appendQuoted(char[] chars, int i) { - return 0; + return appendUnquoted(chars, i); } @Override public int appendUnquotedUTF8(byte[] bytes, int i) { - return 0; + byte[] utf8 = asUnquotedUTF8(); + if (utf8.length > bytes.length - i) { + return -1; + } + System.arraycopy(utf8, 0, bytes, i, utf8.length); + return utf8.length; } @Override public int appendUnquoted(char[] chars, int i) { - return 0; + String v = getValue(); + if (v == null) { + return 0; + } + if (v.length() > chars.length - i) { + return -1; + } + v.getChars(0, v.length(), chars, i); + return v.length(); } @Override - public int writeQuotedUTF8(OutputStream outputStream) + public int writeQuotedUTF8(OutputStream outputStream) throws IOException { - return 0; + return writeUnquotedUTF8(outputStream); } @Override - public int writeUnquotedUTF8(OutputStream outputStream) + public int writeUnquotedUTF8(OutputStream outputStream) throws IOException { - return 0; + byte[] utf8 = asUnquotedUTF8(); + outputStream.write(utf8); + return utf8.length; } @Override public int putQuotedUTF8(ByteBuffer byteBuffer) { - return 0; + return putUnquotedUTF8(byteBuffer); } @Override public int putUnquotedUTF8(ByteBuffer byteBuffer) { - return 0; + byte[] utf8 = asUnquotedUTF8(); + if (utf8.length > byteBuffer.remaining()) { + return -1; + } + byteBuffer.put(utf8); + return utf8.length; } public Object getRawValue() diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java index 4100ef4c..0aa72293 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java @@ -15,45 +15,30 @@ // package org.msgpack.jackson.dataformat; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializationConfig; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.cfg.SerializerFactoryConfig; -import com.fasterxml.jackson.databind.ser.BeanSerializerFactory; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.cfg.SerializerFactoryConfig; +import tools.jackson.databind.ser.BeanSerializerFactory; public class MessagePackSerializerFactory extends BeanSerializerFactory { - /** - * Constructor for creating instances without configuration. - */ public MessagePackSerializerFactory() { super(null); } - /** - * Constructor for creating instances with specified configuration. - * - * @param config - */ public MessagePackSerializerFactory(SerializerFactoryConfig config) { super(config); } - @Override - public JsonSerializer createKeySerializer(SerializerProvider prov, JavaType keyType, JsonSerializer defaultImpl) throws JsonMappingException - { - return new MessagePackKeySerializer(); - } + private static final MessagePackKeySerializer KEY_SERIALIZER = new MessagePackKeySerializer(); @Override - @Deprecated - public JsonSerializer createKeySerializer(SerializationConfig config, JavaType keyType, JsonSerializer defaultImpl) + public ValueSerializer createKeySerializer(SerializationContext ctxt, JavaType keyType) { - return new MessagePackKeySerializer(); + return KEY_SERIALIZER; } } diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackWriteContext.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackWriteContext.java similarity index 100% rename from msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackWriteContext.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackWriteContext.java diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/PackageVersion.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/PackageVersion.java similarity index 100% rename from msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/PackageVersion.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/PackageVersion.java 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 old mode 100755 new mode 100644 index 2216d276..a1df18e4 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java @@ -1,12 +1,27 @@ +// +// 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.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 tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.deser.std.StdDeserializer; +import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.ser.std.StdSerializer; import org.msgpack.core.ExtensionTypeHeader; import org.msgpack.core.MessagePack; import org.msgpack.core.MessagePacker; @@ -34,20 +49,26 @@ protected InstantSerializer(Class t) } @Override - public void serialize(Instant value, JsonGenerator gen, SerializerProvider provider) - throws IOException + public void serialize(Instant value, JsonGenerator gen, SerializationContext provider) { - 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()); + try { + // Per-call allocation is a known limitation carried from the v2 module. + // Manually encoding the timestamp bytes would avoid it but duplicates + // msgpack-core's timestamp logic. Tracked as a future optimization. + ByteArrayOutputStream os = new ByteArrayOutputStream(); + 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); + MessagePackExtensionType extensionType = new MessagePackExtensionType(EXT_TYPE, bytes); + gen.writePOJO(extensionType); + } + } + catch (IOException e) { + throw _wrapIOFailure(provider, e); } } } @@ -61,17 +82,21 @@ protected InstantDeserializer(Class 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())); + try { + // Per-call allocation is a known limitation — see serialize() above. + MessagePackExtensionType ext = p.readValueAs(MessagePackExtensionType.class); + if (ext.getType() != EXT_TYPE) { + ctxt.reportInputMismatch(Instant.class, + "Unexpected extension type (0x%X) for Instant object", ext.getType() & 0xFF); + return null; // unreachable + } + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(ext.getData())) { + return unpacker.unpackTimestamp(new ExtensionTypeHeader(EXT_TYPE, ext.getData().length)); + } } - - // 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)); + catch (IOException e) { + throw _wrapIOFailure(ctxt, e); } } } diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/Tuple.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/Tuple.java index 1a252739..b0f720d8 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/Tuple.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/Tuple.java @@ -15,9 +15,6 @@ // package org.msgpack.jackson.dataformat; -/** - * Created by komamitsu on 5/28/15. - */ public class Tuple { private final F first; diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java index 5414b0bd..6a194a8c 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java @@ -15,18 +15,18 @@ // package org.msgpack.jackson.dataformat; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.TreeNode; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JacksonException; +import tools.jackson.core.TreeNode; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; import org.junit.Test; import java.io.IOException; @@ -90,38 +90,42 @@ public Map getObjects() } static class ObjectContainerSerializer - extends JsonSerializer + extends ValueSerializer { @Override - public void serialize(ObjectContainer value, JsonGenerator gen, SerializerProvider serializers) - throws IOException, JsonProcessingException + public void serialize(ObjectContainer value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException { gen.writeStartObject(); HashMap metadata = new HashMap(); for (Map.Entry entry : value.getObjects().entrySet()) { metadata.put(entry.getKey(), entry.getValue().getClass().getName()); } - gen.writeObjectField("__metadata", metadata); - gen.writeObjectField("objects", value.getObjects()); + gen.writePOJOProperty("__metadata", metadata); + gen.writePOJOProperty("objects", value.getObjects()); gen.writeEndObject(); } } static class ObjectContainerDeserializer - extends JsonDeserializer + extends ValueDeserializer { @Override public ObjectContainer deserialize(JsonParser p, DeserializationContext ctxt) - throws IOException, JsonProcessingException + throws JacksonException { ObjectContainer objectContainer = new ObjectContainer(new HashMap()); TreeNode treeNode = p.readValueAsTree(); - Map metadata = treeNode.get("__metadata").traverse(p.getCodec()).readValueAs(new TypeReference>() {}); + Map metadata = treeNode.get("__metadata") + .traverse(p.objectReadContext()) + .readValueAs(new TypeReference>() {}); TreeNode dataMapTree = treeNode.get("objects"); for (Map.Entry entry : metadata.entrySet()) { try { - Object o = dataMapTree.get(entry.getKey()).traverse(p.getCodec()).readValueAs(Class.forName(entry.getValue())); + Object o = dataMapTree.get(entry.getKey()) + .traverse(p.objectReadContext()) + .readValueAs(Class.forName(entry.getValue())); objectContainer.getObjects().put(entry.getKey(), o); } catch (ClassNotFoundException e) { @@ -151,7 +155,7 @@ public void test() objectContainer.getObjects().put("pi", pi); } - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory()); byte[] bytes = objectMapper.writeValueAsBytes(objectContainer); ObjectContainer restored = objectMapper.readValue(bytes, ObjectContainer.class); diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java index bbac6fb9..23983a39 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java @@ -15,27 +15,28 @@ // package org.msgpack.jackson.dataformat; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.KeyDeserializer; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.deser.NullValueProvider; -import com.fasterxml.jackson.databind.deser.impl.JDKValueInstantiators; -import com.fasterxml.jackson.databind.deser.std.MapDeserializer; -import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.type.TypeFactory; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JacksonException; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.KeyDeserializer; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.deser.NullValueProvider; +import tools.jackson.databind.deser.jdk.JDKValueInstantiators; +import tools.jackson.databind.deser.jdk.MapDeserializer; +import tools.jackson.databind.jsontype.TypeDeserializer; +import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.type.TypeFactory; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.LinkedHashMap; -import org.junit.jupiter.api.Test; +import org.junit.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.Assert.assertEquals; public class MessagePackDataformatForFieldIdTest { @@ -45,7 +46,7 @@ static class MessagePackMapDeserializer extends MapDeserializer { @Override public Object deserializeKey(String s, DeserializationContext deserializationContext) - throws IOException + throws JacksonException { JsonParser parser = deserializationContext.getParser(); if (parser instanceof MessagePackParser) { @@ -61,13 +62,13 @@ public Object deserializeKey(String s, DeserializationContext deserializationCon public MessagePackMapDeserializer() { super( - TypeFactory.defaultInstance().constructMapType(Map.class, Object.class, Object.class), + TypeFactory.createDefaultInstance().constructMapType(Map.class, Object.class, Object.class), JDKValueInstantiators.findStdValueInstantiator(null, LinkedHashMap.class), keyDeserializer, null, null); } public MessagePackMapDeserializer(MapDeserializer src, KeyDeserializer keyDeser, - JsonDeserializer valueDeser, TypeDeserializer valueTypeDeser, NullValueProvider nuller, + ValueDeserializer valueDeser, TypeDeserializer valueTypeDeser, NullValueProvider nuller, Set ignorable, Set includable) { super(src, keyDeser, valueDeser, valueTypeDeser, nuller, ignorable, includable); @@ -75,10 +76,10 @@ public MessagePackMapDeserializer(MapDeserializer src, KeyDeserializer keyDeser, @Override protected MapDeserializer withResolved(KeyDeserializer keyDeser, TypeDeserializer valueTypeDeser, - JsonDeserializer valueDeser, NullValueProvider nuller, Set ignorable, + ValueDeserializer valueDeser, NullValueProvider nuller, Set ignorable, Set includable) { - return new MessagePackMapDeserializer(this, keyDeser, (JsonDeserializer) valueDeser, valueTypeDeser, + return new MessagePackMapDeserializer(this, keyDeser, (ValueDeserializer) valueDeser, valueTypeDeser, nuller, ignorable, includable); } } @@ -87,12 +88,13 @@ protected MapDeserializer withResolved(KeyDeserializer keyDeser, TypeDeserialize public void testMixedKeys() throws IOException { - ObjectMapper mapper = new ObjectMapper( + ObjectMapper mapper = MessagePackMapper.builder( new MessagePackFactory() .setSupportIntegerKeys(true) ) - .registerModule(new SimpleModule() - .addDeserializer(Map.class, new MessagePackMapDeserializer())); + .addModule(new SimpleModule() + .addDeserializer(Map.class, new MessagePackMapDeserializer())) + .build(); Map map = new HashMap<>(); map.put(1, "one"); @@ -108,12 +110,13 @@ public void testMixedKeys() } @Test - public void testMixedKeysBackwardsCompatiable() + public void testMixedKeysBackwardsCompatible() throws IOException { - ObjectMapper mapper = new ObjectMapper(new MessagePackFactory()) - .registerModule(new SimpleModule() - .addDeserializer(Map.class, new MessagePackMapDeserializer())); + ObjectMapper mapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule() + .addDeserializer(Map.class, new MessagePackMapDeserializer())) + .build(); Map map = new HashMap<>(); map.put(1, "one"); diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java index e899e4f4..c8083189 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java @@ -15,8 +15,8 @@ // package org.msgpack.jackson.dataformat; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.Test; +import tools.jackson.databind.ObjectMapper; +import org.junit.Test; import java.io.IOException; import java.nio.charset.Charset; @@ -24,8 +24,8 @@ import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.containsString; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertArrayEquals; import static org.hamcrest.MatcherAssert.assertThat; public class MessagePackDataformatForPojoTest @@ -45,7 +45,7 @@ public void testNormal() assertEquals(normalPojo.d, value.d, 0.000001f); assertArrayEquals(normalPojo.b, value.b); assertEquals(normalPojo.bi, value.bi); - assertEquals(normalPojo.suit, Suit.HEART); + assertEquals(normalPojo.suit, value.suit); assertEquals(normalPojo.sMultibyte, value.sMultibyte); } @@ -135,11 +135,12 @@ public void testChangingPropertyNames() public void testSerializationWithoutSchema() throws IOException { - ObjectMapper objectMapper = new ObjectMapper(factory); // to not affect shared objectMapper state - objectMapper.setAnnotationIntrospector(new JsonArrayFormat()); + ObjectMapper objectMapper = MessagePackMapper.builder(factory) + .annotationIntrospector(new JsonArrayFormat()) + .build(); byte[] bytes = objectMapper.writeValueAsBytes(complexPojo); - String scheme = new String(bytes, Charset.forName("UTF-8")); - assertThat(scheme, not(containsString("name"))); // validating schema doesn't contains keys, that's just array + String schema = new String(bytes, Charset.forName("UTF-8")); + assertThat(schema, not(containsString("name"))); ComplexPojo value = objectMapper.readValue(bytes, ComplexPojo.class); assertEquals("komamitsu", value.name); assertEquals(20, value.age); diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java index b9ef4cf3..8a7f1e52 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java @@ -18,7 +18,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import org.junit.After; import org.junit.Before; @@ -53,7 +53,7 @@ public class MessagePackDataformatTestBase public void setup() { factory = new MessagePackFactory(); - objectMapper = new ObjectMapper(factory); + objectMapper = new MessagePackMapper(factory); out = new ByteArrayOutputStream(); in = new ByteArrayInputStream(new byte[4096]); @@ -223,11 +223,9 @@ public static class IgnoringPropertiesPojo { int code; - // will not be written as JSON; nor assigned from JSON: @JsonIgnore public String internal; - // no annotation, public field is read/written normally public String external; @JsonIgnore @@ -236,7 +234,6 @@ public void setCode(int c) code = c; } - // note: will also be ignored because setter has annotation! public int getCode() { return code; @@ -247,15 +244,12 @@ public static class ChangingPropertyNamesPojo { String name; - // without annotation, we'd get "theName", but we want "name": @JsonProperty("name") public String getTheName() { return name; } - // note: it is enough to add annotation on just getter OR setter; - // so we can omit it here public void setTheName(String n) { name = n; diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java index 9e376541..25ef0fe2 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java @@ -15,27 +15,27 @@ // package org.msgpack.jackson.dataformat; -import com.fasterxml.jackson.core.JsonEncoding; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.AnnotationIntrospector; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; -import org.junit.jupiter.api.Test; +import tools.jackson.core.JsonEncoding; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.TSFBuilder; +import tools.jackson.core.TokenStreamFactory; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import org.junit.Test; import org.msgpack.core.MessagePack; import java.io.IOException; -import java.util.Collection; import java.util.HashMap; import java.util.Map; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.junit.Assert.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; public class MessagePackFactoryTest @@ -58,43 +58,12 @@ public void testCreateParser() assertEquals(MessagePackParser.class, parser.getClass()); } - private void assertCopy(boolean advancedConfig) + @Test + public void testCopyWithDefaultConfig() throws IOException { - // Build base ObjectMapper - ObjectMapper objectMapper; - if (advancedConfig) { - ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); - extTypeCustomDesers.addCustomDeser((byte) 42, - new ExtensionTypeCustomDeserializers.Deser() - { - @Override - public Object deserialize(byte[] data) - throws IOException - { - TinyPojo pojo = new TinyPojo(); - pojo.t = new String(data); - return pojo; - } - } - ); - - MessagePack.PackerConfig msgpackPackerConfig = new MessagePack.PackerConfig().withStr8FormatSupport(false); - - MessagePackFactory messagePackFactory = new MessagePackFactory(msgpackPackerConfig); - messagePackFactory.setExtTypeCustomDesers(extTypeCustomDesers); - - objectMapper = new ObjectMapper(messagePackFactory); - - objectMapper.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false); - objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false); - - objectMapper.setAnnotationIntrospector(new JsonArrayFormat()); - } - else { - MessagePackFactory messagePackFactory = new MessagePackFactory(); - objectMapper = new ObjectMapper(messagePackFactory); - } + MessagePackFactory messagePackFactory = new MessagePackFactory(); + ObjectMapper objectMapper = new MessagePackMapper(messagePackFactory); // Use the original ObjectMapper in advance { @@ -102,42 +71,16 @@ public Object deserialize(byte[] data) assertThat(objectMapper.readValue(bytes, Integer.class), is(1234)); } - // Copy the ObjectMapper - ObjectMapper copiedObjectMapper = objectMapper.copy(); - - // Assert the copied ObjectMapper - JsonFactory copiedFactory = copiedObjectMapper.getFactory(); + // Copy the factory + TokenStreamFactory copiedFactory = messagePackFactory.copy(); assertThat(copiedFactory, is(instanceOf(MessagePackFactory.class))); MessagePackFactory copiedMessagePackFactory = (MessagePackFactory) copiedFactory; - Collection annotationIntrospectors = - copiedObjectMapper.getSerializationConfig().getAnnotationIntrospector().allIntrospectors(); - assertThat(annotationIntrospectors.size(), is(1)); - - if (advancedConfig) { - assertThat(copiedMessagePackFactory.getPackerConfig().isStr8FormatSupport(), is(false)); - - assertThat(copiedMessagePackFactory.getExtTypeCustomDesers().getDeser((byte) 42), is(notNullValue())); - assertThat(copiedMessagePackFactory.getExtTypeCustomDesers().getDeser((byte) 43), is(nullValue())); - - assertThat(copiedMessagePackFactory.isEnabled(JsonGenerator.Feature.AUTO_CLOSE_TARGET), is(false)); - assertThat(copiedMessagePackFactory.isEnabled(JsonParser.Feature.AUTO_CLOSE_SOURCE), is(false)); + assertThat(copiedMessagePackFactory.getPackerConfig().isStr8FormatSupport(), is(true)); + assertThat(copiedMessagePackFactory.getExtTypeCustomDesers(), is(nullValue())); - assertThat(annotationIntrospectors.stream().findFirst().get(), is(instanceOf(JsonArrayFormat.class))); - } - else { - assertThat(copiedMessagePackFactory.getPackerConfig().isStr8FormatSupport(), is(true)); - - assertThat(copiedMessagePackFactory.getExtTypeCustomDesers(), is(nullValue())); - - assertThat(copiedMessagePackFactory.isEnabled(JsonGenerator.Feature.AUTO_CLOSE_TARGET), is(true)); - assertThat(copiedMessagePackFactory.isEnabled(JsonParser.Feature.AUTO_CLOSE_SOURCE), is(true)); - - assertThat(annotationIntrospectors.stream().findFirst().get(), - is(instanceOf(JacksonAnnotationIntrospector.class))); - } - - // Check the copied ObjectMapper works fine + // Check the copied factory works fine + ObjectMapper copiedObjectMapper = new MessagePackMapper(copiedMessagePackFactory); Map map = new HashMap<>(); map.put("one", 1); Map deserialized = copiedObjectMapper @@ -147,16 +90,109 @@ public Object deserialize(byte[] data) } @Test - public void copyWithDefaultConfig() + public void testRebuildWithDefaultConfig() throws IOException { - assertCopy(false); + MessagePackFactory messagePackFactory = new MessagePackFactory(); + TSFBuilder builder = messagePackFactory.rebuild(); + assertThat(builder, is(instanceOf(MessagePackFactoryBuilder.class))); + + MessagePackFactory rebuilt = (MessagePackFactory) builder.build(); + assertThat(rebuilt, is(not(sameInstance(messagePackFactory)))); + assertThat(rebuilt.getPackerConfig().isStr8FormatSupport(), is(true)); + assertThat(rebuilt.getExtTypeCustomDesers(), is(nullValue())); + + ObjectMapper rebuiltObjectMapper = new MessagePackMapper(rebuilt); + byte[] bytes = rebuiltObjectMapper.writeValueAsBytes(42); + assertThat(rebuiltObjectMapper.readValue(bytes, Integer.class), is(42)); } @Test - public void copyWithAdvancedConfig() + public void testRebuildWithAdvancedConfig() throws IOException { - assertCopy(true); + ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); + extTypeCustomDesers.addCustomDeser((byte) 42, + new ExtensionTypeCustomDeserializers.Deser() + { + @Override + public Object deserialize(byte[] data) + throws IOException + { + TinyPojo pojo = new TinyPojo(); + pojo.t = new String(data); + return pojo; + } + } + ); + MessagePack.PackerConfig packerConfig = new MessagePack.PackerConfig().withStr8FormatSupport(false); + MessagePackFactory messagePackFactory = new MessagePackFactory(packerConfig); + messagePackFactory.setExtTypeCustomDesers(extTypeCustomDesers); + + MessagePackFactory rebuilt = (MessagePackFactory) messagePackFactory.rebuild().build(); + assertThat(rebuilt, is(not(sameInstance(messagePackFactory)))); + assertThat(rebuilt.getPackerConfig().isStr8FormatSupport(), is(false)); + assertThat(rebuilt.getExtTypeCustomDesers().getDeser((byte) 42), is(notNullValue())); + assertThat(rebuilt.getExtTypeCustomDesers().getDeser((byte) 43), is(nullValue())); + } + + @Test + public void testSnapshotReturnsNewInstance() + { + MessagePackFactory messagePackFactory = new MessagePackFactory(); + TokenStreamFactory snapshot = messagePackFactory.snapshot(); + assertThat(snapshot, is(not(sameInstance(messagePackFactory)))); + assertThat(snapshot, is(instanceOf(MessagePackFactory.class))); + } + + @Test + public void testCopyWithAdvancedConfig() + throws IOException + { + ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); + extTypeCustomDesers.addCustomDeser((byte) 42, + new ExtensionTypeCustomDeserializers.Deser() + { + @Override + public Object deserialize(byte[] data) + throws IOException + { + TinyPojo pojo = new TinyPojo(); + pojo.t = new String(data); + return pojo; + } + } + ); + + MessagePack.PackerConfig msgpackPackerConfig = new MessagePack.PackerConfig().withStr8FormatSupport(false); + + MessagePackFactory messagePackFactory = new MessagePackFactory(msgpackPackerConfig); + messagePackFactory.setExtTypeCustomDesers(extTypeCustomDesers); + + ObjectMapper objectMapper = new MessagePackMapper(messagePackFactory); + + // Use the original ObjectMapper in advance + { + byte[] bytes = objectMapper.writeValueAsBytes(1234); + assertThat(objectMapper.readValue(bytes, Integer.class), is(1234)); + } + + // Copy the factory + TokenStreamFactory copiedFactory = messagePackFactory.copy(); + assertThat(copiedFactory, is(instanceOf(MessagePackFactory.class))); + MessagePackFactory copiedMessagePackFactory = (MessagePackFactory) copiedFactory; + + assertThat(copiedMessagePackFactory.getPackerConfig().isStr8FormatSupport(), is(false)); + assertThat(copiedMessagePackFactory.getExtTypeCustomDesers().getDeser((byte) 42), is(notNullValue())); + assertThat(copiedMessagePackFactory.getExtTypeCustomDesers().getDeser((byte) 43), is(nullValue())); + + // Check the copied factory works fine + ObjectMapper copiedObjectMapper = new MessagePackMapper(copiedMessagePackFactory); + Map map = new HashMap<>(); + map.put("one", 1); + Map deserialized = copiedObjectMapper + .readValue(objectMapper.writeValueAsBytes(map), new TypeReference>() {}); + assertThat(deserialized.size(), is(1)); + assertThat(deserialized.get("one"), is(1)); } } diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java index a13951b2..2d593846 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java @@ -16,15 +16,23 @@ package org.msgpack.jackson.dataformat; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonEncoding; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.module.SimpleModule; -import org.junit.jupiter.api.Test; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.core.JsonEncoding; +import tools.jackson.core.JacksonException; +import tools.jackson.core.ObjectReadContext; +import tools.jackson.core.ObjectWriteContext; +import tools.jackson.core.StreamWriteFeature; +import tools.jackson.core.TokenStreamContext; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.annotation.JsonSerialize; +import tools.jackson.databind.module.SimpleModule; +import org.junit.Test; import org.msgpack.core.ExtensionTypeHeader; import org.msgpack.core.MessagePack; import org.msgpack.core.MessageUnpacker; @@ -36,13 +44,13 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -53,13 +61,14 @@ import java.util.concurrent.TimeUnit; import static org.hamcrest.CoreMatchers.is; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; import static org.hamcrest.MatcherAssert.assertThat; public class MessagePackGeneratorTest @@ -253,7 +262,7 @@ public void testMessagePackGeneratorDirectly() MessagePackFactory messagePackFactory = new MessagePackFactory(); File tempFile = createTempFile(); - JsonGenerator generator = messagePackFactory.createGenerator(tempFile, JsonEncoding.UTF8); + JsonGenerator generator = messagePackFactory.createGenerator(ObjectWriteContext.empty(), tempFile, JsonEncoding.UTF8); assertTrue(generator instanceof MessagePackGenerator); generator.writeStartArray(); generator.writeNumber(0); @@ -280,7 +289,7 @@ public void testWritePrimitives() MessagePackFactory messagePackFactory = new MessagePackFactory(); File tempFile = createTempFile(); - JsonGenerator generator = messagePackFactory.createGenerator(tempFile, JsonEncoding.UTF8); + JsonGenerator generator = messagePackFactory.createGenerator(ObjectWriteContext.empty(), tempFile, JsonEncoding.UTF8); assertTrue(generator instanceof MessagePackGenerator); generator.writeNumber(0); generator.writeString("one"); @@ -304,7 +313,7 @@ public void testWritePrimitives() public void testBigDecimal() throws IOException { - ObjectMapper mapper = new ObjectMapper(new MessagePackFactory()); + ObjectMapper mapper = new MessagePackMapper(new MessagePackFactory()); { double d0 = 1.23456789; @@ -350,16 +359,38 @@ public void testBigDecimal() } } + @Test + public void testBigDecimalCompareTo() + throws IOException + { + ObjectMapper mapper = new MessagePackMapper(new MessagePackFactory()); + + // BigDecimal with trailing zeros is representable as double — must not throw + BigDecimal trailingZeros = new BigDecimal("1.50"); + byte[] bytes = mapper.writeValueAsBytes(trailingZeros); + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes); + assertEquals(1.5, unpacker.unpackDouble(), 0.0); + + // BigDecimal with precision beyond double range must throw + BigDecimal tooHighPrecision = new BigDecimal("1.00000000000000000000000000000000000001"); + try { + mapper.writeValueAsBytes(tooHighPrecision); + assertTrue(false); + } + catch (IllegalArgumentException e) { + assertTrue(true); + } + } + @Test public void testEnableFeatureAutoCloseTarget() throws IOException { OutputStream out = createTempFileOutputStream(); - MessagePackFactory messagePackFactory = new MessagePackFactory(); - ObjectMapper objectMapper = new ObjectMapper(messagePackFactory); + ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory()); List integers = Arrays.asList(1); objectMapper.writeValue(out, integers); - assertThrows(IOException.class, () -> { + assertThrows(JacksonException.class, () -> { objectMapper.writeValue(out, integers); }); } @@ -370,9 +401,9 @@ public void testDisableFeatureAutoCloseTarget() { File tempFile = createTempFile(); OutputStream out = new FileOutputStream(tempFile); - MessagePackFactory messagePackFactory = new MessagePackFactory(); - ObjectMapper objectMapper = new ObjectMapper(messagePackFactory); - objectMapper.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) + .build(); List integers = Arrays.asList(1); objectMapper.writeValue(out, integers); objectMapper.writeValue(out, integers); @@ -391,8 +422,9 @@ public void testWritePrimitiveObjectViaObjectMapper() { File tempFile = createTempFile(); try (OutputStream out = Files.newOutputStream(tempFile.toPath())) { - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - objectMapper.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) + .build(); objectMapper.writeValue(out, 1); objectMapper.writeValue(out, "two"); objectMapper.writeValue(out, 3.14); @@ -417,8 +449,9 @@ public void testInMultiThreads() int threadCount = 8; final int loopCount = 4000; ExecutorService executorService = Executors.newFixedThreadPool(threadCount); - final ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - objectMapper.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false); + final ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) + .build(); final List buffers = new ArrayList(threadCount); List> results = new ArrayList>(); @@ -437,7 +470,7 @@ public Exception call() } return null; } - catch (IOException e) { + catch (Exception e) { return e; } } @@ -467,14 +500,12 @@ public void testDisableStr8Support() { String str8LengthString = new String(new char[32]).replace("\0", "a"); - // Test that produced value having str8 format - ObjectMapper defaultMapper = new ObjectMapper(new MessagePackFactory()); + ObjectMapper defaultMapper = new MessagePackMapper(new MessagePackFactory()); byte[] resultWithStr8Format = defaultMapper.writeValueAsBytes(str8LengthString); assertEquals(resultWithStr8Format[0], MessagePack.Code.STR8); - // Test that produced value does not having str8 format MessagePack.PackerConfig config = new MessagePack.PackerConfig().withStr8FormatSupport(false); - ObjectMapper mapperWithConfig = new ObjectMapper(new MessagePackFactory(config)); + ObjectMapper mapperWithConfig = new MessagePackMapper(new MessagePackFactory(config)); byte[] resultWithoutStr8Format = mapperWithConfig.writeValueAsBytes(str8LengthString); assertNotEquals(resultWithoutStr8Format[0], MessagePack.Code.STR8); } @@ -658,7 +689,7 @@ public void setBigIntMap(Map bigIntMap) @Test @SuppressWarnings("unchecked") public void testNonStringKey() - throws IOException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException + throws Exception { for (Class clazz : Arrays.asList( @@ -671,11 +702,16 @@ public void testNonStringKey() mapHolder.getDoubleMap().put(Double.MIN_VALUE, "d"); mapHolder.getBigIntMap().put(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE), "bi"); - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + ObjectMapper objectMapper; if (mapHolder instanceof NonStringKeyMapHolderWithoutAnnotation) { SimpleModule mod = new SimpleModule("test"); mod.addKeySerializer(Object.class, new MessagePackKeySerializer()); - objectMapper.registerModule(mod); + objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(mod) + .build(); + } + else { + objectMapper = new MessagePackMapper(new MessagePackFactory()); } byte[] bytes = objectMapper.writeValueAsBytes(mapHolder); @@ -720,10 +756,11 @@ public void testComplexTypeKey() pojo.t = "foo"; map.put(pojo, 42); - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); SimpleModule mod = new SimpleModule("test"); mod.addKeySerializer(TinyPojo.class, new MessagePackKeySerializer()); - objectMapper.registerModule(mod); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(mod) + .build(); byte[] bytes = objectMapper.writeValueAsBytes(map); MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes); @@ -743,11 +780,12 @@ public void testComplexTypeKeyWithV06Format() pojo.t = "foo"; map.put(pojo, 42); - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - objectMapper.setAnnotationIntrospector(new JsonArrayFormat()); SimpleModule mod = new SimpleModule("test"); mod.addKeySerializer(TinyPojo.class, new MessagePackKeySerializer()); - objectMapper.registerModule(mod); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .annotationIntrospector(new JsonArrayFormat()) + .addModule(mod) + .build(); byte[] bytes = objectMapper.writeValueAsBytes(map); MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes); @@ -757,14 +795,12 @@ public void testComplexTypeKeyWithV06Format() assertThat(unpacker.unpackInt(), is(42)); } - // Test serializers that store a string as a number - public static class IntegerSerializerStoringAsString - extends JsonSerializer + extends ValueSerializer { @Override - public void serialize(Integer value, JsonGenerator gen, SerializerProvider serializers) - throws IOException, JsonProcessingException + public void serialize(Integer value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException { gen.writeNumber(String.valueOf(value)); } @@ -774,9 +810,9 @@ public void serialize(Integer value, JsonGenerator gen, SerializerProvider seria public void serializeStringAsInteger() throws IOException { - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - objectMapper.registerModule( - new SimpleModule().addSerializer(Integer.class, new IntegerSerializerStoringAsString())); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule().addSerializer(Integer.class, new IntegerSerializerStoringAsString())) + .build(); assertThat( MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(Integer.MAX_VALUE)).unpackInt(), @@ -784,11 +820,11 @@ public void serializeStringAsInteger() } public static class LongSerializerStoringAsString - extends JsonSerializer + extends ValueSerializer { @Override - public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) - throws IOException, JsonProcessingException + public void serialize(Long value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException { gen.writeNumber(String.valueOf(value)); } @@ -798,9 +834,9 @@ public void serialize(Long value, JsonGenerator gen, SerializerProvider serializ public void serializeStringAsLong() throws IOException { - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - objectMapper.registerModule( - new SimpleModule().addSerializer(Long.class, new LongSerializerStoringAsString())); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule().addSerializer(Long.class, new LongSerializerStoringAsString())) + .build(); assertThat( MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(Long.MIN_VALUE)).unpackLong(), @@ -808,11 +844,11 @@ public void serializeStringAsLong() } public static class FloatSerializerStoringAsString - extends JsonSerializer + extends ValueSerializer { @Override - public void serialize(Float value, JsonGenerator gen, SerializerProvider serializers) - throws IOException, JsonProcessingException + public void serialize(Float value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException { gen.writeNumber(String.valueOf(value)); } @@ -822,9 +858,9 @@ public void serialize(Float value, JsonGenerator gen, SerializerProvider seriali public void serializeStringAsFloat() throws IOException { - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - objectMapper.registerModule( - new SimpleModule().addSerializer(Float.class, new FloatSerializerStoringAsString())); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule().addSerializer(Float.class, new FloatSerializerStoringAsString())) + .build(); assertThat( MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(Float.MAX_VALUE)).unpackFloat(), @@ -832,11 +868,11 @@ public void serializeStringAsFloat() } public static class DoubleSerializerStoringAsString - extends JsonSerializer + extends ValueSerializer { @Override - public void serialize(Double value, JsonGenerator gen, SerializerProvider serializers) - throws IOException, JsonProcessingException + public void serialize(Double value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException { gen.writeNumber(String.valueOf(value)); } @@ -846,9 +882,9 @@ public void serialize(Double value, JsonGenerator gen, SerializerProvider serial public void serializeStringAsDouble() throws IOException { - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - objectMapper.registerModule( - new SimpleModule().addSerializer(Double.class, new DoubleSerializerStoringAsString())); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule().addSerializer(Double.class, new DoubleSerializerStoringAsString())) + .build(); assertThat( MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(Double.MIN_VALUE)).unpackDouble(), @@ -856,11 +892,11 @@ public void serializeStringAsDouble() } public static class BigDecimalSerializerStoringAsString - extends JsonSerializer + extends ValueSerializer { @Override - public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider serializers) - throws IOException, JsonProcessingException + public void serialize(BigDecimal value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException { gen.writeNumber(String.valueOf(value)); } @@ -870,22 +906,22 @@ public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider se public void serializeStringAsBigDecimal() throws IOException { - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - objectMapper.registerModule( - new SimpleModule().addSerializer(BigDecimal.class, new BigDecimalSerializerStoringAsString())); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule().addSerializer(BigDecimal.class, new BigDecimalSerializerStoringAsString())) + .build(); BigDecimal bd = BigDecimal.valueOf(Long.MAX_VALUE).add(BigDecimal.ONE); assertThat( - MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(bd)).unpackDouble(), - is(bd.doubleValue())); + MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(bd)).unpackBigInteger(), + is(bd.toBigIntegerExact())); } public static class BigIntegerSerializerStoringAsString - extends JsonSerializer + extends ValueSerializer { @Override - public void serialize(BigInteger value, JsonGenerator gen, SerializerProvider serializers) - throws IOException, JsonProcessingException + public void serialize(BigInteger value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException { gen.writeNumber(String.valueOf(value)); } @@ -895,22 +931,21 @@ public void serialize(BigInteger value, JsonGenerator gen, SerializerProvider se public void serializeStringAsBigInteger() throws IOException { - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - objectMapper.registerModule( - new SimpleModule().addSerializer(BigInteger.class, new BigIntegerSerializerStoringAsString())); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule().addSerializer(BigInteger.class, new BigIntegerSerializerStoringAsString())) + .build(); BigInteger bi = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE); assertThat( - MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(bi)).unpackDouble(), - is(bi.doubleValue())); + MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(bi)).unpackBigInteger(), + is(bi)); } @Test public void testNestedSerialization() throws Exception { - // The purpose of this test is to confirm if MessagePackFactory.setReuseResourceInGenerator(false) - // works as a workaround for https://github.com/msgpack/msgpack-java/issues/508 - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory().setReuseResourceInGenerator(false)); + ObjectMapper objectMapper = new MessagePackMapper( + new MessagePackFactory().setReuseResourceInGenerator(false)); OuterClass outerClass = objectMapper.readValue( objectMapper.writeValueAsBytes(new OuterClass("Foo")), OuterClass.class); @@ -927,10 +962,8 @@ public OuterClass(@JsonProperty("name") String name) } public String getName() - throws IOException { - // Serialize nested class object - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory()); InnerClass innerClass = objectMapper.readValue( objectMapper.writeValueAsBytes(new InnerClass("Bar")), InnerClass.class); @@ -954,4 +987,460 @@ public String getName() return name; } } + + @Test + public void testIsClosedAfterClose() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + assertFalse(generator.isClosed()); + generator.writeStartArray(); + generator.writeEndArray(); + generator.close(); + assertTrue(generator.isClosed()); + } + + @Test + public void testGeneratorReusableAfterRootContainerClose() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeNumber(1); + generator.writeEndArray(); + generator.flush(); + + // Write a second root value; currentState must have reset to IN_ROOT + generator.writeStartObject(); + generator.writeName("k"); + generator.writeNumber(2); + generator.writeEndObject(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + assertEquals(1, unpacker.unpackArrayHeader()); + assertEquals(1, unpacker.unpackInt()); + assertEquals(1, unpacker.unpackMapHeader()); + assertEquals("k", unpacker.unpackString()); + assertEquals(2, unpacker.unpackInt()); + } + + @Test + public void testWriteRawStringWithOffset() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeRaw("XXhelloXX", 2, 5); // "hello" + generator.writeEndArray(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + unpacker.unpackArrayHeader(); + assertEquals("hello", unpacker.unpackString()); + } + + @Test + public void testWriteStringCharArrayWithOffset() + throws IOException + { + // Padding chars before/after the actual content to test non-zero offset + char[] buf = new char[] {'X', 'X', 'h', 'e', 'l', 'l', 'o', 'X'}; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeString(buf, 2, 5); // "hello" + generator.writeEndArray(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + unpacker.unpackArrayHeader(); + assertEquals("hello", unpacker.unpackString()); + } + + @Test + public void testWriteStringCharArrayWithOffsetNonAscii() + throws IOException + { + // Non-ASCII to exercise the non-fast-path in getBytesIfAscii + char[] buf = new char[] {'X', '三', '四', '五', 'X'}; // 三四五 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeString(buf, 1, 3); + generator.writeEndArray(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + unpacker.unpackArrayHeader(); + assertEquals("三四五", unpacker.unpackString()); + } + + @Test + public void testWriteUTF8StringWithOffset() + throws IOException + { + // Padding bytes before/after to test non-zero offset in writeUTF8String + byte[] buf = new byte[] {'X', 'X', 'h', 'i', 'X'}; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeUTF8String(buf, 2, 2); // "hi" + generator.writeEndArray(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + unpacker.unpackArrayHeader(); + assertEquals("hi", unpacker.unpackString()); + } + + @Test + public void testWriteBinaryWithOffset() + throws IOException + { + byte[] data = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04}; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeBinary(data, 1, 3); // bytes 0x01, 0x02, 0x03 + generator.writeEndArray(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + unpacker.unpackArrayHeader(); + byte[] result = unpacker.readPayload(unpacker.unpackBinaryHeader()); + assertArrayEquals(new byte[] {0x01, 0x02, 0x03}, result); + } + + @Test + public void testWriteBinaryByteBufferWithOffset() + throws IOException + { + byte[] data = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04}; + ByteBuffer bb = ByteBuffer.wrap(data, 1, 3); // position=1, limit=4, remaining=3 + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + ObjectMapper mapper = new MessagePackMapper(factory); + mapper.writeValue(generator, bb); + generator.writeEndArray(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + unpacker.unpackArrayHeader(); + byte[] result = unpacker.readPayload(unpacker.unpackBinaryHeader()); + assertArrayEquals(new byte[] {0x01, 0x02, 0x03}, result); + } + + @Test + public void testStreamWriteContext() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + + TokenStreamContext ctx = generator.streamWriteContext(); + assertNotEquals(null, ctx); + assertTrue(ctx.inRoot()); + + generator.writeStartArray(); + ctx = generator.streamWriteContext(); + assertTrue(ctx.inArray()); + + generator.writeStartObject(); + ctx = generator.streamWriteContext(); + assertTrue(ctx.inObject()); + + generator.writeName("k"); + assertEquals("k", ctx.currentName()); + + generator.writeNumber(1); + generator.writeEndObject(); + ctx = generator.streamWriteContext(); + assertTrue(ctx.inArray()); + + generator.writeEndArray(); + ctx = generator.streamWriteContext(); + assertTrue(ctx.inRoot()); + + generator.close(); + } + + @Test + public void testCurrentValue() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + + Object pojo = new Object(); + generator.writeStartObject(pojo); + assertEquals(pojo, generator.currentValue()); + generator.writeName("k"); + generator.writeNumber(1); + generator.writeEndObject(); + generator.close(); + } + + @Test + public void testVersion() + { + assertNotEquals(null, factory.version()); + assertEquals("org.msgpack", factory.version().getGroupId()); + assertEquals("jackson-dataformat-msgpack-jackson3", factory.version().getArtifactId()); + } + + @Test + public void testSerializedStringMethods() throws IOException + { + MessagePackSerializedString s = new MessagePackSerializedString("hello"); + + byte[] utf8Target = new byte[10]; + int written = s.appendUnquotedUTF8(utf8Target, 2); + assertEquals(5, written); + assertArrayEquals(new byte[] {'h', 'e', 'l', 'l', 'o'}, Arrays.copyOfRange(utf8Target, 2, 7)); + + char[] charTarget = new char[10]; + written = s.appendUnquoted(charTarget, 3); + assertEquals(5, written); + assertEquals("hello", new String(charTarget, 3, 5)); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + written = s.writeUnquotedUTF8(baos); + assertEquals(5, written); + assertArrayEquals("hello".getBytes(java.nio.charset.StandardCharsets.UTF_8), baos.toByteArray()); + } + + // Regression: addValueNode must call writeContext.writeValue() so that the Jackson write + // context resets _gotPropertyId after each value. Without it, the second writeName() in the + // same object finds _gotPropertyId still set from the first writeName() and returns false + // without updating currentName(), leaving streamWriteContext().currentName() stale. + @Test + public void testWriteContextCurrentNameIsUpdatedForEveryProperty() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartObject(); + + generator.writeName("alpha"); + generator.writeNumber(1); + + generator.writeName("beta"); + assertEquals("beta", generator.streamWriteContext().currentName()); + generator.writeNumber(2); + + generator.writeEndObject(); + generator.close(); + } + + // Regression: writePropertyId() must call writeContext.writeName() when supportIntegerKeys + // is true. Without it, streamWriteContext() never learns a name was written, so currentName() + // returns null and any downstream code relying on context state (e.g. duplicate-name + // detection, error messages) sees wrong state. + @Test + public void testWritePropertyIdUpdatesWriteContext() + throws IOException + { + MessagePackFactory intKeyFactory = new MessagePackFactory().setSupportIntegerKeys(true); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = intKeyFactory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartObject(); + generator.writePropertyId(42L); + assertEquals("42", generator.streamWriteContext().currentName()); + generator.writeString("value"); + generator.writeEndObject(); + generator.close(); + } + + @Test + public void testNullSerializedStringKeyDoesNotThrowNpe() + throws IOException + { + // writeName(MessagePackSerializedString(null)) calls getValue() → null.toString() → NPE. + // A null key should be serialized as msgpack nil, not crash. + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = new MessagePackFactory().createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartObject(); + generator.writeName(new MessagePackSerializedString(null)); + generator.writeNumber(42); + generator.writeEndObject(); + generator.close(); + + // Verify the null key round-trips as PROPERTY_NAME with null current name + try (JsonParser parser = + new MessagePackFactory().createParser(ObjectReadContext.empty(), baos.toByteArray())) { + assertEquals(JsonToken.START_OBJECT, parser.nextToken()); + assertEquals(JsonToken.PROPERTY_NAME, parser.nextToken()); + assertNull(parser.currentName()); + assertEquals(JsonToken.VALUE_NUMBER_INT, parser.nextToken()); + assertEquals(42, parser.getIntValue()); + assertEquals(JsonToken.END_OBJECT, parser.nextToken()); + } + } + + @Test + public void testFlushMidWriteOnSecondRootContainerDoesNotCorruptState() + throws IOException + { + // After the first root container closes, isElementsClosed=true. + // Opening a second root container does not reset this flag, so a + // flush() call while the second container is still open will pack + // the incomplete node tree and wipe nodes[], corrupting subsequent writes. + MessagePackFactory factory = new MessagePackFactory(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + + generator.writeStartArray(); + generator.writeNumber(1); + generator.writeEndArray(); + + generator.writeStartArray(); // second root — isElementsClosed still true + generator.flush(); // must NOT pack the incomplete second array + generator.writeNumber(2); + generator.writeEndArray(); + + generator.close(); + + ObjectMapper mapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .build(); + try (JsonParser parser = + new MessagePackFactory().createParser(ObjectReadContext.empty(), baos.toByteArray())) { + List first = mapper.readValue(parser, new TypeReference>() {}); + assertEquals(Collections.singletonList(1), first); + List second = mapper.readValue(parser, new TypeReference>() {}); + assertEquals(Collections.singletonList(2), second); + } + } + + @Test + public void testRootScalarAfterClosedRootContainerPreservesOrder() + throws IOException + { + // A root scalar written after a closed root container must be emitted AFTER + // the container, not before. Without a fix, addValueNode() packs the scalar + // immediately (default branch) while the container stays buffered, reversing order. + MessagePackFactory factory = new MessagePackFactory(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeNumber(1); + generator.writeEndArray(); + generator.writeNumber(2); // root scalar — must come AFTER the array + generator.close(); + + ObjectMapper mapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .build(); + try (JsonParser parser = + new MessagePackFactory().createParser(ObjectReadContext.empty(), baos.toByteArray())) { + List list = mapper.readValue(parser, new TypeReference>() {}); + assertEquals(Collections.singletonList(1), list); + int scalar = mapper.readValue(parser, Integer.class); + assertEquals(2, scalar); + } + } + + // Bug: writeNumber(String) is missing "return this" after addValueNode(d) in the + // NaN/Infinity fallback branch. addValueNode() advances the write-context state + // and (for root-level scalars) writes bytes to the output stream before the + // method falls through to "throw new NumberFormatException(encodedValue)". + @Test + public void testWriteNumberStringNanAndInfinity() throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + // Avoid try-with-resources: if writeNumber throws, the implicit close + // flushes a half-written node list and masks the original failure. + JsonGenerator gen = factory.createGenerator(ObjectWriteContext.empty(), baos); + gen.writeStartArray(); + gen.writeNumber("NaN"); // Bug: NFE thrown after state mutation + gen.writeNumber("Infinity"); + gen.writeNumber("-Infinity"); + gen.writeEndArray(); + gen.close(); + + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), baos.toByteArray())) { + assertEquals(JsonToken.START_ARRAY, p.nextToken()); + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + assertTrue(Double.isNaN(p.getDoubleValue())); + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + assertEquals(Double.POSITIVE_INFINITY, p.getDoubleValue(), 0); + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + assertEquals(Double.NEGATIVE_INFINITY, p.getDoubleValue(), 0); + assertEquals(JsonToken.END_ARRAY, p.nextToken()); + } + } + + // Bug: same DupDetector NPE as the read-path bug, on the write side. + // writeName(SerializableString) calls writeContext.writeName(name.getValue()) + // where MessagePackSerializedString(null).getValue() == null. With + // STRICT_DUPLICATE_DETECTION enabled and a prior non-null key already seen, + // DupDetector.isDup(null) reaches name.equals(_firstName) → NPE. + @Test + public void testNullKeyWithWriteDupDetectionDoesNotNPE() throws IOException + { + MessagePackFactory f = new MessagePackFactoryBuilder() + .enable(StreamWriteFeature.STRICT_DUPLICATE_DETECTION) + .build(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (JsonGenerator gen = f.createGenerator(ObjectWriteContext.empty(), baos)) { + gen.writeStartObject(); + gen.writeName("foo"); + gen.writeNumber(1); + // MessagePackSerializedString(null).getValue() == null + gen.writeName(new MessagePackSerializedString(null)); // Bug: NPE here + gen.writeNumber(2); + gen.writeEndObject(); + } + } + + // Bug: MessagePackSerializedString.charLength() calls getValue().length() + // unconditionally; getValue() returns null when value is null → NPE. + @Test + public void testSerializedStringNullValueCharLengthDoesNotNPE() + { + MessagePackSerializedString s = new MessagePackSerializedString(null); + // Bug: null.length() → NullPointerException + assertEquals(0, s.charLength()); + } + + @Test + public void testMultipleRootContainersWithoutFlush() + throws IOException + { + // Writing two consecutive root-level containers on the same generator without + // an intervening flush() must not throw IndexOutOfBoundsException. + // The second root container sits at node index 1, so the root check + // "currentParentElementIndex == 0" incorrectly falls through to the + // nested-container path and calls nodes.get(-1). + MessagePackFactory factory = new MessagePackFactory(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeNumber(1); + generator.writeEndArray(); + // second root container — no flush() in between + generator.writeStartObject(); + generator.writeStringProperty("key", "value"); + generator.writeEndObject(); + generator.close(); + + // Verify both values were written correctly by reading from a shared parser + ObjectMapper mapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .build(); + try (JsonParser parser = + new MessagePackFactory().createParser(ObjectReadContext.empty(), baos.toByteArray())) { + List list = mapper.readValue(parser, new TypeReference>() {}); + assertEquals(Collections.singletonList(1), list); + Map map = mapper.readValue(parser, new TypeReference>() {}); + assertEquals(Collections.singletonMap("key", "value"), map); + } + } } diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java index d14f97f2..776a1109 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java @@ -15,15 +15,15 @@ // package org.msgpack.jackson.dataformat; -import com.fasterxml.jackson.core.JsonProcessingException; -import org.junit.jupiter.api.Test; +import tools.jackson.core.JacksonException; +import org.junit.Test; import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; public class MessagePackMapperTest { @@ -37,7 +37,7 @@ static class PojoWithBigDecimal public BigDecimal value; } - private void shouldFailToHandleBigInteger(MessagePackMapper messagePackMapper) throws JsonProcessingException + private void shouldFailToHandleBigInteger(MessagePackMapper messagePackMapper) throws JacksonException { PojoWithBigInteger obj = new PojoWithBigInteger(); obj.value = BigInteger.valueOf(Long.MAX_VALUE).multiply(BigInteger.valueOf(10)); @@ -62,7 +62,7 @@ private void shouldSuccessToHandleBigInteger(MessagePackMapper messagePackMapper assertEquals(obj.value, deserialized.value); } - private void shouldFailToHandleBigDecimal(MessagePackMapper messagePackMapper) throws JsonProcessingException + private void shouldFailToHandleBigDecimal(MessagePackMapper messagePackMapper) throws JacksonException { PojoWithBigDecimal obj = new PojoWithBigDecimal(); obj.value = new BigDecimal("1234567890.98765432100"); @@ -91,20 +91,26 @@ private void shouldSuccessToHandleBigDecimal(MessagePackMapper messagePackMapper public void handleBigIntegerAsString() throws IOException { shouldFailToHandleBigInteger(new MessagePackMapper()); - shouldSuccessToHandleBigInteger(new MessagePackMapper().handleBigIntegerAsString()); + shouldSuccessToHandleBigInteger(MessagePackMapper.builder() + .handleBigIntegerAsString() + .build()); } @Test public void handleBigDecimalAsString() throws IOException { shouldFailToHandleBigDecimal(new MessagePackMapper()); - shouldSuccessToHandleBigDecimal(new MessagePackMapper().handleBigDecimalAsString()); + shouldSuccessToHandleBigDecimal(MessagePackMapper.builder() + .handleBigDecimalAsString() + .build()); } @Test public void handleBigIntegerAndBigDecimalAsString() throws IOException { - MessagePackMapper messagePackMapper = new MessagePackMapper().handleBigIntegerAndBigDecimalAsString(); + MessagePackMapper messagePackMapper = MessagePackMapper.builder() + .handleBigIntegerAndBigDecimalAsString() + .build(); shouldSuccessToHandleBigInteger(messagePackMapper); shouldSuccessToHandleBigDecimal(messagePackMapper); } diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java index 256c4332..7b0ceee6 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java @@ -15,19 +15,20 @@ // package org.msgpack.jackson.dataformat; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.core.io.JsonEOFException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.KeyDeserializer; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.module.SimpleModule; -import org.junit.jupiter.api.Test; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonToken; +import tools.jackson.core.ObjectReadContext; +import tools.jackson.core.StreamReadFeature; +import tools.jackson.core.exc.UnexpectedEndOfInputException; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.KeyDeserializer; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.deser.std.StdDeserializer; +import tools.jackson.databind.module.SimpleModule; +import org.junit.Test; import org.msgpack.core.MessagePack; import org.msgpack.core.MessagePacker; import org.msgpack.value.ExtensionValue; @@ -36,6 +37,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.InputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -52,11 +54,12 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; public class MessagePackParserTest extends MessagePackDataformatTestBase @@ -323,7 +326,7 @@ public void testMessagePackParserDirectly() assertEquals(1, parser.currentLocation().getColumnNr()); jsonToken = parser.nextToken(); - assertEquals(JsonToken.FIELD_NAME, jsonToken); + assertEquals(JsonToken.PROPERTY_NAME, jsonToken); assertEquals("zero", parser.currentName()); assertEquals(1, parser.currentTokenLocation().getColumnNr()); assertEquals(6, parser.currentLocation().getColumnNr()); @@ -335,12 +338,10 @@ public void testMessagePackParserDirectly() assertEquals(7, parser.currentLocation().getColumnNr()); jsonToken = parser.nextToken(); - assertEquals(JsonToken.FIELD_NAME, jsonToken); + assertEquals(JsonToken.PROPERTY_NAME, jsonToken); assertEquals("one", parser.currentName()); assertEquals(7, parser.currentTokenLocation().getColumnNr()); assertEquals(11, parser.currentLocation().getColumnNr()); - parser.overrideCurrentName("two"); - assertEquals("two", parser.currentName()); jsonToken = parser.nextToken(); assertEquals(JsonToken.VALUE_NUMBER_FLOAT, jsonToken); @@ -380,24 +381,24 @@ public void testReadPrimitives() JsonParser parser = factory.createParser(new FileInputStream(tempFile)); assertEquals(JsonToken.VALUE_STRING, parser.nextToken()); - assertEquals("foo", parser.getText()); + assertEquals("foo", parser.getString()); assertEquals(JsonToken.VALUE_NUMBER_FLOAT, parser.nextToken()); assertEquals(3.14, parser.getDoubleValue(), 0.0001); - assertEquals("3.14", parser.getText()); + assertEquals("3.14", parser.getString()); assertEquals(JsonToken.VALUE_NUMBER_INT, parser.nextToken()); assertEquals(Integer.MIN_VALUE, parser.getIntValue()); assertEquals(Integer.MIN_VALUE, parser.getLongValue()); - assertEquals("-2147483648", parser.getText()); + assertEquals("-2147483648", parser.getString()); assertEquals(JsonToken.VALUE_NUMBER_INT, parser.nextToken()); assertEquals(Long.MAX_VALUE, parser.getLongValue()); - assertEquals("9223372036854775807", parser.getText()); + assertEquals("9223372036854775807", parser.getString()); assertEquals(JsonToken.VALUE_NUMBER_INT, parser.nextToken()); assertEquals(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE), parser.getBigIntegerValue()); - assertEquals("9223372036854775808", parser.getText()); + assertEquals("9223372036854775808", parser.getString()); assertEquals(JsonToken.VALUE_EMBEDDED_OBJECT, parser.nextToken()); assertEquals(bytes.length, parser.getBinaryValue().length); @@ -421,8 +422,9 @@ public void testBigDecimal() packer.packDouble(Double.MIN_NORMAL); packer.flush(); - ObjectMapper mapper = new ObjectMapper(new MessagePackFactory()); - mapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true); + ObjectMapper mapper = MessagePackMapper.builder(new MessagePackFactory()) + .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS) + .build(); List objects = mapper.readValue(out.toByteArray(), new TypeReference>() {}); assertEquals(5, objects.size()); int idx = 0; @@ -458,9 +460,11 @@ public void testEnableFeatureAutoCloseSource() File tempFile = createTestFile(); MessagePackFactory factory = new MessagePackFactory(); FileInputStream in = new FileInputStream(tempFile); - ObjectMapper objectMapper = new ObjectMapper(factory); + ObjectMapper objectMapper = MessagePackMapper.builder(factory) + .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .build(); objectMapper.readValue(in, new TypeReference>() {}); - assertThrows(IOException.class, () -> { + assertThrows(JacksonException.class, () -> { objectMapper.readValue(in, new TypeReference>() {}); }); } @@ -471,8 +475,10 @@ public void testDisableFeatureAutoCloseSource() { File tempFile = createTestFile(); FileInputStream in = new FileInputStream(tempFile); - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(tools.jackson.core.StreamReadFeature.AUTO_CLOSE_SOURCE) + .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .build(); objectMapper.readValue(in, new TypeReference>() {}); objectMapper.readValue(in, new TypeReference>() {}); } @@ -483,7 +489,7 @@ public void testParseBigDecimal() { ArrayList list = new ArrayList(); list.add(new BigDecimal(Long.MAX_VALUE)); - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory()); byte[] bytes = objectMapper.writeValueAsBytes(list); ArrayList result = objectMapper.readValue( @@ -508,8 +514,10 @@ public void testReadPrimitiveObjectViaObjectMapper() packer.close(); FileInputStream in = new FileInputStream(tempFile); - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(tools.jackson.core.StreamReadFeature.AUTO_CLOSE_SOURCE) + .disable(tools.jackson.databind.DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .build(); assertEquals("foo", objectMapper.readValue(in, new TypeReference() {})); long l = objectMapper.readValue(in, new TypeReference() {}); assertEquals(Long.MAX_VALUE, l); @@ -538,7 +546,7 @@ public void testBinaryKey() packer.packLong(42); packer.close(); - ObjectMapper mapper = new ObjectMapper(new MessagePackFactory()); + ObjectMapper mapper = new MessagePackMapper(new MessagePackFactory()); Map object = mapper.readValue(new FileInputStream(tempFile), new TypeReference>() {}); assertEquals(2, object.size()); assertEquals(3.14, object.get("foo")); @@ -560,7 +568,7 @@ public void testBinaryKeyInNestedObject() packer.packInt(1); packer.close(); - ObjectMapper mapper = new ObjectMapper(new MessagePackFactory()); + ObjectMapper mapper = new MessagePackMapper(new MessagePackFactory()); List objects = mapper.readValue(out.toByteArray(), new TypeReference>() {}); assertEquals(2, objects.size()); @SuppressWarnings(value = "unchecked") @@ -582,18 +590,18 @@ public void testByteArrayKey() messagePacker.packBinaryHeader(1).writePayload(k1).packInt(11); messagePacker.close(); - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - SimpleModule module = new SimpleModule(); - module.addKeyDeserializer(byte[].class, new KeyDeserializer() - { - @Override - public Object deserializeKey(String key, DeserializationContext ctxt) - throws IOException, JsonProcessingException - { - return key.getBytes(); - } - }); - objectMapper.registerModule(module); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule() + .addKeyDeserializer(byte[].class, new KeyDeserializer() + { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) + throws JacksonException + { + return key.getBytes(); + } + })) + .build(); Map map = objectMapper.readValue( out.toByteArray(), new TypeReference>() {}); @@ -619,18 +627,18 @@ public void testIntegerKey() } messagePacker.close(); - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - SimpleModule module = new SimpleModule(); - module.addKeyDeserializer(Integer.class, new KeyDeserializer() - { - @Override - public Object deserializeKey(String key, DeserializationContext ctxt) - throws IOException, JsonProcessingException - { - return Integer.valueOf(key); - } - }); - objectMapper.registerModule(module); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule() + .addKeyDeserializer(Integer.class, new KeyDeserializer() + { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) + throws JacksonException + { + return Integer.valueOf(key); + } + })) + .build(); Map map = objectMapper.readValue( out.toByteArray(), new TypeReference>() {}); @@ -650,18 +658,18 @@ public void testFloatKey() } messagePacker.close(); - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - SimpleModule module = new SimpleModule(); - module.addKeyDeserializer(Float.class, new KeyDeserializer() - { - @Override - public Object deserializeKey(String key, DeserializationContext ctxt) - throws IOException, JsonProcessingException - { - return Float.valueOf(key); - } - }); - objectMapper.registerModule(module); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule() + .addKeyDeserializer(Float.class, new KeyDeserializer() + { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) + throws JacksonException + { + return Float.valueOf(key); + } + })) + .build(); Map map = objectMapper.readValue( out.toByteArray(), new TypeReference>() {}); @@ -680,18 +688,18 @@ public void testBooleanKey() messagePacker.packBoolean(false).packInt(11); messagePacker.close(); - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - SimpleModule module = new SimpleModule(); - module.addKeyDeserializer(Boolean.class, new KeyDeserializer() - { - @Override - public Object deserializeKey(String key, DeserializationContext ctxt) - throws IOException, JsonProcessingException - { - return Boolean.valueOf(key); - } - }); - objectMapper.registerModule(module); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule() + .addKeyDeserializer(Boolean.class, new KeyDeserializer() + { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) + throws JacksonException + { + return Boolean.valueOf(key); + } + })) + .build(); Map map = objectMapper.readValue( out.toByteArray(), new TypeReference>() {}); @@ -700,6 +708,26 @@ public Object deserializeKey(String key, DeserializationContext ctxt) assertEquals((Integer) 11, map.get(false)); } + @Test + public void testNilKey() + throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + MessagePacker packer = MessagePack.newDefaultPacker(out).packMapHeader(1); + packer.packNil(); + packer.packInt(42); + packer.close(); + + JsonParser parser = new MessagePackMapper().createParser(out.toByteArray()); + assertEquals(JsonToken.START_OBJECT, parser.nextToken()); + assertEquals(JsonToken.PROPERTY_NAME, parser.nextToken()); + assertNull(parser.currentName()); + assertEquals(JsonToken.VALUE_NUMBER_INT, parser.nextToken()); + assertEquals(42, parser.getIntValue()); + assertEquals(JsonToken.END_OBJECT, parser.nextToken()); + parser.close(); + } + @Test public void extensionTypeCustomDeserializers() throws IOException @@ -732,7 +760,7 @@ public Object deserialize(byte[] data) } ); ObjectMapper objectMapper = - new ObjectMapper(new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)); + new MessagePackMapper(new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)); List values = objectMapper.readValue(new ByteArrayInputStream(out.toByteArray()), new TypeReference>() {}); assertThat(values.size(), is(3)); @@ -787,7 +815,6 @@ public int hashCode() @Override public String toString() { - // This key format is used when serialized as map key return String.format("%d-%d-%d", first, second, third); } @@ -801,18 +828,18 @@ protected Deserializer() @Override public TripleBytesPojo deserialize(JsonParser p, DeserializationContext ctxt) - throws IOException, JsonProcessingException + throws JacksonException { return TripleBytesPojo.deserialize(p.getBinaryValue()); } } - static class KeyDeserializer - extends com.fasterxml.jackson.databind.KeyDeserializer + static class TripleBytesKeyDeserializer + extends KeyDeserializer { @Override public Object deserializeKey(String key, DeserializationContext ctxt) - throws IOException + throws JacksonException { String[] values = key.split("-"); return new TripleBytesPojo( @@ -852,10 +879,11 @@ public Object deserialize(byte[] value) SimpleModule module = new SimpleModule(); module.addDeserializer(TripleBytesPojo.class, new TripleBytesPojo.Deserializer()); - module.addKeyDeserializer(TripleBytesPojo.class, new TripleBytesPojo.KeyDeserializer()); - ObjectMapper objectMapper = new ObjectMapper( + module.addKeyDeserializer(TripleBytesPojo.class, new TripleBytesPojo.TripleBytesKeyDeserializer()); + ObjectMapper objectMapper = MessagePackMapper.builder( new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) - .registerModule(module); + .addModule(module) + .build(); // Prepare serialized data Map originalMap = new HashMap<>(); @@ -905,10 +933,8 @@ public Object deserialize(byte[] value) } }); - // In this case with UUID, we don't need to add custom deserializers - // since jackson-databind already has it. - ObjectMapper objectMapper = new ObjectMapper( - new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)); + ObjectMapper objectMapper = + new MessagePackMapper(new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)); // Prepare serialized data Map originalMap = new HashMap<>(); @@ -963,9 +989,6 @@ public void parserShouldReadStrAsBin() assertArrayEquals("bar".getBytes(), binKeyPojo.b); } - // Test deserializers that parse a string as a number. - // Actually, com.fasterxml.jackson.databind.deser.std.StdDeserializer._parseInteger() takes care of it. - @Test public void deserializeStringAsInteger() throws IOException @@ -1044,55 +1067,458 @@ public void handleMissingItemInArray() packer.packString("two"); packer.close(); - try { + assertThrows(UnexpectedEndOfInputException.class, () -> { objectMapper.readValue(out.toByteArray(), new TypeReference>() {}); - fail(); - } - catch (JsonMappingException e) { - assertTrue(e.getCause() instanceof JsonEOFException); - } + }); } @Test public void handleMissingKeyValueInMap() - throws IOException { MessagePacker packer = MessagePack.newDefaultPacker(out); - packer.packMapHeader(3); - packer.packString("one"); - packer.packInt(1); - packer.packString("two"); - packer.packInt(2); - packer.close(); - try { - objectMapper.readValue(out.toByteArray(), new TypeReference>() {}); - fail(); + packer.packMapHeader(3); + packer.packString("one"); + packer.packInt(1); + packer.packString("two"); + packer.packInt(2); + packer.close(); } - catch (JsonEOFException e) { - assertTrue(true); + catch (IOException e) { + throw new RuntimeException(e); } + + assertThrows(JacksonException.class, () -> { + objectMapper.readValue(out.toByteArray(), new TypeReference>() {}); + }); } @Test public void handleMissingValueInMap() - throws IOException { MessagePacker packer = MessagePack.newDefaultPacker(out); - packer.packMapHeader(3); - packer.packString("one"); - packer.packInt(1); - packer.packString("two"); - packer.packInt(2); - packer.packString("three"); - packer.close(); - try { + packer.packMapHeader(3); + packer.packString("one"); + packer.packInt(1); + packer.packString("two"); + packer.packInt(2); + packer.packString("three"); + packer.close(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + + assertThrows(JacksonException.class, () -> { objectMapper.readValue(out.toByteArray(), new TypeReference>() {}); - fail(); + }); + } + + @Test + public void testByteArrayThreadLocalClearedAfterClose() + throws IOException + { + ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory()); + + byte[] bytes = objectMapper.writeValueAsBytes(Arrays.asList(1, 2, 3)); + + // Parse once; this caches the byte array in the ThreadLocal + objectMapper.readValue(bytes, new TypeReference>() {}); + + // Parse again with the same byte array instance and AUTO_CLOSE_SOURCE enabled + // (default). The byte array reference should have been cleared from the + // ThreadLocal on close, so the second parse resets the unpacker and starts + // from the beginning rather than continuing from the end. + List result = objectMapper.readValue(bytes, new TypeReference>() {}); + assertEquals(Arrays.asList(1, 2, 3), result); + } + + @Test + public void testByteArrayReuseResetsUnpackerWhenAutoCloseSourceDisabled() + throws IOException + { + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(tools.jackson.core.StreamReadFeature.AUTO_CLOSE_SOURCE) + .build(); + + byte[] bytes = objectMapper.writeValueAsBytes(Arrays.asList(1, 2, 3)); + + // First parse succeeds + List first = objectMapper.readValue(bytes, new TypeReference>() {}); + assertEquals(Arrays.asList(1, 2, 3), first); + + // Second parse with the same byte[] instance and AUTO_CLOSE_SOURCE disabled. + // The byte[] source always triggers an unpacker reset (|| src instanceof byte[]), + // so the second parse succeeds and returns the correct result. + List second = objectMapper.readValue(bytes, new TypeReference>() {}); + assertEquals(Arrays.asList(1, 2, 3), second); + } + + @Test + public void testInputStreamSequentialReadsWithAutoCloseSourceDisabled() + throws IOException + { + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(tools.jackson.core.StreamReadFeature.AUTO_CLOSE_SOURCE) + .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .build(); + + // Two values packed sequentially into a single stream + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(objectMapper.writeValueAsBytes(Arrays.asList(1, 2, 3))); + out.write(objectMapper.writeValueAsBytes(Arrays.asList(4, 5, 6))); + ByteArrayInputStream stream = new ByteArrayInputStream(out.toByteArray()); + + // First parse reads the first value; unpacker may read ahead into the second value + List first = objectMapper.readValue(stream, new TypeReference>() {}); + assertEquals(Arrays.asList(1, 2, 3), first); + + // Second parse must read the second value from the same stream. + // If the source was incorrectly cleared from the ThreadLocal on close(), + // the unpacker's read-ahead buffer is dismissed and the second value is lost. + List second = objectMapper.readValue(stream, new TypeReference>() {}); + assertEquals(Arrays.asList(4, 5, 6), second); + } + + @Test + public void testGetStringOnNullToken() throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packNil(); } - catch (JsonEOFException e) { - assertTrue(true); + MessagePackFactory factory = new MessagePackFactory(); + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { + assertEquals(JsonToken.VALUE_NULL, p.nextToken()); + assertEquals("null", p.getString()); + } + } + + @Test + public void testGetStringOnBoolToken() throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packBoolean(true); + packer.packBoolean(false); + } + MessagePackFactory factory = new MessagePackFactory(); + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { + assertEquals(JsonToken.VALUE_TRUE, p.nextToken()); + assertEquals("true", p.getString()); + assertEquals(JsonToken.VALUE_FALSE, p.nextToken()); + assertEquals("false", p.getString()); + } + } + + @Test + public void testNumericAccessorsOnNullTokenThrow() throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packNil(); + } + byte[] bytes = out.toByteArray(); + MessagePackFactory factory = new MessagePackFactory(); + + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getIntValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getLongValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getDoubleValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getFloatValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getBigIntegerValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getDecimalValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getNumberValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + } + + @Test + public void testNumericAccessorsOnBoolTokenThrow() throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packBoolean(true); + } + byte[] bytes = out.toByteArray(); + MessagePackFactory factory = new MessagePackFactory(); + + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getIntValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getLongValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + p.nextToken(); + try { + p.getDoubleValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + } + } + + @Test + public void testGetNumberTypeOnNonNumericTokens() throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packNil(); + packer.packBoolean(true); + packer.packString("hello"); + } + MessagePackFactory factory = new MessagePackFactory(); + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { + assertEquals(JsonToken.VALUE_NULL, p.nextToken()); + assertNull(p.getNumberType()); + assertEquals(JsonToken.VALUE_TRUE, p.nextToken()); + assertNull(p.getNumberType()); + assertEquals(JsonToken.VALUE_STRING, p.nextToken()); + assertNull(p.getNumberType()); + } + } + + @Test + public void testGetIntValueFromOutOfRangeDoubleThrows() throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packDouble(1e30); + } + MessagePackFactory factory = new MessagePackFactory(); + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + try { + p.getIntValue(); + fail("expected exception for out-of-range double"); + } + catch (JacksonException ignored) { } + } + } + + @Test + public void testGetLongValueFromOutOfRangeDoubleThrows() throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packDouble(1e30); + } + MessagePackFactory factory = new MessagePackFactory(); + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + try { + p.getLongValue(); + fail("expected exception for out-of-range double"); + } + catch (JacksonException ignored) { } + } + } + + @Test + public void testGetLongValueFromDoubleThatEqualsLongMaxValueRoundedUpThrows() throws IOException + { + // (double) Long.MAX_VALUE rounds up to 2^63, which exceeds Long.MAX_VALUE. + // The check `doubleValue > Long.MAX_VALUE` misses this value and silently + // saturates the cast to Long.MAX_VALUE instead of throwing. + double twoTo63 = 9.223372036854776E18; // exact double for 2^63 + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packDouble(twoTo63); + } + MessagePackFactory factory = new MessagePackFactory(); + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + try { + p.getLongValue(); + fail("expected exception for double value 2^63 which exceeds Long.MAX_VALUE"); + } + catch (JacksonException ignored) { } + } + } + + @Test + public void testNumericAccessorsOnStructuralTokenThrow() throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packMapHeader(0); + } + byte[] bytes = out.toByteArray(); + MessagePackFactory factory = new MessagePackFactory(); + + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { + assertEquals(JsonToken.START_OBJECT, p.nextToken()); + try { + p.getIntValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + try { + p.getLongValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + try { + p.getDoubleValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + try { + p.getFloatValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + try { + p.getBigIntegerValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + try { + p.getDecimalValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + try { + p.getNumberValue(); + fail("expected exception"); + } + catch (JacksonException ignored) { } + assertNull(p.getNumberType()); + } + } + + @Test + public void testGetIntValueFromFractionalDoubleTruncates() throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { + packer.packDouble(3.7); + } + MessagePackFactory factory = new MessagePackFactory(); + try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { + assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + assertEquals(3, p.getIntValue()); + } + } + + // Bug: MessagePackParser.close() has no re-entrancy guard; the base-class + // "if (_closed) { return; }" is bypassed because close() is fully overridden + // without calling super. A second call re-enters _closeInput(), which closes + // the underlying InputStream a second time. + @Test + public void testParserCloseIsIdempotent() throws IOException + { + byte[] bytes = objectMapper.writeValueAsBytes(42); + final int[] closeCount = {0}; + InputStream trackingStream = new ByteArrayInputStream(bytes) { + @Override + public void close() throws IOException + { + closeCount[0]++; + super.close(); + } + }; + + // reuseResourceInParser=false keeps ThreadLocal out of the picture so the + // test isolates the re-entrancy guard in close() itself. + MessagePackFactory nonReuseFactory = + new MessagePackFactory().setReuseResourceInParser(false); + JsonParser parser = + nonReuseFactory.createParser(ObjectReadContext.empty(), trackingStream); + parser.nextToken(); + parser.close(); // first close + parser.close(); // Bug: calls _closeInput() again — stream closed twice + + assertEquals("Stream should be closed exactly once", 1, closeCount[0]); + } + + // Bug: DupDetector.isDup(null) throws NullPointerException when a nil map key + // follows a non-null key and STRICT_DUPLICATE_DETECTION is enabled. + // The fault: name.equals(_firstName) where name is null and _firstName holds + // the prior non-null key string → NullPointerException. + @Test + public void testNilMapKeyWithDupDetectionDoesNotNPE() throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(baos)) { + packer.packMapHeader(2); + packer.packString("foo"); + packer.packInt(1); + packer.packNil(); // nil key — triggers DupDetector.isDup(null) + packer.packInt(2); + } + + MessagePackFactory f = new MessagePackFactoryBuilder() + .enable(StreamReadFeature.STRICT_DUPLICATE_DETECTION) + .build(); + + try (JsonParser p = f.createParser(ObjectReadContext.empty(), baos.toByteArray())) { + assertEquals(JsonToken.START_OBJECT, p.nextToken()); + assertEquals(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("foo", p.currentName()); + assertEquals(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals(JsonToken.PROPERTY_NAME, p.nextToken()); // Bug: NPE here + assertNull(p.currentName()); + assertEquals(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals(JsonToken.END_OBJECT, p.nextToken()); } } } diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackWriteContextTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackWriteContextTest.java similarity index 100% rename from msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackWriteContextTest.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackWriteContextTest.java 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 old mode 100755 new mode 100644 index 074d7bf5..36f5c97f --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java @@ -15,9 +15,9 @@ // package org.msgpack.jackson.dataformat; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import tools.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; @@ -26,11 +26,11 @@ import java.io.IOException; import java.time.Instant; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.Assert.assertEquals; public class TimestampExtensionModuleTest { - private final ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + private ObjectMapper objectMapper; private final SingleInstant singleInstant = new SingleInstant(); private final TripleInstants tripleInstants = new TripleInstants(); @@ -46,11 +46,13 @@ private static class TripleInstants public Instant c; } - @BeforeEach + @Before public void setUp() throws Exception { - objectMapper.registerModule(TimestampExtensionModule.INSTANCE); + objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(TimestampExtensionModule.INSTANCE) + .build(); } @Test @@ -86,7 +88,6 @@ public void serialize32BitFormat() byte[] bytes = objectMapper.writeValueAsBytes(singleInstant); - // Check the size of serialized data first try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { unpacker.unpackMapHeader(); assertEquals("instant", unpacker.unpackString()); @@ -108,7 +109,6 @@ public void serialize64BitFormat() byte[] bytes = objectMapper.writeValueAsBytes(singleInstant); - // Check the size of serialized data first try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { unpacker.unpackMapHeader(); assertEquals("instant", unpacker.unpackString()); @@ -130,7 +130,6 @@ public void serialize96BitFormat() byte[] bytes = objectMapper.writeValueAsBytes(singleInstant); - // Check the size of serialized data first try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { unpacker.unpackMapHeader(); assertEquals("instant", unpacker.unpackString()); diff --git a/msgpack-jackson2/README.md b/msgpack-jackson2/README.md new file mode 100644 index 00000000..0156453e --- /dev/null +++ b/msgpack-jackson2/README.md @@ -0,0 +1,492 @@ +# jackson-dataformat-msgpack + +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.msgpack/jackson-dataformat-msgpack/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.msgpack/jackson-dataformat-msgpack/) +[![Javadoc](https://www.javadoc.io/badge/org.msgpack/jackson-dataformat-msgpack.svg)](https://www.javadoc.io/doc/org.msgpack/jackson-dataformat-msgpack) + +This Jackson extension library is a component to easily read and write [MessagePack](http://msgpack.org/) encoded data through jackson-databind API. + +It extends standard Jackson streaming API (`JsonFactory`, `JsonParser`, `JsonGenerator`), and as such works seamlessly with all the higher level data abstractions (data binding, tree model, and pluggable extensions). For the details of Jackson-annotations, please see https://github.com/FasterXML/jackson-annotations. + +This library isn't compatible with msgpack-java v0.6 or earlier by default in serialization/deserialization of POJO. See **Advanced usage** below for details. + +## Install + +### Maven + +``` + + org.msgpack + jackson-dataformat-msgpack + (version) + +``` + +### Sbt + +``` +libraryDependencies += "org.msgpack" % "jackson-dataformat-msgpack" % "(version)" +``` + +### Gradle +``` +repositories { + mavenCentral() +} + +dependencies { + compile 'org.msgpack:jackson-dataformat-msgpack:(version)' +} +``` + + +## Basic usage + +### Serialization/Deserialization of POJO + +Only thing you need to do is to instantiate `MessagePackFactory` and pass it to the constructor of `com.fasterxml.jackson.databind.ObjectMapper`. And then, you can use it for MessagePack format data in the same way as jackson-databind. + +```java + // Instantiate ObjectMapper for MessagePack + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + + // Serialize a Java object to byte array + ExamplePojo pojo = new ExamplePojo("komamitsu"); + byte[] bytes = objectMapper.writeValueAsBytes(pojo); + + // Deserialize the byte array to a Java object + ExamplePojo deserialized = objectMapper.readValue(bytes, ExamplePojo.class); + System.out.println(deserialized.getName()); // => komamitsu +``` + +Or more easily: + +```java + ObjectMapper objectMapper = new MessagePackMapper(); +``` + +We strongly recommend to call `MessagePackMapper#handleBigIntegerAndBigDecimalAsString()` if you serialize and/or deserialize BigInteger/BigDecimal values. See [Serialize and deserialize BigDecimal as str type internally in MessagePack format](#serialize-and-deserialize-bigdecimal-as-str-type-internally-in-messagepack-format) for details. + +```java + ObjectMapper objectMapper = new MessagePackMapper().handleBigIntegerAndBigDecimalAsString(); +``` + +### Serialization/Deserialization of List + +```java + // Instantiate ObjectMapper for MessagePack + ObjectMapper objectMapper = new MessagePackMapper(); + + // Serialize a List to byte array + List list = new ArrayList<>(); + list.add("Foo"); + list.add("Bar"); + list.add(42); + byte[] bytes = objectMapper.writeValueAsBytes(list); + + // Deserialize the byte array to a List + List deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); + System.out.println(deserialized); // => [Foo, Bar, 42] +``` + +### Serialization/Deserialization of Map + +```java + // Instantiate ObjectMapper for MessagePack + ObjectMapper objectMapper = MessagePackMapper(); + + // Serialize a Map to byte array + Map map = new HashMap<>(); + map.put("name", "komamitsu"); + map.put("age", 42); + byte[] bytes = objectMapper.writeValueAsBytes(map); + + // Deserialize the byte array to a Map + Map deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); + System.out.println(deserialized); // => {name=komamitsu, age=42} + + ``` + +### Example of Serialization/Deserialization over multiple languages + +Java + +```java + // Serialize + Map obj = new HashMap(); + obj.put("foo", "hello"); + obj.put("bar", "world"); + byte[] bs = objectMapper.writeValueAsBytes(obj); + // bs => [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111, + // -93, 98, 97, 114, -91, 119, 111, 114, 108, 100] +``` + +Ruby + +```ruby + require 'msgpack' + + # Deserialize + xs = [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111, + -93, 98, 97, 114, -91, 119, 111, 114, 108, 100] + MessagePack.unpack(xs.pack("C*")) + # => {"foo"=>"hello", "bar"=>"world"} + + # Serialize + ["zero", 1, 2.0, nil].to_msgpack.unpack('C*') + # => [148, 164, 122, 101, 114, 111, 1, 203, 64, 0, 0, 0, 0, 0, 0, 0, 192] +``` + +Java + +```java + // Deserialize + bs = new byte[] {(byte) 148, (byte) 164, 122, 101, 114, 111, 1, + (byte) 203, 64, 0, 0, 0, 0, 0, 0, 0, (byte) 192}; + TypeReference> typeReference = new TypeReference>(){}; + List xs = objectMapper.readValue(bs, typeReference); + // xs => [zero, 1, 2.0, null] +``` + +## Advanced usage + +### Serialize/Deserialize POJO as MessagePack array type to keep compatibility with msgpack-java:0.6 + +In msgpack-java:0.6 or earlier, a POJO was serliazed and deserialized as an array of values in MessagePack format. The order of values depended on an internal order of Java class's variables and it was a naive way and caused some issues since Java class's variables order isn't guaranteed over Java implementations. + +On the other hand, jackson-databind serializes and deserializes a POJO as a key-value object. So this `jackson-dataformat-msgpack` also handles POJOs in the same way. As a result, it isn't compatible with msgpack-java:0.6 or earlier in serialization and deserialization of POJOs. + +But if you want to make this library handle POJOs in the same way as msgpack-java:0.6 or earlier, you can use `JsonArrayFormat` like this: + +```java + ObjectMapper objectMapper = new MessagePackMapper(); + objectMapper.setAnnotationIntrospector(new JsonArrayFormat()); +``` + +### Serialize multiple values without closing an output stream + +`com.fasterxml.jackson.databind.ObjectMapper` closes an output stream by default after it writes a value. If you want to serialize multiple values in a row without closing an output stream, set `JsonGenerator.Feature.AUTO_CLOSE_TARGET` to false. + +```java + OutputStream out = new FileOutputStream(tempFile); + ObjectMapper objectMapper = new MessagePackMapper(); + objectMapper.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false); + + objectMapper.writeValue(out, 1); + objectMapper.writeValue(out, "two"); + objectMapper.writeValue(out, 3.14); + out.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(new FileInputStream(tempFile)); + System.out.println(unpacker.unpackInt()); // => 1 + System.out.println(unpacker.unpackString()); // => two + System.out.println(unpacker.unpackFloat()); // => 3.14 +``` + +### Deserialize multiple values without closing an input stream + +`com.fasterxml.jackson.databind.ObjectMapper` closes an input stream by default after it reads a value. If you want to deserialize multiple values in a row without closing an output stream, set `JsonParser.Feature.AUTO_CLOSE_SOURCE` to false. + +```java + MessagePacker packer = MessagePack.newDefaultPacker(new FileOutputStream(tempFile)); + packer.packInt(42); + packer.packString("Hello"); + packer.close(); + + FileInputStream in = new FileInputStream(tempFile); + ObjectMapper objectMapper = new MessagePackMapper(); + objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false); + System.out.println(objectMapper.readValue(in, Integer.class)); + System.out.println(objectMapper.readValue(in, String.class)); + in.close(); +``` + +### Serialize not using str8 type + +Old msgpack-java (e.g 0.6.7) doesn't support MessagePack str8 type. When your application needs to comunicate with such an old MessagePack library, you can disable the data type like this: + +```java + MessagePack.PackerConfig config = new MessagePack.PackerConfig().withStr8FormatSupport(false); + ObjectMapper mapperWithConfig = new MessagePackMapper(new MessagePackFactory(config)); + // This string is serialized as bin8 type + byte[] resultWithoutStr8Format = mapperWithConfig.writeValueAsBytes(str8LengthString); +``` + +### Serialize using non-String as a key of Map + +When you want to use non-String value as a key of Map, use `MessagePackKeySerializer` for key serialization. + +```java + @JsonSerialize(keyUsing = MessagePackKeySerializer.class) + private Map intMap = new HashMap<>(); + + : + + intMap.put(42, "Hello"); + + ObjectMapper objectMapper = new MessagePackMapper(); + byte[] bytes = objectMapper.writeValueAsBytes(intMap); + + Map deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); + System.out.println(deserialized); // => {42=Hello} +``` + +### Serialize and deserialize BigDecimal as str type internally in MessagePack format + +`jackson-dataformat-msgpack` represents BigDecimal values as float type in MessagePack format by default for backward compatibility. But the default behavior could fail when handling too large value for `double` type. So we strongly recommend to call `MessagePackMapper#handleBigIntegerAndBigDecimalAsString()` to internally handle BigDecimal values as String. + +```java + ObjectMapper objectMapper = new MessagePackMapper().handleBigIntegerAndBigDecimalAsString(); + + Pojo obj = new Pojo(); + // This value is too large to be serialized as double + obj.value = new BigDecimal("1234567890.98765432100"); + + byte[] converted = objectMapper.writeValueAsBytes(obj); + + System.out.println(objectMapper.readValue(converted, Pojo.class)); // => Pojo{value=1234567890.98765432100} +``` +`MessagePackMapper#handleBigIntegerAndDecimalAsString()` is equivalent to the following configuration. + +```java + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + objectMapper.configOverride(BigInteger.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); + objectMapper.configOverride(BigDecimal.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); +``` + + +### 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 MessagePackMapper() + .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 your own custom extension types easily. + +#### Deserialize extension type value directly + +```java + // In this application, extension type 59 is used for byte[] + byte[] bytes; + { + // This ObjectMapper is just for temporary serialization + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + MessagePacker packer = MessagePack.newDefaultPacker(outputStream); + + packer.packExtensionTypeHeader((byte) 59, hexspeak.length); + packer.addPayload(hexspeak); + packer.close(); + + bytes = outputStream.toByteArray(); + } + + // Register the type and a deserializer to ExtensionTypeCustomDeserializers + ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); + extTypeCustomDesers.addCustomDeser((byte) 59, data -> { + if (Arrays.equals(data, + new byte[] {(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE})) { + return "Java"; + } + return "Not Java"; + }); + + ObjectMapper objectMapper = new ObjectMapper( + new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)); + + System.out.println(objectMapper.readValue(bytes, Object.class)); + // => Java +``` + +#### Use extension type as Map key + +```java + static class TripleBytesPojo + { + public byte first; + public byte second; + public byte third; + + public TripleBytesPojo(byte first, byte second, byte third) + { + this.first = first; + this.second = second; + this.third = third; + } + + @Override + public boolean equals(Object o) + { + : + } + + @Override + public int hashCode() + { + : + } + + @Override + public String toString() + { + // This key format is used when serialized as map key + return String.format("%d-%d-%d", first, second, third); + } + + static class KeyDeserializer + extends com.fasterxml.jackson.databind.KeyDeserializer + { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) + throws IOException + { + String[] values = key.split("-"); + return new TripleBytesPojo(Byte.parseByte(values[0]), Byte.parseByte(values[1]), Byte.parseByte(values[2])); + } + } + + static TripleBytesPojo deserialize(byte[] bytes) + { + return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]); + } + } + + : + + byte extTypeCode = 42; + + ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); + extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() + { + @Override + public Object deserialize(byte[] value) + throws IOException + { + return TripleBytesPojo.deserialize(value); + } + }); + + SimpleModule module = new SimpleModule(); + module.addKeyDeserializer(TripleBytesPojo.class, new TripleBytesPojo.KeyDeserializer()); + ObjectMapper objectMapper = new ObjectMapper( + new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) + .registerModule(module); + + Map deserializedMap = + objectMapper.readValue(serializedData, + new TypeReference>() {}); +``` + +#### Use extension type as Map value + +```java + static class TripleBytesPojo + { + public byte first; + public byte second; + public byte third; + + public TripleBytesPojo(byte first, byte second, byte third) + { + this.first = first; + this.second = second; + this.third = third; + } + + static class Deserializer + extends StdDeserializer + { + protected Deserializer() + { + super(TripleBytesPojo.class); + } + + @Override + public TripleBytesPojo deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException, JsonProcessingException + { + return TripleBytesPojo.deserialize(p.getBinaryValue()); + } + } + + static TripleBytesPojo deserialize(byte[] bytes) + { + return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]); + } + } + + : + + byte extTypeCode = 42; + + ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); + extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() + { + @Override + public Object deserialize(byte[] value) + throws IOException + { + return TripleBytesPojo.deserialize(value); + } + }); + + SimpleModule module = new SimpleModule(); + module.addDeserializer(TripleBytesPojo.class, new TripleBytesPojo.Deserializer()); + ObjectMapper objectMapper = new ObjectMapper( + new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) + .registerModule(module); + + Map deserializedMap = + objectMapper.readValue(serializedData, + new TypeReference>() {}); +``` + +### Serialize a nested object that also serializes + +When you serialize an object that has a nested object also serializing with ObjectMapper and MessagePackFactory like the following code, it throws NullPointerException since the nested MessagePackFactory modifies a shared state stored in ThreadLocal. + +```java + @Test + public void testNestedSerialization() throws Exception + { + ObjectMapper objectMapper = new MessagePackMapper(); + objectMapper.writeValueAsBytes(new OuterClass()); + } + + public class OuterClass + { + public String getInner() throws JsonProcessingException + { + ObjectMapper m = new MessagePackMapper(); + m.writeValueAsBytes(new InnerClass()); + return "EFG"; + } + } + + public class InnerClass + { + public String getName() + { + return "ABC"; + } + } +``` + +There are a few options to fix this issue, but they introduce performance degredations while this usage is a corner case. A workaround that doesn't affect performance is to call `MessagePackFactory#setReuseResourceInGenerator(false)`. It might be inconvenient to call the API for users, but it's a reasonable tradeoff with performance for now. + +```java + ObjectMapper objectMapper = new ObjectMapper( + new MessagePackFactory().setReuseResourceInGenerator(false)); +``` diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java similarity index 100% rename from msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java rename to msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/JavaInfo.java b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/JavaInfo.java similarity index 100% rename from msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/JavaInfo.java rename to msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/JavaInfo.java diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java new file mode 100644 index 00000000..39155030 --- /dev/null +++ b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java @@ -0,0 +1,35 @@ +package org.msgpack.jackson.dataformat; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; + +import static com.fasterxml.jackson.annotation.JsonFormat.Shape.ARRAY; + +/** + * Provides the ability of serializing POJOs without their schema. + * Similar to @JsonFormat annotation with JsonFormat.Shape.ARRAY, but in a programmatic + * way. + * + * This also provides same behavior as msgpack-java 0.6.x serialization api. + */ +public class JsonArrayFormat extends JacksonAnnotationIntrospector +{ + private static final JsonFormat.Value ARRAY_FORMAT = new JsonFormat.Value().withShape(ARRAY); + + /** + * Defines array format for serialized entities with ObjectMapper, without actually + * including the schema + */ + @Override + public JsonFormat.Value findFormat(Annotated ann) + { + // If the entity contains JsonFormat annotation, give it higher priority. + JsonFormat.Value precedenceFormat = super.findFormat(ann); + if (precedenceFormat != null) { + return precedenceFormat; + } + + return ARRAY_FORMAT; + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java similarity index 53% rename from msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java rename to msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java index 1b62b80e..e11c2cd0 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java +++ b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java @@ -1,27 +1,12 @@ -// -// 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 tools.jackson.core.JsonGenerator; -import tools.jackson.databind.SerializationContext; -import tools.jackson.databind.annotation.JsonSerialize; -import tools.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.io.IOException; import java.util.Arrays; -import java.util.Objects; @JsonSerialize(using = MessagePackExtensionType.Serializer.class) public class MessagePackExtensionType @@ -32,7 +17,7 @@ public class MessagePackExtensionType public MessagePackExtensionType(byte type, byte[] data) { this.type = type; - this.data = Objects.requireNonNull(data, "data"); + this.data = data; } public byte getType() @@ -71,21 +56,11 @@ public int hashCode() return result; } - @Override - public String toString() - { - return "MessagePackExtensionType(type=" + type + ", data.length=" + data.length + ")"; - } - - public static class Serializer extends StdSerializer + public static class Serializer extends JsonSerializer { - public Serializer() - { - super(MessagePackExtensionType.class); - } - @Override - public void serialize(MessagePackExtensionType value, JsonGenerator gen, SerializationContext serializers) + public void serialize(MessagePackExtensionType value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { if (gen instanceof MessagePackGenerator) { MessagePackGenerator msgpackGenerator = (MessagePackGenerator) gen; diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java new file mode 100644 index 00000000..865c0cf4 --- /dev/null +++ b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java @@ -0,0 +1,187 @@ +// +// 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.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.io.ContentReference; +import com.fasterxml.jackson.core.io.IOContext; +import org.msgpack.core.MessagePack; +import org.msgpack.core.annotations.VisibleForTesting; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Writer; +import java.util.Arrays; + +public class MessagePackFactory + extends JsonFactory +{ + private static final long serialVersionUID = 2578263992015504347L; + + private final MessagePack.PackerConfig packerConfig; + private boolean reuseResourceInGenerator = true; + private boolean reuseResourceInParser = true; + private boolean supportIntegerKeys = false; + private ExtensionTypeCustomDeserializers extTypeCustomDesers; + + public MessagePackFactory() + { + this(MessagePack.DEFAULT_PACKER_CONFIG); + } + + public MessagePackFactory(MessagePack.PackerConfig packerConfig) + { + this.packerConfig = packerConfig; + } + + public MessagePackFactory(MessagePackFactory src) + { + super(src, null); + this.packerConfig = src.packerConfig.clone(); + this.reuseResourceInGenerator = src.reuseResourceInGenerator; + this.reuseResourceInParser = src.reuseResourceInParser; + if (src.extTypeCustomDesers != null) { + this.extTypeCustomDesers = new ExtensionTypeCustomDeserializers(src.extTypeCustomDesers); + } + } + + public MessagePackFactory setReuseResourceInGenerator(boolean reuseResourceInGenerator) + { + this.reuseResourceInGenerator = reuseResourceInGenerator; + return this; + } + + public MessagePackFactory setReuseResourceInParser(boolean reuseResourceInParser) + { + this.reuseResourceInParser = reuseResourceInParser; + return this; + } + + public MessagePackFactory setSupportIntegerKeys(boolean supportIntegerKeys) + { + this.supportIntegerKeys = supportIntegerKeys; + return this; + } + + public MessagePackFactory setExtTypeCustomDesers(ExtensionTypeCustomDeserializers extTypeCustomDesers) + { + this.extTypeCustomDesers = extTypeCustomDesers; + return this; + } + + @Override + public JsonGenerator createGenerator(OutputStream out, JsonEncoding enc) + throws IOException + { + return new MessagePackGenerator(_generatorFeatures, _objectCodec, out, packerConfig, reuseResourceInGenerator, supportIntegerKeys); + } + + @Override + public JsonGenerator createGenerator(File f, JsonEncoding enc) + throws IOException + { + return createGenerator(new FileOutputStream(f), enc); + } + + @Override + public JsonGenerator createGenerator(Writer w) + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonParser createParser(byte[] data) + throws IOException + { + IOContext ioContext = _createContext(ContentReference.rawReference(data), false); + return _createParser(data, 0, data.length, ioContext); + } + + @Override + public JsonParser createParser(InputStream in) + throws IOException + { + IOContext ioContext = _createContext(ContentReference.rawReference(in), false); + return _createParser(in, ioContext); + } + + @Override + protected MessagePackParser _createParser(InputStream in, IOContext ctxt) + throws IOException + { + MessagePackParser parser = new MessagePackParser(ctxt, _parserFeatures, _objectCodec, in, reuseResourceInParser); + if (extTypeCustomDesers != null) { + parser.setExtensionTypeCustomDeserializers(extTypeCustomDesers); + } + return parser; + } + + @Override + protected JsonParser _createParser(byte[] data, int offset, int len, IOContext ctxt) + throws IOException + { + if (offset != 0 || len != data.length) { + data = Arrays.copyOfRange(data, offset, offset + len); + } + MessagePackParser parser = new MessagePackParser(ctxt, _parserFeatures, _objectCodec, data, reuseResourceInParser); + if (extTypeCustomDesers != null) { + parser.setExtensionTypeCustomDeserializers(extTypeCustomDesers); + } + return parser; + } + + @Override + public JsonFactory copy() + { + return new MessagePackFactory(this); + } + + @VisibleForTesting + MessagePack.PackerConfig getPackerConfig() + { + return packerConfig; + } + + @VisibleForTesting + boolean isReuseResourceInParser() + { + return reuseResourceInParser; + } + + @VisibleForTesting + ExtensionTypeCustomDeserializers getExtTypeCustomDesers() + { + return extTypeCustomDesers; + } + + @Override + public String getFormatName() + { + return "msgpack"; + } + + @Override + public boolean canHandleBinaryNatively() + { + return true; + } +} diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java new file mode 100644 index 00000000..3dde5604 --- /dev/null +++ b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java @@ -0,0 +1,830 @@ +// +// 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.core.Base64Variant; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.core.SerializableString; +import com.fasterxml.jackson.core.base.GeneratorBase; +import com.fasterxml.jackson.core.io.ContentReference; +import com.fasterxml.jackson.core.io.IOContext; +import com.fasterxml.jackson.core.io.SerializedString; +import com.fasterxml.jackson.core.json.JsonWriteContext; +import com.fasterxml.jackson.core.util.BufferRecycler; +import org.msgpack.core.MessagePack; +import org.msgpack.core.MessagePacker; +import org.msgpack.core.annotations.Nullable; +import org.msgpack.core.buffer.MessageBufferOutput; +import org.msgpack.core.buffer.OutputStreamBufferOutput; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import static org.msgpack.jackson.dataformat.JavaInfo.STRING_VALUE_FIELD_IS_CHARS; + +public class MessagePackGenerator + extends GeneratorBase +{ + private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + private static final int IN_ROOT = 0; + private static final int IN_OBJECT = 1; + private static final int IN_ARRAY = 2; + private final MessagePacker messagePacker; + private static final ThreadLocal messageBufferOutputHolder = new ThreadLocal<>(); + private final OutputStream output; + private final MessagePack.PackerConfig packerConfig; + private final boolean supportIntegerKeys; + + private int currentParentElementIndex = -1; + private int currentState = IN_ROOT; + private final List nodes; + private boolean isElementsClosed = false; + + private static final class AsciiCharString + { + public final byte[] bytes; + + public AsciiCharString(byte[] bytes) + { + this.bytes = bytes; + } + } + + private abstract static class Node + { + // Root containers have -1. + final int parentIndex; + + public Node(int parentIndex) + { + this.parentIndex = parentIndex; + } + + abstract void incrementChildCount(); + + abstract int currentStateAsParent(); + } + + private abstract static class NodeContainer extends Node + { + // Only for containers. + int childCount; + + public NodeContainer(int parentIndex) + { + super(parentIndex); + } + + @Override + void incrementChildCount() + { + childCount++; + } + } + + private static final class NodeArray extends NodeContainer + { + public NodeArray(int parentIndex) + { + super(parentIndex); + } + + @Override + int currentStateAsParent() + { + return IN_ARRAY; + } + } + + private static final class NodeObject extends NodeContainer + { + public NodeObject(int parentIndex) + { + super(parentIndex); + } + + @Override + int currentStateAsParent() + { + return IN_OBJECT; + } + } + + private static final class NodeEntryInArray extends Node + { + final Object value; + + public NodeEntryInArray(int parentIndex, Object value) + { + super(parentIndex); + this.value = value; + } + + @Override + void incrementChildCount() + { + throw new UnsupportedOperationException(); + } + + @Override + int currentStateAsParent() + { + throw new UnsupportedOperationException(); + } + } + + private static final class NodeEntryInObject extends Node + { + final Object key; + // Lazily initialized. + Object value; + + public NodeEntryInObject(int parentIndex, Object key) + { + super(parentIndex); + this.key = key; + } + + @Override + void incrementChildCount() + { + assert value instanceof NodeContainer; + ((NodeContainer) value).childCount++; + } + + @Override + int currentStateAsParent() + { + if (value instanceof NodeObject) { + return IN_OBJECT; + } + else if (value instanceof NodeArray) { + return IN_ARRAY; + } + else { + throw new AssertionError(); + } + } + } + + // This is an internal constructor for nested serialization. + @SuppressWarnings("deprecation") + private MessagePackGenerator( + int features, + ObjectCodec codec, + OutputStream out, + MessagePack.PackerConfig packerConfig, + boolean supportIntegerKeys) + { + super(features, codec, new IOContext(new BufferRecycler(), ContentReference.rawReference(out), false), JsonWriteContext.createRootContext(null)); + this.output = out; + this.messagePacker = packerConfig.newPacker(out); + this.packerConfig = packerConfig; + this.nodes = new ArrayList<>(); + this.supportIntegerKeys = supportIntegerKeys; + } + + @SuppressWarnings("deprecation") + public MessagePackGenerator( + int features, + ObjectCodec codec, + OutputStream out, + MessagePack.PackerConfig packerConfig, + boolean reuseResourceInGenerator, + boolean supportIntegerKeys) + throws IOException + { + super(features, codec, new IOContext(new BufferRecycler(), ContentReference.rawReference(out), false), JsonWriteContext.createRootContext(null)); + this.output = out; + this.messagePacker = packerConfig.newPacker(getMessageBufferOutputForOutputStream(out, reuseResourceInGenerator)); + this.packerConfig = packerConfig; + this.nodes = new ArrayList<>(); + this.supportIntegerKeys = supportIntegerKeys; + } + + private MessageBufferOutput getMessageBufferOutputForOutputStream( + OutputStream out, + boolean reuseResourceInGenerator) + throws IOException + { + OutputStreamBufferOutput messageBufferOutput; + if (reuseResourceInGenerator) { + messageBufferOutput = messageBufferOutputHolder.get(); + if (messageBufferOutput == null) { + messageBufferOutput = new OutputStreamBufferOutput(out); + messageBufferOutputHolder.set(messageBufferOutput); + } + else { + messageBufferOutput.reset(out); + } + } + else { + messageBufferOutput = new OutputStreamBufferOutput(out); + } + return messageBufferOutput; + } + + private String currentStateStr() + { + switch (currentState) { + case IN_OBJECT: + return "IN_OBJECT"; + case IN_ARRAY: + return "IN_ARRAY"; + default: + return "IN_ROOT"; + } + } + + @Override + public void writeStartArray() + { + if (currentState == IN_OBJECT) { + Node node = nodes.get(nodes.size() - 1); + assert node instanceof NodeEntryInObject; + NodeEntryInObject nodeEntryInObject = (NodeEntryInObject) node; + nodeEntryInObject.value = new NodeArray(currentParentElementIndex); + } + else { + nodes.add(new NodeArray(currentParentElementIndex)); + } + currentParentElementIndex = nodes.size() - 1; + currentState = IN_ARRAY; + } + + @Override + public void writeEndArray() + throws IOException + { + if (currentState != IN_ARRAY) { + _reportError("Current context not an array but " + currentStateStr()); + } + endCurrentContainer(); + } + + @Override + public void writeStartObject() + { + if (currentState == IN_OBJECT) { + Node node = nodes.get(nodes.size() - 1); + assert node instanceof NodeEntryInObject; + NodeEntryInObject nodeEntryInObject = (NodeEntryInObject) node; + nodeEntryInObject.value = new NodeObject(currentParentElementIndex); + } + else { + nodes.add(new NodeObject(currentParentElementIndex)); + } + currentParentElementIndex = nodes.size() - 1; + currentState = IN_OBJECT; + } + + @Override + public void writeEndObject() + throws IOException + { + if (currentState != IN_OBJECT) { + _reportError("Current context not an object but " + currentStateStr()); + } + endCurrentContainer(); + } + + private void endCurrentContainer() + { + Node parent = nodes.get(currentParentElementIndex); + if (currentParentElementIndex == 0) { + isElementsClosed = true; + currentParentElementIndex = parent.parentIndex; + return; + } + + currentParentElementIndex = parent.parentIndex; + assert currentParentElementIndex >= 0; + Node currentParent = nodes.get(currentParentElementIndex); + currentParent.incrementChildCount(); + currentState = currentParent.currentStateAsParent(); + } + + private void packNonContainer(Object v) + throws IOException + { + MessagePacker messagePacker = getMessagePacker(); + if (v instanceof String) { + messagePacker.packString((String) v); + } + else if (v instanceof AsciiCharString) { + byte[] bytes = ((AsciiCharString) v).bytes; + messagePacker.packRawStringHeader(bytes.length); + messagePacker.writePayload(bytes); + } + else if (v instanceof Integer) { + messagePacker.packInt((Integer) v); + } + else if (v == null) { + messagePacker.packNil(); + } + else if (v instanceof Float) { + messagePacker.packFloat((Float) v); + } + else if (v instanceof Long) { + messagePacker.packLong((Long) v); + } + else if (v instanceof Double) { + messagePacker.packDouble((Double) v); + } + else if (v instanceof BigInteger) { + messagePacker.packBigInteger((BigInteger) v); + } + else if (v instanceof BigDecimal) { + packBigDecimal((BigDecimal) v); + } + else if (v instanceof Boolean) { + messagePacker.packBoolean((Boolean) v); + } + else if (v instanceof ByteBuffer) { + ByteBuffer bb = (ByteBuffer) v; + int len = bb.remaining(); + if (bb.hasArray()) { + messagePacker.packBinaryHeader(len); + messagePacker.writePayload(bb.array(), bb.arrayOffset(), len); + } + else { + byte[] data = new byte[len]; + bb.get(data); + messagePacker.packBinaryHeader(len); + messagePacker.addPayload(data); + } + } + else if (v instanceof MessagePackExtensionType) { + MessagePackExtensionType extensionType = (MessagePackExtensionType) v; + byte[] extData = extensionType.getData(); + messagePacker.packExtensionTypeHeader(extensionType.getType(), extData.length); + messagePacker.writePayload(extData); + } + else { + messagePacker.flush(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + MessagePackGenerator messagePackGenerator = new MessagePackGenerator(getFeatureMask(), getCodec(), outputStream, packerConfig, supportIntegerKeys); + getCodec().writeValue(messagePackGenerator, v); + output.write(outputStream.toByteArray()); + } + } + + private void packBigDecimal(BigDecimal decimal) + throws IOException + { + MessagePacker messagePacker = getMessagePacker(); + boolean failedToPackAsBI = false; + try { + //Check to see if this BigDecimal can be converted to BigInteger + BigInteger integer = decimal.toBigIntegerExact(); + messagePacker.packBigInteger(integer); + } + catch (ArithmeticException | IllegalArgumentException e) { + failedToPackAsBI = true; + } + + if (failedToPackAsBI) { + double doubleValue = decimal.doubleValue(); + //Check to make sure this BigDecimal can be represented as a double + if (!decimal.stripTrailingZeros().toEngineeringString().equals( + BigDecimal.valueOf(doubleValue).stripTrailingZeros().toEngineeringString())) { + throw new IllegalArgumentException("MessagePack cannot serialize a BigDecimal that can't be represented as double. " + decimal); + } + messagePacker.packDouble(doubleValue); + } + } + + private void packObject(NodeObject container) + throws IOException + { + MessagePacker messagePacker = getMessagePacker(); + messagePacker.packMapHeader(container.childCount); + } + + private void packArray(NodeArray container) + throws IOException + { + MessagePacker messagePacker = getMessagePacker(); + messagePacker.packArrayHeader(container.childCount); + } + + private void addKeyNode(Object key) + { + if (currentState != IN_OBJECT) { + throw new IllegalStateException(); + } + Node node = new NodeEntryInObject(currentParentElementIndex, key); + nodes.add(node); + } + + private void addValueNode(Object value) throws IOException + { + switch (currentState) { + case IN_OBJECT: { + Node node = nodes.get(nodes.size() - 1); + assert node instanceof NodeEntryInObject; + NodeEntryInObject nodeEntryInObject = (NodeEntryInObject) node; + nodeEntryInObject.value = value; + nodes.get(node.parentIndex).incrementChildCount(); + break; + } + case IN_ARRAY: { + Node node = new NodeEntryInArray(currentParentElementIndex, value); + nodes.add(node); + nodes.get(node.parentIndex).incrementChildCount(); + break; + } + default: + packNonContainer(value); + flushMessagePacker(); + break; + } + } + + @Nullable + private byte[] getBytesIfAscii(char[] chars, int offset, int len) + { + byte[] bytes = new byte[len]; + for (int i = offset; i < offset + len; i++) { + char c = chars[i]; + if (c >= 0x80) { + return null; + } + bytes[i] = (byte) c; + } + return bytes; + } + + private boolean areAllAsciiBytes(byte[] bytes, int offset, int len) + { + for (int i = offset; i < offset + len; i++) { + if ((bytes[i] & 0x80) != 0) { + return false; + } + } + return true; + } + + private void writeCharArrayTextKey(char[] text, int offset, int len) + { + byte[] bytes = getBytesIfAscii(text, offset, len); + if (bytes != null) { + addKeyNode(new AsciiCharString(bytes)); + return; + } + addKeyNode(new String(text, offset, len)); + } + + private void writeCharArrayTextValue(char[] text, int offset, int len) throws IOException + { + byte[] bytes = getBytesIfAscii(text, offset, len); + if (bytes != null) { + addValueNode(new AsciiCharString(bytes)); + return; + } + addValueNode(new String(text, offset, len)); + } + + private void writeByteArrayTextValue(byte[] text, int offset, int len) throws IOException + { + if (areAllAsciiBytes(text, offset, len)) { + addValueNode(new AsciiCharString(text)); + return; + } + addValueNode(new String(text, offset, len, DEFAULT_CHARSET)); + } + + private void writeByteArrayTextKey(byte[] text, int offset, int len) throws IOException + { + if (areAllAsciiBytes(text, offset, len)) { + addValueNode(new AsciiCharString(text)); + return; + } + addValueNode(new String(text, offset, len, DEFAULT_CHARSET)); + } + + @Override + public void writeFieldId(long id) throws IOException + { + if (this.supportIntegerKeys) { + addKeyNode(id); + } + else { + super.writeFieldId(id); + } + } + + @Override + public void writeFieldName(String name) + { + if (STRING_VALUE_FIELD_IS_CHARS.get()) { + char[] chars = name.toCharArray(); + writeCharArrayTextKey(chars, 0, chars.length); + } + else { + addKeyNode(name); + } + } + + @Override + public void writeFieldName(SerializableString name) + { + if (name instanceof SerializedString) { + writeFieldName(name.getValue()); + } + else if (name instanceof MessagePackSerializedString) { + addKeyNode(((MessagePackSerializedString) name).getRawValue()); + } + else { + throw new IllegalArgumentException("Unsupported key: " + name); + } + } + + @Override + public void writeString(String text) + throws IOException + { + if (STRING_VALUE_FIELD_IS_CHARS.get()) { + char[] chars = text.toCharArray(); + writeCharArrayTextValue(chars, 0, chars.length); + } + else { + addValueNode(text); + } + } + + @Override + public void writeString(char[] text, int offset, int len) + throws IOException + { + writeCharArrayTextValue(text, offset, len); + } + + @Override + public void writeRawUTF8String(byte[] text, int offset, int length) + throws IOException + { + writeByteArrayTextValue(text, offset, length); + } + + @Override + public void writeUTF8String(byte[] text, int offset, int length) + throws IOException + { + writeByteArrayTextValue(text, offset, length); + } + + @Override + public void writeRaw(String text) + throws IOException + { + if (STRING_VALUE_FIELD_IS_CHARS.get()) { + char[] chars = text.toCharArray(); + writeCharArrayTextValue(chars, 0, chars.length); + } + else { + addValueNode(text); + } + } + + @Override + public void writeRaw(String text, int offset, int len) + throws IOException + { + // TODO: There is room to optimize this. + char[] chars = text.toCharArray(); + writeCharArrayTextValue(chars, offset, len); + } + + @Override + public void writeRaw(char[] text, int offset, int len) + throws IOException + { + writeCharArrayTextValue(text, offset, len); + } + + @Override + public void writeRaw(char c) + throws IOException + { + writeCharArrayTextValue(new char[] { c }, 0, 1); + } + + @Override + public void writeBinary(Base64Variant b64variant, byte[] data, int offset, int len) + throws IOException + { + addValueNode(ByteBuffer.wrap(data, offset, len)); + } + + @Override + public void writeNumber(int v) + throws IOException + { + addValueNode(v); + } + + @Override + public void writeNumber(long v) + throws IOException + { + addValueNode(v); + } + + @Override + public void writeNumber(BigInteger v) + throws IOException + { + addValueNode(v); + } + + @Override + public void writeNumber(double d) + throws IOException + { + addValueNode(d); + } + + @Override + public void writeNumber(float f) + throws IOException + { + addValueNode(f); + } + + @Override + public void writeNumber(BigDecimal dec) + throws IOException + { + addValueNode(dec); + } + + @Override + public void writeNumber(String encodedValue) + throws IOException, UnsupportedOperationException + { + // There is a room to improve this API's performance while the implementation is robust. + // If users can use other MessagePackGenerator#writeNumber APIs that accept + // proper numeric types not String, it's better to use the other APIs instead. + try { + long l = Long.parseLong(encodedValue); + addValueNode(l); + return; + } + catch (NumberFormatException ignored) { + } + + try { + double d = Double.parseDouble(encodedValue); + addValueNode(d); + return; + } + catch (NumberFormatException ignored) { + } + + try { + BigInteger bi = new BigInteger(encodedValue); + addValueNode(bi); + return; + } + catch (NumberFormatException ignored) { + } + + try { + BigDecimal bc = new BigDecimal(encodedValue); + addValueNode(bc); + return; + } + catch (NumberFormatException ignored) { + } + + throw new NumberFormatException(encodedValue); + } + + @Override + public void writeBoolean(boolean state) + throws IOException + { + addValueNode(state); + } + + @Override + public void writeNull() + throws IOException + { + addValueNode(null); + } + + public void writeExtensionType(MessagePackExtensionType extensionType) + throws IOException + { + addValueNode(extensionType); + } + + @Override + public void close() + throws IOException + { + try { + flush(); + } + finally { + if (isEnabled(Feature.AUTO_CLOSE_TARGET)) { + MessagePacker messagePacker = getMessagePacker(); + messagePacker.close(); + } + } + } + + @Override + public void flush() + throws IOException + { + if (!isElementsClosed) { + // The whole elements are not closed yet. + return; + } + + for (int i = 0; i < nodes.size(); i++) { + Node node = nodes.get(i); + if (node instanceof NodeEntryInObject) { + NodeEntryInObject nodeEntry = (NodeEntryInObject) node; + packNonContainer(nodeEntry.key); + if (nodeEntry.value instanceof NodeObject) { + packObject((NodeObject) nodeEntry.value); + } + else if (nodeEntry.value instanceof NodeArray) { + packArray((NodeArray) nodeEntry.value); + } + else { + packNonContainer(nodeEntry.value); + } + } + else if (node instanceof NodeObject) { + packObject((NodeObject) node); + } + else if (node instanceof NodeEntryInArray) { + packNonContainer(((NodeEntryInArray) node).value); + } + else if (node instanceof NodeArray) { + packArray((NodeArray) node); + } + else { + throw new AssertionError(); + } + } + flushMessagePacker(); + nodes.clear(); + isElementsClosed = false; + } + + private void flushMessagePacker() + throws IOException + { + MessagePacker messagePacker = getMessagePacker(); + messagePacker.flush(); + } + + @Override + protected void _releaseBuffers() + { + try { + messagePacker.close(); + } + catch (IOException e) { + throw new RuntimeException("Failed to close MessagePacker", e); + } + } + + @Override + protected void _verifyValueWrite(String typeMsg) throws IOException + { + // FIXME? + } + + private MessagePacker getMessagePacker() + { + return messagePacker; + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java similarity index 73% rename from msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java rename to msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java index 836a905d..36fb235d 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java +++ b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java @@ -15,9 +15,11 @@ // package org.msgpack.jackson.dataformat; -import tools.jackson.core.JsonGenerator; -import tools.jackson.databind.SerializationContext; -import tools.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import java.io.IOException; public class MessagePackKeySerializer extends StdSerializer @@ -28,8 +30,9 @@ public MessagePackKeySerializer() } @Override - public void serialize(Object value, JsonGenerator jgen, SerializationContext provider) + public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) + throws IOException { - jgen.writeName(new MessagePackSerializedString(value)); + jgen.writeFieldName(new MessagePackSerializedString(value)); } } diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java new file mode 100644 index 00000000..144c8d1a --- /dev/null +++ b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java @@ -0,0 +1,73 @@ +// +// 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.annotation.JsonFormat; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.cfg.MapperBuilder; + +import java.math.BigDecimal; +import java.math.BigInteger; + +public class MessagePackMapper extends ObjectMapper +{ + private static final long serialVersionUID = 3L; + + public static class Builder extends MapperBuilder + { + public Builder(MessagePackMapper m) + { + super(m); + } + } + + public MessagePackMapper() + { + this(new MessagePackFactory()); + } + + public MessagePackMapper(MessagePackFactory f) + { + super(f); + } + + public MessagePackMapper handleBigIntegerAsString() + { + configOverride(BigInteger.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); + return this; + } + + public MessagePackMapper handleBigDecimalAsString() + { + configOverride(BigDecimal.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); + return this; + } + + public MessagePackMapper handleBigIntegerAndBigDecimalAsString() + { + return handleBigIntegerAsString().handleBigDecimalAsString(); + } + + public static Builder builder() + { + return new Builder(new MessagePackMapper()); + } + + public static Builder builder(MessagePackFactory f) + { + return new Builder(new MessagePackMapper(f)); + } +} diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java new file mode 100644 index 00000000..72aeed20 --- /dev/null +++ b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java @@ -0,0 +1,650 @@ +// +// 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.core.Base64Variant; +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonStreamContext; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.base.ParserMinimalBase; +import com.fasterxml.jackson.core.io.IOContext; +import com.fasterxml.jackson.core.io.JsonEOFException; +import com.fasterxml.jackson.core.json.DupDetector; +import org.msgpack.core.ExtensionTypeHeader; +import org.msgpack.core.MessageFormat; +import org.msgpack.core.MessagePack; +import org.msgpack.core.MessageUnpacker; +import org.msgpack.core.buffer.ArrayBufferInput; +import org.msgpack.core.buffer.InputStreamBufferInput; +import org.msgpack.core.buffer.MessageBufferInput; +import org.msgpack.value.ValueType; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; + +import static org.msgpack.jackson.dataformat.JavaInfo.STRING_VALUE_FIELD_IS_CHARS; + +public class MessagePackParser + extends ParserMinimalBase +{ + private static final ThreadLocal> messageUnpackerHolder = new ThreadLocal<>(); + private final MessageUnpacker messageUnpacker; + + private static final BigInteger LONG_MIN = BigInteger.valueOf(Long.MIN_VALUE); + private static final BigInteger LONG_MAX = BigInteger.valueOf(Long.MAX_VALUE); + + private ObjectCodec codec; + private MessagePackReadContext streamReadContext; + + private boolean isClosed; + private long tokenPosition; + private long currentPosition; + private final IOContext ioContext; + private ExtensionTypeCustomDeserializers extTypeCustomDesers; + private final byte[] tempBytes = new byte[64]; + private final char[] tempChars = new char[64]; + + private enum Type + { + INT, LONG, DOUBLE, STRING, BYTES, BIG_INT, EXT + } + private Type type; + private int intValue; + private long longValue; + private double doubleValue; + private byte[] bytesValue; + private String stringValue; + private BigInteger biValue; + private MessagePackExtensionType extensionTypeValue; + + public MessagePackParser( + IOContext ctxt, + int features, + ObjectCodec objectCodec, + InputStream in, + boolean reuseResourceInParser) + throws IOException + { + this(ctxt, features, new InputStreamBufferInput(in), objectCodec, in, reuseResourceInParser); + } + + public MessagePackParser( + IOContext ctxt, + int features, + ObjectCodec objectCodec, + byte[] bytes, + boolean reuseResourceInParser) + throws IOException + { + this(ctxt, features, new ArrayBufferInput(bytes), objectCodec, bytes, reuseResourceInParser); + } + + private MessagePackParser(IOContext ctxt, + int features, + MessageBufferInput input, + ObjectCodec objectCodec, + Object src, + boolean reuseResourceInParser) + throws IOException + { + super(features, ctxt.streamReadConstraints()); + + this.codec = objectCodec; + ioContext = ctxt; + DupDetector dups = Feature.STRICT_DUPLICATE_DETECTION.enabledIn(features) + ? DupDetector.rootDetector(this) : null; + streamReadContext = MessagePackReadContext.createRootContext(dups); + if (!reuseResourceInParser) { + messageUnpacker = MessagePack.newDefaultUnpacker(input); + return; + } + + Tuple messageUnpackerTuple = messageUnpackerHolder.get(); + if (messageUnpackerTuple == null) { + messageUnpacker = MessagePack.newDefaultUnpacker(input); + } + else { + // Considering to reuse InputStream with JsonParser.Feature.AUTO_CLOSE_SOURCE, + // MessagePackParser needs to use the MessageUnpacker that has the same InputStream + // since it has buffer which has loaded the InputStream data ahead. + // However, it needs to call MessageUnpacker#reset when the source is different from the previous one. + if (isEnabled(JsonParser.Feature.AUTO_CLOSE_SOURCE) || messageUnpackerTuple.first() != src) { + messageUnpackerTuple.second().reset(input); + } + messageUnpacker = messageUnpackerTuple.second(); + } + messageUnpackerHolder.set(new Tuple<>(src, messageUnpacker)); + } + + public void setExtensionTypeCustomDeserializers(ExtensionTypeCustomDeserializers extTypeCustomDesers) + { + this.extTypeCustomDesers = extTypeCustomDesers; + } + + @Override + public ObjectCodec getCodec() + { + return codec; + } + + @Override + public void setCodec(ObjectCodec c) + { + codec = c; + } + + @Override + public Version version() + { + return null; + } + + private String unpackString(MessageUnpacker messageUnpacker) throws IOException + { + int strLen = messageUnpacker.unpackRawStringHeader(); + if (strLen <= tempBytes.length) { + messageUnpacker.readPayload(tempBytes, 0, strLen); + if (STRING_VALUE_FIELD_IS_CHARS.get()) { + for (int i = 0; i < strLen; i++) { + byte b = tempBytes[i]; + if ((0x80 & b) != 0) { + return new String(tempBytes, 0, strLen, StandardCharsets.UTF_8); + } + tempChars[i] = (char) b; + } + return new String(tempChars, 0, strLen); + } + else { + return new String(tempBytes, 0, strLen); + } + } + else { + byte[] bytes = messageUnpacker.readPayload(strLen); + return new String(bytes, 0, strLen, StandardCharsets.UTF_8); + } + } + + @Override + public JsonToken nextToken() throws IOException + { + tokenPosition = messageUnpacker.getTotalReadBytes(); + + boolean isObjectValueSet = streamReadContext.inObject() && _currToken != JsonToken.FIELD_NAME; + if (isObjectValueSet) { + if (!streamReadContext.expectMoreValues()) { + streamReadContext = streamReadContext.getParent(); + return _updateToken(JsonToken.END_OBJECT); + } + } + else if (streamReadContext.inArray()) { + if (!streamReadContext.expectMoreValues()) { + streamReadContext = streamReadContext.getParent(); + return _updateToken(JsonToken.END_ARRAY); + } + } + + if (!messageUnpacker.hasNext()) { + throw new JsonEOFException(this, null, "Unexpected EOF"); + } + + MessageFormat format = messageUnpacker.getNextFormat(); + ValueType valueType = format.getValueType(); + + JsonToken nextToken; + switch (valueType) { + case STRING: + type = Type.STRING; + stringValue = unpackString(messageUnpacker); + if (isObjectValueSet) { + streamReadContext.setCurrentName(stringValue); + nextToken = JsonToken.FIELD_NAME; + } + else { + nextToken = JsonToken.VALUE_STRING; + } + break; + case INTEGER: + Object v; + switch (format) { + case UINT64: + BigInteger bi = messageUnpacker.unpackBigInteger(); + if (0 <= bi.compareTo(LONG_MIN) && bi.compareTo(LONG_MAX) <= 0) { + type = Type.LONG; + longValue = bi.longValue(); + v = longValue; + } + else { + type = Type.BIG_INT; + biValue = bi; + v = biValue; + } + break; + default: + long l = messageUnpacker.unpackLong(); + if (Integer.MIN_VALUE <= l && l <= Integer.MAX_VALUE) { + type = Type.INT; + intValue = (int) l; + v = intValue; + } + else { + type = Type.LONG; + longValue = l; + v = longValue; + } + break; + } + + if (isObjectValueSet) { + streamReadContext.setCurrentName(String.valueOf(v)); + nextToken = JsonToken.FIELD_NAME; + } + else { + nextToken = JsonToken.VALUE_NUMBER_INT; + } + break; + case NIL: + messageUnpacker.unpackNil(); + nextToken = JsonToken.VALUE_NULL; + break; + case BOOLEAN: + boolean b = messageUnpacker.unpackBoolean(); + if (isObjectValueSet) { + streamReadContext.setCurrentName(Boolean.toString(b)); + nextToken = JsonToken.FIELD_NAME; + } + else { + nextToken = b ? JsonToken.VALUE_TRUE : JsonToken.VALUE_FALSE; + } + break; + case FLOAT: + type = Type.DOUBLE; + doubleValue = messageUnpacker.unpackDouble(); + if (isObjectValueSet) { + streamReadContext.setCurrentName(String.valueOf(doubleValue)); + nextToken = JsonToken.FIELD_NAME; + } + else { + nextToken = JsonToken.VALUE_NUMBER_FLOAT; + } + break; + case BINARY: + type = Type.BYTES; + int len = messageUnpacker.unpackBinaryHeader(); + bytesValue = messageUnpacker.readPayload(len); + if (isObjectValueSet) { + streamReadContext.setCurrentName(new String(bytesValue, MessagePack.UTF8)); + nextToken = JsonToken.FIELD_NAME; + } + else { + nextToken = JsonToken.VALUE_EMBEDDED_OBJECT; + } + break; + case ARRAY: + nextToken = JsonToken.START_ARRAY; + streamReadContext = streamReadContext.createChildArrayContext(messageUnpacker.unpackArrayHeader()); + break; + case MAP: + nextToken = JsonToken.START_OBJECT; + streamReadContext = streamReadContext.createChildObjectContext(messageUnpacker.unpackMapHeader()); + break; + case EXTENSION: + type = Type.EXT; + ExtensionTypeHeader header = messageUnpacker.unpackExtensionTypeHeader(); + extensionTypeValue = new MessagePackExtensionType(header.getType(), messageUnpacker.readPayload(header.getLength())); + if (isObjectValueSet) { + streamReadContext.setCurrentName(deserializedExtensionTypeValue().toString()); + nextToken = JsonToken.FIELD_NAME; + } + else { + nextToken = JsonToken.VALUE_EMBEDDED_OBJECT; + } + break; + default: + throw new IllegalStateException("Shouldn't reach here"); + } + currentPosition = messageUnpacker.getTotalReadBytes(); + + _updateToken(nextToken); + + return nextToken; + } + + @Override + protected void _handleEOF() + { + } + + @Override + public String getText() throws IOException + { + switch (type) { + case STRING: + return stringValue; + case BYTES: + return new String(bytesValue, MessagePack.UTF8); + case INT: + return String.valueOf(intValue); + case LONG: + return String.valueOf(longValue); + case DOUBLE: + return String.valueOf(doubleValue); + case BIG_INT: + return String.valueOf(biValue); + case EXT: + return deserializedExtensionTypeValue().toString(); + default: + throw new IllegalStateException("Invalid type=" + type); + } + } + + @Override + public char[] getTextCharacters() throws IOException + { + return getText().toCharArray(); + } + + @Override + public boolean hasTextCharacters() + { + return false; + } + + @Override + public int getTextLength() throws IOException + { + return getText().length(); + } + + @Override + public int getTextOffset() + { + return 0; + } + + @Override + public byte[] getBinaryValue(Base64Variant b64variant) + { + switch (type) { + case BYTES: + return bytesValue; + case STRING: + return stringValue.getBytes(MessagePack.UTF8); + case EXT: + return extensionTypeValue.getData(); + default: + throw new IllegalStateException("Invalid type=" + type); + } + } + + @Override + public Number getNumberValue() + { + switch (type) { + case INT: + return intValue; + case LONG: + return longValue; + case DOUBLE: + return doubleValue; + case BIG_INT: + return biValue; + default: + throw new IllegalStateException("Invalid type=" + type); + } + } + + @Override + public int getIntValue() + { + switch (type) { + case INT: + return intValue; + case LONG: + return (int) longValue; + case DOUBLE: + return (int) doubleValue; + case BIG_INT: + return biValue.intValue(); + default: + throw new IllegalStateException("Invalid type=" + type); + } + } + + @Override + public long getLongValue() + { + switch (type) { + case INT: + return intValue; + case LONG: + return longValue; + case DOUBLE: + return (long) doubleValue; + case BIG_INT: + return biValue.longValue(); + default: + throw new IllegalStateException("Invalid type=" + type); + } + } + + @Override + public BigInteger getBigIntegerValue() + { + switch (type) { + case INT: + return BigInteger.valueOf(intValue); + case LONG: + return BigInteger.valueOf(longValue); + case DOUBLE: + return BigInteger.valueOf((long) doubleValue); + case BIG_INT: + return biValue; + default: + throw new IllegalStateException("Invalid type=" + type); + } + } + + @Override + public float getFloatValue() + { + switch (type) { + case INT: + return (float) intValue; + case LONG: + return (float) longValue; + case DOUBLE: + return (float) doubleValue; + case BIG_INT: + return biValue.floatValue(); + default: + throw new IllegalStateException("Invalid type=" + type); + } + } + + @Override + public double getDoubleValue() + { + switch (type) { + case INT: + return intValue; + case LONG: + return (double) longValue; + case DOUBLE: + return doubleValue; + case BIG_INT: + return biValue.doubleValue(); + default: + throw new IllegalStateException("Invalid type=" + type); + } + } + + @Override + public BigDecimal getDecimalValue() + { + switch (type) { + case INT: + return BigDecimal.valueOf(intValue); + case LONG: + return BigDecimal.valueOf(longValue); + case DOUBLE: + return BigDecimal.valueOf(doubleValue); + case BIG_INT: + return new BigDecimal(biValue); + default: + throw new IllegalStateException("Invalid type=" + type); + } + } + + private Object deserializedExtensionTypeValue() + throws IOException + { + if (extTypeCustomDesers != null) { + ExtensionTypeCustomDeserializers.Deser deser = extTypeCustomDesers.getDeser(extensionTypeValue.getType()); + if (deser != null) { + return deser.deserialize(extensionTypeValue.getData()); + } + } + return extensionTypeValue; + } + + @Override + public Object getEmbeddedObject() throws IOException + { + switch (type) { + case BYTES: + return bytesValue; + case EXT: + return deserializedExtensionTypeValue(); + default: + throw new IllegalStateException("Invalid type=" + type); + } + } + + @Override + public NumberType getNumberType() + { + switch (type) { + case INT: + return NumberType.INT; + case LONG: + return NumberType.LONG; + case DOUBLE: + return NumberType.DOUBLE; + case BIG_INT: + return NumberType.BIG_INTEGER; + default: + throw new IllegalStateException("Invalid type=" + type); + } + } + + @Override + public void close() + throws IOException + { + try { + if (isEnabled(JsonParser.Feature.AUTO_CLOSE_SOURCE)) { + messageUnpacker.close(); + } + } + finally { + isClosed = true; + } + } + + @Override + public boolean isClosed() + { + return isClosed; + } + + @Override + public JsonStreamContext getParsingContext() + { + return streamReadContext; + } + + @Override + public JsonLocation currentTokenLocation() + { + return new JsonLocation(ioContext.contentReference(), tokenPosition, -1, -1); + } + + @Override + public JsonLocation currentLocation() + { + return new JsonLocation(ioContext.contentReference(), currentPosition, -1, -1); + } + + @Override + @Deprecated + public JsonLocation getTokenLocation() + { + return currentTokenLocation(); + } + + @Override + @Deprecated + public JsonLocation getCurrentLocation() + { + return currentLocation(); + } + + @Override + public void overrideCurrentName(String name) + { + // Simple, but need to look for START_OBJECT/ARRAY's "off-by-one" thing: + MessagePackReadContext ctxt = streamReadContext; + if (_currToken == JsonToken.START_OBJECT || _currToken == JsonToken.START_ARRAY) { + ctxt = ctxt.getParent(); + } + // Unfortunate, but since we did not expose exceptions, need to wrap + try { + ctxt.setCurrentName(name); + } + catch (IOException e) { + throw new IllegalStateException(e); + } + } + + @Override + public String currentName() + { + if (_currToken == JsonToken.START_OBJECT || _currToken == JsonToken.START_ARRAY) { + MessagePackReadContext parent = streamReadContext.getParent(); + return parent.getCurrentName(); + } + return streamReadContext.getCurrentName(); + } + + public boolean isCurrentFieldId() + { + return this.type == Type.INT || this.type == Type.LONG; + } + + @Override + @Deprecated + public String getCurrentName() + throws IOException + { + return currentName(); + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java similarity index 55% rename from msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java rename to msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java index ea830626..403f1b36 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java +++ b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java @@ -1,35 +1,27 @@ -// -// 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 tools.jackson.core.TokenStreamContext; -import tools.jackson.core.TokenStreamLocation; -import tools.jackson.core.exc.StreamReadException; -import tools.jackson.core.io.ContentReference; -import tools.jackson.core.json.DupDetector; +import com.fasterxml.jackson.core.JsonLocation; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonStreamContext; +import com.fasterxml.jackson.core.io.CharTypes; +import com.fasterxml.jackson.core.io.ContentReference; +import com.fasterxml.jackson.core.json.DupDetector; /** - * Replacement of {@link tools.jackson.core.json.JsonReadContext} + * Replacement of {@link com.fasterxml.jackson.core.json.JsonReadContext} * to support features needed by MessagePack format. */ public final class MessagePackReadContext - extends TokenStreamContext + extends JsonStreamContext { + /** + * Parent context for this context; null for root context. + */ protected final MessagePackReadContext parent; + // // // Optional duplicate detection + protected final DupDetector dups; /** @@ -37,12 +29,26 @@ public final class MessagePackReadContext */ protected int expEntryCount; + // // // Location information (minus source reference) + protected String currentName; protected Object currentValue; + /* + /********************************************************** + /* Simple instance reuse slots + /********************************************************** + */ + protected MessagePackReadContext child = null; + /* + /********************************************************** + /* Instance construction, reuse + /********************************************************** + */ + public MessagePackReadContext(MessagePackReadContext parent, DupDetector dups, int type, int expEntryCount) { @@ -68,17 +74,19 @@ protected void reset(int type, int expEntryCount) } @Override - public Object currentValue() + public Object getCurrentValue() { return currentValue; } @Override - public void assignCurrentValue(Object v) + public void setCurrentValue(Object v) { currentValue = v; } + // // // Factory methods + public static MessagePackReadContext createRootContext(DupDetector dups) { return new MessagePackReadContext(null, dups, TYPE_ROOT, -1); @@ -113,8 +121,14 @@ public MessagePackReadContext createChildObjectContext(int expEntryCount) return ctxt; } + /* + /********************************************************** + /* Abstract method implementation + /********************************************************** + */ + @Override - public String currentName() + public String getCurrentName() { return currentName; } @@ -125,6 +139,12 @@ public MessagePackReadContext getParent() return parent; } + /* + /********************************************************** + /* Extended API + /********************************************************** + */ + public boolean hasExpectedLength() { return (expEntryCount >= 0); @@ -143,6 +163,7 @@ public boolean isEmpty() public int getRemainingExpectedLength() { int diff = expEntryCount - _index; + // Negative values would occur when expected count is -1 return Math.max(0, diff); } @@ -151,6 +172,15 @@ public boolean acceptsBreakMarker() return (expEntryCount < 0) && _type != TYPE_ROOT; } + /** + * Method called to increment the current entry count (Object property, Array + * element or Root value) for this context level + * and then see if more entries are accepted. + * The only case where more entries are NOT expected is for fixed-count + * Objects and Arrays that just reached the entry count. + *

+ * Note that since the entry count is updated this is a state-changing method. + */ public boolean expectMoreValues() { if (++_index == expEntryCount) { @@ -159,12 +189,30 @@ public boolean expectMoreValues() return true; } - public TokenStreamLocation startLocation(ContentReference srcRef) + /** + * @return Location pointing to the point where the context + * start marker was found + */ + @Override + public JsonLocation startLocation(ContentReference srcRef) + { + return new JsonLocation(srcRef, 1L, -1, -1); + } + + @Override + @Deprecated // since 2.13 + public JsonLocation getStartLocation(Object rawSrc) { - return new TokenStreamLocation(srcRef, 1L, -1, -1); + return startLocation(ContentReference.rawReference(rawSrc)); } - public void setCurrentName(String name) + /* + /********************************************************** + /* State changes + /********************************************************** + */ + + public void setCurrentName(String name) throws JsonProcessingException { currentName = name; if (dups != null) { @@ -172,16 +220,24 @@ public void setCurrentName(String name) } } - private void _checkDup(DupDetector dd, String name) + private void _checkDup(DupDetector dd, String name) throws JsonProcessingException { - // Null names (nil map keys) cannot be duplicate-checked via DupDetector - // because DupDetector.isDup(null) NPEs when a prior non-null name exists. - if (name != null && dd.isDup(name)) { - throw new StreamReadException(null, + if (dd.isDup(name)) { + throw new JsonParseException(null, "Duplicate field '" + name + "'", dd.findLocation()); } } + /* + /********************************************************** + /* Overridden standard methods + /********************************************************** + */ + + /** + * Overridden to provide developer readable "JsonPath" representation + * of the context. + */ @Override public String toString() { @@ -199,7 +255,7 @@ public String toString() sb.append('{'); if (currentName != null) { sb.append('"'); - sb.append(currentName.replace("\\", "\\\\").replace("\"", "\\\"")); + CharTypes.appendQuoted(sb, currentName); sb.append('"'); } else { diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java similarity index 56% rename from msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java rename to msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java index 8bff3000..72ed5d8d 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java +++ b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java @@ -15,9 +15,8 @@ // package org.msgpack.jackson.dataformat; -import tools.jackson.core.SerializableString; +import com.fasterxml.jackson.core.SerializableString; -import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -37,102 +36,79 @@ public MessagePackSerializedString(Object value) @Override public String getValue() { - return value == null ? null : value.toString(); + return value.toString(); } @Override public int charLength() { - String v = getValue(); - return v == null ? 0 : v.length(); + return getValue().length(); } @Override public char[] asQuotedChars() { - String v = getValue(); - return v == null ? new char[0] : v.toCharArray(); + return getValue().toCharArray(); } @Override public byte[] asUnquotedUTF8() { - String v = getValue(); - return v == null ? new byte[0] : v.getBytes(UTF8); + return getValue().getBytes(UTF8); } @Override public byte[] asQuotedUTF8() { - return asUnquotedUTF8(); + return ("\"" + getValue() + "\"").getBytes(UTF8); } @Override public int appendQuotedUTF8(byte[] bytes, int i) { - return appendUnquotedUTF8(bytes, i); + return 0; } @Override public int appendQuoted(char[] chars, int i) { - return appendUnquoted(chars, i); + return 0; } @Override public int appendUnquotedUTF8(byte[] bytes, int i) { - byte[] utf8 = asUnquotedUTF8(); - if (utf8.length > bytes.length - i) { - return -1; - } - System.arraycopy(utf8, 0, bytes, i, utf8.length); - return utf8.length; + return 0; } @Override public int appendUnquoted(char[] chars, int i) { - String v = getValue(); - if (v == null) { - return 0; - } - if (v.length() > chars.length - i) { - return -1; - } - v.getChars(0, v.length(), chars, i); - return v.length(); + return 0; } @Override - public int writeQuotedUTF8(OutputStream outputStream) throws IOException + public int writeQuotedUTF8(OutputStream outputStream) { - return writeUnquotedUTF8(outputStream); + return 0; } @Override - public int writeUnquotedUTF8(OutputStream outputStream) throws IOException + public int writeUnquotedUTF8(OutputStream outputStream) { - byte[] utf8 = asUnquotedUTF8(); - outputStream.write(utf8); - return utf8.length; + return 0; } @Override public int putQuotedUTF8(ByteBuffer byteBuffer) { - return putUnquotedUTF8(byteBuffer); + return 0; } @Override public int putUnquotedUTF8(ByteBuffer byteBuffer) { - byte[] utf8 = asUnquotedUTF8(); - if (utf8.length > byteBuffer.remaining()) { - return -1; - } - byteBuffer.put(utf8); - return utf8.length; + return 0; } public Object getRawValue() diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java new file mode 100644 index 00000000..4100ef4c --- /dev/null +++ b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java @@ -0,0 +1,59 @@ +// +// 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.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.cfg.SerializerFactoryConfig; +import com.fasterxml.jackson.databind.ser.BeanSerializerFactory; + +public class MessagePackSerializerFactory + extends BeanSerializerFactory +{ + /** + * Constructor for creating instances without configuration. + */ + public MessagePackSerializerFactory() + { + super(null); + } + + /** + * Constructor for creating instances with specified configuration. + * + * @param config + */ + public MessagePackSerializerFactory(SerializerFactoryConfig config) + { + super(config); + } + + @Override + public JsonSerializer createKeySerializer(SerializerProvider prov, JavaType keyType, JsonSerializer defaultImpl) throws JsonMappingException + { + return new MessagePackKeySerializer(); + } + + @Override + @Deprecated + public JsonSerializer createKeySerializer(SerializationConfig config, JavaType keyType, JsonSerializer defaultImpl) + { + return new MessagePackKeySerializer(); + } +} diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java new file mode 100755 index 00000000..2216d276 --- /dev/null +++ b/msgpack-jackson2/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-jackson3/src/main/java/org/msgpack/jackson/dataformat/Tuple.java b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/Tuple.java similarity index 95% rename from msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/Tuple.java rename to msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/Tuple.java index b0f720d8..1a252739 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/Tuple.java +++ b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/Tuple.java @@ -15,6 +15,9 @@ // package org.msgpack.jackson.dataformat; +/** + * Created by komamitsu on 5/28/15. + */ public class Tuple { private final F first; diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java similarity index 76% rename from msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java rename to msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java index 6a194a8c..5414b0bd 100644 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java +++ b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java @@ -15,18 +15,18 @@ // package org.msgpack.jackson.dataformat; -import tools.jackson.core.JsonGenerator; -import tools.jackson.core.JsonParser; -import tools.jackson.core.JacksonException; -import tools.jackson.core.TreeNode; -import tools.jackson.core.type.TypeReference; -import tools.jackson.databind.DeserializationContext; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.SerializationContext; -import tools.jackson.databind.ValueDeserializer; -import tools.jackson.databind.ValueSerializer; -import tools.jackson.databind.annotation.JsonDeserialize; -import tools.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.junit.Test; import java.io.IOException; @@ -90,42 +90,38 @@ public Map getObjects() } static class ObjectContainerSerializer - extends ValueSerializer + extends JsonSerializer { @Override - public void serialize(ObjectContainer value, JsonGenerator gen, SerializationContext serializers) - throws JacksonException + public void serialize(ObjectContainer value, JsonGenerator gen, SerializerProvider serializers) + throws IOException, JsonProcessingException { gen.writeStartObject(); HashMap metadata = new HashMap(); for (Map.Entry entry : value.getObjects().entrySet()) { metadata.put(entry.getKey(), entry.getValue().getClass().getName()); } - gen.writePOJOProperty("__metadata", metadata); - gen.writePOJOProperty("objects", value.getObjects()); + gen.writeObjectField("__metadata", metadata); + gen.writeObjectField("objects", value.getObjects()); gen.writeEndObject(); } } static class ObjectContainerDeserializer - extends ValueDeserializer + extends JsonDeserializer { @Override public ObjectContainer deserialize(JsonParser p, DeserializationContext ctxt) - throws JacksonException + throws IOException, JsonProcessingException { ObjectContainer objectContainer = new ObjectContainer(new HashMap()); TreeNode treeNode = p.readValueAsTree(); - Map metadata = treeNode.get("__metadata") - .traverse(p.objectReadContext()) - .readValueAs(new TypeReference>() {}); + Map metadata = treeNode.get("__metadata").traverse(p.getCodec()).readValueAs(new TypeReference>() {}); TreeNode dataMapTree = treeNode.get("objects"); for (Map.Entry entry : metadata.entrySet()) { try { - Object o = dataMapTree.get(entry.getKey()) - .traverse(p.objectReadContext()) - .readValueAs(Class.forName(entry.getValue())); + Object o = dataMapTree.get(entry.getKey()).traverse(p.getCodec()).readValueAs(Class.forName(entry.getValue())); objectContainer.getObjects().put(entry.getKey(), o); } catch (ClassNotFoundException e) { @@ -155,7 +151,7 @@ public void test() objectContainer.getObjects().put("pi", pi); } - ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory()); + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); byte[] bytes = objectMapper.writeValueAsBytes(objectContainer); ObjectContainer restored = objectMapper.readValue(bytes, ObjectContainer.class); diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java similarity index 70% rename from msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java rename to msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java index 23983a39..bbac6fb9 100644 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java +++ b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java @@ -15,28 +15,27 @@ // package org.msgpack.jackson.dataformat; -import tools.jackson.core.JsonParser; -import tools.jackson.core.JacksonException; -import tools.jackson.core.type.TypeReference; -import tools.jackson.databind.DeserializationContext; -import tools.jackson.databind.KeyDeserializer; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.ValueDeserializer; -import tools.jackson.databind.deser.NullValueProvider; -import tools.jackson.databind.deser.jdk.JDKValueInstantiators; -import tools.jackson.databind.deser.jdk.MapDeserializer; -import tools.jackson.databind.jsontype.TypeDeserializer; -import tools.jackson.databind.module.SimpleModule; -import tools.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.NullValueProvider; +import com.fasterxml.jackson.databind.deser.impl.JDKValueInstantiators; +import com.fasterxml.jackson.databind.deser.std.MapDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.type.TypeFactory; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.LinkedHashMap; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class MessagePackDataformatForFieldIdTest { @@ -46,7 +45,7 @@ static class MessagePackMapDeserializer extends MapDeserializer { @Override public Object deserializeKey(String s, DeserializationContext deserializationContext) - throws JacksonException + throws IOException { JsonParser parser = deserializationContext.getParser(); if (parser instanceof MessagePackParser) { @@ -62,13 +61,13 @@ public Object deserializeKey(String s, DeserializationContext deserializationCon public MessagePackMapDeserializer() { super( - TypeFactory.createDefaultInstance().constructMapType(Map.class, Object.class, Object.class), + TypeFactory.defaultInstance().constructMapType(Map.class, Object.class, Object.class), JDKValueInstantiators.findStdValueInstantiator(null, LinkedHashMap.class), keyDeserializer, null, null); } public MessagePackMapDeserializer(MapDeserializer src, KeyDeserializer keyDeser, - ValueDeserializer valueDeser, TypeDeserializer valueTypeDeser, NullValueProvider nuller, + JsonDeserializer valueDeser, TypeDeserializer valueTypeDeser, NullValueProvider nuller, Set ignorable, Set includable) { super(src, keyDeser, valueDeser, valueTypeDeser, nuller, ignorable, includable); @@ -76,10 +75,10 @@ public MessagePackMapDeserializer(MapDeserializer src, KeyDeserializer keyDeser, @Override protected MapDeserializer withResolved(KeyDeserializer keyDeser, TypeDeserializer valueTypeDeser, - ValueDeserializer valueDeser, NullValueProvider nuller, Set ignorable, + JsonDeserializer valueDeser, NullValueProvider nuller, Set ignorable, Set includable) { - return new MessagePackMapDeserializer(this, keyDeser, (ValueDeserializer) valueDeser, valueTypeDeser, + return new MessagePackMapDeserializer(this, keyDeser, (JsonDeserializer) valueDeser, valueTypeDeser, nuller, ignorable, includable); } } @@ -88,13 +87,12 @@ protected MapDeserializer withResolved(KeyDeserializer keyDeser, TypeDeserialize public void testMixedKeys() throws IOException { - ObjectMapper mapper = MessagePackMapper.builder( + ObjectMapper mapper = new ObjectMapper( new MessagePackFactory() .setSupportIntegerKeys(true) ) - .addModule(new SimpleModule() - .addDeserializer(Map.class, new MessagePackMapDeserializer())) - .build(); + .registerModule(new SimpleModule() + .addDeserializer(Map.class, new MessagePackMapDeserializer())); Map map = new HashMap<>(); map.put(1, "one"); @@ -110,13 +108,12 @@ public void testMixedKeys() } @Test - public void testMixedKeysBackwardsCompatible() + public void testMixedKeysBackwardsCompatiable() throws IOException { - ObjectMapper mapper = MessagePackMapper.builder(new MessagePackFactory()) - .addModule(new SimpleModule() - .addDeserializer(Map.class, new MessagePackMapDeserializer())) - .build(); + ObjectMapper mapper = new ObjectMapper(new MessagePackFactory()) + .registerModule(new SimpleModule() + .addDeserializer(Map.class, new MessagePackMapDeserializer())); Map map = new HashMap<>(); map.put(1, "one"); diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java similarity index 89% rename from msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java rename to msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java index c8083189..e899e4f4 100644 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java +++ b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java @@ -15,8 +15,8 @@ // package org.msgpack.jackson.dataformat; -import tools.jackson.databind.ObjectMapper; -import org.junit.Test; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; import java.io.IOException; import java.nio.charset.Charset; @@ -24,8 +24,8 @@ import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.containsString; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.hamcrest.MatcherAssert.assertThat; public class MessagePackDataformatForPojoTest @@ -45,7 +45,7 @@ public void testNormal() assertEquals(normalPojo.d, value.d, 0.000001f); assertArrayEquals(normalPojo.b, value.b); assertEquals(normalPojo.bi, value.bi); - assertEquals(normalPojo.suit, value.suit); + assertEquals(normalPojo.suit, Suit.HEART); assertEquals(normalPojo.sMultibyte, value.sMultibyte); } @@ -135,12 +135,11 @@ public void testChangingPropertyNames() public void testSerializationWithoutSchema() throws IOException { - ObjectMapper objectMapper = MessagePackMapper.builder(factory) - .annotationIntrospector(new JsonArrayFormat()) - .build(); + ObjectMapper objectMapper = new ObjectMapper(factory); // to not affect shared objectMapper state + objectMapper.setAnnotationIntrospector(new JsonArrayFormat()); byte[] bytes = objectMapper.writeValueAsBytes(complexPojo); - String schema = new String(bytes, Charset.forName("UTF-8")); - assertThat(schema, not(containsString("name"))); + String scheme = new String(bytes, Charset.forName("UTF-8")); + assertThat(scheme, not(containsString("name"))); // validating schema doesn't contains keys, that's just array ComplexPojo value = objectMapper.readValue(bytes, ComplexPojo.class); assertEquals("komamitsu", value.name); assertEquals(20, value.age); diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java similarity index 94% rename from msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java rename to msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java index 8a7f1e52..b9ef4cf3 100644 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java +++ b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java @@ -18,7 +18,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import tools.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.After; import org.junit.Before; @@ -53,7 +53,7 @@ public class MessagePackDataformatTestBase public void setup() { factory = new MessagePackFactory(); - objectMapper = new MessagePackMapper(factory); + objectMapper = new ObjectMapper(factory); out = new ByteArrayOutputStream(); in = new ByteArrayInputStream(new byte[4096]); @@ -223,9 +223,11 @@ public static class IgnoringPropertiesPojo { int code; + // will not be written as JSON; nor assigned from JSON: @JsonIgnore public String internal; + // no annotation, public field is read/written normally public String external; @JsonIgnore @@ -234,6 +236,7 @@ public void setCode(int c) code = c; } + // note: will also be ignored because setter has annotation! public int getCode() { return code; @@ -244,12 +247,15 @@ public static class ChangingPropertyNamesPojo { String name; + // without annotation, we'd get "theName", but we want "name": @JsonProperty("name") public String getTheName() { return name; } + // note: it is enough to add annotation on just getter OR setter; + // so we can omit it here public void setTheName(String n) { name = n; diff --git a/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java new file mode 100644 index 00000000..9e376541 --- /dev/null +++ b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java @@ -0,0 +1,162 @@ +// +// 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.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +import org.junit.jupiter.api.Test; +import org.msgpack.core.MessagePack; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.hamcrest.MatcherAssert.assertThat; + +public class MessagePackFactoryTest + extends MessagePackDataformatTestBase +{ + @Test + public void testCreateGenerator() + throws IOException + { + JsonEncoding enc = JsonEncoding.UTF8; + JsonGenerator generator = factory.createGenerator(out, enc); + assertEquals(MessagePackGenerator.class, generator.getClass()); + } + + @Test + public void testCreateParser() + throws IOException + { + JsonParser parser = factory.createParser(in); + assertEquals(MessagePackParser.class, parser.getClass()); + } + + private void assertCopy(boolean advancedConfig) + throws IOException + { + // Build base ObjectMapper + ObjectMapper objectMapper; + if (advancedConfig) { + ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); + extTypeCustomDesers.addCustomDeser((byte) 42, + new ExtensionTypeCustomDeserializers.Deser() + { + @Override + public Object deserialize(byte[] data) + throws IOException + { + TinyPojo pojo = new TinyPojo(); + pojo.t = new String(data); + return pojo; + } + } + ); + + MessagePack.PackerConfig msgpackPackerConfig = new MessagePack.PackerConfig().withStr8FormatSupport(false); + + MessagePackFactory messagePackFactory = new MessagePackFactory(msgpackPackerConfig); + messagePackFactory.setExtTypeCustomDesers(extTypeCustomDesers); + + objectMapper = new ObjectMapper(messagePackFactory); + + objectMapper.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false); + objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false); + + objectMapper.setAnnotationIntrospector(new JsonArrayFormat()); + } + else { + MessagePackFactory messagePackFactory = new MessagePackFactory(); + objectMapper = new ObjectMapper(messagePackFactory); + } + + // Use the original ObjectMapper in advance + { + byte[] bytes = objectMapper.writeValueAsBytes(1234); + assertThat(objectMapper.readValue(bytes, Integer.class), is(1234)); + } + + // Copy the ObjectMapper + ObjectMapper copiedObjectMapper = objectMapper.copy(); + + // Assert the copied ObjectMapper + JsonFactory copiedFactory = copiedObjectMapper.getFactory(); + assertThat(copiedFactory, is(instanceOf(MessagePackFactory.class))); + MessagePackFactory copiedMessagePackFactory = (MessagePackFactory) copiedFactory; + + Collection annotationIntrospectors = + copiedObjectMapper.getSerializationConfig().getAnnotationIntrospector().allIntrospectors(); + assertThat(annotationIntrospectors.size(), is(1)); + + if (advancedConfig) { + assertThat(copiedMessagePackFactory.getPackerConfig().isStr8FormatSupport(), is(false)); + + assertThat(copiedMessagePackFactory.getExtTypeCustomDesers().getDeser((byte) 42), is(notNullValue())); + assertThat(copiedMessagePackFactory.getExtTypeCustomDesers().getDeser((byte) 43), is(nullValue())); + + assertThat(copiedMessagePackFactory.isEnabled(JsonGenerator.Feature.AUTO_CLOSE_TARGET), is(false)); + assertThat(copiedMessagePackFactory.isEnabled(JsonParser.Feature.AUTO_CLOSE_SOURCE), is(false)); + + assertThat(annotationIntrospectors.stream().findFirst().get(), is(instanceOf(JsonArrayFormat.class))); + } + else { + assertThat(copiedMessagePackFactory.getPackerConfig().isStr8FormatSupport(), is(true)); + + assertThat(copiedMessagePackFactory.getExtTypeCustomDesers(), is(nullValue())); + + assertThat(copiedMessagePackFactory.isEnabled(JsonGenerator.Feature.AUTO_CLOSE_TARGET), is(true)); + assertThat(copiedMessagePackFactory.isEnabled(JsonParser.Feature.AUTO_CLOSE_SOURCE), is(true)); + + assertThat(annotationIntrospectors.stream().findFirst().get(), + is(instanceOf(JacksonAnnotationIntrospector.class))); + } + + // Check the copied ObjectMapper works fine + Map map = new HashMap<>(); + map.put("one", 1); + Map deserialized = copiedObjectMapper + .readValue(objectMapper.writeValueAsBytes(map), new TypeReference>() {}); + assertThat(deserialized.size(), is(1)); + assertThat(deserialized.get("one"), is(1)); + } + + @Test + public void copyWithDefaultConfig() + throws IOException + { + assertCopy(false); + } + + @Test + public void copyWithAdvancedConfig() + throws IOException + { + assertCopy(true); + } +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java similarity index 53% rename from msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java rename to msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java index 2d593846..a13951b2 100644 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java +++ b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java @@ -16,23 +16,15 @@ package org.msgpack.jackson.dataformat; import com.fasterxml.jackson.annotation.JsonProperty; -import tools.jackson.core.JsonGenerator; -import tools.jackson.core.JsonParser; -import tools.jackson.core.JsonToken; -import tools.jackson.core.JsonEncoding; -import tools.jackson.core.JacksonException; -import tools.jackson.core.ObjectReadContext; -import tools.jackson.core.ObjectWriteContext; -import tools.jackson.core.StreamWriteFeature; -import tools.jackson.core.TokenStreamContext; -import tools.jackson.core.type.TypeReference; -import tools.jackson.databind.DeserializationFeature; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.SerializationContext; -import tools.jackson.databind.ValueSerializer; -import tools.jackson.databind.annotation.JsonSerialize; -import tools.jackson.databind.module.SimpleModule; -import org.junit.Test; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.junit.jupiter.api.Test; import org.msgpack.core.ExtensionTypeHeader; import org.msgpack.core.MessagePack; import org.msgpack.core.MessageUnpacker; @@ -44,13 +36,13 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -61,14 +53,13 @@ import java.util.concurrent.TimeUnit; import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.hamcrest.MatcherAssert.assertThat; public class MessagePackGeneratorTest @@ -262,7 +253,7 @@ public void testMessagePackGeneratorDirectly() MessagePackFactory messagePackFactory = new MessagePackFactory(); File tempFile = createTempFile(); - JsonGenerator generator = messagePackFactory.createGenerator(ObjectWriteContext.empty(), tempFile, JsonEncoding.UTF8); + JsonGenerator generator = messagePackFactory.createGenerator(tempFile, JsonEncoding.UTF8); assertTrue(generator instanceof MessagePackGenerator); generator.writeStartArray(); generator.writeNumber(0); @@ -289,7 +280,7 @@ public void testWritePrimitives() MessagePackFactory messagePackFactory = new MessagePackFactory(); File tempFile = createTempFile(); - JsonGenerator generator = messagePackFactory.createGenerator(ObjectWriteContext.empty(), tempFile, JsonEncoding.UTF8); + JsonGenerator generator = messagePackFactory.createGenerator(tempFile, JsonEncoding.UTF8); assertTrue(generator instanceof MessagePackGenerator); generator.writeNumber(0); generator.writeString("one"); @@ -313,7 +304,7 @@ public void testWritePrimitives() public void testBigDecimal() throws IOException { - ObjectMapper mapper = new MessagePackMapper(new MessagePackFactory()); + ObjectMapper mapper = new ObjectMapper(new MessagePackFactory()); { double d0 = 1.23456789; @@ -359,38 +350,16 @@ public void testBigDecimal() } } - @Test - public void testBigDecimalCompareTo() - throws IOException - { - ObjectMapper mapper = new MessagePackMapper(new MessagePackFactory()); - - // BigDecimal with trailing zeros is representable as double — must not throw - BigDecimal trailingZeros = new BigDecimal("1.50"); - byte[] bytes = mapper.writeValueAsBytes(trailingZeros); - MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes); - assertEquals(1.5, unpacker.unpackDouble(), 0.0); - - // BigDecimal with precision beyond double range must throw - BigDecimal tooHighPrecision = new BigDecimal("1.00000000000000000000000000000000000001"); - try { - mapper.writeValueAsBytes(tooHighPrecision); - assertTrue(false); - } - catch (IllegalArgumentException e) { - assertTrue(true); - } - } - @Test public void testEnableFeatureAutoCloseTarget() throws IOException { OutputStream out = createTempFileOutputStream(); - ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory()); + MessagePackFactory messagePackFactory = new MessagePackFactory(); + ObjectMapper objectMapper = new ObjectMapper(messagePackFactory); List integers = Arrays.asList(1); objectMapper.writeValue(out, integers); - assertThrows(JacksonException.class, () -> { + assertThrows(IOException.class, () -> { objectMapper.writeValue(out, integers); }); } @@ -401,9 +370,9 @@ public void testDisableFeatureAutoCloseTarget() { File tempFile = createTempFile(); OutputStream out = new FileOutputStream(tempFile); - ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) - .build(); + MessagePackFactory messagePackFactory = new MessagePackFactory(); + ObjectMapper objectMapper = new ObjectMapper(messagePackFactory); + objectMapper.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false); List integers = Arrays.asList(1); objectMapper.writeValue(out, integers); objectMapper.writeValue(out, integers); @@ -422,9 +391,8 @@ public void testWritePrimitiveObjectViaObjectMapper() { File tempFile = createTempFile(); try (OutputStream out = Files.newOutputStream(tempFile.toPath())) { - ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) - .build(); + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + objectMapper.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false); objectMapper.writeValue(out, 1); objectMapper.writeValue(out, "two"); objectMapper.writeValue(out, 3.14); @@ -449,9 +417,8 @@ public void testInMultiThreads() int threadCount = 8; final int loopCount = 4000; ExecutorService executorService = Executors.newFixedThreadPool(threadCount); - final ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) - .build(); + final ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + objectMapper.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false); final List buffers = new ArrayList(threadCount); List> results = new ArrayList>(); @@ -470,7 +437,7 @@ public Exception call() } return null; } - catch (Exception e) { + catch (IOException e) { return e; } } @@ -500,12 +467,14 @@ public void testDisableStr8Support() { String str8LengthString = new String(new char[32]).replace("\0", "a"); - ObjectMapper defaultMapper = new MessagePackMapper(new MessagePackFactory()); + // Test that produced value having str8 format + ObjectMapper defaultMapper = new ObjectMapper(new MessagePackFactory()); byte[] resultWithStr8Format = defaultMapper.writeValueAsBytes(str8LengthString); assertEquals(resultWithStr8Format[0], MessagePack.Code.STR8); + // Test that produced value does not having str8 format MessagePack.PackerConfig config = new MessagePack.PackerConfig().withStr8FormatSupport(false); - ObjectMapper mapperWithConfig = new MessagePackMapper(new MessagePackFactory(config)); + ObjectMapper mapperWithConfig = new ObjectMapper(new MessagePackFactory(config)); byte[] resultWithoutStr8Format = mapperWithConfig.writeValueAsBytes(str8LengthString); assertNotEquals(resultWithoutStr8Format[0], MessagePack.Code.STR8); } @@ -689,7 +658,7 @@ public void setBigIntMap(Map bigIntMap) @Test @SuppressWarnings("unchecked") public void testNonStringKey() - throws Exception + throws IOException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { for (Class clazz : Arrays.asList( @@ -702,16 +671,11 @@ public void testNonStringKey() mapHolder.getDoubleMap().put(Double.MIN_VALUE, "d"); mapHolder.getBigIntMap().put(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE), "bi"); - ObjectMapper objectMapper; + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); if (mapHolder instanceof NonStringKeyMapHolderWithoutAnnotation) { SimpleModule mod = new SimpleModule("test"); mod.addKeySerializer(Object.class, new MessagePackKeySerializer()); - objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .addModule(mod) - .build(); - } - else { - objectMapper = new MessagePackMapper(new MessagePackFactory()); + objectMapper.registerModule(mod); } byte[] bytes = objectMapper.writeValueAsBytes(mapHolder); @@ -756,11 +720,10 @@ public void testComplexTypeKey() pojo.t = "foo"; map.put(pojo, 42); + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); SimpleModule mod = new SimpleModule("test"); mod.addKeySerializer(TinyPojo.class, new MessagePackKeySerializer()); - ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .addModule(mod) - .build(); + objectMapper.registerModule(mod); byte[] bytes = objectMapper.writeValueAsBytes(map); MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes); @@ -780,12 +743,11 @@ public void testComplexTypeKeyWithV06Format() pojo.t = "foo"; map.put(pojo, 42); + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + objectMapper.setAnnotationIntrospector(new JsonArrayFormat()); SimpleModule mod = new SimpleModule("test"); mod.addKeySerializer(TinyPojo.class, new MessagePackKeySerializer()); - ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .annotationIntrospector(new JsonArrayFormat()) - .addModule(mod) - .build(); + objectMapper.registerModule(mod); byte[] bytes = objectMapper.writeValueAsBytes(map); MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes); @@ -795,12 +757,14 @@ public void testComplexTypeKeyWithV06Format() assertThat(unpacker.unpackInt(), is(42)); } + // Test serializers that store a string as a number + public static class IntegerSerializerStoringAsString - extends ValueSerializer + extends JsonSerializer { @Override - public void serialize(Integer value, JsonGenerator gen, SerializationContext serializers) - throws JacksonException + public void serialize(Integer value, JsonGenerator gen, SerializerProvider serializers) + throws IOException, JsonProcessingException { gen.writeNumber(String.valueOf(value)); } @@ -810,9 +774,9 @@ public void serialize(Integer value, JsonGenerator gen, SerializationContext ser public void serializeStringAsInteger() throws IOException { - ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .addModule(new SimpleModule().addSerializer(Integer.class, new IntegerSerializerStoringAsString())) - .build(); + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + objectMapper.registerModule( + new SimpleModule().addSerializer(Integer.class, new IntegerSerializerStoringAsString())); assertThat( MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(Integer.MAX_VALUE)).unpackInt(), @@ -820,11 +784,11 @@ public void serializeStringAsInteger() } public static class LongSerializerStoringAsString - extends ValueSerializer + extends JsonSerializer { @Override - public void serialize(Long value, JsonGenerator gen, SerializationContext serializers) - throws JacksonException + public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) + throws IOException, JsonProcessingException { gen.writeNumber(String.valueOf(value)); } @@ -834,9 +798,9 @@ public void serialize(Long value, JsonGenerator gen, SerializationContext serial public void serializeStringAsLong() throws IOException { - ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .addModule(new SimpleModule().addSerializer(Long.class, new LongSerializerStoringAsString())) - .build(); + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + objectMapper.registerModule( + new SimpleModule().addSerializer(Long.class, new LongSerializerStoringAsString())); assertThat( MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(Long.MIN_VALUE)).unpackLong(), @@ -844,11 +808,11 @@ public void serializeStringAsLong() } public static class FloatSerializerStoringAsString - extends ValueSerializer + extends JsonSerializer { @Override - public void serialize(Float value, JsonGenerator gen, SerializationContext serializers) - throws JacksonException + public void serialize(Float value, JsonGenerator gen, SerializerProvider serializers) + throws IOException, JsonProcessingException { gen.writeNumber(String.valueOf(value)); } @@ -858,9 +822,9 @@ public void serialize(Float value, JsonGenerator gen, SerializationContext seria public void serializeStringAsFloat() throws IOException { - ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .addModule(new SimpleModule().addSerializer(Float.class, new FloatSerializerStoringAsString())) - .build(); + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + objectMapper.registerModule( + new SimpleModule().addSerializer(Float.class, new FloatSerializerStoringAsString())); assertThat( MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(Float.MAX_VALUE)).unpackFloat(), @@ -868,11 +832,11 @@ public void serializeStringAsFloat() } public static class DoubleSerializerStoringAsString - extends ValueSerializer + extends JsonSerializer { @Override - public void serialize(Double value, JsonGenerator gen, SerializationContext serializers) - throws JacksonException + public void serialize(Double value, JsonGenerator gen, SerializerProvider serializers) + throws IOException, JsonProcessingException { gen.writeNumber(String.valueOf(value)); } @@ -882,9 +846,9 @@ public void serialize(Double value, JsonGenerator gen, SerializationContext seri public void serializeStringAsDouble() throws IOException { - ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .addModule(new SimpleModule().addSerializer(Double.class, new DoubleSerializerStoringAsString())) - .build(); + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + objectMapper.registerModule( + new SimpleModule().addSerializer(Double.class, new DoubleSerializerStoringAsString())); assertThat( MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(Double.MIN_VALUE)).unpackDouble(), @@ -892,11 +856,11 @@ public void serializeStringAsDouble() } public static class BigDecimalSerializerStoringAsString - extends ValueSerializer + extends JsonSerializer { @Override - public void serialize(BigDecimal value, JsonGenerator gen, SerializationContext serializers) - throws JacksonException + public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider serializers) + throws IOException, JsonProcessingException { gen.writeNumber(String.valueOf(value)); } @@ -906,22 +870,22 @@ public void serialize(BigDecimal value, JsonGenerator gen, SerializationContext public void serializeStringAsBigDecimal() throws IOException { - ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .addModule(new SimpleModule().addSerializer(BigDecimal.class, new BigDecimalSerializerStoringAsString())) - .build(); + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + objectMapper.registerModule( + new SimpleModule().addSerializer(BigDecimal.class, new BigDecimalSerializerStoringAsString())); BigDecimal bd = BigDecimal.valueOf(Long.MAX_VALUE).add(BigDecimal.ONE); assertThat( - MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(bd)).unpackBigInteger(), - is(bd.toBigIntegerExact())); + MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(bd)).unpackDouble(), + is(bd.doubleValue())); } public static class BigIntegerSerializerStoringAsString - extends ValueSerializer + extends JsonSerializer { @Override - public void serialize(BigInteger value, JsonGenerator gen, SerializationContext serializers) - throws JacksonException + public void serialize(BigInteger value, JsonGenerator gen, SerializerProvider serializers) + throws IOException, JsonProcessingException { gen.writeNumber(String.valueOf(value)); } @@ -931,21 +895,22 @@ public void serialize(BigInteger value, JsonGenerator gen, SerializationContext public void serializeStringAsBigInteger() throws IOException { - ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .addModule(new SimpleModule().addSerializer(BigInteger.class, new BigIntegerSerializerStoringAsString())) - .build(); + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + objectMapper.registerModule( + new SimpleModule().addSerializer(BigInteger.class, new BigIntegerSerializerStoringAsString())); BigInteger bi = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE); assertThat( - MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(bi)).unpackBigInteger(), - is(bi)); + MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(bi)).unpackDouble(), + is(bi.doubleValue())); } @Test public void testNestedSerialization() throws Exception { - ObjectMapper objectMapper = new MessagePackMapper( - new MessagePackFactory().setReuseResourceInGenerator(false)); + // The purpose of this test is to confirm if MessagePackFactory.setReuseResourceInGenerator(false) + // works as a workaround for https://github.com/msgpack/msgpack-java/issues/508 + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory().setReuseResourceInGenerator(false)); OuterClass outerClass = objectMapper.readValue( objectMapper.writeValueAsBytes(new OuterClass("Foo")), OuterClass.class); @@ -962,8 +927,10 @@ public OuterClass(@JsonProperty("name") String name) } public String getName() + throws IOException { - ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory()); + // Serialize nested class object + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); InnerClass innerClass = objectMapper.readValue( objectMapper.writeValueAsBytes(new InnerClass("Bar")), InnerClass.class); @@ -987,460 +954,4 @@ public String getName() return name; } } - - @Test - public void testIsClosedAfterClose() - throws IOException - { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); - assertFalse(generator.isClosed()); - generator.writeStartArray(); - generator.writeEndArray(); - generator.close(); - assertTrue(generator.isClosed()); - } - - @Test - public void testGeneratorReusableAfterRootContainerClose() - throws IOException - { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); - generator.writeStartArray(); - generator.writeNumber(1); - generator.writeEndArray(); - generator.flush(); - - // Write a second root value; currentState must have reset to IN_ROOT - generator.writeStartObject(); - generator.writeName("k"); - generator.writeNumber(2); - generator.writeEndObject(); - generator.close(); - - MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); - assertEquals(1, unpacker.unpackArrayHeader()); - assertEquals(1, unpacker.unpackInt()); - assertEquals(1, unpacker.unpackMapHeader()); - assertEquals("k", unpacker.unpackString()); - assertEquals(2, unpacker.unpackInt()); - } - - @Test - public void testWriteRawStringWithOffset() - throws IOException - { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); - generator.writeStartArray(); - generator.writeRaw("XXhelloXX", 2, 5); // "hello" - generator.writeEndArray(); - generator.close(); - - MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); - unpacker.unpackArrayHeader(); - assertEquals("hello", unpacker.unpackString()); - } - - @Test - public void testWriteStringCharArrayWithOffset() - throws IOException - { - // Padding chars before/after the actual content to test non-zero offset - char[] buf = new char[] {'X', 'X', 'h', 'e', 'l', 'l', 'o', 'X'}; - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); - generator.writeStartArray(); - generator.writeString(buf, 2, 5); // "hello" - generator.writeEndArray(); - generator.close(); - - MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); - unpacker.unpackArrayHeader(); - assertEquals("hello", unpacker.unpackString()); - } - - @Test - public void testWriteStringCharArrayWithOffsetNonAscii() - throws IOException - { - // Non-ASCII to exercise the non-fast-path in getBytesIfAscii - char[] buf = new char[] {'X', '三', '四', '五', 'X'}; // 三四五 - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); - generator.writeStartArray(); - generator.writeString(buf, 1, 3); - generator.writeEndArray(); - generator.close(); - - MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); - unpacker.unpackArrayHeader(); - assertEquals("三四五", unpacker.unpackString()); - } - - @Test - public void testWriteUTF8StringWithOffset() - throws IOException - { - // Padding bytes before/after to test non-zero offset in writeUTF8String - byte[] buf = new byte[] {'X', 'X', 'h', 'i', 'X'}; - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); - generator.writeStartArray(); - generator.writeUTF8String(buf, 2, 2); // "hi" - generator.writeEndArray(); - generator.close(); - - MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); - unpacker.unpackArrayHeader(); - assertEquals("hi", unpacker.unpackString()); - } - - @Test - public void testWriteBinaryWithOffset() - throws IOException - { - byte[] data = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04}; - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); - generator.writeStartArray(); - generator.writeBinary(data, 1, 3); // bytes 0x01, 0x02, 0x03 - generator.writeEndArray(); - generator.close(); - - MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); - unpacker.unpackArrayHeader(); - byte[] result = unpacker.readPayload(unpacker.unpackBinaryHeader()); - assertArrayEquals(new byte[] {0x01, 0x02, 0x03}, result); - } - - @Test - public void testWriteBinaryByteBufferWithOffset() - throws IOException - { - byte[] data = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04}; - ByteBuffer bb = ByteBuffer.wrap(data, 1, 3); // position=1, limit=4, remaining=3 - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); - generator.writeStartArray(); - ObjectMapper mapper = new MessagePackMapper(factory); - mapper.writeValue(generator, bb); - generator.writeEndArray(); - generator.close(); - - MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); - unpacker.unpackArrayHeader(); - byte[] result = unpacker.readPayload(unpacker.unpackBinaryHeader()); - assertArrayEquals(new byte[] {0x01, 0x02, 0x03}, result); - } - - @Test - public void testStreamWriteContext() - throws IOException - { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); - - TokenStreamContext ctx = generator.streamWriteContext(); - assertNotEquals(null, ctx); - assertTrue(ctx.inRoot()); - - generator.writeStartArray(); - ctx = generator.streamWriteContext(); - assertTrue(ctx.inArray()); - - generator.writeStartObject(); - ctx = generator.streamWriteContext(); - assertTrue(ctx.inObject()); - - generator.writeName("k"); - assertEquals("k", ctx.currentName()); - - generator.writeNumber(1); - generator.writeEndObject(); - ctx = generator.streamWriteContext(); - assertTrue(ctx.inArray()); - - generator.writeEndArray(); - ctx = generator.streamWriteContext(); - assertTrue(ctx.inRoot()); - - generator.close(); - } - - @Test - public void testCurrentValue() - throws IOException - { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); - - Object pojo = new Object(); - generator.writeStartObject(pojo); - assertEquals(pojo, generator.currentValue()); - generator.writeName("k"); - generator.writeNumber(1); - generator.writeEndObject(); - generator.close(); - } - - @Test - public void testVersion() - { - assertNotEquals(null, factory.version()); - assertEquals("org.msgpack", factory.version().getGroupId()); - assertEquals("jackson-dataformat-msgpack-jackson3", factory.version().getArtifactId()); - } - - @Test - public void testSerializedStringMethods() throws IOException - { - MessagePackSerializedString s = new MessagePackSerializedString("hello"); - - byte[] utf8Target = new byte[10]; - int written = s.appendUnquotedUTF8(utf8Target, 2); - assertEquals(5, written); - assertArrayEquals(new byte[] {'h', 'e', 'l', 'l', 'o'}, Arrays.copyOfRange(utf8Target, 2, 7)); - - char[] charTarget = new char[10]; - written = s.appendUnquoted(charTarget, 3); - assertEquals(5, written); - assertEquals("hello", new String(charTarget, 3, 5)); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - written = s.writeUnquotedUTF8(baos); - assertEquals(5, written); - assertArrayEquals("hello".getBytes(java.nio.charset.StandardCharsets.UTF_8), baos.toByteArray()); - } - - // Regression: addValueNode must call writeContext.writeValue() so that the Jackson write - // context resets _gotPropertyId after each value. Without it, the second writeName() in the - // same object finds _gotPropertyId still set from the first writeName() and returns false - // without updating currentName(), leaving streamWriteContext().currentName() stale. - @Test - public void testWriteContextCurrentNameIsUpdatedForEveryProperty() - throws IOException - { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); - generator.writeStartObject(); - - generator.writeName("alpha"); - generator.writeNumber(1); - - generator.writeName("beta"); - assertEquals("beta", generator.streamWriteContext().currentName()); - generator.writeNumber(2); - - generator.writeEndObject(); - generator.close(); - } - - // Regression: writePropertyId() must call writeContext.writeName() when supportIntegerKeys - // is true. Without it, streamWriteContext() never learns a name was written, so currentName() - // returns null and any downstream code relying on context state (e.g. duplicate-name - // detection, error messages) sees wrong state. - @Test - public void testWritePropertyIdUpdatesWriteContext() - throws IOException - { - MessagePackFactory intKeyFactory = new MessagePackFactory().setSupportIntegerKeys(true); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = intKeyFactory.createGenerator(ObjectWriteContext.empty(), baos); - generator.writeStartObject(); - generator.writePropertyId(42L); - assertEquals("42", generator.streamWriteContext().currentName()); - generator.writeString("value"); - generator.writeEndObject(); - generator.close(); - } - - @Test - public void testNullSerializedStringKeyDoesNotThrowNpe() - throws IOException - { - // writeName(MessagePackSerializedString(null)) calls getValue() → null.toString() → NPE. - // A null key should be serialized as msgpack nil, not crash. - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = new MessagePackFactory().createGenerator(ObjectWriteContext.empty(), baos); - generator.writeStartObject(); - generator.writeName(new MessagePackSerializedString(null)); - generator.writeNumber(42); - generator.writeEndObject(); - generator.close(); - - // Verify the null key round-trips as PROPERTY_NAME with null current name - try (JsonParser parser = - new MessagePackFactory().createParser(ObjectReadContext.empty(), baos.toByteArray())) { - assertEquals(JsonToken.START_OBJECT, parser.nextToken()); - assertEquals(JsonToken.PROPERTY_NAME, parser.nextToken()); - assertNull(parser.currentName()); - assertEquals(JsonToken.VALUE_NUMBER_INT, parser.nextToken()); - assertEquals(42, parser.getIntValue()); - assertEquals(JsonToken.END_OBJECT, parser.nextToken()); - } - } - - @Test - public void testFlushMidWriteOnSecondRootContainerDoesNotCorruptState() - throws IOException - { - // After the first root container closes, isElementsClosed=true. - // Opening a second root container does not reset this flag, so a - // flush() call while the second container is still open will pack - // the incomplete node tree and wipe nodes[], corrupting subsequent writes. - MessagePackFactory factory = new MessagePackFactory(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); - - generator.writeStartArray(); - generator.writeNumber(1); - generator.writeEndArray(); - - generator.writeStartArray(); // second root — isElementsClosed still true - generator.flush(); // must NOT pack the incomplete second array - generator.writeNumber(2); - generator.writeEndArray(); - - generator.close(); - - ObjectMapper mapper = MessagePackMapper.builder(new MessagePackFactory()) - .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) - .build(); - try (JsonParser parser = - new MessagePackFactory().createParser(ObjectReadContext.empty(), baos.toByteArray())) { - List first = mapper.readValue(parser, new TypeReference>() {}); - assertEquals(Collections.singletonList(1), first); - List second = mapper.readValue(parser, new TypeReference>() {}); - assertEquals(Collections.singletonList(2), second); - } - } - - @Test - public void testRootScalarAfterClosedRootContainerPreservesOrder() - throws IOException - { - // A root scalar written after a closed root container must be emitted AFTER - // the container, not before. Without a fix, addValueNode() packs the scalar - // immediately (default branch) while the container stays buffered, reversing order. - MessagePackFactory factory = new MessagePackFactory(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); - generator.writeStartArray(); - generator.writeNumber(1); - generator.writeEndArray(); - generator.writeNumber(2); // root scalar — must come AFTER the array - generator.close(); - - ObjectMapper mapper = MessagePackMapper.builder(new MessagePackFactory()) - .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) - .build(); - try (JsonParser parser = - new MessagePackFactory().createParser(ObjectReadContext.empty(), baos.toByteArray())) { - List list = mapper.readValue(parser, new TypeReference>() {}); - assertEquals(Collections.singletonList(1), list); - int scalar = mapper.readValue(parser, Integer.class); - assertEquals(2, scalar); - } - } - - // Bug: writeNumber(String) is missing "return this" after addValueNode(d) in the - // NaN/Infinity fallback branch. addValueNode() advances the write-context state - // and (for root-level scalars) writes bytes to the output stream before the - // method falls through to "throw new NumberFormatException(encodedValue)". - @Test - public void testWriteNumberStringNanAndInfinity() throws IOException - { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - // Avoid try-with-resources: if writeNumber throws, the implicit close - // flushes a half-written node list and masks the original failure. - JsonGenerator gen = factory.createGenerator(ObjectWriteContext.empty(), baos); - gen.writeStartArray(); - gen.writeNumber("NaN"); // Bug: NFE thrown after state mutation - gen.writeNumber("Infinity"); - gen.writeNumber("-Infinity"); - gen.writeEndArray(); - gen.close(); - - try (JsonParser p = factory.createParser(ObjectReadContext.empty(), baos.toByteArray())) { - assertEquals(JsonToken.START_ARRAY, p.nextToken()); - assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); - assertTrue(Double.isNaN(p.getDoubleValue())); - assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); - assertEquals(Double.POSITIVE_INFINITY, p.getDoubleValue(), 0); - assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); - assertEquals(Double.NEGATIVE_INFINITY, p.getDoubleValue(), 0); - assertEquals(JsonToken.END_ARRAY, p.nextToken()); - } - } - - // Bug: same DupDetector NPE as the read-path bug, on the write side. - // writeName(SerializableString) calls writeContext.writeName(name.getValue()) - // where MessagePackSerializedString(null).getValue() == null. With - // STRICT_DUPLICATE_DETECTION enabled and a prior non-null key already seen, - // DupDetector.isDup(null) reaches name.equals(_firstName) → NPE. - @Test - public void testNullKeyWithWriteDupDetectionDoesNotNPE() throws IOException - { - MessagePackFactory f = new MessagePackFactoryBuilder() - .enable(StreamWriteFeature.STRICT_DUPLICATE_DETECTION) - .build(); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (JsonGenerator gen = f.createGenerator(ObjectWriteContext.empty(), baos)) { - gen.writeStartObject(); - gen.writeName("foo"); - gen.writeNumber(1); - // MessagePackSerializedString(null).getValue() == null - gen.writeName(new MessagePackSerializedString(null)); // Bug: NPE here - gen.writeNumber(2); - gen.writeEndObject(); - } - } - - // Bug: MessagePackSerializedString.charLength() calls getValue().length() - // unconditionally; getValue() returns null when value is null → NPE. - @Test - public void testSerializedStringNullValueCharLengthDoesNotNPE() - { - MessagePackSerializedString s = new MessagePackSerializedString(null); - // Bug: null.length() → NullPointerException - assertEquals(0, s.charLength()); - } - - @Test - public void testMultipleRootContainersWithoutFlush() - throws IOException - { - // Writing two consecutive root-level containers on the same generator without - // an intervening flush() must not throw IndexOutOfBoundsException. - // The second root container sits at node index 1, so the root check - // "currentParentElementIndex == 0" incorrectly falls through to the - // nested-container path and calls nodes.get(-1). - MessagePackFactory factory = new MessagePackFactory(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); - generator.writeStartArray(); - generator.writeNumber(1); - generator.writeEndArray(); - // second root container — no flush() in between - generator.writeStartObject(); - generator.writeStringProperty("key", "value"); - generator.writeEndObject(); - generator.close(); - - // Verify both values were written correctly by reading from a shared parser - ObjectMapper mapper = MessagePackMapper.builder(new MessagePackFactory()) - .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) - .build(); - try (JsonParser parser = - new MessagePackFactory().createParser(ObjectReadContext.empty(), baos.toByteArray())) { - List list = mapper.readValue(parser, new TypeReference>() {}); - assertEquals(Collections.singletonList(1), list); - Map map = mapper.readValue(parser, new TypeReference>() {}); - assertEquals(Collections.singletonMap("key", "value"), map); - } - } } diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java similarity index 82% rename from msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java rename to msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java index 776a1109..d14f97f2 100644 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java +++ b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java @@ -15,15 +15,15 @@ // package org.msgpack.jackson.dataformat; -import tools.jackson.core.JacksonException; -import org.junit.Test; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.jupiter.api.Test; import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; public class MessagePackMapperTest { @@ -37,7 +37,7 @@ static class PojoWithBigDecimal public BigDecimal value; } - private void shouldFailToHandleBigInteger(MessagePackMapper messagePackMapper) throws JacksonException + private void shouldFailToHandleBigInteger(MessagePackMapper messagePackMapper) throws JsonProcessingException { PojoWithBigInteger obj = new PojoWithBigInteger(); obj.value = BigInteger.valueOf(Long.MAX_VALUE).multiply(BigInteger.valueOf(10)); @@ -62,7 +62,7 @@ private void shouldSuccessToHandleBigInteger(MessagePackMapper messagePackMapper assertEquals(obj.value, deserialized.value); } - private void shouldFailToHandleBigDecimal(MessagePackMapper messagePackMapper) throws JacksonException + private void shouldFailToHandleBigDecimal(MessagePackMapper messagePackMapper) throws JsonProcessingException { PojoWithBigDecimal obj = new PojoWithBigDecimal(); obj.value = new BigDecimal("1234567890.98765432100"); @@ -91,26 +91,20 @@ private void shouldSuccessToHandleBigDecimal(MessagePackMapper messagePackMapper public void handleBigIntegerAsString() throws IOException { shouldFailToHandleBigInteger(new MessagePackMapper()); - shouldSuccessToHandleBigInteger(MessagePackMapper.builder() - .handleBigIntegerAsString() - .build()); + shouldSuccessToHandleBigInteger(new MessagePackMapper().handleBigIntegerAsString()); } @Test public void handleBigDecimalAsString() throws IOException { shouldFailToHandleBigDecimal(new MessagePackMapper()); - shouldSuccessToHandleBigDecimal(MessagePackMapper.builder() - .handleBigDecimalAsString() - .build()); + shouldSuccessToHandleBigDecimal(new MessagePackMapper().handleBigDecimalAsString()); } @Test public void handleBigIntegerAndBigDecimalAsString() throws IOException { - MessagePackMapper messagePackMapper = MessagePackMapper.builder() - .handleBigIntegerAndBigDecimalAsString() - .build(); + MessagePackMapper messagePackMapper = new MessagePackMapper().handleBigIntegerAndBigDecimalAsString(); shouldSuccessToHandleBigInteger(messagePackMapper); shouldSuccessToHandleBigDecimal(messagePackMapper); } diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java similarity index 59% rename from msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java rename to msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java index 7b0ceee6..256c4332 100644 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java +++ b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java @@ -15,20 +15,19 @@ // package org.msgpack.jackson.dataformat; -import tools.jackson.core.JsonParser; -import tools.jackson.core.JacksonException; -import tools.jackson.core.JsonToken; -import tools.jackson.core.ObjectReadContext; -import tools.jackson.core.StreamReadFeature; -import tools.jackson.core.exc.UnexpectedEndOfInputException; -import tools.jackson.core.type.TypeReference; -import tools.jackson.databind.DeserializationContext; -import tools.jackson.databind.DeserializationFeature; -import tools.jackson.databind.KeyDeserializer; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.deser.std.StdDeserializer; -import tools.jackson.databind.module.SimpleModule; -import org.junit.Test; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.io.JsonEOFException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.junit.jupiter.api.Test; import org.msgpack.core.MessagePack; import org.msgpack.core.MessagePacker; import org.msgpack.value.ExtensionValue; @@ -37,7 +36,6 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.InputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -54,12 +52,11 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertThrows; public class MessagePackParserTest extends MessagePackDataformatTestBase @@ -326,7 +323,7 @@ public void testMessagePackParserDirectly() assertEquals(1, parser.currentLocation().getColumnNr()); jsonToken = parser.nextToken(); - assertEquals(JsonToken.PROPERTY_NAME, jsonToken); + assertEquals(JsonToken.FIELD_NAME, jsonToken); assertEquals("zero", parser.currentName()); assertEquals(1, parser.currentTokenLocation().getColumnNr()); assertEquals(6, parser.currentLocation().getColumnNr()); @@ -338,10 +335,12 @@ public void testMessagePackParserDirectly() assertEquals(7, parser.currentLocation().getColumnNr()); jsonToken = parser.nextToken(); - assertEquals(JsonToken.PROPERTY_NAME, jsonToken); + assertEquals(JsonToken.FIELD_NAME, jsonToken); assertEquals("one", parser.currentName()); assertEquals(7, parser.currentTokenLocation().getColumnNr()); assertEquals(11, parser.currentLocation().getColumnNr()); + parser.overrideCurrentName("two"); + assertEquals("two", parser.currentName()); jsonToken = parser.nextToken(); assertEquals(JsonToken.VALUE_NUMBER_FLOAT, jsonToken); @@ -381,24 +380,24 @@ public void testReadPrimitives() JsonParser parser = factory.createParser(new FileInputStream(tempFile)); assertEquals(JsonToken.VALUE_STRING, parser.nextToken()); - assertEquals("foo", parser.getString()); + assertEquals("foo", parser.getText()); assertEquals(JsonToken.VALUE_NUMBER_FLOAT, parser.nextToken()); assertEquals(3.14, parser.getDoubleValue(), 0.0001); - assertEquals("3.14", parser.getString()); + assertEquals("3.14", parser.getText()); assertEquals(JsonToken.VALUE_NUMBER_INT, parser.nextToken()); assertEquals(Integer.MIN_VALUE, parser.getIntValue()); assertEquals(Integer.MIN_VALUE, parser.getLongValue()); - assertEquals("-2147483648", parser.getString()); + assertEquals("-2147483648", parser.getText()); assertEquals(JsonToken.VALUE_NUMBER_INT, parser.nextToken()); assertEquals(Long.MAX_VALUE, parser.getLongValue()); - assertEquals("9223372036854775807", parser.getString()); + assertEquals("9223372036854775807", parser.getText()); assertEquals(JsonToken.VALUE_NUMBER_INT, parser.nextToken()); assertEquals(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE), parser.getBigIntegerValue()); - assertEquals("9223372036854775808", parser.getString()); + assertEquals("9223372036854775808", parser.getText()); assertEquals(JsonToken.VALUE_EMBEDDED_OBJECT, parser.nextToken()); assertEquals(bytes.length, parser.getBinaryValue().length); @@ -422,9 +421,8 @@ public void testBigDecimal() packer.packDouble(Double.MIN_NORMAL); packer.flush(); - ObjectMapper mapper = MessagePackMapper.builder(new MessagePackFactory()) - .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS) - .build(); + ObjectMapper mapper = new ObjectMapper(new MessagePackFactory()); + mapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true); List objects = mapper.readValue(out.toByteArray(), new TypeReference>() {}); assertEquals(5, objects.size()); int idx = 0; @@ -460,11 +458,9 @@ public void testEnableFeatureAutoCloseSource() File tempFile = createTestFile(); MessagePackFactory factory = new MessagePackFactory(); FileInputStream in = new FileInputStream(tempFile); - ObjectMapper objectMapper = MessagePackMapper.builder(factory) - .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) - .build(); + ObjectMapper objectMapper = new ObjectMapper(factory); objectMapper.readValue(in, new TypeReference>() {}); - assertThrows(JacksonException.class, () -> { + assertThrows(IOException.class, () -> { objectMapper.readValue(in, new TypeReference>() {}); }); } @@ -475,10 +471,8 @@ public void testDisableFeatureAutoCloseSource() { File tempFile = createTestFile(); FileInputStream in = new FileInputStream(tempFile); - ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .disable(tools.jackson.core.StreamReadFeature.AUTO_CLOSE_SOURCE) - .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) - .build(); + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false); objectMapper.readValue(in, new TypeReference>() {}); objectMapper.readValue(in, new TypeReference>() {}); } @@ -489,7 +483,7 @@ public void testParseBigDecimal() { ArrayList list = new ArrayList(); list.add(new BigDecimal(Long.MAX_VALUE)); - ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory()); + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); byte[] bytes = objectMapper.writeValueAsBytes(list); ArrayList result = objectMapper.readValue( @@ -514,10 +508,8 @@ public void testReadPrimitiveObjectViaObjectMapper() packer.close(); FileInputStream in = new FileInputStream(tempFile); - ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .disable(tools.jackson.core.StreamReadFeature.AUTO_CLOSE_SOURCE) - .disable(tools.jackson.databind.DeserializationFeature.FAIL_ON_TRAILING_TOKENS) - .build(); + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false); assertEquals("foo", objectMapper.readValue(in, new TypeReference() {})); long l = objectMapper.readValue(in, new TypeReference() {}); assertEquals(Long.MAX_VALUE, l); @@ -546,7 +538,7 @@ public void testBinaryKey() packer.packLong(42); packer.close(); - ObjectMapper mapper = new MessagePackMapper(new MessagePackFactory()); + ObjectMapper mapper = new ObjectMapper(new MessagePackFactory()); Map object = mapper.readValue(new FileInputStream(tempFile), new TypeReference>() {}); assertEquals(2, object.size()); assertEquals(3.14, object.get("foo")); @@ -568,7 +560,7 @@ public void testBinaryKeyInNestedObject() packer.packInt(1); packer.close(); - ObjectMapper mapper = new MessagePackMapper(new MessagePackFactory()); + ObjectMapper mapper = new ObjectMapper(new MessagePackFactory()); List objects = mapper.readValue(out.toByteArray(), new TypeReference>() {}); assertEquals(2, objects.size()); @SuppressWarnings(value = "unchecked") @@ -590,18 +582,18 @@ public void testByteArrayKey() messagePacker.packBinaryHeader(1).writePayload(k1).packInt(11); messagePacker.close(); - ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .addModule(new SimpleModule() - .addKeyDeserializer(byte[].class, new KeyDeserializer() - { - @Override - public Object deserializeKey(String key, DeserializationContext ctxt) - throws JacksonException - { - return key.getBytes(); - } - })) - .build(); + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + SimpleModule module = new SimpleModule(); + module.addKeyDeserializer(byte[].class, new KeyDeserializer() + { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) + throws IOException, JsonProcessingException + { + return key.getBytes(); + } + }); + objectMapper.registerModule(module); Map map = objectMapper.readValue( out.toByteArray(), new TypeReference>() {}); @@ -627,18 +619,18 @@ public void testIntegerKey() } messagePacker.close(); - ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .addModule(new SimpleModule() - .addKeyDeserializer(Integer.class, new KeyDeserializer() - { - @Override - public Object deserializeKey(String key, DeserializationContext ctxt) - throws JacksonException - { - return Integer.valueOf(key); - } - })) - .build(); + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + SimpleModule module = new SimpleModule(); + module.addKeyDeserializer(Integer.class, new KeyDeserializer() + { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) + throws IOException, JsonProcessingException + { + return Integer.valueOf(key); + } + }); + objectMapper.registerModule(module); Map map = objectMapper.readValue( out.toByteArray(), new TypeReference>() {}); @@ -658,18 +650,18 @@ public void testFloatKey() } messagePacker.close(); - ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .addModule(new SimpleModule() - .addKeyDeserializer(Float.class, new KeyDeserializer() - { - @Override - public Object deserializeKey(String key, DeserializationContext ctxt) - throws JacksonException - { - return Float.valueOf(key); - } - })) - .build(); + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + SimpleModule module = new SimpleModule(); + module.addKeyDeserializer(Float.class, new KeyDeserializer() + { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) + throws IOException, JsonProcessingException + { + return Float.valueOf(key); + } + }); + objectMapper.registerModule(module); Map map = objectMapper.readValue( out.toByteArray(), new TypeReference>() {}); @@ -688,18 +680,18 @@ public void testBooleanKey() messagePacker.packBoolean(false).packInt(11); messagePacker.close(); - ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .addModule(new SimpleModule() - .addKeyDeserializer(Boolean.class, new KeyDeserializer() - { - @Override - public Object deserializeKey(String key, DeserializationContext ctxt) - throws JacksonException - { - return Boolean.valueOf(key); - } - })) - .build(); + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + SimpleModule module = new SimpleModule(); + module.addKeyDeserializer(Boolean.class, new KeyDeserializer() + { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) + throws IOException, JsonProcessingException + { + return Boolean.valueOf(key); + } + }); + objectMapper.registerModule(module); Map map = objectMapper.readValue( out.toByteArray(), new TypeReference>() {}); @@ -708,26 +700,6 @@ public Object deserializeKey(String key, DeserializationContext ctxt) assertEquals((Integer) 11, map.get(false)); } - @Test - public void testNilKey() - throws IOException - { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - MessagePacker packer = MessagePack.newDefaultPacker(out).packMapHeader(1); - packer.packNil(); - packer.packInt(42); - packer.close(); - - JsonParser parser = new MessagePackMapper().createParser(out.toByteArray()); - assertEquals(JsonToken.START_OBJECT, parser.nextToken()); - assertEquals(JsonToken.PROPERTY_NAME, parser.nextToken()); - assertNull(parser.currentName()); - assertEquals(JsonToken.VALUE_NUMBER_INT, parser.nextToken()); - assertEquals(42, parser.getIntValue()); - assertEquals(JsonToken.END_OBJECT, parser.nextToken()); - parser.close(); - } - @Test public void extensionTypeCustomDeserializers() throws IOException @@ -760,7 +732,7 @@ public Object deserialize(byte[] data) } ); ObjectMapper objectMapper = - new MessagePackMapper(new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)); + new ObjectMapper(new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)); List values = objectMapper.readValue(new ByteArrayInputStream(out.toByteArray()), new TypeReference>() {}); assertThat(values.size(), is(3)); @@ -815,6 +787,7 @@ public int hashCode() @Override public String toString() { + // This key format is used when serialized as map key return String.format("%d-%d-%d", first, second, third); } @@ -828,18 +801,18 @@ protected Deserializer() @Override public TripleBytesPojo deserialize(JsonParser p, DeserializationContext ctxt) - throws JacksonException + throws IOException, JsonProcessingException { return TripleBytesPojo.deserialize(p.getBinaryValue()); } } - static class TripleBytesKeyDeserializer - extends KeyDeserializer + static class KeyDeserializer + extends com.fasterxml.jackson.databind.KeyDeserializer { @Override public Object deserializeKey(String key, DeserializationContext ctxt) - throws JacksonException + throws IOException { String[] values = key.split("-"); return new TripleBytesPojo( @@ -879,11 +852,10 @@ public Object deserialize(byte[] value) SimpleModule module = new SimpleModule(); module.addDeserializer(TripleBytesPojo.class, new TripleBytesPojo.Deserializer()); - module.addKeyDeserializer(TripleBytesPojo.class, new TripleBytesPojo.TripleBytesKeyDeserializer()); - ObjectMapper objectMapper = MessagePackMapper.builder( + module.addKeyDeserializer(TripleBytesPojo.class, new TripleBytesPojo.KeyDeserializer()); + ObjectMapper objectMapper = new ObjectMapper( new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) - .addModule(module) - .build(); + .registerModule(module); // Prepare serialized data Map originalMap = new HashMap<>(); @@ -933,8 +905,10 @@ public Object deserialize(byte[] value) } }); - ObjectMapper objectMapper = - new MessagePackMapper(new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)); + // In this case with UUID, we don't need to add custom deserializers + // since jackson-databind already has it. + ObjectMapper objectMapper = new ObjectMapper( + new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)); // Prepare serialized data Map originalMap = new HashMap<>(); @@ -989,6 +963,9 @@ public void parserShouldReadStrAsBin() assertArrayEquals("bar".getBytes(), binKeyPojo.b); } + // Test deserializers that parse a string as a number. + // Actually, com.fasterxml.jackson.databind.deser.std.StdDeserializer._parseInteger() takes care of it. + @Test public void deserializeStringAsInteger() throws IOException @@ -1067,458 +1044,55 @@ public void handleMissingItemInArray() packer.packString("two"); packer.close(); - assertThrows(UnexpectedEndOfInputException.class, () -> { + try { objectMapper.readValue(out.toByteArray(), new TypeReference>() {}); - }); + fail(); + } + catch (JsonMappingException e) { + assertTrue(e.getCause() instanceof JsonEOFException); + } } @Test public void handleMissingKeyValueInMap() + throws IOException { MessagePacker packer = MessagePack.newDefaultPacker(out); + packer.packMapHeader(3); + packer.packString("one"); + packer.packInt(1); + packer.packString("two"); + packer.packInt(2); + packer.close(); + try { - packer.packMapHeader(3); - packer.packString("one"); - packer.packInt(1); - packer.packString("two"); - packer.packInt(2); - packer.close(); + objectMapper.readValue(out.toByteArray(), new TypeReference>() {}); + fail(); } - catch (IOException e) { - throw new RuntimeException(e); + catch (JsonEOFException e) { + assertTrue(true); } - - assertThrows(JacksonException.class, () -> { - objectMapper.readValue(out.toByteArray(), new TypeReference>() {}); - }); } @Test public void handleMissingValueInMap() + throws IOException { MessagePacker packer = MessagePack.newDefaultPacker(out); - try { - packer.packMapHeader(3); - packer.packString("one"); - packer.packInt(1); - packer.packString("two"); - packer.packInt(2); - packer.packString("three"); - packer.close(); - } - catch (IOException e) { - throw new RuntimeException(e); - } + packer.packMapHeader(3); + packer.packString("one"); + packer.packInt(1); + packer.packString("two"); + packer.packInt(2); + packer.packString("three"); + packer.close(); - assertThrows(JacksonException.class, () -> { + try { objectMapper.readValue(out.toByteArray(), new TypeReference>() {}); - }); - } - - @Test - public void testByteArrayThreadLocalClearedAfterClose() - throws IOException - { - ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory()); - - byte[] bytes = objectMapper.writeValueAsBytes(Arrays.asList(1, 2, 3)); - - // Parse once; this caches the byte array in the ThreadLocal - objectMapper.readValue(bytes, new TypeReference>() {}); - - // Parse again with the same byte array instance and AUTO_CLOSE_SOURCE enabled - // (default). The byte array reference should have been cleared from the - // ThreadLocal on close, so the second parse resets the unpacker and starts - // from the beginning rather than continuing from the end. - List result = objectMapper.readValue(bytes, new TypeReference>() {}); - assertEquals(Arrays.asList(1, 2, 3), result); - } - - @Test - public void testByteArrayReuseResetsUnpackerWhenAutoCloseSourceDisabled() - throws IOException - { - ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .disable(tools.jackson.core.StreamReadFeature.AUTO_CLOSE_SOURCE) - .build(); - - byte[] bytes = objectMapper.writeValueAsBytes(Arrays.asList(1, 2, 3)); - - // First parse succeeds - List first = objectMapper.readValue(bytes, new TypeReference>() {}); - assertEquals(Arrays.asList(1, 2, 3), first); - - // Second parse with the same byte[] instance and AUTO_CLOSE_SOURCE disabled. - // The byte[] source always triggers an unpacker reset (|| src instanceof byte[]), - // so the second parse succeeds and returns the correct result. - List second = objectMapper.readValue(bytes, new TypeReference>() {}); - assertEquals(Arrays.asList(1, 2, 3), second); - } - - @Test - public void testInputStreamSequentialReadsWithAutoCloseSourceDisabled() - throws IOException - { - ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .disable(tools.jackson.core.StreamReadFeature.AUTO_CLOSE_SOURCE) - .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) - .build(); - - // Two values packed sequentially into a single stream - ByteArrayOutputStream out = new ByteArrayOutputStream(); - out.write(objectMapper.writeValueAsBytes(Arrays.asList(1, 2, 3))); - out.write(objectMapper.writeValueAsBytes(Arrays.asList(4, 5, 6))); - ByteArrayInputStream stream = new ByteArrayInputStream(out.toByteArray()); - - // First parse reads the first value; unpacker may read ahead into the second value - List first = objectMapper.readValue(stream, new TypeReference>() {}); - assertEquals(Arrays.asList(1, 2, 3), first); - - // Second parse must read the second value from the same stream. - // If the source was incorrectly cleared from the ThreadLocal on close(), - // the unpacker's read-ahead buffer is dismissed and the second value is lost. - List second = objectMapper.readValue(stream, new TypeReference>() {}); - assertEquals(Arrays.asList(4, 5, 6), second); - } - - @Test - public void testGetStringOnNullToken() throws IOException - { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { - packer.packNil(); + fail(); } - MessagePackFactory factory = new MessagePackFactory(); - try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { - assertEquals(JsonToken.VALUE_NULL, p.nextToken()); - assertEquals("null", p.getString()); - } - } - - @Test - public void testGetStringOnBoolToken() throws IOException - { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { - packer.packBoolean(true); - packer.packBoolean(false); - } - MessagePackFactory factory = new MessagePackFactory(); - try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { - assertEquals(JsonToken.VALUE_TRUE, p.nextToken()); - assertEquals("true", p.getString()); - assertEquals(JsonToken.VALUE_FALSE, p.nextToken()); - assertEquals("false", p.getString()); - } - } - - @Test - public void testNumericAccessorsOnNullTokenThrow() throws IOException - { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { - packer.packNil(); - } - byte[] bytes = out.toByteArray(); - MessagePackFactory factory = new MessagePackFactory(); - - try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { - p.nextToken(); - try { - p.getIntValue(); - fail("expected exception"); - } - catch (JacksonException ignored) { } - } - try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { - p.nextToken(); - try { - p.getLongValue(); - fail("expected exception"); - } - catch (JacksonException ignored) { } - } - try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { - p.nextToken(); - try { - p.getDoubleValue(); - fail("expected exception"); - } - catch (JacksonException ignored) { } - } - try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { - p.nextToken(); - try { - p.getFloatValue(); - fail("expected exception"); - } - catch (JacksonException ignored) { } - } - try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { - p.nextToken(); - try { - p.getBigIntegerValue(); - fail("expected exception"); - } - catch (JacksonException ignored) { } - } - try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { - p.nextToken(); - try { - p.getDecimalValue(); - fail("expected exception"); - } - catch (JacksonException ignored) { } - } - try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { - p.nextToken(); - try { - p.getNumberValue(); - fail("expected exception"); - } - catch (JacksonException ignored) { } - } - } - - @Test - public void testNumericAccessorsOnBoolTokenThrow() throws IOException - { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { - packer.packBoolean(true); - } - byte[] bytes = out.toByteArray(); - MessagePackFactory factory = new MessagePackFactory(); - - try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { - p.nextToken(); - try { - p.getIntValue(); - fail("expected exception"); - } - catch (JacksonException ignored) { } - } - try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { - p.nextToken(); - try { - p.getLongValue(); - fail("expected exception"); - } - catch (JacksonException ignored) { } - } - try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { - p.nextToken(); - try { - p.getDoubleValue(); - fail("expected exception"); - } - catch (JacksonException ignored) { } - } - } - - @Test - public void testGetNumberTypeOnNonNumericTokens() throws IOException - { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { - packer.packNil(); - packer.packBoolean(true); - packer.packString("hello"); - } - MessagePackFactory factory = new MessagePackFactory(); - try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { - assertEquals(JsonToken.VALUE_NULL, p.nextToken()); - assertNull(p.getNumberType()); - assertEquals(JsonToken.VALUE_TRUE, p.nextToken()); - assertNull(p.getNumberType()); - assertEquals(JsonToken.VALUE_STRING, p.nextToken()); - assertNull(p.getNumberType()); - } - } - - @Test - public void testGetIntValueFromOutOfRangeDoubleThrows() throws IOException - { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { - packer.packDouble(1e30); - } - MessagePackFactory factory = new MessagePackFactory(); - try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { - assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); - try { - p.getIntValue(); - fail("expected exception for out-of-range double"); - } - catch (JacksonException ignored) { } - } - } - - @Test - public void testGetLongValueFromOutOfRangeDoubleThrows() throws IOException - { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { - packer.packDouble(1e30); - } - MessagePackFactory factory = new MessagePackFactory(); - try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { - assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); - try { - p.getLongValue(); - fail("expected exception for out-of-range double"); - } - catch (JacksonException ignored) { } - } - } - - @Test - public void testGetLongValueFromDoubleThatEqualsLongMaxValueRoundedUpThrows() throws IOException - { - // (double) Long.MAX_VALUE rounds up to 2^63, which exceeds Long.MAX_VALUE. - // The check `doubleValue > Long.MAX_VALUE` misses this value and silently - // saturates the cast to Long.MAX_VALUE instead of throwing. - double twoTo63 = 9.223372036854776E18; // exact double for 2^63 - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { - packer.packDouble(twoTo63); - } - MessagePackFactory factory = new MessagePackFactory(); - try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { - assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); - try { - p.getLongValue(); - fail("expected exception for double value 2^63 which exceeds Long.MAX_VALUE"); - } - catch (JacksonException ignored) { } - } - } - - @Test - public void testNumericAccessorsOnStructuralTokenThrow() throws IOException - { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { - packer.packMapHeader(0); - } - byte[] bytes = out.toByteArray(); - MessagePackFactory factory = new MessagePackFactory(); - - try (JsonParser p = factory.createParser(ObjectReadContext.empty(), bytes)) { - assertEquals(JsonToken.START_OBJECT, p.nextToken()); - try { - p.getIntValue(); - fail("expected exception"); - } - catch (JacksonException ignored) { } - try { - p.getLongValue(); - fail("expected exception"); - } - catch (JacksonException ignored) { } - try { - p.getDoubleValue(); - fail("expected exception"); - } - catch (JacksonException ignored) { } - try { - p.getFloatValue(); - fail("expected exception"); - } - catch (JacksonException ignored) { } - try { - p.getBigIntegerValue(); - fail("expected exception"); - } - catch (JacksonException ignored) { } - try { - p.getDecimalValue(); - fail("expected exception"); - } - catch (JacksonException ignored) { } - try { - p.getNumberValue(); - fail("expected exception"); - } - catch (JacksonException ignored) { } - assertNull(p.getNumberType()); - } - } - - @Test - public void testGetIntValueFromFractionalDoubleTruncates() throws IOException - { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try (MessagePacker packer = MessagePack.newDefaultPacker(out)) { - packer.packDouble(3.7); - } - MessagePackFactory factory = new MessagePackFactory(); - try (JsonParser p = factory.createParser(ObjectReadContext.empty(), out.toByteArray())) { - assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); - assertEquals(3, p.getIntValue()); - } - } - - // Bug: MessagePackParser.close() has no re-entrancy guard; the base-class - // "if (_closed) { return; }" is bypassed because close() is fully overridden - // without calling super. A second call re-enters _closeInput(), which closes - // the underlying InputStream a second time. - @Test - public void testParserCloseIsIdempotent() throws IOException - { - byte[] bytes = objectMapper.writeValueAsBytes(42); - final int[] closeCount = {0}; - InputStream trackingStream = new ByteArrayInputStream(bytes) { - @Override - public void close() throws IOException - { - closeCount[0]++; - super.close(); - } - }; - - // reuseResourceInParser=false keeps ThreadLocal out of the picture so the - // test isolates the re-entrancy guard in close() itself. - MessagePackFactory nonReuseFactory = - new MessagePackFactory().setReuseResourceInParser(false); - JsonParser parser = - nonReuseFactory.createParser(ObjectReadContext.empty(), trackingStream); - parser.nextToken(); - parser.close(); // first close - parser.close(); // Bug: calls _closeInput() again — stream closed twice - - assertEquals("Stream should be closed exactly once", 1, closeCount[0]); - } - - // Bug: DupDetector.isDup(null) throws NullPointerException when a nil map key - // follows a non-null key and STRICT_DUPLICATE_DETECTION is enabled. - // The fault: name.equals(_firstName) where name is null and _firstName holds - // the prior non-null key string → NullPointerException. - @Test - public void testNilMapKeyWithDupDetectionDoesNotNPE() throws IOException - { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (MessagePacker packer = MessagePack.newDefaultPacker(baos)) { - packer.packMapHeader(2); - packer.packString("foo"); - packer.packInt(1); - packer.packNil(); // nil key — triggers DupDetector.isDup(null) - packer.packInt(2); - } - - MessagePackFactory f = new MessagePackFactoryBuilder() - .enable(StreamReadFeature.STRICT_DUPLICATE_DETECTION) - .build(); - - try (JsonParser p = f.createParser(ObjectReadContext.empty(), baos.toByteArray())) { - assertEquals(JsonToken.START_OBJECT, p.nextToken()); - assertEquals(JsonToken.PROPERTY_NAME, p.nextToken()); - assertEquals("foo", p.currentName()); - assertEquals(JsonToken.VALUE_NUMBER_INT, p.nextToken()); - assertEquals(JsonToken.PROPERTY_NAME, p.nextToken()); // Bug: NPE here - assertNull(p.currentName()); - assertEquals(JsonToken.VALUE_NUMBER_INT, p.nextToken()); - assertEquals(JsonToken.END_OBJECT, p.nextToken()); + catch (JsonEOFException e) { + assertTrue(true); } } } diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java old mode 100644 new mode 100755 similarity index 93% rename from msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java rename to msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java index 36f5c97f..074d7bf5 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java +++ b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java @@ -15,9 +15,9 @@ // package org.msgpack.jackson.dataformat; -import tools.jackson.databind.ObjectMapper; -import org.junit.Before; -import org.junit.Test; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.msgpack.core.MessagePack; import org.msgpack.core.MessagePacker; import org.msgpack.core.MessageUnpacker; @@ -26,11 +26,11 @@ import java.io.IOException; import java.time.Instant; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TimestampExtensionModuleTest { - private ObjectMapper objectMapper; + private final ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); private final SingleInstant singleInstant = new SingleInstant(); private final TripleInstants tripleInstants = new TripleInstants(); @@ -46,13 +46,11 @@ private static class TripleInstants public Instant c; } - @Before + @BeforeEach public void setUp() throws Exception { - objectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .addModule(TimestampExtensionModule.INSTANCE) - .build(); + objectMapper.registerModule(TimestampExtensionModule.INSTANCE); } @Test @@ -88,6 +86,7 @@ public void serialize32BitFormat() byte[] bytes = objectMapper.writeValueAsBytes(singleInstant); + // Check the size of serialized data first try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { unpacker.unpackMapHeader(); assertEquals("instant", unpacker.unpackString()); @@ -109,6 +108,7 @@ public void serialize64BitFormat() byte[] bytes = objectMapper.writeValueAsBytes(singleInstant); + // Check the size of serialized data first try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { unpacker.unpackMapHeader(); assertEquals("instant", unpacker.unpackString()); @@ -130,6 +130,7 @@ public void serialize96BitFormat() byte[] bytes = objectMapper.writeValueAsBytes(singleInstant); + // Check the size of serialized data first try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { unpacker.unpackMapHeader(); assertEquals("instant", unpacker.unpackString()); diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/Benchmarker.java b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/benchmark/Benchmarker.java similarity index 100% rename from msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/Benchmarker.java rename to msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/benchmark/Benchmarker.java diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java similarity index 100% rename from msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java rename to msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java similarity index 100% rename from msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java rename to msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java diff --git a/msgpack-jackson3/README.md b/msgpack-jackson3/README.md deleted file mode 100644 index 9e149e4d..00000000 --- a/msgpack-jackson3/README.md +++ /dev/null @@ -1,480 +0,0 @@ -# jackson-dataformat-msgpack-jackson3 - -This Jackson 3.x extension library is a component to easily read and write [MessagePack](http://msgpack.org/) encoded data through jackson-databind API. - -It extends standard Jackson streaming API (`JsonFactory`, `JsonParser`, `JsonGenerator`), and as such works seamlessly with all the higher level data abstractions (data binding, tree model, and pluggable extensions). - -**Requirements:** Java 17+ and Jackson 3.x. For the Jackson 2.x compatible version, see [`msgpack-jackson`](../msgpack-jackson/). - -**Note on imports:** Jackson 3 moved its core and databind packages from `com.fasterxml.jackson` to `tools.jackson`. User-facing annotations (`@JsonProperty`, `@JsonFormat`, etc.) remain in `com.fasterxml.jackson.annotation` for backward compatibility. - -## Install - -### Maven - -```xml - - org.msgpack - jackson-dataformat-msgpack-jackson3 - (version) - -``` - -### Sbt - -```scala -libraryDependencies += "org.msgpack" % "jackson-dataformat-msgpack-jackson3" % "(version)" -``` - -### Gradle - -```groovy -repositories { - mavenCentral() -} - -dependencies { - implementation 'org.msgpack:jackson-dataformat-msgpack-jackson3:(version)' -} -``` - -## Basic usage - -### Serialization/Deserialization of POJO - -Only thing you need to do is to instantiate `MessagePackFactory` and pass it to the constructor of `tools.jackson.databind.ObjectMapper`. And then, you can use it for MessagePack format data in the same way as jackson-databind. - -```java -// Instantiate ObjectMapper for MessagePack -ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - -// Serialize a Java object to byte array -ExamplePojo pojo = new ExamplePojo("komamitsu"); -byte[] bytes = objectMapper.writeValueAsBytes(pojo); - -// Deserialize the byte array to a Java object -ExamplePojo deserialized = objectMapper.readValue(bytes, ExamplePojo.class); -System.out.println(deserialized.getName()); // => komamitsu -``` - -Or more easily: - -```java -ObjectMapper objectMapper = new MessagePackMapper(); -``` - -We strongly recommend calling `MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString()` if you serialize and/or deserialize BigInteger/BigDecimal values. See [Serialize and deserialize BigDecimal as str type internally in MessagePack format](#serialize-and-deserialize-bigdecimal-as-str-type-internally-in-messagepack-format) for details. - -```java -ObjectMapper objectMapper = MessagePackMapper.builder().handleBigIntegerAndBigDecimalAsString().build(); -``` - -### Serialization/Deserialization of List - -```java -// Instantiate ObjectMapper for MessagePack -ObjectMapper objectMapper = new MessagePackMapper(); - -// Serialize a List to byte array -List list = new ArrayList<>(); -list.add("Foo"); -list.add("Bar"); -list.add(42); -byte[] bytes = objectMapper.writeValueAsBytes(list); - -// Deserialize the byte array to a List -List deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); -System.out.println(deserialized); // => [Foo, Bar, 42] -``` - -### Serialization/Deserialization of Map - -```java -// Instantiate ObjectMapper for MessagePack -ObjectMapper objectMapper = new MessagePackMapper(); - -// Serialize a Map to byte array -Map map = new HashMap<>(); -map.put("name", "komamitsu"); -map.put("age", 42); -byte[] bytes = objectMapper.writeValueAsBytes(map); - -// Deserialize the byte array to a Map -Map deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); -System.out.println(deserialized); // => {name=komamitsu, age=42} -``` - -### Example of Serialization/Deserialization over multiple languages - -Java - -```java -// Serialize -Map obj = new HashMap(); -obj.put("foo", "hello"); -obj.put("bar", "world"); -byte[] bs = objectMapper.writeValueAsBytes(obj); -// bs => [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111, -// -93, 98, 97, 114, -91, 119, 111, 114, 108, 100] -``` - -Ruby - -```ruby -require 'msgpack' - -# Deserialize -xs = [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111, - -93, 98, 97, 114, -91, 119, 111, 114, 108, 100] -MessagePack.unpack(xs.pack("C*")) -# => {"foo"=>"hello", "bar"=>"world"} - -# Serialize -["zero", 1, 2.0, nil].to_msgpack.unpack('C*') -# => [148, 164, 122, 101, 114, 111, 1, 203, 64, 0, 0, 0, 0, 0, 0, 0, 192] -``` - -Java - -```java -// Deserialize -bs = new byte[] {(byte) 148, (byte) 164, 122, 101, 114, 111, 1, - (byte) 203, 64, 0, 0, 0, 0, 0, 0, 0, (byte) 192}; -TypeReference> typeReference = new TypeReference>(){}; -List xs = objectMapper.readValue(bs, typeReference); -// xs => [zero, 1, 2.0, null] -``` - -## Advanced usage - -### Serialize multiple values without closing an output stream - -`tools.jackson.databind.ObjectMapper` closes an output stream by default after it writes a value. If you want to serialize multiple values in a row without closing an output stream, disable `StreamWriteFeature.AUTO_CLOSE_TARGET`. - -```java -OutputStream out = new FileOutputStream(tempFile); -ObjectMapper objectMapper = MessagePackMapper.builder() - .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) - .build(); - -objectMapper.writeValue(out, 1); -objectMapper.writeValue(out, "two"); -objectMapper.writeValue(out, 3.14); -out.close(); - -MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(new FileInputStream(tempFile)); -System.out.println(unpacker.unpackInt()); // => 1 -System.out.println(unpacker.unpackString()); // => two -System.out.println(unpacker.unpackFloat()); // => 3.14 -``` - -### Deserialize multiple values without closing an input stream - -`tools.jackson.databind.ObjectMapper` closes an input stream by default after it reads a value. If you want to deserialize multiple values in a row without closing an input stream, disable `StreamReadFeature.AUTO_CLOSE_SOURCE`. - -```java -MessagePacker packer = MessagePack.newDefaultPacker(new FileOutputStream(tempFile)); -packer.packInt(42); -packer.packString("Hello"); -packer.close(); - -FileInputStream in = new FileInputStream(tempFile); -ObjectMapper objectMapper = MessagePackMapper.builder() - .disable(StreamReadFeature.AUTO_CLOSE_SOURCE) - .build(); -System.out.println(objectMapper.readValue(in, Integer.class)); -System.out.println(objectMapper.readValue(in, String.class)); -in.close(); -``` - -### Serialize not using str8 type - -Old msgpack-java (e.g 0.6.7) doesn't support MessagePack str8 type. When your application needs to communicate with such an old MessagePack library, you can disable the data type like this: - -```java -MessagePack.PackerConfig config = new MessagePack.PackerConfig().withStr8FormatSupport(false); -ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory(config)); -// This string is serialized as bin8 type -byte[] resultWithoutStr8Format = objectMapper.writeValueAsBytes(str8LengthString); -``` - -### Serialize using non-String as a key of Map - -When you want to use non-String value as a key of Map, use `MessagePackKeySerializer` for key serialization. - -```java -@JsonSerialize(keyUsing = MessagePackKeySerializer.class) -private Map intMap = new HashMap<>(); - - : - -intMap.put(42, "Hello"); - -ObjectMapper objectMapper = new MessagePackMapper(); -byte[] bytes = objectMapper.writeValueAsBytes(intMap); - -Map deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); -System.out.println(deserialized); // => {42=Hello} -``` - -### Serialize and deserialize BigDecimal as str type internally in MessagePack format - -`jackson-dataformat-msgpack-jackson3` represents BigDecimal values as float type in MessagePack format by default for backward compatibility. But the default behavior could fail when handling too large value for `double` type. So we strongly recommend calling `MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString()` to internally handle BigDecimal values as String. - -```java -ObjectMapper objectMapper = MessagePackMapper.builder().handleBigIntegerAndBigDecimalAsString().build(); - -Pojo obj = new Pojo(); -// This value is too large to be serialized as double -obj.value = new BigDecimal("1234567890.98765432100"); - -byte[] converted = objectMapper.writeValueAsBytes(obj); - -System.out.println(objectMapper.readValue(converted, Pojo.class)); // => Pojo{value=1234567890.98765432100} -``` - -`MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString()` is equivalent to the following configuration. - -```java -ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); -objectMapper.configOverride(BigInteger.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); -objectMapper.configOverride(BigDecimal.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); -``` - -### 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 = MessagePackMapper.builder() - .addModule(TimestampExtensionModule.INSTANCE) - .build(); -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 your own custom extension types easily. - -#### Deserialize extension type value directly - -```java -// In this application, extension type 59 is used for byte[] -byte[] bytes; -{ - // This ObjectMapper is just for temporary serialization - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - MessagePacker packer = MessagePack.newDefaultPacker(outputStream); - - packer.packExtensionTypeHeader((byte) 59, hexspeak.length); - packer.addPayload(hexspeak); - packer.close(); - - bytes = outputStream.toByteArray(); -} - -// Register the type and a deserializer to ExtensionTypeCustomDeserializers -ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); -extTypeCustomDesers.addCustomDeser((byte) 59, data -> { - if (Arrays.equals(data, - new byte[] {(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE})) { - return "Java"; - } - return "Not Java"; -}); - -ObjectMapper objectMapper = new ObjectMapper( - new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)); - -System.out.println(objectMapper.readValue(bytes, Object.class)); - // => Java -``` - -#### Use extension type as Map key - -```java -static class TripleBytesPojo -{ - public byte first; - public byte second; - public byte third; - - public TripleBytesPojo(byte first, byte second, byte third) - { - this.first = first; - this.second = second; - this.third = third; - } - - @Override - public boolean equals(Object o) - { - : - } - - @Override - public int hashCode() - { - : - } - - @Override - public String toString() - { - // This key format is used when serialized as map key - return String.format("%d-%d-%d", first, second, third); - } - - static class KeyDeserializer - extends tools.jackson.databind.KeyDeserializer - { - @Override - public Object deserializeKey(String key, DeserializationContext ctxt) - { - String[] values = key.split("-"); - return new TripleBytesPojo(Byte.parseByte(values[0]), Byte.parseByte(values[1]), Byte.parseByte(values[2])); - } - } - - static TripleBytesPojo deserialize(byte[] bytes) - { - return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]); - } -} - -: - -byte extTypeCode = 42; - -ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); -extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() -{ - @Override - public Object deserialize(byte[] value) - throws IOException - { - return TripleBytesPojo.deserialize(value); - } -}); - -SimpleModule module = new SimpleModule(); -module.addKeyDeserializer(TripleBytesPojo.class, new TripleBytesPojo.KeyDeserializer()); -ObjectMapper objectMapper = MessagePackMapper.builder( - new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) - .addModule(module) - .build(); - -Map deserializedMap = - objectMapper.readValue(serializedData, - new TypeReference>() {}); -``` - -#### Use extension type as Map value - -```java -static class TripleBytesPojo -{ - public byte first; - public byte second; - public byte third; - - public TripleBytesPojo(byte first, byte second, byte third) - { - this.first = first; - this.second = second; - this.third = third; - } - - static class Deserializer - extends StdDeserializer - { - protected Deserializer() - { - super(TripleBytesPojo.class); - } - - @Override - public TripleBytesPojo deserialize(JsonParser p, DeserializationContext ctxt) - { - return TripleBytesPojo.deserialize(p.getBinaryValue()); - } - } - - static TripleBytesPojo deserialize(byte[] bytes) - { - return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]); - } -} - -: - -byte extTypeCode = 42; - -ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); -extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() -{ - @Override - public Object deserialize(byte[] value) - throws IOException - { - return TripleBytesPojo.deserialize(value); - } -}); - -SimpleModule module = new SimpleModule(); -module.addDeserializer(TripleBytesPojo.class, new TripleBytesPojo.Deserializer()); -ObjectMapper objectMapper = MessagePackMapper.builder( - new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) - .addModule(module) - .build(); - -Map deserializedMap = - objectMapper.readValue(serializedData, - new TypeReference>() {}); -``` - -### Serialize a nested object that also serializes - -When you serialize an object that has a nested object also serializing with ObjectMapper and MessagePackFactory like the following code, it throws NullPointerException since the nested MessagePackFactory modifies a shared state stored in ThreadLocal. - -```java -@Test -public void testNestedSerialization() throws Exception -{ - ObjectMapper objectMapper = new MessagePackMapper(); - objectMapper.writeValueAsBytes(new OuterClass()); -} - -public class OuterClass -{ - public String getInner() throws JacksonException - { - ObjectMapper m = new MessagePackMapper(); - m.writeValueAsBytes(new InnerClass()); - return "EFG"; - } -} - -public class InnerClass -{ - public String getName() - { - return "ABC"; - } -} -``` - -There are a few options to fix this issue, but they introduce performance degradations while this usage is a corner case. A workaround that doesn't affect performance is to call `MessagePackFactory#setReuseResourceInGenerator(false)`. It might be inconvenient to call the API for users, but it's a reasonable tradeoff with performance for now. - -```java -ObjectMapper objectMapper = new ObjectMapper( - new MessagePackFactory().setReuseResourceInGenerator(false)); -``` diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java deleted file mode 100644 index 1b1e048b..00000000 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java +++ /dev/null @@ -1,47 +0,0 @@ -// -// 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 tools.jackson.databind.cfg.MapperConfig; -import tools.jackson.databind.introspect.Annotated; -import tools.jackson.databind.introspect.JacksonAnnotationIntrospector; - -import com.fasterxml.jackson.annotation.JsonFormat; - -import static com.fasterxml.jackson.annotation.JsonFormat.Shape.ARRAY; - -/** - * Provides the ability of serializing POJOs without their schema. - * Similar to @JsonFormat annotation with JsonFormat.Shape.ARRAY, but in a programmatic - * way. - * - * This also provides same behavior as msgpack-java 0.6.x serialization api. - */ -public class JsonArrayFormat extends JacksonAnnotationIntrospector -{ - private static final JsonFormat.Value ARRAY_FORMAT = new JsonFormat.Value().withShape(ARRAY); - - @Override - public JsonFormat.Value findFormat(MapperConfig config, Annotated ann) - { - JsonFormat.Value precedenceFormat = super.findFormat(config, ann); - if (precedenceFormat != null) { - return precedenceFormat; - } - - return ARRAY_FORMAT; - } -} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java deleted file mode 100644 index e6b0bf01..00000000 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java +++ /dev/null @@ -1,255 +0,0 @@ -// -// 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 tools.jackson.core.ErrorReportConfiguration; -import tools.jackson.core.FormatFeature; -import tools.jackson.core.FormatSchema; -import tools.jackson.core.JacksonException; -import tools.jackson.core.JsonGenerator; -import tools.jackson.core.JsonParser; -import tools.jackson.core.ObjectReadContext; -import tools.jackson.core.ObjectWriteContext; -import tools.jackson.core.StreamReadConstraints; -import tools.jackson.core.StreamWriteConstraints; -import tools.jackson.core.TSFBuilder; -import tools.jackson.core.TokenStreamFactory; -import tools.jackson.core.Version; -import tools.jackson.core.base.BinaryTSFactory; -import tools.jackson.core.io.IOContext; -import org.msgpack.core.MessagePack; -import org.msgpack.core.annotations.VisibleForTesting; - -import org.msgpack.core.buffer.ArrayBufferInput; -import org.msgpack.core.buffer.InputStreamBufferInput; -import org.msgpack.core.buffer.MessageBufferInput; - -import java.io.DataInput; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public class MessagePackFactory - extends BinaryTSFactory - implements java.io.Serializable -{ - private static final long serialVersionUID = 2578263992015504348L; - - private final MessagePack.PackerConfig packerConfig; - private boolean reuseResourceInGenerator = true; - private boolean reuseResourceInParser = true; - private boolean supportIntegerKeys = false; - private ExtensionTypeCustomDeserializers extTypeCustomDesers; - - public MessagePackFactory() - { - this(MessagePack.DEFAULT_PACKER_CONFIG); - } - - public MessagePackFactory(MessagePack.PackerConfig packerConfig) - { - super(StreamReadConstraints.defaults(), StreamWriteConstraints.defaults(), - ErrorReportConfiguration.defaults(), 0, 0); - this.packerConfig = packerConfig; - } - - public MessagePackFactory(MessagePackFactory src) - { - super(src); - this.packerConfig = src.packerConfig.clone(); - this.reuseResourceInGenerator = src.reuseResourceInGenerator; - this.reuseResourceInParser = src.reuseResourceInParser; - this.supportIntegerKeys = src.supportIntegerKeys; - if (src.extTypeCustomDesers != null) { - this.extTypeCustomDesers = new ExtensionTypeCustomDeserializers(src.extTypeCustomDesers); - } - } - - protected MessagePackFactory(MessagePackFactoryBuilder b) - { - super(b); - this.packerConfig = b.packerConfig().clone(); - this.reuseResourceInGenerator = b.reuseResourceInGenerator(); - this.reuseResourceInParser = b.reuseResourceInParser(); - this.supportIntegerKeys = b.supportIntegerKeys(); - this.extTypeCustomDesers = b.extTypeCustomDesers(); - } - - public MessagePackFactory setReuseResourceInGenerator(boolean reuseResourceInGenerator) - { - this.reuseResourceInGenerator = reuseResourceInGenerator; - return this; - } - - public MessagePackFactory setReuseResourceInParser(boolean reuseResourceInParser) - { - this.reuseResourceInParser = reuseResourceInParser; - return this; - } - - public MessagePackFactory setSupportIntegerKeys(boolean supportIntegerKeys) - { - this.supportIntegerKeys = supportIntegerKeys; - return this; - } - - public MessagePackFactory setExtTypeCustomDesers(ExtensionTypeCustomDeserializers extTypeCustomDesers) - { - this.extTypeCustomDesers = extTypeCustomDesers; - return this; - } - - @Override - protected JsonParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt, - InputStream in) throws JacksonException - { - try { - MessagePackParser parser = new MessagePackParser(readCtxt, ioCtxt, - readCtxt.getStreamReadFeatures(_streamReadFeatures), - new InputStreamBufferInput(in), in, reuseResourceInParser); - if (extTypeCustomDesers != null) { - parser.setExtensionTypeCustomDeserializers(extTypeCustomDesers); - } - return parser; - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - } - - @Override - protected JsonParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt, - byte[] data, int offset, int len) throws JacksonException - { - try { - MessageBufferInput input = new ArrayBufferInput(data, offset, len); - MessagePackParser parser = new MessagePackParser(readCtxt, ioCtxt, - readCtxt.getStreamReadFeatures(_streamReadFeatures), input, data, reuseResourceInParser); - if (extTypeCustomDesers != null) { - parser.setExtensionTypeCustomDeserializers(extTypeCustomDesers); - } - return parser; - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - } - - @Override - protected JsonParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt, - DataInput input) throws JacksonException - { - return _unsupported(); - } - - @Override - protected JsonGenerator _createGenerator(ObjectWriteContext writeCtxt, IOContext ioCtxt, - OutputStream out) throws JacksonException - { - try { - return new MessagePackGenerator(writeCtxt, ioCtxt, - writeCtxt.getStreamWriteFeatures(_streamWriteFeatures), - out, packerConfig, reuseResourceInGenerator, supportIntegerKeys); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - } - - @Override - public TokenStreamFactory copy() - { - return new MessagePackFactory(this); - } - - @Override - public TokenStreamFactory snapshot() - { - return copy(); - } - - @Override - public TSFBuilder rebuild() - { - return new MessagePackFactoryBuilder(this); - } - - @Override - public Version version() - { - return PackageVersion.VERSION; - } - - @VisibleForTesting - MessagePack.PackerConfig getPackerConfig() - { - return packerConfig; - } - - @VisibleForTesting - boolean isReuseResourceInGenerator() - { - return reuseResourceInGenerator; - } - - @VisibleForTesting - boolean isReuseResourceInParser() - { - return reuseResourceInParser; - } - - @VisibleForTesting - boolean isSupportIntegerKeys() - { - return supportIntegerKeys; - } - - @VisibleForTesting - ExtensionTypeCustomDeserializers getExtTypeCustomDesers() - { - return extTypeCustomDesers; - } - - @Override - public String getFormatName() - { - return "msgpack"; - } - - @Override - public boolean canParseAsync() - { - return false; - } - - @Override - public boolean canUseSchema(FormatSchema schema) - { - return false; - } - - @Override - public Class getFormatReadFeatureType() - { - return null; - } - - @Override - public Class getFormatWriteFeatureType() - { - return null; - } -} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java deleted file mode 100644 index cc6e9137..00000000 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java +++ /dev/null @@ -1,1041 +0,0 @@ -// -// 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 tools.jackson.core.Base64Variant; -import tools.jackson.core.JacksonException; -import tools.jackson.core.util.JacksonFeatureSet; -import tools.jackson.core.JsonGenerator; -import tools.jackson.core.ObjectWriteContext; -import tools.jackson.core.SerializableString; -import tools.jackson.core.StreamWriteCapability; -import tools.jackson.core.StreamWriteFeature; -import tools.jackson.core.json.DupDetector; -import tools.jackson.core.TokenStreamContext; -import tools.jackson.core.base.GeneratorBase; -import tools.jackson.core.io.IOContext; -import org.msgpack.core.MessagePack; -import org.msgpack.core.MessagePacker; -import org.msgpack.core.buffer.MessageBufferOutput; -import org.msgpack.core.buffer.OutputStreamBufferOutput; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.Reader; - -import java.math.BigDecimal; -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -public class MessagePackGenerator - extends GeneratorBase -{ - private static final int IN_ROOT = 0; - private static final int IN_OBJECT = 1; - private static final int IN_ARRAY = 2; - private final MessagePacker messagePacker; - // Retained heap per idle thread: ~8 KB (OutputStreamBufferOutput + internal MessageBuffer). - // Negligible compared to Jackson's own per-thread buffer retention. - private static final ThreadLocal messageBufferOutputHolder = new ThreadLocal<>(); - private final OutputStream output; - private final MessagePack.PackerConfig packerConfig; - private final boolean supportIntegerKeys; - - private int currentParentElementIndex = -1; - private int currentState = IN_ROOT; - private final List nodes; - private boolean isElementsClosed = false; - private MessagePackWriteContext writeContext; - private final boolean ownsThreadLocalBuffer; - - private static final class RawUtf8String - { - public final byte[] bytes; - public final int offset; - public final int len; - - public RawUtf8String(byte[] bytes, int offset, int len) - { - this.bytes = bytes; - this.offset = offset; - this.len = len; - } - } - - private abstract static class Node - { - // Root containers have -1. - final int parentIndex; - - public Node(int parentIndex) - { - this.parentIndex = parentIndex; - } - - abstract void incrementChildCount(); - - abstract int currentStateAsParent(); - } - - private abstract static class NodeContainer extends Node - { - // Only for containers. - int childCount; - - public NodeContainer(int parentIndex) - { - super(parentIndex); - } - - @Override - void incrementChildCount() - { - childCount++; - } - } - - private static final class NodeArray extends NodeContainer - { - public NodeArray(int parentIndex) - { - super(parentIndex); - } - - @Override - int currentStateAsParent() - { - return IN_ARRAY; - } - } - - private static final class NodeObject extends NodeContainer - { - public NodeObject(int parentIndex) - { - super(parentIndex); - } - - @Override - int currentStateAsParent() - { - return IN_OBJECT; - } - } - - private static final class NodeEntryInArray extends Node - { - final Object value; - - public NodeEntryInArray(int parentIndex, Object value) - { - super(parentIndex); - this.value = value; - } - - @Override - void incrementChildCount() - { - throw new UnsupportedOperationException(); - } - - @Override - int currentStateAsParent() - { - throw new UnsupportedOperationException(); - } - } - - private static final class NodeEntryInObject extends Node - { - final Object key; - // Lazily initialized. - Object value; - - public NodeEntryInObject(int parentIndex, Object key) - { - super(parentIndex); - this.key = key; - } - - @Override - void incrementChildCount() - { - assert value instanceof NodeContainer; - ((NodeContainer) value).childCount++; - } - - @Override - int currentStateAsParent() - { - if (value instanceof NodeObject) { - return IN_OBJECT; - } - else if (value instanceof NodeArray) { - return IN_ARRAY; - } - else { - throw new AssertionError(); - } - } - } - - // Internal constructor for nested serialization. - private MessagePackGenerator( - ObjectWriteContext writeCtxt, - IOContext ioCtxt, - int streamWriteFeatures, - OutputStream out, - MessagePack.PackerConfig packerConfig, - boolean supportIntegerKeys) - { - super(writeCtxt, ioCtxt, streamWriteFeatures); - this.output = out; - this.messagePacker = packerConfig.newPacker(out); - this.packerConfig = packerConfig; - this.nodes = new ArrayList<>(); - this.supportIntegerKeys = supportIntegerKeys; - this.writeContext = MessagePackWriteContext.createRootContext( - StreamWriteFeature.STRICT_DUPLICATE_DETECTION.enabledIn(streamWriteFeatures) - ? DupDetector.rootDetector(this) : null); - this.ownsThreadLocalBuffer = false; - } - - public MessagePackGenerator( - ObjectWriteContext writeCtxt, - IOContext ioCtxt, - int streamWriteFeatures, - OutputStream out, - MessagePack.PackerConfig packerConfig, - boolean reuseResourceInGenerator, - boolean supportIntegerKeys) - throws IOException - { - super(writeCtxt, ioCtxt, streamWriteFeatures); - this.output = out; - this.messagePacker = packerConfig.newPacker(getMessageBufferOutputForOutputStream(out, reuseResourceInGenerator)); - this.packerConfig = packerConfig; - this.nodes = new ArrayList<>(); - this.supportIntegerKeys = supportIntegerKeys; - this.writeContext = MessagePackWriteContext.createRootContext( - StreamWriteFeature.STRICT_DUPLICATE_DETECTION.enabledIn(streamWriteFeatures) - ? DupDetector.rootDetector(this) : null); - this.ownsThreadLocalBuffer = reuseResourceInGenerator; - } - - private MessageBufferOutput getMessageBufferOutputForOutputStream( - OutputStream out, - boolean reuseResourceInGenerator) - throws IOException - { - OutputStreamBufferOutput messageBufferOutput; - if (reuseResourceInGenerator) { - messageBufferOutput = messageBufferOutputHolder.get(); - if (messageBufferOutput == null) { - messageBufferOutput = new OutputStreamBufferOutput(out); - messageBufferOutputHolder.set(messageBufferOutput); - } - else { - messageBufferOutput.reset(out); - } - } - else { - messageBufferOutput = new OutputStreamBufferOutput(out); - } - return messageBufferOutput; - } - - private String currentStateStr() - { - switch (currentState) { - case IN_OBJECT: - return "IN_OBJECT"; - case IN_ARRAY: - return "IN_ARRAY"; - default: - return "IN_ROOT"; - } - } - - @Override - public JsonGenerator writeStartArray() throws JacksonException - { - return writeStartArray(null); - } - - @Override - public JsonGenerator writeStartArray(Object currentValue) throws JacksonException - { - return writeStartArray(currentValue, -1); - } - - // size is ignored: element count is determined at flush time from the actual child nodes. - // When size >= 0, it could in principle be used to skip buffering and write the array - // header immediately, but Jackson does not guarantee it — dynamic filters (Views, - // @JsonFilter) evaluate entries incrementally and will pass -1 even for known-size - // collections. Backends must handle both cases (Jackson author confirmed, see #841). - @Override - public JsonGenerator writeStartArray(Object currentValue, int size) throws JacksonException - { - _verifyValueWrite("start an array"); - writeContext = writeContext.createChildArrayContext(currentValue); - if (currentState == IN_OBJECT) { - Node node = nodes.get(nodes.size() - 1); - assert node instanceof NodeEntryInObject; - NodeEntryInObject nodeEntryInObject = (NodeEntryInObject) node; - nodeEntryInObject.value = new NodeArray(currentParentElementIndex); - } - else { - if (isElementsClosed) { - flush(); - } - nodes.add(new NodeArray(currentParentElementIndex)); - } - currentParentElementIndex = nodes.size() - 1; - currentState = IN_ARRAY; - return this; - } - - @Override - public JsonGenerator writeEndArray() throws JacksonException - { - if (currentState != IN_ARRAY) { - _reportError("Current context not an array but " + currentStateStr()); - } - endCurrentContainer(); - return this; - } - - @Override - public JsonGenerator writeStartObject() throws JacksonException - { - return writeStartObject(null); - } - - @Override - public JsonGenerator writeStartObject(Object currentValue) throws JacksonException - { - return writeStartObject(currentValue, -1); - } - - // size is ignored: same reasoning as writeStartArray(Object, int) above. - @Override - public JsonGenerator writeStartObject(Object forValue, int size) throws JacksonException - { - _verifyValueWrite("start an object"); - writeContext = writeContext.createChildObjectContext(forValue); - if (currentState == IN_OBJECT) { - Node node = nodes.get(nodes.size() - 1); - assert node instanceof NodeEntryInObject; - NodeEntryInObject nodeEntryInObject = (NodeEntryInObject) node; - nodeEntryInObject.value = new NodeObject(currentParentElementIndex); - } - else { - if (isElementsClosed) { - flush(); - } - nodes.add(new NodeObject(currentParentElementIndex)); - } - currentParentElementIndex = nodes.size() - 1; - currentState = IN_OBJECT; - return this; - } - - @Override - public JsonGenerator writeEndObject() throws JacksonException - { - if (currentState != IN_OBJECT) { - _reportError("Current context not an object but " + currentStateStr()); - } - if (writeContext.isExpectingValue()) { - _reportError("Cannot close Object, property name written but no value"); - } - endCurrentContainer(); - return this; - } - - private void endCurrentContainer() - { - writeContext = writeContext.getParent(); - Node parent = nodes.get(currentParentElementIndex); - if (parent.parentIndex == -1) { - isElementsClosed = true; - currentParentElementIndex = parent.parentIndex; - currentState = IN_ROOT; - return; - } - - currentParentElementIndex = parent.parentIndex; - Node currentParent = nodes.get(currentParentElementIndex); - currentParent.incrementChildCount(); - currentState = currentParent.currentStateAsParent(); - } - - private void packNonContainer(Object v) - throws IOException - { - MessagePacker messagePacker = getMessagePacker(); - if (v instanceof String) { - messagePacker.packString((String) v); - } - else if (v instanceof RawUtf8String) { - RawUtf8String raw = (RawUtf8String) v; - messagePacker.packRawStringHeader(raw.len); - messagePacker.writePayload(raw.bytes, raw.offset, raw.len); - } - else if (v instanceof Integer) { - messagePacker.packInt((Integer) v); - } - else if (v == null) { - messagePacker.packNil(); - } - else if (v instanceof Float) { - messagePacker.packFloat((Float) v); - } - else if (v instanceof Long) { - messagePacker.packLong((Long) v); - } - else if (v instanceof Double) { - messagePacker.packDouble((Double) v); - } - else if (v instanceof BigInteger) { - messagePacker.packBigInteger((BigInteger) v); - } - else if (v instanceof BigDecimal) { - packBigDecimal((BigDecimal) v); - } - else if (v instanceof Boolean) { - messagePacker.packBoolean((Boolean) v); - } - else if (v instanceof ByteBuffer) { - ByteBuffer bb = (ByteBuffer) v; - int len = bb.remaining(); - if (bb.hasArray() && !bb.isReadOnly()) { - messagePacker.packBinaryHeader(len); - messagePacker.writePayload(bb.array(), bb.arrayOffset() + bb.position(), len); - } - else { - byte[] data = new byte[len]; - bb.duplicate().get(data); - messagePacker.packBinaryHeader(len); - messagePacker.addPayload(data); - } - } - else if (v instanceof MessagePackExtensionType) { - MessagePackExtensionType extensionType = (MessagePackExtensionType) v; - byte[] extData = extensionType.getData(); - messagePacker.packExtensionTypeHeader(extensionType.getType(), extData.length); - messagePacker.writePayload(extData); - } - else { - messagePacker.flush(); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - try (MessagePackGenerator messagePackGenerator = new MessagePackGenerator( - objectWriteContext(), _ioContext, _streamWriteFeatures, - outputStream, packerConfig, supportIntegerKeys)) { - objectWriteContext().writeValue(messagePackGenerator, v); - } - output.write(outputStream.toByteArray()); - } - } - - private void packBigDecimal(BigDecimal decimal) - throws IOException - { - MessagePacker messagePacker = getMessagePacker(); - boolean failedToPackAsBI = false; - try { - //Check to see if this BigDecimal can be converted to BigInteger - BigInteger integer = decimal.toBigIntegerExact(); - messagePacker.packBigInteger(integer); - } - catch (ArithmeticException | IllegalArgumentException e) { - failedToPackAsBI = true; - } - - if (failedToPackAsBI) { - double doubleValue = decimal.doubleValue(); - //Check to make sure this BigDecimal can be represented as a double - if (Double.isInfinite(doubleValue) || decimal.compareTo(BigDecimal.valueOf(doubleValue)) != 0) { - throw new IllegalArgumentException("MessagePack cannot serialize a BigDecimal that can't be represented as double. " + decimal); - } - messagePacker.packDouble(doubleValue); - } - } - - private void packObject(NodeObject container) - throws IOException - { - MessagePacker messagePacker = getMessagePacker(); - messagePacker.packMapHeader(container.childCount); - } - - private void packArray(NodeArray container) - throws IOException - { - MessagePacker messagePacker = getMessagePacker(); - messagePacker.packArrayHeader(container.childCount); - } - - private void addKeyNode(Object key) - { - if (currentState != IN_OBJECT) { - _reportError("Can not write a property name, expecting a value"); - } - nodes.add(new NodeEntryInObject(currentParentElementIndex, key)); - } - - private void addValueNode(Object value) throws IOException - { - if (!writeContext.writeValue()) { - _reportError("Cannot write value: expecting a property name in Object context"); - } - switch (currentState) { - case IN_OBJECT: { - Node node = nodes.get(nodes.size() - 1); - assert node instanceof NodeEntryInObject; - NodeEntryInObject nodeEntryInObject = (NodeEntryInObject) node; - nodeEntryInObject.value = value; - nodes.get(node.parentIndex).incrementChildCount(); - break; - } - case IN_ARRAY: { - Node node = new NodeEntryInArray(currentParentElementIndex, value); - nodes.add(node); - nodes.get(node.parentIndex).incrementChildCount(); - break; - } - default: - // Flush any buffered root container before packing a root scalar, - // otherwise the scalar would be emitted before the container. - if (isElementsClosed) { - flush(); - } - packNonContainer(value); - flushMessagePacker(); - break; - } - } - - private void writeCharArrayTextValue(char[] text, int offset, int len) throws IOException - { - addValueNode(new String(text, offset, len)); - } - - private void writeByteArrayTextValue(byte[] text, int offset, int len) throws IOException - { - addValueNode(new RawUtf8String(text, offset, len)); - } - - @Override - public JsonGenerator writePropertyId(long id) throws JacksonException - { - if (this.supportIntegerKeys) { - if (!writeContext.writeName(String.valueOf(id))) { - _reportError("Can not write a property id, expecting a value"); - } - addKeyNode(id); - } - else { - writeName(String.valueOf(id)); - } - return this; - } - - @Override - public JacksonFeatureSet streamWriteCapabilities() - { - return DEFAULT_BINARY_WRITE_CAPABILITIES; - } - - @Override - public JsonGenerator writeName(String name) throws JacksonException - { - if (!writeContext.writeName(name)) { - _reportError("Can not write a property name, expecting a value"); - } - addKeyNode(name); - return this; - } - - @Override - public JsonGenerator writeName(SerializableString name) throws JacksonException - { - if (name instanceof MessagePackSerializedString) { - if (!writeContext.writeName(name.getValue())) { - _reportError("Can not write a property name, expecting a value"); - } - addKeyNode(((MessagePackSerializedString) name).getRawValue()); - } - else { - writeName(name.getValue()); - } - return this; - } - - @Override - public JsonGenerator writeString(String text) throws JacksonException - { - try { - addValueNode(text); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - return this; - } - - @Override - public JsonGenerator writeString(char[] text, int offset, int len) throws JacksonException - { - try { - writeCharArrayTextValue(text, offset, len); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - return this; - } - - @Override - public JsonGenerator writeString(Reader reader, int len) throws JacksonException - { - try { - long remaining = len < 0 ? Long.MAX_VALUE : len; - // Cap chunk size: len is a caller hint and can be arbitrarily large. - // Pre-allocating new StringBuilder(len) would reserve len*2 bytes upfront, - // which is an OOM risk for large inputs. The StringBuilder grows as needed. - int chunkSize = (int) Math.min(remaining, 8192); - StringBuilder sb = new StringBuilder(chunkSize); - char[] tmpBuf = new char[chunkSize]; - while (remaining > 0) { - int read = reader.read(tmpBuf, 0, (int) Math.min(remaining, tmpBuf.length)); - if (read < 0) { - break; - } - sb.append(tmpBuf, 0, read); - remaining -= read; - } - addValueNode(sb.toString()); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - return this; - } - - @Override - public JsonGenerator writeString(SerializableString text) throws JacksonException - { - return writeString(text.getValue()); - } - - @Override - public JsonGenerator writeRawUTF8String(byte[] text, int offset, int length) throws JacksonException - { - try { - writeByteArrayTextValue(text, offset, length); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - return this; - } - - @Override - public JsonGenerator writeUTF8String(byte[] text, int offset, int length) throws JacksonException - { - try { - writeByteArrayTextValue(text, offset, length); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - return this; - } - - @Override - public JsonGenerator writeRaw(String text) throws JacksonException - { - try { - addValueNode(text); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - return this; - } - - @Override - public JsonGenerator writeRaw(String text, int offset, int len) throws JacksonException - { - try { - addValueNode(text.substring(offset, offset + len)); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - return this; - } - - @Override - public JsonGenerator writeRaw(char[] text, int offset, int len) throws JacksonException - { - try { - writeCharArrayTextValue(text, offset, len); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - return this; - } - - @Override - public JsonGenerator writeRaw(char c) throws JacksonException - { - try { - writeCharArrayTextValue(new char[] { c }, 0, 1); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - return this; - } - - @Override - public JsonGenerator writeBinary(Base64Variant b64variant, byte[] data, int offset, int len) throws JacksonException - { - try { - addValueNode(ByteBuffer.wrap(data, offset, len)); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - return this; - } - - @Override - public JsonGenerator writeNumber(short v) throws JacksonException - { - return writeNumber((int) v); - } - - @Override - public JsonGenerator writeNumber(int v) throws JacksonException - { - try { - addValueNode(v); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - return this; - } - - @Override - public JsonGenerator writeNumber(long v) throws JacksonException - { - try { - addValueNode(v); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - return this; - } - - @Override - public JsonGenerator writeNumber(BigInteger v) throws JacksonException - { - try { - addValueNode(v); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - return this; - } - - @Override - public JsonGenerator writeNumber(double d) throws JacksonException - { - try { - addValueNode(d); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - return this; - } - - @Override - public JsonGenerator writeNumber(float f) throws JacksonException - { - try { - addValueNode(f); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - return this; - } - - @Override - public JsonGenerator writeNumber(BigDecimal dec) throws JacksonException - { - try { - addValueNode(dec); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - return this; - } - - @Override - public JsonGenerator writeNumber(String encodedValue) throws JacksonException - { - // There is a room to improve this API's performance while the implementation is robust. - // If users can use other MessagePackGenerator#writeNumber APIs that accept - // proper numeric types not String, it's better to use the other APIs instead. - try { - try { - long l = Long.parseLong(encodedValue); - addValueNode(l); - return this; - } - catch (NumberFormatException ignored) { - } - - try { - BigInteger bi = new BigInteger(encodedValue); - addValueNode(bi); - return this; - } - catch (NumberFormatException ignored) { - } - - try { - BigDecimal bd = new BigDecimal(encodedValue); - double d = bd.doubleValue(); - - // Check if the double can perfectly represent the exact decimal value. - // isInfinite guard: values like "1e309" overflow double to Infinity; keep as BigDecimal. - if (!Double.isInfinite(d) && bd.compareTo(new BigDecimal(String.valueOf(d))) == 0) { - // It's a safe ordinary floating-point number. - addValueNode(d); - } - else { - // It has more precision than a double can handle, or overflows double range. - addValueNode(bd); - } - return this; - } - catch (NumberFormatException e) { - // Fall back for NaN, Infinity, -Infinity which BigDecimal rejects. - try { - double d = Double.parseDouble(encodedValue); - addValueNode(d); - return this; - } - catch (NumberFormatException ignored) { - } - } - - throw new NumberFormatException(encodedValue); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - } - - @Override - public JsonGenerator writeBoolean(boolean state) throws JacksonException - { - try { - addValueNode(state); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - return this; - } - - @Override - public JsonGenerator writeNull() throws JacksonException - { - try { - addValueNode(null); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - return this; - } - - public void writeExtensionType(MessagePackExtensionType extensionType) - { - try { - addValueNode(extensionType); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - } - - @Override - public void close() throws JacksonException - { - if (!_closed) { - try { - flush(); - } - finally { - super.close(); - } - } - } - - @Override - public void flush() throws JacksonException - { - if (!isElementsClosed) { - // The whole elements are not closed yet. - return; - } - - try { - for (int i = 0; i < nodes.size(); i++) { - Node node = nodes.get(i); - if (node instanceof NodeEntryInObject) { - NodeEntryInObject nodeEntry = (NodeEntryInObject) node; - packNonContainer(nodeEntry.key); - if (nodeEntry.value instanceof NodeObject) { - packObject((NodeObject) nodeEntry.value); - } - else if (nodeEntry.value instanceof NodeArray) { - packArray((NodeArray) nodeEntry.value); - } - else { - packNonContainer(nodeEntry.value); - } - } - else if (node instanceof NodeObject) { - packObject((NodeObject) node); - } - else if (node instanceof NodeEntryInArray) { - packNonContainer(((NodeEntryInArray) node).value); - } - else if (node instanceof NodeArray) { - packArray((NodeArray) node); - } - else { - throw new AssertionError(); - } - } - flushMessagePacker(); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - nodes.clear(); - isElementsClosed = false; - } - - private void flushMessagePacker() - throws IOException - { - getMessagePacker().flush(); - } - - @Override - public tools.jackson.core.Version version() - { - return PackageVersion.VERSION; - } - - @Override - public TokenStreamContext streamWriteContext() - { - return writeContext; - } - - @Override - public Object streamWriteOutputTarget() - { - return output; - } - - @Override - public int streamWriteOutputBuffered() - { - return -1; - } - - @Override - public Object currentValue() - { - return writeContext.currentValue(); - } - - @Override - public void assignCurrentValue(Object v) - { - writeContext.assignCurrentValue(v); - } - - @Override - protected void _closeInput() throws IOException - { - if (StreamWriteFeature.AUTO_CLOSE_TARGET.enabledIn(_streamWriteFeatures)) { - messagePacker.close(); - } - } - - @Override - protected void _releaseBuffers() - { - // No null check on get(): generators are single-threaded by contract so this - // ThreadLocal is always set on the calling thread. A null here would indicate - // cross-thread misuse; letting it NPE surfaces that bug immediately. - if (ownsThreadLocalBuffer) { - OutputStreamBufferOutput buf = messageBufferOutputHolder.get(); - if (buf != null) { - try { - buf.reset(null); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - } - } - } - - @Override - protected void _verifyValueWrite(String typeMsg) throws JacksonException - { - if (!writeContext.writeValue()) { - _reportError("Cannot " + typeMsg + ", expecting a property name"); - } - } - - private MessagePacker getMessagePacker() - { - return messagePacker; - } -} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java deleted file mode 100644 index 674b2733..00000000 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java +++ /dev/null @@ -1,120 +0,0 @@ -// -// 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.annotation.JsonFormat; - -import tools.jackson.core.Version; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.cfg.MapperBuilder; -import tools.jackson.databind.cfg.MapperBuilderState; - -import java.math.BigDecimal; -import java.math.BigInteger; - -public class MessagePackMapper extends ObjectMapper -{ - private static final long serialVersionUID = 3L; - - public static class Builder extends MapperBuilder - { - public Builder(MessagePackFactory f) - { - super(f); - } - - protected Builder(StateImpl state) - { - super(state); - } - - @Override - public MessagePackMapper build() - { - return new MessagePackMapper(this); - } - - public Builder handleBigIntegerAsString() - { - return withConfigOverride(BigInteger.class, - o -> o.setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING))); - } - - public Builder handleBigDecimalAsString() - { - return withConfigOverride(BigDecimal.class, - o -> o.setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING))); - } - - public Builder handleBigIntegerAndBigDecimalAsString() - { - return handleBigIntegerAsString().handleBigDecimalAsString(); - } - - @Override - protected MapperBuilderState _saveState() - { - return new StateImpl(this); - } - - protected static class StateImpl extends MapperBuilderState - { - private static final long serialVersionUID = 3L; - - public StateImpl(Builder src) - { - super(src); - } - - @Override - protected Object readResolve() - { - return new Builder(this).build(); - } - } - } - - public MessagePackMapper() - { - this(new Builder(new MessagePackFactory())); - } - - public MessagePackMapper(MessagePackFactory f) - { - this(new Builder(f)); - } - - protected MessagePackMapper(Builder builder) - { - super(builder); - } - - public static Builder builder() - { - return new Builder(new MessagePackFactory()); - } - - public static Builder builder(MessagePackFactory f) - { - return new Builder(f); - } - - @Override - public Version version() - { - return PackageVersion.VERSION; - } -} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java deleted file mode 100644 index 98fb9c88..00000000 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java +++ /dev/null @@ -1,801 +0,0 @@ -// -// 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 tools.jackson.core.Base64Variant; -import tools.jackson.core.JacksonException; -import tools.jackson.core.JsonToken; -import tools.jackson.core.ObjectReadContext; -import tools.jackson.core.StreamReadFeature; -import tools.jackson.core.TokenStreamContext; -import tools.jackson.core.TokenStreamLocation; -import tools.jackson.core.Version; -import tools.jackson.core.base.ParserMinimalBase; -import tools.jackson.core.exc.UnexpectedEndOfInputException; -import tools.jackson.core.io.IOContext; -import tools.jackson.core.json.DupDetector; -import org.msgpack.core.ExtensionTypeHeader; -import org.msgpack.core.MessageFormat; -import org.msgpack.core.MessagePack; -import org.msgpack.core.MessageUnpacker; -import org.msgpack.core.buffer.MessageBufferInput; -import org.msgpack.value.ValueType; - -import java.io.IOException; - -import java.math.BigDecimal; -import java.math.BigInteger; - -public class MessagePackParser - extends ParserMinimalBase -{ - // Retained heap per idle thread: ~0.2 KB (MessageUnpacker with cleared input buffer). - // Negligible compared to Jackson's own per-thread buffer retention. - private static final ThreadLocal> messageUnpackerHolder = new ThreadLocal<>(); - private final MessageUnpacker messageUnpacker; - - private static final BigInteger LONG_MIN = BigInteger.valueOf(Long.MIN_VALUE); - private static final BigInteger LONG_MAX = BigInteger.valueOf(Long.MAX_VALUE); - - private MessagePackReadContext streamReadContext; - - private boolean isClosed; - private long tokenPosition; - private long currentPosition; - private final IOContext ioContext; - private ExtensionTypeCustomDeserializers extTypeCustomDesers; - private final boolean ownsThreadLocalUnpacker; - - private enum Type - { - INT, LONG, DOUBLE, STRING, BYTES, BOOL, BIG_INT, EXT, NULL - } - private Type type; - private int intValue; - private long longValue; - private double doubleValue; - private boolean booleanValue; - private byte[] bytesValue; - private String stringValue; - private BigInteger biValue; - private MessagePackExtensionType extensionTypeValue; - - MessagePackParser(ObjectReadContext readCtxt, - IOContext ioCtxt, - int streamReadFeatures, - MessageBufferInput input, - Object src, - boolean reuseResourceInParser) - throws IOException - { - super(readCtxt, ioCtxt, streamReadFeatures); - - ioContext = ioCtxt; - DupDetector dups = StreamReadFeature.STRICT_DUPLICATE_DETECTION.enabledIn(streamReadFeatures) - ? DupDetector.rootDetector(this) : null; - streamReadContext = MessagePackReadContext.createRootContext(dups); - if (!reuseResourceInParser) { - messageUnpacker = MessagePack.newDefaultUnpacker(input); - ownsThreadLocalUnpacker = false; - return; - } - - Tuple messageUnpackerTuple = messageUnpackerHolder.get(); - if (messageUnpackerTuple == null) { - messageUnpacker = MessagePack.newDefaultUnpacker(input); - } - else { - // Considering to reuse InputStream with StreamReadFeature.AUTO_CLOSE_SOURCE, - // MessagePackParser needs to use the MessageUnpacker that has the same InputStream - // since it has buffer which has loaded the InputStream data ahead. - // However, it needs to call MessageUnpacker#reset when the source is different from the previous one. - Object cachedSrc = messageUnpackerTuple.first(); - if (StreamReadFeature.AUTO_CLOSE_SOURCE.enabledIn(streamReadFeatures) || cachedSrc != src || src instanceof byte[]) { - // reset() replaces the internal MessageBufferInput and clears the unpacker's - // internal read buffer to EMPTY_BUFFER. The old ArrayBufferInput becomes - // unreachable here (we discard the return value), so its byte[] is GC-eligible. - messageUnpackerTuple.second().reset(input); - } - messageUnpacker = messageUnpackerTuple.second(); - } - messageUnpackerHolder.set(new Tuple<>(src, messageUnpacker)); - ownsThreadLocalUnpacker = true; - } - - public void setExtensionTypeCustomDeserializers(ExtensionTypeCustomDeserializers extTypeCustomDesers) - { - this.extTypeCustomDesers = extTypeCustomDesers; - } - - @Override - public Version version() - { - return PackageVersion.VERSION; - } - - private String unpackString(MessageUnpacker messageUnpacker) throws IOException - { - return messageUnpacker.unpackString(); - } - - @Override - public JsonToken nextToken() throws JacksonException - { - try { - return _nextToken(); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - } - - private JsonToken _nextToken() throws IOException - { - type = null; - tokenPosition = messageUnpacker.getTotalReadBytes(); - - boolean isObjectValueSet = streamReadContext.inObject() && _currToken != JsonToken.PROPERTY_NAME; - if (isObjectValueSet) { - if (!streamReadContext.expectMoreValues()) { - streamReadContext = streamReadContext.getParent(); - return _updateToken(JsonToken.END_OBJECT); - } - } - else if (streamReadContext.inArray()) { - if (!streamReadContext.expectMoreValues()) { - streamReadContext = streamReadContext.getParent(); - return _updateToken(JsonToken.END_ARRAY); - } - } - - if (!messageUnpacker.hasNext()) { - if (streamReadContext.inRoot()) { - return null; - } - throw new UnexpectedEndOfInputException(this, null, null); - } - - MessageFormat format = messageUnpacker.getNextFormat(); - ValueType valueType = format.getValueType(); - - JsonToken nextToken; - switch (valueType) { - case STRING: - type = Type.STRING; - stringValue = unpackString(messageUnpacker); - _streamReadConstraints.validateStringLength(stringValue.length()); - if (isObjectValueSet) { - streamReadContext.setCurrentName(stringValue); - nextToken = JsonToken.PROPERTY_NAME; - } - else { - nextToken = JsonToken.VALUE_STRING; - } - break; - case INTEGER: - Object v; - switch (format) { - case UINT64: - BigInteger bi = messageUnpacker.unpackBigInteger(); - if (0 <= bi.compareTo(LONG_MIN) && bi.compareTo(LONG_MAX) <= 0) { - type = Type.LONG; - longValue = bi.longValue(); - v = longValue; - } - else { - type = Type.BIG_INT; - biValue = bi; - v = biValue; - } - break; - default: - long l = messageUnpacker.unpackLong(); - if (Integer.MIN_VALUE <= l && l <= Integer.MAX_VALUE) { - type = Type.INT; - intValue = (int) l; - v = intValue; - } - else { - type = Type.LONG; - longValue = l; - v = longValue; - } - break; - } - - if (isObjectValueSet) { - streamReadContext.setCurrentName(String.valueOf(v)); - nextToken = JsonToken.PROPERTY_NAME; - } - else { - nextToken = JsonToken.VALUE_NUMBER_INT; - } - break; - case NIL: - type = Type.NULL; - messageUnpacker.unpackNil(); - if (isObjectValueSet) { - streamReadContext.setCurrentName(null); - nextToken = JsonToken.PROPERTY_NAME; - } - else { - nextToken = JsonToken.VALUE_NULL; - } - break; - case BOOLEAN: - boolean b = messageUnpacker.unpackBoolean(); - type = Type.BOOL; - booleanValue = b; - if (isObjectValueSet) { - streamReadContext.setCurrentName(Boolean.toString(b)); - nextToken = JsonToken.PROPERTY_NAME; - } - else { - nextToken = b ? JsonToken.VALUE_TRUE : JsonToken.VALUE_FALSE; - } - break; - case FLOAT: - type = Type.DOUBLE; - doubleValue = messageUnpacker.unpackDouble(); - if (isObjectValueSet) { - streamReadContext.setCurrentName(String.valueOf(doubleValue)); - nextToken = JsonToken.PROPERTY_NAME; - } - else { - nextToken = JsonToken.VALUE_NUMBER_FLOAT; - } - break; - case BINARY: - type = Type.BYTES; - int len = messageUnpacker.unpackBinaryHeader(); - _streamReadConstraints.validateStringLength(len); - bytesValue = messageUnpacker.readPayload(len); - if (isObjectValueSet) { - streamReadContext.setCurrentName(new String(bytesValue, MessagePack.UTF8)); - nextToken = JsonToken.PROPERTY_NAME; - } - else { - nextToken = JsonToken.VALUE_EMBEDDED_OBJECT; - } - break; - case ARRAY: - nextToken = JsonToken.START_ARRAY; - streamReadContext = streamReadContext.createChildArrayContext(messageUnpacker.unpackArrayHeader()); - _streamReadConstraints.validateNestingDepth(streamReadContext.getNestingDepth()); - break; - case MAP: - nextToken = JsonToken.START_OBJECT; - streamReadContext = streamReadContext.createChildObjectContext(messageUnpacker.unpackMapHeader()); - _streamReadConstraints.validateNestingDepth(streamReadContext.getNestingDepth()); - break; - case EXTENSION: - type = Type.EXT; - ExtensionTypeHeader header = messageUnpacker.unpackExtensionTypeHeader(); - _streamReadConstraints.validateStringLength(header.getLength()); - extensionTypeValue = new MessagePackExtensionType(header.getType(), messageUnpacker.readPayload(header.getLength())); - if (isObjectValueSet) { - streamReadContext.setCurrentName(deserializedExtensionTypeValue().toString()); - nextToken = JsonToken.PROPERTY_NAME; - } - else { - nextToken = JsonToken.VALUE_EMBEDDED_OBJECT; - } - break; - default: - nextToken = _reportError("Unexpected MessagePack format type: " + valueType); - } - currentPosition = messageUnpacker.getTotalReadBytes(); - - _updateToken(nextToken); - - return nextToken; - } - - @Override - protected void _handleEOF() - { - } - - @Override - public String getString() - { - if (type == null) { - return _currToken == null ? null : _currToken.asString(); - } - switch (type) { - case STRING: - return stringValue; - case BYTES: - return new String(bytesValue, MessagePack.UTF8); - case INT: - return String.valueOf(intValue); - case LONG: - return String.valueOf(longValue); - case DOUBLE: - return String.valueOf(doubleValue); - case BOOL: - return Boolean.toString(booleanValue); - case BIG_INT: - return String.valueOf(biValue); - case EXT: - try { - return deserializedExtensionTypeValue().toString(); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - case NULL: - return "null"; - default: - return _reportError("Unexpected MessagePack value type: " + type); - } - } - - @Override - public char[] getStringCharacters() - { - return getString().toCharArray(); - } - - @Override - public boolean hasStringCharacters() - { - return false; - } - - @Override - public int getStringLength() - { - return getString().length(); - } - - @Override - public int getStringOffset() - { - return 0; - } - - @Override - public byte[] getBinaryValue(Base64Variant b64variant) - { - if (type == null) { - return _reportError("Current token (" + _currToken + ") not of binary type"); - } - switch (type) { - case BYTES: - return bytesValue; - case STRING: - return stringValue.getBytes(MessagePack.UTF8); - case EXT: - return extensionTypeValue.getData(); - case INT: - case LONG: - case DOUBLE: - case BOOL: - case BIG_INT: - case NULL: - return _reportError("Current token (" + _currToken + ") not of binary type"); - default: - return _reportError("Unexpected MessagePack value type: " + type); - } - } - - @Override - public Number getNumberValue() - { - if (type == null) { - return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); - } - switch (type) { - case INT: - return intValue; - case LONG: - return longValue; - case DOUBLE: - return doubleValue; - case BIG_INT: - return biValue; - case NULL: - case BOOL: - case STRING: - case BYTES: - case EXT: - return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); - default: - return _reportError("Unexpected MessagePack value type: " + type); - } - } - - @Override - public int getIntValue() - { - if (type == null) { - return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); - } - switch (type) { - case INT: - return intValue; - case LONG: - if (longValue < Integer.MIN_VALUE || longValue > Integer.MAX_VALUE) { - return _reportError("Numeric value (" + longValue + ") out of range for `int`"); - } - return (int) longValue; - case DOUBLE: - if (!Double.isFinite(doubleValue)) { - return _reportError("Cannot convert non-finite double (" + doubleValue + ") to `int`"); - } - if (doubleValue < Integer.MIN_VALUE || doubleValue > Integer.MAX_VALUE) { - return _reportError("Numeric value (" + doubleValue + ") out of range for `int`"); - } - return (int) doubleValue; - case BIG_INT: - try { - return biValue.intValueExact(); - } - catch (ArithmeticException e) { - return _reportError("Numeric value (" + biValue + ") out of range for `int`"); - } - case NULL: - case BOOL: - case STRING: - case BYTES: - case EXT: - return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); - default: - return _reportError("Unexpected MessagePack value type: " + type); - } - } - - @Override - public long getLongValue() - { - if (type == null) { - return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); - } - switch (type) { - case INT: - return intValue; - case LONG: - return longValue; - case DOUBLE: - if (!Double.isFinite(doubleValue)) { - return _reportError("Cannot convert non-finite double (" + doubleValue + ") to `long`"); - } - // (double)Long.MAX_VALUE rounds up to 2^63; use >= to reject 2^63 itself. - if (doubleValue < Long.MIN_VALUE || doubleValue >= (double) Long.MAX_VALUE) { - return _reportError("Numeric value (" + doubleValue + ") out of range for `long`"); - } - return (long) doubleValue; - case BIG_INT: - try { - return biValue.longValueExact(); - } - catch (ArithmeticException e) { - return _reportError("Numeric value (" + biValue + ") out of range for `long`"); - } - case NULL: - case BOOL: - case STRING: - case BYTES: - case EXT: - return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); - default: - return _reportError("Unexpected MessagePack value type: " + type); - } - } - - @Override - public BigInteger getBigIntegerValue() - { - if (type == null) { - return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); - } - switch (type) { - case INT: - return BigInteger.valueOf(intValue); - case LONG: - return BigInteger.valueOf(longValue); - case DOUBLE: - if (!Double.isFinite(doubleValue)) { - return _reportError("Cannot convert non-finite double (" + doubleValue + ") to BigInteger"); - } - return BigDecimal.valueOf(doubleValue).toBigInteger(); // truncates fractional part - case BIG_INT: - return biValue; - case NULL: - case BOOL: - case STRING: - case BYTES: - case EXT: - return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); - default: - return _reportError("Unexpected MessagePack value type: " + type); - } - } - - @Override - public float getFloatValue() - { - // No bounds/range check: a finite double or large BigInteger may overflow to - // Float.POSITIVE_INFINITY. This is intentional — same as ParserBase and CBORParser. - if (type == null) { - return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); - } - switch (type) { - case INT: - return (float) intValue; - case LONG: - return (float) longValue; - case DOUBLE: - return (float) doubleValue; - case BIG_INT: - return biValue.floatValue(); - case NULL: - case BOOL: - case STRING: - case BYTES: - case EXT: - return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); - default: - return _reportError("Unexpected MessagePack value type: " + type); - } - } - - @Override - public double getDoubleValue() - { - // No bounds/range check: large BigInteger may overflow to Double.POSITIVE_INFINITY, - // and large long values may lose precision. Intentional — same as ParserBase. - if (type == null) { - return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); - } - switch (type) { - case INT: - return intValue; - case LONG: - return (double) longValue; - case DOUBLE: - return doubleValue; - case BIG_INT: - return biValue.doubleValue(); - case NULL: - case BOOL: - case STRING: - case BYTES: - case EXT: - return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); - default: - return _reportError("Unexpected MessagePack value type: " + type); - } - } - - @Override - public BigDecimal getDecimalValue() - { - if (type == null) { - return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); - } - switch (type) { - case INT: - return BigDecimal.valueOf(intValue); - case LONG: - return BigDecimal.valueOf(longValue); - case DOUBLE: - if (!Double.isFinite(doubleValue)) { - return _reportError("Cannot convert non-finite double (" + doubleValue + ") to BigDecimal"); - } - return BigDecimal.valueOf(doubleValue); - case BIG_INT: - return new BigDecimal(biValue); - case NULL: - case BOOL: - case STRING: - case BYTES: - case EXT: - return _reportError("Current token (" + _currToken + ") not numeric, cannot use numeric value accessors"); - default: - return _reportError("Unexpected MessagePack value type: " + type); - } - } - - private Object deserializedExtensionTypeValue() - throws IOException - { - if (extTypeCustomDesers != null) { - ExtensionTypeCustomDeserializers.Deser deser = extTypeCustomDesers.getDeser(extensionTypeValue.getType()); - if (deser != null) { - return deser.deserialize(extensionTypeValue.getData()); - } - } - return extensionTypeValue; - } - - @Override - public Object getEmbeddedObject() - { - if (type == null) { - return _reportError("Current token (" + _currToken + ") not of embeddable type"); - } - switch (type) { - case BYTES: - return bytesValue; - case EXT: - try { - return deserializedExtensionTypeValue(); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - case INT: - case LONG: - case DOUBLE: - case BOOL: - case BIG_INT: - case STRING: - case NULL: - return _reportError("Current token (" + _currToken + ") not of embeddable type"); - default: - return _reportError("Unexpected MessagePack value type: " + type); - } - } - - @Override - public NumberType getNumberType() - { - if (type == null) { - return null; - } - switch (type) { - case INT: - return NumberType.INT; - case LONG: - return NumberType.LONG; - case DOUBLE: - return NumberType.DOUBLE; - case BIG_INT: - return NumberType.BIG_INTEGER; - case NULL: - case BOOL: - case STRING: - case BYTES: - case EXT: - return null; - default: - return _reportError("Unexpected MessagePack value type: " + type); - } - } - - @Override - protected void _closeInput() throws IOException - { - if (StreamReadFeature.AUTO_CLOSE_SOURCE.enabledIn(_streamReadFeatures)) { - messageUnpacker.close(); - } - if (ownsThreadLocalUnpacker) { - Tuple tuple = messageUnpackerHolder.get(); - if (tuple != null) { - if (tuple.first() instanceof byte[]) { - // close() calls ArrayBufferInput.close() which sets buffer = null, - // releasing the byte[] payload reference held by the unpacker's input. - // The unpacker itself is kept alive for reuse on the next parse. - tuple.second().close(); - messageUnpackerHolder.set(new Tuple<>(null, tuple.second())); - } - else if (StreamReadFeature.AUTO_CLOSE_SOURCE.enabledIn(_streamReadFeatures)) { - // Stream is already closed above; release the reference so it doesn't - // linger on the thread until the next parse. - messageUnpackerHolder.set(new Tuple<>(null, tuple.second())); - } - // else: InputStream with AUTO_CLOSE_SOURCE disabled — keep the reference - // so the next parse on the same thread can detect same-stream reuse and - // avoid resetting the unpacker (which would discard its read-ahead buffer). - } - } - } - - @Override - protected void _releaseBuffers() - { - } - - @Override - public boolean isClosed() - { - return isClosed; - } - - @Override - public void close() - { - if (isClosed) { - return; - } - // Parsers are single-threaded by contract; close() is expected on the same - // thread that created the parser. Cross-thread close is not a supported use case. - try { - _closeInput(); - } - catch (IOException e) { - throw _wrapIOFailure(e); - } - finally { - isClosed = true; - } - } - - @Override - public TokenStreamContext streamReadContext() - { - return streamReadContext; - } - - @Override - public TokenStreamLocation currentTokenLocation() - { - // columnNr repurposed as byte offset; truncates for inputs > 2 GB - return new TokenStreamLocation(ioContext.contentReference(), tokenPosition, -1, (int) tokenPosition); - } - - @Override - public TokenStreamLocation currentLocation() - { - // columnNr repurposed as byte offset; truncates for inputs > 2 GB - return new TokenStreamLocation(ioContext.contentReference(), currentPosition, -1, (int) currentPosition); - } - - @Override - public String currentName() - { - // Simple, but need to look for START_OBJECT/ARRAY's "off-by-one" thing: - if (_currToken == JsonToken.START_OBJECT || _currToken == JsonToken.START_ARRAY) { - MessagePackReadContext parent = streamReadContext.getParent(); - return parent.currentName(); - } - return streamReadContext.currentName(); - } - - @Override - public Object streamReadInputSource() - { - return ioContext.contentReference().getRawContent(); - } - - @Override - public Object currentValue() - { - return streamReadContext.currentValue(); - } - - @Override - public void assignCurrentValue(Object v) - { - streamReadContext.assignCurrentValue(v); - } - - @Override - public boolean isNaN() - { - if (type == Type.DOUBLE) { - return Double.isNaN(doubleValue) || Double.isInfinite(doubleValue); - } - return false; - } - - public boolean isCurrentFieldId() - { - return this.type == Type.INT || this.type == Type.LONG; - } -} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java deleted file mode 100644 index 0aa72293..00000000 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java +++ /dev/null @@ -1,44 +0,0 @@ -// -// 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 tools.jackson.databind.JavaType; -import tools.jackson.databind.SerializationContext; -import tools.jackson.databind.ValueSerializer; -import tools.jackson.databind.cfg.SerializerFactoryConfig; -import tools.jackson.databind.ser.BeanSerializerFactory; - -public class MessagePackSerializerFactory - extends BeanSerializerFactory -{ - public MessagePackSerializerFactory() - { - super(null); - } - - public MessagePackSerializerFactory(SerializerFactoryConfig config) - { - super(config); - } - - private static final MessagePackKeySerializer KEY_SERIALIZER = new MessagePackKeySerializer(); - - @Override - public ValueSerializer createKeySerializer(SerializationContext ctxt, JavaType keyType) - { - return KEY_SERIALIZER; - } -} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java deleted file mode 100644 index a1df18e4..00000000 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java +++ /dev/null @@ -1,107 +0,0 @@ -// -// 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 tools.jackson.core.JsonGenerator; -import tools.jackson.core.JsonParser; -import tools.jackson.databind.DeserializationContext; -import tools.jackson.databind.SerializationContext; -import tools.jackson.databind.deser.std.StdDeserializer; -import tools.jackson.databind.module.SimpleModule; -import tools.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, SerializationContext provider) - { - try { - // Per-call allocation is a known limitation carried from the v2 module. - // Manually encoding the timestamp bytes would avoid it but duplicates - // msgpack-core's timestamp logic. Tracked as a future optimization. - ByteArrayOutputStream os = new ByteArrayOutputStream(); - 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.writePOJO(extensionType); - } - } - catch (IOException e) { - throw _wrapIOFailure(provider, e); - } - } - } - - private static class InstantDeserializer extends StdDeserializer - { - protected InstantDeserializer(Class vc) - { - super(vc); - } - - @Override - public Instant deserialize(JsonParser p, DeserializationContext ctxt) - { - try { - // Per-call allocation is a known limitation — see serialize() above. - MessagePackExtensionType ext = p.readValueAs(MessagePackExtensionType.class); - if (ext.getType() != EXT_TYPE) { - ctxt.reportInputMismatch(Instant.class, - "Unexpected extension type (0x%X) for Instant object", ext.getType() & 0xFF); - return null; // unreachable - } - try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(ext.getData())) { - return unpacker.unpackTimestamp(new ExtensionTypeHeader(EXT_TYPE, ext.getData().length)); - } - } - catch (IOException e) { - throw _wrapIOFailure(ctxt, e); - } - } - } - - private TimestampExtensionModule() - { - } -} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java deleted file mode 100644 index 25ef0fe2..00000000 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java +++ /dev/null @@ -1,198 +0,0 @@ -// -// 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 tools.jackson.core.JsonEncoding; -import tools.jackson.core.JsonGenerator; -import tools.jackson.core.JsonParser; -import tools.jackson.core.TSFBuilder; -import tools.jackson.core.TokenStreamFactory; -import tools.jackson.core.type.TypeReference; -import tools.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.msgpack.core.MessagePack; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.CoreMatchers.sameInstance; -import static org.junit.Assert.assertEquals; -import static org.hamcrest.MatcherAssert.assertThat; - -public class MessagePackFactoryTest - extends MessagePackDataformatTestBase -{ - @Test - public void testCreateGenerator() - throws IOException - { - JsonEncoding enc = JsonEncoding.UTF8; - JsonGenerator generator = factory.createGenerator(out, enc); - assertEquals(MessagePackGenerator.class, generator.getClass()); - } - - @Test - public void testCreateParser() - throws IOException - { - JsonParser parser = factory.createParser(in); - assertEquals(MessagePackParser.class, parser.getClass()); - } - - @Test - public void testCopyWithDefaultConfig() - throws IOException - { - MessagePackFactory messagePackFactory = new MessagePackFactory(); - ObjectMapper objectMapper = new MessagePackMapper(messagePackFactory); - - // Use the original ObjectMapper in advance - { - byte[] bytes = objectMapper.writeValueAsBytes(1234); - assertThat(objectMapper.readValue(bytes, Integer.class), is(1234)); - } - - // Copy the factory - TokenStreamFactory copiedFactory = messagePackFactory.copy(); - assertThat(copiedFactory, is(instanceOf(MessagePackFactory.class))); - MessagePackFactory copiedMessagePackFactory = (MessagePackFactory) copiedFactory; - - assertThat(copiedMessagePackFactory.getPackerConfig().isStr8FormatSupport(), is(true)); - assertThat(copiedMessagePackFactory.getExtTypeCustomDesers(), is(nullValue())); - - // Check the copied factory works fine - ObjectMapper copiedObjectMapper = new MessagePackMapper(copiedMessagePackFactory); - Map map = new HashMap<>(); - map.put("one", 1); - Map deserialized = copiedObjectMapper - .readValue(objectMapper.writeValueAsBytes(map), new TypeReference>() {}); - assertThat(deserialized.size(), is(1)); - assertThat(deserialized.get("one"), is(1)); - } - - @Test - public void testRebuildWithDefaultConfig() - throws IOException - { - MessagePackFactory messagePackFactory = new MessagePackFactory(); - TSFBuilder builder = messagePackFactory.rebuild(); - assertThat(builder, is(instanceOf(MessagePackFactoryBuilder.class))); - - MessagePackFactory rebuilt = (MessagePackFactory) builder.build(); - assertThat(rebuilt, is(not(sameInstance(messagePackFactory)))); - assertThat(rebuilt.getPackerConfig().isStr8FormatSupport(), is(true)); - assertThat(rebuilt.getExtTypeCustomDesers(), is(nullValue())); - - ObjectMapper rebuiltObjectMapper = new MessagePackMapper(rebuilt); - byte[] bytes = rebuiltObjectMapper.writeValueAsBytes(42); - assertThat(rebuiltObjectMapper.readValue(bytes, Integer.class), is(42)); - } - - @Test - public void testRebuildWithAdvancedConfig() - throws IOException - { - ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); - extTypeCustomDesers.addCustomDeser((byte) 42, - new ExtensionTypeCustomDeserializers.Deser() - { - @Override - public Object deserialize(byte[] data) - throws IOException - { - TinyPojo pojo = new TinyPojo(); - pojo.t = new String(data); - return pojo; - } - } - ); - MessagePack.PackerConfig packerConfig = new MessagePack.PackerConfig().withStr8FormatSupport(false); - MessagePackFactory messagePackFactory = new MessagePackFactory(packerConfig); - messagePackFactory.setExtTypeCustomDesers(extTypeCustomDesers); - - MessagePackFactory rebuilt = (MessagePackFactory) messagePackFactory.rebuild().build(); - assertThat(rebuilt, is(not(sameInstance(messagePackFactory)))); - assertThat(rebuilt.getPackerConfig().isStr8FormatSupport(), is(false)); - assertThat(rebuilt.getExtTypeCustomDesers().getDeser((byte) 42), is(notNullValue())); - assertThat(rebuilt.getExtTypeCustomDesers().getDeser((byte) 43), is(nullValue())); - } - - @Test - public void testSnapshotReturnsNewInstance() - { - MessagePackFactory messagePackFactory = new MessagePackFactory(); - TokenStreamFactory snapshot = messagePackFactory.snapshot(); - assertThat(snapshot, is(not(sameInstance(messagePackFactory)))); - assertThat(snapshot, is(instanceOf(MessagePackFactory.class))); - } - - @Test - public void testCopyWithAdvancedConfig() - throws IOException - { - ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); - extTypeCustomDesers.addCustomDeser((byte) 42, - new ExtensionTypeCustomDeserializers.Deser() - { - @Override - public Object deserialize(byte[] data) - throws IOException - { - TinyPojo pojo = new TinyPojo(); - pojo.t = new String(data); - return pojo; - } - } - ); - - MessagePack.PackerConfig msgpackPackerConfig = new MessagePack.PackerConfig().withStr8FormatSupport(false); - - MessagePackFactory messagePackFactory = new MessagePackFactory(msgpackPackerConfig); - messagePackFactory.setExtTypeCustomDesers(extTypeCustomDesers); - - ObjectMapper objectMapper = new MessagePackMapper(messagePackFactory); - - // Use the original ObjectMapper in advance - { - byte[] bytes = objectMapper.writeValueAsBytes(1234); - assertThat(objectMapper.readValue(bytes, Integer.class), is(1234)); - } - - // Copy the factory - TokenStreamFactory copiedFactory = messagePackFactory.copy(); - assertThat(copiedFactory, is(instanceOf(MessagePackFactory.class))); - MessagePackFactory copiedMessagePackFactory = (MessagePackFactory) copiedFactory; - - assertThat(copiedMessagePackFactory.getPackerConfig().isStr8FormatSupport(), is(false)); - assertThat(copiedMessagePackFactory.getExtTypeCustomDesers().getDeser((byte) 42), is(notNullValue())); - assertThat(copiedMessagePackFactory.getExtTypeCustomDesers().getDeser((byte) 43), is(nullValue())); - - // Check the copied factory works fine - ObjectMapper copiedObjectMapper = new MessagePackMapper(copiedMessagePackFactory); - Map map = new HashMap<>(); - map.put("one", 1); - Map deserialized = copiedObjectMapper - .readValue(objectMapper.writeValueAsBytes(map), new TypeReference>() {}); - assertThat(deserialized.size(), is(1)); - assertThat(deserialized.get("one"), is(1)); - } -} From aa8cbfc473a9881c967ed65dfe3201c9737c1093 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Tue, 28 Jul 2026 13:37:54 -0700 Subject: [PATCH 5/7] Restructure Jackson modules for 1.0.0: Jackson 3 becomes jackson-dataformat-msgpack - msgpack-jackson now hosts the Jackson 3.x integration (artifact jackson-dataformat-msgpack, package org.msgpack.jackson3.dataformat, Java 17+) - msgpack-jackson2 hosts the Jackson 2.x integration in maintenance mode (artifact jackson2-dataformat-msgpack, package org.msgpack.jackson.dataformat unchanged, Java 8+) - Distinct packages and artifactIds let both integrations coexist on one classpath for incremental migration - Add sbt-jupiter-interface so the Jackson 2 module's JUnit 5 tests actually run; they had been silently skipped (0 tests detected) on main - Fix MessagePackDataformatTestBase lifecycle annotations (JUnit 4 @Before/ @After on JUnit 5 tests meant setup never ran) - Restore byte-offset-as-columnNr in Jackson 2 MessagePackParser locations, regressed unnoticed in #903 when the deprecated JsonLocation constructor was replaced Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_014ZZUaHusAjVx6SNu4sA42s --- .github/workflows/CI.yml | 8 +++--- .github/workflows/release.yml | 8 +++--- CLAUDE.md | 10 ++++--- README.md | 9 ++++--- build.sbt | 26 +++++++++++-------- msgpack-jackson/README.md | 14 +++++----- .../dataformat/benchmark/BenchmarkState.java | 10 +++---- .../benchmark/MsgpackReadBenchmark.java | 4 +-- .../benchmark/MsgpackWriteBenchmark.java | 6 ++--- .../dataformat/benchmark/NopOutputStream.java | 2 +- .../benchmark/WriteUTF8StringBenchmark.java | 4 +-- .../dataformat/benchmark/model/Image.java | 2 +- .../benchmark/model/MediaContent.java | 2 +- .../dataformat/benchmark/model/MediaItem.java | 2 +- .../benchmark/model/MediaItems.java | 2 +- .../dataformat/benchmark/model/Player.java | 2 +- .../dataformat/benchmark/model/Size.java | 2 +- .../ExtensionTypeCustomDeserializers.java | 2 +- .../dataformat/JsonArrayFormat.java | 2 +- .../dataformat/MessagePackExtensionType.java | 2 +- .../dataformat/MessagePackFactory.java | 2 +- .../dataformat/MessagePackFactoryBuilder.java | 2 +- .../dataformat/MessagePackGenerator.java | 2 +- .../dataformat/MessagePackKeySerializer.java | 2 +- .../dataformat/MessagePackMapper.java | 2 +- .../dataformat/MessagePackParser.java | 2 +- .../dataformat/MessagePackReadContext.java | 2 +- .../MessagePackSerializedString.java | 2 +- .../MessagePackSerializerFactory.java | 2 +- .../dataformat/MessagePackWriteContext.java | 2 +- .../dataformat/PackageVersion.java | 4 +-- .../dataformat/TimestampExtensionModule.java | 2 +- .../dataformat/Tuple.java | 2 +- .../ExampleOfTypeInformationSerDe.java | 2 +- .../MessagePackDataformatForFieldIdTest.java | 2 +- .../MessagePackDataformatForPojoTest.java | 2 +- .../MessagePackDataformatTestBase.java | 2 +- .../dataformat/MessagePackFactoryTest.java | 2 +- .../dataformat/MessagePackGeneratorTest.java | 4 +-- .../dataformat/MessagePackMapperTest.java | 2 +- .../dataformat/MessagePackParserTest.java | 2 +- .../MessagePackWriteContextTest.java | 2 +- .../TimestampExtensionModuleTest.java | 2 +- msgpack-jackson2/README.md | 20 +++++++------- .../jackson/dataformat/MessagePackParser.java | 5 ++-- .../MessagePackDataformatTestBase.java | 8 +++--- project/plugins.sbt | 2 ++ 47 files changed, 109 insertions(+), 95 deletions(-) rename msgpack-jackson/src/jmh/java/org/msgpack/{jackson => jackson3}/dataformat/benchmark/BenchmarkState.java (82%) rename msgpack-jackson/src/jmh/java/org/msgpack/{jackson => jackson3}/dataformat/benchmark/MsgpackReadBenchmark.java (93%) rename msgpack-jackson/src/jmh/java/org/msgpack/{jackson => jackson3}/dataformat/benchmark/MsgpackWriteBenchmark.java (90%) rename msgpack-jackson/src/jmh/java/org/msgpack/{jackson => jackson3}/dataformat/benchmark/NopOutputStream.java (94%) rename msgpack-jackson/src/jmh/java/org/msgpack/{jackson => jackson3}/dataformat/benchmark/WriteUTF8StringBenchmark.java (96%) rename msgpack-jackson/src/jmh/java/org/msgpack/{jackson => jackson3}/dataformat/benchmark/model/Image.java (94%) rename msgpack-jackson/src/jmh/java/org/msgpack/{jackson => jackson3}/dataformat/benchmark/model/MediaContent.java (95%) rename msgpack-jackson/src/jmh/java/org/msgpack/{jackson => jackson3}/dataformat/benchmark/model/MediaItem.java (95%) rename msgpack-jackson/src/jmh/java/org/msgpack/{jackson => jackson3}/dataformat/benchmark/model/MediaItems.java (96%) rename msgpack-jackson/src/jmh/java/org/msgpack/{jackson => jackson3}/dataformat/benchmark/model/Player.java (91%) rename msgpack-jackson/src/jmh/java/org/msgpack/{jackson => jackson3}/dataformat/benchmark/model/Size.java (91%) rename msgpack-jackson/src/main/java/org/msgpack/{jackson => jackson3}/dataformat/ExtensionTypeCustomDeserializers.java (97%) rename msgpack-jackson/src/main/java/org/msgpack/{jackson => jackson3}/dataformat/JsonArrayFormat.java (97%) rename msgpack-jackson/src/main/java/org/msgpack/{jackson => jackson3}/dataformat/MessagePackExtensionType.java (98%) rename msgpack-jackson/src/main/java/org/msgpack/{jackson => jackson3}/dataformat/MessagePackFactory.java (99%) rename msgpack-jackson/src/main/java/org/msgpack/{jackson => jackson3}/dataformat/MessagePackFactoryBuilder.java (98%) rename msgpack-jackson/src/main/java/org/msgpack/{jackson => jackson3}/dataformat/MessagePackGenerator.java (99%) rename msgpack-jackson/src/main/java/org/msgpack/{jackson => jackson3}/dataformat/MessagePackKeySerializer.java (96%) rename msgpack-jackson/src/main/java/org/msgpack/{jackson => jackson3}/dataformat/MessagePackMapper.java (98%) rename msgpack-jackson/src/main/java/org/msgpack/{jackson => jackson3}/dataformat/MessagePackParser.java (99%) rename msgpack-jackson/src/main/java/org/msgpack/{jackson => jackson3}/dataformat/MessagePackReadContext.java (99%) rename msgpack-jackson/src/main/java/org/msgpack/{jackson => jackson3}/dataformat/MessagePackSerializedString.java (98%) rename msgpack-jackson/src/main/java/org/msgpack/{jackson => jackson3}/dataformat/MessagePackSerializerFactory.java (97%) rename msgpack-jackson/src/main/java/org/msgpack/{jackson => jackson3}/dataformat/MessagePackWriteContext.java (98%) rename msgpack-jackson/src/main/java/org/msgpack/{jackson => jackson3}/dataformat/PackageVersion.java (83%) rename msgpack-jackson/src/main/java/org/msgpack/{jackson => jackson3}/dataformat/TimestampExtensionModule.java (99%) rename msgpack-jackson/src/main/java/org/msgpack/{jackson => jackson3}/dataformat/Tuple.java (95%) rename msgpack-jackson/src/test/java/org/msgpack/{jackson => jackson3}/dataformat/ExampleOfTypeInformationSerDe.java (99%) rename msgpack-jackson/src/test/java/org/msgpack/{jackson => jackson3}/dataformat/MessagePackDataformatForFieldIdTest.java (99%) rename msgpack-jackson/src/test/java/org/msgpack/{jackson => jackson3}/dataformat/MessagePackDataformatForPojoTest.java (99%) rename msgpack-jackson/src/test/java/org/msgpack/{jackson => jackson3}/dataformat/MessagePackDataformatTestBase.java (99%) rename msgpack-jackson/src/test/java/org/msgpack/{jackson => jackson3}/dataformat/MessagePackFactoryTest.java (99%) rename msgpack-jackson/src/test/java/org/msgpack/{jackson => jackson3}/dataformat/MessagePackGeneratorTest.java (99%) rename msgpack-jackson/src/test/java/org/msgpack/{jackson => jackson3}/dataformat/MessagePackMapperTest.java (98%) rename msgpack-jackson/src/test/java/org/msgpack/{jackson => jackson3}/dataformat/MessagePackParserTest.java (99%) rename msgpack-jackson/src/test/java/org/msgpack/{jackson => jackson3}/dataformat/MessagePackWriteContextTest.java (99%) rename msgpack-jackson/src/test/java/org/msgpack/{jackson => jackson3}/dataformat/TimestampExtensionModuleTest.java (99%) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 9640ffe0..3da7f02d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -28,7 +28,7 @@ jobs: - 'project/build.properties' - 'msgpack-core/**' - 'msgpack-jackson/**' - - 'msgpack-jackson3/**' + - 'msgpack-jackson2/**' docs: - '**.md' - '**.txt' @@ -83,8 +83,10 @@ jobs: env: TEST_JAVA_HOME: ${{ steps.target-jdk.outputs.path }} run: | + # msgpack-jackson (Jackson 3) requires JDK 17+; older lanes test the + # Java 8 compatible modules only if [[ ${{ matrix.java }} -lt 17 ]]; then - ./sbt msgpack-core/test msgpack-jackson/test + ./sbt msgpack-core/test msgpack-jackson2/test else ./sbt test fi @@ -93,7 +95,7 @@ jobs: TEST_JAVA_HOME: ${{ steps.target-jdk.outputs.path }} run: | if [[ ${{ matrix.java }} -lt 17 ]]; then - ./sbt msgpack-core/test msgpack-jackson/test -J-Dmsgpack.universal-buffer=true + ./sbt msgpack-core/test msgpack-jackson2/test -J-Dmsgpack.universal-buffer=true else ./sbt test -J-Dmsgpack.universal-buffer=true fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3e90dde6..9bc590d6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,17 +38,17 @@ jobs: PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} TEST_JAVA_HOME: ${{ steps.jdk8.outputs.path }} run: | - ./sbt msgpack-core/publishSigned msgpack-jackson/publishSigned - # msgpack-jackson3 requires JDK 17+ + ./sbt msgpack-core/publishSigned msgpack-jackson2/publishSigned + # msgpack-jackson (Jackson 3) requires JDK 17+ - uses: actions/setup-java@v5 with: java-version: 17 distribution: temurin - - name: Build bundle for msgpack-jackson3 + - name: Build bundle for msgpack-jackson (Jackson 3) env: PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} run: | - ./sbt msgpack-jackson3/publishSigned + ./sbt msgpack-jackson/publishSigned - name: Release to Sonatype env: SONATYPE_USERNAME: '${{ secrets.SONATYPE_USERNAME }}' diff --git a/CLAUDE.md b/CLAUDE.md index 2e72e982..e784bb13 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co MessagePack-Java is a binary serialization library that provides a fast and compact alternative to JSON. The project consists of two main modules: - **msgpack-core**: Standalone MessagePack implementation with no external dependencies -- **msgpack-jackson**: Jackson integration for object mapping capabilities +- **msgpack-jackson**: Jackson 3.x integration for object mapping capabilities (artifact `jackson-dataformat-msgpack`, Java 17+) +- **msgpack-jackson2**: Jackson 2.x integration in maintenance mode (artifact `jackson2-dataformat-msgpack`, Java 8+) ## Essential Development Commands @@ -49,7 +50,8 @@ The main entry point is the `MessagePack` factory class which creates: Key locations: - Core interfaces: `msgpack-core/src/main/java/org/msgpack/core/` -- Jackson integration: `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/` +- Jackson 3 integration: `msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/` +- Jackson 2 integration: `msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/` ### Buffer Management System MessagePack uses an efficient buffer abstraction layer: @@ -68,8 +70,8 @@ The msgpack-jackson module provides: ### Testing Structure - **msgpack-core tests**: Written in Scala (always use the latest Scala 3 version) using AirSpec framework - Location: `msgpack-core/src/test/scala/` -- **msgpack-jackson tests**: Written in Java using JUnit - - Location: `msgpack-jackson/src/test/java/` +- **msgpack-jackson / msgpack-jackson2 tests**: Written in Java using JUnit + - Location: `msgpack-jackson/src/test/java/`, `msgpack-jackson2/src/test/java/` ## Important JVM Options diff --git a/README.md b/README.md index e76da08d..77f7b5fa 100644 --- a/README.md +++ b/README.md @@ -59,10 +59,10 @@ Two artifacts are published depending on which Jackson major version your projec | Jackson version | Artifact | Requirements | | --- | --- | --- | -| Jackson 2.x | [`jackson-dataformat-msgpack`](https://github.com/msgpack/msgpack-java/blob/develop/msgpack-jackson/README.md) | Java 8+ | -| Jackson 3.x | [`jackson-dataformat-msgpack-jackson3`](https://github.com/msgpack/msgpack-java/blob/develop/msgpack-jackson3/README.md) | Java 17+ | +| Jackson 3.x | [`jackson-dataformat-msgpack`](https://github.com/msgpack/msgpack-java/blob/main/msgpack-jackson/README.md) (1.x) | Java 17+ | +| Jackson 2.x | [`jackson2-dataformat-msgpack`](https://github.com/msgpack/msgpack-java/blob/main/msgpack-jackson2/README.md) (maintenance mode) | Java 8+ | -Use `jackson-dataformat-msgpack` if your project still depends on Jackson 2.x, or `jackson-dataformat-msgpack-jackson3` if you've upgraded to Jackson 3.x. See each module's README for install instructions and usage details. +Since msgpack-java 1.0.0, `jackson-dataformat-msgpack` targets Jackson 3.x. If your project still depends on Jackson 2.x, either stay on `jackson-dataformat-msgpack` 0.9.x or switch to `jackson2-dataformat-msgpack` to keep receiving msgpack-core updates (only the artifactId changes; the Java package is the same). See each module's README for install instructions and usage details. - [Release Notes](https://github.com/msgpack/msgpack-java/blob/develop/RELEASE_NOTES.md) @@ -153,5 +153,6 @@ If some sporadic error happens (e.g., Sonatype timeout), rerun `sonaRelease` aga ``` msgpack-core # Contains packer/unpacker implementation that never uses third-party libraries -msgpack-jackson # Contains jackson-dataformat-java implementation +msgpack-jackson # jackson-dataformat-msgpack: Jackson 3.x integration (Java 17+) +msgpack-jackson2 # jackson2-dataformat-msgpack: Jackson 2.x integration (maintenance mode) ``` diff --git a/build.sbt b/build.sbt index e8192110..34b4ae8d 100644 --- a/build.sbt +++ b/build.sbt @@ -123,7 +123,8 @@ val junitInterface = "com.github.sbt" % "junit-interface" % "0.13.3" % " val isJava17Plus: Boolean = { val v = sys.props.getOrElse("java.specification.version", "1.8") // getOrElse(false): non-numeric versions (e.g. early-access "17-ea") fail safe - // by not compiling the jackson3 module rather than making an optimistic guess. + // by not compiling the Jackson 3 module (msgpack-jackson) rather than making an + // optimistic guess. if (v.startsWith("1.")) false else scala.util.Try(v.toInt >= 17).getOrElse(false) } @@ -136,8 +137,8 @@ lazy val root = Project(id = "msgpack-java", base = file(".")) publishLocal := {} ) .aggregate( - Seq[ProjectReference](msgpackCore, msgpackJackson) ++ - (if (isJava17Plus) Seq[ProjectReference](msgpackJackson3) else Nil): _* + Seq[ProjectReference](msgpackCore, msgpackJackson2) ++ + (if (isJava17Plus) Seq[ProjectReference](msgpackJackson) else Nil): _* ) lazy val msgpackCore = Project(id = "msgpack-core", base = file("msgpack-core")) @@ -181,33 +182,36 @@ lazy val msgpackCore = Project(id = "msgpack-core", base = file("msgpack-core")) ) ) -lazy val msgpackJackson = Project(id = "msgpack-jackson", base = file("msgpack-jackson")) +// Maintenance-mode module for Jackson 2.x users: Jackson 2.x dependency bumps and +// bug fixes only. To be dropped in a future major release when Jackson 2 usage fades. +lazy val msgpackJackson2 = Project(id = "msgpack-jackson2", base = file("msgpack-jackson2")) .enablePlugins(SbtOsgi) .settings( buildSettings, - name := "jackson-dataformat-msgpack", - description := "Jackson extension that adds support for MessagePack", - OsgiKeys.bundleSymbolicName := "org.msgpack.msgpack-jackson", + name := "jackson2-dataformat-msgpack", + description := "Jackson 2.x extension that adds support for MessagePack", + OsgiKeys.bundleSymbolicName := "org.msgpack.msgpack-jackson2", OsgiKeys.exportPackage := Seq("org.msgpack.jackson", "org.msgpack.jackson.dataformat"), libraryDependencies ++= Seq( "com.fasterxml.jackson.core" % "jackson-databind" % "2.22.1", junitJupiter, junitVintage, + "com.github.sbt.junit" % "jupiter-interface" % JupiterKeys.jupiterVersion.value % "test", "org.apache.commons" % "commons-math3" % "3.6.1" % "test" ), testOptions += Tests.Argument(TestFrameworks.JUnit, "-v") ) .dependsOn(msgpackCore) -lazy val msgpackJackson3 = Project(id = "msgpack-jackson3", base = file("msgpack-jackson3")) +lazy val msgpackJackson = Project(id = "msgpack-jackson", base = file("msgpack-jackson")) .enablePlugins(SbtOsgi, JmhPlugin) .settings( buildSettings, - name := "jackson-dataformat-msgpack-jackson3", + name := "jackson-dataformat-msgpack", description := "Jackson 3.x extension that adds support for MessagePack", - OsgiKeys.bundleSymbolicName := "org.msgpack.msgpack-jackson3", - OsgiKeys.exportPackage := Seq("org.msgpack.jackson", "org.msgpack.jackson.dataformat"), + OsgiKeys.bundleSymbolicName := "org.msgpack.msgpack-jackson", + OsgiKeys.exportPackage := Seq("org.msgpack.jackson3", "org.msgpack.jackson3.dataformat"), OsgiKeys.importPackage := Seq("!android.os", "!sun.*"), Test / fork := true, javacOptions := Seq("--release", "17"), diff --git a/msgpack-jackson/README.md b/msgpack-jackson/README.md index 9e149e4d..c04f8bc5 100644 --- a/msgpack-jackson/README.md +++ b/msgpack-jackson/README.md @@ -1,12 +1,12 @@ -# jackson-dataformat-msgpack-jackson3 +# jackson-dataformat-msgpack This Jackson 3.x extension library is a component to easily read and write [MessagePack](http://msgpack.org/) encoded data through jackson-databind API. It extends standard Jackson streaming API (`JsonFactory`, `JsonParser`, `JsonGenerator`), and as such works seamlessly with all the higher level data abstractions (data binding, tree model, and pluggable extensions). -**Requirements:** Java 17+ and Jackson 3.x. For the Jackson 2.x compatible version, see [`msgpack-jackson`](../msgpack-jackson/). +**Requirements:** Java 17+ and Jackson 3.x. This artifact supports Jackson 3.x since version 1.0.0 (version 0.9.x supported Jackson 2.x). For the Jackson 2.x compatible version, see [`jackson2-dataformat-msgpack`](../msgpack-jackson2/). -**Note on imports:** Jackson 3 moved its core and databind packages from `com.fasterxml.jackson` to `tools.jackson`. User-facing annotations (`@JsonProperty`, `@JsonFormat`, etc.) remain in `com.fasterxml.jackson.annotation` for backward compatibility. +**Note on imports:** Jackson 3 moved its core and databind packages from `com.fasterxml.jackson` to `tools.jackson`. User-facing annotations (`@JsonProperty`, `@JsonFormat`, etc.) remain in `com.fasterxml.jackson.annotation` for backward compatibility. Similarly, this library uses the `org.msgpack.jackson3.dataformat` package, while the Jackson 2.x artifact keeps `org.msgpack.jackson.dataformat` — so both artifacts can coexist on the same classpath during an incremental migration. ## Install @@ -15,7 +15,7 @@ It extends standard Jackson streaming API (`JsonFactory`, `JsonParser`, `JsonGen ```xml org.msgpack - jackson-dataformat-msgpack-jackson3 + jackson-dataformat-msgpack (version) ``` @@ -23,7 +23,7 @@ It extends standard Jackson streaming API (`JsonFactory`, `JsonParser`, `JsonGen ### Sbt ```scala -libraryDependencies += "org.msgpack" % "jackson-dataformat-msgpack-jackson3" % "(version)" +libraryDependencies += "org.msgpack" % "jackson-dataformat-msgpack" % "(version)" ``` ### Gradle @@ -34,7 +34,7 @@ repositories { } dependencies { - implementation 'org.msgpack:jackson-dataformat-msgpack-jackson3:(version)' + implementation 'org.msgpack:jackson-dataformat-msgpack:(version)' } ``` @@ -219,7 +219,7 @@ System.out.println(deserialized); // => {42=Hello} ### Serialize and deserialize BigDecimal as str type internally in MessagePack format -`jackson-dataformat-msgpack-jackson3` represents BigDecimal values as float type in MessagePack format by default for backward compatibility. But the default behavior could fail when handling too large value for `double` type. So we strongly recommend calling `MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString()` to internally handle BigDecimal values as String. +`jackson-dataformat-msgpack` represents BigDecimal values as float type in MessagePack format by default for backward compatibility. But the default behavior could fail when handling too large value for `double` type. So we strongly recommend calling `MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString()` to internally handle BigDecimal values as String. ```java ObjectMapper objectMapper = MessagePackMapper.builder().handleBigIntegerAndBigDecimalAsString().build(); diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/BenchmarkState.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/BenchmarkState.java similarity index 82% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/BenchmarkState.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/BenchmarkState.java index 8e47ceaa..5217f099 100644 --- a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/BenchmarkState.java +++ b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/BenchmarkState.java @@ -13,12 +13,12 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat.benchmark; +package org.msgpack.jackson3.dataformat.benchmark; -import org.msgpack.jackson.dataformat.MessagePackFactory; -import org.msgpack.jackson.dataformat.MessagePackMapper; -import org.msgpack.jackson.dataformat.benchmark.model.MediaItem; -import org.msgpack.jackson.dataformat.benchmark.model.MediaItems; +import org.msgpack.jackson3.dataformat.MessagePackFactory; +import org.msgpack.jackson3.dataformat.MessagePackMapper; +import org.msgpack.jackson3.dataformat.benchmark.model.MediaItem; +import org.msgpack.jackson3.dataformat.benchmark.model.MediaItems; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; import tools.jackson.databind.ObjectMapper; diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackReadBenchmark.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/MsgpackReadBenchmark.java similarity index 93% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackReadBenchmark.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/MsgpackReadBenchmark.java index f7860bc5..49b410d3 100644 --- a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackReadBenchmark.java +++ b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/MsgpackReadBenchmark.java @@ -13,9 +13,9 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat.benchmark; +package org.msgpack.jackson3.dataformat.benchmark; -import org.msgpack.jackson.dataformat.benchmark.model.MediaItem; +import org.msgpack.jackson3.dataformat.benchmark.model.MediaItem; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackWriteBenchmark.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/MsgpackWriteBenchmark.java similarity index 90% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackWriteBenchmark.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/MsgpackWriteBenchmark.java index 9ed2bfa6..491b8415 100644 --- a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackWriteBenchmark.java +++ b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/MsgpackWriteBenchmark.java @@ -13,10 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat.benchmark; +package org.msgpack.jackson3.dataformat.benchmark; -import org.msgpack.jackson.dataformat.benchmark.model.MediaItem; -import org.msgpack.jackson.dataformat.benchmark.model.MediaItems; +import org.msgpack.jackson3.dataformat.benchmark.model.MediaItem; +import org.msgpack.jackson3.dataformat.benchmark.model.MediaItems; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/NopOutputStream.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/NopOutputStream.java similarity index 94% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/NopOutputStream.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/NopOutputStream.java index 983a27e2..050644c0 100644 --- a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/NopOutputStream.java +++ b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/NopOutputStream.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat.benchmark; +package org.msgpack.jackson3.dataformat.benchmark; import java.io.OutputStream; diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/WriteUTF8StringBenchmark.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/WriteUTF8StringBenchmark.java similarity index 96% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/WriteUTF8StringBenchmark.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/WriteUTF8StringBenchmark.java index 764a0efb..b835c008 100644 --- a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/WriteUTF8StringBenchmark.java +++ b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/WriteUTF8StringBenchmark.java @@ -13,9 +13,9 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat.benchmark; +package org.msgpack.jackson3.dataformat.benchmark; -import org.msgpack.jackson.dataformat.MessagePackFactory; +import org.msgpack.jackson3.dataformat.MessagePackFactory; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Image.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Image.java similarity index 94% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Image.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Image.java index 607d813c..38cbca3f 100644 --- a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Image.java +++ b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Image.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat.benchmark.model; +package org.msgpack.jackson3.dataformat.benchmark.model; public class Image { diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaContent.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaContent.java similarity index 95% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaContent.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaContent.java index 9af96caa..0ccea6d7 100644 --- a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaContent.java +++ b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaContent.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat.benchmark.model; +package org.msgpack.jackson3.dataformat.benchmark.model; import java.util.ArrayList; import java.util.List; diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItem.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaItem.java similarity index 95% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItem.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaItem.java index 0bed7a69..3b8ceaec 100644 --- a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItem.java +++ b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaItem.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat.benchmark.model; +package org.msgpack.jackson3.dataformat.benchmark.model; import com.fasterxml.jackson.annotation.JsonPropertyOrder; diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItems.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaItems.java similarity index 96% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItems.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaItems.java index dd77700f..adb498f8 100644 --- a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItems.java +++ b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaItems.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat.benchmark.model; +package org.msgpack.jackson3.dataformat.benchmark.model; public class MediaItems { diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Player.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Player.java similarity index 91% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Player.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Player.java index 82740c66..f36e8c0c 100644 --- a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Player.java +++ b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Player.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat.benchmark.model; +package org.msgpack.jackson3.dataformat.benchmark.model; public enum Player { diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Size.java b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Size.java similarity index 91% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Size.java rename to msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Size.java index 36e56489..5ab05d59 100644 --- a/msgpack-jackson/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Size.java +++ b/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Size.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat.benchmark.model; +package org.msgpack.jackson3.dataformat.benchmark.model; public enum Size { diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/ExtensionTypeCustomDeserializers.java similarity index 97% rename from msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/ExtensionTypeCustomDeserializers.java index aa587975..fef827ec 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/ExtensionTypeCustomDeserializers.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import java.io.IOException; import java.util.Map; diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/JsonArrayFormat.java similarity index 97% rename from msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/JsonArrayFormat.java index 1b1e048b..aab44f72 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/JsonArrayFormat.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.databind.cfg.MapperConfig; import tools.jackson.databind.introspect.Annotated; diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackExtensionType.java similarity index 98% rename from msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackExtensionType.java index 1b62b80e..08fc7936 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackExtensionType.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.core.JsonGenerator; import tools.jackson.databind.SerializationContext; diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackFactory.java similarity index 99% rename from msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackFactory.java index e6b0bf01..9162b0a2 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackFactory.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.core.ErrorReportConfiguration; import tools.jackson.core.FormatFeature; diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactoryBuilder.java b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackFactoryBuilder.java similarity index 98% rename from msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactoryBuilder.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackFactoryBuilder.java index 59f80991..dcef30c3 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactoryBuilder.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackFactoryBuilder.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.core.ErrorReportConfiguration; import tools.jackson.core.StreamReadConstraints; diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackGenerator.java similarity index 99% rename from msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackGenerator.java index cc6e9137..379c6bb5 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackGenerator.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.core.Base64Variant; import tools.jackson.core.JacksonException; diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackKeySerializer.java similarity index 96% rename from msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackKeySerializer.java index 836a905d..55c44202 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackKeySerializer.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.core.JsonGenerator; import tools.jackson.databind.SerializationContext; diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackMapper.java similarity index 98% rename from msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackMapper.java index 674b2733..9eb1a5cd 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackMapper.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import com.fasterxml.jackson.annotation.JsonFormat; diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackParser.java similarity index 99% rename from msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackParser.java index 98fb9c88..0b5363bb 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackParser.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.core.Base64Variant; import tools.jackson.core.JacksonException; diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackReadContext.java similarity index 99% rename from msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackReadContext.java index ea830626..b6b54a0c 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackReadContext.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.core.TokenStreamContext; import tools.jackson.core.TokenStreamLocation; diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackSerializedString.java similarity index 98% rename from msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackSerializedString.java index 8bff3000..802db2cf 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackSerializedString.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.core.SerializableString; diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackSerializerFactory.java similarity index 97% rename from msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackSerializerFactory.java index 0aa72293..9578ca95 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackSerializerFactory.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.databind.JavaType; import tools.jackson.databind.SerializationContext; diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackWriteContext.java b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackWriteContext.java similarity index 98% rename from msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackWriteContext.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackWriteContext.java index 99d7e2ac..9d3857e1 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackWriteContext.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackWriteContext.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.core.TokenStreamContext; import tools.jackson.core.exc.StreamWriteException; diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/PackageVersion.java b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/PackageVersion.java similarity index 83% rename from msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/PackageVersion.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/PackageVersion.java index 255ce1d6..423c03ab 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/PackageVersion.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/PackageVersion.java @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.core.Version; import tools.jackson.core.Versioned; @@ -20,7 +20,7 @@ public class PackageVersion implements Versioned { - public static final Version VERSION = new Version(0, 9, 12, null, "org.msgpack", "jackson-dataformat-msgpack-jackson3"); + public static final Version VERSION = new Version(1, 0, 0, null, "org.msgpack", "jackson-dataformat-msgpack"); @Override public Version version() diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/TimestampExtensionModule.java similarity index 99% rename from msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/TimestampExtensionModule.java index a1df18e4..0bbc684d 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/TimestampExtensionModule.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.core.JsonGenerator; import tools.jackson.core.JsonParser; diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/Tuple.java b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/Tuple.java similarity index 95% rename from msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/Tuple.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/Tuple.java index b0f720d8..c11bb98c 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/Tuple.java +++ b/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/Tuple.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; public class Tuple { diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/ExampleOfTypeInformationSerDe.java similarity index 99% rename from msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/ExampleOfTypeInformationSerDe.java index 6a194a8c..857f83c3 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/ExampleOfTypeInformationSerDe.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.core.JsonGenerator; import tools.jackson.core.JsonParser; diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatForFieldIdTest.java similarity index 99% rename from msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatForFieldIdTest.java index 23983a39..6f3d48da 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatForFieldIdTest.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.core.JsonParser; import tools.jackson.core.JacksonException; diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatForPojoTest.java similarity index 99% rename from msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatForPojoTest.java index c8083189..d3dd4d6f 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatForPojoTest.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.databind.ObjectMapper; import org.junit.Test; diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatTestBase.java similarity index 99% rename from msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatTestBase.java index 8a7f1e52..5442cdff 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatTestBase.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackFactoryTest.java similarity index 99% rename from msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackFactoryTest.java index 25ef0fe2..a04faf23 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackFactoryTest.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.core.JsonEncoding; import tools.jackson.core.JsonGenerator; diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackGeneratorTest.java similarity index 99% rename from msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackGeneratorTest.java index 2d593846..0323f705 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackGeneratorTest.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import com.fasterxml.jackson.annotation.JsonProperty; import tools.jackson.core.JsonGenerator; @@ -1191,7 +1191,7 @@ public void testVersion() { assertNotEquals(null, factory.version()); assertEquals("org.msgpack", factory.version().getGroupId()); - assertEquals("jackson-dataformat-msgpack-jackson3", factory.version().getArtifactId()); + assertEquals("jackson-dataformat-msgpack", factory.version().getArtifactId()); } @Test diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackMapperTest.java similarity index 98% rename from msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackMapperTest.java index 776a1109..b1a87eda 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackMapperTest.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.core.JacksonException; import org.junit.Test; diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackParserTest.java similarity index 99% rename from msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackParserTest.java index 7b0ceee6..0b200abe 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackParserTest.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.core.JsonParser; import tools.jackson.core.JacksonException; diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackWriteContextTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackWriteContextTest.java similarity index 99% rename from msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackWriteContextTest.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackWriteContextTest.java index 1f542fc7..eb95c2bc 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackWriteContextTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackWriteContextTest.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.core.JsonGenerator; import tools.jackson.core.ObjectWriteContext; diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/TimestampExtensionModuleTest.java similarity index 99% rename from msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/TimestampExtensionModuleTest.java index 36f5c97f..091fb9c0 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java +++ b/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/TimestampExtensionModuleTest.java @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -package org.msgpack.jackson.dataformat; +package org.msgpack.jackson3.dataformat; import tools.jackson.databind.ObjectMapper; import org.junit.Before; diff --git a/msgpack-jackson2/README.md b/msgpack-jackson2/README.md index 0156453e..7d823e6e 100644 --- a/msgpack-jackson2/README.md +++ b/msgpack-jackson2/README.md @@ -1,9 +1,11 @@ -# jackson-dataformat-msgpack +# jackson2-dataformat-msgpack -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.msgpack/jackson-dataformat-msgpack/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.msgpack/jackson-dataformat-msgpack/) -[![Javadoc](https://www.javadoc.io/badge/org.msgpack/jackson-dataformat-msgpack.svg)](https://www.javadoc.io/doc/org.msgpack/jackson-dataformat-msgpack) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.msgpack/jackson2-dataformat-msgpack/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.msgpack/jackson2-dataformat-msgpack/) +[![Javadoc](https://www.javadoc.io/badge/org.msgpack/jackson2-dataformat-msgpack.svg)](https://www.javadoc.io/doc/org.msgpack/jackson2-dataformat-msgpack) -This Jackson extension library is a component to easily read and write [MessagePack](http://msgpack.org/) encoded data through jackson-databind API. +This Jackson 2.x extension library is a component to easily read and write [MessagePack](http://msgpack.org/) encoded data through jackson-databind API. + +**Maintenance mode:** this module is maintained for existing Jackson 2.x users — it receives Jackson 2.x dependency bumps and bug fixes only, and will be dropped in a future major release when Jackson 2 usage fades. For Jackson 3.x, use [`jackson-dataformat-msgpack`](../msgpack-jackson/) (Java 17+). Before msgpack-java 1.0.0, this code was published as `jackson-dataformat-msgpack` (0.9.x); migrating from 0.9.x only requires changing the artifactId — the Java package (`org.msgpack.jackson.dataformat`) is unchanged. It extends standard Jackson streaming API (`JsonFactory`, `JsonParser`, `JsonGenerator`), and as such works seamlessly with all the higher level data abstractions (data binding, tree model, and pluggable extensions). For the details of Jackson-annotations, please see https://github.com/FasterXML/jackson-annotations. @@ -16,7 +18,7 @@ This library isn't compatible with msgpack-java v0.6 or earlier by default in se ``` org.msgpack - jackson-dataformat-msgpack + jackson2-dataformat-msgpack (version) ``` @@ -24,7 +26,7 @@ This library isn't compatible with msgpack-java v0.6 or earlier by default in se ### Sbt ``` -libraryDependencies += "org.msgpack" % "jackson-dataformat-msgpack" % "(version)" +libraryDependencies += "org.msgpack" % "jackson2-dataformat-msgpack" % "(version)" ``` ### Gradle @@ -34,7 +36,7 @@ repositories { } dependencies { - compile 'org.msgpack:jackson-dataformat-msgpack:(version)' + compile 'org.msgpack:jackson2-dataformat-msgpack:(version)' } ``` @@ -153,7 +155,7 @@ Java In msgpack-java:0.6 or earlier, a POJO was serliazed and deserialized as an array of values in MessagePack format. The order of values depended on an internal order of Java class's variables and it was a naive way and caused some issues since Java class's variables order isn't guaranteed over Java implementations. -On the other hand, jackson-databind serializes and deserializes a POJO as a key-value object. So this `jackson-dataformat-msgpack` also handles POJOs in the same way. As a result, it isn't compatible with msgpack-java:0.6 or earlier in serialization and deserialization of POJOs. +On the other hand, jackson-databind serializes and deserializes a POJO as a key-value object. So this `jackson2-dataformat-msgpack` also handles POJOs in the same way. As a result, it isn't compatible with msgpack-java:0.6 or earlier in serialization and deserialization of POJOs. But if you want to make this library handle POJOs in the same way as msgpack-java:0.6 or earlier, you can use `JsonArrayFormat` like this: @@ -232,7 +234,7 @@ When you want to use non-String value as a key of Map, use `MessagePackKeySerial ### Serialize and deserialize BigDecimal as str type internally in MessagePack format -`jackson-dataformat-msgpack` represents BigDecimal values as float type in MessagePack format by default for backward compatibility. But the default behavior could fail when handling too large value for `double` type. So we strongly recommend to call `MessagePackMapper#handleBigIntegerAndBigDecimalAsString()` to internally handle BigDecimal values as String. +`jackson2-dataformat-msgpack` represents BigDecimal values as float type in MessagePack format by default for backward compatibility. But the default behavior could fail when handling too large value for `double` type. So we strongly recommend to call `MessagePackMapper#handleBigIntegerAndBigDecimalAsString()` to internally handle BigDecimal values as String. ```java ObjectMapper objectMapper = new MessagePackMapper().handleBigIntegerAndBigDecimalAsString(); diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java index 72aeed20..4731a31b 100644 --- a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java +++ b/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java @@ -585,13 +585,14 @@ public JsonStreamContext getParsingContext() @Override public JsonLocation currentTokenLocation() { - return new JsonLocation(ioContext.contentReference(), tokenPosition, -1, -1); + // Byte offset is exposed as columnNr since JsonLocation has no field for it + return new JsonLocation(ioContext.contentReference(), tokenPosition, -1, -1, (int) tokenPosition); } @Override public JsonLocation currentLocation() { - return new JsonLocation(ioContext.contentReference(), currentPosition, -1, -1); + return new JsonLocation(ioContext.contentReference(), currentPosition, -1, -1, (int) currentPosition); } @Override diff --git a/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java index b9ef4cf3..5c37df2c 100644 --- a/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java +++ b/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java @@ -19,8 +19,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.After; -import org.junit.Before; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -49,7 +49,7 @@ public class MessagePackDataformatTestBase protected TinyPojo tinyPojo; protected ComplexPojo complexPojo; - @Before + @BeforeEach public void setup() { factory = new MessagePackFactory(); @@ -100,7 +100,7 @@ public void setup() complexPojo.values = Arrays.asList("one", "two", "three"); } - @After + @AfterEach public void teardown() { if (in != null) { diff --git a/project/plugins.sbt b/project/plugins.sbt index f886a411..b0223931 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -7,5 +7,7 @@ addSbtPlugin("com.github.sbt" % "sbt-osgi" % "0.11.0-RC1") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.2") addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.1") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8") +// Runs JUnit 5 (Jupiter) tests from sbt; without this, JUnit 5 tests are silently skipped +addSbtPlugin("com.github.sbt.junit" % "sbt-jupiter-interface" % "0.19.0") scalacOptions ++= Seq("-deprecation", "-feature") From 3b3ea6a9e88bbcc691309e762de8323d41578be5 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Tue, 28 Jul 2026 14:19:01 -0700 Subject: [PATCH 6/7] Pin sbt-jupiter-interface to 0.15.2 for JDK 8 compatibility 0.17+ upgrades to JUnit 6, which requires Java 17; the JDK 8 CI lane failed compiling msgpack-jackson2 tests against junit-jupiter-api 6.0.3 (class file version 61). 0.15.2 stays on JUnit 5.14, and the module's explicit junit-jupiter 5.14.4 dependency wins resolution. Verified by forking the tests onto a real JDK 8. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_014ZZUaHusAjVx6SNu4sA42s --- project/plugins.sbt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index b0223931..91cf328e 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -7,7 +7,9 @@ addSbtPlugin("com.github.sbt" % "sbt-osgi" % "0.11.0-RC1") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.2") addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.1") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8") -// Runs JUnit 5 (Jupiter) tests from sbt; without this, JUnit 5 tests are silently skipped -addSbtPlugin("com.github.sbt.junit" % "sbt-jupiter-interface" % "0.19.0") +// Runs JUnit 5 (Jupiter) tests from sbt; without this, JUnit 5 tests are silently skipped. +// Pinned to 0.15.x: 0.17+ upgrades to JUnit 6, which requires Java 17 and would break +// running msgpack-jackson2 tests on JDK 8. +addSbtPlugin("com.github.sbt.junit" % "sbt-jupiter-interface" % "0.15.2") scalacOptions ++= Seq("-deprecation", "-feature") From 98e90f5910b3cb4309a54dee049f8e73bdf55758 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Mon, 10 Aug 2026 14:08:34 -0700 Subject: [PATCH 7/7] Publish Jackson 3 module under new groupId org.msgpack.jackson3 Restructure the module layout per PR review feedback (JLBP-6): instead of renaming the Jackson 2 artifact to jackson2-dataformat-msgpack and giving Jackson 3 the unqualified jackson-dataformat-msgpack artifactId, rename the Maven coordinates and the Java package together: - Jackson 2 (msgpack-jackson): keeps org.msgpack:jackson-dataformat-msgpack and the org.msgpack.jackson.dataformat package, exactly as 0.9.x. Existing users need no changes. Maintenance mode: Jackson 2.x bumps and bug fixes only. - Jackson 3 (msgpack-jackson3): published as org.msgpack.jackson3:jackson-dataformat-msgpack with the org.msgpack.jackson3.dataformat package, mirroring Jackson's own 2->3 groupId rename (com.fasterxml.jackson.dataformat -> tools.jackson.dataformat). Because the two artifacts share the same fully-qualified class names in neither direction, they can coexist on one classpath, and dependency resolution can never silently replace the Jackson 2 classes with Jackson 3 ones (or vice versa). The 1.0.0 version bump is no longer needed. Both sbt subprojects now share the module name jackson-dataformat-msgpack, so the Jackson 3 project overrides outputPath to keep sbt 2 build output directories from overlapping. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01C4Sp7Q2urgnYfuNXK2fVFm --- .github/workflows/CI.yml | 8 +- .github/workflows/release.yml | 8 +- CLAUDE.md | 12 +- README.md | 14 +- build.sbt | 60 +- msgpack-jackson/README.md | 594 +++++++++--------- .../ExtensionTypeCustomDeserializers.java | 0 .../msgpack/jackson/dataformat/JavaInfo.java | 0 .../jackson/dataformat/JsonArrayFormat.java | 0 .../dataformat/MessagePackExtensionType.java | 0 .../dataformat/MessagePackFactory.java | 0 .../dataformat/MessagePackGenerator.java | 0 .../dataformat/MessagePackKeySerializer.java | 0 .../jackson/dataformat/MessagePackMapper.java | 0 .../jackson/dataformat/MessagePackParser.java | 0 .../dataformat/MessagePackReadContext.java | 0 .../MessagePackSerializedString.java | 0 .../MessagePackSerializerFactory.java | 0 .../dataformat/TimestampExtensionModule.java | 0 .../org/msgpack/jackson/dataformat/Tuple.java | 0 .../ExampleOfTypeInformationSerDe.java | 0 .../MessagePackDataformatForFieldIdTest.java | 0 .../MessagePackDataformatForPojoTest.java | 0 .../MessagePackDataformatTestBase.java | 0 .../dataformat/MessagePackFactoryTest.java | 0 .../dataformat/MessagePackGeneratorTest.java | 0 .../dataformat/MessagePackMapperTest.java | 0 .../dataformat/MessagePackParserTest.java | 0 .../TimestampExtensionModuleTest.java | 0 .../dataformat/benchmark/Benchmarker.java | 0 ...gePackDataformatHugeDataBenchmarkTest.java | 0 ...essagePackDataformatPojoBenchmarkTest.java | 0 msgpack-jackson2/README.md | 494 --------------- msgpack-jackson3/README.md | 480 ++++++++++++++ .../dataformat/benchmark/BenchmarkState.java | 0 .../benchmark/MsgpackReadBenchmark.java | 0 .../benchmark/MsgpackWriteBenchmark.java | 0 .../dataformat/benchmark/NopOutputStream.java | 0 .../benchmark/WriteUTF8StringBenchmark.java | 0 .../dataformat/benchmark/model/Image.java | 0 .../benchmark/model/MediaContent.java | 0 .../dataformat/benchmark/model/MediaItem.java | 0 .../benchmark/model/MediaItems.java | 0 .../dataformat/benchmark/model/Player.java | 0 .../dataformat/benchmark/model/Size.java | 0 .../ExtensionTypeCustomDeserializers.java | 0 .../jackson3/dataformat/JsonArrayFormat.java | 0 .../dataformat/MessagePackExtensionType.java | 0 .../dataformat/MessagePackFactory.java | 0 .../dataformat/MessagePackFactoryBuilder.java | 0 .../dataformat/MessagePackGenerator.java | 0 .../dataformat/MessagePackKeySerializer.java | 0 .../dataformat/MessagePackMapper.java | 0 .../dataformat/MessagePackParser.java | 0 .../dataformat/MessagePackReadContext.java | 0 .../MessagePackSerializedString.java | 0 .../MessagePackSerializerFactory.java | 0 .../dataformat/MessagePackWriteContext.java | 0 .../jackson3/dataformat/PackageVersion.java | 2 +- .../dataformat/TimestampExtensionModule.java | 0 .../msgpack/jackson3/dataformat/Tuple.java | 0 .../ExampleOfTypeInformationSerDe.java | 0 .../MessagePackDataformatForFieldIdTest.java | 0 .../MessagePackDataformatForPojoTest.java | 0 .../MessagePackDataformatTestBase.java | 0 .../dataformat/MessagePackFactoryTest.java | 0 .../dataformat/MessagePackGeneratorTest.java | 2 +- .../dataformat/MessagePackMapperTest.java | 0 .../dataformat/MessagePackParserTest.java | 0 .../MessagePackWriteContextTest.java | 0 .../TimestampExtensionModuleTest.java | 0 project/plugins.sbt | 12 +- 72 files changed, 850 insertions(+), 836 deletions(-) rename {msgpack-jackson2 => msgpack-jackson}/src/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/main/java/org/msgpack/jackson/dataformat/JavaInfo.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/main/java/org/msgpack/jackson/dataformat/Tuple.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/test/java/org/msgpack/jackson/dataformat/benchmark/Benchmarker.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java (100%) rename {msgpack-jackson2 => msgpack-jackson}/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java (100%) delete mode 100644 msgpack-jackson2/README.md create mode 100644 msgpack-jackson3/README.md rename {msgpack-jackson => msgpack-jackson3}/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/BenchmarkState.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/MsgpackReadBenchmark.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/MsgpackWriteBenchmark.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/NopOutputStream.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/WriteUTF8StringBenchmark.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Image.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaContent.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaItem.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaItems.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Player.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Size.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/main/java/org/msgpack/jackson3/dataformat/ExtensionTypeCustomDeserializers.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/main/java/org/msgpack/jackson3/dataformat/JsonArrayFormat.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/main/java/org/msgpack/jackson3/dataformat/MessagePackExtensionType.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/main/java/org/msgpack/jackson3/dataformat/MessagePackFactory.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/main/java/org/msgpack/jackson3/dataformat/MessagePackFactoryBuilder.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/main/java/org/msgpack/jackson3/dataformat/MessagePackGenerator.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/main/java/org/msgpack/jackson3/dataformat/MessagePackKeySerializer.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/main/java/org/msgpack/jackson3/dataformat/MessagePackMapper.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/main/java/org/msgpack/jackson3/dataformat/MessagePackParser.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/main/java/org/msgpack/jackson3/dataformat/MessagePackReadContext.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/main/java/org/msgpack/jackson3/dataformat/MessagePackSerializedString.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/main/java/org/msgpack/jackson3/dataformat/MessagePackSerializerFactory.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/main/java/org/msgpack/jackson3/dataformat/MessagePackWriteContext.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/main/java/org/msgpack/jackson3/dataformat/PackageVersion.java (87%) rename {msgpack-jackson => msgpack-jackson3}/src/main/java/org/msgpack/jackson3/dataformat/TimestampExtensionModule.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/main/java/org/msgpack/jackson3/dataformat/Tuple.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/test/java/org/msgpack/jackson3/dataformat/ExampleOfTypeInformationSerDe.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatForFieldIdTest.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatForPojoTest.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatTestBase.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/test/java/org/msgpack/jackson3/dataformat/MessagePackFactoryTest.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/test/java/org/msgpack/jackson3/dataformat/MessagePackGeneratorTest.java (99%) rename {msgpack-jackson => msgpack-jackson3}/src/test/java/org/msgpack/jackson3/dataformat/MessagePackMapperTest.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/test/java/org/msgpack/jackson3/dataformat/MessagePackParserTest.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/test/java/org/msgpack/jackson3/dataformat/MessagePackWriteContextTest.java (100%) rename {msgpack-jackson => msgpack-jackson3}/src/test/java/org/msgpack/jackson3/dataformat/TimestampExtensionModuleTest.java (100%) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 3da7f02d..178765d8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -28,7 +28,7 @@ jobs: - 'project/build.properties' - 'msgpack-core/**' - 'msgpack-jackson/**' - - 'msgpack-jackson2/**' + - 'msgpack-jackson3/**' docs: - '**.md' - '**.txt' @@ -83,10 +83,10 @@ jobs: env: TEST_JAVA_HOME: ${{ steps.target-jdk.outputs.path }} run: | - # msgpack-jackson (Jackson 3) requires JDK 17+; older lanes test the + # msgpack-jackson3 (Jackson 3) requires JDK 17+; older lanes test the # Java 8 compatible modules only if [[ ${{ matrix.java }} -lt 17 ]]; then - ./sbt msgpack-core/test msgpack-jackson2/test + ./sbt msgpack-core/test msgpack-jackson/test else ./sbt test fi @@ -95,7 +95,7 @@ jobs: TEST_JAVA_HOME: ${{ steps.target-jdk.outputs.path }} run: | if [[ ${{ matrix.java }} -lt 17 ]]; then - ./sbt msgpack-core/test msgpack-jackson2/test -J-Dmsgpack.universal-buffer=true + ./sbt msgpack-core/test msgpack-jackson/test -J-Dmsgpack.universal-buffer=true else ./sbt test -J-Dmsgpack.universal-buffer=true fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9bc590d6..067d1a6a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,17 +38,17 @@ jobs: PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} TEST_JAVA_HOME: ${{ steps.jdk8.outputs.path }} run: | - ./sbt msgpack-core/publishSigned msgpack-jackson2/publishSigned - # msgpack-jackson (Jackson 3) requires JDK 17+ + ./sbt msgpack-core/publishSigned msgpack-jackson/publishSigned + # msgpack-jackson3 (Jackson 3) requires JDK 17+ - uses: actions/setup-java@v5 with: java-version: 17 distribution: temurin - - name: Build bundle for msgpack-jackson (Jackson 3) + - name: Build bundle for msgpack-jackson3 (Jackson 3) env: PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} run: | - ./sbt msgpack-jackson/publishSigned + ./sbt msgpack-jackson3/publishSigned - name: Release to Sonatype env: SONATYPE_USERNAME: '${{ secrets.SONATYPE_USERNAME }}' diff --git a/CLAUDE.md b/CLAUDE.md index e784bb13..171f8d76 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,8 +6,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co MessagePack-Java is a binary serialization library that provides a fast and compact alternative to JSON. The project consists of two main modules: - **msgpack-core**: Standalone MessagePack implementation with no external dependencies -- **msgpack-jackson**: Jackson 3.x integration for object mapping capabilities (artifact `jackson-dataformat-msgpack`, Java 17+) -- **msgpack-jackson2**: Jackson 2.x integration in maintenance mode (artifact `jackson2-dataformat-msgpack`, Java 8+) +- **msgpack-jackson**: Jackson 2.x integration in maintenance mode (`org.msgpack:jackson-dataformat-msgpack`, Java 8+) +- **msgpack-jackson3**: Jackson 3.x integration for object mapping capabilities (`org.msgpack.jackson3:jackson-dataformat-msgpack`, Java 17+) ## Essential Development Commands @@ -50,8 +50,8 @@ The main entry point is the `MessagePack` factory class which creates: Key locations: - Core interfaces: `msgpack-core/src/main/java/org/msgpack/core/` -- Jackson 3 integration: `msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/` -- Jackson 2 integration: `msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/` +- Jackson 2 integration: `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/` +- Jackson 3 integration: `msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/` ### Buffer Management System MessagePack uses an efficient buffer abstraction layer: @@ -70,8 +70,8 @@ The msgpack-jackson module provides: ### Testing Structure - **msgpack-core tests**: Written in Scala (always use the latest Scala 3 version) using AirSpec framework - Location: `msgpack-core/src/test/scala/` -- **msgpack-jackson / msgpack-jackson2 tests**: Written in Java using JUnit - - Location: `msgpack-jackson/src/test/java/`, `msgpack-jackson2/src/test/java/` +- **msgpack-jackson / msgpack-jackson3 tests**: Written in Java using JUnit + - Location: `msgpack-jackson/src/test/java/`, `msgpack-jackson3/src/test/java/` ## Important JVM Options diff --git a/README.md b/README.md index 77f7b5fa..520a9da7 100644 --- a/README.md +++ b/README.md @@ -57,12 +57,12 @@ msgpack-java supports serialization and deserialization of Java objects through Two artifacts are published depending on which Jackson major version your project uses: -| Jackson version | Artifact | Requirements | -| --- | --- | --- | -| Jackson 3.x | [`jackson-dataformat-msgpack`](https://github.com/msgpack/msgpack-java/blob/main/msgpack-jackson/README.md) (1.x) | Java 17+ | -| Jackson 2.x | [`jackson2-dataformat-msgpack`](https://github.com/msgpack/msgpack-java/blob/main/msgpack-jackson2/README.md) (maintenance mode) | Java 8+ | +| Jackson version | groupId | artifactId | Java package | Requirements | +| --- | --- | --- | --- | --- | +| Jackson 3.x | `org.msgpack.jackson3` | [`jackson-dataformat-msgpack`](https://github.com/msgpack/msgpack-java/blob/main/msgpack-jackson3/README.md) | `org.msgpack.jackson3.dataformat` | Java 17+ | +| Jackson 2.x | `org.msgpack` | [`jackson-dataformat-msgpack`](https://github.com/msgpack/msgpack-java/blob/main/msgpack-jackson/README.md) (maintenance mode) | `org.msgpack.jackson.dataformat` | Java 8+ | -Since msgpack-java 1.0.0, `jackson-dataformat-msgpack` targets Jackson 3.x. If your project still depends on Jackson 2.x, either stay on `jackson-dataformat-msgpack` 0.9.x or switch to `jackson2-dataformat-msgpack` to keep receiving msgpack-core updates (only the artifactId changes; the Java package is the same). See each module's README for install instructions and usage details. +The Jackson 2.x artifact keeps the same Maven coordinates and Java package as before, so existing users need no changes. The Jackson 3.x support is published under a new groupId (`org.msgpack.jackson3`) with a new Java package, following the same convention as Jackson itself (`com.fasterxml.jackson.dataformat` → `tools.jackson.dataformat`); renaming the Maven coordinates and the Java package together allows both artifacts to coexist on the same classpath for incremental migration. See each module's README for install instructions and usage details. - [Release Notes](https://github.com/msgpack/msgpack-java/blob/develop/RELEASE_NOTES.md) @@ -153,6 +153,6 @@ If some sporadic error happens (e.g., Sonatype timeout), rerun `sonaRelease` aga ``` msgpack-core # Contains packer/unpacker implementation that never uses third-party libraries -msgpack-jackson # jackson-dataformat-msgpack: Jackson 3.x integration (Java 17+) -msgpack-jackson2 # jackson2-dataformat-msgpack: Jackson 2.x integration (maintenance mode) +msgpack-jackson # org.msgpack:jackson-dataformat-msgpack: Jackson 2.x integration (maintenance mode) +msgpack-jackson3 # org.msgpack.jackson3:jackson-dataformat-msgpack: Jackson 3.x integration (Java 17+) ``` diff --git a/build.sbt b/build.sbt index 34b4ae8d..e1144f67 100644 --- a/build.sbt +++ b/build.sbt @@ -125,7 +125,10 @@ val isJava17Plus: Boolean = { // getOrElse(false): non-numeric versions (e.g. early-access "17-ea") fail safe // by not compiling the Jackson 3 module (msgpack-jackson) rather than making an // optimistic guess. - if (v.startsWith("1.")) false else scala.util.Try(v.toInt >= 17).getOrElse(false) + if (v.startsWith("1.")) + false + else + scala.util.Try(v.toInt >= 17).getOrElse(false) } lazy val root = Project(id = "msgpack-java", base = file(".")) @@ -137,8 +140,12 @@ lazy val root = Project(id = "msgpack-java", base = file(".")) publishLocal := {} ) .aggregate( - Seq[ProjectReference](msgpackCore, msgpackJackson2) ++ - (if (isJava17Plus) Seq[ProjectReference](msgpackJackson) else Nil): _* + Seq[ProjectReference](msgpackCore, msgpackJackson) ++ ( + if (isJava17Plus) + Seq[ProjectReference](msgpackJackson3) + else + Nil + ): _* ) lazy val msgpackCore = Project(id = "msgpack-core", base = file("msgpack-core")) @@ -182,15 +189,16 @@ lazy val msgpackCore = Project(id = "msgpack-core", base = file("msgpack-core")) ) ) -// Maintenance-mode module for Jackson 2.x users: Jackson 2.x dependency bumps and -// bug fixes only. To be dropped in a future major release when Jackson 2 usage fades. -lazy val msgpackJackson2 = Project(id = "msgpack-jackson2", base = file("msgpack-jackson2")) +// Jackson 2.x module. Keeps the same Maven coordinates (org.msgpack:jackson-dataformat-msgpack) +// and Java package as the 0.9.x line, so existing Jackson 2.x users need no changes. +// Maintenance mode: Jackson 2.x dependency bumps and bug fixes only. +lazy val msgpackJackson = Project(id = "msgpack-jackson", base = file("msgpack-jackson")) .enablePlugins(SbtOsgi) .settings( buildSettings, - name := "jackson2-dataformat-msgpack", + name := "jackson-dataformat-msgpack", description := "Jackson 2.x extension that adds support for MessagePack", - OsgiKeys.bundleSymbolicName := "org.msgpack.msgpack-jackson2", + OsgiKeys.bundleSymbolicName := "org.msgpack.msgpack-jackson", OsgiKeys.exportPackage := Seq("org.msgpack.jackson", "org.msgpack.jackson.dataformat"), libraryDependencies ++= Seq( @@ -198,33 +206,39 @@ lazy val msgpackJackson2 = Project(id = "msgpack-jackson2", base = file("msgpack junitJupiter, junitVintage, "com.github.sbt.junit" % "jupiter-interface" % JupiterKeys.jupiterVersion.value % "test", - "org.apache.commons" % "commons-math3" % "3.6.1" % "test" + "org.apache.commons" % "commons-math3" % "3.6.1" % "test" ), testOptions += Tests.Argument(TestFrameworks.JUnit, "-v") ) .dependsOn(msgpackCore) -lazy val msgpackJackson = Project(id = "msgpack-jackson", base = file("msgpack-jackson")) +// Jackson 3.x module. Published under a new groupId (org.msgpack.jackson3) with a new +// Java package (org.msgpack.jackson3.dataformat), following JLBP-6: the Maven coordinates +// and the Java package are renamed together, so this artifact can coexist on the same +// classpath with the Jackson 2.x artifact (org.msgpack:jackson-dataformat-msgpack). +lazy val msgpackJackson3 = Project(id = "msgpack-jackson3", base = file("msgpack-jackson3")) .enablePlugins(SbtOsgi, JmhPlugin) .settings( buildSettings, - name := "jackson-dataformat-msgpack", + organization := "org.msgpack.jackson3", + name := "jackson-dataformat-msgpack", + // sbt derives build output paths from moduleName, which would collide with the + // Jackson 2 module publishing the same artifactId; use the project id instead + outputPath := s"${platform.value}/u/msgpack-jackson3", description := "Jackson 3.x extension that adds support for MessagePack", - OsgiKeys.bundleSymbolicName := "org.msgpack.msgpack-jackson", + OsgiKeys.bundleSymbolicName := "org.msgpack.jackson3.jackson-dataformat-msgpack", OsgiKeys.exportPackage := Seq("org.msgpack.jackson3", "org.msgpack.jackson3.dataformat"), OsgiKeys.importPackage := Seq("!android.os", "!sun.*"), - Test / fork := true, - javacOptions := Seq("--release", "17"), - doc / javacOptions := Seq("--release", "17", "-Xdoclint:none"), + Test / fork := true, + javacOptions := Seq("--release", "17"), + doc / javacOptions := Seq("--release", "17", "-Xdoclint:none"), libraryDependencies ++= - Seq( - "tools.jackson.core" % "jackson-databind" % "3.1.2", - junitInterface - ), + Seq("tools.jackson.core" % "jackson-databind" % "3.1.2", junitInterface), testOptions += Tests.Argument(TestFrameworks.JUnit, "-v"), - Jmh / javaOptions ++= Seq( - "--add-opens=java.base/java.nio=ALL-UNNAMED", - "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED" - ) + Jmh / javaOptions ++= + Seq( + "--add-opens=java.base/java.nio=ALL-UNNAMED", + "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED" + ) ) .dependsOn(msgpackCore) diff --git a/msgpack-jackson/README.md b/msgpack-jackson/README.md index c04f8bc5..0f3ed35e 100644 --- a/msgpack-jackson/README.md +++ b/msgpack-jackson/README.md @@ -1,18 +1,21 @@ # jackson-dataformat-msgpack -This Jackson 3.x extension library is a component to easily read and write [MessagePack](http://msgpack.org/) encoded data through jackson-databind API. +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.msgpack/jackson-dataformat-msgpack/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.msgpack/jackson-dataformat-msgpack/) +[![Javadoc](https://www.javadoc.io/badge/org.msgpack/jackson-dataformat-msgpack.svg)](https://www.javadoc.io/doc/org.msgpack/jackson-dataformat-msgpack) -It extends standard Jackson streaming API (`JsonFactory`, `JsonParser`, `JsonGenerator`), and as such works seamlessly with all the higher level data abstractions (data binding, tree model, and pluggable extensions). +This Jackson 2.x extension library is a component to easily read and write [MessagePack](http://msgpack.org/) encoded data through jackson-databind API. -**Requirements:** Java 17+ and Jackson 3.x. This artifact supports Jackson 3.x since version 1.0.0 (version 0.9.x supported Jackson 2.x). For the Jackson 2.x compatible version, see [`jackson2-dataformat-msgpack`](../msgpack-jackson2/). +**Maintenance mode:** this module keeps the same Maven coordinates (`org.msgpack:jackson-dataformat-msgpack`) and Java package (`org.msgpack.jackson.dataformat`) as before, so existing Jackson 2.x users need no changes. It receives Jackson 2.x dependency bumps and bug fixes only. For Jackson 3.x, use [`org.msgpack.jackson3:jackson-dataformat-msgpack`](../msgpack-jackson3/) (Java 17+), which uses a different groupId and Java package so both artifacts can coexist on the same classpath. -**Note on imports:** Jackson 3 moved its core and databind packages from `com.fasterxml.jackson` to `tools.jackson`. User-facing annotations (`@JsonProperty`, `@JsonFormat`, etc.) remain in `com.fasterxml.jackson.annotation` for backward compatibility. Similarly, this library uses the `org.msgpack.jackson3.dataformat` package, while the Jackson 2.x artifact keeps `org.msgpack.jackson.dataformat` — so both artifacts can coexist on the same classpath during an incremental migration. +It extends standard Jackson streaming API (`JsonFactory`, `JsonParser`, `JsonGenerator`), and as such works seamlessly with all the higher level data abstractions (data binding, tree model, and pluggable extensions). For the details of Jackson-annotations, please see https://github.com/FasterXML/jackson-annotations. + +This library isn't compatible with msgpack-java v0.6 or earlier by default in serialization/deserialization of POJO. See **Advanced usage** below for details. ## Install ### Maven -```xml +``` org.msgpack jackson-dataformat-msgpack @@ -22,180 +25,192 @@ It extends standard Jackson streaming API (`JsonFactory`, `JsonParser`, `JsonGen ### Sbt -```scala +``` libraryDependencies += "org.msgpack" % "jackson-dataformat-msgpack" % "(version)" ``` ### Gradle - -```groovy +``` repositories { mavenCentral() } dependencies { - implementation 'org.msgpack:jackson-dataformat-msgpack:(version)' + compile 'org.msgpack:jackson-dataformat-msgpack:(version)' } ``` + ## Basic usage ### Serialization/Deserialization of POJO -Only thing you need to do is to instantiate `MessagePackFactory` and pass it to the constructor of `tools.jackson.databind.ObjectMapper`. And then, you can use it for MessagePack format data in the same way as jackson-databind. +Only thing you need to do is to instantiate `MessagePackFactory` and pass it to the constructor of `com.fasterxml.jackson.databind.ObjectMapper`. And then, you can use it for MessagePack format data in the same way as jackson-databind. ```java -// Instantiate ObjectMapper for MessagePack -ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - -// Serialize a Java object to byte array -ExamplePojo pojo = new ExamplePojo("komamitsu"); -byte[] bytes = objectMapper.writeValueAsBytes(pojo); - -// Deserialize the byte array to a Java object -ExamplePojo deserialized = objectMapper.readValue(bytes, ExamplePojo.class); -System.out.println(deserialized.getName()); // => komamitsu + // Instantiate ObjectMapper for MessagePack + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + + // Serialize a Java object to byte array + ExamplePojo pojo = new ExamplePojo("komamitsu"); + byte[] bytes = objectMapper.writeValueAsBytes(pojo); + + // Deserialize the byte array to a Java object + ExamplePojo deserialized = objectMapper.readValue(bytes, ExamplePojo.class); + System.out.println(deserialized.getName()); // => komamitsu ``` Or more easily: ```java -ObjectMapper objectMapper = new MessagePackMapper(); + ObjectMapper objectMapper = new MessagePackMapper(); ``` -We strongly recommend calling `MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString()` if you serialize and/or deserialize BigInteger/BigDecimal values. See [Serialize and deserialize BigDecimal as str type internally in MessagePack format](#serialize-and-deserialize-bigdecimal-as-str-type-internally-in-messagepack-format) for details. +We strongly recommend to call `MessagePackMapper#handleBigIntegerAndBigDecimalAsString()` if you serialize and/or deserialize BigInteger/BigDecimal values. See [Serialize and deserialize BigDecimal as str type internally in MessagePack format](#serialize-and-deserialize-bigdecimal-as-str-type-internally-in-messagepack-format) for details. ```java -ObjectMapper objectMapper = MessagePackMapper.builder().handleBigIntegerAndBigDecimalAsString().build(); + ObjectMapper objectMapper = new MessagePackMapper().handleBigIntegerAndBigDecimalAsString(); ``` ### Serialization/Deserialization of List ```java -// Instantiate ObjectMapper for MessagePack -ObjectMapper objectMapper = new MessagePackMapper(); - -// Serialize a List to byte array -List list = new ArrayList<>(); -list.add("Foo"); -list.add("Bar"); -list.add(42); -byte[] bytes = objectMapper.writeValueAsBytes(list); - -// Deserialize the byte array to a List -List deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); -System.out.println(deserialized); // => [Foo, Bar, 42] + // Instantiate ObjectMapper for MessagePack + ObjectMapper objectMapper = new MessagePackMapper(); + + // Serialize a List to byte array + List list = new ArrayList<>(); + list.add("Foo"); + list.add("Bar"); + list.add(42); + byte[] bytes = objectMapper.writeValueAsBytes(list); + + // Deserialize the byte array to a List + List deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); + System.out.println(deserialized); // => [Foo, Bar, 42] ``` ### Serialization/Deserialization of Map ```java -// Instantiate ObjectMapper for MessagePack -ObjectMapper objectMapper = new MessagePackMapper(); - -// Serialize a Map to byte array -Map map = new HashMap<>(); -map.put("name", "komamitsu"); -map.put("age", 42); -byte[] bytes = objectMapper.writeValueAsBytes(map); - -// Deserialize the byte array to a Map -Map deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); -System.out.println(deserialized); // => {name=komamitsu, age=42} -``` + // Instantiate ObjectMapper for MessagePack + ObjectMapper objectMapper = MessagePackMapper(); + + // Serialize a Map to byte array + Map map = new HashMap<>(); + map.put("name", "komamitsu"); + map.put("age", 42); + byte[] bytes = objectMapper.writeValueAsBytes(map); + + // Deserialize the byte array to a Map + Map deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); + System.out.println(deserialized); // => {name=komamitsu, age=42} + + ``` ### Example of Serialization/Deserialization over multiple languages Java ```java -// Serialize -Map obj = new HashMap(); -obj.put("foo", "hello"); -obj.put("bar", "world"); -byte[] bs = objectMapper.writeValueAsBytes(obj); -// bs => [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111, -// -93, 98, 97, 114, -91, 119, 111, 114, 108, 100] + // Serialize + Map obj = new HashMap(); + obj.put("foo", "hello"); + obj.put("bar", "world"); + byte[] bs = objectMapper.writeValueAsBytes(obj); + // bs => [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111, + // -93, 98, 97, 114, -91, 119, 111, 114, 108, 100] ``` Ruby ```ruby -require 'msgpack' + require 'msgpack' -# Deserialize -xs = [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111, - -93, 98, 97, 114, -91, 119, 111, 114, 108, 100] -MessagePack.unpack(xs.pack("C*")) -# => {"foo"=>"hello", "bar"=>"world"} + # Deserialize + xs = [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111, + -93, 98, 97, 114, -91, 119, 111, 114, 108, 100] + MessagePack.unpack(xs.pack("C*")) + # => {"foo"=>"hello", "bar"=>"world"} -# Serialize -["zero", 1, 2.0, nil].to_msgpack.unpack('C*') -# => [148, 164, 122, 101, 114, 111, 1, 203, 64, 0, 0, 0, 0, 0, 0, 0, 192] + # Serialize + ["zero", 1, 2.0, nil].to_msgpack.unpack('C*') + # => [148, 164, 122, 101, 114, 111, 1, 203, 64, 0, 0, 0, 0, 0, 0, 0, 192] ``` Java ```java -// Deserialize -bs = new byte[] {(byte) 148, (byte) 164, 122, 101, 114, 111, 1, - (byte) 203, 64, 0, 0, 0, 0, 0, 0, 0, (byte) 192}; -TypeReference> typeReference = new TypeReference>(){}; -List xs = objectMapper.readValue(bs, typeReference); -// xs => [zero, 1, 2.0, null] + // Deserialize + bs = new byte[] {(byte) 148, (byte) 164, 122, 101, 114, 111, 1, + (byte) 203, 64, 0, 0, 0, 0, 0, 0, 0, (byte) 192}; + TypeReference> typeReference = new TypeReference>(){}; + List xs = objectMapper.readValue(bs, typeReference); + // xs => [zero, 1, 2.0, null] ``` ## Advanced usage +### Serialize/Deserialize POJO as MessagePack array type to keep compatibility with msgpack-java:0.6 + +In msgpack-java:0.6 or earlier, a POJO was serliazed and deserialized as an array of values in MessagePack format. The order of values depended on an internal order of Java class's variables and it was a naive way and caused some issues since Java class's variables order isn't guaranteed over Java implementations. + +On the other hand, jackson-databind serializes and deserializes a POJO as a key-value object. So this `jackson-dataformat-msgpack` also handles POJOs in the same way. As a result, it isn't compatible with msgpack-java:0.6 or earlier in serialization and deserialization of POJOs. + +But if you want to make this library handle POJOs in the same way as msgpack-java:0.6 or earlier, you can use `JsonArrayFormat` like this: + +```java + ObjectMapper objectMapper = new MessagePackMapper(); + objectMapper.setAnnotationIntrospector(new JsonArrayFormat()); +``` + ### Serialize multiple values without closing an output stream -`tools.jackson.databind.ObjectMapper` closes an output stream by default after it writes a value. If you want to serialize multiple values in a row without closing an output stream, disable `StreamWriteFeature.AUTO_CLOSE_TARGET`. +`com.fasterxml.jackson.databind.ObjectMapper` closes an output stream by default after it writes a value. If you want to serialize multiple values in a row without closing an output stream, set `JsonGenerator.Feature.AUTO_CLOSE_TARGET` to false. ```java -OutputStream out = new FileOutputStream(tempFile); -ObjectMapper objectMapper = MessagePackMapper.builder() - .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) - .build(); - -objectMapper.writeValue(out, 1); -objectMapper.writeValue(out, "two"); -objectMapper.writeValue(out, 3.14); -out.close(); - -MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(new FileInputStream(tempFile)); -System.out.println(unpacker.unpackInt()); // => 1 -System.out.println(unpacker.unpackString()); // => two -System.out.println(unpacker.unpackFloat()); // => 3.14 + OutputStream out = new FileOutputStream(tempFile); + ObjectMapper objectMapper = new MessagePackMapper(); + objectMapper.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false); + + objectMapper.writeValue(out, 1); + objectMapper.writeValue(out, "two"); + objectMapper.writeValue(out, 3.14); + out.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(new FileInputStream(tempFile)); + System.out.println(unpacker.unpackInt()); // => 1 + System.out.println(unpacker.unpackString()); // => two + System.out.println(unpacker.unpackFloat()); // => 3.14 ``` ### Deserialize multiple values without closing an input stream -`tools.jackson.databind.ObjectMapper` closes an input stream by default after it reads a value. If you want to deserialize multiple values in a row without closing an input stream, disable `StreamReadFeature.AUTO_CLOSE_SOURCE`. +`com.fasterxml.jackson.databind.ObjectMapper` closes an input stream by default after it reads a value. If you want to deserialize multiple values in a row without closing an output stream, set `JsonParser.Feature.AUTO_CLOSE_SOURCE` to false. ```java -MessagePacker packer = MessagePack.newDefaultPacker(new FileOutputStream(tempFile)); -packer.packInt(42); -packer.packString("Hello"); -packer.close(); - -FileInputStream in = new FileInputStream(tempFile); -ObjectMapper objectMapper = MessagePackMapper.builder() - .disable(StreamReadFeature.AUTO_CLOSE_SOURCE) - .build(); -System.out.println(objectMapper.readValue(in, Integer.class)); -System.out.println(objectMapper.readValue(in, String.class)); -in.close(); + MessagePacker packer = MessagePack.newDefaultPacker(new FileOutputStream(tempFile)); + packer.packInt(42); + packer.packString("Hello"); + packer.close(); + + FileInputStream in = new FileInputStream(tempFile); + ObjectMapper objectMapper = new MessagePackMapper(); + objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false); + System.out.println(objectMapper.readValue(in, Integer.class)); + System.out.println(objectMapper.readValue(in, String.class)); + in.close(); ``` ### Serialize not using str8 type -Old msgpack-java (e.g 0.6.7) doesn't support MessagePack str8 type. When your application needs to communicate with such an old MessagePack library, you can disable the data type like this: +Old msgpack-java (e.g 0.6.7) doesn't support MessagePack str8 type. When your application needs to comunicate with such an old MessagePack library, you can disable the data type like this: ```java -MessagePack.PackerConfig config = new MessagePack.PackerConfig().withStr8FormatSupport(false); -ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory(config)); -// This string is serialized as bin8 type -byte[] resultWithoutStr8Format = objectMapper.writeValueAsBytes(str8LengthString); + MessagePack.PackerConfig config = new MessagePack.PackerConfig().withStr8FormatSupport(false); + ObjectMapper mapperWithConfig = new MessagePackMapper(new MessagePackFactory(config)); + // This string is serialized as bin8 type + byte[] resultWithoutStr8Format = mapperWithConfig.writeValueAsBytes(str8LengthString); ``` ### Serialize using non-String as a key of Map @@ -203,61 +218,60 @@ byte[] resultWithoutStr8Format = objectMapper.writeValueAsBytes(str8LengthString When you want to use non-String value as a key of Map, use `MessagePackKeySerializer` for key serialization. ```java -@JsonSerialize(keyUsing = MessagePackKeySerializer.class) -private Map intMap = new HashMap<>(); + @JsonSerialize(keyUsing = MessagePackKeySerializer.class) + private Map intMap = new HashMap<>(); - : + : -intMap.put(42, "Hello"); + intMap.put(42, "Hello"); -ObjectMapper objectMapper = new MessagePackMapper(); -byte[] bytes = objectMapper.writeValueAsBytes(intMap); + ObjectMapper objectMapper = new MessagePackMapper(); + byte[] bytes = objectMapper.writeValueAsBytes(intMap); -Map deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); -System.out.println(deserialized); // => {42=Hello} + Map deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); + System.out.println(deserialized); // => {42=Hello} ``` ### Serialize and deserialize BigDecimal as str type internally in MessagePack format -`jackson-dataformat-msgpack` represents BigDecimal values as float type in MessagePack format by default for backward compatibility. But the default behavior could fail when handling too large value for `double` type. So we strongly recommend calling `MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString()` to internally handle BigDecimal values as String. +`jackson-dataformat-msgpack` represents BigDecimal values as float type in MessagePack format by default for backward compatibility. But the default behavior could fail when handling too large value for `double` type. So we strongly recommend to call `MessagePackMapper#handleBigIntegerAndBigDecimalAsString()` to internally handle BigDecimal values as String. ```java -ObjectMapper objectMapper = MessagePackMapper.builder().handleBigIntegerAndBigDecimalAsString().build(); + ObjectMapper objectMapper = new MessagePackMapper().handleBigIntegerAndBigDecimalAsString(); -Pojo obj = new Pojo(); -// This value is too large to be serialized as double -obj.value = new BigDecimal("1234567890.98765432100"); + Pojo obj = new Pojo(); + // This value is too large to be serialized as double + obj.value = new BigDecimal("1234567890.98765432100"); -byte[] converted = objectMapper.writeValueAsBytes(obj); + byte[] converted = objectMapper.writeValueAsBytes(obj); -System.out.println(objectMapper.readValue(converted, Pojo.class)); // => Pojo{value=1234567890.98765432100} + System.out.println(objectMapper.readValue(converted, Pojo.class)); // => Pojo{value=1234567890.98765432100} ``` - -`MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString()` is equivalent to the following configuration. +`MessagePackMapper#handleBigIntegerAndDecimalAsString()` is equivalent to the following configuration. ```java -ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); -objectMapper.configOverride(BigInteger.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); -objectMapper.configOverride(BigDecimal.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); + ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + objectMapper.configOverride(BigInteger.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); + objectMapper.configOverride(BigDecimal.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); ``` + ### 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. +`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 = MessagePackMapper.builder() - .addModule(TimestampExtensionModule.INSTANCE) - .build(); -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" + ObjectMapper objectMapper = new MessagePackMapper() + .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 @@ -267,178 +281,178 @@ System.out.println(deserialized); // "2022-09-14T08:47:24.922Z" #### Deserialize extension type value directly ```java -// In this application, extension type 59 is used for byte[] -byte[] bytes; -{ - // This ObjectMapper is just for temporary serialization - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - MessagePacker packer = MessagePack.newDefaultPacker(outputStream); - - packer.packExtensionTypeHeader((byte) 59, hexspeak.length); - packer.addPayload(hexspeak); - packer.close(); - - bytes = outputStream.toByteArray(); -} + // In this application, extension type 59 is used for byte[] + byte[] bytes; + { + // This ObjectMapper is just for temporary serialization + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + MessagePacker packer = MessagePack.newDefaultPacker(outputStream); -// Register the type and a deserializer to ExtensionTypeCustomDeserializers -ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); -extTypeCustomDesers.addCustomDeser((byte) 59, data -> { - if (Arrays.equals(data, - new byte[] {(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE})) { - return "Java"; - } - return "Not Java"; -}); + packer.packExtensionTypeHeader((byte) 59, hexspeak.length); + packer.addPayload(hexspeak); + packer.close(); -ObjectMapper objectMapper = new ObjectMapper( - new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)); + bytes = outputStream.toByteArray(); + } -System.out.println(objectMapper.readValue(bytes, Object.class)); - // => Java + // Register the type and a deserializer to ExtensionTypeCustomDeserializers + ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); + extTypeCustomDesers.addCustomDeser((byte) 59, data -> { + if (Arrays.equals(data, + new byte[] {(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE})) { + return "Java"; + } + return "Not Java"; + }); + + ObjectMapper objectMapper = new ObjectMapper( + new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)); + + System.out.println(objectMapper.readValue(bytes, Object.class)); + // => Java ``` #### Use extension type as Map key ```java -static class TripleBytesPojo -{ - public byte first; - public byte second; - public byte third; - - public TripleBytesPojo(byte first, byte second, byte third) + static class TripleBytesPojo { - this.first = first; - this.second = second; - this.third = third; - } + public byte first; + public byte second; + public byte third; - @Override - public boolean equals(Object o) - { - : - } + public TripleBytesPojo(byte first, byte second, byte third) + { + this.first = first; + this.second = second; + this.third = third; + } - @Override - public int hashCode() - { - : - } + @Override + public boolean equals(Object o) + { + : + } - @Override - public String toString() - { - // This key format is used when serialized as map key - return String.format("%d-%d-%d", first, second, third); - } + @Override + public int hashCode() + { + : + } - static class KeyDeserializer - extends tools.jackson.databind.KeyDeserializer - { @Override - public Object deserializeKey(String key, DeserializationContext ctxt) + public String toString() { - String[] values = key.split("-"); - return new TripleBytesPojo(Byte.parseByte(values[0]), Byte.parseByte(values[1]), Byte.parseByte(values[2])); + // This key format is used when serialized as map key + return String.format("%d-%d-%d", first, second, third); } - } - static TripleBytesPojo deserialize(byte[] bytes) - { - return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]); + static class KeyDeserializer + extends com.fasterxml.jackson.databind.KeyDeserializer + { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) + throws IOException + { + String[] values = key.split("-"); + return new TripleBytesPojo(Byte.parseByte(values[0]), Byte.parseByte(values[1]), Byte.parseByte(values[2])); + } + } + + static TripleBytesPojo deserialize(byte[] bytes) + { + return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]); + } } -} -: + : -byte extTypeCode = 42; + byte extTypeCode = 42; -ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); -extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() -{ - @Override - public Object deserialize(byte[] value) - throws IOException + ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); + extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() { - return TripleBytesPojo.deserialize(value); - } -}); - -SimpleModule module = new SimpleModule(); -module.addKeyDeserializer(TripleBytesPojo.class, new TripleBytesPojo.KeyDeserializer()); -ObjectMapper objectMapper = MessagePackMapper.builder( - new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) - .addModule(module) - .build(); - -Map deserializedMap = - objectMapper.readValue(serializedData, - new TypeReference>() {}); + @Override + public Object deserialize(byte[] value) + throws IOException + { + return TripleBytesPojo.deserialize(value); + } + }); + + SimpleModule module = new SimpleModule(); + module.addKeyDeserializer(TripleBytesPojo.class, new TripleBytesPojo.KeyDeserializer()); + ObjectMapper objectMapper = new ObjectMapper( + new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) + .registerModule(module); + + Map deserializedMap = + objectMapper.readValue(serializedData, + new TypeReference>() {}); ``` #### Use extension type as Map value ```java -static class TripleBytesPojo -{ - public byte first; - public byte second; - public byte third; - - public TripleBytesPojo(byte first, byte second, byte third) + static class TripleBytesPojo { - this.first = first; - this.second = second; - this.third = third; - } + public byte first; + public byte second; + public byte third; - static class Deserializer - extends StdDeserializer - { - protected Deserializer() + public TripleBytesPojo(byte first, byte second, byte third) { - super(TripleBytesPojo.class); + this.first = first; + this.second = second; + this.third = third; } - @Override - public TripleBytesPojo deserialize(JsonParser p, DeserializationContext ctxt) + static class Deserializer + extends StdDeserializer { - return TripleBytesPojo.deserialize(p.getBinaryValue()); + protected Deserializer() + { + super(TripleBytesPojo.class); + } + + @Override + public TripleBytesPojo deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException, JsonProcessingException + { + return TripleBytesPojo.deserialize(p.getBinaryValue()); + } } - } - static TripleBytesPojo deserialize(byte[] bytes) - { - return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]); + static TripleBytesPojo deserialize(byte[] bytes) + { + return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]); + } } -} -: + : -byte extTypeCode = 42; + byte extTypeCode = 42; -ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); -extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() -{ - @Override - public Object deserialize(byte[] value) - throws IOException + ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); + extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() { - return TripleBytesPojo.deserialize(value); - } -}); - -SimpleModule module = new SimpleModule(); -module.addDeserializer(TripleBytesPojo.class, new TripleBytesPojo.Deserializer()); -ObjectMapper objectMapper = MessagePackMapper.builder( - new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) - .addModule(module) - .build(); - -Map deserializedMap = - objectMapper.readValue(serializedData, - new TypeReference>() {}); + @Override + public Object deserialize(byte[] value) + throws IOException + { + return TripleBytesPojo.deserialize(value); + } + }); + + SimpleModule module = new SimpleModule(); + module.addDeserializer(TripleBytesPojo.class, new TripleBytesPojo.Deserializer()); + ObjectMapper objectMapper = new ObjectMapper( + new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) + .registerModule(module); + + Map deserializedMap = + objectMapper.readValue(serializedData, + new TypeReference>() {}); ``` ### Serialize a nested object that also serializes @@ -446,35 +460,35 @@ Map deserializedMap = When you serialize an object that has a nested object also serializing with ObjectMapper and MessagePackFactory like the following code, it throws NullPointerException since the nested MessagePackFactory modifies a shared state stored in ThreadLocal. ```java -@Test -public void testNestedSerialization() throws Exception -{ - ObjectMapper objectMapper = new MessagePackMapper(); - objectMapper.writeValueAsBytes(new OuterClass()); -} + @Test + public void testNestedSerialization() throws Exception + { + ObjectMapper objectMapper = new MessagePackMapper(); + objectMapper.writeValueAsBytes(new OuterClass()); + } -public class OuterClass -{ - public String getInner() throws JacksonException + public class OuterClass { - ObjectMapper m = new MessagePackMapper(); - m.writeValueAsBytes(new InnerClass()); - return "EFG"; + public String getInner() throws JsonProcessingException + { + ObjectMapper m = new MessagePackMapper(); + m.writeValueAsBytes(new InnerClass()); + return "EFG"; + } } -} -public class InnerClass -{ - public String getName() + public class InnerClass { - return "ABC"; + public String getName() + { + return "ABC"; + } } -} ``` -There are a few options to fix this issue, but they introduce performance degradations while this usage is a corner case. A workaround that doesn't affect performance is to call `MessagePackFactory#setReuseResourceInGenerator(false)`. It might be inconvenient to call the API for users, but it's a reasonable tradeoff with performance for now. +There are a few options to fix this issue, but they introduce performance degredations while this usage is a corner case. A workaround that doesn't affect performance is to call `MessagePackFactory#setReuseResourceInGenerator(false)`. It might be inconvenient to call the API for users, but it's a reasonable tradeoff with performance for now. ```java -ObjectMapper objectMapper = new ObjectMapper( - new MessagePackFactory().setReuseResourceInGenerator(false)); + ObjectMapper objectMapper = new ObjectMapper( + new MessagePackFactory().setReuseResourceInGenerator(false)); ``` diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java similarity index 100% rename from msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/JavaInfo.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/JavaInfo.java similarity index 100% rename from msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/JavaInfo.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/JavaInfo.java diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java similarity index 100% rename from msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java similarity index 100% rename from msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java similarity index 100% rename from msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java similarity index 100% rename from msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java similarity index 100% rename from msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java similarity index 100% rename from msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java similarity index 100% rename from msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java similarity index 100% rename from msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java similarity index 100% rename from msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java similarity index 100% rename from msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java similarity index 100% rename from msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java diff --git a/msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/Tuple.java b/msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/Tuple.java similarity index 100% rename from msgpack-jackson2/src/main/java/org/msgpack/jackson/dataformat/Tuple.java rename to msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/Tuple.java diff --git a/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java similarity index 100% rename from msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java diff --git a/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java similarity index 100% rename from msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java diff --git a/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java similarity index 100% rename from msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java diff --git a/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java similarity index 100% rename from msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java diff --git a/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java similarity index 100% rename from msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java diff --git a/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java similarity index 100% rename from msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java diff --git a/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java similarity index 100% rename from msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java diff --git a/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java similarity index 100% rename from msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java diff --git a/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java similarity index 100% rename from msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java diff --git a/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/benchmark/Benchmarker.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/Benchmarker.java similarity index 100% rename from msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/benchmark/Benchmarker.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/Benchmarker.java diff --git a/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java similarity index 100% rename from msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java diff --git a/msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java b/msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java similarity index 100% rename from msgpack-jackson2/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java rename to msgpack-jackson/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java diff --git a/msgpack-jackson2/README.md b/msgpack-jackson2/README.md deleted file mode 100644 index 7d823e6e..00000000 --- a/msgpack-jackson2/README.md +++ /dev/null @@ -1,494 +0,0 @@ -# jackson2-dataformat-msgpack - -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.msgpack/jackson2-dataformat-msgpack/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.msgpack/jackson2-dataformat-msgpack/) -[![Javadoc](https://www.javadoc.io/badge/org.msgpack/jackson2-dataformat-msgpack.svg)](https://www.javadoc.io/doc/org.msgpack/jackson2-dataformat-msgpack) - -This Jackson 2.x extension library is a component to easily read and write [MessagePack](http://msgpack.org/) encoded data through jackson-databind API. - -**Maintenance mode:** this module is maintained for existing Jackson 2.x users — it receives Jackson 2.x dependency bumps and bug fixes only, and will be dropped in a future major release when Jackson 2 usage fades. For Jackson 3.x, use [`jackson-dataformat-msgpack`](../msgpack-jackson/) (Java 17+). Before msgpack-java 1.0.0, this code was published as `jackson-dataformat-msgpack` (0.9.x); migrating from 0.9.x only requires changing the artifactId — the Java package (`org.msgpack.jackson.dataformat`) is unchanged. - -It extends standard Jackson streaming API (`JsonFactory`, `JsonParser`, `JsonGenerator`), and as such works seamlessly with all the higher level data abstractions (data binding, tree model, and pluggable extensions). For the details of Jackson-annotations, please see https://github.com/FasterXML/jackson-annotations. - -This library isn't compatible with msgpack-java v0.6 or earlier by default in serialization/deserialization of POJO. See **Advanced usage** below for details. - -## Install - -### Maven - -``` - - org.msgpack - jackson2-dataformat-msgpack - (version) - -``` - -### Sbt - -``` -libraryDependencies += "org.msgpack" % "jackson2-dataformat-msgpack" % "(version)" -``` - -### Gradle -``` -repositories { - mavenCentral() -} - -dependencies { - compile 'org.msgpack:jackson2-dataformat-msgpack:(version)' -} -``` - - -## Basic usage - -### Serialization/Deserialization of POJO - -Only thing you need to do is to instantiate `MessagePackFactory` and pass it to the constructor of `com.fasterxml.jackson.databind.ObjectMapper`. And then, you can use it for MessagePack format data in the same way as jackson-databind. - -```java - // Instantiate ObjectMapper for MessagePack - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - - // Serialize a Java object to byte array - ExamplePojo pojo = new ExamplePojo("komamitsu"); - byte[] bytes = objectMapper.writeValueAsBytes(pojo); - - // Deserialize the byte array to a Java object - ExamplePojo deserialized = objectMapper.readValue(bytes, ExamplePojo.class); - System.out.println(deserialized.getName()); // => komamitsu -``` - -Or more easily: - -```java - ObjectMapper objectMapper = new MessagePackMapper(); -``` - -We strongly recommend to call `MessagePackMapper#handleBigIntegerAndBigDecimalAsString()` if you serialize and/or deserialize BigInteger/BigDecimal values. See [Serialize and deserialize BigDecimal as str type internally in MessagePack format](#serialize-and-deserialize-bigdecimal-as-str-type-internally-in-messagepack-format) for details. - -```java - ObjectMapper objectMapper = new MessagePackMapper().handleBigIntegerAndBigDecimalAsString(); -``` - -### Serialization/Deserialization of List - -```java - // Instantiate ObjectMapper for MessagePack - ObjectMapper objectMapper = new MessagePackMapper(); - - // Serialize a List to byte array - List list = new ArrayList<>(); - list.add("Foo"); - list.add("Bar"); - list.add(42); - byte[] bytes = objectMapper.writeValueAsBytes(list); - - // Deserialize the byte array to a List - List deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); - System.out.println(deserialized); // => [Foo, Bar, 42] -``` - -### Serialization/Deserialization of Map - -```java - // Instantiate ObjectMapper for MessagePack - ObjectMapper objectMapper = MessagePackMapper(); - - // Serialize a Map to byte array - Map map = new HashMap<>(); - map.put("name", "komamitsu"); - map.put("age", 42); - byte[] bytes = objectMapper.writeValueAsBytes(map); - - // Deserialize the byte array to a Map - Map deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); - System.out.println(deserialized); // => {name=komamitsu, age=42} - - ``` - -### Example of Serialization/Deserialization over multiple languages - -Java - -```java - // Serialize - Map obj = new HashMap(); - obj.put("foo", "hello"); - obj.put("bar", "world"); - byte[] bs = objectMapper.writeValueAsBytes(obj); - // bs => [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111, - // -93, 98, 97, 114, -91, 119, 111, 114, 108, 100] -``` - -Ruby - -```ruby - require 'msgpack' - - # Deserialize - xs = [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111, - -93, 98, 97, 114, -91, 119, 111, 114, 108, 100] - MessagePack.unpack(xs.pack("C*")) - # => {"foo"=>"hello", "bar"=>"world"} - - # Serialize - ["zero", 1, 2.0, nil].to_msgpack.unpack('C*') - # => [148, 164, 122, 101, 114, 111, 1, 203, 64, 0, 0, 0, 0, 0, 0, 0, 192] -``` - -Java - -```java - // Deserialize - bs = new byte[] {(byte) 148, (byte) 164, 122, 101, 114, 111, 1, - (byte) 203, 64, 0, 0, 0, 0, 0, 0, 0, (byte) 192}; - TypeReference> typeReference = new TypeReference>(){}; - List xs = objectMapper.readValue(bs, typeReference); - // xs => [zero, 1, 2.0, null] -``` - -## Advanced usage - -### Serialize/Deserialize POJO as MessagePack array type to keep compatibility with msgpack-java:0.6 - -In msgpack-java:0.6 or earlier, a POJO was serliazed and deserialized as an array of values in MessagePack format. The order of values depended on an internal order of Java class's variables and it was a naive way and caused some issues since Java class's variables order isn't guaranteed over Java implementations. - -On the other hand, jackson-databind serializes and deserializes a POJO as a key-value object. So this `jackson2-dataformat-msgpack` also handles POJOs in the same way. As a result, it isn't compatible with msgpack-java:0.6 or earlier in serialization and deserialization of POJOs. - -But if you want to make this library handle POJOs in the same way as msgpack-java:0.6 or earlier, you can use `JsonArrayFormat` like this: - -```java - ObjectMapper objectMapper = new MessagePackMapper(); - objectMapper.setAnnotationIntrospector(new JsonArrayFormat()); -``` - -### Serialize multiple values without closing an output stream - -`com.fasterxml.jackson.databind.ObjectMapper` closes an output stream by default after it writes a value. If you want to serialize multiple values in a row without closing an output stream, set `JsonGenerator.Feature.AUTO_CLOSE_TARGET` to false. - -```java - OutputStream out = new FileOutputStream(tempFile); - ObjectMapper objectMapper = new MessagePackMapper(); - objectMapper.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false); - - objectMapper.writeValue(out, 1); - objectMapper.writeValue(out, "two"); - objectMapper.writeValue(out, 3.14); - out.close(); - - MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(new FileInputStream(tempFile)); - System.out.println(unpacker.unpackInt()); // => 1 - System.out.println(unpacker.unpackString()); // => two - System.out.println(unpacker.unpackFloat()); // => 3.14 -``` - -### Deserialize multiple values without closing an input stream - -`com.fasterxml.jackson.databind.ObjectMapper` closes an input stream by default after it reads a value. If you want to deserialize multiple values in a row without closing an output stream, set `JsonParser.Feature.AUTO_CLOSE_SOURCE` to false. - -```java - MessagePacker packer = MessagePack.newDefaultPacker(new FileOutputStream(tempFile)); - packer.packInt(42); - packer.packString("Hello"); - packer.close(); - - FileInputStream in = new FileInputStream(tempFile); - ObjectMapper objectMapper = new MessagePackMapper(); - objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false); - System.out.println(objectMapper.readValue(in, Integer.class)); - System.out.println(objectMapper.readValue(in, String.class)); - in.close(); -``` - -### Serialize not using str8 type - -Old msgpack-java (e.g 0.6.7) doesn't support MessagePack str8 type. When your application needs to comunicate with such an old MessagePack library, you can disable the data type like this: - -```java - MessagePack.PackerConfig config = new MessagePack.PackerConfig().withStr8FormatSupport(false); - ObjectMapper mapperWithConfig = new MessagePackMapper(new MessagePackFactory(config)); - // This string is serialized as bin8 type - byte[] resultWithoutStr8Format = mapperWithConfig.writeValueAsBytes(str8LengthString); -``` - -### Serialize using non-String as a key of Map - -When you want to use non-String value as a key of Map, use `MessagePackKeySerializer` for key serialization. - -```java - @JsonSerialize(keyUsing = MessagePackKeySerializer.class) - private Map intMap = new HashMap<>(); - - : - - intMap.put(42, "Hello"); - - ObjectMapper objectMapper = new MessagePackMapper(); - byte[] bytes = objectMapper.writeValueAsBytes(intMap); - - Map deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); - System.out.println(deserialized); // => {42=Hello} -``` - -### Serialize and deserialize BigDecimal as str type internally in MessagePack format - -`jackson2-dataformat-msgpack` represents BigDecimal values as float type in MessagePack format by default for backward compatibility. But the default behavior could fail when handling too large value for `double` type. So we strongly recommend to call `MessagePackMapper#handleBigIntegerAndBigDecimalAsString()` to internally handle BigDecimal values as String. - -```java - ObjectMapper objectMapper = new MessagePackMapper().handleBigIntegerAndBigDecimalAsString(); - - Pojo obj = new Pojo(); - // This value is too large to be serialized as double - obj.value = new BigDecimal("1234567890.98765432100"); - - byte[] converted = objectMapper.writeValueAsBytes(obj); - - System.out.println(objectMapper.readValue(converted, Pojo.class)); // => Pojo{value=1234567890.98765432100} -``` -`MessagePackMapper#handleBigIntegerAndDecimalAsString()` is equivalent to the following configuration. - -```java - ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); - objectMapper.configOverride(BigInteger.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); - objectMapper.configOverride(BigDecimal.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); -``` - - -### 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 MessagePackMapper() - .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 your own custom extension types easily. - -#### Deserialize extension type value directly - -```java - // In this application, extension type 59 is used for byte[] - byte[] bytes; - { - // This ObjectMapper is just for temporary serialization - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - MessagePacker packer = MessagePack.newDefaultPacker(outputStream); - - packer.packExtensionTypeHeader((byte) 59, hexspeak.length); - packer.addPayload(hexspeak); - packer.close(); - - bytes = outputStream.toByteArray(); - } - - // Register the type and a deserializer to ExtensionTypeCustomDeserializers - ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); - extTypeCustomDesers.addCustomDeser((byte) 59, data -> { - if (Arrays.equals(data, - new byte[] {(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE})) { - return "Java"; - } - return "Not Java"; - }); - - ObjectMapper objectMapper = new ObjectMapper( - new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)); - - System.out.println(objectMapper.readValue(bytes, Object.class)); - // => Java -``` - -#### Use extension type as Map key - -```java - static class TripleBytesPojo - { - public byte first; - public byte second; - public byte third; - - public TripleBytesPojo(byte first, byte second, byte third) - { - this.first = first; - this.second = second; - this.third = third; - } - - @Override - public boolean equals(Object o) - { - : - } - - @Override - public int hashCode() - { - : - } - - @Override - public String toString() - { - // This key format is used when serialized as map key - return String.format("%d-%d-%d", first, second, third); - } - - static class KeyDeserializer - extends com.fasterxml.jackson.databind.KeyDeserializer - { - @Override - public Object deserializeKey(String key, DeserializationContext ctxt) - throws IOException - { - String[] values = key.split("-"); - return new TripleBytesPojo(Byte.parseByte(values[0]), Byte.parseByte(values[1]), Byte.parseByte(values[2])); - } - } - - static TripleBytesPojo deserialize(byte[] bytes) - { - return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]); - } - } - - : - - byte extTypeCode = 42; - - ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); - extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() - { - @Override - public Object deserialize(byte[] value) - throws IOException - { - return TripleBytesPojo.deserialize(value); - } - }); - - SimpleModule module = new SimpleModule(); - module.addKeyDeserializer(TripleBytesPojo.class, new TripleBytesPojo.KeyDeserializer()); - ObjectMapper objectMapper = new ObjectMapper( - new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) - .registerModule(module); - - Map deserializedMap = - objectMapper.readValue(serializedData, - new TypeReference>() {}); -``` - -#### Use extension type as Map value - -```java - static class TripleBytesPojo - { - public byte first; - public byte second; - public byte third; - - public TripleBytesPojo(byte first, byte second, byte third) - { - this.first = first; - this.second = second; - this.third = third; - } - - static class Deserializer - extends StdDeserializer - { - protected Deserializer() - { - super(TripleBytesPojo.class); - } - - @Override - public TripleBytesPojo deserialize(JsonParser p, DeserializationContext ctxt) - throws IOException, JsonProcessingException - { - return TripleBytesPojo.deserialize(p.getBinaryValue()); - } - } - - static TripleBytesPojo deserialize(byte[] bytes) - { - return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]); - } - } - - : - - byte extTypeCode = 42; - - ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); - extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() - { - @Override - public Object deserialize(byte[] value) - throws IOException - { - return TripleBytesPojo.deserialize(value); - } - }); - - SimpleModule module = new SimpleModule(); - module.addDeserializer(TripleBytesPojo.class, new TripleBytesPojo.Deserializer()); - ObjectMapper objectMapper = new ObjectMapper( - new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) - .registerModule(module); - - Map deserializedMap = - objectMapper.readValue(serializedData, - new TypeReference>() {}); -``` - -### Serialize a nested object that also serializes - -When you serialize an object that has a nested object also serializing with ObjectMapper and MessagePackFactory like the following code, it throws NullPointerException since the nested MessagePackFactory modifies a shared state stored in ThreadLocal. - -```java - @Test - public void testNestedSerialization() throws Exception - { - ObjectMapper objectMapper = new MessagePackMapper(); - objectMapper.writeValueAsBytes(new OuterClass()); - } - - public class OuterClass - { - public String getInner() throws JsonProcessingException - { - ObjectMapper m = new MessagePackMapper(); - m.writeValueAsBytes(new InnerClass()); - return "EFG"; - } - } - - public class InnerClass - { - public String getName() - { - return "ABC"; - } - } -``` - -There are a few options to fix this issue, but they introduce performance degredations while this usage is a corner case. A workaround that doesn't affect performance is to call `MessagePackFactory#setReuseResourceInGenerator(false)`. It might be inconvenient to call the API for users, but it's a reasonable tradeoff with performance for now. - -```java - ObjectMapper objectMapper = new ObjectMapper( - new MessagePackFactory().setReuseResourceInGenerator(false)); -``` diff --git a/msgpack-jackson3/README.md b/msgpack-jackson3/README.md new file mode 100644 index 00000000..deb6bd88 --- /dev/null +++ b/msgpack-jackson3/README.md @@ -0,0 +1,480 @@ +# jackson-dataformat-msgpack + +This Jackson 3.x extension library is a component to easily read and write [MessagePack](http://msgpack.org/) encoded data through jackson-databind API. + +It extends standard Jackson streaming API (`JsonFactory`, `JsonParser`, `JsonGenerator`), and as such works seamlessly with all the higher level data abstractions (data binding, tree model, and pluggable extensions). + +**Requirements:** Java 17+ and Jackson 3.x. This is a new artifact published under the `org.msgpack.jackson3` groupId. For the Jackson 2.x compatible version, see [`org.msgpack:jackson-dataformat-msgpack`](../msgpack-jackson/). + +**Note on imports:** Jackson 3 moved its core and databind packages from `com.fasterxml.jackson` to `tools.jackson`, while renaming the Maven groupId from `com.fasterxml.jackson.core` to `tools.jackson.core`. Similarly, this library renames both the Maven groupId (`org.msgpack` → `org.msgpack.jackson3`) and the Java package (`org.msgpack.jackson.dataformat` → `org.msgpack.jackson3.dataformat`) together, so this artifact and the Jackson 2.x artifact can coexist on the same classpath during an incremental migration. User-facing Jackson annotations (`@JsonProperty`, `@JsonFormat`, etc.) remain in `com.fasterxml.jackson.annotation` for backward compatibility. + +## Install + +### Maven + +```xml + + org.msgpack.jackson3 + jackson-dataformat-msgpack + (version) + +``` + +### Sbt + +```scala +libraryDependencies += "org.msgpack.jackson3" % "jackson-dataformat-msgpack" % "(version)" +``` + +### Gradle + +```groovy +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.msgpack.jackson3:jackson-dataformat-msgpack:(version)' +} +``` + +## Basic usage + +### Serialization/Deserialization of POJO + +Only thing you need to do is to instantiate `MessagePackFactory` and pass it to the constructor of `tools.jackson.databind.ObjectMapper`. And then, you can use it for MessagePack format data in the same way as jackson-databind. + +```java +// Instantiate ObjectMapper for MessagePack +ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); + +// Serialize a Java object to byte array +ExamplePojo pojo = new ExamplePojo("komamitsu"); +byte[] bytes = objectMapper.writeValueAsBytes(pojo); + +// Deserialize the byte array to a Java object +ExamplePojo deserialized = objectMapper.readValue(bytes, ExamplePojo.class); +System.out.println(deserialized.getName()); // => komamitsu +``` + +Or more easily: + +```java +ObjectMapper objectMapper = new MessagePackMapper(); +``` + +We strongly recommend calling `MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString()` if you serialize and/or deserialize BigInteger/BigDecimal values. See [Serialize and deserialize BigDecimal as str type internally in MessagePack format](#serialize-and-deserialize-bigdecimal-as-str-type-internally-in-messagepack-format) for details. + +```java +ObjectMapper objectMapper = MessagePackMapper.builder().handleBigIntegerAndBigDecimalAsString().build(); +``` + +### Serialization/Deserialization of List + +```java +// Instantiate ObjectMapper for MessagePack +ObjectMapper objectMapper = new MessagePackMapper(); + +// Serialize a List to byte array +List list = new ArrayList<>(); +list.add("Foo"); +list.add("Bar"); +list.add(42); +byte[] bytes = objectMapper.writeValueAsBytes(list); + +// Deserialize the byte array to a List +List deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); +System.out.println(deserialized); // => [Foo, Bar, 42] +``` + +### Serialization/Deserialization of Map + +```java +// Instantiate ObjectMapper for MessagePack +ObjectMapper objectMapper = new MessagePackMapper(); + +// Serialize a Map to byte array +Map map = new HashMap<>(); +map.put("name", "komamitsu"); +map.put("age", 42); +byte[] bytes = objectMapper.writeValueAsBytes(map); + +// Deserialize the byte array to a Map +Map deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); +System.out.println(deserialized); // => {name=komamitsu, age=42} +``` + +### Example of Serialization/Deserialization over multiple languages + +Java + +```java +// Serialize +Map obj = new HashMap(); +obj.put("foo", "hello"); +obj.put("bar", "world"); +byte[] bs = objectMapper.writeValueAsBytes(obj); +// bs => [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111, +// -93, 98, 97, 114, -91, 119, 111, 114, 108, 100] +``` + +Ruby + +```ruby +require 'msgpack' + +# Deserialize +xs = [-126, -93, 102, 111, 111, -91, 104, 101, 108, 108, 111, + -93, 98, 97, 114, -91, 119, 111, 114, 108, 100] +MessagePack.unpack(xs.pack("C*")) +# => {"foo"=>"hello", "bar"=>"world"} + +# Serialize +["zero", 1, 2.0, nil].to_msgpack.unpack('C*') +# => [148, 164, 122, 101, 114, 111, 1, 203, 64, 0, 0, 0, 0, 0, 0, 0, 192] +``` + +Java + +```java +// Deserialize +bs = new byte[] {(byte) 148, (byte) 164, 122, 101, 114, 111, 1, + (byte) 203, 64, 0, 0, 0, 0, 0, 0, 0, (byte) 192}; +TypeReference> typeReference = new TypeReference>(){}; +List xs = objectMapper.readValue(bs, typeReference); +// xs => [zero, 1, 2.0, null] +``` + +## Advanced usage + +### Serialize multiple values without closing an output stream + +`tools.jackson.databind.ObjectMapper` closes an output stream by default after it writes a value. If you want to serialize multiple values in a row without closing an output stream, disable `StreamWriteFeature.AUTO_CLOSE_TARGET`. + +```java +OutputStream out = new FileOutputStream(tempFile); +ObjectMapper objectMapper = MessagePackMapper.builder() + .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) + .build(); + +objectMapper.writeValue(out, 1); +objectMapper.writeValue(out, "two"); +objectMapper.writeValue(out, 3.14); +out.close(); + +MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(new FileInputStream(tempFile)); +System.out.println(unpacker.unpackInt()); // => 1 +System.out.println(unpacker.unpackString()); // => two +System.out.println(unpacker.unpackFloat()); // => 3.14 +``` + +### Deserialize multiple values without closing an input stream + +`tools.jackson.databind.ObjectMapper` closes an input stream by default after it reads a value. If you want to deserialize multiple values in a row without closing an input stream, disable `StreamReadFeature.AUTO_CLOSE_SOURCE`. + +```java +MessagePacker packer = MessagePack.newDefaultPacker(new FileOutputStream(tempFile)); +packer.packInt(42); +packer.packString("Hello"); +packer.close(); + +FileInputStream in = new FileInputStream(tempFile); +ObjectMapper objectMapper = MessagePackMapper.builder() + .disable(StreamReadFeature.AUTO_CLOSE_SOURCE) + .build(); +System.out.println(objectMapper.readValue(in, Integer.class)); +System.out.println(objectMapper.readValue(in, String.class)); +in.close(); +``` + +### Serialize not using str8 type + +Old msgpack-java (e.g 0.6.7) doesn't support MessagePack str8 type. When your application needs to communicate with such an old MessagePack library, you can disable the data type like this: + +```java +MessagePack.PackerConfig config = new MessagePack.PackerConfig().withStr8FormatSupport(false); +ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory(config)); +// This string is serialized as bin8 type +byte[] resultWithoutStr8Format = objectMapper.writeValueAsBytes(str8LengthString); +``` + +### Serialize using non-String as a key of Map + +When you want to use non-String value as a key of Map, use `MessagePackKeySerializer` for key serialization. + +```java +@JsonSerialize(keyUsing = MessagePackKeySerializer.class) +private Map intMap = new HashMap<>(); + + : + +intMap.put(42, "Hello"); + +ObjectMapper objectMapper = new MessagePackMapper(); +byte[] bytes = objectMapper.writeValueAsBytes(intMap); + +Map deserialized = objectMapper.readValue(bytes, new TypeReference>() {}); +System.out.println(deserialized); // => {42=Hello} +``` + +### Serialize and deserialize BigDecimal as str type internally in MessagePack format + +`jackson-dataformat-msgpack` represents BigDecimal values as float type in MessagePack format by default for backward compatibility. But the default behavior could fail when handling too large value for `double` type. So we strongly recommend calling `MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString()` to internally handle BigDecimal values as String. + +```java +ObjectMapper objectMapper = MessagePackMapper.builder().handleBigIntegerAndBigDecimalAsString().build(); + +Pojo obj = new Pojo(); +// This value is too large to be serialized as double +obj.value = new BigDecimal("1234567890.98765432100"); + +byte[] converted = objectMapper.writeValueAsBytes(obj); + +System.out.println(objectMapper.readValue(converted, Pojo.class)); // => Pojo{value=1234567890.98765432100} +``` + +`MessagePackMapper.Builder#handleBigIntegerAndBigDecimalAsString()` is equivalent to the following configuration. + +```java +ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory()); +objectMapper.configOverride(BigInteger.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); +objectMapper.configOverride(BigDecimal.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); +``` + +### 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 = MessagePackMapper.builder() + .addModule(TimestampExtensionModule.INSTANCE) + .build(); +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 your own custom extension types easily. + +#### Deserialize extension type value directly + +```java +// In this application, extension type 59 is used for byte[] +byte[] bytes; +{ + // This ObjectMapper is just for temporary serialization + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + MessagePacker packer = MessagePack.newDefaultPacker(outputStream); + + packer.packExtensionTypeHeader((byte) 59, hexspeak.length); + packer.addPayload(hexspeak); + packer.close(); + + bytes = outputStream.toByteArray(); +} + +// Register the type and a deserializer to ExtensionTypeCustomDeserializers +ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); +extTypeCustomDesers.addCustomDeser((byte) 59, data -> { + if (Arrays.equals(data, + new byte[] {(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE})) { + return "Java"; + } + return "Not Java"; +}); + +ObjectMapper objectMapper = new ObjectMapper( + new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)); + +System.out.println(objectMapper.readValue(bytes, Object.class)); + // => Java +``` + +#### Use extension type as Map key + +```java +static class TripleBytesPojo +{ + public byte first; + public byte second; + public byte third; + + public TripleBytesPojo(byte first, byte second, byte third) + { + this.first = first; + this.second = second; + this.third = third; + } + + @Override + public boolean equals(Object o) + { + : + } + + @Override + public int hashCode() + { + : + } + + @Override + public String toString() + { + // This key format is used when serialized as map key + return String.format("%d-%d-%d", first, second, third); + } + + static class KeyDeserializer + extends tools.jackson.databind.KeyDeserializer + { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) + { + String[] values = key.split("-"); + return new TripleBytesPojo(Byte.parseByte(values[0]), Byte.parseByte(values[1]), Byte.parseByte(values[2])); + } + } + + static TripleBytesPojo deserialize(byte[] bytes) + { + return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]); + } +} + +: + +byte extTypeCode = 42; + +ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); +extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() +{ + @Override + public Object deserialize(byte[] value) + throws IOException + { + return TripleBytesPojo.deserialize(value); + } +}); + +SimpleModule module = new SimpleModule(); +module.addKeyDeserializer(TripleBytesPojo.class, new TripleBytesPojo.KeyDeserializer()); +ObjectMapper objectMapper = MessagePackMapper.builder( + new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) + .addModule(module) + .build(); + +Map deserializedMap = + objectMapper.readValue(serializedData, + new TypeReference>() {}); +``` + +#### Use extension type as Map value + +```java +static class TripleBytesPojo +{ + public byte first; + public byte second; + public byte third; + + public TripleBytesPojo(byte first, byte second, byte third) + { + this.first = first; + this.second = second; + this.third = third; + } + + static class Deserializer + extends StdDeserializer + { + protected Deserializer() + { + super(TripleBytesPojo.class); + } + + @Override + public TripleBytesPojo deserialize(JsonParser p, DeserializationContext ctxt) + { + return TripleBytesPojo.deserialize(p.getBinaryValue()); + } + } + + static TripleBytesPojo deserialize(byte[] bytes) + { + return new TripleBytesPojo(bytes[0], bytes[1], bytes[2]); + } +} + +: + +byte extTypeCode = 42; + +ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); +extTypeCustomDesers.addCustomDeser(extTypeCode, new ExtensionTypeCustomDeserializers.Deser() +{ + @Override + public Object deserialize(byte[] value) + throws IOException + { + return TripleBytesPojo.deserialize(value); + } +}); + +SimpleModule module = new SimpleModule(); +module.addDeserializer(TripleBytesPojo.class, new TripleBytesPojo.Deserializer()); +ObjectMapper objectMapper = MessagePackMapper.builder( + new MessagePackFactory().setExtTypeCustomDesers(extTypeCustomDesers)) + .addModule(module) + .build(); + +Map deserializedMap = + objectMapper.readValue(serializedData, + new TypeReference>() {}); +``` + +### Serialize a nested object that also serializes + +When you serialize an object that has a nested object also serializing with ObjectMapper and MessagePackFactory like the following code, it throws NullPointerException since the nested MessagePackFactory modifies a shared state stored in ThreadLocal. + +```java +@Test +public void testNestedSerialization() throws Exception +{ + ObjectMapper objectMapper = new MessagePackMapper(); + objectMapper.writeValueAsBytes(new OuterClass()); +} + +public class OuterClass +{ + public String getInner() throws JacksonException + { + ObjectMapper m = new MessagePackMapper(); + m.writeValueAsBytes(new InnerClass()); + return "EFG"; + } +} + +public class InnerClass +{ + public String getName() + { + return "ABC"; + } +} +``` + +There are a few options to fix this issue, but they introduce performance degradations while this usage is a corner case. A workaround that doesn't affect performance is to call `MessagePackFactory#setReuseResourceInGenerator(false)`. It might be inconvenient to call the API for users, but it's a reasonable tradeoff with performance for now. + +```java +ObjectMapper objectMapper = new ObjectMapper( + new MessagePackFactory().setReuseResourceInGenerator(false)); +``` diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/BenchmarkState.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/BenchmarkState.java similarity index 100% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/BenchmarkState.java rename to msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/BenchmarkState.java diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/MsgpackReadBenchmark.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/MsgpackReadBenchmark.java similarity index 100% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/MsgpackReadBenchmark.java rename to msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/MsgpackReadBenchmark.java diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/MsgpackWriteBenchmark.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/MsgpackWriteBenchmark.java similarity index 100% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/MsgpackWriteBenchmark.java rename to msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/MsgpackWriteBenchmark.java diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/NopOutputStream.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/NopOutputStream.java similarity index 100% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/NopOutputStream.java rename to msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/NopOutputStream.java diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/WriteUTF8StringBenchmark.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/WriteUTF8StringBenchmark.java similarity index 100% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/WriteUTF8StringBenchmark.java rename to msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/WriteUTF8StringBenchmark.java diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Image.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Image.java similarity index 100% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Image.java rename to msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Image.java diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaContent.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaContent.java similarity index 100% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaContent.java rename to msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaContent.java diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaItem.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaItem.java similarity index 100% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaItem.java rename to msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaItem.java diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaItems.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaItems.java similarity index 100% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaItems.java rename to msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/MediaItems.java diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Player.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Player.java similarity index 100% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Player.java rename to msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Player.java diff --git a/msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Size.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Size.java similarity index 100% rename from msgpack-jackson/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Size.java rename to msgpack-jackson3/src/jmh/java/org/msgpack/jackson3/dataformat/benchmark/model/Size.java diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/ExtensionTypeCustomDeserializers.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/ExtensionTypeCustomDeserializers.java similarity index 100% rename from msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/ExtensionTypeCustomDeserializers.java rename to msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/ExtensionTypeCustomDeserializers.java diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/JsonArrayFormat.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/JsonArrayFormat.java similarity index 100% rename from msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/JsonArrayFormat.java rename to msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/JsonArrayFormat.java diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackExtensionType.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackExtensionType.java similarity index 100% rename from msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackExtensionType.java rename to msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackExtensionType.java diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackFactory.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackFactory.java similarity index 100% rename from msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackFactory.java rename to msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackFactory.java diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackFactoryBuilder.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackFactoryBuilder.java similarity index 100% rename from msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackFactoryBuilder.java rename to msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackFactoryBuilder.java diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackGenerator.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackGenerator.java similarity index 100% rename from msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackGenerator.java rename to msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackGenerator.java diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackKeySerializer.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackKeySerializer.java similarity index 100% rename from msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackKeySerializer.java rename to msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackKeySerializer.java diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackMapper.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackMapper.java similarity index 100% rename from msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackMapper.java rename to msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackMapper.java diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackParser.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackParser.java similarity index 100% rename from msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackParser.java rename to msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackParser.java diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackReadContext.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackReadContext.java similarity index 100% rename from msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackReadContext.java rename to msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackReadContext.java diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackSerializedString.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackSerializedString.java similarity index 100% rename from msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackSerializedString.java rename to msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackSerializedString.java diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackSerializerFactory.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackSerializerFactory.java similarity index 100% rename from msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackSerializerFactory.java rename to msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackSerializerFactory.java diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackWriteContext.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackWriteContext.java similarity index 100% rename from msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/MessagePackWriteContext.java rename to msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/MessagePackWriteContext.java diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/PackageVersion.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/PackageVersion.java similarity index 87% rename from msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/PackageVersion.java rename to msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/PackageVersion.java index 423c03ab..a6940b4d 100644 --- a/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/PackageVersion.java +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/PackageVersion.java @@ -20,7 +20,7 @@ public class PackageVersion implements Versioned { - public static final Version VERSION = new Version(1, 0, 0, null, "org.msgpack", "jackson-dataformat-msgpack"); + public static final Version VERSION = new Version(0, 9, 12, null, "org.msgpack.jackson3", "jackson-dataformat-msgpack"); @Override public Version version() diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/TimestampExtensionModule.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/TimestampExtensionModule.java similarity index 100% rename from msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/TimestampExtensionModule.java rename to msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/TimestampExtensionModule.java diff --git a/msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/Tuple.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/Tuple.java similarity index 100% rename from msgpack-jackson/src/main/java/org/msgpack/jackson3/dataformat/Tuple.java rename to msgpack-jackson3/src/main/java/org/msgpack/jackson3/dataformat/Tuple.java diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/ExampleOfTypeInformationSerDe.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/ExampleOfTypeInformationSerDe.java similarity index 100% rename from msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/ExampleOfTypeInformationSerDe.java rename to msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/ExampleOfTypeInformationSerDe.java diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatForFieldIdTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatForFieldIdTest.java similarity index 100% rename from msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatForFieldIdTest.java rename to msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatForFieldIdTest.java diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatForPojoTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatForPojoTest.java similarity index 100% rename from msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatForPojoTest.java rename to msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatForPojoTest.java diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatTestBase.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatTestBase.java similarity index 100% rename from msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatTestBase.java rename to msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/MessagePackDataformatTestBase.java diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackFactoryTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/MessagePackFactoryTest.java similarity index 100% rename from msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackFactoryTest.java rename to msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/MessagePackFactoryTest.java diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackGeneratorTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/MessagePackGeneratorTest.java similarity index 99% rename from msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackGeneratorTest.java rename to msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/MessagePackGeneratorTest.java index 0323f705..7b50370d 100644 --- a/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackGeneratorTest.java +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/MessagePackGeneratorTest.java @@ -1190,7 +1190,7 @@ public void testCurrentValue() public void testVersion() { assertNotEquals(null, factory.version()); - assertEquals("org.msgpack", factory.version().getGroupId()); + assertEquals("org.msgpack.jackson3", factory.version().getGroupId()); assertEquals("jackson-dataformat-msgpack", factory.version().getArtifactId()); } diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackMapperTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/MessagePackMapperTest.java similarity index 100% rename from msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackMapperTest.java rename to msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/MessagePackMapperTest.java diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackParserTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/MessagePackParserTest.java similarity index 100% rename from msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackParserTest.java rename to msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/MessagePackParserTest.java diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackWriteContextTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/MessagePackWriteContextTest.java similarity index 100% rename from msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/MessagePackWriteContextTest.java rename to msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/MessagePackWriteContextTest.java diff --git a/msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/TimestampExtensionModuleTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/TimestampExtensionModuleTest.java similarity index 100% rename from msgpack-jackson/src/test/java/org/msgpack/jackson3/dataformat/TimestampExtensionModuleTest.java rename to msgpack-jackson3/src/test/java/org/msgpack/jackson3/dataformat/TimestampExtensionModuleTest.java diff --git a/project/plugins.sbt b/project/plugins.sbt index 91cf328e..1e9675de 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,14 +2,14 @@ addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1") // TODO: Fixes jacoco error: // java.lang.NoClassDefFoundError: Could not initialize class org.jacoco.core.internal.flow.ClassProbesAdapter //addSbtPlugin("com.github.sbt" % "sbt-jacoco" % "3.3.0") -addSbtPlugin("org.xerial.sbt" % "sbt-jcheckstyle" % "0.3.0") -addSbtPlugin("com.github.sbt" % "sbt-osgi" % "0.11.0-RC1") -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.2") -addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.1") -addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8") +addSbtPlugin("org.xerial.sbt" % "sbt-jcheckstyle" % "0.3.0") +addSbtPlugin("com.github.sbt" % "sbt-osgi" % "0.11.0-RC1") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.2") +addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.1") +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8") // Runs JUnit 5 (Jupiter) tests from sbt; without this, JUnit 5 tests are silently skipped. // Pinned to 0.15.x: 0.17+ upgrades to JUnit 6, which requires Java 17 and would break -// running msgpack-jackson2 tests on JDK 8. +// running msgpack-jackson (Jackson 2) tests on JDK 8. addSbtPlugin("com.github.sbt.junit" % "sbt-jupiter-interface" % "0.15.2") scalacOptions ++= Seq("-deprecation", "-feature")