diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 26c11bfb..4f8c2ae4 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 36d6f05e..8ec4f4c0 100644 --- a/build.sbt +++ b/build.sbt @@ -96,10 +96,16 @@ val buildSettings = Seq[Setting[?]]( Test / compile := ((Test / compile) dependsOn (Test / jcheckStyle)).value ) -val junitJupiter = "org.junit.jupiter" % "junit-jupiter" % "5.14.4" % "test" -val junitVintage = "org.junit.vintage" % "junit-vintage-engine" % "5.14.4" % "test" +val junitJupiter = "org.junit.jupiter" % "junit-jupiter" % "5.14.4" % "test" +val junitVintage = "org.junit.vintage" % "junit-vintage-engine" % "5.14.4" % "test" +val junitInterface = "com.github.sbt" % "junit-interface" % "0.13.3" % "test" // Project settings +val isJava17Plus: Boolean = { + val v = sys.props.getOrElse("java.specification.version", "1.8") + if (v.startsWith("1.")) false else scala.util.Try(v.toInt >= 17).getOrElse(false) +} + lazy val root = Project(id = "msgpack-java", base = file(".")) .settings( buildSettings, @@ -108,7 +114,10 @@ lazy val root = Project(id = "msgpack-java", base = file(".")) publish := {}, publishLocal := {} ) - .aggregate(msgpackCore, msgpackJackson) + .aggregate( + Seq[ProjectReference](msgpackCore, msgpackJackson) ++ + (if (isJava17Plus) Seq[ProjectReference](msgpackJackson3) else Nil): _* + ) lazy val msgpackCore = Project(id = "msgpack-core", base = file("msgpack-core")) .enablePlugins(SbtOsgi) @@ -170,3 +179,28 @@ lazy val msgpackJackson = Project(id = "msgpack-jackson", base = file("msgpack-j testOptions += Tests.Argument(TestFrameworks.JUnit, "-v") ) .dependsOn(msgpackCore) + +lazy val msgpackJackson3 = Project(id = "msgpack-jackson3", base = file("msgpack-jackson3")) + .enablePlugins(SbtOsgi, JmhPlugin) + .settings( + buildSettings, + name := "jackson-dataformat-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"), + OsgiKeys.importPackage := Seq("!android.os", "!sun.*"), + Test / fork := true, + javacOptions := Seq("-source", "17", "-target", "17"), + doc / javacOptions := Seq("-source", "17", "-Xdoclint:none"), + libraryDependencies ++= + Seq( + "tools.jackson.core" % "jackson-databind" % "3.1.2", + junitInterface + ), + testOptions += Tests.Argument(TestFrameworks.JUnit, "-v"), + Jmh / javaOptions ++= Seq( + "--add-opens=java.base/java.nio=ALL-UNNAMED", + "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED" + ) + ) + .dependsOn(msgpackCore) diff --git a/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/BenchmarkState.java b/msgpack-jackson3/src/jmh/java/org/msgpack/jackson/dataformat/benchmark/BenchmarkState.java new file mode 100644 index 00000000..63d43064 --- /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 00000000..48e702cf --- /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 00000000..3f7da52a --- /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 00000000..e3b932c5 --- /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 00000000..1d78c1e5 --- /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 00000000..91e30846 --- /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 00000000..b3c3b5a2 --- /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 00000000..fa9fd41f --- /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 00000000..58477386 --- /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 00000000..43803743 --- /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/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java new file mode 100644 index 00000000..aa587975 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/ExtensionTypeCustomDeserializers.java @@ -0,0 +1,56 @@ +// +// MessagePack for Java +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +package org.msgpack.jackson.dataformat; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class ExtensionTypeCustomDeserializers +{ + private Map deserTable = new ConcurrentHashMap<>(); + + public ExtensionTypeCustomDeserializers() + { + } + + public ExtensionTypeCustomDeserializers(ExtensionTypeCustomDeserializers src) + { + this(); + this.deserTable.putAll(src.deserTable); + } + + public void addCustomDeser(byte type, final Deser deser) + { + deserTable.put(type, deser); + } + + public Deser getDeser(byte type) + { + return deserTable.get(type); + } + + public void clearEntries() + { + deserTable.clear(); + } + + public interface Deser + { + Object deserialize(byte[] data) + throws IOException; + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/JsonArrayFormat.java new file mode 100644 index 00000000..84be3f5c --- /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 00000000..2c7869f6 --- /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 00000000..3e61e7df --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactory.java @@ -0,0 +1,253 @@ +// +// 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); + } + } + + protected MessagePackFactory(MessagePackFactoryBuilder b) + { + super(b); + this.packerConfig = b.packerConfig().clone(); + this.reuseResourceInGenerator = b.reuseResourceInGenerator(); + this.reuseResourceInParser = b.reuseResourceInParser(); + this.supportIntegerKeys = b.supportIntegerKeys(); + this.extTypeCustomDesers = b.extTypeCustomDesers(); + } + + public MessagePackFactory setReuseResourceInGenerator(boolean reuseResourceInGenerator) + { + this.reuseResourceInGenerator = reuseResourceInGenerator; + return this; + } + + public MessagePackFactory setReuseResourceInParser(boolean reuseResourceInParser) + { + this.reuseResourceInParser = reuseResourceInParser; + return this; + } + + public MessagePackFactory setSupportIntegerKeys(boolean supportIntegerKeys) + { + this.supportIntegerKeys = supportIntegerKeys; + return this; + } + + public MessagePackFactory setExtTypeCustomDesers(ExtensionTypeCustomDeserializers extTypeCustomDesers) + { + this.extTypeCustomDesers = extTypeCustomDesers; + return this; + } + + @Override + protected JsonParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt, + InputStream in) throws JacksonException + { + try { + MessagePackParser parser = new MessagePackParser(readCtxt, ioCtxt, + readCtxt.getStreamReadFeatures(_streamReadFeatures), 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 copy(); + } + + @Override + public TSFBuilder rebuild() + { + return new MessagePackFactoryBuilder(this); + } + + @Override + public Version version() + { + return PackageVersion.VERSION; + } + + @VisibleForTesting + MessagePack.PackerConfig getPackerConfig() + { + return packerConfig; + } + + @VisibleForTesting + boolean isReuseResourceInGenerator() + { + return reuseResourceInGenerator; + } + + @VisibleForTesting + boolean isReuseResourceInParser() + { + return reuseResourceInParser; + } + + @VisibleForTesting + boolean isSupportIntegerKeys() + { + return supportIntegerKeys; + } + + @VisibleForTesting + ExtensionTypeCustomDeserializers getExtTypeCustomDesers() + { + return extTypeCustomDesers; + } + + @Override + public String getFormatName() + { + return "msgpack"; + } + + @Override + public boolean canParseAsync() + { + return false; + } + + @Override + public boolean canUseSchema(FormatSchema schema) + { + return false; + } + + @Override + public Class getFormatReadFeatureType() + { + return null; + } + + @Override + public Class getFormatWriteFeatureType() + { + return null; + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactoryBuilder.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackFactoryBuilder.java new file mode 100644 index 00000000..521ab4d9 --- /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/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java new file mode 100644 index 00000000..a3ad4337 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackGenerator.java @@ -0,0 +1,975 @@ +// +// 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.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 org.msgpack.core.MessagePack; +import org.msgpack.core.MessagePacker; +import org.msgpack.core.buffer.MessageBufferOutput; +import org.msgpack.core.buffer.OutputStreamBufferOutput; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +public class MessagePackGenerator + extends GeneratorBase +{ + private static final int IN_ROOT = 0; + private static final int IN_OBJECT = 1; + private static final int IN_ARRAY = 2; + private final MessagePacker messagePacker; + 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 SimpleStreamWriteContext writeContext; + + private static final class RawUtf8String + { + public final byte[] bytes; + + public RawUtf8String(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; + this.writeContext = SimpleStreamWriteContext.createRootContext(null); + } + + public MessagePackGenerator( + ObjectWriteContext writeCtxt, + IOContext ioCtxt, + int streamWriteFeatures, + OutputStream out, + MessagePack.PackerConfig packerConfig, + boolean reuseResourceInGenerator, + boolean supportIntegerKeys) + throws IOException + { + super(writeCtxt, ioCtxt, streamWriteFeatures); + this.output = out; + this.messagePacker = packerConfig.newPacker(getMessageBufferOutputForOutputStream(out, reuseResourceInGenerator)); + this.packerConfig = packerConfig; + this.nodes = new ArrayList<>(); + this.supportIntegerKeys = supportIntegerKeys; + this.writeContext = SimpleStreamWriteContext.createRootContext(null); + } + + 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 + { + writeContext = writeContext.createChildArrayContext(currentValue); + if (currentState == IN_OBJECT) { + Node node = nodes.get(nodes.size() - 1); + assert node instanceof NodeEntryInObject; + NodeEntryInObject nodeEntryInObject = (NodeEntryInObject) node; + nodeEntryInObject.value = new NodeArray(currentParentElementIndex); + } + else { + 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 + { + writeContext = writeContext.createChildObjectContext(forValue); + if (currentState == IN_OBJECT) { + Node node = nodes.get(nodes.size() - 1); + assert node instanceof NodeEntryInObject; + NodeEntryInObject nodeEntryInObject = (NodeEntryInObject) node; + nodeEntryInObject.value = new NodeObject(currentParentElementIndex); + } + else { + 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() + { + writeContext = writeContext.clearAndGetParent(); + Node parent = nodes.get(currentParentElementIndex); + if (currentParentElementIndex == 0) { + isElementsClosed = true; + currentParentElementIndex = parent.parentIndex; + currentState = IN_ROOT; + return; + } + + currentParentElementIndex = parent.parentIndex; + assert currentParentElementIndex >= 0; + Node currentParent = nodes.get(currentParentElementIndex); + currentParent.incrementChildCount(); + currentState = currentParent.currentStateAsParent(); + } + + private void packNonContainer(Object v) + throws IOException + { + MessagePacker messagePacker = getMessagePacker(); + if (v instanceof String) { + messagePacker.packString((String) v); + } + else if (v instanceof RawUtf8String) { + byte[] bytes = ((RawUtf8String) 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() + bb.position(), 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.compareTo(BigDecimal.valueOf(doubleValue)) != 0) { + throw new IllegalArgumentException("MessagePack cannot serialize a BigDecimal that can't be represented as double. " + decimal); + } + messagePacker.packDouble(doubleValue); + } + } + + private void packObject(NodeObject container) + throws IOException + { + MessagePacker messagePacker = getMessagePacker(); + messagePacker.packMapHeader(container.childCount); + } + + private void packArray(NodeArray container) + throws IOException + { + MessagePacker messagePacker = getMessagePacker(); + messagePacker.packArrayHeader(container.childCount); + } + + private void addKeyNode(Object key) + { + if (currentState != IN_OBJECT) { + 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; + } + } + + private void writeCharArrayTextValue(char[] text, int offset, int len) throws IOException + { + addValueNode(new String(text, offset, len)); + } + + private void writeByteArrayTextValue(byte[] text, int offset, int len) throws IOException + { + byte[] slice = new byte[len]; + System.arraycopy(text, offset, slice, 0, len); + addValueNode(new RawUtf8String(slice)); + } + + @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 + { + writeContext.writeName(name); + addKeyNode(name); + return this; + } + + @Override + public JsonGenerator writeName(SerializableString name) throws JacksonException + { + if (name instanceof MessagePackSerializedString) { + writeContext.writeName(name.getValue()); + addKeyNode(((MessagePackSerializedString) name).getRawValue()); + } + else { + writeName(name.getValue()); + } + return this; + } + + @Override + public JsonGenerator writeString(String text) throws JacksonException + { + try { + addValueNode(text); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeString(char[] text, int offset, int len) throws JacksonException + { + try { + writeCharArrayTextValue(text, offset, len); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeString(Reader reader, int len) throws JacksonException + { + try { + 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); + } + addValueNode(sb.toString()); + } + 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); + } + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeString(SerializableString text) throws JacksonException + { + return writeString(text.getValue()); + } + + @Override + public JsonGenerator writeRawUTF8String(byte[] text, int offset, int length) throws JacksonException + { + try { + writeByteArrayTextValue(text, offset, length); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeUTF8String(byte[] text, int offset, int length) throws JacksonException + { + try { + writeByteArrayTextValue(text, offset, length); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeRaw(String text) throws JacksonException + { + try { + addValueNode(text); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeRaw(String text, int offset, int len) throws JacksonException + { + try { + addValueNode(text.substring(offset, offset + len)); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeRaw(char[] text, int offset, int len) throws JacksonException + { + try { + writeCharArrayTextValue(text, offset, len); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeRaw(char c) throws JacksonException + { + try { + writeCharArrayTextValue(new char[] { c }, 0, 1); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeBinary(Base64Variant b64variant, byte[] data, int offset, int len) throws JacksonException + { + try { + addValueNode(ByteBuffer.wrap(data, offset, len)); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeNumber(short v) throws JacksonException + { + return writeNumber((int) v); + } + + @Override + public JsonGenerator writeNumber(int v) throws JacksonException + { + try { + addValueNode(v); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeNumber(long v) throws JacksonException + { + try { + addValueNode(v); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeNumber(BigInteger v) throws JacksonException + { + try { + addValueNode(v); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeNumber(double d) throws JacksonException + { + try { + addValueNode(d); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeNumber(float f) throws JacksonException + { + try { + addValueNode(f); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeNumber(BigDecimal dec) throws JacksonException + { + try { + addValueNode(dec); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + return this; + } + + @Override + public JsonGenerator writeNumber(String encodedValue) throws JacksonException + { + try { + try { + long l = Long.parseLong(encodedValue); + addValueNode(l); + return this; + } + catch (NumberFormatException ignored) { + } + + try { + BigInteger bi = new BigInteger(encodedValue); + addValueNode(bi); + return this; + } + catch (NumberFormatException ignored) { + } + + try { + double d = Double.parseDouble(encodedValue); + addValueNode(d); + 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(); + if (StreamWriteFeature.AUTO_CLOSE_TARGET.enabledIn(_streamWriteFeatures)) { + try { + MessagePacker messagePacker = getMessagePacker(); + messagePacker.close(); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + } + } + finally { + _closed = true; + _releaseBuffers(); + } + } + + @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 PackageVersion.VERSION; + } + + @Override + public TokenStreamContext streamWriteContext() + { + return writeContext; + } + + @Override + public Object streamWriteOutputTarget() + { + return output; + } + + @Override + public int streamWriteOutputBuffered() + { + return -1; + } + + @Override + public Object currentValue() + { + return writeContext.currentValue(); + } + + @Override + public void assignCurrentValue(Object v) + { + writeContext.assignCurrentValue(v); + } + + @Override + protected void _closeInput() throws IOException + { + messagePacker.close(); + } + + @Override + protected void _releaseBuffers() + { + } + + @Override + protected void _verifyValueWrite(String typeMsg) throws JacksonException + { + writeContext.writeValue(); + } + + private MessagePacker getMessagePacker() + { + return messagePacker; + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java new file mode 100644 index 00000000..836a905d --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackKeySerializer.java @@ -0,0 +1,35 @@ +// +// MessagePack for Java +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +package org.msgpack.jackson.dataformat; + +import tools.jackson.core.JsonGenerator; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ser.std.StdSerializer; + +public class MessagePackKeySerializer + extends StdSerializer +{ + public MessagePackKeySerializer() + { + super(Object.class); + } + + @Override + public void serialize(Object value, JsonGenerator jgen, SerializationContext provider) + { + jgen.writeName(new MessagePackSerializedString(value)); + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java new file mode 100644 index 00000000..674b2733 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackMapper.java @@ -0,0 +1,120 @@ +// +// MessagePack for Java +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +package org.msgpack.jackson.dataformat; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import tools.jackson.core.Version; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.cfg.MapperBuilder; +import tools.jackson.databind.cfg.MapperBuilderState; + +import java.math.BigDecimal; +import java.math.BigInteger; + +public class MessagePackMapper extends ObjectMapper +{ + private static final long serialVersionUID = 3L; + + public static class Builder extends MapperBuilder + { + public Builder(MessagePackFactory f) + { + super(f); + } + + protected Builder(StateImpl state) + { + super(state); + } + + @Override + public MessagePackMapper build() + { + return new MessagePackMapper(this); + } + + public Builder handleBigIntegerAsString() + { + return withConfigOverride(BigInteger.class, + o -> o.setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING))); + } + + public Builder handleBigDecimalAsString() + { + return withConfigOverride(BigDecimal.class, + o -> o.setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING))); + } + + public Builder handleBigIntegerAndBigDecimalAsString() + { + return handleBigIntegerAsString().handleBigDecimalAsString(); + } + + @Override + protected MapperBuilderState _saveState() + { + return new StateImpl(this); + } + + protected static class StateImpl extends MapperBuilderState + { + private static final long serialVersionUID = 3L; + + public StateImpl(Builder src) + { + super(src); + } + + @Override + protected Object readResolve() + { + return new Builder(this).build(); + } + } + } + + public MessagePackMapper() + { + this(new Builder(new MessagePackFactory())); + } + + public MessagePackMapper(MessagePackFactory f) + { + this(new Builder(f)); + } + + protected MessagePackMapper(Builder builder) + { + super(builder); + } + + public static Builder builder() + { + return new Builder(new MessagePackFactory()); + } + + public static Builder builder(MessagePackFactory f) + { + return new Builder(f); + } + + @Override + public Version version() + { + return PackageVersion.VERSION; + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java new file mode 100644 index 00000000..790b05b2 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackParser.java @@ -0,0 +1,638 @@ +// +// 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; + +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 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 PackageVersion.VERSION; + } + + private String unpackString(MessageUnpacker messageUnpacker) throws IOException + { + return messageUnpacker.unpackString(); + } + + @Override + public JsonToken nextToken() throws JacksonException + { + try { + return _nextToken(); + } + catch (IOException e) { + throw _wrapIOFailure(e); + } + } + + private JsonToken _nextToken() throws IOException + { + 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; + Tuple tuple = messageUnpackerHolder.get(); + if (tuple != null && tuple.first() instanceof byte[]) { + messageUnpackerHolder.set(new Tuple<>(null, tuple.second())); + } + } + } + + @Override + public TokenStreamContext streamReadContext() + { + return streamReadContext; + } + + @Override + public TokenStreamLocation currentTokenLocation() + { + // columnNr repurposed as byte offset; truncates for inputs > 2 GB + return new TokenStreamLocation(ioContext.contentReference(), tokenPosition, -1, (int) tokenPosition); + } + + @Override + public TokenStreamLocation currentLocation() + { + // columnNr repurposed as byte offset; truncates for inputs > 2 GB + return new TokenStreamLocation(ioContext.contentReference(), currentPosition, -1, (int) currentPosition); + } + + @Override + public String currentName() + { + if (_currToken == JsonToken.START_OBJECT || _currToken == JsonToken.START_ARRAY) { + MessagePackReadContext parent = streamReadContext.getParent(); + return parent.currentName(); + } + return streamReadContext.currentName(); + } + + @Override + public Object streamReadInputSource() + { + return ioContext.contentReference().getRawContent(); + } + + @Override + public Object currentValue() + { + return streamReadContext.currentValue(); + } + + @Override + public void assignCurrentValue(Object v) + { + streamReadContext.assignCurrentValue(v); + } + + @Override + public boolean isNaN() + { + if (type == Type.DOUBLE) { + return Double.isNaN(doubleValue) || Double.isInfinite(doubleValue); + } + return false; + } + + public boolean isCurrentFieldId() + { + return this.type == Type.INT || this.type == Type.LONG; + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackReadContext.java new file mode 100644 index 00000000..1da2574c --- /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 00000000..ed01790f --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializedString.java @@ -0,0 +1,145 @@ +// +// MessagePack for Java +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +package org.msgpack.jackson.dataformat; + +import tools.jackson.core.SerializableString; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +public class MessagePackSerializedString + implements SerializableString +{ + private static final Charset UTF8 = StandardCharsets.UTF_8; + private final Object value; + + public MessagePackSerializedString(Object value) + { + this.value = value; + } + + @Override + public String getValue() + { + return value.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) + { + byte[] utf8 = asQuotedUTF8(); + System.arraycopy(utf8, 0, bytes, i, utf8.length); + return utf8.length; + } + + @Override + public int appendQuoted(char[] chars, int i) + { + char[] q = asQuotedChars(); + System.arraycopy(q, 0, chars, i, q.length); + return q.length; + } + + @Override + public int appendUnquotedUTF8(byte[] bytes, int i) + { + byte[] utf8 = asUnquotedUTF8(); + System.arraycopy(utf8, 0, bytes, i, utf8.length); + return utf8.length; + } + + @Override + public int appendUnquoted(char[] chars, int i) + { + String v = getValue(); + v.getChars(0, v.length(), chars, i); + return v.length(); + } + + @Override + public int writeQuotedUTF8(OutputStream outputStream) + { + try { + byte[] utf8 = asQuotedUTF8(); + outputStream.write(utf8); + return utf8.length; + } + catch (IOException e) { + return -1; + } + } + + @Override + public int writeUnquotedUTF8(OutputStream outputStream) + { + try { + byte[] utf8 = asUnquotedUTF8(); + outputStream.write(utf8); + return utf8.length; + } + catch (IOException e) { + return -1; + } + } + + @Override + public int putQuotedUTF8(ByteBuffer byteBuffer) + { + byte[] utf8 = asQuotedUTF8(); + byteBuffer.put(utf8); + return utf8.length; + } + + @Override + public int putUnquotedUTF8(ByteBuffer byteBuffer) + { + byte[] utf8 = asUnquotedUTF8(); + byteBuffer.put(utf8); + return utf8.length; + } + + public Object getRawValue() + { + return value; + } +} diff --git a/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/MessagePackSerializerFactory.java new file mode 100644 index 00000000..67ee2e2f --- /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/PackageVersion.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/PackageVersion.java new file mode 100644 index 00000000..29d704cc --- /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/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/TimestampExtensionModule.java new file mode 100644 index 00000000..5c719541 --- /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 00000000..b0f720d8 --- /dev/null +++ b/msgpack-jackson3/src/main/java/org/msgpack/jackson/dataformat/Tuple.java @@ -0,0 +1,38 @@ +// +// MessagePack for Java +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +package org.msgpack.jackson.dataformat; + +public class Tuple +{ + private final F first; + private final S second; + + public Tuple(F first, S second) + { + this.first = first; + this.second = second; + } + + public F first() + { + return first; + } + + public S second() + { + return second; + } +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java new file mode 100644 index 00000000..6a194a8c --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/ExampleOfTypeInformationSerDe.java @@ -0,0 +1,171 @@ +// +// MessagePack for Java +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +package org.msgpack.jackson.dataformat; + +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JacksonException; +import tools.jackson.core.TreeNode; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class ExampleOfTypeInformationSerDe + extends MessagePackDataformatTestBase +{ + static class A + { + private List list = new ArrayList(); + + public List getList() + { + return list; + } + + public void setList(List list) + { + this.list = list; + } + } + + static class B + { + private String str; + + public String getStr() + { + return str; + } + + public void setStr(String str) + { + this.str = str; + } + } + + @JsonSerialize(using = ObjectContainerSerializer.class) + @JsonDeserialize(using = ObjectContainerDeserializer.class) + static class ObjectContainer + { + private final Map objects; + + public ObjectContainer(Map objects) + { + this.objects = objects; + } + + public Map getObjects() + { + return objects; + } + } + + static class ObjectContainerSerializer + extends ValueSerializer + { + @Override + public void serialize(ObjectContainer value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException + { + gen.writeStartObject(); + HashMap metadata = new HashMap(); + for (Map.Entry entry : value.getObjects().entrySet()) { + metadata.put(entry.getKey(), entry.getValue().getClass().getName()); + } + gen.writePOJOProperty("__metadata", metadata); + gen.writePOJOProperty("objects", value.getObjects()); + gen.writeEndObject(); + } + } + + static class ObjectContainerDeserializer + extends ValueDeserializer + { + @Override + public ObjectContainer deserialize(JsonParser p, DeserializationContext ctxt) + throws JacksonException + { + ObjectContainer objectContainer = new ObjectContainer(new HashMap()); + TreeNode treeNode = p.readValueAsTree(); + + Map metadata = treeNode.get("__metadata") + .traverse(p.objectReadContext()) + .readValueAs(new TypeReference>() {}); + TreeNode dataMapTree = treeNode.get("objects"); + for (Map.Entry entry : metadata.entrySet()) { + try { + Object o = dataMapTree.get(entry.getKey()) + .traverse(p.objectReadContext()) + .readValueAs(Class.forName(entry.getValue())); + objectContainer.getObjects().put(entry.getKey(), o); + } + catch (ClassNotFoundException e) { + throw new RuntimeException("Failed to deserialize: " + entry, e); + } + } + + return objectContainer; + } + } + + @Test + public void test() + throws IOException + { + ObjectContainer objectContainer = new ObjectContainer(new HashMap()); + { + A a = new A(); + a.setList(Arrays.asList("first", "second", "third")); + objectContainer.getObjects().put("a", a); + + B b = new B(); + b.setStr("hello world"); + objectContainer.getObjects().put("b", b); + + Double pi = 3.14; + objectContainer.getObjects().put("pi", pi); + } + + ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory()); + byte[] bytes = objectMapper.writeValueAsBytes(objectContainer); + ObjectContainer restored = objectMapper.readValue(bytes, ObjectContainer.class); + + { + assertEquals(3, restored.getObjects().size()); + A a = (A) restored.getObjects().get("a"); + assertArrayEquals(new String[] {"first", "second", "third"}, a.getList().toArray()); + B b = (B) restored.getObjects().get("b"); + assertEquals("hello world", b.getStr()); + assertEquals(3.14, restored.getObjects().get("pi")); + } + } +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatForFieldIdTest.java new file mode 100644 index 00000000..86e9e8c3 --- /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 00000000..417c1389 --- /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 schema = new String(bytes, Charset.forName("UTF-8")); + assertThat(schema, not(containsString("name"))); + ComplexPojo value = objectMapper.readValue(bytes, ComplexPojo.class); + assertEquals("komamitsu", value.name); + assertEquals(20, value.age); + assertArrayEquals(complexPojo.values.toArray(), value.values.toArray()); + assertEquals(complexPojo.grades.get("math"), value.grades.get("math")); + } +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java new file mode 100644 index 00000000..8a7f1e52 --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackDataformatTestBase.java @@ -0,0 +1,289 @@ +// +// MessagePack for Java +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +package org.msgpack.jackson.dataformat; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import tools.jackson.databind.ObjectMapper; +import org.junit.After; +import org.junit.Before; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Collections; + +public class MessagePackDataformatTestBase +{ + protected MessagePackFactory factory; + protected ByteArrayOutputStream out; + protected ByteArrayInputStream in; + protected ObjectMapper objectMapper; + protected NormalPojo normalPojo; + protected NestedListPojo nestedListPojo; + protected NestedListComplexPojo1 nestedListComplexPojo1; + protected NestedListComplexPojo2 nestedListComplexPojo2; + protected StringPojo stringPojo; + protected TinyPojo tinyPojo; + protected ComplexPojo complexPojo; + + @Before + public void setup() + { + factory = new MessagePackFactory(); + objectMapper = new MessagePackMapper(factory); + out = new ByteArrayOutputStream(); + in = new ByteArrayInputStream(new byte[4096]); + + normalPojo = new NormalPojo(); + normalPojo.setS("komamitsu"); + normalPojo.bool = true; + normalPojo.i = Integer.MAX_VALUE; + normalPojo.l = Long.MIN_VALUE; + normalPojo.f = Float.MIN_VALUE; + normalPojo.d = Double.MAX_VALUE; + normalPojo.b = new byte[] {0x01, 0x02, (byte) 0xFE, (byte) 0xFF}; + normalPojo.bi = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE); + normalPojo.suit = Suit.HEART; + normalPojo.sMultibyte = "text文字"; + + nestedListPojo = new NestedListPojo(); + nestedListPojo.s = "a string"; + nestedListPojo.strs = Arrays.asList("string#1", "string#2", "string#3"); + + tinyPojo = new TinyPojo(); + tinyPojo.t = "t string"; + + nestedListComplexPojo1 = new NestedListComplexPojo1(); + nestedListComplexPojo1.s = "a string"; + nestedListComplexPojo1.foos = new ArrayList<>(); + nestedListComplexPojo1.foos.add(tinyPojo); + + nestedListComplexPojo2 = new NestedListComplexPojo2(); + nestedListComplexPojo2.foos = new ArrayList<>(); + nestedListComplexPojo2.foos.add(tinyPojo); + nestedListComplexPojo2.foos.add(tinyPojo); + nestedListComplexPojo2.s = "another string"; + + stringPojo = new StringPojo(); + stringPojo.shortSingleByte = "hello"; + stringPojo.longSingleByte = "helloworldhelloworldhelloworldhelloworldhelloworldhelloworldhelloworldhelloworld"; + stringPojo.shortMultiByte = "こんにちは"; + stringPojo.longMultiByte = "こんにちは、世界!!こんにちは、世界!!こんにちは、世界!!こんにちは、世界!!こんにちは、世界!!"; + + complexPojo = new ComplexPojo(); + complexPojo.name = "komamitsu"; + complexPojo.age = 20; + complexPojo.grades = Collections.singletonMap("math", 97); + complexPojo.values = Arrays.asList("one", "two", "three"); + } + + @After + public void teardown() + { + if (in != null) { + try { + in.close(); + } + catch (IOException e) { + e.printStackTrace(); + } + } + + if (out != null) { + try { + out.close(); + } + catch (IOException e) { + e.printStackTrace(); + } + } + } + + public enum Suit + { + SPADE, HEART, DIAMOND, CLUB; + } + + public static class NestedListPojo + { + public String s; + public List strs; + } + + public static class ComplexPojo + { + public String name; + public int age; + public List values; + public Map grades; + } + + public static class TinyPojo + { + public String t; + } + + public static class NestedListComplexPojo1 + { + public String s; + public List foos; + } + + public static class NestedListComplexPojo2 + { + public List foos; + public String s; + } + + public static class StringPojo + { + public String shortSingleByte; + public String longSingleByte; + public String shortMultiByte; + public String longMultiByte; + } + + public static class NormalPojo + { + String s; + public boolean bool; + public int i; + public long l; + public Float f; + public Double d; + public byte[] b; + public BigInteger bi; + public Suit suit; + public String sMultibyte; + + public String getS() + { + return s; + } + + public void setS(String s) + { + this.s = s; + } + } + + public static class BinKeyPojo + { + public byte[] b; + public String s; + } + + public static class UsingCustomConstructorPojo + { + final String name; + final int age; + + public UsingCustomConstructorPojo(@JsonProperty("name") String name, @JsonProperty("age") int age) + { + this.name = name; + this.age = age; + } + + public String getName() + { + return name; + } + + public int getAge() + { + return age; + } + } + + @JsonIgnoreProperties({"foo", "bar"}) + public static class IgnoringPropertiesPojo + { + int code; + + @JsonIgnore + public String internal; + + public String external; + + @JsonIgnore + public void setCode(int c) + { + code = c; + } + + public int getCode() + { + return code; + } + } + + public static class ChangingPropertyNamesPojo + { + String name; + + @JsonProperty("name") + public String getTheName() + { + return name; + } + + public void setTheName(String n) + { + name = n; + } + } + + protected interface FileSetup + { + void setup(File f) + throws Exception; + } + + protected File createTempFile() + throws Exception + { + return createTempFile(null); + } + + protected File createTempFile(FileSetup fileSetup) + throws Exception + { + File tempFile = File.createTempFile("test", "msgpack"); + tempFile.deleteOnExit(); + if (fileSetup != null) { + fileSetup.setup(tempFile); + } + return tempFile; + } + + protected OutputStream createTempFileOutputStream() + throws IOException + { + File tempFile = File.createTempFile("test", "msgpack"); + tempFile.deleteOnExit(); + return new FileOutputStream(tempFile); + } +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java new file mode 100644 index 00000000..25ef0fe2 --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackFactoryTest.java @@ -0,0 +1,198 @@ +// +// MessagePack for Java +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +package org.msgpack.jackson.dataformat; + +import tools.jackson.core.JsonEncoding; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.TSFBuilder; +import tools.jackson.core.TokenStreamFactory; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import org.junit.Test; +import org.msgpack.core.MessagePack; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.junit.Assert.assertEquals; +import static org.hamcrest.MatcherAssert.assertThat; + +public class MessagePackFactoryTest + extends MessagePackDataformatTestBase +{ + @Test + public void testCreateGenerator() + throws IOException + { + JsonEncoding enc = JsonEncoding.UTF8; + JsonGenerator generator = factory.createGenerator(out, enc); + assertEquals(MessagePackGenerator.class, generator.getClass()); + } + + @Test + public void testCreateParser() + throws IOException + { + JsonParser parser = factory.createParser(in); + assertEquals(MessagePackParser.class, parser.getClass()); + } + + @Test + public void testCopyWithDefaultConfig() + throws IOException + { + MessagePackFactory messagePackFactory = new MessagePackFactory(); + ObjectMapper objectMapper = new MessagePackMapper(messagePackFactory); + + // Use the original ObjectMapper in advance + { + byte[] bytes = objectMapper.writeValueAsBytes(1234); + assertThat(objectMapper.readValue(bytes, Integer.class), is(1234)); + } + + // Copy the factory + TokenStreamFactory copiedFactory = messagePackFactory.copy(); + assertThat(copiedFactory, is(instanceOf(MessagePackFactory.class))); + MessagePackFactory copiedMessagePackFactory = (MessagePackFactory) copiedFactory; + + assertThat(copiedMessagePackFactory.getPackerConfig().isStr8FormatSupport(), is(true)); + assertThat(copiedMessagePackFactory.getExtTypeCustomDesers(), is(nullValue())); + + // Check the copied factory works fine + ObjectMapper copiedObjectMapper = new MessagePackMapper(copiedMessagePackFactory); + Map map = new HashMap<>(); + map.put("one", 1); + Map deserialized = copiedObjectMapper + .readValue(objectMapper.writeValueAsBytes(map), new TypeReference>() {}); + assertThat(deserialized.size(), is(1)); + assertThat(deserialized.get("one"), is(1)); + } + + @Test + public void testRebuildWithDefaultConfig() + throws IOException + { + MessagePackFactory messagePackFactory = new MessagePackFactory(); + TSFBuilder builder = messagePackFactory.rebuild(); + assertThat(builder, is(instanceOf(MessagePackFactoryBuilder.class))); + + MessagePackFactory rebuilt = (MessagePackFactory) builder.build(); + assertThat(rebuilt, is(not(sameInstance(messagePackFactory)))); + assertThat(rebuilt.getPackerConfig().isStr8FormatSupport(), is(true)); + assertThat(rebuilt.getExtTypeCustomDesers(), is(nullValue())); + + ObjectMapper rebuiltObjectMapper = new MessagePackMapper(rebuilt); + byte[] bytes = rebuiltObjectMapper.writeValueAsBytes(42); + assertThat(rebuiltObjectMapper.readValue(bytes, Integer.class), is(42)); + } + + @Test + public void testRebuildWithAdvancedConfig() + throws IOException + { + ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); + extTypeCustomDesers.addCustomDeser((byte) 42, + new ExtensionTypeCustomDeserializers.Deser() + { + @Override + public Object deserialize(byte[] data) + throws IOException + { + TinyPojo pojo = new TinyPojo(); + pojo.t = new String(data); + return pojo; + } + } + ); + MessagePack.PackerConfig packerConfig = new MessagePack.PackerConfig().withStr8FormatSupport(false); + MessagePackFactory messagePackFactory = new MessagePackFactory(packerConfig); + messagePackFactory.setExtTypeCustomDesers(extTypeCustomDesers); + + MessagePackFactory rebuilt = (MessagePackFactory) messagePackFactory.rebuild().build(); + assertThat(rebuilt, is(not(sameInstance(messagePackFactory)))); + assertThat(rebuilt.getPackerConfig().isStr8FormatSupport(), is(false)); + assertThat(rebuilt.getExtTypeCustomDesers().getDeser((byte) 42), is(notNullValue())); + assertThat(rebuilt.getExtTypeCustomDesers().getDeser((byte) 43), is(nullValue())); + } + + @Test + public void testSnapshotReturnsNewInstance() + { + MessagePackFactory messagePackFactory = new MessagePackFactory(); + TokenStreamFactory snapshot = messagePackFactory.snapshot(); + assertThat(snapshot, is(not(sameInstance(messagePackFactory)))); + assertThat(snapshot, is(instanceOf(MessagePackFactory.class))); + } + + @Test + public void testCopyWithAdvancedConfig() + throws IOException + { + ExtensionTypeCustomDeserializers extTypeCustomDesers = new ExtensionTypeCustomDeserializers(); + extTypeCustomDesers.addCustomDeser((byte) 42, + new ExtensionTypeCustomDeserializers.Deser() + { + @Override + public Object deserialize(byte[] data) + throws IOException + { + TinyPojo pojo = new TinyPojo(); + pojo.t = new String(data); + return pojo; + } + } + ); + + MessagePack.PackerConfig msgpackPackerConfig = new MessagePack.PackerConfig().withStr8FormatSupport(false); + + MessagePackFactory messagePackFactory = new MessagePackFactory(msgpackPackerConfig); + messagePackFactory.setExtTypeCustomDesers(extTypeCustomDesers); + + ObjectMapper objectMapper = new MessagePackMapper(messagePackFactory); + + // Use the original ObjectMapper in advance + { + byte[] bytes = objectMapper.writeValueAsBytes(1234); + assertThat(objectMapper.readValue(bytes, Integer.class), is(1234)); + } + + // Copy the factory + TokenStreamFactory copiedFactory = messagePackFactory.copy(); + assertThat(copiedFactory, is(instanceOf(MessagePackFactory.class))); + MessagePackFactory copiedMessagePackFactory = (MessagePackFactory) copiedFactory; + + assertThat(copiedMessagePackFactory.getPackerConfig().isStr8FormatSupport(), is(false)); + assertThat(copiedMessagePackFactory.getExtTypeCustomDesers().getDeser((byte) 42), is(notNullValue())); + assertThat(copiedMessagePackFactory.getExtTypeCustomDesers().getDeser((byte) 43), is(nullValue())); + + // Check the copied factory works fine + ObjectMapper copiedObjectMapper = new MessagePackMapper(copiedMessagePackFactory); + Map map = new HashMap<>(); + map.put("one", 1); + Map deserialized = copiedObjectMapper + .readValue(objectMapper.writeValueAsBytes(map), new TypeReference>() {}); + assertThat(deserialized.size(), is(1)); + assertThat(deserialized.get("one"), is(1)); + } +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java new file mode 100644 index 00000000..19c8a699 --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackGeneratorTest.java @@ -0,0 +1,1210 @@ +// +// 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.core.TokenStreamContext; +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 testBigDecimalCompareTo() + throws IOException + { + ObjectMapper mapper = new MessagePackMapper(new MessagePackFactory()); + + // BigDecimal with trailing zeros is representable as double — must not throw + BigDecimal trailingZeros = new BigDecimal("1.50"); + byte[] bytes = mapper.writeValueAsBytes(trailingZeros); + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes); + assertEquals(1.5, unpacker.unpackDouble(), 0.0); + + // BigDecimal with precision beyond double range must throw + BigDecimal tooHighPrecision = new BigDecimal("1.00000000000000000000000000000000000001"); + try { + mapper.writeValueAsBytes(tooHighPrecision); + assertTrue(false); + } + catch (IllegalArgumentException e) { + assertTrue(true); + } + } + + @Test + public void testEnableFeatureAutoCloseTarget() + throws IOException + { + OutputStream out = createTempFileOutputStream(); + ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory()); + List integers = Arrays.asList(1); + objectMapper.writeValue(out, integers); + assertThrows(JacksonException.class, () -> { + objectMapper.writeValue(out, integers); + }); + } + + @Test + public void testDisableFeatureAutoCloseTarget() + throws Exception + { + File tempFile = createTempFile(); + OutputStream out = new FileOutputStream(tempFile); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) + .build(); + List integers = Arrays.asList(1); + objectMapper.writeValue(out, integers); + objectMapper.writeValue(out, integers); + out.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(new FileInputStream(tempFile)); + assertEquals(1, unpacker.unpackArrayHeader()); + assertEquals(1, unpacker.unpackInt()); + assertEquals(1, unpacker.unpackArrayHeader()); + assertEquals(1, unpacker.unpackInt()); + } + + @Test + public void testWritePrimitiveObjectViaObjectMapper() + throws Exception + { + File tempFile = createTempFile(); + try (OutputStream out = Files.newOutputStream(tempFile.toPath())) { + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) + .build(); + objectMapper.writeValue(out, 1); + objectMapper.writeValue(out, "two"); + objectMapper.writeValue(out, 3.14); + objectMapper.writeValue(out, Arrays.asList(4)); + objectMapper.writeValue(out, 5L); + } + + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(new FileInputStream(tempFile))) { + assertEquals(1, unpacker.unpackInt()); + assertEquals("two", unpacker.unpackString()); + assertEquals(3.14, unpacker.unpackFloat(), 0.0001); + assertEquals(1, unpacker.unpackArrayHeader()); + assertEquals(4, unpacker.unpackInt()); + assertEquals(5, unpacker.unpackLong()); + } + } + + @Test + public void testInMultiThreads() + throws Exception + { + int threadCount = 8; + final int loopCount = 4000; + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + final ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .disable(StreamWriteFeature.AUTO_CLOSE_TARGET) + .build(); + final List buffers = new ArrayList(threadCount); + List> results = new ArrayList>(); + + for (int ti = 0; ti < threadCount; ti++) { + buffers.add(new ByteArrayOutputStream()); + final int threadIndex = ti; + results.add(executorService.submit(new Callable() + { + @Override + public Exception call() + throws Exception + { + try { + for (int i = 0; i < loopCount; i++) { + objectMapper.writeValue(buffers.get(threadIndex), threadIndex); + } + return null; + } + catch (Exception e) { + return e; + } + } + })); + } + + for (int ti = 0; ti < threadCount; ti++) { + Future exceptionFuture = results.get(ti); + Exception exception = exceptionFuture.get(20, TimeUnit.SECONDS); + if (exception != null) { + throw exception; + } + else { + try (ByteArrayOutputStream outputStream = buffers.get(ti); + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(outputStream.toByteArray())) { + for (int i = 0; i < loopCount; i++) { + assertEquals(ti, unpacker.unpackInt()); + } + } + } + } + } + + @Test + public void testDisableStr8Support() + throws Exception + { + String str8LengthString = new String(new char[32]).replace("\0", "a"); + + ObjectMapper defaultMapper = new MessagePackMapper(new MessagePackFactory()); + byte[] resultWithStr8Format = defaultMapper.writeValueAsBytes(str8LengthString); + assertEquals(resultWithStr8Format[0], MessagePack.Code.STR8); + + MessagePack.PackerConfig config = new MessagePack.PackerConfig().withStr8FormatSupport(false); + ObjectMapper mapperWithConfig = new MessagePackMapper(new MessagePackFactory(config)); + byte[] resultWithoutStr8Format = mapperWithConfig.writeValueAsBytes(str8LengthString); + assertNotEquals(resultWithoutStr8Format[0], MessagePack.Code.STR8); + } + + interface NonStringKeyMapHolder + { + Map getIntMap(); + + void setIntMap(Map intMap); + + Map getLongMap(); + + void setLongMap(Map longMap); + + Map getFloatMap(); + + void setFloatMap(Map floatMap); + + Map getDoubleMap(); + + void setDoubleMap(Map doubleMap); + + Map getBigIntMap(); + + void setBigIntMap(Map doubleMap); + } + + public static class NonStringKeyMapHolderWithAnnotation + implements NonStringKeyMapHolder + { + @JsonSerialize(keyUsing = MessagePackKeySerializer.class) + private Map intMap = new HashMap(); + + @JsonSerialize(keyUsing = MessagePackKeySerializer.class) + private Map longMap = new HashMap(); + + @JsonSerialize(keyUsing = MessagePackKeySerializer.class) + private Map floatMap = new HashMap(); + + @JsonSerialize(keyUsing = MessagePackKeySerializer.class) + private Map doubleMap = new HashMap(); + + @JsonSerialize(keyUsing = MessagePackKeySerializer.class) + private Map bigIntMap = new HashMap(); + + @Override + public Map getIntMap() + { + return intMap; + } + + @Override + public void setIntMap(Map intMap) + { + this.intMap = intMap; + } + + @Override + public Map getLongMap() + { + return longMap; + } + + @Override + public void setLongMap(Map longMap) + { + this.longMap = longMap; + } + + @Override + public Map getFloatMap() + { + return floatMap; + } + + @Override + public void setFloatMap(Map floatMap) + { + this.floatMap = floatMap; + } + + @Override + public Map getDoubleMap() + { + return doubleMap; + } + + @Override + public void setDoubleMap(Map doubleMap) + { + this.doubleMap = doubleMap; + } + + @Override + public Map getBigIntMap() + { + return bigIntMap; + } + + @Override + public void setBigIntMap(Map bigIntMap) + { + this.bigIntMap = bigIntMap; + } + } + + public static class NonStringKeyMapHolderWithoutAnnotation + implements NonStringKeyMapHolder + { + private Map intMap = new HashMap(); + + private Map longMap = new HashMap(); + + private Map floatMap = new HashMap(); + + private Map doubleMap = new HashMap(); + + private Map bigIntMap = new HashMap(); + + @Override + public Map getIntMap() + { + return intMap; + } + + @Override + public void setIntMap(Map intMap) + { + this.intMap = intMap; + } + + @Override + public Map getLongMap() + { + return longMap; + } + + @Override + public void setLongMap(Map longMap) + { + this.longMap = longMap; + } + + @Override + public Map getFloatMap() + { + return floatMap; + } + + @Override + public void setFloatMap(Map floatMap) + { + this.floatMap = floatMap; + } + + @Override + public Map getDoubleMap() + { + return doubleMap; + } + + @Override + public void setDoubleMap(Map doubleMap) + { + this.doubleMap = doubleMap; + } + + @Override + public Map getBigIntMap() + { + return bigIntMap; + } + + @Override + public void setBigIntMap(Map bigIntMap) + { + this.bigIntMap = bigIntMap; + } + } + + @Test + @SuppressWarnings("unchecked") + public void testNonStringKey() + throws Exception + { + for (Class clazz : + Arrays.asList( + NonStringKeyMapHolderWithAnnotation.class, + NonStringKeyMapHolderWithoutAnnotation.class)) { + NonStringKeyMapHolder mapHolder = clazz.getConstructor().newInstance(); + mapHolder.getIntMap().put(Integer.MAX_VALUE, "i"); + mapHolder.getLongMap().put(Long.MIN_VALUE, "l"); + mapHolder.getFloatMap().put(Float.MAX_VALUE, "f"); + mapHolder.getDoubleMap().put(Double.MIN_VALUE, "d"); + mapHolder.getBigIntMap().put(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE), "bi"); + + ObjectMapper objectMapper; + if (mapHolder instanceof NonStringKeyMapHolderWithoutAnnotation) { + SimpleModule mod = new SimpleModule("test"); + mod.addKeySerializer(Object.class, new MessagePackKeySerializer()); + objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(mod) + .build(); + } + else { + objectMapper = new MessagePackMapper(new MessagePackFactory()); + } + + byte[] bytes = objectMapper.writeValueAsBytes(mapHolder); + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes); + assertEquals(5, unpacker.unpackMapHeader()); + for (int i = 0; i < 5; i++) { + String keyName = unpacker.unpackString(); + assertThat(unpacker.unpackMapHeader(), is(1)); + if (keyName.equals("intMap")) { + assertThat(unpacker.unpackInt(), is(Integer.MAX_VALUE)); + assertThat(unpacker.unpackString(), is("i")); + } + else if (keyName.equals("longMap")) { + assertThat(unpacker.unpackLong(), is(Long.MIN_VALUE)); + assertThat(unpacker.unpackString(), is("l")); + } + else if (keyName.equals("floatMap")) { + assertThat(unpacker.unpackFloat(), is(Float.MAX_VALUE)); + assertThat(unpacker.unpackString(), is("f")); + } + else if (keyName.equals("doubleMap")) { + assertThat(unpacker.unpackDouble(), is(Double.MIN_VALUE)); + assertThat(unpacker.unpackString(), is("d")); + } + else if (keyName.equals("bigIntMap")) { + assertThat(unpacker.unpackBigInteger(), is(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE))); + assertThat(unpacker.unpackString(), is("bi")); + } + else { + fail("Unexpected key name: " + keyName); + } + } + } + } + + @Test + public void testComplexTypeKey() + throws IOException + { + HashMap map = new HashMap(); + TinyPojo pojo = new TinyPojo(); + pojo.t = "foo"; + map.put(pojo, 42); + + SimpleModule mod = new SimpleModule("test"); + mod.addKeySerializer(TinyPojo.class, new MessagePackKeySerializer()); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(mod) + .build(); + byte[] bytes = objectMapper.writeValueAsBytes(map); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes); + assertThat(unpacker.unpackMapHeader(), is(1)); + assertThat(unpacker.unpackMapHeader(), is(1)); + assertThat(unpacker.unpackString(), is("t")); + assertThat(unpacker.unpackString(), is("foo")); + assertThat(unpacker.unpackInt(), is(42)); + } + + @Test + public void testComplexTypeKeyWithV06Format() + throws IOException + { + HashMap map = new HashMap(); + TinyPojo pojo = new TinyPojo(); + pojo.t = "foo"; + map.put(pojo, 42); + + SimpleModule mod = new SimpleModule("test"); + mod.addKeySerializer(TinyPojo.class, new MessagePackKeySerializer()); + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .annotationIntrospector(new JsonArrayFormat()) + .addModule(mod) + .build(); + byte[] bytes = objectMapper.writeValueAsBytes(map); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes); + assertThat(unpacker.unpackMapHeader(), is(1)); + assertThat(unpacker.unpackArrayHeader(), is(1)); + assertThat(unpacker.unpackString(), is("foo")); + assertThat(unpacker.unpackInt(), is(42)); + } + + public static class IntegerSerializerStoringAsString + extends ValueSerializer + { + @Override + public void serialize(Integer value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException + { + gen.writeNumber(String.valueOf(value)); + } + } + + @Test + public void serializeStringAsInteger() + throws IOException + { + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule().addSerializer(Integer.class, new IntegerSerializerStoringAsString())) + .build(); + + assertThat( + MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(Integer.MAX_VALUE)).unpackInt(), + is(Integer.MAX_VALUE)); + } + + public static class LongSerializerStoringAsString + extends ValueSerializer + { + @Override + public void serialize(Long value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException + { + gen.writeNumber(String.valueOf(value)); + } + } + + @Test + public void serializeStringAsLong() + throws IOException + { + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule().addSerializer(Long.class, new LongSerializerStoringAsString())) + .build(); + + assertThat( + MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(Long.MIN_VALUE)).unpackLong(), + is(Long.MIN_VALUE)); + } + + public static class FloatSerializerStoringAsString + extends ValueSerializer + { + @Override + public void serialize(Float value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException + { + gen.writeNumber(String.valueOf(value)); + } + } + + @Test + public void serializeStringAsFloat() + throws IOException + { + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule().addSerializer(Float.class, new FloatSerializerStoringAsString())) + .build(); + + assertThat( + MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(Float.MAX_VALUE)).unpackFloat(), + is(Float.MAX_VALUE)); + } + + public static class DoubleSerializerStoringAsString + extends ValueSerializer + { + @Override + public void serialize(Double value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException + { + gen.writeNumber(String.valueOf(value)); + } + } + + @Test + public void serializeStringAsDouble() + throws IOException + { + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule().addSerializer(Double.class, new DoubleSerializerStoringAsString())) + .build(); + + assertThat( + MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(Double.MIN_VALUE)).unpackDouble(), + is(Double.MIN_VALUE)); + } + + public static class BigDecimalSerializerStoringAsString + extends ValueSerializer + { + @Override + public void serialize(BigDecimal value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException + { + gen.writeNumber(String.valueOf(value)); + } + } + + @Test + public void serializeStringAsBigDecimal() + throws IOException + { + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule().addSerializer(BigDecimal.class, new BigDecimalSerializerStoringAsString())) + .build(); + + BigDecimal bd = BigDecimal.valueOf(Long.MAX_VALUE).add(BigDecimal.ONE); + assertThat( + MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(bd)).unpackBigInteger(), + is(bd.toBigIntegerExact())); + } + + public static class BigIntegerSerializerStoringAsString + extends ValueSerializer + { + @Override + public void serialize(BigInteger value, JsonGenerator gen, SerializationContext serializers) + throws JacksonException + { + gen.writeNumber(String.valueOf(value)); + } + } + + @Test + public void serializeStringAsBigInteger() + throws IOException + { + ObjectMapper objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(new SimpleModule().addSerializer(BigInteger.class, new BigIntegerSerializerStoringAsString())) + .build(); + + BigInteger bi = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE); + assertThat( + MessagePack.newDefaultUnpacker(objectMapper.writeValueAsBytes(bi)).unpackBigInteger(), + is(bi)); + } + + @Test + public void testNestedSerialization() throws Exception + { + ObjectMapper objectMapper = new MessagePackMapper( + new MessagePackFactory().setReuseResourceInGenerator(false)); + OuterClass outerClass = objectMapper.readValue( + objectMapper.writeValueAsBytes(new OuterClass("Foo")), + OuterClass.class); + assertEquals("Foo", outerClass.getName()); + } + + static class OuterClass + { + private final String name; + + public OuterClass(@JsonProperty("name") String name) + { + this.name = name; + } + + public String getName() + { + ObjectMapper objectMapper = new MessagePackMapper(new MessagePackFactory()); + InnerClass innerClass = objectMapper.readValue( + objectMapper.writeValueAsBytes(new InnerClass("Bar")), + InnerClass.class); + assertEquals("Bar", innerClass.getName()); + + return name; + } + } + + static class InnerClass + { + private final String name; + + public InnerClass(@JsonProperty("name") String name) + { + this.name = name; + } + + public String getName() + { + return name; + } + } + + @Test + public void testIsClosedAfterClose() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + assertFalse(generator.isClosed()); + generator.writeStartArray(); + generator.writeEndArray(); + generator.close(); + assertTrue(generator.isClosed()); + } + + @Test + public void testGeneratorReusableAfterRootContainerClose() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeNumber(1); + generator.writeEndArray(); + generator.flush(); + + // Write a second root value; currentState must have reset to IN_ROOT + generator.writeStartObject(); + generator.writeName("k"); + generator.writeNumber(2); + generator.writeEndObject(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + assertEquals(1, unpacker.unpackArrayHeader()); + assertEquals(1, unpacker.unpackInt()); + assertEquals(1, unpacker.unpackMapHeader()); + assertEquals("k", unpacker.unpackString()); + assertEquals(2, unpacker.unpackInt()); + } + + @Test + public void testWriteRawStringWithOffset() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeRaw("XXhelloXX", 2, 5); // "hello" + generator.writeEndArray(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + unpacker.unpackArrayHeader(); + assertEquals("hello", unpacker.unpackString()); + } + + @Test + public void testWriteStringCharArrayWithOffset() + throws IOException + { + // Padding chars before/after the actual content to test non-zero offset + char[] buf = new char[] {'X', 'X', 'h', 'e', 'l', 'l', 'o', 'X'}; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeString(buf, 2, 5); // "hello" + generator.writeEndArray(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + unpacker.unpackArrayHeader(); + assertEquals("hello", unpacker.unpackString()); + } + + @Test + public void testWriteStringCharArrayWithOffsetNonAscii() + throws IOException + { + // Non-ASCII to exercise the non-fast-path in getBytesIfAscii + char[] buf = new char[] {'X', '三', '四', '五', 'X'}; // 三四五 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeString(buf, 1, 3); + generator.writeEndArray(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + unpacker.unpackArrayHeader(); + assertEquals("三四五", unpacker.unpackString()); + } + + @Test + public void testWriteUTF8StringWithOffset() + throws IOException + { + // Padding bytes before/after to test non-zero offset in writeUTF8String + byte[] buf = new byte[] {'X', 'X', 'h', 'i', 'X'}; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeUTF8String(buf, 2, 2); // "hi" + generator.writeEndArray(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + unpacker.unpackArrayHeader(); + assertEquals("hi", unpacker.unpackString()); + } + + @Test + public void testWriteBinaryWithOffset() + throws IOException + { + byte[] data = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04}; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + generator.writeBinary(data, 1, 3); // bytes 0x01, 0x02, 0x03 + generator.writeEndArray(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + unpacker.unpackArrayHeader(); + byte[] result = unpacker.readPayload(unpacker.unpackBinaryHeader()); + assertArrayEquals(new byte[] {0x01, 0x02, 0x03}, result); + } + + @Test + public void testWriteBinaryByteBufferWithOffset() + throws IOException + { + byte[] data = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04}; + ByteBuffer bb = ByteBuffer.wrap(data, 1, 3); // position=1, limit=4, remaining=3 + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + generator.writeStartArray(); + ObjectMapper mapper = new MessagePackMapper(factory); + mapper.writeValue(generator, bb); + generator.writeEndArray(); + generator.close(); + + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(baos.toByteArray()); + unpacker.unpackArrayHeader(); + byte[] result = unpacker.readPayload(unpacker.unpackBinaryHeader()); + assertArrayEquals(new byte[] {0x01, 0x02, 0x03}, result); + } + + @Test + public void testStreamWriteContext() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + + TokenStreamContext ctx = generator.streamWriteContext(); + assertNotEquals(null, ctx); + assertTrue(ctx.inRoot()); + + generator.writeStartArray(); + ctx = generator.streamWriteContext(); + assertTrue(ctx.inArray()); + + generator.writeStartObject(); + ctx = generator.streamWriteContext(); + assertTrue(ctx.inObject()); + + generator.writeName("k"); + assertEquals("k", ctx.currentName()); + + generator.writeNumber(1); + generator.writeEndObject(); + ctx = generator.streamWriteContext(); + assertTrue(ctx.inArray()); + + generator.writeEndArray(); + ctx = generator.streamWriteContext(); + assertTrue(ctx.inRoot()); + + generator.close(); + } + + @Test + public void testCurrentValue() + throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = factory.createGenerator(ObjectWriteContext.empty(), baos); + + Object pojo = new Object(); + generator.writeStartObject(pojo); + assertEquals(pojo, generator.currentValue()); + generator.writeName("k"); + generator.writeNumber(1); + generator.writeEndObject(); + generator.close(); + } + + @Test + public void testVersion() + { + assertNotEquals(null, factory.version()); + assertEquals("org.msgpack", factory.version().getGroupId()); + assertEquals("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/MessagePackMapperTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java new file mode 100644 index 00000000..776a1109 --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackMapperTest.java @@ -0,0 +1,117 @@ +// +// MessagePack for Java +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +package org.msgpack.jackson.dataformat; + +import tools.jackson.core.JacksonException; +import org.junit.Test; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class MessagePackMapperTest +{ + static class PojoWithBigInteger + { + public BigInteger value; + } + + static class PojoWithBigDecimal + { + public BigDecimal value; + } + + private void shouldFailToHandleBigInteger(MessagePackMapper messagePackMapper) throws JacksonException + { + PojoWithBigInteger obj = new PojoWithBigInteger(); + obj.value = BigInteger.valueOf(Long.MAX_VALUE).multiply(BigInteger.valueOf(10)); + + try { + messagePackMapper.writeValueAsBytes(obj); + fail(); + } + catch (IllegalArgumentException e) { + // Expected + } + } + + private void shouldSuccessToHandleBigInteger(MessagePackMapper messagePackMapper) throws IOException + { + PojoWithBigInteger obj = new PojoWithBigInteger(); + obj.value = BigInteger.valueOf(Long.MAX_VALUE).multiply(BigInteger.valueOf(10)); + + byte[] converted = messagePackMapper.writeValueAsBytes(obj); + + PojoWithBigInteger deserialized = messagePackMapper.readValue(converted, PojoWithBigInteger.class); + assertEquals(obj.value, deserialized.value); + } + + private void shouldFailToHandleBigDecimal(MessagePackMapper messagePackMapper) throws JacksonException + { + PojoWithBigDecimal obj = new PojoWithBigDecimal(); + obj.value = new BigDecimal("1234567890.98765432100"); + + try { + messagePackMapper.writeValueAsBytes(obj); + fail(); + } + catch (IllegalArgumentException e) { + // Expected + } + } + + private void shouldSuccessToHandleBigDecimal(MessagePackMapper messagePackMapper) throws IOException + { + PojoWithBigDecimal obj = new PojoWithBigDecimal(); + obj.value = new BigDecimal("1234567890.98765432100"); + + byte[] converted = messagePackMapper.writeValueAsBytes(obj); + + PojoWithBigDecimal deserialized = messagePackMapper.readValue(converted, PojoWithBigDecimal.class); + assertEquals(obj.value, deserialized.value); + } + + @Test + public void handleBigIntegerAsString() throws IOException + { + shouldFailToHandleBigInteger(new MessagePackMapper()); + shouldSuccessToHandleBigInteger(MessagePackMapper.builder() + .handleBigIntegerAsString() + .build()); + } + + @Test + public void handleBigDecimalAsString() throws IOException + { + shouldFailToHandleBigDecimal(new MessagePackMapper()); + shouldSuccessToHandleBigDecimal(MessagePackMapper.builder() + .handleBigDecimalAsString() + .build()); + } + + @Test + public void handleBigIntegerAndBigDecimalAsString() throws IOException + { + MessagePackMapper messagePackMapper = MessagePackMapper.builder() + .handleBigIntegerAndBigDecimalAsString() + .build(); + shouldSuccessToHandleBigInteger(messagePackMapper); + shouldSuccessToHandleBigDecimal(messagePackMapper); + } +} diff --git a/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java new file mode 100644 index 00000000..38eb7a0c --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/MessagePackParserTest.java @@ -0,0 +1,1111 @@ +// +// 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>() {}); + }); + } + + @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/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java new file mode 100644 index 00000000..36f5c97f --- /dev/null +++ b/msgpack-jackson3/src/test/java/org/msgpack/jackson/dataformat/TimestampExtensionModuleTest.java @@ -0,0 +1,217 @@ +// +// MessagePack for Java +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +package org.msgpack.jackson.dataformat; + +import tools.jackson.databind.ObjectMapper; +import org.junit.Before; +import org.junit.Test; +import org.msgpack.core.MessagePack; +import org.msgpack.core.MessagePacker; +import org.msgpack.core.MessageUnpacker; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.Instant; + +import static org.junit.Assert.assertEquals; + +public class TimestampExtensionModuleTest +{ + private ObjectMapper objectMapper; + private final SingleInstant singleInstant = new SingleInstant(); + private final TripleInstants tripleInstants = new TripleInstants(); + + private static class SingleInstant + { + public Instant instant; + } + + private static class TripleInstants + { + public Instant a; + public Instant b; + public Instant c; + } + + @Before + public void setUp() + throws Exception + { + objectMapper = MessagePackMapper.builder(new MessagePackFactory()) + .addModule(TimestampExtensionModule.INSTANCE) + .build(); + } + + @Test + public void testSingleInstantPojo() + throws IOException + { + singleInstant.instant = Instant.now(); + byte[] bytes = objectMapper.writeValueAsBytes(singleInstant); + SingleInstant deserialized = objectMapper.readValue(bytes, SingleInstant.class); + assertEquals(singleInstant.instant, deserialized.instant); + } + + @Test + public void testTripleInstantsPojo() + throws IOException + { + Instant now = Instant.now(); + tripleInstants.a = now.minusSeconds(1); + tripleInstants.b = now; + tripleInstants.c = now.plusSeconds(1); + byte[] bytes = objectMapper.writeValueAsBytes(tripleInstants); + TripleInstants deserialized = objectMapper.readValue(bytes, TripleInstants.class); + assertEquals(now.minusSeconds(1), deserialized.a); + assertEquals(now, deserialized.b); + assertEquals(now.plusSeconds(1), deserialized.c); + } + + @Test + public void serialize32BitFormat() + throws IOException + { + singleInstant.instant = Instant.ofEpochSecond(Instant.now().getEpochSecond()); + + byte[] bytes = objectMapper.writeValueAsBytes(singleInstant); + + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { + unpacker.unpackMapHeader(); + assertEquals("instant", unpacker.unpackString()); + assertEquals(4, unpacker.unpackExtensionTypeHeader().getLength()); + } + + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { + unpacker.unpackMapHeader(); + unpacker.unpackString(); + assertEquals(singleInstant.instant, unpacker.unpackTimestamp()); + } + } + + @Test + public void serialize64BitFormat() + throws IOException + { + singleInstant.instant = Instant.ofEpochSecond(Instant.now().getEpochSecond(), 1234); + + byte[] bytes = objectMapper.writeValueAsBytes(singleInstant); + + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { + unpacker.unpackMapHeader(); + assertEquals("instant", unpacker.unpackString()); + assertEquals(8, unpacker.unpackExtensionTypeHeader().getLength()); + } + + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { + unpacker.unpackMapHeader(); + unpacker.unpackString(); + assertEquals(singleInstant.instant, unpacker.unpackTimestamp()); + } + } + + @Test + public void serialize96BitFormat() + throws IOException + { + singleInstant.instant = Instant.ofEpochSecond(19880866800L /* 2600-01-01 */, 1234); + + byte[] bytes = objectMapper.writeValueAsBytes(singleInstant); + + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { + unpacker.unpackMapHeader(); + assertEquals("instant", unpacker.unpackString()); + assertEquals(12, unpacker.unpackExtensionTypeHeader().getLength()); + } + + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { + unpacker.unpackMapHeader(); + unpacker.unpackString(); + assertEquals(singleInstant.instant, unpacker.unpackTimestamp()); + } + } + + @Test + public void deserialize32BitFormat() + throws IOException + { + Instant instant = Instant.ofEpochSecond(Instant.now().getEpochSecond()); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(os)) { + packer.packMapHeader(1) + .packString("instant") + .packTimestamp(instant); + } + + byte[] bytes = os.toByteArray(); + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { + unpacker.unpackMapHeader(); + unpacker.unpackString(); + assertEquals(4, unpacker.unpackExtensionTypeHeader().getLength()); + } + + SingleInstant deserialized = objectMapper.readValue(bytes, SingleInstant.class); + assertEquals(instant, deserialized.instant); + } + + @Test + public void deserialize64BitFormat() + throws IOException + { + Instant instant = Instant.ofEpochSecond(Instant.now().getEpochSecond(), 1234); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(os)) { + packer.packMapHeader(1) + .packString("instant") + .packTimestamp(instant); + } + + byte[] bytes = os.toByteArray(); + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { + unpacker.unpackMapHeader(); + unpacker.unpackString(); + assertEquals(8, unpacker.unpackExtensionTypeHeader().getLength()); + } + + SingleInstant deserialized = objectMapper.readValue(bytes, SingleInstant.class); + assertEquals(instant, deserialized.instant); + } + + @Test + public void deserialize96BitFormat() + throws IOException + { + Instant instant = Instant.ofEpochSecond(19880866800L /* 2600-01-01 */, 1234); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(os)) { + packer.packMapHeader(1) + .packString("instant") + .packTimestamp(instant); + } + + byte[] bytes = os.toByteArray(); + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(bytes)) { + unpacker.unpackMapHeader(); + unpacker.unpackString(); + assertEquals(12, unpacker.unpackExtensionTypeHeader().getLength()); + } + + SingleInstant deserialized = objectMapper.readValue(bytes, SingleInstant.class); + assertEquals(instant, deserialized.instant); + } +} diff --git a/plans/preexisting-issues.md b/plans/preexisting-issues.md new file mode 100644 index 00000000..e986b53f --- /dev/null +++ b/plans/preexisting-issues.md @@ -0,0 +1,178 @@ +# Issues to address + +## msgpack-jackson3-specific + +### 1. `isClosed()` always returns false — FIXED + +**File:** `msgpack-jackson3/.../MessagePackGenerator.java` (close method) + +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 + +**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 — FIXED + +`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 + +**File:** `msgpack-jackson3/.../MessagePackGenerator.java` + +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 + +**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 + +**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:** +- `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 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 + +**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` + +**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 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 + +**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. 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 + +**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. + diff --git a/project/plugins.sbt b/project/plugins.sbt index 2d12cde5..28f255ba 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,5 +6,6 @@ addSbtPlugin("org.xerial.sbt" % "sbt-jcheckstyle" % "0.2.1") addSbtPlugin("com.github.sbt" % "sbt-osgi" % "0.10.0") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.1") addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.1") +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") scalacOptions ++= Seq("-deprecation", "-feature")