From 9b818d39b144e6f68495dd7c07967381a0c31bdc Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Sun, 31 May 2026 21:25:10 +0900 Subject: [PATCH 1/6] 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/6] 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/6] 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/6] 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/6] 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/6] 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")