From 2ac3cbc0dd5cc7d2c424d2be5dad1197b26991fc Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Sat, 16 May 2026 23:59:25 +0900 Subject: [PATCH 01/17] Add msgpack-jackson3 module for Jackson 3.x support Introduces a new msgpack-jackson3 subproject that provides MessagePack serialization for Jackson 3.x (tools.jackson), which requires Java 17+. Tests use JUnit 4 via junit-interface to keep the build simple. --- build.sbt | 39 +- .../ExtensionTypeCustomDeserializers.java | 56 + .../msgpack/jackson/dataformat/JavaInfo.java | 41 + .../jackson/dataformat/JsonArrayFormat.java | 32 + .../dataformat/MessagePackExtensionType.java | 77 ++ .../dataformat/MessagePackFactory.java | 231 ++++ .../dataformat/MessagePackGenerator.java | 1020 +++++++++++++++ .../dataformat/MessagePackKeySerializer.java | 35 + .../jackson/dataformat/MessagePackMapper.java | 120 ++ .../jackson/dataformat/MessagePackParser.java | 657 ++++++++++ .../dataformat/MessagePackReadContext.java | 196 +++ .../MessagePackSerializedString.java | 118 ++ .../MessagePackSerializerFactory.java | 42 + .../dataformat/TimestampExtensionModule.java | 88 ++ .../org/msgpack/jackson/dataformat/Tuple.java | 38 + .../ExampleOfTypeInformationSerDe.java | 171 +++ .../MessagePackDataformatForFieldIdTest.java | 135 ++ .../MessagePackDataformatForPojoTest.java | 150 +++ .../MessagePackDataformatTestBase.java | 289 +++++ .../dataformat/MessagePackFactoryTest.java | 139 +++ .../dataformat/MessagePackGeneratorTest.java | 959 +++++++++++++++ .../dataformat/MessagePackMapperTest.java | 117 ++ .../dataformat/MessagePackParserTest.java | 1092 +++++++++++++++++ .../TimestampExtensionModuleTest.java | 217 ++++ .../dataformat/benchmark/Benchmarker.java | 98 ++ ...gePackDataformatHugeDataBenchmarkTest.java | 136 ++ ...essagePackDataformatPojoBenchmarkTest.java | 157 +++ 27 files changed, 6447 insertions(+), 3 deletions(-) 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/JavaInfo.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/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/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/TimestampExtensionModuleTest.java create mode 100644 msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/Benchmarker.java create mode 100644 msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java create mode 100644 msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java diff --git a/build.sbt b/build.sbt index 36d6f05ed..eb953835d 100644 --- a/build.sbt +++ b/build.sbt @@ -96,8 +96,9 @@ 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 lazy val root = Project(id = "msgpack-java", base = file(".")) @@ -108,7 +109,7 @@ lazy val root = Project(id = "msgpack-java", base = file(".")) publish := {}, publishLocal := {} ) - .aggregate(msgpackCore, msgpackJackson) + .aggregate(msgpackCore, msgpackJackson, msgpackJackson3) lazy val msgpackCore = Project(id = "msgpack-core", base = file("msgpack-core")) .enablePlugins(SbtOsgi) @@ -170,3 +171,35 @@ 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) + .settings( + buildSettings, + name := "jackson-dataformat-msgpack3", + 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"), + // Jackson 3.x requires Java 17+ + Compile / javaHome := { + val home = sys.env.getOrElse("JAVA17_HOME", + sys.env.getOrElse("JAVA_HOME", + sys.props.getOrElse("java.home", ""))) + val jdk17 = file(home) + if (home.nonEmpty && jdk17.exists()) Some(jdk17) + else throw new RuntimeException("Java 17 home not found. Set JAVA17_HOME or JAVA_HOME environment variable.") + }, + Test / javaHome := (Compile / javaHome).value, + doc / javaHome := (Compile / javaHome).value, + Test / fork := true, + javacOptions := Seq("-source", "17", "-target", "17", "-encoding", "UTF-8", "-Xlint:unchecked", "-Xlint:deprecation"), + doc / javacOptions := Seq("-source", "17", "-Xdoclint:none"), + libraryDependencies ++= + Seq( + "tools.jackson.core" % "jackson-databind" % "3.1.2", + junitInterface, + "org.apache.commons" % "commons-math3" % "3.6.1" % "test" + ), + testOptions += Tests.Argument(TestFrameworks.JUnit, "-v") + ) + .dependsOn(msgpackCore) 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 000000000..aa5879755 --- /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/JavaInfo.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/JavaInfo.java new file mode 100644 index 000000000..f5fda8c28 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/JavaInfo.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; + +import java.lang.reflect.Field; +import java.util.function.Supplier; + +public final class JavaInfo +{ + static final Supplier STRING_VALUE_FIELD_IS_CHARS; + static { + boolean stringValueFieldIsChars = false; + try { + Field stringValueField = String.class.getDeclaredField("value"); + stringValueFieldIsChars = stringValueField.getType() == char[].class; + } + catch (NoSuchFieldException ignored) { + } + if (stringValueFieldIsChars) { + STRING_VALUE_FIELD_IS_CHARS = () -> true; + } + else { + STRING_VALUE_FIELD_IS_CHARS = () -> false; + } + } + + private JavaInfo() {} +} 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 000000000..84be3f5ca --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java @@ -0,0 +1,32 @@ +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 000000000..2c7869f65 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackExtensionType.java @@ -0,0 +1,77 @@ +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; + +@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 = 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; + } + + 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 000000000..a7e1b0de4 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java @@ -0,0 +1,231 @@ +// +// 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 java.io.DataInput; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; + +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); + } + } + + 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), 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 { + if (offset != 0 || len != data.length) { + data = Arrays.copyOfRange(data, offset, offset + len); + } + MessagePackParser parser = new MessagePackParser(readCtxt, ioCtxt, + readCtxt.getStreamReadFeatures(_streamReadFeatures), 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 this; + } + + @Override + public TSFBuilder rebuild() + { + throw new UnsupportedOperationException("MessagePackFactory does not support TSFBuilder yet"); + } + + @Override + public Version version() + { + return Version.unknownVersion(); + } + + @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 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 new file mode 100644 index 000000000..d7d384d4f --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java @@ -0,0 +1,1020 @@ +// +// 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.base.GeneratorBase; +import tools.jackson.core.io.IOContext; +import tools.jackson.core.io.SerializedString; +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; + 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 + { + final int parentIndex; + + public Node(int parentIndex) + { + this.parentIndex = parentIndex; + } + + abstract void incrementChildCount(); + + abstract int currentStateAsParent(); + } + + private abstract static class NodeContainer extends Node + { + 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; + 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; + } + + 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; + } + + 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); + } + + @Override + public JsonGenerator writeStartArray(Object currentValue, int size) throws JacksonException + { + 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; + 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); + } + + @Override + public JsonGenerator writeStartObject(Object forValue, int size) throws JacksonException + { + 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; + return this; + } + + @Override + public JsonGenerator writeEndObject() throws JacksonException + { + if (currentState != IN_OBJECT) { + _reportError("Current context not an object but " + currentStateStr()); + } + endCurrentContainer(); + return this; + } + + 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( + objectWriteContext(), _ioContext, _streamWriteFeatures, + outputStream, packerConfig, supportIntegerKeys); + objectWriteContext().writeValue(messagePackGenerator, v); + messagePackGenerator.flush(); + output.write(outputStream.toByteArray()); + } + } + + private void packBigDecimal(BigDecimal decimal) + throws IOException + { + MessagePacker messagePacker = getMessagePacker(); + boolean failedToPackAsBI = false; + try { + BigInteger integer = decimal.toBigIntegerExact(); + messagePacker.packBigInteger(integer); + } + catch (ArithmeticException | IllegalArgumentException e) { + failedToPackAsBI = true; + } + + if (failedToPackAsBI) { + double doubleValue = decimal.doubleValue(); + 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)); + } + + @Override + public JsonGenerator writePropertyId(long id) throws JacksonException + { + if (this.supportIntegerKeys) { + 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 (STRING_VALUE_FIELD_IS_CHARS.get()) { + char[] chars = name.toCharArray(); + writeCharArrayTextKey(chars, 0, chars.length); + } + else { + addKeyNode(name); + } + return this; + } + + @Override + public JsonGenerator writeName(SerializableString name) throws JacksonException + { + if (name instanceof SerializedString) { + writeName(name.getValue()); + } + else if (name instanceof MessagePackSerializedString) { + addKeyNode(((MessagePackSerializedString) name).getRawValue()); + } + else { + throw new IllegalArgumentException("Unsupported key: " + name); + } + return this; + } + + @Override + public JsonGenerator writeString(String text) throws JacksonException + { + try { + if (STRING_VALUE_FIELD_IS_CHARS.get()) { + char[] chars = text.toCharArray(); + writeCharArrayTextValue(chars, 0, chars.length); + } + else { + 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 { + char[] buf = new char[len]; + int totalRead = 0; + while (totalRead < len) { + int read = reader.read(buf, totalRead, len - totalRead); + if (read < 0) { + break; + } + totalRead += read; + } + writeCharArrayTextValue(buf, 0, totalRead); + } + 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 { + if (STRING_VALUE_FIELD_IS_CHARS.get()) { + char[] chars = text.toCharArray(); + writeCharArrayTextValue(chars, 0, chars.length); + } + else { + addValueNode(text); + } + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeRaw(String text, int offset, int len) throws JacksonException + { + try { + char[] chars = text.toCharArray(); + writeCharArrayTextValue(chars, 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 + { + try { + try { + long l = Long.parseLong(encodedValue); + addValueNode(l); + return this; + } + catch (NumberFormatException ignored) { + } + + try { + double d = Double.parseDouble(encodedValue); + addValueNode(d); + return this; + } + catch (NumberFormatException ignored) { + } + + try { + BigInteger bi = new BigInteger(encodedValue); + addValueNode(bi); + return this; + } + catch (NumberFormatException ignored) { + } + + try { + BigDecimal bc = new BigDecimal(encodedValue); + addValueNode(bc); + 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 + { + try { + flush(); + } + finally { + if (StreamWriteFeature.AUTO_CLOSE_TARGET.enabledIn(_streamWriteFeatures)) { + try { + MessagePacker messagePacker = getMessagePacker(); + messagePacker.close(); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + } + } + } + + @Override + public void flush() throws JacksonException + { + if (!isElementsClosed) { + 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 + { + MessagePacker messagePacker = getMessagePacker(); + messagePacker.flush(); + } + + @Override + public tools.jackson.core.Version version() + { + return tools.jackson.core.Version.unknownVersion(); + } + + @Override + public tools.jackson.core.TokenStreamContext streamWriteContext() + { + return null; + } + + @Override + public Object streamWriteOutputTarget() + { + return output; + } + + @Override + public int streamWriteOutputBuffered() + { + return -1; + } + + @Override + public Object currentValue() + { + return null; + } + + @Override + public void assignCurrentValue(Object v) + { + } + + @Override + protected void _closeInput() throws IOException + { + messagePacker.close(); + } + + @Override + protected void _releaseBuffers() + { + } + + @Override + protected void _verifyValueWrite(String typeMsg) throws JacksonException + { + } + + 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 000000000..836a905dd --- /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 000000000..3da263e0c --- /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 Version.unknownVersion(); + } +} 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 000000000..c9824df9c --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java @@ -0,0 +1,657 @@ +// +// 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.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 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( + ObjectReadContext readCtxt, + IOContext ioCtxt, + int streamReadFeatures, + InputStream in, + boolean reuseResourceInParser) + throws IOException + { + this(readCtxt, ioCtxt, streamReadFeatures, new InputStreamBufferInput(in), in, reuseResourceInParser); + } + + public MessagePackParser( + ObjectReadContext readCtxt, + IOContext ioCtxt, + int streamReadFeatures, + byte[] bytes, + boolean reuseResourceInParser) + throws IOException + { + this(readCtxt, ioCtxt, streamReadFeatures, new ArrayBufferInput(bytes), bytes, reuseResourceInParser); + } + + private 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); + return; + } + + Tuple messageUnpackerTuple = messageUnpackerHolder.get(); + if (messageUnpackerTuple == null) { + messageUnpacker = MessagePack.newDefaultUnpacker(input); + } + else { + if (StreamReadFeature.AUTO_CLOSE_SOURCE.enabledIn(streamReadFeatures) || 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 Version version() + { + return Version.unknownVersion(); + } + + 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 JacksonException + { + try { + return _nextToken(); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + } + + private JsonToken _nextToken() throws IOException + { + 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); + 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: + messageUnpacker.unpackNil(); + nextToken = JsonToken.VALUE_NULL; + break; + case BOOLEAN: + boolean b = messageUnpacker.unpackBoolean(); + 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(); + 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()); + 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.PROPERTY_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 getString() + { + 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: + try { + return deserializedExtensionTypeValue().toString(); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + default: + throw new IllegalStateException("Invalid 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) + { + 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() + { + switch (type) { + case BYTES: + return bytesValue; + case EXT: + try { + return deserializedExtensionTypeValue(); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + 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 + protected void _closeInput() throws IOException + { + if (StreamReadFeature.AUTO_CLOSE_SOURCE.enabledIn(_streamReadFeatures)) { + messageUnpacker.close(); + } + } + + @Override + protected void _releaseBuffers() + { + } + + @Override + public boolean isClosed() + { + return isClosed; + } + + @Override + public void close() + { + try { + _closeInput(); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + finally { + isClosed = true; + } + } + + @Override + public TokenStreamContext streamReadContext() + { + return streamReadContext; + } + + @Override + public TokenStreamLocation currentTokenLocation() + { + return new TokenStreamLocation(ioContext.contentReference(), tokenPosition, -1, (int) tokenPosition); + } + + @Override + public TokenStreamLocation currentLocation() + { + return new TokenStreamLocation(ioContext.contentReference(), currentPosition, -1, (int) currentPosition); + } + + @Override + public String currentName() + { + 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 000000000..1da2574c3 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java @@ -0,0 +1,196 @@ +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) + { + if (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); + 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 000000000..5903cfcf9 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java @@ -0,0 +1,118 @@ +// +// 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.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.toString(); + } + + @Override + public int charLength() + { + return getValue().length(); + } + + @Override + public char[] asQuotedChars() + { + return getValue().toCharArray(); + } + + @Override + public byte[] asUnquotedUTF8() + { + return getValue().getBytes(UTF8); + } + + @Override + public byte[] asQuotedUTF8() + { + return ("\"" + getValue() + "\"").getBytes(UTF8); + } + + @Override + public int appendQuotedUTF8(byte[] bytes, int i) + { + return 0; + } + + @Override + public int appendQuoted(char[] chars, int i) + { + return 0; + } + + @Override + public int appendUnquotedUTF8(byte[] bytes, int i) + { + return 0; + } + + @Override + public int appendUnquoted(char[] chars, int i) + { + return 0; + } + + @Override + public int writeQuotedUTF8(OutputStream outputStream) + { + return 0; + } + + @Override + public int writeUnquotedUTF8(OutputStream outputStream) + { + return 0; + } + + @Override + public int putQuotedUTF8(ByteBuffer byteBuffer) + { + return 0; + } + + @Override + public int putUnquotedUTF8(ByteBuffer byteBuffer) + { + return 0; + } + + 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 000000000..67ee2e2f0 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java @@ -0,0 +1,42 @@ +// +// 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); + } + + @Override + public ValueSerializer createKeySerializer(SerializationContext ctxt, JavaType keyType) + { + return new MessagePackKeySerializer(); + } +} 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 000000000..5c7195417 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java @@ -0,0 +1,88 @@ +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 { + 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 new RuntimeException(e); + } + } + } + + private static class InstantDeserializer extends StdDeserializer + { + protected InstantDeserializer(Class vc) + { + super(vc); + } + + @Override + public Instant deserialize(JsonParser p, DeserializationContext ctxt) + { + try { + 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 (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(ext.getData())) { + return unpacker.unpackTimestamp(new ExtensionTypeHeader(EXT_TYPE, ext.getData().length)); + } + } + catch (IOException e) { + throw new RuntimeException(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 000000000..b0f720d86 --- /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 000000000..6a194a8c4 --- /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 000000000..86e9e8c39 --- /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 testMixedKeysBackwardsCompatiable() + 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 000000000..c12f0d5e8 --- /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, Suit.HEART); + 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 scheme = new String(bytes, Charset.forName("UTF-8")); + assertThat(scheme, 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 000000000..8a7f1e52d --- /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 000000000..cfaeb2d20 --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.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.JsonEncoding; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +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.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +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 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 000000000..0e8ee60b9 --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java @@ -0,0 +1,959 @@ +// +// 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.JsonEncoding; +import tools.jackson.core.JacksonException; +import tools.jackson.core.ObjectWriteContext; +import tools.jackson.core.StreamWriteFeature; +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.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.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 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)).unpackDouble(), + is(bd.doubleValue())); + } + + 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)).unpackDouble(), + is(bi.doubleValue())); + } + + @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; + } + } +} 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 000000000..776a11095 --- /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 000000000..8a501301f --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java @@ -0,0 +1,1092 @@ +// +// 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.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.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.assertTrue; +import static org.junit.Assert.assertThrows; + +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 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>() {}); + }); + } +} 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 000000000..36f5c97f6 --- /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/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/Benchmarker.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/Benchmarker.java new file mode 100644 index 000000000..980348024 --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/Benchmarker.java @@ -0,0 +1,98 @@ +// +// 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.apache.commons.math3.stat.StatUtils; +import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Benchmarker +{ + private final List benchmarkableList = new ArrayList(); + + public abstract static class Benchmarkable + { + private final String label; + + protected Benchmarkable(String label) + { + this.label = label; + } + + public abstract void run() throws Exception; + } + + public void addBenchmark(Benchmarkable benchmark) + { + benchmarkableList.add(benchmark); + } + + private static class Tuple + { + F first; + S second; + + public Tuple(F first, S second) + { + this.first = first; + this.second = second; + } + } + + public void run(int count, int warmupCount) + throws Exception + { + List> benchmarksResults = new ArrayList>(benchmarkableList.size()); + for (Benchmarkable benchmark : benchmarkableList) { + benchmarksResults.add(new Tuple(benchmark.label, new double[count])); + } + + for (int i = 0; i < count + warmupCount; i++) { + for (int bi = 0; bi < benchmarkableList.size(); bi++) { + Benchmarkable benchmark = benchmarkableList.get(bi); + long currentTimeNanos = System.nanoTime(); + benchmark.run(); + + if (i >= warmupCount) { + benchmarksResults.get(bi).second[i - warmupCount] = (System.nanoTime() - currentTimeNanos) / 1000000.0; + } + } + } + + for (Tuple benchmarkResult : benchmarksResults) { + printStat(benchmarkResult.first, benchmarkResult.second); + } + } + + private void printStat(String label, double[] origValues) + { + double[] values = origValues; + Arrays.sort(origValues); + if (origValues.length > 2) { + values = Arrays.copyOfRange(origValues, 1, origValues.length - 1); + } + StandardDeviation standardDeviation = new StandardDeviation(); + System.out.println(label + ":"); + System.out.println(String.format(" mean : %8.3f", StatUtils.mean(values))); + System.out.println(String.format(" min : %8.3f", StatUtils.min(values))); + System.out.println(String.format(" max : %8.3f", StatUtils.max(values))); + System.out.println(String.format(" stdev: %8.3f", standardDeviation.evaluate(values))); + System.out.println(""); + } +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java new file mode 100644 index 000000000..9112897c6 --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java @@ -0,0 +1,136 @@ +// +// 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 tools.jackson.core.JacksonException; +import tools.jackson.core.StreamWriteFeature; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import org.junit.Test; +import org.msgpack.jackson.dataformat.MessagePackFactory; +import org.msgpack.jackson.dataformat.MessagePackMapper; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +public class MessagePackDataformatHugeDataBenchmarkTest +{ + private static final int ELM_NUM = 1000000; + private static final int COUNT = 6; + private static final int WARMUP_COUNT = 4; + private final ObjectMapper origObjectMapper = JsonMapper.builder() + .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) + .build(); + private final ObjectMapper msgpackObjectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) + .build(); + private static final List value; + private static final byte[] packedByOriginal; + private static final byte[] packedByMsgPack; + + static { + value = new ArrayList(); + for (int i = 0; i < ELM_NUM; i++) { + value.add((long) i); + } + for (int i = 0; i < ELM_NUM; i++) { + value.add((double) i); + } + for (int i = 0; i < ELM_NUM; i++) { + value.add(String.valueOf(i)); + } + + byte[] bytes = null; + try { + bytes = JsonMapper.builder().build().writeValueAsBytes(value); + } + catch (JacksonException e) { + e.printStackTrace(); + } + packedByOriginal = bytes; + + try { + bytes = new MessagePackMapper(new MessagePackFactory()).writeValueAsBytes(value); + } + catch (JacksonException e) { + e.printStackTrace(); + } + packedByMsgPack = bytes; + } + + @Test + public void testBenchmark() + throws Exception + { + Benchmarker benchmarker = new Benchmarker(); + + File tempFileJackson = File.createTempFile("msgpack-jackson-", "-huge-jackson"); + tempFileJackson.deleteOnExit(); + final OutputStream outputStreamJackson = new FileOutputStream(tempFileJackson); + + File tempFileMsgpack = File.createTempFile("msgpack-jackson-", "-huge-msgpack"); + tempFileMsgpack.deleteOnExit(); + final OutputStream outputStreamMsgpack = new FileOutputStream(tempFileMsgpack); + + benchmarker.addBenchmark(new Benchmarker.Benchmarkable("serialize(huge) with JSON") { + @Override + public void run() + throws Exception + { + origObjectMapper.writeValue(outputStreamJackson, value); + } + }); + + benchmarker.addBenchmark(new Benchmarker.Benchmarkable("serialize(huge) with MessagePack") { + @Override + public void run() + throws Exception + { + msgpackObjectMapper.writeValue(outputStreamMsgpack, value); + } + }); + + benchmarker.addBenchmark(new Benchmarker.Benchmarkable("deserialize(huge) with JSON") { + @Override + public void run() + throws Exception + { + origObjectMapper.readValue(packedByOriginal, new TypeReference>() {}); + } + }); + + benchmarker.addBenchmark(new Benchmarker.Benchmarkable("deserialize(huge) with MessagePack") { + @Override + public void run() + throws Exception + { + msgpackObjectMapper.readValue(packedByMsgPack, new TypeReference>() {}); + } + }); + + try { + benchmarker.run(COUNT, WARMUP_COUNT); + } + finally { + outputStreamJackson.close(); + outputStreamMsgpack.close(); + } + } +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java new file mode 100644 index 000000000..3cb7b0889 --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java @@ -0,0 +1,157 @@ +// +// 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 tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import org.junit.Test; +import org.msgpack.jackson.dataformat.MessagePackFactory; +import org.msgpack.jackson.dataformat.MessagePackMapper; +import static org.msgpack.jackson.dataformat.MessagePackDataformatTestBase.NormalPojo; +import static org.msgpack.jackson.dataformat.MessagePackDataformatTestBase.Suit; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +public class MessagePackDataformatPojoBenchmarkTest +{ + private static final int LOOP_MAX = 200; + private static final int LOOP_FACTOR_SER = 40; + private static final int LOOP_FACTOR_DESER = 200; + private static final int COUNT = 6; + private static final int WARMUP_COUNT = 4; + private final List pojos = new ArrayList(LOOP_MAX); + private final List pojosSerWithOrig = new ArrayList(LOOP_MAX); + private final List pojosSerWithMsgPack = new ArrayList(LOOP_MAX); + private final ObjectMapper origObjectMapper = JsonMapper.builder().build(); + private final ObjectMapper msgpackObjectMapper = new MessagePackMapper(new MessagePackFactory()); + + public MessagePackDataformatPojoBenchmarkTest() + { + for (int i = 0; i < LOOP_MAX; i++) { + NormalPojo pojo = new NormalPojo(); + pojo.i = i; + pojo.l = i; + pojo.f = Float.valueOf(i); + pojo.d = Double.valueOf(i); + StringBuilder sb = new StringBuilder(); + for (int sbi = 0; sbi < i * 50; sbi++) { + sb.append("x"); + } + pojo.setS(sb.toString()); + pojo.bool = i % 2 == 0; + pojo.bi = BigInteger.valueOf(i); + switch (i % 4) { + case 0: + pojo.suit = Suit.SPADE; + break; + case 1: + pojo.suit = Suit.HEART; + break; + case 2: + pojo.suit = Suit.DIAMOND; + break; + case 3: + pojo.suit = Suit.CLUB; + break; + } + pojo.b = new byte[] {(byte) i}; + pojo.sMultibyte = "012345678Ⅸ"; + pojos.add(pojo); + } + + for (int i = 0; i < LOOP_MAX; i++) { + try { + pojosSerWithOrig.add(origObjectMapper.writeValueAsBytes(pojos.get(i))); + } + catch (JacksonException e) { + throw new RuntimeException("Failed to create test data"); + } + } + + for (int i = 0; i < LOOP_MAX; i++) { + try { + pojosSerWithMsgPack.add(msgpackObjectMapper.writeValueAsBytes(pojos.get(i))); + } + catch (JacksonException e) { + throw new RuntimeException("Failed to create test data"); + } + } + } + + @Test + public void testBenchmark() + throws Exception + { + Benchmarker benchmarker = new Benchmarker(); + + benchmarker.addBenchmark(new Benchmarker.Benchmarkable("serialize(pojo) with JSON") { + @Override + public void run() + throws Exception + { + for (int j = 0; j < LOOP_FACTOR_SER; j++) { + for (int i = 0; i < LOOP_MAX; i++) { + origObjectMapper.writeValueAsBytes(pojos.get(i)); + } + } + } + }); + + benchmarker.addBenchmark(new Benchmarker.Benchmarkable("serialize(pojo) with MessagePack") { + @Override + public void run() + throws Exception + { + for (int j = 0; j < LOOP_FACTOR_SER; j++) { + for (int i = 0; i < LOOP_MAX; i++) { + msgpackObjectMapper.writeValueAsBytes(pojos.get(i)); + } + } + } + }); + + benchmarker.addBenchmark(new Benchmarker.Benchmarkable("deserialize(pojo) with JSON") { + @Override + public void run() + throws Exception + { + for (int j = 0; j < LOOP_FACTOR_DESER; j++) { + for (int i = 0; i < LOOP_MAX; i++) { + origObjectMapper.readValue(pojosSerWithOrig.get(i), NormalPojo.class); + } + } + } + }); + + benchmarker.addBenchmark(new Benchmarker.Benchmarkable("deserialize(pojo) with MessagePack") { + @Override + public void run() + throws Exception + { + for (int j = 0; j < LOOP_FACTOR_DESER; j++) { + for (int i = 0; i < LOOP_MAX; i++) { + msgpackObjectMapper.readValue(pojosSerWithMsgPack.get(i), NormalPojo.class); + } + } + } + }); + + benchmarker.run(COUNT, WARMUP_COUNT); + } +} From d8c66c166a6eacefc874b5e3f4751a514e38c48e Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Sun, 17 May 2026 00:32:51 +0900 Subject: [PATCH 02/17] Remove javaHome hack and deduplicate javacOptions in msgpack-jackson3 Replace the fragile JAVA17_HOME/JAVA_HOME path resolution with a simple requirement to run sbt under Java 17+ (managed via mise). Drop redundant -encoding/-Xlint flags already provided by buildSettings. --- build.sbt | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/build.sbt b/build.sbt index eb953835d..7b9737ce9 100644 --- a/build.sbt +++ b/build.sbt @@ -180,19 +180,8 @@ lazy val msgpackJackson3 = Project(id = "msgpack-jackson3", base = file("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"), - // Jackson 3.x requires Java 17+ - Compile / javaHome := { - val home = sys.env.getOrElse("JAVA17_HOME", - sys.env.getOrElse("JAVA_HOME", - sys.props.getOrElse("java.home", ""))) - val jdk17 = file(home) - if (home.nonEmpty && jdk17.exists()) Some(jdk17) - else throw new RuntimeException("Java 17 home not found. Set JAVA17_HOME or JAVA_HOME environment variable.") - }, - Test / javaHome := (Compile / javaHome).value, - doc / javaHome := (Compile / javaHome).value, - Test / fork := true, - javacOptions := Seq("-source", "17", "-target", "17", "-encoding", "UTF-8", "-Xlint:unchecked", "-Xlint:deprecation"), + Test / fork := true, + javacOptions := Seq("-source", "17", "-target", "17"), doc / javacOptions := Seq("-source", "17", "-Xdoclint:none"), libraryDependencies ++= Seq( From 1f26be85be70cf86167d05a4d76efb26c98e0b19 Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Sun, 17 May 2026 01:03:08 +0900 Subject: [PATCH 03/17] Clean up dead code and add OSGi import exclusions for msgpack-jackson3 - Remove JavaInfo.java: STRING_VALUE_FIELD_IS_CHARS is always false on Java 17+ since String.value is byte[], not char[]; simplify parser and generator accordingly - Remove dead writeCharArrayTextKey method and tempChars field - Add OsgiKeys.importPackage exclusions for android.os and sun.* packages - Add comments on (int) cast overflow risk in currentTokenLocation/currentLocation --- build.sbt | 1 + .../msgpack/jackson/dataformat/JavaInfo.java | 41 ------------------- .../dataformat/MessagePackGenerator.java | 36 ++-------------- .../jackson/dataformat/MessagePackParser.java | 19 ++------- 4 files changed, 7 insertions(+), 90 deletions(-) delete mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/JavaInfo.java diff --git a/build.sbt b/build.sbt index 7b9737ce9..828e7976b 100644 --- a/build.sbt +++ b/build.sbt @@ -180,6 +180,7 @@ lazy val msgpackJackson3 = Project(id = "msgpack-jackson3", base = file("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.importPackage := Seq("!android.os", "!sun.*"), Test / fork := true, javacOptions := Seq("-source", "17", "-target", "17"), doc / javacOptions := Seq("-source", "17", "-Xdoclint:none"), diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/JavaInfo.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/JavaInfo.java deleted file mode 100644 index f5fda8c28..000000000 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/JavaInfo.java +++ /dev/null @@ -1,41 +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 java.lang.reflect.Field; -import java.util.function.Supplier; - -public final class JavaInfo -{ - static final Supplier STRING_VALUE_FIELD_IS_CHARS; - static { - boolean stringValueFieldIsChars = false; - try { - Field stringValueField = String.class.getDeclaredField("value"); - stringValueFieldIsChars = stringValueField.getType() == char[].class; - } - catch (NoSuchFieldException ignored) { - } - if (stringValueFieldIsChars) { - STRING_VALUE_FIELD_IS_CHARS = () -> true; - } - else { - STRING_VALUE_FIELD_IS_CHARS = () -> false; - } - } - - private JavaInfo() {} -} 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 index d7d384d4f..be4660b9b 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java @@ -44,8 +44,6 @@ 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 { @@ -513,16 +511,6 @@ private boolean areAllAsciiBytes(byte[] bytes, int offset, int len) 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); @@ -563,13 +551,7 @@ public JacksonFeatureSet streamWriteCapabilities() @Override public JsonGenerator writeName(String name) throws JacksonException { - if (STRING_VALUE_FIELD_IS_CHARS.get()) { - char[] chars = name.toCharArray(); - writeCharArrayTextKey(chars, 0, chars.length); - } - else { - addKeyNode(name); - } + addKeyNode(name); return this; } @@ -592,13 +574,7 @@ else if (name instanceof MessagePackSerializedString) { public JsonGenerator writeString(String text) throws JacksonException { try { - if (STRING_VALUE_FIELD_IS_CHARS.get()) { - char[] chars = text.toCharArray(); - writeCharArrayTextValue(chars, 0, chars.length); - } - else { - addValueNode(text); - } + addValueNode(text); } catch (IOException e) { throw _wrapIOFailure(e); @@ -673,13 +649,7 @@ public JsonGenerator writeUTF8String(byte[] text, int offset, int length) throws public JsonGenerator writeRaw(String text) throws JacksonException { try { - if (STRING_VALUE_FIELD_IS_CHARS.get()) { - char[] chars = text.toCharArray(); - writeCharArrayTextValue(chars, 0, chars.length); - } - else { - addValueNode(text); - } + addValueNode(text); } catch (IOException e) { throw _wrapIOFailure(e); 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 index c9824df9c..f3ddc9991 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java @@ -42,8 +42,6 @@ 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 { @@ -61,7 +59,6 @@ public class MessagePackParser private final IOContext ioContext; private ExtensionTypeCustomDeserializers extTypeCustomDesers; private final byte[] tempBytes = new byte[64]; - private final char[] tempChars = new char[64]; private enum Type { @@ -146,19 +143,7 @@ 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); - } + return new String(tempBytes, 0, strLen); } else { byte[] bytes = messageUnpacker.readPayload(strLen); @@ -604,12 +589,14 @@ public TokenStreamContext 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); } From bbc4de972fd4817d219b5a705e2c41d76df4cd24 Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Sun, 17 May 2026 12:19:31 +0900 Subject: [PATCH 04/17] Skip msgpack-jackson3 tests on Java < 17 in CI --- .github/workflows/CI.yml | 15 +++++++++++++-- build.sbt | 10 +++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 26c11bfb8..4f8c2ae4c 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 \ No newline at end of file diff --git a/build.sbt b/build.sbt index 828e7976b..1b588d294 100644 --- a/build.sbt +++ b/build.sbt @@ -101,6 +101,11 @@ val junitVintage = "org.junit.vintage" % "junit-vintage-engine" % "5.14.4" % " 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") + 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, @@ -109,7 +114,10 @@ lazy val root = Project(id = "msgpack-java", base = file(".")) publish := {}, publishLocal := {} ) - .aggregate(msgpackCore, msgpackJackson, msgpackJackson3) + .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) From 752ccdf1415b7a9d51d0827da58235e178be7262 Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Sun, 17 May 2026 13:54:40 +0900 Subject: [PATCH 05/17] Fix bugs in msgpack-jackson3 - MessagePackParser.unpackString: use UTF-8 for short strings (<=64 bytes) to match the long-string path; platform default charset was used before - MessagePackGenerator.writeNumber(String): try BigInteger before Double to avoid precision loss for large integer strings - MessagePackGenerator.writeString(Reader, int): handle len=-1 (unknown length) by reading until EOF instead of throwing NegativeArraySizeException --- .../dataformat/MessagePackGenerator.java | 37 +++++++++++++------ .../jackson/dataformat/MessagePackParser.java | 2 +- .../dataformat/MessagePackGeneratorTest.java | 8 ++-- 3 files changed, 30 insertions(+), 17 deletions(-) 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 index be4660b9b..93e4921cf 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java @@ -598,16 +598,29 @@ public JsonGenerator writeString(char[] text, int offset, int len) throws Jackso public JsonGenerator writeString(Reader reader, int len) throws JacksonException { try { - char[] buf = new char[len]; - int totalRead = 0; - while (totalRead < len) { - int read = reader.read(buf, totalRead, len - totalRead); - if (read < 0) { - break; + if (len < 0) { + StringBuilder sb = new StringBuilder(); + char[] tmpBuf = new char[1024]; + int read; + while ((read = reader.read(tmpBuf)) >= 0) { + sb.append(tmpBuf, 0, read); } - totalRead += read; + char[] chars = new char[sb.length()]; + sb.getChars(0, chars.length, chars, 0); + writeCharArrayTextValue(chars, 0, chars.length); + } + else { + char[] buf = new char[len]; + int totalRead = 0; + while (totalRead < len) { + int read = reader.read(buf, totalRead, len - totalRead); + if (read < 0) { + break; + } + totalRead += read; + } + writeCharArrayTextValue(buf, 0, totalRead); } - writeCharArrayTextValue(buf, 0, totalRead); } catch (IOException e) { throw _wrapIOFailure(e); @@ -797,16 +810,16 @@ public JsonGenerator writeNumber(String encodedValue) throws JacksonException } try { - double d = Double.parseDouble(encodedValue); - addValueNode(d); + BigInteger bi = new BigInteger(encodedValue); + addValueNode(bi); return this; } catch (NumberFormatException ignored) { } try { - BigInteger bi = new BigInteger(encodedValue); - addValueNode(bi); + double d = Double.parseDouble(encodedValue); + addValueNode(d); return this; } catch (NumberFormatException ignored) { 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 index f3ddc9991..655220b3b 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java @@ -143,7 +143,7 @@ private String unpackString(MessageUnpacker messageUnpacker) throws IOException int strLen = messageUnpacker.unpackRawStringHeader(); if (strLen <= tempBytes.length) { messageUnpacker.readPayload(tempBytes, 0, strLen); - return new String(tempBytes, 0, strLen); + return new String(tempBytes, 0, strLen, StandardCharsets.UTF_8); } else { byte[] bytes = messageUnpacker.readPayload(strLen); 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 index 0e8ee60b9..614a04079 100644 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java @@ -881,8 +881,8 @@ public void serializeStringAsBigDecimal() 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 @@ -906,8 +906,8 @@ public void serializeStringAsBigInteger() 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 From c33d896ee6dcf5e1a32923bc0a6db9df7bf59295 Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Sun, 17 May 2026 14:09:42 +0900 Subject: [PATCH 06/17] Fix MessagePackFactory.snapshot() to return a copy instead of this --- .../java/org/msgpack/jackson/dataformat/MessagePackFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index a7e1b0de4..d93ac5fd5 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java @@ -166,7 +166,7 @@ public TokenStreamFactory copy() @Override public TokenStreamFactory snapshot() { - return this; + return copy(); } @Override From ab62c4d538166f343501c21b20c71e29eed0998c Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Sun, 17 May 2026 14:50:42 +0900 Subject: [PATCH 07/17] Add TSFBuilder support via MessagePackFactoryBuilder Implement MessagePackFactoryBuilder extending DecorableTSFBuilder, wire it into MessagePackFactory via a builder constructor and rebuild(), and fix snapshot() to return a copy instead of this. Add tests covering rebuild() and snapshot() behavior. --- .../dataformat/MessagePackFactory.java | 24 +++- .../dataformat/MessagePackFactoryBuilder.java | 113 ++++++++++++++++++ .../dataformat/MessagePackFactoryTest.java | 59 +++++++++ 3 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactoryBuilder.java 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 index d93ac5fd5..5865aca5a 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java @@ -75,6 +75,16 @@ public MessagePackFactory(MessagePackFactory src) } } + 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; @@ -172,7 +182,7 @@ public TokenStreamFactory snapshot() @Override public TSFBuilder rebuild() { - throw new UnsupportedOperationException("MessagePackFactory does not support TSFBuilder yet"); + return new MessagePackFactoryBuilder(this); } @Override @@ -187,12 +197,24 @@ MessagePack.PackerConfig getPackerConfig() return packerConfig; } + @VisibleForTesting + boolean isReuseResourceInGenerator() + { + return reuseResourceInGenerator; + } + @VisibleForTesting boolean isReuseResourceInParser() { return reuseResourceInParser; } + @VisibleForTesting + boolean isSupportIntegerKeys() + { + return supportIntegerKeys; + } + @VisibleForTesting ExtensionTypeCustomDeserializers getExtTypeCustomDesers() { 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 000000000..521ab4d98 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactoryBuilder.java @@ -0,0 +1,113 @@ +// +// 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(); + this.extTypeCustomDesers = base.getExtTypeCustomDesers(); + } + + 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/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java index cfaeb2d20..25ef0fe2f 100644 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java @@ -18,6 +18,7 @@ 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; @@ -30,8 +31,10 @@ 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; @@ -86,6 +89,62 @@ public void testCopyWithDefaultConfig() 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 From ffc69dbf3d11d2bfe917011419956b31651d538a Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Sun, 17 May 2026 16:00:36 +0900 Subject: [PATCH 08/17] Fix offset bugs in MessagePackGenerator and document pre-existing issues Three bugs where non-zero offset/position was ignored: - getBytesIfAscii: bytes[i] -> bytes[i - offset] - writeByteArrayTextValue: store slice instead of whole array - ByteBuffer: use arrayOffset() + position() as payload start All three bugs exist identically in msgpack-jackson; documented in plans/preexisting-issues.md with practical impact notes. --- .../dataformat/MessagePackGenerator.java | 8 +- .../dataformat/MessagePackGeneratorTest.java | 93 +++++++++++ plans/preexisting-issues.md | 150 ++++++++++++++++++ 3 files changed, 248 insertions(+), 3 deletions(-) create mode 100644 plans/preexisting-issues.md 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 index 93e4921cf..9359286f6 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java @@ -390,7 +390,7 @@ else if (v instanceof ByteBuffer) { int len = bb.remaining(); if (bb.hasArray()) { 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]; @@ -496,7 +496,7 @@ private byte[] getBytesIfAscii(char[] chars, int offset, int len) if (c >= 0x80) { return null; } - bytes[i] = (byte) c; + bytes[i - offset] = (byte) c; } return bytes; } @@ -524,7 +524,9 @@ private void writeCharArrayTextValue(char[] text, int offset, int len) throws IO private void writeByteArrayTextValue(byte[] text, int offset, int len) throws IOException { if (areAllAsciiBytes(text, offset, len)) { - addValueNode(new AsciiCharString(text)); + byte[] slice = new byte[len]; + System.arraycopy(text, offset, slice, 0, len); + addValueNode(new AsciiCharString(slice)); return; } addValueNode(new String(text, offset, len, DEFAULT_CHARSET)); 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 index 614a04079..7cdea875f 100644 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java @@ -956,4 +956,97 @@ public String getName() return name; } } + + @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(baos, JsonEncoding.UTF8); + 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(baos, JsonEncoding.UTF8); + 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(baos, JsonEncoding.UTF8); + 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(baos, JsonEncoding.UTF8); + 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(baos, JsonEncoding.UTF8); + 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); + } } diff --git a/plans/preexisting-issues.md b/plans/preexisting-issues.md new file mode 100644 index 000000000..f72a1eaee --- /dev/null +++ b/plans/preexisting-issues.md @@ -0,0 +1,150 @@ +# Issues to address + +## msgpack-jackson3-specific + +### 1. `isClosed()` always returns false + +**File:** `msgpack-jackson3/.../MessagePackGenerator.java` (close method) + +`close()` cannot call `super.close()` because `GeneratorBase.close()` in Jackson 3 +closes the underlying output stream as a side effect, breaking tests that disable +`AUTO_CLOSE_TARGET`. As a result `isClosed()` always returns false and callers can +continue writing into a closed generator without getting the standard exception. + +### 2. `MessagePackFactory.snapshot()` returns `this` — FIXED + +**File:** `msgpack-jackson3/.../MessagePackFactory.java` + +Fixed: `snapshot()` now delegates to `copy()`, and `rebuild()` is implemented via `MessagePackFactoryBuilder`. + +### 3. Build: `msgpack-jackson3` fails to compile locally on Java < 17 + +`msgpack-jackson3` is in the root aggregate unconditionally. The CI works around +this with a bash version check, but a developer running `./sbt test` locally on +Java 8 or 11 gets a hard compilation failure. A cleaner build-level solution +(conditional aggregate, toolchain support, or a separate profile) is needed. + +### 4. `MessagePackGenerator.streamWriteContext()` returns null + +**File:** `msgpack-jackson3/.../MessagePackGenerator.java` (streamWriteContext method) + +`streamWriteContext()` returns `null`, bypassing Jackson's standard write context +management. This can cause NPEs in Jackson code paths that use the context for path +tracking in error messages or certain serialization features. Fixing it properly +requires integrating with Jackson 3's `TokenStreamContext` / `_streamWriteContext` +managed by `GeneratorBase`, which needs investigation. + +### 5. `writeString(Reader, int)` len=-1 implementation allocates an extra copy + +**File:** `msgpack-jackson3/.../MessagePackGenerator.java` (writeString(Reader, int)) + +The len=-1 path buffers into a `StringBuilder` then copies to a `char[]`. Using a +`CharArrayWriter` would avoid the intermediate allocation. + +--- + +## msgpack-jackson pre-existing issues + +These issues were identified during review of msgpack-jackson3 and confirmed to exist +identically in msgpack-jackson. They should be addressed in both modules together. + +## 1. `MessagePackParser`: Same byte-array input skips unpacker reset + +**File:** `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java:130` + +When `AUTO_CLOSE_SOURCE` is disabled and the same byte-array instance is parsed more +than once (e.g. reused buffer), the condition `messageUnpackerTuple.first() != src` +is false, so the unpacker is not reset. The second parse continues from where the first +left off instead of from the beginning. + +## 2. `MessagePackParser`: ThreadLocal retains last byte-array payload per thread + +**File:** `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java:135` + +`messageUnpackerHolder` is never cleared on parser close. For byte-array inputs this +retains the entire last parsed payload for each thread in a pool indefinitely, which +can cause unbounded memory retention after large messages. + +## 3. `MessagePackGenerator`: `close()` does not call `super.close()` + +**File:** `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java` (close method) + +The `close()` override never delegates to `GeneratorBase.close()`, so `isClosed()` +remains false after close. Callers can continue writing into a closed generator +instead of getting the standard closed-generator exception. + +## 4. `MessagePackGenerator`: `writeString(Reader, int)` crashes on length -1 + +**File:** `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java` (writeString(Reader, int)) + +`new char[len]` throws `NegativeArraySizeException` when `len` is -1, which is a +valid Jackson API usage meaning "unknown length, read until EOF". + +## 5. `MessagePackGenerator`: `writeNumber(String)` tries `Double.parseDouble` before `BigInteger` + +**File:** `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java:699` + +Integer strings outside the `long` range are serialized as floating-point values, +losing precision, even though MessagePack can encode big integers exactly. The order +should try integer parsing first. + +## 6. `MessagePackGenerator`: Closing a container leaves stale `currentState` + +**File:** `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java` (writeEndArray/writeEndObject) + +After `flush()` clears `nodes`, any subsequent root-level value written with the +same generator is treated as if inside the old container, which can corrupt output. + +## 7. `MessagePackSerializedString`: Most interface methods are stubs + +**File:** `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java:70` + +Most `SerializableString` methods return 0 or do nothing. Any Jackson code path +that calls these methods (e.g. for length or byte-copy operations) will silently +produce incorrect results. + +## 8. `MessagePackGenerator`: `getBytesIfAscii` writes to wrong index when offset > 0 + +**Files:** +- `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java:474` +- `msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java` — FIXED + +`bytes[i] = (byte) c` should be `bytes[i - offset] = (byte) c`. When offset > 0, `i` +starts above 0 but `bytes` is length `len`, so it throws `ArrayIndexOutOfBoundsException`. +Fixed in msgpack-jackson3; needs the same fix in msgpack-jackson. + +**Practical impact:** Low. `writeString(char[], offset, len)` is a low-level Jackson +streaming API used by Jackson's own internals and performance-sensitive custom serializers, +not by typical application code. Normal users call `writeString(String)` instead. + +## 9. `MessagePackGenerator`: `writeByteArrayTextValue` ASCII path ignores offset + +**Files:** +- `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java:512` +- `msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java` — FIXED + +`addValueNode(new AsciiCharString(text))` stores the entire backing array instead of +the requested slice `[offset, offset+len)`. Callers such as `writeRawUTF8String` and +`writeUTF8String` with non-zero offsets will serialize garbage bytes. +Fixed in msgpack-jackson3 using `System.arraycopy`; needs the same fix in msgpack-jackson. + +**Practical impact:** Low. `writeUTF8String(byte[], offset, len)` is a low-level API +called by Jackson's streaming infrastructure or custom serializers working with raw +byte buffers. Typical application code goes through ObjectMapper, which always passes +offset=0 for plain byte arrays. + +## 10. `MessagePackGenerator`: ByteBuffer serialization ignores `position()` + +**Files:** +- `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java:369` +- `msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java` — FIXED + +`writePayload(bb.array(), bb.arrayOffset(), len)` ignores `bb.position()`. For any +ByteBuffer with a non-zero position (e.g. from `ByteBuffer.wrap(data, offset, len)` or +after reads), this serializes bytes starting at the wrong offset. +Fixed in msgpack-jackson3 using `bb.arrayOffset() + bb.position()`; needs the same fix in msgpack-jackson. + +**Practical impact:** Moderate. This is the most realistic end-user scenario: a POJO +with a `ByteBuffer` field that was sliced or partially consumed will silently produce +corrupt serialized output. No exception is thrown. + From 08876164a0629acc047045a4e922aa958532dcb9 Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Sun, 17 May 2026 18:41:11 +0900 Subject: [PATCH 09/17] Fix isClosed() always false and stale currentState after container close - close(): set _closed=true directly instead of calling super.close(), which would unconditionally close the underlying stream regardless of AUTO_CLOSE_TARGET - endCurrentContainer(): reset currentState to IN_ROOT when closing the root container, preventing stale state if the generator is reused Both bugs exist in msgpack-jackson; documented in plans/preexisting-issues.md --- .../dataformat/MessagePackGenerator.java | 7 +++- .../dataformat/MessagePackGeneratorTest.java | 39 +++++++++++++++++++ plans/preexisting-issues.md | 32 ++++++++------- 3 files changed, 63 insertions(+), 15 deletions(-) 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 index 9359286f6..1b29b72d3 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java @@ -339,6 +339,7 @@ private void endCurrentContainer() if (currentParentElementIndex == 0) { isElementsClosed = true; currentParentElementIndex = parent.parentIndex; + currentState = IN_ROOT; return; } @@ -881,8 +882,6 @@ public void close() throws JacksonException { try { flush(); - } - finally { if (StreamWriteFeature.AUTO_CLOSE_TARGET.enabledIn(_streamWriteFeatures)) { try { MessagePacker messagePacker = getMessagePacker(); @@ -893,6 +892,10 @@ public void close() throws JacksonException } } } + finally { + _closed = true; + _releaseBuffers(); + } } @Override 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 index 7cdea875f..3ab44e560 100644 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java @@ -957,6 +957,45 @@ public String getName() } } + @Test + public void testIsClosedAfterClose() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(baos, JsonEncoding.UTF8); + 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(baos, JsonEncoding.UTF8); + 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 testWriteStringCharArrayWithOffset() throws IOException diff --git a/plans/preexisting-issues.md b/plans/preexisting-issues.md index f72a1eaee..f50252530 100644 --- a/plans/preexisting-issues.md +++ b/plans/preexisting-issues.md @@ -2,14 +2,13 @@ ## msgpack-jackson3-specific -### 1. `isClosed()` always returns false +### 1. `isClosed()` always returns false — FIXED **File:** `msgpack-jackson3/.../MessagePackGenerator.java` (close method) -`close()` cannot call `super.close()` because `GeneratorBase.close()` in Jackson 3 -closes the underlying output stream as a side effect, breaking tests that disable -`AUTO_CLOSE_TARGET`. As a result `isClosed()` always returns false and callers can -continue writing into a closed generator without getting the standard exception. +Fixed by setting `_closed = true` directly in the `finally` block of `close()`, without +calling `super.close()` (which would unconditionally close the underlying stream via +`_closeInput()`, ignoring the `AUTO_CLOSE_TARGET` flag). ### 2. `MessagePackFactory.snapshot()` returns `this` — FIXED @@ -65,13 +64,16 @@ left off instead of from the beginning. retains the entire last parsed payload for each thread in a pool indefinitely, which can cause unbounded memory retention after large messages. -## 3. `MessagePackGenerator`: `close()` does not call `super.close()` +## 3. `MessagePackGenerator`: `close()` does not set `isClosed()` to true -**File:** `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java` (close method) +**Files:** +- `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java` (close method) +- `msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java` — FIXED -The `close()` override never delegates to `GeneratorBase.close()`, so `isClosed()` -remains false after close. Callers can continue writing into a closed generator -instead of getting the standard closed-generator exception. +The `close()` override never sets the closed flag, so `isClosed()` remains false. +Fixed in msgpack-jackson3 by setting `_closed = true` directly (calling `super.close()` +is not viable since it unconditionally closes the underlying stream, ignoring +`AUTO_CLOSE_TARGET`). Needs the same fix in msgpack-jackson. ## 4. `MessagePackGenerator`: `writeString(Reader, int)` crashes on length -1 @@ -90,10 +92,14 @@ should try integer parsing first. ## 6. `MessagePackGenerator`: Closing a container leaves stale `currentState` -**File:** `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java` (writeEndArray/writeEndObject) +**Files:** +- `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java` (writeEndArray/writeEndObject) +- `msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java` — FIXED -After `flush()` clears `nodes`, any subsequent root-level value written with the -same generator is treated as if inside the old container, which can corrupt output. +After closing the root container, `currentState` was not reset to `IN_ROOT`. After +`flush()` clears `nodes`, any subsequent root-level value was treated as if inside the +old container. Fixed in msgpack-jackson3 by adding `currentState = IN_ROOT` in +`endCurrentContainer()`; needs the same fix in msgpack-jackson. ## 7. `MessagePackSerializedString`: Most interface methods are stubs From 07da78b16e0957119142a6e9a6a814f2fe164e1e Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Sun, 17 May 2026 18:44:22 +0900 Subject: [PATCH 10/17] Fix writeRaw(String, offset, len) to copy only the requested slice Use getChars(offset, offset+len) instead of toCharArray() which copies the entire string unnecessarily. --- .../jackson/dataformat/MessagePackGenerator.java | 5 +++-- .../dataformat/MessagePackGeneratorTest.java | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) 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 index 1b29b72d3..dbf75e0f4 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java @@ -677,8 +677,9 @@ public JsonGenerator writeRaw(String text) throws JacksonException public JsonGenerator writeRaw(String text, int offset, int len) throws JacksonException { try { - char[] chars = text.toCharArray(); - writeCharArrayTextValue(chars, offset, len); + char[] chars = new char[len]; + text.getChars(offset, offset + len, chars, 0); + writeCharArrayTextValue(chars, 0, len); } catch (IOException e) { throw _wrapIOFailure(e); 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 index 3ab44e560..c56bf8bfa 100644 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java @@ -996,6 +996,22 @@ public void testGeneratorReusableAfterRootContainerClose() assertEquals(2, unpacker.unpackInt()); } + @Test + public void testWriteRawStringWithOffset() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(baos, JsonEncoding.UTF8); + 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 From ec29bb095b01302fa2c76c41b63b545ecb6bc1e6 Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Sun, 17 May 2026 18:47:40 +0900 Subject: [PATCH 11/17] Use non-deprecated createGenerator(ObjectWriteContext, OutputStream) in tests --- .../dataformat/MessagePackGeneratorTest.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 index c56bf8bfa..b826e9ed0 100644 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java @@ -962,7 +962,7 @@ public void testIsClosedAfterClose() throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(baos, JsonEncoding.UTF8); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); assertFalse(generator.isClosed()); generator.writeStartArray(); generator.writeEndArray(); @@ -975,7 +975,7 @@ public void testGeneratorReusableAfterRootContainerClose() throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(baos, JsonEncoding.UTF8); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); generator.writeStartArray(); generator.writeNumber(1); generator.writeEndArray(); @@ -1001,7 +1001,7 @@ public void testWriteRawStringWithOffset() throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(baos, JsonEncoding.UTF8); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); generator.writeStartArray(); generator.writeRaw("XXhelloXX", 2, 5); // "hello" generator.writeEndArray(); @@ -1019,7 +1019,7 @@ public void testWriteStringCharArrayWithOffset() // 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(baos, JsonEncoding.UTF8); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); generator.writeStartArray(); generator.writeString(buf, 2, 5); // "hello" generator.writeEndArray(); @@ -1037,7 +1037,7 @@ public void testWriteStringCharArrayWithOffsetNonAscii() // Non-ASCII to exercise the non-fast-path in getBytesIfAscii char[] buf = new char[] {'X', '三', '四', '五', 'X'}; // 三四五 ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(baos, JsonEncoding.UTF8); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); generator.writeStartArray(); generator.writeString(buf, 1, 3); generator.writeEndArray(); @@ -1055,7 +1055,7 @@ public void testWriteUTF8StringWithOffset() // 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(baos, JsonEncoding.UTF8); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); generator.writeStartArray(); generator.writeUTF8String(buf, 2, 2); // "hi" generator.writeEndArray(); @@ -1072,7 +1072,7 @@ public void testWriteBinaryWithOffset() { byte[] data = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04}; ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(baos, JsonEncoding.UTF8); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); generator.writeStartArray(); generator.writeBinary(data, 1, 3); // bytes 0x01, 0x02, 0x03 generator.writeEndArray(); @@ -1092,7 +1092,7 @@ public void testWriteBinaryByteBufferWithOffset() ByteBuffer bb = ByteBuffer.wrap(data, 1, 3); // position=1, limit=4, remaining=3 ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = factory.createGenerator(baos, JsonEncoding.UTF8); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); generator.writeStartArray(); ObjectMapper mapper = new MessagePackMapper(factory); mapper.writeValue(generator, bb); From fa8f901a67d7196f9bd3468607e77e65f2cd90e4 Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Sun, 17 May 2026 19:29:43 +0900 Subject: [PATCH 12/17] Use BigDecimal.compareTo instead of string comparison in packBigDecimal Replace the string-based lossy check with `decimal.compareTo(BigDecimal.valueOf(doubleValue)) != 0`, avoiding two stripTrailingZeros + toEngineeringString allocations per BigDecimal write. --- .../dataformat/MessagePackGenerator.java | 3 +-- .../dataformat/MessagePackGeneratorTest.java | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) 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 index dbf75e0f4..1fbdef427 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java @@ -433,8 +433,7 @@ private void packBigDecimal(BigDecimal decimal) if (failedToPackAsBI) { double doubleValue = decimal.doubleValue(); - if (!decimal.stripTrailingZeros().toEngineeringString().equals( - BigDecimal.valueOf(doubleValue).stripTrailingZeros().toEngineeringString())) { + if (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); 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 index b826e9ed0..2be7e3401 100644 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java @@ -351,6 +351,29 @@ 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 From 43f92f105ee931959eb31440b58b5f6d45f965e6 Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Sun, 17 May 2026 23:06:41 +0900 Subject: [PATCH 13/17] Fix streamWriteContext, parser ThreadLocal byte-array leak, SerializedString stubs, and Version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MessagePackGenerator: implement streamWriteContext() using SimpleStreamWriteContext; wire writeStartArray/Object, endCurrentContainer, writeName, _verifyValueWrite, currentValue, and assignCurrentValue to track write context correctly - MessagePackParser: on close, null out the byte-array source in the ThreadLocal to release large payload references; InputStream sources are preserved to keep the sequential-read-from-same-stream behavior working - MessagePackSerializedString: implement all stub append/write/put methods - Add PackageVersion (0.9.12) and use it in generator, parser, and factory - Note: messageBufferOutputHolder ThreadLocal not fixed — clearing it causes 23% serialization regression by defeating the OutputStreamBufferOutput reuse cache --- .../dataformat/MessagePackFactory.java | 2 +- .../dataformat/MessagePackGenerator.java | 19 ++++- .../jackson/dataformat/MessagePackParser.java | 6 +- .../MessagePackSerializedString.java | 43 ++++++++-- .../jackson/dataformat/PackageVersion.java | 30 +++++++ .../dataformat/MessagePackGeneratorTest.java | 80 +++++++++++++++++++ .../dataformat/MessagePackParserTest.java | 19 +++++ plans/preexisting-issues.md | 43 +++++++--- 8 files changed, 218 insertions(+), 24 deletions(-) create mode 100644 msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/PackageVersion.java 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 index 5865aca5a..3e61e7dfa 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java @@ -188,7 +188,7 @@ public TokenStreamFactory snapshot() @Override public Version version() { - return Version.unknownVersion(); + return PackageVersion.VERSION; } @VisibleForTesting 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 index 1fbdef427..e234be4ad 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java @@ -18,11 +18,13 @@ import tools.jackson.core.Base64Variant; import tools.jackson.core.JacksonException; import tools.jackson.core.util.JacksonFeatureSet; +import tools.jackson.core.util.SimpleStreamWriteContext; 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.TokenStreamContext; import tools.jackson.core.base.GeneratorBase; import tools.jackson.core.io.IOContext; import tools.jackson.core.io.SerializedString; @@ -61,6 +63,7 @@ public class MessagePackGenerator private int currentState = IN_ROOT; private final List nodes; private boolean isElementsClosed = false; + private SimpleStreamWriteContext writeContext; private static final class AsciiCharString { @@ -201,6 +204,7 @@ private MessagePackGenerator( this.packerConfig = packerConfig; this.nodes = new ArrayList<>(); this.supportIntegerKeys = supportIntegerKeys; + this.writeContext = SimpleStreamWriteContext.createRootContext(null); } public MessagePackGenerator( @@ -219,6 +223,7 @@ public MessagePackGenerator( this.packerConfig = packerConfig; this.nodes = new ArrayList<>(); this.supportIntegerKeys = supportIntegerKeys; + this.writeContext = SimpleStreamWriteContext.createRootContext(null); } private MessageBufferOutput getMessageBufferOutputForOutputStream( @@ -270,6 +275,7 @@ public JsonGenerator writeStartArray(Object currentValue) throws JacksonExceptio @Override public JsonGenerator writeStartArray(Object currentValue, int size) throws JacksonException { + writeContext = writeContext.createChildArrayContext(currentValue); if (currentState == IN_OBJECT) { Node node = nodes.get(nodes.size() - 1); assert node instanceof NodeEntryInObject; @@ -309,6 +315,7 @@ public JsonGenerator writeStartObject(Object currentValue) throws JacksonExcepti @Override public JsonGenerator writeStartObject(Object forValue, int size) throws JacksonException { + writeContext = writeContext.createChildObjectContext(forValue); if (currentState == IN_OBJECT) { Node node = nodes.get(nodes.size() - 1); assert node instanceof NodeEntryInObject; @@ -335,6 +342,7 @@ public JsonGenerator writeEndObject() throws JacksonException private void endCurrentContainer() { + writeContext = writeContext.clearAndGetParent(); Node parent = nodes.get(currentParentElementIndex); if (currentParentElementIndex == 0) { isElementsClosed = true; @@ -553,6 +561,7 @@ public JacksonFeatureSet streamWriteCapabilities() @Override public JsonGenerator writeName(String name) throws JacksonException { + writeContext.writeName(name); addKeyNode(name); return this; } @@ -953,13 +962,13 @@ private void flushMessagePacker() @Override public tools.jackson.core.Version version() { - return tools.jackson.core.Version.unknownVersion(); + return PackageVersion.VERSION; } @Override - public tools.jackson.core.TokenStreamContext streamWriteContext() + public TokenStreamContext streamWriteContext() { - return null; + return writeContext; } @Override @@ -977,12 +986,13 @@ public int streamWriteOutputBuffered() @Override public Object currentValue() { - return null; + return writeContext.currentValue(); } @Override public void assignCurrentValue(Object v) { + writeContext.assignCurrentValue(v); } @Override @@ -999,6 +1009,7 @@ protected void _releaseBuffers() @Override protected void _verifyValueWrite(String typeMsg) throws JacksonException { + writeContext.writeValue(); } private MessagePacker getMessagePacker() 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 index 655220b3b..509e7b63f 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java @@ -135,7 +135,7 @@ public void setExtensionTypeCustomDeserializers(ExtensionTypeCustomDeserializers @Override public Version version() { - return Version.unknownVersion(); + return PackageVersion.VERSION; } private String unpackString(MessageUnpacker messageUnpacker) throws IOException @@ -577,6 +577,10 @@ public void close() } finally { isClosed = true; + Tuple tuple = messageUnpackerHolder.get(); + if (tuple != null && tuple.first() instanceof byte[]) { + messageUnpackerHolder.set(new Tuple<>(null, tuple.second())); + } } } 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 index 5903cfcf9..ed01790fa 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java @@ -17,6 +17,7 @@ import tools.jackson.core.SerializableString; +import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -66,49 +67,75 @@ public byte[] asQuotedUTF8() @Override public int appendQuotedUTF8(byte[] bytes, int i) { - return 0; + byte[] utf8 = asQuotedUTF8(); + System.arraycopy(utf8, 0, bytes, i, utf8.length); + return utf8.length; } @Override public int appendQuoted(char[] chars, int i) { - return 0; + char[] q = asQuotedChars(); + System.arraycopy(q, 0, chars, i, q.length); + return q.length; } @Override public int appendUnquotedUTF8(byte[] bytes, int i) { - return 0; + byte[] utf8 = asUnquotedUTF8(); + System.arraycopy(utf8, 0, bytes, i, utf8.length); + return utf8.length; } @Override public int appendUnquoted(char[] chars, int i) { - return 0; + String v = getValue(); + v.getChars(0, v.length(), chars, i); + return v.length(); } @Override public int writeQuotedUTF8(OutputStream outputStream) { - return 0; + try { + byte[] utf8 = asQuotedUTF8(); + outputStream.write(utf8); + return utf8.length; + } + catch (IOException e) { + return -1; + } } @Override public int writeUnquotedUTF8(OutputStream outputStream) { - return 0; + try { + byte[] utf8 = asUnquotedUTF8(); + outputStream.write(utf8); + return utf8.length; + } + catch (IOException e) { + return -1; + } } @Override public int putQuotedUTF8(ByteBuffer byteBuffer) { - return 0; + byte[] utf8 = asQuotedUTF8(); + byteBuffer.put(utf8); + return utf8.length; } @Override public int putUnquotedUTF8(ByteBuffer byteBuffer) { - return 0; + byte[] utf8 = asUnquotedUTF8(); + byteBuffer.put(utf8); + return utf8.length; } public Object getRawValue() 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 000000000..29d704cc9 --- /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", "msgpack-jackson3"); + + @Override + public Version version() + { + return VERSION; + } +} 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 index 2be7e3401..19c8a6995 100644 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java @@ -21,6 +21,7 @@ import tools.jackson.core.JacksonException; import tools.jackson.core.ObjectWriteContext; import tools.jackson.core.StreamWriteFeature; +import tools.jackson.core.TokenStreamContext; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.SerializationContext; import tools.jackson.databind.ValueSerializer; @@ -1127,4 +1128,83 @@ public void testWriteBinaryByteBufferWithOffset() 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("msgpack-jackson3", factory.version().getArtifactId()); + } + + @Test + public void testSerializedStringMethods() + { + 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()); + } } 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 index 8a501301f..38eb7a0cc 100644 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java @@ -1089,4 +1089,23 @@ public void handleMissingValueInMap() 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); + } } diff --git a/plans/preexisting-issues.md b/plans/preexisting-issues.md index f50252530..604165e3c 100644 --- a/plans/preexisting-issues.md +++ b/plans/preexisting-issues.md @@ -23,15 +23,27 @@ this with a bash version check, but a developer running `./sbt test` locally on Java 8 or 11 gets a hard compilation failure. A cleaner build-level solution (conditional aggregate, toolchain support, or a separate profile) is needed. -### 4. `MessagePackGenerator.streamWriteContext()` returns null +### 4. `MessagePackGenerator.streamWriteContext()` returns null — FIXED -**File:** `msgpack-jackson3/.../MessagePackGenerator.java` (streamWriteContext method) +**File:** `msgpack-jackson3/.../MessagePackGenerator.java` -`streamWriteContext()` returns `null`, bypassing Jackson's standard write context -management. This can cause NPEs in Jackson code paths that use the context for path -tracking in error messages or certain serialization features. Fixing it properly -requires integrating with Jackson 3's `TokenStreamContext` / `_streamWriteContext` -managed by `GeneratorBase`, which needs investigation. +Fixed by adding a `SimpleStreamWriteContext writeContext` field initialized to +`SimpleStreamWriteContext.createRootContext(null)`. `streamWriteContext()` returns +it; `writeStartArray/Object` push a child context; `endCurrentContainer` pops via +`clearAndGetParent()`; `writeName` calls `writeContext.writeName(name)`; +`_verifyValueWrite` calls `writeContext.writeValue()`. `currentValue()` and +`assignCurrentValue()` now delegate to the write context. + +Also fixed in the same pass: +- `version()` now returns `PackageVersion.VERSION` (0.9.12) in generator, parser, + and factory, replacing `Version.unknownVersion()`. + +Note: `messageBufferOutputHolder` ThreadLocal was NOT fixed here — calling +`messageBufferOutputHolder.remove()` in `_releaseBuffers()` caused a 23% serialization +regression by defeating the ThreadLocal caching (each close allocates a new +`OutputStreamBufferOutput` on the next generator creation). Dropped in favour of +accepting the minor OutputStream retention, which is only observable when a thread +creates exactly one generator and never creates another. ### 5. `writeString(Reader, int)` len=-1 implementation allocates an extra copy @@ -58,12 +70,19 @@ left off instead of from the beginning. ## 2. `MessagePackParser`: ThreadLocal retains last byte-array payload per thread -**File:** `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java:135` +**Files:** +- `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java:135` +- `msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java` — FIXED `messageUnpackerHolder` is never cleared on parser close. For byte-array inputs this retains the entire last parsed payload for each thread in a pool indefinitely, which can cause unbounded memory retention after large messages. +Fixed in msgpack-jackson3: on `close()`, if the cached source is a `byte[]`, it is +replaced with `null` in the ThreadLocal (keeping the unpacker alive for reuse but +releasing the byte-array reference). InputStream sources are left unchanged because +they are needed to detect same-stream reuse. Needs the same fix in msgpack-jackson. + ## 3. `MessagePackGenerator`: `close()` does not set `isClosed()` to true **Files:** @@ -103,11 +122,15 @@ old container. Fixed in msgpack-jackson3 by adding `currentState = IN_ROOT` in ## 7. `MessagePackSerializedString`: Most interface methods are stubs -**File:** `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java:70` +**Files:** +- `msgpack-jackson/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java:70` +- `msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java` — FIXED Most `SerializableString` methods return 0 or do nothing. Any Jackson code path that calls these methods (e.g. for length or byte-copy operations) will silently -produce incorrect results. +produce incorrect results. Fixed in msgpack-jackson3 by implementing all append/write/put +methods using the existing `asUnquotedUTF8()` / `asQuotedUTF8()` helpers. +Needs the same fix in msgpack-jackson. ## 8. `MessagePackGenerator`: `getBytesIfAscii` writes to wrong index when offset > 0 From b087eb789105f3545997e78b5496da64b1cd7052 Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Sun, 17 May 2026 23:12:45 +0900 Subject: [PATCH 14/17] Mark build Java-version guard as fixed in preexisting-issues.md --- plans/preexisting-issues.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plans/preexisting-issues.md b/plans/preexisting-issues.md index 604165e3c..e986b53fd 100644 --- a/plans/preexisting-issues.md +++ b/plans/preexisting-issues.md @@ -16,12 +16,11 @@ calling `super.close()` (which would unconditionally close the underlying stream Fixed: `snapshot()` now delegates to `copy()`, and `rebuild()` is implemented via `MessagePackFactoryBuilder`. -### 3. Build: `msgpack-jackson3` fails to compile locally on Java < 17 +### 3. Build: `msgpack-jackson3` fails to compile locally on Java < 17 — FIXED -`msgpack-jackson3` is in the root aggregate unconditionally. The CI works around -this with a bash version check, but a developer running `./sbt test` locally on -Java 8 or 11 gets a hard compilation failure. A cleaner build-level solution -(conditional aggregate, toolchain support, or a separate profile) is needed. +`build.sbt` conditionally includes `msgpack-jackson3` in the root aggregate only when +running on Java 17+. Developers on older JDKs and CI on older JDK matrix entries +skip the module cleanly. ### 4. `MessagePackGenerator.streamWriteContext()` returns null — FIXED From 2e90092abd872f82aaa94767ea5bca2664566c9f Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Mon, 18 May 2026 00:00:28 +0900 Subject: [PATCH 15/17] Fix several bugs identified in PR review - MessagePackMapper.version() now returns PackageVersion.VERSION instead of Version.unknownVersion(), consistent with factory and parser. - writeName(SerializableString): remove IllegalArgumentException for unknown implementations (fall back to writeName(getValue())); also fix missing writeContext.writeName() call for MessagePackSerializedString. - unpackString(): delegate to messageUnpacker.unpackString() instead of manual header-read + payload copy; removes tempBytes allocation. ~25% deserialization speedup on pojo benchmark (fewer copies). - Rename local variable 'scheme' -> 'schema' in test. --- .../jackson/dataformat/MessagePackGenerator.java | 9 +++------ .../jackson/dataformat/MessagePackMapper.java | 2 +- .../jackson/dataformat/MessagePackParser.java | 12 +----------- .../dataformat/MessagePackDataformatForPojoTest.java | 4 ++-- 4 files changed, 7 insertions(+), 20 deletions(-) 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 index e234be4ad..078260691 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java @@ -27,7 +27,6 @@ import tools.jackson.core.TokenStreamContext; import tools.jackson.core.base.GeneratorBase; import tools.jackson.core.io.IOContext; -import tools.jackson.core.io.SerializedString; import org.msgpack.core.MessagePack; import org.msgpack.core.MessagePacker; import org.msgpack.core.annotations.Nullable; @@ -569,14 +568,12 @@ public JsonGenerator writeName(String name) throws JacksonException @Override public JsonGenerator writeName(SerializableString name) throws JacksonException { - if (name instanceof SerializedString) { - writeName(name.getValue()); - } - else if (name instanceof MessagePackSerializedString) { + if (name instanceof MessagePackSerializedString) { + writeContext.writeName(name.getValue()); addKeyNode(((MessagePackSerializedString) name).getRawValue()); } else { - throw new IllegalArgumentException("Unsupported key: " + name); + writeName(name.getValue()); } return this; } 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 index 3da263e0c..674b27333 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java @@ -115,6 +115,6 @@ public static Builder builder(MessagePackFactory f) @Override public Version version() { - return Version.unknownVersion(); + 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 index 509e7b63f..790b05b22 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java @@ -40,7 +40,6 @@ import java.io.InputStream; import java.math.BigDecimal; import java.math.BigInteger; -import java.nio.charset.StandardCharsets; public class MessagePackParser extends ParserMinimalBase @@ -58,7 +57,6 @@ public class MessagePackParser private long currentPosition; private final IOContext ioContext; private ExtensionTypeCustomDeserializers extTypeCustomDesers; - private final byte[] tempBytes = new byte[64]; private enum Type { @@ -140,15 +138,7 @@ public Version version() private String unpackString(MessageUnpacker messageUnpacker) throws IOException { - int strLen = messageUnpacker.unpackRawStringHeader(); - if (strLen <= tempBytes.length) { - messageUnpacker.readPayload(tempBytes, 0, strLen); - return new String(tempBytes, 0, strLen, StandardCharsets.UTF_8); - } - else { - byte[] bytes = messageUnpacker.readPayload(strLen); - return new String(bytes, 0, strLen, StandardCharsets.UTF_8); - } + return messageUnpacker.unpackString(); } @Override 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 index c12f0d5e8..417c1389c 100644 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForPojoTest.java @@ -139,8 +139,8 @@ public void testSerializationWithoutSchema() .annotationIntrospector(new JsonArrayFormat()) .build(); byte[] bytes = objectMapper.writeValueAsBytes(complexPojo); - String scheme = new String(bytes, Charset.forName("UTF-8")); - assertThat(scheme, not(containsString("name"))); + 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); From 1605bf4da9cdf16ccafa44e549ee62d6c83ddcd0 Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Mon, 18 May 2026 00:14:02 +0900 Subject: [PATCH 16/17] Simplify string handling for Java 9+ compact strings msgpack-jackson3 requires Java 17+, so char[]-backed strings (Java 8) are not a concern. Java 9+ compact strings make the old AsciiCharString optimization redundant for the char[] path. - Rename AsciiCharString -> RawUtf8String; extend its use to all raw UTF-8 byte[] inputs (not just ASCII), eliminating the areAllAsciiBytes scan and the wasteful decode+re-encode for non-ASCII content. - Simplify writeCharArrayTextValue: drop getBytesIfAscii scan and just use new String(char[], offset, len); packString handles encoding efficiently via compact strings. - Simplify writeRaw(String, int, int): drop getChars -> char[] detour, use substring directly. - Simplify writeString(Reader, int) len<0 path: drop StringBuilder -> char[] copy, use sb.toString() directly. - Remove unused imports and fields (Nullable, Charset, StandardCharsets, DEFAULT_CHARSET). --- .../dataformat/MessagePackGenerator.java | 59 +++---------------- 1 file changed, 9 insertions(+), 50 deletions(-) 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 index 078260691..a3ad4337b 100644 --- a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java @@ -29,7 +29,6 @@ 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; @@ -40,15 +39,12 @@ 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; 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; @@ -64,11 +60,11 @@ public class MessagePackGenerator private boolean isElementsClosed = false; private SimpleStreamWriteContext writeContext; - private static final class AsciiCharString + private static final class RawUtf8String { public final byte[] bytes; - public AsciiCharString(byte[] bytes) + public RawUtf8String(byte[] bytes) { this.bytes = bytes; } @@ -364,8 +360,8 @@ private void packNonContainer(Object v) if (v instanceof String) { messagePacker.packString((String) v); } - else if (v instanceof AsciiCharString) { - byte[] bytes = ((AsciiCharString) v).bytes; + else if (v instanceof RawUtf8String) { + byte[] bytes = ((RawUtf8String) v).bytes; messagePacker.packRawStringHeader(bytes.length); messagePacker.writePayload(bytes); } @@ -494,49 +490,16 @@ private void addValueNode(Object value) throws IOException } } - @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 - offset] = (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 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)) { - byte[] slice = new byte[len]; - System.arraycopy(text, offset, slice, 0, len); - addValueNode(new AsciiCharString(slice)); - return; - } - addValueNode(new String(text, offset, len, DEFAULT_CHARSET)); + byte[] slice = new byte[len]; + System.arraycopy(text, offset, slice, 0, len); + addValueNode(new RawUtf8String(slice)); } @Override @@ -613,9 +576,7 @@ public JsonGenerator writeString(Reader reader, int len) throws JacksonException while ((read = reader.read(tmpBuf)) >= 0) { sb.append(tmpBuf, 0, read); } - char[] chars = new char[sb.length()]; - sb.getChars(0, chars.length, chars, 0); - writeCharArrayTextValue(chars, 0, chars.length); + addValueNode(sb.toString()); } else { char[] buf = new char[len]; @@ -682,9 +643,7 @@ public JsonGenerator writeRaw(String text) throws JacksonException public JsonGenerator writeRaw(String text, int offset, int len) throws JacksonException { try { - char[] chars = new char[len]; - text.getChars(offset, offset + len, chars, 0); - writeCharArrayTextValue(chars, 0, len); + addValueNode(text.substring(offset, offset + len)); } catch (IOException e) { throw _wrapIOFailure(e); From daefba93ef8470d32e8711f6d95a6a0e1aa40a07 Mon Sep 17 00:00:00 2001 From: Mitsunori Komatsu Date: Mon, 18 May 2026 00:40:38 +0900 Subject: [PATCH 17/17] Replace hand-rolled benchmarks with JMH for msgpack-jackson3 The previous Benchmarker/System.nanoTime() approach had no forking, no dead-code elimination (Blackhole), and no JIT-aware warmup, producing ~25ms stdev results. Replace with proper JMH benchmarks modelled on https://github.com/FasterXML/jackson-benchmarks. Changes: - Add sbt-jmh 0.4.7 plugin; enable JmhPlugin on msgpack-jackson3 - Add --add-opens JVM args for forked JMH processes (required for msgpack-core's Unsafe usage) - Remove commons-math3 test dependency (no longer needed) - Delete Benchmarker, MessagePackDataformatPojoBenchmarkTest, MessagePackDataformatHugeDataBenchmarkTest - Add src/jmh/java benchmark sources: * model/: MediaItem, MediaContent, Image, Size, Player, MediaItems (canonical jvm-serializers dataset, same as jackson-benchmarks) * BenchmarkState: pre-serialized bytes + ObjectMapper instances * MsgpackReadBenchmark: readPojoMsgpack / readPojoJson * MsgpackWriteBenchmark: writePojoMsgpack / writePojoJson * NopOutputStream: /dev/null sink for write benchmarks Run: ./sbt "msgpack-jackson3/jmh:run" Smoke-test: ./sbt 'msgpack-jackson3/jmh:run -f 1 -wi 1 -i 1' --- build.sbt | 13 +- .../dataformat/benchmark/BenchmarkState.java | 32 ++++ .../benchmark/MsgpackReadBenchmark.java | 40 +++++ .../benchmark/MsgpackWriteBenchmark.java | 46 +++++ .../dataformat/benchmark/NopOutputStream.java | 26 +++ .../dataformat/benchmark/model/Image.java | 21 +++ .../benchmark/model/MediaContent.java | 29 ++++ .../dataformat/benchmark/model/MediaItem.java | 28 ++++ .../benchmark/model/MediaItems.java | 33 ++++ .../dataformat/benchmark/model/Player.java | 6 + .../dataformat/benchmark/model/Size.java | 6 + .../dataformat/benchmark/Benchmarker.java | 98 ----------- ...gePackDataformatHugeDataBenchmarkTest.java | 136 --------------- ...essagePackDataformatPojoBenchmarkTest.java | 157 ------------------ project/plugins.sbt | 1 + 15 files changed, 276 insertions(+), 396 deletions(-) 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/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 delete mode 100644 msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/Benchmarker.java delete mode 100644 msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java delete mode 100644 msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java diff --git a/build.sbt b/build.sbt index 1b588d294..8ec4f4c00 100644 --- a/build.sbt +++ b/build.sbt @@ -181,7 +181,7 @@ lazy val msgpackJackson = Project(id = "msgpack-jackson", base = file("msgpack-j .dependsOn(msgpackCore) lazy val msgpackJackson3 = Project(id = "msgpack-jackson3", base = file("msgpack-jackson3")) - .enablePlugins(SbtOsgi) + .enablePlugins(SbtOsgi, JmhPlugin) .settings( buildSettings, name := "jackson-dataformat-msgpack3", @@ -194,10 +194,13 @@ lazy val msgpackJackson3 = Project(id = "msgpack-jackson3", base = file("msgpack doc / javacOptions := Seq("-source", "17", "-Xdoclint:none"), libraryDependencies ++= Seq( - "tools.jackson.core" % "jackson-databind" % "3.1.2", - junitInterface, - "org.apache.commons" % "commons-math3" % "3.6.1" % "test" + "tools.jackson.core" % "jackson-databind" % "3.1.2", + junitInterface ), - testOptions += Tests.Argument(TestFrameworks.JUnit, "-v") + 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/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 000000000..63d430649 --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/BenchmarkState.java @@ -0,0 +1,32 @@ +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 000000000..48e702cf0 --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackReadBenchmark.java @@ -0,0 +1,40 @@ +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(value = 2, jvmArgsAppend = { + "--add-opens=java.base/java.nio=ALL-UNNAMED", + "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED" +}) +@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 000000000..3f7da52a5 --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/MsgpackWriteBenchmark.java @@ -0,0 +1,46 @@ +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(value = 2, jvmArgsAppend = { + "--add-opens=java.base/java.nio=ALL-UNNAMED", + "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED" +}) +@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 000000000..e3b932c5d --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/NopOutputStream.java @@ -0,0 +1,26 @@ +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/model/Image.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Image.java new file mode 100644 index 000000000..1d78c1e51 --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Image.java @@ -0,0 +1,21 @@ +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 000000000..91e308461 --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaContent.java @@ -0,0 +1,29 @@ +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 000000000..b3c3b5a2e --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItem.java @@ -0,0 +1,28 @@ +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 000000000..fa9fd41f2 --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/MediaItems.java @@ -0,0 +1,33 @@ +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 000000000..584773861 --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Player.java @@ -0,0 +1,6 @@ +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 000000000..438037439 --- /dev/null +++ b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/model/Size.java @@ -0,0 +1,6 @@ +package org.msgpack.jackson.dataformat.benchmark.model; + +public enum Size +{ + SMALL, LARGE +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/Benchmarker.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/Benchmarker.java deleted file mode 100644 index 980348024..000000000 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/Benchmarker.java +++ /dev/null @@ -1,98 +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.benchmark; - -import org.apache.commons.math3.stat.StatUtils; -import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public class Benchmarker -{ - private final List benchmarkableList = new ArrayList(); - - public abstract static class Benchmarkable - { - private final String label; - - protected Benchmarkable(String label) - { - this.label = label; - } - - public abstract void run() throws Exception; - } - - public void addBenchmark(Benchmarkable benchmark) - { - benchmarkableList.add(benchmark); - } - - private static class Tuple - { - F first; - S second; - - public Tuple(F first, S second) - { - this.first = first; - this.second = second; - } - } - - public void run(int count, int warmupCount) - throws Exception - { - List> benchmarksResults = new ArrayList>(benchmarkableList.size()); - for (Benchmarkable benchmark : benchmarkableList) { - benchmarksResults.add(new Tuple(benchmark.label, new double[count])); - } - - for (int i = 0; i < count + warmupCount; i++) { - for (int bi = 0; bi < benchmarkableList.size(); bi++) { - Benchmarkable benchmark = benchmarkableList.get(bi); - long currentTimeNanos = System.nanoTime(); - benchmark.run(); - - if (i >= warmupCount) { - benchmarksResults.get(bi).second[i - warmupCount] = (System.nanoTime() - currentTimeNanos) / 1000000.0; - } - } - } - - for (Tuple benchmarkResult : benchmarksResults) { - printStat(benchmarkResult.first, benchmarkResult.second); - } - } - - private void printStat(String label, double[] origValues) - { - double[] values = origValues; - Arrays.sort(origValues); - if (origValues.length > 2) { - values = Arrays.copyOfRange(origValues, 1, origValues.length - 1); - } - StandardDeviation standardDeviation = new StandardDeviation(); - System.out.println(label + ":"); - System.out.println(String.format(" mean : %8.3f", StatUtils.mean(values))); - System.out.println(String.format(" min : %8.3f", StatUtils.min(values))); - System.out.println(String.format(" max : %8.3f", StatUtils.max(values))); - System.out.println(String.format(" stdev: %8.3f", standardDeviation.evaluate(values))); - System.out.println(""); - } -} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java deleted file mode 100644 index 9112897c6..000000000 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatHugeDataBenchmarkTest.java +++ /dev/null @@ -1,136 +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.benchmark; - -import tools.jackson.core.JacksonException; -import tools.jackson.core.StreamWriteFeature; -import tools.jackson.core.type.TypeReference; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.json.JsonMapper; -import org.junit.Test; -import org.msgpack.jackson.dataformat.MessagePackFactory; -import org.msgpack.jackson.dataformat.MessagePackMapper; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -public class MessagePackDataformatHugeDataBenchmarkTest -{ - private static final int ELM_NUM = 1000000; - private static final int COUNT = 6; - private static final int WARMUP_COUNT = 4; - private final ObjectMapper origObjectMapper = JsonMapper.builder() - .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) - .build(); - private final ObjectMapper msgpackObjectMapper = MessagePackMapper.builder(new MessagePackFactory()) - .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) - .build(); - private static final List value; - private static final byte[] packedByOriginal; - private static final byte[] packedByMsgPack; - - static { - value = new ArrayList(); - for (int i = 0; i < ELM_NUM; i++) { - value.add((long) i); - } - for (int i = 0; i < ELM_NUM; i++) { - value.add((double) i); - } - for (int i = 0; i < ELM_NUM; i++) { - value.add(String.valueOf(i)); - } - - byte[] bytes = null; - try { - bytes = JsonMapper.builder().build().writeValueAsBytes(value); - } - catch (JacksonException e) { - e.printStackTrace(); - } - packedByOriginal = bytes; - - try { - bytes = new MessagePackMapper(new MessagePackFactory()).writeValueAsBytes(value); - } - catch (JacksonException e) { - e.printStackTrace(); - } - packedByMsgPack = bytes; - } - - @Test - public void testBenchmark() - throws Exception - { - Benchmarker benchmarker = new Benchmarker(); - - File tempFileJackson = File.createTempFile("msgpack-jackson-", "-huge-jackson"); - tempFileJackson.deleteOnExit(); - final OutputStream outputStreamJackson = new FileOutputStream(tempFileJackson); - - File tempFileMsgpack = File.createTempFile("msgpack-jackson-", "-huge-msgpack"); - tempFileMsgpack.deleteOnExit(); - final OutputStream outputStreamMsgpack = new FileOutputStream(tempFileMsgpack); - - benchmarker.addBenchmark(new Benchmarker.Benchmarkable("serialize(huge) with JSON") { - @Override - public void run() - throws Exception - { - origObjectMapper.writeValue(outputStreamJackson, value); - } - }); - - benchmarker.addBenchmark(new Benchmarker.Benchmarkable("serialize(huge) with MessagePack") { - @Override - public void run() - throws Exception - { - msgpackObjectMapper.writeValue(outputStreamMsgpack, value); - } - }); - - benchmarker.addBenchmark(new Benchmarker.Benchmarkable("deserialize(huge) with JSON") { - @Override - public void run() - throws Exception - { - origObjectMapper.readValue(packedByOriginal, new TypeReference>() {}); - } - }); - - benchmarker.addBenchmark(new Benchmarker.Benchmarkable("deserialize(huge) with MessagePack") { - @Override - public void run() - throws Exception - { - msgpackObjectMapper.readValue(packedByMsgPack, new TypeReference>() {}); - } - }); - - try { - benchmarker.run(COUNT, WARMUP_COUNT); - } - finally { - outputStreamJackson.close(); - outputStreamMsgpack.close(); - } - } -} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java deleted file mode 100644 index 3cb7b0889..000000000 --- a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/benchmark/MessagePackDataformatPojoBenchmarkTest.java +++ /dev/null @@ -1,157 +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.benchmark; - -import tools.jackson.core.JacksonException; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.json.JsonMapper; -import org.junit.Test; -import org.msgpack.jackson.dataformat.MessagePackFactory; -import org.msgpack.jackson.dataformat.MessagePackMapper; -import static org.msgpack.jackson.dataformat.MessagePackDataformatTestBase.NormalPojo; -import static org.msgpack.jackson.dataformat.MessagePackDataformatTestBase.Suit; - -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.List; - -public class MessagePackDataformatPojoBenchmarkTest -{ - private static final int LOOP_MAX = 200; - private static final int LOOP_FACTOR_SER = 40; - private static final int LOOP_FACTOR_DESER = 200; - private static final int COUNT = 6; - private static final int WARMUP_COUNT = 4; - private final List pojos = new ArrayList(LOOP_MAX); - private final List pojosSerWithOrig = new ArrayList(LOOP_MAX); - private final List pojosSerWithMsgPack = new ArrayList(LOOP_MAX); - private final ObjectMapper origObjectMapper = JsonMapper.builder().build(); - private final ObjectMapper msgpackObjectMapper = new MessagePackMapper(new MessagePackFactory()); - - public MessagePackDataformatPojoBenchmarkTest() - { - for (int i = 0; i < LOOP_MAX; i++) { - NormalPojo pojo = new NormalPojo(); - pojo.i = i; - pojo.l = i; - pojo.f = Float.valueOf(i); - pojo.d = Double.valueOf(i); - StringBuilder sb = new StringBuilder(); - for (int sbi = 0; sbi < i * 50; sbi++) { - sb.append("x"); - } - pojo.setS(sb.toString()); - pojo.bool = i % 2 == 0; - pojo.bi = BigInteger.valueOf(i); - switch (i % 4) { - case 0: - pojo.suit = Suit.SPADE; - break; - case 1: - pojo.suit = Suit.HEART; - break; - case 2: - pojo.suit = Suit.DIAMOND; - break; - case 3: - pojo.suit = Suit.CLUB; - break; - } - pojo.b = new byte[] {(byte) i}; - pojo.sMultibyte = "012345678Ⅸ"; - pojos.add(pojo); - } - - for (int i = 0; i < LOOP_MAX; i++) { - try { - pojosSerWithOrig.add(origObjectMapper.writeValueAsBytes(pojos.get(i))); - } - catch (JacksonException e) { - throw new RuntimeException("Failed to create test data"); - } - } - - for (int i = 0; i < LOOP_MAX; i++) { - try { - pojosSerWithMsgPack.add(msgpackObjectMapper.writeValueAsBytes(pojos.get(i))); - } - catch (JacksonException e) { - throw new RuntimeException("Failed to create test data"); - } - } - } - - @Test - public void testBenchmark() - throws Exception - { - Benchmarker benchmarker = new Benchmarker(); - - benchmarker.addBenchmark(new Benchmarker.Benchmarkable("serialize(pojo) with JSON") { - @Override - public void run() - throws Exception - { - for (int j = 0; j < LOOP_FACTOR_SER; j++) { - for (int i = 0; i < LOOP_MAX; i++) { - origObjectMapper.writeValueAsBytes(pojos.get(i)); - } - } - } - }); - - benchmarker.addBenchmark(new Benchmarker.Benchmarkable("serialize(pojo) with MessagePack") { - @Override - public void run() - throws Exception - { - for (int j = 0; j < LOOP_FACTOR_SER; j++) { - for (int i = 0; i < LOOP_MAX; i++) { - msgpackObjectMapper.writeValueAsBytes(pojos.get(i)); - } - } - } - }); - - benchmarker.addBenchmark(new Benchmarker.Benchmarkable("deserialize(pojo) with JSON") { - @Override - public void run() - throws Exception - { - for (int j = 0; j < LOOP_FACTOR_DESER; j++) { - for (int i = 0; i < LOOP_MAX; i++) { - origObjectMapper.readValue(pojosSerWithOrig.get(i), NormalPojo.class); - } - } - } - }); - - benchmarker.addBenchmark(new Benchmarker.Benchmarkable("deserialize(pojo) with MessagePack") { - @Override - public void run() - throws Exception - { - for (int j = 0; j < LOOP_FACTOR_DESER; j++) { - for (int i = 0; i < LOOP_MAX; i++) { - msgpackObjectMapper.readValue(pojosSerWithMsgPack.get(i), NormalPojo.class); - } - } - } - }); - - benchmarker.run(COUNT, WARMUP_COUNT); - } -} diff --git a/project/plugins.sbt b/project/plugins.sbt index 2d12cde51..28f255ba1 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")