diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle index 962430e73..c7ce40b10 100644 --- a/benchmarks/build.gradle +++ b/benchmarks/build.gradle @@ -10,6 +10,7 @@ dependencies { annotationProcessor(mn.micronaut.inject.java) jmhAnnotationProcessor(libs.jmh.generator.annprocess) jmhAnnotationProcessor(mn.micronaut.inject.java) + jmhAnnotationProcessor(projects.micronautSerdeProcessor) implementation(projects.micronautSerdeJackson) implementation(projects.micronautSerdeSupport) @@ -28,3 +29,7 @@ jmh { shadowJar { mergeServiceFiles() } + +configurations.configureEach { + resolutionStrategy.preferProjectModules() +} diff --git a/benchmarks/src/jmh/java/io/micronaut/serde/JacksonBenchmark.java b/benchmarks/src/jmh/java/io/micronaut/serde/JacksonBenchmark.java index dadd6ae8f..8f2db6002 100644 --- a/benchmarks/src/jmh/java/io/micronaut/serde/JacksonBenchmark.java +++ b/benchmarks/src/jmh/java/io/micronaut/serde/JacksonBenchmark.java @@ -18,25 +18,19 @@ import io.micronaut.serde.data.StringField; import io.micronaut.serde.data.StringListConstructor; import io.micronaut.serde.data.StringListField; +import io.micronaut.serde.data.User; import io.micronaut.serde.data.Users; import io.micronaut.serde.data.UsersNoArrays; import io.micronaut.serde.jackson.JacksonJsonMapper; import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.Param; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.TearDown; -import org.openjdk.jmh.profile.AsyncProfiler; -import org.openjdk.jmh.profile.LinuxPerfAsmProfiler; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.options.Options; -import org.openjdk.jmh.runner.options.OptionsBuilder; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.concurrent.TimeUnit; public class JacksonBenchmark { @@ -65,6 +59,8 @@ public class JacksonBenchmark { private static final Argument INTEGER_ARRAY_CONSTRUCTOR_ARGUMENT = Argument.of(IntArrayConstructor.class); private static final Argument INTEGER_ARRAY_FIELD_ARGUMENT = Argument.of(IntArrayField.class); + private static final Argument USER_UNWRAPPED_ARGUMENT = Argument.of(User.class); + private static final byte[] HAYSTACK_6_6 = "{\"haystack\": [\"xniomb\", \"seelzp\", \"nzogdq\", \"omblsg\", \"idgtlm\", \"ydonzo\"], \"needle\": \"idg\"}".getBytes(StandardCharsets.UTF_8); private static final byte[] INPUT_USERS = """ {"users":[{"_id":"39771757156730064829","index":1031703887,"guid":"ifhsrU6geU4PijjDE8Q5","isActive":false,"balance":"TKl0GcwTs72S4CPx5rfg","picture":"FkKrg6ZOPC5REchlhixu5WgIl3gNAqq28iLtFm6dKfTSQs8d3P0cYxKsEvbvMB2C6BVgExop3khRlNSFE4SV8dVFitFs7RyyecN8","age":5,"eyeColor":"AY79Pw4sYByUZEMLxnYJ","name":"XjXrEZMuTvPnuOPBg7hL","gender":"VaMcuWBHvnWvIlCC9q4T","company":"6pmCe1LxouRGfZD79ena","email":"TboNtpmAS0ppZ07jITFE","phone":"j8OoUhtmwBlI20EgD1LS","address":"Aqo4fSYBpvvAWTDqbFbK","about":"1kXFSA2782BLqNBbKIbp","registered":"Mc7h3gZJcQ11ShGQYdXI","latitude":13.474549605725421,"longitude":35.010833129741435,"tags":["8tGfPhZkZD","XYmwuAAtZ4","u9iBDMpS9G","4udy1eRqme","Lg48Ogrf0I","zku019kVpo","iuIMkiZzog","MuI1uYeCjc","49n7qisFD8","TtVgWerCRh","H604QRJmi1","ZIQMfqInNH","CbDyjjA19F","pNFwPdkVdU","aPFLsUbIUh","fA735PT0Hd","00etYDYL87","mlyEf1lI2B","RQ05IJSzXF","3jJt0Zrkhw","ZINP8GH4Bm","XebX8UvviN","EXqZ9G0ATB","ssyzWZVAa2"],"friends":[{"id":"2668","name":"lcxeDXPbnoIxAPqTNdkwbcGIJxLnPe"},{"id":"9395","name":"dxNBbezfkbotyCmFzjodONShlGFaAg"},{"id":"5249","name":"fYHSDXScMSzQvxzFuuPHYWfyjdGQLg"},{"id":"4978","name":"qfoxPWmoWUyUduVkRwhzyBusuflrFY"},{"id":"9710","name":"vUAJwshFGLoBHfwLcsEVNLJLwdaCAg"},{"id":"7404","name":"BhVMdvhPRdpwpDWAmfhNDikncdNgGr"},{"id":"1343","name":"ZeDoizPcOBafZtVYDOmpzGoHekfoxf"},{"id":"7382","name":"KtqXeVdCQJlwSNHkgkxuoIGdOWrmqG"},{"id":"1365","name":"rCSTlgbmTAFhbSfPmnftcDLwdiKsHt"},{"id":"8037","name":"PUvwVYoSvSTnwjJCQITTcwNvMOpxie"},{"id":"4858","name":"cUfQfDIiyMfCMYBKGwhZSWnRRKwlxG"},{"id":"9141","name":"rJxMGOWRjdkphthcaKTspFrMcvcLLb"},{"id":"9128","name":"gcsYaolAQqrNMQTluIAKOkwYTWVUXe"},{"id":"2268","name":"jwXOUcXAiLurRlgTdxyKWvsbNHfFxl"},{"id":"5447","name":"whivfJXOdxoHtLIGpytTdbOXxlZpUY"},{"id":"7551","name":"whykuIjZUgvOFGpmNHjoPeTeYCPNby"},{"id":"719","name":"SmbiwQaORLdsbAlUZbQwgCKfuoPLVr"},{"id":"7773","name":"LZmRMXmXXHzlzFFJAopDNnWkuBqndD"},{"id":"9602","name":"xCNsDBFMygEwZuecJKTUrqeDLBJlrR"},{"id":"1536","name":"hrfeFnKnmVgZDDOxAHgXfgcJSRyiXB"},{"id":"3549","name":"NvvhXwWgCSaYijqhxsrxIWrHbBOOIa"}],"greeting":"hTAIJLspvLr8DJPG3jYh","favoriteFruit":"f6ZsZ3saRGKMBCZLAkiP"}]} @@ -74,6 +70,9 @@ public class JacksonBenchmark { private static final byte[] INPUT_STR_ARRAY_SHORT = "{\"strs\":[\"myString1\",\"myString2\"]}".getBytes(StandardCharsets.UTF_8); private static final byte[] INPUT_INT = "{\"integer\":123}".getBytes(StandardCharsets.UTF_8); private static final byte[] INPUT_INT_ARRAY = "{\"integers\":[123, 456]}".getBytes(StandardCharsets.UTF_8); + private static final byte[] INPUT_USER_UNWRAPPED = """ + {"id": 123, "firstName": "Foo", "lastName": "Bar"} + """.getBytes(StandardCharsets.UTF_8); @Benchmark public Object decodeInputConstructor(Holder holder) throws IOException { @@ -219,46 +218,44 @@ public Object decodeIntField(Holder holder) throws IOException { ); } - public static void main(String[] args) throws Exception { - ApplicationContext ctx = ApplicationContext.run(); - Holder holder = new Holder(); - holder.jsonMapper = ctx.getBean(JacksonJsonMapper.class); -// holder.jsonMapper = ctx.getBean(JacksonJsonMapper.class); - Object obj = new JacksonBenchmark().decodeUsers(holder); - Options opt = new OptionsBuilder() - //.include(JacksonBenchmark.class.getName() + ".*") - .include(JacksonBenchmark.class.getName() + ".decodeUsers$") - .warmupIterations(20) - .measurementIterations(10) - .mode(Mode.AverageTime) - .timeUnit(TimeUnit.NANOSECONDS) -// .addProfiler(AsyncProfiler.class, "libPath=/Users/denisstepanov/dev/async-profiler-2.9-macos/build/libasyncProfiler.dylib;output=flamegraph") -// .addProfiler(AsyncProfiler.class, "libPath=/Users/denisstepanov/dev/async-profiler-2.9-macos/build/libasyncProfiler.dylib;output=flamegraph") - //.addProfiler(AsyncProfiler.class, "libPath=/home/yawkat/bin/async-profiler-2.9-linux-x64/build/libasyncProfiler.so;output=flamegraph") - .addProfiler(LinuxPerfAsmProfiler.class, "intelSyntax=true;hotThreshold=0.05") - .forks(1) -// .jvmArgsAppend("-XX:+UnlockDiagnosticVMOptions", "-XX:+DebugNonSafepoints") -// .jvmArgsPrepend("-Dio.type.pollution.file=out.txt", "-javaagent:/Users/denisstepanov/dev/micronaut-core/type-pollution-agent-0.1-SNAPSHOT.jar") - .build(); - - new Runner(opt).run(); + @Benchmark + public Object decodeUnwrapped(Holder holder) throws IOException { + return holder.jsonMapper.readValue( + INPUT_USER_UNWRAPPED, + USER_UNWRAPPED_ARGUMENT + ); } - public static void mainx(String[] args) throws Exception { - ApplicationContext ctx = ApplicationContext.run(); - Holder holder = new Holder(); - holder.jsonMapper = ctx.getBean(JacksonJsonMapper.class); +// public static void main(String[] args) throws Exception { +// ApplicationContext ctx = ApplicationContext.run(); +// Holder holder = new Holder(); // holder.jsonMapper = ctx.getBean(JacksonJsonMapper.class); - Object obj = new JacksonBenchmark().decodeUsers(holder); - - System.out.println(obj); - } +//// holder.jsonMapper = ctx.getBean(JacksonJsonMapper.class); +//// Object obj = new JacksonBenchmark().decodeUsers(holder); +// Options opt = new OptionsBuilder() +// //.include(JacksonBenchmark.class.getName() + ".*") +// .include(JacksonBenchmark.class.getName() + ".decodeUnwrapped$") +// .warmupIterations(3) +// .measurementIterations(10) +// .mode(Mode.AverageTime) +// .timeUnit(TimeUnit.NANOSECONDS) +//// .addProfiler(AsyncProfiler.class, "libPath=/Users/denisstepanov/dev/async-profiler-2.9-macos/build/libasyncProfiler.dylib;output=flamegraph") +//// .addProfiler(AsyncProfiler.class, "libPath=/Users/denisstepanov/dev/async-profiler-2.9-macos/build/libasyncProfiler.dylib;output=flamegraph") +// //.addProfiler(AsyncProfiler.class, "libPath=/home/yawkat/bin/async-profiler-2.9-linux-x64/build/libasyncProfiler.so;output=flamegraph") +//// .addProfiler(LinuxPerfAsmProfiler.class, "intelSyntax=true;hotThreshold=0.05") +// .forks(1) +//// .jvmArgsAppend("-XX:+UnlockDiagnosticVMOptions", "-XX:+DebugNonSafepoints") +//// .jvmArgsPrepend("-Dio.type.pollution.file=out.txt", "-javaagent:/Users/denisstepanov/dev/micronaut-core/type-pollution-agent-0.1-SNAPSHOT.jar") +// .build(); +// +// new Runner(opt).run(); +// } @State(Scope.Thread) public static class Holder { @Param({ - //"JACKSON_DATABIND_INTROSPECTION", - //"JACKSON_DATABIND_REFLECTION", +// "JACKSON_DATABIND_INTROSPECTION", +// "JACKSON_DATABIND_REFLECTION", "SERDE_JACKSON" }) Stack stack = Stack.SERDE_JACKSON; diff --git a/benchmarks/src/jmh/java/io/micronaut/serde/data/Name.java b/benchmarks/src/jmh/java/io/micronaut/serde/data/Name.java new file mode 100644 index 000000000..599acd1b9 --- /dev/null +++ b/benchmarks/src/jmh/java/io/micronaut/serde/data/Name.java @@ -0,0 +1,7 @@ +package io.micronaut.serde.data; + +import io.micronaut.core.annotation.Introspected; + +@Introspected +public record Name(String firstName, String lastName) { +} diff --git a/benchmarks/src/jmh/java/io/micronaut/serde/data/User.java b/benchmarks/src/jmh/java/io/micronaut/serde/data/User.java new file mode 100644 index 000000000..975ee1937 --- /dev/null +++ b/benchmarks/src/jmh/java/io/micronaut/serde/data/User.java @@ -0,0 +1,27 @@ +package io.micronaut.serde.data; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.micronaut.core.annotation.Introspected; + +@Introspected +public class User { + private Long id; + @JsonUnwrapped + private Name name; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Name getName() { + return name; + } + + public void setName(Name name) { + this.name = name; + } +} diff --git a/serde-api/src/main/java/io/micronaut/serde/DelegatingDecoder.java b/serde-api/src/main/java/io/micronaut/serde/DelegatingDecoder.java new file mode 100644 index 000000000..d29ba698c --- /dev/null +++ b/serde-api/src/main/java/io/micronaut/serde/DelegatingDecoder.java @@ -0,0 +1,244 @@ +/* + * Copyright 2017-2023 original authors + * + * 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 + * + * https://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 io.micronaut.serde; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.type.Argument; +import io.micronaut.json.tree.JsonNode; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * Decoder that delegates to another decoder. + * + * @author Jonas Konrad + * @since 2.3.0 + */ +@SuppressWarnings("resource") +@Internal +public abstract class DelegatingDecoder implements Decoder { + protected abstract Decoder delegate() throws IOException; + + /** + * @return The delegated decode value + * @throws IOException + */ + public Decoder delegateForDecodeValue() throws IOException { + return delegate(); + } + + @Override + public @NonNull Decoder decodeArray(Argument type) throws IOException { + return delegate().decodeArray(type); + } + + @Override + public @NonNull Decoder decodeArray() throws IOException { + return delegate().decodeArray(); + } + + @Override + public boolean hasNextArrayValue() throws IOException { + return delegate().hasNextArrayValue(); + } + + @Override + public @NonNull Decoder decodeObject(@NonNull Argument type) throws IOException { + return delegate().decodeObject(type); + } + + @Override + public @NonNull Decoder decodeObject() throws IOException { + return delegate().decodeObject(); + } + + @Override + public @Nullable String decodeKey() throws IOException { + return delegate().decodeKey(); + } + + @Override + public @NonNull String decodeString() throws IOException { + return delegate().decodeString(); + } + + @Override + public @Nullable String decodeStringNullable() throws IOException { + return delegate().decodeStringNullable(); + } + + @Override + public boolean decodeBoolean() throws IOException { + return delegate().decodeBoolean(); + } + + @Override + public @Nullable Boolean decodeBooleanNullable() throws IOException { + return delegate().decodeBooleanNullable(); + } + + @Override + public byte decodeByte() throws IOException { + return delegate().decodeByte(); + } + + @Override + public @Nullable Byte decodeByteNullable() throws IOException { + return delegate().decodeByteNullable(); + } + + @Override + public short decodeShort() throws IOException { + return delegate().decodeShort(); + } + + @Override + public @Nullable Short decodeShortNullable() throws IOException { + return delegate().decodeShortNullable(); + } + + @Override + public char decodeChar() throws IOException { + return delegate().decodeChar(); + } + + @Override + public @Nullable Character decodeCharNullable() throws IOException { + return delegate().decodeCharNullable(); + } + + @Override + public int decodeInt() throws IOException { + return delegate().decodeInt(); + } + + @Override + public @Nullable Integer decodeIntNullable() throws IOException { + return delegate().decodeIntNullable(); + } + + @Override + public long decodeLong() throws IOException { + return delegate().decodeLong(); + } + + @Override + public @Nullable Long decodeLongNullable() throws IOException { + return delegate().decodeLongNullable(); + } + + @Override + public float decodeFloat() throws IOException { + return delegate().decodeFloat(); + } + + @Override + public @Nullable Float decodeFloatNullable() throws IOException { + return delegate().decodeFloatNullable(); + } + + @Override + public double decodeDouble() throws IOException { + return delegate().decodeDouble(); + } + + @Override + public @Nullable Double decodeDoubleNullable() throws IOException { + return delegate().decodeDoubleNullable(); + } + + @Override + public @NonNull BigInteger decodeBigInteger() throws IOException { + return delegate().decodeBigInteger(); + } + + @Override + public @Nullable BigInteger decodeBigIntegerNullable() throws IOException { + return delegate().decodeBigIntegerNullable(); + } + + @Override + public @NonNull BigDecimal decodeBigDecimal() throws IOException { + return delegate().decodeBigDecimal(); + } + + @Override + public @Nullable BigDecimal decodeBigDecimalNullable() throws IOException { + return delegate().decodeBigDecimalNullable(); + } + + @Override + public byte @NonNull [] decodeBinary() throws IOException { + return delegate().decodeBinary(); + } + + @Override + public byte @Nullable [] decodeBinaryNullable() throws IOException { + return delegate().decodeBinaryNullable(); + } + + @Override + public boolean decodeNull() throws IOException { + return delegate().decodeNull(); + } + + @Override + public @Nullable Object decodeArbitrary() throws IOException { + return delegate().decodeArbitrary(); + } + + @Override + public @NonNull JsonNode decodeNode() throws IOException { + return delegate().decodeNode(); + } + + @Override + public Decoder decodeBuffer() throws IOException { + return delegate().decodeBuffer(); + } + + @Override + public void skipValue() throws IOException { + delegate().skipValue(); + } + + @Override + public void finishStructure() throws IOException { + delegate().finishStructure(); + } + + @Override + public void finishStructure(boolean consumeLeftElements) throws IOException { + delegate().finishStructure(consumeLeftElements); + } + + @Override + public void close() throws IOException { + delegate().close(); + } + + /** + * This method remains abstract because it doesn't throw IOException and thus can't call decoder(). + *
+ * {@inheritDoc} + */ + @Override + public abstract @NonNull IOException createDeserializationException(@NonNull String message, @Nullable Object invalidValue); +} diff --git a/serde-bson/src/main/java/io/micronaut/serde/bson/BsonReaderDecoder.java b/serde-bson/src/main/java/io/micronaut/serde/bson/BsonReaderDecoder.java index 33ad74300..843709b97 100644 --- a/serde-bson/src/main/java/io/micronaut/serde/bson/BsonReaderDecoder.java +++ b/serde-bson/src/main/java/io/micronaut/serde/bson/BsonReaderDecoder.java @@ -185,40 +185,24 @@ protected String getCurrentKey() { @Override protected String coerceScalarToString(TokenType currentToken) throws IOException { - switch (currentBsonType) { - case DOUBLE: - return String.valueOf(bsonReader.readDouble()); - case STRING: - return bsonReader.readString(); - case OBJECT_ID: - return bsonReader.readObjectId().toHexString(); - case BOOLEAN: - return String.valueOf(bsonReader.readBoolean()); - case DATE_TIME: - return String.valueOf(bsonReader.readDateTime()); - case REGULAR_EXPRESSION: - return bsonReader.readRegularExpression().toString(); - case JAVASCRIPT: - return bsonReader.readJavaScript(); - case SYMBOL: - return bsonReader.readSymbol(); - case JAVASCRIPT_WITH_SCOPE: - return bsonReader.readJavaScriptWithScope(); - case INT32: - return String.valueOf(bsonReader.readInt32()); - case TIMESTAMP: - return bsonReader.readTimestamp().toString(); - case INT64: - return String.valueOf(bsonReader.readInt64()); - case DECIMAL128: - return bsonReader.readDecimal128().toString(); - case BINARY: - return new String(bsonReader.readBinaryData().getData(), StandardCharsets.UTF_8); - case DB_POINTER: - return bsonReader.readDBPointer().toString(); - default: - throw new SerdeException("Can't decode " + currentBsonType + " as string"); - } + return switch (currentBsonType) { + case DOUBLE -> String.valueOf(bsonReader.readDouble()); + case STRING -> bsonReader.readString(); + case OBJECT_ID -> bsonReader.readObjectId().toHexString(); + case BOOLEAN -> String.valueOf(bsonReader.readBoolean()); + case DATE_TIME -> String.valueOf(bsonReader.readDateTime()); + case REGULAR_EXPRESSION -> bsonReader.readRegularExpression().toString(); + case JAVASCRIPT -> bsonReader.readJavaScript(); + case SYMBOL -> bsonReader.readSymbol(); + case JAVASCRIPT_WITH_SCOPE -> bsonReader.readJavaScriptWithScope(); + case INT32 -> String.valueOf(bsonReader.readInt32()); + case TIMESTAMP -> bsonReader.readTimestamp().toString(); + case INT64 -> String.valueOf(bsonReader.readInt64()); + case DECIMAL128 -> bsonReader.readDecimal128().toString(); + case BINARY -> new String(bsonReader.readBinaryData().getData(), StandardCharsets.UTF_8); + case DB_POINTER -> bsonReader.readDBPointer().toString(); + default -> throw new SerdeException("Can't decode " + currentBsonType + " as string"); + }; } @Override @@ -238,82 +222,61 @@ protected boolean getBoolean() { @Override protected long getLong() { - switch (currentBsonType) { - case INT32: - return bsonReader.readInt32(); - case INT64: - return bsonReader.readInt64(); - case DOUBLE: - return (long) bsonReader.readDouble(); - case DECIMAL128: - return bsonReader.readDecimal128().longValue(); - default: - throw new IllegalStateException("Not in number state"); - } + return switch (currentBsonType) { + case INT32 -> bsonReader.readInt32(); + case INT64 -> bsonReader.readInt64(); + case DOUBLE -> (long) bsonReader.readDouble(); + case DECIMAL128 -> bsonReader.readDecimal128().longValue(); + default -> throw getNotInNumberState(); + }; + } + + private IllegalStateException getNotInNumberState() { + return new IllegalStateException("Not in number state"); } @Override protected double getDouble() { - switch (currentBsonType) { - case INT32: - return bsonReader.readInt32(); - case INT64: - return bsonReader.readInt64(); - case DOUBLE: - return bsonReader.readDouble(); - case DECIMAL128: - return bsonReader.readDecimal128().doubleValue(); - default: - throw new IllegalStateException("Not in number state"); - } + return switch (currentBsonType) { + case INT32 -> bsonReader.readInt32(); + case INT64 -> bsonReader.readInt64(); + case DOUBLE -> bsonReader.readDouble(); + case DECIMAL128 -> bsonReader.readDecimal128().doubleValue(); + default -> throw getNotInNumberState(); + }; } @Override protected BigInteger getBigInteger() { - switch (currentBsonType) { - case INT32: - return BigInteger.valueOf(bsonReader.readInt32()); - case INT64: - return BigInteger.valueOf(bsonReader.readInt64()); - case DOUBLE: - return BigDecimal.valueOf(bsonReader.readDouble()).toBigInteger(); - case DECIMAL128: - return bsonReader.readDecimal128().bigDecimalValue().toBigInteger(); - default: - throw new IllegalStateException("Not in number state"); - } + return switch (currentBsonType) { + case INT32 -> BigInteger.valueOf(bsonReader.readInt32()); + case INT64 -> BigInteger.valueOf(bsonReader.readInt64()); + case DOUBLE -> BigDecimal.valueOf(bsonReader.readDouble()).toBigInteger(); + case DECIMAL128 -> bsonReader.readDecimal128().bigDecimalValue().toBigInteger(); + default -> throw getNotInNumberState(); + }; } @Override protected BigDecimal getBigDecimal() { - switch (currentBsonType) { - case INT32: - return BigDecimal.valueOf(bsonReader.readInt32()); - case INT64: - return BigDecimal.valueOf(bsonReader.readInt64()); - case DOUBLE: - return BigDecimal.valueOf(bsonReader.readDouble()); - case DECIMAL128: - return bsonReader.readDecimal128().bigDecimalValue(); - default: - throw new IllegalStateException("Not in number state"); - } + return switch (currentBsonType) { + case INT32 -> BigDecimal.valueOf(bsonReader.readInt32()); + case INT64 -> BigDecimal.valueOf(bsonReader.readInt64()); + case DOUBLE -> BigDecimal.valueOf(bsonReader.readDouble()); + case DECIMAL128 -> bsonReader.readDecimal128().bigDecimalValue(); + default -> throw getNotInNumberState(); + }; } @Override protected Number getBestNumber() { - switch (currentBsonType) { - case INT32: - return bsonReader.readInt32(); - case INT64: - return bsonReader.readInt64(); - case DOUBLE: - return bsonReader.readDouble(); - case DECIMAL128: - return bsonReader.readDecimal128(); - default: - throw new IllegalStateException("Not in number state"); - } + return switch (currentBsonType) { + case INT32 -> bsonReader.readInt32(); + case INT64 -> bsonReader.readInt64(); + case DOUBLE -> bsonReader.readDouble(); + case DECIMAL128 -> bsonReader.readDecimal128(); + default -> throw getNotInNumberState(); + }; } @Override @@ -342,18 +305,13 @@ public IOException createDeserializationException(String message, Object invalid } private Decimal128 getDecimal128() { - switch (currentBsonType) { - case INT32: - return new Decimal128(bsonReader.readInt32()); - case INT64: - return new Decimal128(bsonReader.readInt64()); - case DOUBLE: - return new Decimal128(BigDecimal.valueOf(bsonReader.readDouble())); - case DECIMAL128: - return bsonReader.readDecimal128(); - default: - throw new IllegalStateException("Not in number state"); - } + return switch (currentBsonType) { + case INT32 -> new Decimal128(bsonReader.readInt32()); + case INT64 -> new Decimal128(bsonReader.readInt64()); + case DOUBLE -> new Decimal128(BigDecimal.valueOf(bsonReader.readDouble())); + case DECIMAL128 -> bsonReader.readDecimal128(); + default -> throw getNotInNumberState(); + }; } /** @@ -409,13 +367,17 @@ private byte[] copyValueToDocument() { return buffer.getInternalBuffer(); } + private Decoder decoderFromBytes(byte[] documentBytes) throws IOException { + BsonReaderDecoder topDecoder = new BsonReaderDecoder(new BsonBinaryReader(ByteBuffer.wrap(documentBytes)), ourLimits()); + Decoder decoder = topDecoder.decodeObject(); + decoder.decodeKey(); // Unwrap + return decoder; + } + @Override public Decoder decodeBuffer() throws IOException { byte[] documentBytes = decodeCustom(p -> ((BsonReaderDecoder) p).copyValueToDocument()); - BsonReaderDecoder topDecoder = new BsonReaderDecoder(new BsonBinaryReader(ByteBuffer.wrap(documentBytes)), ourLimits()); - Decoder objectDecoder = topDecoder.decodeObject(); - objectDecoder.decodeKey(); // skip key - return objectDecoder; + return decoderFromBytes(documentBytes); } private static void transfer(BsonReader src, BsonWriter dest, BsonType type) { @@ -427,7 +389,19 @@ private static void transfer(BsonReader src, BsonWriter dest, BsonType type) { dest.writeString(src.readString()); break; case DOCUMENT: - dest.pipe(src); + src.readStartDocument(); + dest.writeStartDocument(); + while (src.readBsonType() != BsonType.END_OF_DOCUMENT) { + String name = src.readName(); + if (name != null) { + dest.writeName(name); + } else { + break; + } + transfer(src, dest, src.getCurrentBsonType()); + } + src.readEndDocument(); + dest.writeEndDocument(); break; case ARRAY: src.readStartArray(); diff --git a/serde-bson/src/main/java/io/micronaut/serde/bson/custom/AbstractBsonSerde.java b/serde-bson/src/main/java/io/micronaut/serde/bson/custom/AbstractBsonSerde.java index 4821f67d6..1b02f515e 100644 --- a/serde-bson/src/main/java/io/micronaut/serde/bson/custom/AbstractBsonSerde.java +++ b/serde-bson/src/main/java/io/micronaut/serde/bson/custom/AbstractBsonSerde.java @@ -17,6 +17,7 @@ import io.micronaut.core.type.Argument; import io.micronaut.serde.Decoder; +import io.micronaut.serde.DelegatingDecoder; import io.micronaut.serde.Encoder; import io.micronaut.serde.Serde; import io.micronaut.serde.bson.BsonReaderDecoder; @@ -45,9 +46,12 @@ public final void serialize(Encoder encoder, EncoderContext context, Argumentdemuxes an object: The same object can be iterated over multiple + * times with multiple decoders (concurrently or sequentially, though multi-threading is not + * allowed). Properties that are skipped by one decoder can be read by another. + *
+ * The use for this is subtype detection: An object is iterated over once to find the {@code type} + * property to detect the subtype, and then the remaining properties (before and after the type) + * are deserialized as normal. + * + * @author Jonas Konrad + * @since 2.2.7 + */ +@Internal +final class DemuxingObjectDecoder extends DelegatingDecoder { + private final DemuxerState state; + private int nextKeyIndex; + + private DemuxingObjectDecoder(DemuxerState state) { + this.state = state; + state.outputCount++; + } + + /** + * Create a new primed decoder that can decode the same object multiple times. This + * decoder is very restricted: It must be {@link AutoCloseable#close() closed} after + * use, and it only supports {@link #decodeObject()}. Each {@link #decodeObject()} call + * returns a decoder of the same object. + * + *
{@code
+     * try (Decoder primed = DemuxingObjectDecoder.prime(...)) {
+     *     Decoder d1 = primed.decodeObject();
+     *     decodeSomeProperties(d1);
+     *     d1.finishStructure(true);
+     *
+     *     Decoder d2 = primed.decodeObject();
+     *     decodeOtherProperties(d2);
+     *     d2.finishStructure(true);
+     * }
+     * }
+ * + * @param decoder The input to read from. The primed decoder will call {@link #decodeObject()} + * on this input exactly once + * @return The primed decoder + */ + public static Decoder prime(Decoder decoder) { + return new PrimedDecoder(decoder); + } + + @Override + public @Nullable String decodeKey() throws IOException { + DemuxerState.Entry entry; + do { + entry = state.getEntry(nextKeyIndex++); + if (entry == null) { + // end of object + return null; + } + } while (entry.consumed); + return entry.key; + } + + @NonNull + private DemuxerState.Entry entryForValue() throws IOException { + if (nextKeyIndex == 0) { + throw new IllegalStateException("Must call decodeKey first"); + } + DemuxerState.Entry entry = state.getEntry(nextKeyIndex - 1); + if (entry == null) { + throw new IllegalStateException("End of object, decodeKey should have returned null"); + } + return entry; + } + + @Override + protected Decoder delegate() throws IOException { + return entryForValue().consume(); + } + + @Override + public boolean decodeNull() throws IOException { + return entryForValue().decodeNull(); + } + + @Override + public void skipValue() throws IOException { + // normal checks, but don't consume the entry + entryForValue(); + } + + @Override + public void finishStructure() throws IOException { + finishStructure(false); + } + + @Override + public void finishStructure(boolean consumeLeftElements) throws IOException { + if (!consumeLeftElements && + // if nextKeyIndex > buffer.size, then decodeKey already returned null before + nextKeyIndex <= state.buffer.size() && + // decodeKey will iterate over remaining entries, skipping those that are consumed by other streams + decodeKey() != null) { + + throw new IllegalStateException("Not all items consumed"); + } + state.removeOutput(); + } + + @Override + public boolean hasNextArrayValue() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void close() throws IOException { + finishStructure(true); + } + + @Override + public @NonNull IOException createDeserializationException(@NonNull String message, @Nullable Object invalidValue) { + return state.delegate.createDeserializationException(message, invalidValue); + } + + private static class DemuxerState { + private final Decoder delegate; + private final List buffer = new ArrayList<>(); + private boolean hitEnd = false; + private int outputCount = 0; + + DemuxerState(Decoder delegate) { + this.delegate = delegate; + } + + Entry getEntry(int i) throws IOException { + if (buffer.size() > i) { + return buffer.get(i); + } else { + if (buffer.size() != i) { + throw new IllegalArgumentException("Must access entries in sequence"); + } + if (hitEnd) { + return null; + } + if (!buffer.isEmpty()) { + Entry lastEntry = buffer.get(buffer.size() - 1); + if (!lastEntry.consumed && lastEntry.buffer == null) { + lastEntry.buffer = delegate.decodeBuffer(); + } + } + String key = delegate.decodeKey(); + if (key == null) { + hitEnd = true; + return null; + } + Entry entry = new Entry(key); + buffer.add(entry); + return entry; + } + } + + void removeOutput() throws IOException { + if (--outputCount == 0) { + delegate.finishStructure(true); + } + } + + private class Entry { + final String key; + Decoder buffer = null; + boolean consumed = false; + + Entry(String key) { + this.key = key; + } + + Decoder consume() { + if (consumed) { + throw new IllegalStateException("Entry already consumed"); + } + consumed = true; + if (buffer != null) { + return buffer; + } else { + return delegate; + } + } + + boolean decodeNull() throws IOException { + // this call has the expectation that a proper consume() will follow + if (consumed) { + throw new IllegalStateException("Entry already consumed"); + } + if (buffer != null) { + return buffer.decodeNull(); + } else { + return delegate.decodeNull(); + } + } + } + } + + private static class PrimedDecoder extends DelegatingDecoder { + private final Decoder delegate; + @Nullable + private DemuxerState state; + + PrimedDecoder(Decoder delegate) { + this.delegate = delegate; + } + + @Override + public @NonNull DemuxingObjectDecoder decodeObject() throws IOException { + if (state == null) { + state = new DemuxerState(delegate.decodeObject()); + state.outputCount++; + } + return new DemuxingObjectDecoder(state); + } + + @Override + public @NonNull DemuxingObjectDecoder decodeObject(@NonNull Argument type) throws IOException { + if (state == null) { + state = new DemuxerState(delegate.decodeObject(type)); + state.outputCount++; + } + return new DemuxingObjectDecoder(state); + } + + @Override + public boolean decodeNull() throws IOException { + return false; + } + + @Override + protected Decoder delegate() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public @NonNull IOException createDeserializationException(@NonNull String message, @Nullable Object invalidValue) { + return delegate.createDeserializationException(message, invalidValue); + } + + @Override + public void close() throws IOException { + if (state == null) { + state = new DemuxerState(delegate.decodeObject()); + state.outputCount++; + } + state.removeOutput(); + } + } +} diff --git a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java index a712ba677..a78e6ef89 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java @@ -15,6 +15,7 @@ */ package io.micronaut.serde.support.deserializers; +import io.micronaut.context.annotation.DefaultImplementation; import io.micronaut.core.annotation.AnnotatedElement; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Creator; @@ -55,13 +56,15 @@ import java.util.OptionalLong; import java.util.function.BiConsumer; +import static io.micronaut.serde.config.annotation.SerdeConfig.SerSubtyped.DiscriminatorValueKind.CLASS_NAME; + /** * Holder for data about a deserializable bean. * * @param The generic type */ @Internal -class DeserBean { +final class DeserBean { private static final String JK_PROP = "com.fasterxml.jackson.annotation.JsonProperty"; // CHECKSTYLE:OFF @@ -70,17 +73,20 @@ class DeserBean { @Nullable public final PropertiesBag creatorParams; @Nullable - public final DerProperty[] creatorUnwrapped; + public final DerProperty[] creatorUnwrapped; @Nullable - public final PropertiesBag readProperties; + public final PropertiesBag injectProperties; @Nullable public final DerProperty[] unwrappedProperties; @Nullable public final AnySetter anySetter; @Nullable public final String wrapperProperty; + @Nullable + public final SubtypeInfo subtypeInfo; public final int creatorSize; + public final int injectPropertiesSize; public final boolean ignoreUnknown; public final boolean delegating; @@ -90,22 +96,32 @@ class DeserBean { public final boolean hasBuilder; public final ConversionService conversionService; + private final Map> bounds; + private volatile boolean initialized; private volatile boolean initializing; // CHECKSTYLE:ON - public DeserBean( - BeanIntrospection introspection, - Deserializer.DecoderContext decoderContext, - DeserBeanRegistry deserBeanRegistry) - throws SerdeException { + public DeserBean(Argument type, + BeanIntrospection introspection, + Deserializer.DecoderContext decoderContext, + DeserBeanRegistry deserBeanRegistry, + @Nullable String propertiesNamePrefix, @Nullable String propertiesNameSuffix) + throws SerdeException { + + if (type.hasTypeVariables()) { + bounds = type.getTypeVariables(); + } else { + bounds = Collections.emptyMap(); + } + this.conversionService = decoderContext.getConversionService(); this.introspection = introspection; final SerdeConfig.SerCreatorMode creatorMode = introspection - .getConstructor().getAnnotationMetadata() - .enumValue(Creator.class, "mode", SerdeConfig.SerCreatorMode.class) - .orElse(null); + .getConstructor().getAnnotationMetadata() + .enumValue(Creator.class, "mode", SerdeConfig.SerCreatorMode.class) + .orElse(null); delegating = creatorMode == SerdeConfig.SerCreatorMode.DELEGATING; hasBuilder = introspection.hasBuilder(); final Argument[] constructorArguments = hasBuilder ? introspection.builder().getBuildMethodArguments() : introspection.getConstructorArguments(); @@ -127,86 +143,66 @@ public DeserBean( anySetterValue = new AnySetter<>(constructorArgument, i); final String n = constructorArgument.getName(); creatorPropertiesBuilder.register( + n, + new DerProperty<>( + conversionService, + introspection, + i, n, - new DerProperty<>( - conversionService, - introspection, - i, - n, - constructorArgument, - null, - null, - null - ), - false + constructorArgument, + null, + null, + null, + null + ), + false ); continue; } PropertyNamingStrategy propertyNamingStrategy = getPropertyNamingStrategy(annotationMetadata, decoderContext, entityPropertyNamingStrategy); - final String propertyName = resolveName(constructorArgument, annotationMetadata, propertyNamingStrategy); + final String propertyName = resolveName(propertiesNamePrefix, propertiesNameSuffix, constructorArgument, annotationMetadata, propertyNamingStrategy); Argument constructorWithPropertyArgument = Argument.of( - constructorArgument.getType(), - constructorArgument.getName(), - annotationMetadata, - constructorArgument.getTypeParameters() + constructorArgument.getType(), + constructorArgument.getName(), + annotationMetadata, + constructorArgument.getTypeParameters() ); final boolean isUnwrapped = annotationMetadata.hasAnnotation(SerdeConfig.SerUnwrapped.class); - final DerProperty derProperty; + DeserBean unwrapped = null; if (isUnwrapped) { - if (creatorUnwrapped == null) { - creatorUnwrapped = new ArrayList<>(); - } - - final DeserBean unwrapped = deserBeanRegistry.getDeserializableBean( - constructorArgument, - decoderContext - ); - creatorUnwrapped.add(new DerProperty( - conversionService, - introspection, - i, - propertyName, - constructorWithPropertyArgument, - null, - null, - unwrapped - )); String prefix = annotationMetadata.stringValue(SerdeConfig.SerUnwrapped.class, SerdeConfig.SerUnwrapped.PREFIX).orElse(""); String suffix = annotationMetadata.stringValue(SerdeConfig.SerUnwrapped.class, SerdeConfig.SerUnwrapped.SUFFIX).orElse(""); - - final PropertiesBag unwrappedCreatorParams = unwrapped.creatorParams; - if (unwrappedCreatorParams != null) { - for (Map.Entry> e : unwrappedCreatorParams.getProperties()) { - String resolved = prefix + e.getKey() + suffix; - //noinspection unchecked - creatorPropertiesBuilder.register(resolved, (DerProperty) e.getValue(), false); - } + if (propertiesNamePrefix != null) { + prefix = propertiesNamePrefix + prefix; } - - derProperty = new DerProperty<>( - conversionService, - introspection, - i, - propertyName, - constructorWithPropertyArgument, - null, - null, - unwrapped - ); - - } else { - derProperty = new DerProperty<>( - conversionService, - introspection, - i, - propertyName, - constructorWithPropertyArgument, - introspection.getProperty(propertyName).orElse(null), - null, - null + if (propertiesNameSuffix != null) { + suffix = suffix + propertiesNameSuffix; + } + unwrapped = deserBeanRegistry.getWrappedDeserializableBean( + constructorArgument, + prefix, + suffix, + decoderContext ); } + DerProperty derProperty = new DerProperty<>( + conversionService, + introspection, + i, + propertyName, + constructorWithPropertyArgument, + introspection.getProperty(propertyName).orElse(null), + null, + unwrapped, + null + ); + if (isUnwrapped) { + if (creatorUnwrapped == null) { + creatorUnwrapped = new ArrayList<>(); + } + creatorUnwrapped.add(derProperty); + } creatorPropertiesBuilder.register(propertyName, derProperty, true); } @@ -222,9 +218,9 @@ public DeserBean( PropertyNamingStrategy propertyNamingStrategy = getPropertyNamingStrategy(annotationMetadata, decoderContext, entityPropertyNamingStrategy); final String jsonProperty = resolveName( builderArgument, - matchingOuterProperty - .map(outer -> List.of(annotationMetadata, outer.getAnnotationMetadata())) - .orElse(List.of(annotationMetadata)), + matchingOuterProperty + .map(outer -> List.of(annotationMetadata, outer.getAnnotationMetadata())) + .orElse(List.of(annotationMetadata)), propertyNamingStrategy ); final DerProperty derProperty = new DerProperty<>( @@ -235,11 +231,13 @@ public DeserBean( builderArgument, null, null, + null, null ); readPropertiesBuilder.register(jsonProperty, derProperty, true); } - readProperties = readPropertiesBuilder.build(); + injectProperties = readPropertiesBuilder.build(); + injectPropertiesSize = injectProperties.getProperties().size(); } else { final List> beanProperties = introspection.getBeanProperties() @@ -274,70 +272,50 @@ public DeserBean( anySetterValue = new AnySetter(beanProperty); } else { final boolean isUnwrapped = annotationMetadata.hasAnnotation(SerdeConfig.SerUnwrapped.class); - final Argument t = resolveArgument(beanProperty.asArgument()); + final Argument propertyArgument = resolveArgument(beanProperty.asArgument()); + DeserBean unwrapped = null; if (isUnwrapped) { - if (unwrappedProperties == null) { - unwrappedProperties = new ArrayList<>(); - } - final DeserBean unwrapped = deserBeanRegistry.getDeserializableBean( - t, - decoderContext - ); - final AnnotationMetadataHierarchy combinedMetadata = - new AnnotationMetadataHierarchy(annotationMetadata, - t.getAnnotationMetadata()); - unwrappedProperties.add(new DerProperty<>( - conversionService, - introspection, - i, - t.getName(), - t, - combinedMetadata, - beanProperty, - null, - unwrapped - )); String prefix = annotationMetadata.stringValue(SerdeConfig.SerUnwrapped.class, SerdeConfig.SerUnwrapped.PREFIX).orElse(""); String suffix = annotationMetadata.stringValue(SerdeConfig.SerUnwrapped.class, SerdeConfig.SerUnwrapped.SUFFIX).orElse(""); - - PropertiesBag unwrappedProps = (PropertiesBag) unwrapped.readProperties; - if (unwrappedProps != null) { - for (Map.Entry> e : unwrappedProps.getProperties()) { - String resolved = prefix + e.getKey() + suffix; - readPropertiesBuilder.register(resolved, e.getValue(), false); - - } + if (propertiesNamePrefix != null) { + prefix = propertiesNamePrefix + prefix; } - final PropertiesBag unwrappedCreatorParams = unwrapped.creatorParams; - if (unwrappedCreatorParams != null) { - for (Map.Entry> e : unwrappedCreatorParams.getProperties()) { - String resolved = prefix + e.getKey() + suffix; - //noinspection unchecked - creatorPropertiesBuilder.register(resolved, (DerProperty) e.getValue(), false); - } + if (propertiesNameSuffix != null) { + suffix = suffix + propertiesNameSuffix; } - } else { - - final String jsonProperty = resolveName(beanProperty, annotationMetadata, propertyNamingStrategy); - final DerProperty derProperty = new DerProperty<>( - conversionService, - introspection, - i, - jsonProperty, - t, - beanProperty, - null, - null + unwrapped = deserBeanRegistry.getWrappedDeserializableBean( + propertyArgument, + prefix, + suffix, + decoderContext ); - readPropertiesBuilder.register(jsonProperty, derProperty, true); } + final String propertyName = resolveName(propertiesNamePrefix, propertiesNameSuffix, beanProperty, annotationMetadata, propertyNamingStrategy); + final DerProperty derProperty = new DerProperty<>( + conversionService, + introspection, + i, + propertyName, + propertyArgument, + beanProperty, + null, + unwrapped, + null + ); + if (isUnwrapped) { + if (unwrappedProperties == null) { + unwrappedProperties = new ArrayList<>(); + } + unwrappedProperties.add(derProperty); + } + readPropertiesBuilder.register(propertyName, derProperty, true); } } for (BeanMethod jsonSetter : jsonSetters) { PropertyNamingStrategy propertyNamingStrategy = getPropertyNamingStrategy(jsonSetter.getAnnotationMetadata(), decoderContext, entityPropertyNamingStrategy); - final String property = resolveName( + final String property = resolveName(propertiesNamePrefix, propertiesNameSuffix, new AnnotatedElement() { @Override public String getName() { @@ -361,13 +339,16 @@ public AnnotationMetadata getAnnotationMetadata() { argument, null, jsonSetter, + null, null ); readPropertiesBuilder.register(property, derProperty, true); } - readProperties = readPropertiesBuilder.build(); + injectProperties = readPropertiesBuilder.build(); + injectPropertiesSize = injectProperties.getProperties().size(); } else { - readProperties = null; + injectProperties = null; + injectPropertiesSize = 0; } } this.wrapperProperty = introspection.stringValue(SerdeConfig.class, SerdeConfig.WRAPPER_PROPERTY).orElse(null); @@ -379,12 +360,81 @@ public AnnotationMetadata getAnnotationMetadata() { //noinspection unchecked this.unwrappedProperties = unwrappedProperties != null ? unwrappedProperties.toArray(new DerProperty[0]) : null; + this.subtypeInfo = createSubtypeInfo(type, introspection, decoderContext, deserBeanRegistry); + simpleBean = isSimpleBean(); recordLikeBean = isRecordLikeBean(); } - public boolean isSubtyped() { - return false; + private static SubtypeInfo createSubtypeInfo(Argument type, + BeanIntrospection introspection, + Deserializer.DecoderContext decoderContext, + DeserBeanRegistry deserBeanRegistry) throws SerdeException { + AnnotationMetadata annotationMetadata = new AnnotationMetadataHierarchy( + type.getAnnotationMetadata(), + introspection.getAnnotationMetadata() + ); + if (!annotationMetadata.hasAnnotation(SerdeConfig.SerSubtyped.class)) { + return null; + } + + SerdeConfig.SerSubtyped.DiscriminatorType discriminatorType = annotationMetadata.enumValue( + SerdeConfig.SerSubtyped.class, + SerdeConfig.SerSubtyped.DISCRIMINATOR_TYPE, + SerdeConfig.SerSubtyped.DiscriminatorType.class + ).orElse(SerdeConfig.SerSubtyped.DiscriminatorType.PROPERTY); + SerdeConfig.SerSubtyped.DiscriminatorValueKind discriminatorValue = annotationMetadata.enumValue( + SerdeConfig.SerSubtyped.class, + SerdeConfig.SerSubtyped.DISCRIMINATOR_VALUE, + SerdeConfig.SerSubtyped.DiscriminatorValueKind.class + ).orElse(CLASS_NAME); + String discriminatorName = annotationMetadata.stringValue( + SerdeConfig.SerSubtyped.class, + SerdeConfig.SerSubtyped.DISCRIMINATOR_PROP + ).orElse(discriminatorValue == CLASS_NAME ? "@class" : "@type"); + + final Class superType = introspection.getBeanType(); + final Collection> subtypeIntrospections = + decoderContext.getDeserializableSubtypes(superType); + Map> subtypes = CollectionUtils.newHashMap(subtypeIntrospections.size()); + Class defaultType = annotationMetadata.classValue(DefaultImplementation.class).orElse(null); + String defaultDiscriminator = null; + for (BeanIntrospection subtypeIntrospection : subtypeIntrospections) { + Class subBeanType = subtypeIntrospection.getBeanType(); + final DeserBean deserBean = deserBeanRegistry.getDeserializableBean( + Argument.of(subBeanType), + decoderContext + ); + final String dn; + if (discriminatorValue == SerdeConfig.SerSubtyped.DiscriminatorValueKind.CLASS_NAME) { + dn = subBeanType.getName(); + } else if (discriminatorValue == SerdeConfig.SerSubtyped.DiscriminatorValueKind.CLASS_SIMPLE_NAME) { + dn = subBeanType.getSimpleName(); + } else { + dn = deserBean.introspection.stringValue(SerdeConfig.class, SerdeConfig.TYPE_NAME) + .orElse(deserBean.introspection.getBeanType().getSimpleName()); + } + subtypes.put( + dn, + deserBean + ); + if (defaultType != null && defaultType.equals(subBeanType)) { + defaultDiscriminator = dn; + } + + String[] names = subtypeIntrospection.stringValues(SerdeConfig.class, SerdeConfig.TYPE_NAMES); + for (String name : names) { + subtypes.put(name, deserBean); + } + } + + return new SubtypeInfo<>( + subtypes, + discriminatorType, + discriminatorValue, + discriminatorName, + defaultDiscriminator + ); } void initialize(Deserializer.DecoderContext decoderContext) throws SerdeException { @@ -402,8 +452,8 @@ void initialize(Deserializer.DecoderContext decoderContext) throws SerdeExceptio } private void initializeInternal(Deserializer.DecoderContext decoderContext) throws SerdeException { - if (readProperties != null) { - List>> properties = readProperties.getProperties(); + if (injectProperties != null) { + List>> properties = injectProperties.getProperties(); for (Map.Entry> e : properties) { DerProperty property = e.getValue(); initProperty(property, decoderContext); @@ -427,11 +477,11 @@ private void initializeInternal(Deserializer.DecoderContext decoderContext) thro } private boolean isSimpleBean() { - if (delegating || this instanceof SubtypedDeserBean || creatorParams != null || creatorUnwrapped != null || unwrappedProperties != null || anySetter != null) { + if (delegating || subtypeInfo != null || creatorParams != null || creatorUnwrapped != null || unwrappedProperties != null || anySetter != null) { return false; } - if (readProperties != null) { - for (Map.Entry> e : readProperties.getProperties()) { + if (injectProperties != null) { + for (Map.Entry> e : injectProperties.getProperties()) { DerProperty property = e.getValue(); if (property.isAnySetter || property.views != null || property.managedRef != null || introspection != property.instrospection || property.backRef != null || property.beanProperty == null) { return false; @@ -442,7 +492,7 @@ private boolean isSimpleBean() { } private boolean isRecordLikeBean() { - if (delegating || this instanceof SubtypedDeserBean || readProperties != null || creatorUnwrapped != null || unwrappedProperties != null || anySetter != null) { + if (delegating || subtypeInfo != null || injectProperties != null || creatorUnwrapped != null || unwrappedProperties != null || anySetter != null) { return false; } if (creatorParams != null) { @@ -464,7 +514,7 @@ private PropertyNamingStrategy getPropertyNamingStrategy(AnnotationMetadata anno Deserializer.DecoderContext decoderContext, PropertyNamingStrategy defaultNamingStrategy) throws SerdeException { Class namingStrategyClass = annotationMetadata.classValue(SerdeConfig.class, SerdeConfig.RUNTIME_NAMING) - .orElse(null); + .orElse(null); return namingStrategyClass == null ? defaultNamingStrategy : decoderContext.findNamingStrategy(namingStrategyClass); } @@ -487,26 +537,26 @@ private Argument resolveArgument(Argument argument, Map resolved = bounds.get(gp.getVariableName()); if (resolved != null) { return (Argument) Argument.of( - resolved.getType(), - argument.getName(), - argument.getAnnotationMetadata(), - typeParameters + resolved.getType(), + argument.getName(), + argument.getAnnotationMetadata(), + typeParameters ); } else if (typeParameters != declaredParameters) { return Argument.ofTypeVariable( - argument.getType(), - argument.getName(), - gp.getVariableName(), - gp.getAnnotationMetadata(), - typeParameters + argument.getType(), + argument.getName(), + gp.getVariableName(), + gp.getAnnotationMetadata(), + typeParameters ); } } else if (typeParameters != declaredParameters) { return Argument.of( - argument.getType(), - argument.getName(), - argument.getAnnotationMetadata(), - typeParameters + argument.getType(), + argument.getName(), + argument.getAnnotationMetadata(), + typeParameters ); } return argument; @@ -538,11 +588,22 @@ private Argument[] resolveParameters(Map> bounds, Argumen */ protected @NonNull Map> getBounds() { - return Collections.emptyMap(); + return bounds; } - private String resolveName(AnnotatedElement annotatedElement, AnnotationMetadata annotationMetadata, PropertyNamingStrategy namingStrategy) { - return resolveName(annotatedElement, List.of(annotationMetadata), namingStrategy); + private String resolveName(@Nullable String prefix, + @Nullable String suffix, + AnnotatedElement annotatedElement, + AnnotationMetadata annotationMetadata, + PropertyNamingStrategy namingStrategy) { + String name = resolveName(annotatedElement, List.of(annotationMetadata), namingStrategy); + if (prefix != null) { + name = prefix + name; + } + if (suffix != null) { + name = name + suffix; + } + return name; } private String resolveName(AnnotatedElement annotatedElement, List annotationMetadata, PropertyNamingStrategy namingStrategy) { @@ -567,7 +628,7 @@ private static Deserializer findDeserializer(Deserializer.DecoderContext if (customDeser != null) { return decoderContext.findCustomDeserializer(customDeser).createSpecific(decoderContext, argument); } - return (Deserializer) decoderContext.findDeserializer(argument).createSpecific(decoderContext, argument); + return (Deserializer) decoderContext.findDeserializer(argument).createSpecific(decoderContext, argument); } static final class AnySetter { @@ -578,6 +639,8 @@ static final class AnySetter { // Null when DeserBean not initialized public Deserializer deserializer; + + public final boolean constructorArgument; // CHECKSTYLE:ON private AnySetter(BeanMethod anySetter) { @@ -586,7 +649,7 @@ private AnySetter(BeanMethod anySetter) { // otherwise we are dealing with 2 parameter variant final boolean singleArg = arguments.length == 1; final Argument argument = - (Argument) (singleArg ? arguments[0].getTypeVariable("V").orElse(Argument.OBJECT_ARGUMENT) : arguments[1]); + (Argument) (singleArg ? arguments[0].getTypeVariable("V").orElse(Argument.OBJECT_ARGUMENT) : arguments[1]); this.valueType = argument; // this.deserializer = argument.equalsType(Argument.OBJECT_ARGUMENT) ? null : findDeserializer(decoderContext, argument); if (singleArg) { @@ -596,6 +659,7 @@ private AnySetter(BeanMethod anySetter) { this.valueSetter = anySetter::invoke; this.mapSetter = null; } + constructorArgument = false; } private AnySetter(BeanProperty anySetter) { @@ -606,6 +670,7 @@ private AnySetter(BeanProperty anySetter) { // this.deserializer = argument.equalsType(Argument.OBJECT_ARGUMENT) ? null : findDeserializer(decoderContext, argument); this.mapSetter = anySetter::set; this.valueSetter = null; + this.constructorArgument = false; } private AnySetter(Argument anySetter, int index) throws SerdeException { @@ -618,6 +683,7 @@ private AnySetter(Argument anySetter, int index) throws SerdeException { ((Object[]) o)[index] = map; }; this.valueSetter = null; + this.constructorArgument = true; } void bind(Map values, Object object) { @@ -665,6 +731,7 @@ public static final class DerProperty { public final UnsafeBeanProperty beanProperty; public final DeserBean

unwrapped; + public final DerProperty unwrappedProperty; public final String managedRef; public final String backRef; @@ -678,16 +745,18 @@ public DerProperty(ConversionService conversionService, Argument

argument, @Nullable BeanProperty beanProperty, @Nullable BeanMethod beanMethod, - @Nullable DeserBean

unwrapped) throws SerdeException { - this( conversionService, - introspection, - index, - property, - argument, - argument.getAnnotationMetadata(), - beanProperty, - beanMethod, - unwrapped + @Nullable DeserBean

unwrapped, + @Nullable DerProperty unwrappedProperty) throws SerdeException { + this(conversionService, + introspection, + index, + property, + argument, + argument.getAnnotationMetadata(), + beanProperty, + beanMethod, + unwrapped, + unwrappedProperty ); } @@ -699,18 +768,19 @@ public DerProperty(ConversionService conversionService, AnnotationMetadata argumentMetadata, @Nullable BeanProperty beanProperty, @Nullable BeanMethod beanMethod, - @Nullable DeserBean

unwrapped) throws SerdeException { + @Nullable DeserBean

unwrapped, + @Nullable DerProperty unwrappedProperty) throws SerdeException { this.instrospection = instrospection; this.index = index; this.argument = argument; Class type = argument.getType(); this.mustSetField = argument.isNonNull() || type.equals(Optional.class) - || type.equals(OptionalLong.class) - || type.equals(OptionalDouble.class) - || type.equals(OptionalInt.class); + || type.equals(OptionalLong.class) + || type.equals(OptionalDouble.class) + || type.equals(OptionalInt.class); this.nonNull = argument.isNonNull(); this.nullable = argument.isNullable(); - if (beanProperty != null) { + if (beanProperty != null) { this.beanProperty = (UnsafeBeanProperty) beanProperty; } else if (beanMethod != null) { this.beanProperty = new BeanMethodAsBeanProperty<>(property, beanMethod); @@ -723,13 +793,14 @@ public DerProperty(ConversionService conversionService, try { this.defaultValue = annotationMetadata - .stringValue(Bindable.class, "defaultValue") - .map(s -> conversionService.convertRequired(s, argument)) - .orElse(null); + .stringValue(Bindable.class, "defaultValue") + .map(s -> conversionService.convertRequired(s, argument)) + .orElse(null); } catch (ConversionErrorException e) { throw new SerdeException((index > -1 ? "Constructor Argument" : "Property") + " [" + argument + "] of type [" + instrospection.getBeanType().getName() + "] defines an invalid default value", e); } this.unwrapped = unwrapped; + this.unwrappedProperty = unwrappedProperty; this.isAnySetter = annotationMetadata.isAnnotationPresent(SerdeConfig.SerAnySetter.class); final String[] aliases = annotationMetadata.stringValues(SerdeConfig.class, SerdeConfig.ALIASES); if (ArrayUtils.isNotEmpty(aliases)) { @@ -738,11 +809,11 @@ public DerProperty(ConversionService conversionService, this.aliases = null; } this.managedRef = annotationMetadata.stringValue(SerdeConfig.SerManagedRef.class) - .orElse(null); + .orElse(null); this.backRef = annotationMetadata.stringValue(SerdeConfig.SerBackRef.class) - .orElse(null); + .orElse(null); this.explicitlyRequired = annotationMetadata.booleanValue(SerdeConfig.class, SerdeConfig.REQUIRED) - .orElse(false); + .orElse(false); } public void setDefaultPropertyValue(Deserializer.DecoderContext decoderContext, @NonNull B bean) throws SerdeException { @@ -762,7 +833,7 @@ public void setDefaultPropertyValue(Deserializer.DecoderContext decoderContext, } } throw new SerdeException("Unable to deserialize type [" + instrospection.getBeanType().getName() + "]. Required property [" + argument + - "] is not present in supplied data"); + "] is not present in supplied data"); } public void setDefaultConstructorValue(Deserializer.DecoderContext decoderContext, @NonNull Object[] params) throws SerdeException { @@ -786,7 +857,7 @@ public void setDefaultConstructorValue(Deserializer.DecoderContext decoderContex public void set(@NonNull B obj, @Nullable P v) throws SerdeException { if (v == null && nonNull) { throw new SerdeException("Unable to deserialize type [" + instrospection.getBeanType().getName() + "]. Required property [" + argument + - "] is not present in supplied data"); + "] is not present in supplied data"); } if (beanProperty != null) { @@ -877,8 +948,8 @@ public void deserializeAndCallBuilder(Decoder objectDecoder, Deserializer.Decode private static AnnotationMetadata resolveArgumentMetadata(BeanIntrospection instrospection, Argument

argument, AnnotationMetadata annotationMetadata) { // records store metadata in the bean property final AnnotationMetadata propertyMetadata = instrospection.getProperty(argument.getName(), argument.getType()) - .map(BeanProperty::getAnnotationMetadata) - .orElse(AnnotationMetadata.EMPTY_METADATA); + .map(BeanProperty::getAnnotationMetadata) + .orElse(AnnotationMetadata.EMPTY_METADATA); return new AnnotationMetadataHierarchy(propertyMetadata, annotationMetadata); } @@ -946,4 +1017,19 @@ public AnnotationMetadata getAnnotationMetadata() { return beanMethod.getAnnotationMetadata(); } } + + @Internal + public record SubtypeInfo( + @NonNull + Map> subtypes, + @NonNull + SerdeConfig.SerSubtyped.DiscriminatorType discriminatorType, + @NonNull + SerdeConfig.SerSubtyped.DiscriminatorValueKind discriminatorValue, + @NonNull + String discriminatorName, + @Nullable + String defaultImpl + ) { + } } diff --git a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBeanRegistry.java b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBeanRegistry.java index beed53c4a..781083687 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBeanRegistry.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBeanRegistry.java @@ -16,6 +16,7 @@ package io.micronaut.serde.support.deserializers; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.type.Argument; import io.micronaut.serde.Deserializer; import io.micronaut.serde.exceptions.SerdeException; @@ -23,4 +24,6 @@ @Internal interface DeserBeanRegistry { DeserBean getDeserializableBean(Argument type, Deserializer.DecoderContext decoderContext) throws SerdeException; + + DeserBean getWrappedDeserializableBean(Argument type, @Nullable String namePrefix, @Nullable String nameSuffix, Deserializer.DecoderContext decoderContext) throws SerdeException; } diff --git a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/ObjectDeserializer.java b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/ObjectDeserializer.java index 10c267a3c..594603389 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/ObjectDeserializer.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/ObjectDeserializer.java @@ -17,21 +17,19 @@ import io.micronaut.context.annotation.BootstrapContextCompatible; import io.micronaut.context.annotation.Primary; -import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.beans.BeanIntrospection; import io.micronaut.core.beans.exceptions.IntrospectionException; import io.micronaut.core.type.Argument; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.SupplierUtil; -import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.serde.Decoder; import io.micronaut.serde.Deserializer; import io.micronaut.serde.SerdeIntrospections; import io.micronaut.serde.config.DeserializationConfiguration; import io.micronaut.serde.config.annotation.SerdeConfig; import io.micronaut.serde.exceptions.SerdeException; -import io.micronaut.serde.support.util.TypeKey; +import io.micronaut.serde.support.util.BeanDefKey; import io.micronaut.serde.util.CustomizableDeserializer; import jakarta.inject.Singleton; @@ -52,7 +50,7 @@ public class ObjectDeserializer implements CustomizableDeserializer, Des private final SerdeIntrospections introspections; private final boolean ignoreUnknown; private final boolean strictNullable; - private final Map>> deserBeanMap = new ConcurrentHashMap<>(50); + private final Map>> deserBeanMap = new ConcurrentHashMap<>(50); @Nullable private final SerdeDeserializationPreInstantiateCallback preInstantiateCallback; @@ -73,36 +71,42 @@ public Deserializer createSpecific(DecoderContext context, Argument deserBean = getDeserializableBean(type, context); - if (deserBean instanceof SubtypedDeserBean subtypedDeserBean - && subtypedDeserBean.discriminatorType == SerdeConfig.SerSubtyped.DiscriminatorType.WRAPPER_OBJECT) { - Map> subtypeDeserializers = CollectionUtils.newHashMap(subtypedDeserBean.subtypes.size()); - for (Map.Entry> e : subtypedDeserBean.subtypes.entrySet()) { + if (deserBean.subtypeInfo != null) { + DeserBean.SubtypeInfo subtypeInfo = deserBean.subtypeInfo; + Map> subtypeDeserializers = CollectionUtils.newHashMap(subtypeInfo.subtypes().size()); + for (Map.Entry> e : subtypeInfo.subtypes().entrySet()) { subtypeDeserializers.put( e.getKey(), - findDeserializer((DeserBean) e.getValue(), true) + findDeserializer((DeserBean) e.getValue(), true) ); } - - return new WrappedObjectSubtypedDeserializer( - subtypeDeserializers, - deserBean.ignoreUnknown - ); + Deserializer supertypeDeserializer = findDeserializer(deserBean, false); + if (subtypeInfo.discriminatorType() == SerdeConfig.SerSubtyped.DiscriminatorType.WRAPPER_OBJECT) { + return new WrappedObjectSubtypedDeserializer( + subtypeDeserializers, + deserBean.ignoreUnknown + ); + } + if (subtypeInfo.discriminatorType() == SerdeConfig.SerSubtyped.DiscriminatorType.PROPERTY) { + return new SubtypedPropertyObjectDeserializer(deserBean, subtypeDeserializers, supertypeDeserializer); + } + throw new IllegalStateException("Unrecognized discriminator type: " + subtypeInfo.discriminatorType()); } + return findDeserializer(deserBean, false); } private Deserializer findDeserializer(DeserBean deserBean, boolean isSubtype) { Deserializer deserializer; if (deserBean.simpleBean) { - deserializer = new SimpleObjectDeserializer(ignoreUnknown, strictNullable, deserBean, preInstantiateCallback); + deserializer = new SimpleObjectDeserializer(ignoreUnknown, strictNullable, deserBean, preInstantiateCallback); } else if (deserBean.recordLikeBean) { - deserializer = new SimpleRecordLikeObjectDeserializer(ignoreUnknown, strictNullable, deserBean, preInstantiateCallback); + deserializer = new SimpleRecordLikeObjectDeserializer(ignoreUnknown, strictNullable, deserBean, preInstantiateCallback); } else if (deserBean.delegating) { deserializer = new DelegatingObjectDeserializer(strictNullable, deserBean, preInstantiateCallback); } else { deserializer = new SpecificObjectDeserializer(ignoreUnknown, strictNullable, deserBean, preInstantiateCallback); } - if (!isSubtype && deserBean.wrapperProperty != null) { deserializer = new WrappedObjectDeserializer( deserializer, @@ -115,46 +119,36 @@ private Deserializer findDeserializer(DeserBean deserBea @Override public DeserBean getDeserializableBean(Argument type, DecoderContext decoderContext) throws SerdeException { - TypeKey key = new TypeKey(type); + return gettDeserBean(type, null, null, decoderContext); + } + + @Override + public DeserBean getWrappedDeserializableBean(Argument type, + @Nullable String namePrefix, + @Nullable String nameSuffix, + DecoderContext decoderContext) throws SerdeException { + return gettDeserBean(type, namePrefix, nameSuffix, decoderContext); + } + + private DeserBean gettDeserBean(Argument type, + @Nullable String namePrefix, + @Nullable String nameSuffix, + DecoderContext decoderContext) throws SerdeException { + BeanDefKey key = new BeanDefKey(type, namePrefix, nameSuffix); // Use suppliers to prevent recursive update because the lambda will can call the same method again - Supplier> deserBeanSupplier = deserBeanMap.computeIfAbsent(key, ignore -> SupplierUtil.memoizedNonEmpty(() -> createDeserBean(type, decoderContext))); + Supplier> deserBeanSupplier = deserBeanMap.computeIfAbsent(key, ignore -> SupplierUtil.memoizedNonEmpty(() -> createDeserBean(type, namePrefix, nameSuffix, decoderContext))); DeserBean deserBean = deserBeanSupplier.get(); deserBean.initialize(decoderContext); return (DeserBean) deserBean; } - private DeserBean createDeserBean(Argument type, DecoderContext decoderContext) { + private DeserBean createDeserBean(Argument type, + @Nullable String namePrefix, + @Nullable String nameSuffix, + DecoderContext decoderContext) { try { final BeanIntrospection deserializableIntrospection = introspections.getDeserializableIntrospection(type); - AnnotationMetadata annotationMetadata = new AnnotationMetadataHierarchy( - type.getAnnotationMetadata(), - deserializableIntrospection.getAnnotationMetadata() - ); - if (annotationMetadata.hasAnnotation(SerdeConfig.SerSubtyped.class)) { - if (type.hasTypeVariables()) { - final Map> bounds = type.getTypeVariables(); - return new SubtypedDeserBean<>(annotationMetadata, deserializableIntrospection, decoderContext, this) { - @Override - protected Map> getBounds() { - return bounds; - } - }; - } else { - return new SubtypedDeserBean<>(annotationMetadata, deserializableIntrospection, decoderContext, this); - } - } else { - if (type.hasTypeVariables()) { - final Map> bounds = type.getTypeVariables(); - return new DeserBean<>(deserializableIntrospection, decoderContext, this) { - @Override - protected Map> getBounds() { - return bounds; - } - }; - } else { - return new DeserBean<>(deserializableIntrospection, decoderContext, this); - } - } + return new DeserBean<>(type, deserializableIntrospection, decoderContext, this, namePrefix, nameSuffix); } catch (SerdeException e) { throw new IntrospectionException("Error creating deserializer for type [" + type + "]: " + e.getMessage(), e); } diff --git a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/PropertiesBag.java b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/PropertiesBag.java index 6006f08c4..09226825d 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/PropertiesBag.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/PropertiesBag.java @@ -133,6 +133,10 @@ public List> getDerProperties() { return Collections.unmodifiableList(Arrays.asList(properties)); } + public DeserBean.DerProperty[] getPropertiesArray() { + return properties; + } + public int propertyIndexOf(@NonNull String name) { int i = probe(name); return i < 0 ? -1 : indexTable[i]; @@ -183,6 +187,14 @@ public DeserBean.DerProperty consume(String name) { return properties[propertyIndex]; } + public DeserBean.DerProperty consume(int propertyIndex) { + if (propertyIndex == -1 || isConsumed(propertyIndex)) { + return null; + } + setConsumed(propertyIndex); + return properties[propertyIndex]; + } + public List> getNotConsumed() { List> list = new ArrayList<>(properties.length); int bound = properties.length; diff --git a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SimpleObjectDeserializer.java b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SimpleObjectDeserializer.java index b69fd3647..96cf1baf3 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SimpleObjectDeserializer.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SimpleObjectDeserializer.java @@ -48,7 +48,7 @@ final class SimpleObjectDeserializer implements Deserializer, UpdatingDe this.ignoreUnknown = ignoreUnknown && deserBean.ignoreUnknown; this.strictNullable = strictNullable; this.introspection = deserBean.introspection; - this.properties = deserBean.readProperties; + this.properties = deserBean.injectProperties; this.preInstantiateCallback = preInstantiateCallback; } diff --git a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SpecificObjectDeserializer.java b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SpecificObjectDeserializer.java index 776a9718d..d7de06150 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SpecificObjectDeserializer.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SpecificObjectDeserializer.java @@ -24,22 +24,20 @@ import io.micronaut.serde.Decoder; import io.micronaut.serde.Deserializer; import io.micronaut.serde.UpdatingDeserializer; -import io.micronaut.serde.config.annotation.SerdeConfig; import io.micronaut.serde.exceptions.InvalidFormatException; import io.micronaut.serde.exceptions.InvalidPropertyFormatException; import io.micronaut.serde.exceptions.SerdeException; import io.micronaut.serde.reference.PropertyReference; import java.io.IOException; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; -import java.util.NoSuchElementException; /** * Implementation for deserialization of objects that uses introspection metadata. * * @author graemerocher + * @author Denis Stepanov * @since 1.0.0 */ final class SpecificObjectDeserializer implements Deserializer, UpdatingDeserializer { @@ -61,334 +59,51 @@ public SpecificObjectDeserializer(boolean ignoreUnknown, } @Override - public Object deserialize(Decoder decoder, DecoderContext decoderContext, Argument type) - throws IOException { - - DeserBean db = this.deserBean; - Class objectType = db.introspection.getBeanType(); + public Object deserialize(Decoder decoder, DecoderContext decoderContext, Argument type) throws IOException { + BeanDeserializer consumer = newBeanDeserializer(null, deserBean, strictNullable, preInstantiateCallback); + consumer.init(decoderContext); + return deserialize(decoder, decoderContext, type, consumer); + } - PropertiesBag.Consumer readProperties = db.readProperties != null ? db.readProperties.newConsumer() : null; - boolean hasProperties = readProperties != null; + @Override + public void deserializeInto(Decoder decoder, DecoderContext decoderContext, Argument type, Object value) throws IOException { + BeanDeserializer deserializer = newBeanDeserializer(value, deserBean, strictNullable, preInstantiateCallback); + deserializer.init(decoderContext); + deserialize(decoder, decoderContext, type, deserializer); + } + private Object deserialize(Decoder decoder, DecoderContext decoderContext, Argument type, BeanDeserializer beanDeserializer) throws IOException { Decoder objectDecoder = decoder.decodeObject(type); - TokenBuffer tokenBuffer = null; - AnyValues anyValues = db.anySetter != null ? new AnyValues<>(db.anySetter) : null; - Object obj; - - if (db.isSubtyped() && db instanceof SubtypedDeserBean subtypedDeserBean) { - // subtyped binding required - final String defaultImpl = subtypedDeserBean.defaultImpl; - final String discriminatorName = subtypedDeserBean.discriminatorName; - final Map> subtypes = subtypedDeserBean.subtypes; - - if (subtypedDeserBean.discriminatorType != SerdeConfig.SerSubtyped.DiscriminatorType.PROPERTY) { - throw new IllegalStateException("Unsupported discriminator type: " + subtypedDeserBean.discriminatorType); - } - - while (true) { - final String key = objectDecoder.decodeKey(); - if (key == null) { - break; - } - - if (key.equals(discriminatorName)) { - if (!objectDecoder.decodeNull()) { - final String subtypeName = objectDecoder.decodeString(); - final DeserBean subtypeDeser = subtypes.get(subtypeName); - if (subtypeDeser != null) { - //noinspection unchecked - db = (DeserBean) subtypeDeser; - //noinspection unchecked - objectType = (Class) subtypeDeser.introspection.getBeanType(); - readProperties = db.readProperties != null ? db.readProperties.newConsumer() : null; - hasProperties = readProperties != null; - anyValues = db.anySetter != null ? new AnyValues<>(db.anySetter) : null; - } - } - break; - } else { - tokenBuffer = initTokenBuffer(tokenBuffer, objectDecoder, key); - } - } - - if (defaultImpl != null && subtypedDeserBean == db) { - @SuppressWarnings("unchecked") - DeserBean defaultSubType = (DeserBean) subtypes.get(defaultImpl); - if (defaultSubType != null) { - db = defaultSubType; - objectType = defaultSubType.introspection.getBeanType(); - readProperties = db.readProperties != null ? db.readProperties.newConsumer() : null; - hasProperties = readProperties != null; - - } - } - - } - - if (db.creatorParams != null) { - final PropertiesBag.Consumer creatorParameters = db.creatorParams.newConsumer(); - int creatorSize = db.creatorSize; - Object[] params = new Object[creatorSize]; - PropertyBuffer buffer = initFromTokenBuffer( - tokenBuffer, - creatorParameters, - readProperties, - anyValues, - decoderContext - ); - while (true) { - final String prop = objectDecoder.decodeKey(); - if (prop == null) { - break; - } - final DeserBean.DerProperty sp = creatorParameters.consume(prop); - boolean consumed = false; - if (sp != null) { - if (sp.views != null && !decoderContext.hasView(sp.views)) { - objectDecoder.skipValue(); - continue; - } - final Object val = deserializeValue(decoderContext, objectDecoder, sp, sp.argument, null); - if (val == null) { - sp.setDefaultConstructorValue(decoderContext, params); - // Skip consume - continue; - } - if (sp.instrospection.getBeanType() == objectType) { - params[sp.index] = val; - if (hasProperties && readProperties.isNotConsumed(prop)) { - // will need binding to properties as well - buffer = initBuffer(buffer, sp, prop, val); - } - } else { - buffer = initBuffer(buffer, sp, prop, val); - } - if (creatorParameters.isAllConsumed()) { - break; - } - consumed = true; - } else if (hasProperties) { - final DeserBean.DerProperty rp = readProperties.findNotConsumed(prop); - if (rp != null) { - if (rp.managedRef != null) { - tokenBuffer = initTokenBuffer(tokenBuffer, objectDecoder, prop); - } else { - final Object val = deserializeValue(decoderContext, objectDecoder, rp, rp.argument, null); - buffer = initBuffer(buffer, rp, prop, val); - } - consumed = true; - } - } - if (!consumed) { - skipOrSetAny( - decoderContext, - objectDecoder, - prop, - anyValues, - ignoreUnknown, - type - ); - } - } - - if (buffer != null && !creatorParameters.isAllConsumed()) { - for (PropertyBuffer propertyBuffer : buffer) { - final DeserBean.DerProperty derProperty = creatorParameters.consume(propertyBuffer.name); - if (derProperty != null) { - propertyBuffer.set( - params, - decoderContext - ); - } - } + Object instance = null; + while (true) { + final String propertyName = objectDecoder.decodeKey(); + if (propertyName == null) { + break; } - - if (!creatorParameters.isAllConsumed()) { - // set unsatisfied parameters to defaults or fail - for (DeserBean.DerProperty sp : creatorParameters.getNotConsumed()) { - if (sp.backRef != null) { - final PropertyReference ref = decoderContext.resolveReference( - new PropertyReference<>( - sp.backRef, - sp.instrospection, - sp.argument, - null - ) - ); - if (ref != null) { - final Object o = ref.getReference(); - if (o == null) { - sp.setDefaultConstructorValue(decoderContext, params); - } else { - params[sp.index] = o; - } - continue; - } - } - if (sp.unwrapped != null && buffer != null) { - final Object o = materializeFromBuffer(sp, buffer, decoderContext); - if (o == null) { - sp.setDefaultConstructorValue(decoderContext, params); - } else { - params[sp.index] = o; - } - } else { - if (sp.isAnySetter && anyValues != null) { - anyValues.bind(params); - anyValues = null; - } else { - sp.setDefaultConstructorValue(decoderContext, params); - } - } + boolean consumed = beanDeserializer.tryConsume(propertyName, objectDecoder, decoderContext); + if (!consumed) { + if (ignoreUnknown) { + objectDecoder.skipValue(); + } else { + throw new SerdeException("Unknown property [" + propertyName + "] encountered during deserialization of type: " + type); } } - - try { - if (preInstantiateCallback != null) { - preInstantiateCallback.preInstantiate(db.introspection, params); - } - obj = db.introspection.instantiate(strictNullable, params); - } catch (InstantiationException e) { - throw new SerdeException(PREFIX_UNABLE_TO_DESERIALIZE_TYPE + type + "]: " + e.getMessage(), e); + if (beanDeserializer.isAllConsumed()) { + instance = beanDeserializer.provideInstance(decoderContext); + break; } - if (hasProperties) { - if (buffer != null) { - processPropertyBuffer(decoderContext, objectType, readProperties, obj, buffer); - } - if (tokenBuffer != null) { - buffer = initFromTokenBuffer(tokenBuffer, creatorParameters, readProperties, anyValues, decoderContext); - if (buffer != null) { - processPropertyBuffer(decoderContext, objectType, readProperties, obj, buffer); - } - } - if (!readProperties.isAllConsumed()) { - // more properties still to be read - buffer = decodeProperties( - db, - decoderContext, - obj, - objectDecoder, - readProperties, - db.unwrappedProperties == null, - buffer, - anyValues, - ignoreUnknown, - type - ); - } + } - applyDefaultValuesOrFail( - obj, - readProperties, - db.unwrappedProperties, - buffer, - decoderContext - ); - } - } else if (db.hasBuilder) { - BeanIntrospection.Builder builder; - try { - if (preInstantiateCallback != null) { - preInstantiateCallback.preInstantiate(db.introspection); - } - builder = db.introspection.builder(); - } catch (InstantiationException e) { - throw new SerdeException(PREFIX_UNABLE_TO_DESERIALIZE_TYPE + type + "]: " + e.getMessage(), e); - } - if (hasProperties) { - if (tokenBuffer != null) { - for (TokenBuffer buffer : tokenBuffer) { - final DeserBean.DerProperty property = readProperties.consume(buffer.name); - if (property != null) { - property.deserializeAndCallBuilder(buffer.decoder, decoderContext, builder); - } else { - skipOrSetAny( - decoderContext, - buffer.decoder, - buffer.name, - anyValues, - ignoreUnknown, - type - ); - } - } - } - while (true) { - final String prop = objectDecoder.decodeKey(); - if (prop == null) { - break; - } - final DeserBean.DerProperty property = readProperties.consume(prop); - if (property != null) { - property.deserializeAndCallBuilder(objectDecoder, decoderContext, builder); - } else { - skipOrSetAny( - decoderContext, - objectDecoder, - prop, - anyValues, - ignoreUnknown, - type - ); - } - } - } - try { - obj = builder.build(); - } catch (InstantiationException e) { - throw new SerdeException(PREFIX_UNABLE_TO_DESERIALIZE_TYPE + type + "]: " + e.getMessage(), e); - } + if (instance == null) { + instance = beanDeserializer.provideInstance(decoderContext); + } + if (ignoreUnknown) { + objectDecoder.finishStructure(true); } else { - try { - if (preInstantiateCallback != null) { - preInstantiateCallback.preInstantiate(db.introspection); - } - obj = db.introspection.instantiate(strictNullable, ArrayUtils.EMPTY_OBJECT_ARRAY); - } catch (InstantiationException e) { - throw new SerdeException(PREFIX_UNABLE_TO_DESERIALIZE_TYPE + type + "]: " + e.getMessage(), e); - } - if (hasProperties) { - final PropertyBuffer existingBuffer = initFromTokenBuffer( - tokenBuffer, - null, - readProperties, - anyValues, - decoderContext); - final PropertyBuffer propertyBuffer = decodeProperties(db, - decoderContext, - obj, - objectDecoder, - readProperties, - db.unwrappedProperties == null, - existingBuffer, - anyValues, - ignoreUnknown, - type); - // the property buffer will be non-null if there were any unwrapped - // properties in which case we need to go through and materialize unwrapped - // from the buffer - applyDefaultValuesOrFail( - obj, - readProperties, - db.unwrappedProperties, - propertyBuffer, - decoderContext - ); - } else if (anyValues != null && tokenBuffer != null) { - for (TokenBuffer buffer : tokenBuffer) { - anyValues.handle( - buffer.name, - buffer.decoder, - decoderContext - ); - } - } + objectDecoder.finishStructure(); } - // finish up - finalizeObjectDecoder(decoderContext, type, ignoreUnknown, objectDecoder, anyValues, obj); - - return obj; + return instance; } @Override @@ -399,46 +114,43 @@ public Object deserializeNullable(@NonNull Decoder decoder, @NonNull DecoderCont return deserialize(decoder, context, type); } - private void processPropertyBuffer(DecoderContext decoderContext, - Class objectType, - PropertiesBag.Consumer readProperties, - Object obj, - PropertyBuffer buffer) throws IOException { - for (PropertyBuffer propertyBuffer : buffer) { - final DeserBean.DerProperty derProperty = readProperties.consume(propertyBuffer.name); - if (derProperty != null) { - if (derProperty.instrospection.getBeanType() == objectType) { - propertyBuffer.set(obj, decoderContext); - } - } + private static BeanDeserializer newBeanDeserializer(Object instance, + DeserBean db, + boolean strictNullable, + @Nullable SerdeDeserializationPreInstantiateCallback preInstantiateCallback) { + if (db.hasBuilder) { + return new BuilderDeserializer(db, preInstantiateCallback); } + if (db.creatorParams != null) { + return new ArgsConstructorBeanDeserializer(db, strictNullable, preInstantiateCallback); + } + return new NoArgsConstructorDeserializer(instance, db, strictNullable, preInstantiateCallback); } - private Object deserializeValue(DecoderContext decoderContext, - Decoder objectDecoder, - DeserBean.DerProperty derProperty, - Argument propertyType, - Object constructedBean) throws IOException { - final boolean hasRef = constructedBean != null && derProperty.managedRef != null; + private static Object deserializeValue(DecoderContext decoderContext, + Decoder objectDecoder, + DeserBean.DerProperty derProperty, + @NonNull Object instance) throws IOException { + final boolean hasRef = derProperty.managedRef != null; + try { if (hasRef) { decoderContext.pushManagedRef( - new PropertyReference<>( - derProperty.managedRef, - derProperty.instrospection, - derProperty.argument, - constructedBean - ) + new PropertyReference<>( + derProperty.managedRef, + derProperty.instrospection, + derProperty.argument, + instance + ) ); } - Deserializer deserializer = derProperty.deserializer; - return deserializer.deserializeNullable( - objectDecoder, - decoderContext, - propertyType + return derProperty.deserializer.deserializeNullable( + objectDecoder, + decoderContext, + derProperty.argument ); } catch (InvalidFormatException e) { - throw new InvalidPropertyFormatException(e, propertyType); + throw new InvalidPropertyFormatException(e, derProperty.argument); } finally { if (hasRef) { decoderContext.popManagedRef(); @@ -446,514 +158,761 @@ private Object deserializeValue(DecoderContext decoderContext, } } - private void finalizeObjectDecoder(DecoderContext decoderContext, - Argument type, - boolean ignoreUnknown, - Decoder objectDecoder, - AnyValues anyValues, - Object obj) throws IOException { - if (anyValues == null && ignoreUnknown) { - objectDecoder.finishStructure(true); - } else { - while (true) { - final String key = objectDecoder.decodeKey(); - if (key == null) { - break; - } - skipOrSetAny(decoderContext, objectDecoder, key, anyValues, ignoreUnknown, type); - } - if (anyValues != null) { - anyValues.bind(obj); - } - objectDecoder.finishStructure(); + private static Object deserializeValue(DecoderContext decoderContext, + Decoder objectDecoder, + DeserBean.DerProperty derProperty) throws IOException { + try { + return derProperty.deserializer.deserializeNullable( + objectDecoder, + decoderContext, + derProperty.argument + ); + } catch (InvalidFormatException e) { + throw new InvalidPropertyFormatException(e, derProperty.argument); } } - private TokenBuffer initTokenBuffer(TokenBuffer tokenBuffer, Decoder objectDecoder, String key) throws IOException { - return tokenBuffer == null ? new TokenBuffer( - key, - objectDecoder.decodeBuffer(), - null - ) : tokenBuffer.next(key, objectDecoder.decodeBuffer()); - } + /** + * Deserializes unknown properties into the any values map. + * + * @author Denis Stepanov + */ + private static final class AnyValuesDeserializer { - private @Nullable PropertyBuffer initFromTokenBuffer(@Nullable TokenBuffer tokenBuffer, - @Nullable PropertiesBag.Consumer creatorParameters, - @Nullable PropertiesBag.Consumer readProperties, - @Nullable AnyValues anyValues, - DecoderContext decoderContext) throws IOException { - if (tokenBuffer != null) { - PropertyBuffer propertyBuffer = null; - for (TokenBuffer buffer : tokenBuffer) { - final String n = buffer.name; - if (creatorParameters != null) { - final DeserBean.DerProperty derProperty = creatorParameters.findNotConsumed(n); - if (derProperty != null) { - propertyBuffer = initBuffer( - propertyBuffer, - derProperty, - n, - buffer.decoder - ); - continue; - } - } - if (readProperties != null) { - final DeserBean.DerProperty derProperty = readProperties.findNotConsumed(n); - if (derProperty != null) { - propertyBuffer = initBuffer( - propertyBuffer, - derProperty, - n, - buffer.decoder - ); - continue; - } - } - if (anyValues != null) { - anyValues.handle( - buffer.name, - buffer.decoder, - decoderContext + private final DeserBean.AnySetter anySetter; + private Map values; + + AnyValuesDeserializer(DeserBean.AnySetter anySetter) { + this.anySetter = anySetter; + } + + void bind(Object instance) { + if (values != null) { + anySetter.bind(values, instance); + } + } + + boolean tryConsume(String propertyName, Decoder decoder, DecoderContext decoderContext) throws IOException { + if (values == null) { + values = new LinkedHashMap<>(); + } + if (decoder.decodeNull()) { + values.put(propertyName, null); + } else { + if (anySetter.deserializer != null) { + Object deserializedValue = anySetter.deserializer.deserializeNullable( + decoder, + decoderContext, + anySetter.valueType ); + values.put(propertyName, deserializedValue); + } else { + values.put(propertyName, decoder.decodeArbitrary()); } } - return propertyBuffer; + return true; } - return null; } - private PropertyBuffer initBuffer(PropertyBuffer buffer, - DeserBean.DerProperty rp, - String prop, - Object val) { - if (buffer == null) { - buffer = new PropertyBuffer(rp, prop, val, null); - } else { - buffer = buffer.next(rp, prop, val); + /** + * Deserializes the properties into an array to be set later after the bean instance is created. + * + * @author Denis Stepanov + */ + private static final class CachedPropertiesValuesDeserializer { + + private final PropertiesBag properties; + private final PropertiesBag.Consumer propertiesConsumer; + private final Object[] values; + private final Decoder[] buffered; + + @Nullable + private final UnwrappedPropertyDeserializer[] unwrappedProperties; + + CachedPropertiesValuesDeserializer(DeserBean db, + boolean strictNullable, + @Nullable SerdeDeserializationPreInstantiateCallback preInstantiateCallback) { + properties = db.injectProperties; + propertiesConsumer = properties.newConsumer(); + values = new Object[db.injectPropertiesSize]; + buffered = new Decoder[db.injectPropertiesSize]; + if (db.unwrappedProperties == null) { + unwrappedProperties = null; + } else { + unwrappedProperties = new UnwrappedPropertyDeserializer[db.unwrappedProperties.length]; + for (int i = 0; i < db.unwrappedProperties.length; i++) { + unwrappedProperties[i] = new UnwrappedPropertyDeserializer(db.unwrappedProperties[i], strictNullable, preInstantiateCallback); + } + } } - return buffer; - } - private PropertyBuffer decodeProperties( - DeserBean introspection, - DecoderContext decoderContext, - Object obj, - Decoder objectDecoder, - PropertiesBag.Consumer readProperties, - boolean hasNoUnwrapped, - PropertyBuffer propertyBuffer, - @Nullable AnyValues anyValues, - boolean ignoreUnknown, - Argument beanType) throws IOException { - while (true) { - final String prop = objectDecoder.decodeKey(); - if (prop == null) { - break; + void init(DecoderContext decoderContext) throws SerdeException { + if (unwrappedProperties != null) { + for (UnwrappedPropertyDeserializer unwrappedProperty : unwrappedProperties) { + unwrappedProperty.beanDeserializer.init(decoderContext); + } } - final DeserBean.DerProperty property = readProperties.consume(prop); + } + + boolean tryConsume(String propertyName, Decoder decoder, DecoderContext decoderContext) throws IOException { + final DeserBean.DerProperty property = propertiesConsumer.consume(propertyName); if (property != null && (property.beanProperty != null)) { if (property.views != null && !decoderContext.hasView(property.views)) { - objectDecoder.skipValue(); - continue; + decoder.skipValue(); + return true; } + if (property.managedRef == null) { + values[property.index] = deserializeValue(decoderContext, decoder, property); + } else { + buffered[property.index] = decoder.decodeBuffer(); + } + return true; + } + if (unwrappedProperties != null) { + for (UnwrappedPropertyDeserializer unwrappedProperty : unwrappedProperties) { + if (unwrappedProperty.tryConsume(propertyName, decoder, decoderContext)) { + return true; + } + } + } + return false; + } - boolean isNull = objectDecoder.decodeNull(); - if (isNull) { - if (property.argument.isNullable()) { - property.set(obj, null); + void injectProperties(Object instance, DecoderContext decoderContext) throws IOException { + DeserBean.DerProperty[] propertiesArray = properties.getPropertiesArray(); + for (int i = 0; i < propertiesArray.length; i++) { + DeserBean.DerProperty property = propertiesArray[i]; + if (property.unwrapped != null) { + continue; + } + if (property.views != null && !decoderContext.hasView(property.views)) { + continue; + } + Object value; + if (property.backRef != null) { + final PropertyReference ref = decoderContext.resolveReference( + new PropertyReference<>( + property.backRef, + property.instrospection, + property.argument, + null + ) + ); + if (ref != null) { + value = ref.getReference(); } else { - property.setDefaultPropertyValue(decoderContext, obj); + value = null; + } + } else { + Decoder bufferedDecoder = buffered[i]; + if (bufferedDecoder != null) { + value = deserializeValue(decoderContext, bufferedDecoder, property, instance); + } else { + value = values[i]; } - continue; } - final Object val = deserializeValue(decoderContext, objectDecoder, property, property.argument, obj); - - if (introspection.introspection == property.instrospection) { - property.set(obj, val); + if (value == null) { + property.setDefaultPropertyValue(decoderContext, instance); } else { - propertyBuffer = initBuffer(propertyBuffer, property, prop, val); + property.set(instance, value); } - if (readProperties.isAllConsumed() && hasNoUnwrapped && introspection.anySetter == null) { - break; + } + if (unwrappedProperties != null) { + for (UnwrappedPropertyDeserializer unwrappedProperty : unwrappedProperties) { + DeserBean.DerProperty wrappedProperty = unwrappedProperty.wrappedProperty; + if (wrappedProperty.views != null && !decoderContext.hasView(wrappedProperty.views)) { + continue; + } + Object value = unwrappedProperty.beanDeserializer.provideInstance(decoderContext); + if (value == null) { + wrappedProperty.setDefaultPropertyValue(decoderContext, instance); + } else { + wrappedProperty.set(instance, value); + } } - } else { - skipOrSetAny( - decoderContext, - objectDecoder, - prop, - anyValues, - ignoreUnknown, - beanType - ); } } - return propertyBuffer; - } - private void skipOrSetAny(DecoderContext decoderContext, - Decoder objectDecoder, - String property, - @Nullable AnyValues anyValues, - boolean ignoreUnknown, - Argument type) throws IOException { - if (anyValues != null) { - anyValues.handle( - property, - objectDecoder, - decoderContext - ); - } else { - if (ignoreUnknown) { - objectDecoder.skipValue(); - } else { - throw new SerdeException("Unknown property [" + property + "] encountered during deserialization of type: " + type); + boolean isAllConsumed() { + if (!propertiesConsumer.isAllConsumed()) { + return false; } + if (unwrappedProperties != null) { + for (UnwrappedPropertyDeserializer unwrappedProperty : unwrappedProperties) { + if (!unwrappedProperty.isAllConsumed()) { + return false; + } + + } + } + return true; } } - private void applyDefaultValuesOrFail( - Object obj, - PropertiesBag.Consumer readProperties, - @Nullable DeserBean.DerProperty[] unwrappedProperties, - @Nullable PropertyBuffer buffer, - DecoderContext decoderContext) - throws IOException { - if (ArrayUtils.isNotEmpty(unwrappedProperties)) { - for (DeserBean.DerProperty dp : unwrappedProperties) { - if (dp.views != null && !decoderContext.hasView(dp.views)) { - continue; - } - if (buffer == null) { - dp.set(obj, null); - } else { - Object v = materializeFromBuffer(dp, buffer, decoderContext); - dp.set(obj, v); + /** + * Deserializes the properties and sets them directly into the bean instance. + * + * @author Denis Stepanov + */ + private static final class PropertiesValuesDeserializer { + + private final PropertiesBag properties; + @Nullable + private final PropertiesBag.Consumer propertiesConsumer; + + @Nullable + private final UnwrappedPropertyDeserializer[] unwrappedProperties; + + PropertiesValuesDeserializer(DeserBean db, + boolean strictNullable, + @Nullable SerdeDeserializationPreInstantiateCallback preInstantiateCallback) { + properties = db.injectProperties; + propertiesConsumer = properties.newConsumer(); + if (db.unwrappedProperties == null) { + unwrappedProperties = null; + } else { + unwrappedProperties = new UnwrappedPropertyDeserializer[db.unwrappedProperties.length]; + for (int i = 0; i < db.unwrappedProperties.length; i++) { + unwrappedProperties[i] = new UnwrappedPropertyDeserializer(db.unwrappedProperties[i], strictNullable, preInstantiateCallback); } } } - if (buffer != null && !readProperties.isAllConsumed()) { - for (PropertyBuffer propertyBuffer : buffer) { - final DeserBean.DerProperty derProperty = readProperties.consume(propertyBuffer.name); - if (derProperty != null) { - propertyBuffer.set(obj, decoderContext); + + void init(DecoderContext decoderContext) throws SerdeException { + if (unwrappedProperties != null) { + for (UnwrappedPropertyDeserializer unwrappedProperty : unwrappedProperties) { + unwrappedProperty.beanDeserializer.init(decoderContext); } } } - if (!readProperties.isAllConsumed()) { - for (DeserBean.DerProperty dp : readProperties.getNotConsumed()) { - if (dp.backRef != null) { + + boolean tryConsumeAndSet(String propertyName, Decoder decoder, DecoderContext decoderContext, Object instance) throws IOException { + final DeserBean.DerProperty property = propertiesConsumer.consume(propertyName); + if (property != null) { + if (property.views != null && !decoderContext.hasView(property.views)) { + decoder.skipValue(); + return true; + } + Object value; + if (property.backRef != null) { final PropertyReference ref = decoderContext.resolveReference( - new PropertyReference<>( - dp.backRef, - dp.instrospection, - dp.argument, - null - ) + new PropertyReference<>( + property.backRef, + property.instrospection, + property.argument, + instance + ) ); if (ref != null) { - final Object o = ref.getReference(); - if (o == null) { - dp.setDefaultPropertyValue(decoderContext, obj); - } else { - dp.set(obj, o); - } + value = ref.getReference(); + } else { + value = null; } } else { - dp.setDefaultPropertyValue(decoderContext, obj); + value = deserializeValue(decoderContext, decoder, property, instance); + } + if (value == null) { + property.setDefaultPropertyValue(decoderContext, instance); + } else { + property.set(instance, value); } + return true; } + if (unwrappedProperties != null) { + for (UnwrappedPropertyDeserializer up : unwrappedProperties) { + if (up.tryConsume(propertyName, decoder, decoderContext)) { + if (up.isAllConsumed()) { + DeserBean.DerProperty wrappedProperty = up.wrappedProperty; + if (wrappedProperty.views != null && !decoderContext.hasView(wrappedProperty.views)) { + continue; + } + propertiesConsumer.consume(wrappedProperty.index); + Object wrappedInstance = up.beanDeserializer.provideInstance(decoderContext); + wrappedProperty.set(instance, wrappedInstance); + } + return true; + } + } + } + return false; } - } - private @Nullable Object materializeFromBuffer(DeserBean.DerProperty property, - PropertyBuffer buffer, - DecoderContext decoderContext) throws IOException { - final DeserBean unwrapped = property.unwrapped; - if (unwrapped != null) { - return materializeUnwrapped(buffer, decoderContext, unwrapped); + void finalizeProperties(DecoderContext decoderContext, Object instance) throws IOException { + if (unwrappedProperties != null) { + for (UnwrappedPropertyDeserializer unwrappedProperty : unwrappedProperties) { + DeserBean.DerProperty wrappedProperty = unwrappedProperty.wrappedProperty; + if (propertiesConsumer.isConsumed(wrappedProperty.index)) { + continue; + } + if (wrappedProperty.views != null && !decoderContext.hasView(wrappedProperty.views)) { + continue; + } + Object value = unwrappedProperty.beanDeserializer.provideInstance(decoderContext); + if (value == null) { + wrappedProperty.setDefaultPropertyValue(decoderContext, instance); + } else { + wrappedProperty.set(instance, value); + } + } + } + DeserBean.DerProperty[] propertiesArray = properties.getPropertiesArray(); + for (int i = 0; i < propertiesArray.length; i++) { + if (propertiesConsumer.isConsumed(i)) { + continue; + } + DeserBean.DerProperty property = propertiesArray[i]; + if (property.unwrapped != null) { + continue; + } + property.setDefaultPropertyValue(decoderContext, instance); + } + } - return null; - } - private Object materializeUnwrapped(PropertyBuffer buffer, DecoderContext decoderContext, DeserBean unwrapped) throws IOException { - Object object; - if (unwrapped.creatorParams != null) { - Object[] params = new Object[unwrapped.creatorSize]; - // handle construction - for (DeserBean.DerProperty der : unwrapped.creatorParams.getDerProperties()) { - boolean satisfied = false; - for (PropertyBuffer pb : buffer) { - if (pb.property == der) { - pb.set(params, decoderContext); - satisfied = true; - break; + boolean isAllConsumed() { + if (!propertiesConsumer.isAllConsumed()) { + return false; + } + if (unwrappedProperties != null) { + for (UnwrappedPropertyDeserializer unwrappedProperty : unwrappedProperties) { + if (!unwrappedProperty.isAllConsumed()) { + return false; } + } - if (!satisfied) { - if (der.defaultValue != null) { - params[der.index] = der.defaultValue; - } else if (der.mustSetField) { - throw new SerdeException(PREFIX_UNABLE_TO_DESERIALIZE_TYPE + unwrapped.introspection.getBeanType() + "]. Required constructor parameter [" + der.argument + "] at index [" + der.index + "] is not present in supplied data"); + } + return true; + } + } - } + /** + * Deserializes the constructor values into an array to be used to instantiate the bean. + * + * @author Denis Stepanov + */ + private static final class ConstructorValuesDeserializer { + + private final PropertiesBag parameters; + private final PropertiesBag.Consumer creatorParameters; + private final Object[] values; + + @Nullable + private final UnwrappedPropertyDeserializer[] unwrappedProperties; + @Nullable + private final AnyValuesDeserializer anyValuesDeserializer; + private boolean allConsumed; + + ConstructorValuesDeserializer(DeserBean db, + boolean strictNullable, + @Nullable SerdeDeserializationPreInstantiateCallback preInstantiateCallback) { + parameters = db.creatorParams; + creatorParameters = db.creatorParams.newConsumer(); + int creatorSize = db.creatorSize; + values = new Object[creatorSize]; + if (db.creatorUnwrapped == null) { + unwrappedProperties = null; + } else { + unwrappedProperties = new UnwrappedPropertyDeserializer[db.creatorUnwrapped.length]; + for (int i = 0; i < db.creatorUnwrapped.length; i++) { + unwrappedProperties[i] = new UnwrappedPropertyDeserializer(db.creatorUnwrapped[i], strictNullable, preInstantiateCallback); } } - if (preInstantiateCallback != null) { - preInstantiateCallback.preInstantiate(unwrapped.introspection, params); + if (db.anySetter == null || !db.anySetter.constructorArgument) { + anyValuesDeserializer = null; + } else { + anyValuesDeserializer = new AnyValuesDeserializer(db.anySetter); } - object = unwrapped.introspection.instantiate(strictNullable, params); - } else { - if (preInstantiateCallback != null) { - preInstantiateCallback.preInstantiate(unwrapped.introspection); - } - object = unwrapped.introspection.instantiate(strictNullable, ArrayUtils.EMPTY_OBJECT_ARRAY); - } - - if (unwrapped.readProperties != null) { - // nested unwrapped - DeserBean.@Nullable DerProperty[] nestedUnwrappedProperties = unwrapped.unwrappedProperties; - if (nestedUnwrappedProperties != null) { - for (DeserBean.DerProperty nestedUnwrappedProperty : nestedUnwrappedProperties) { - DeserBean nested = nestedUnwrappedProperty.unwrapped; - Object o = materializeUnwrapped(buffer, decoderContext, nested); - nestedUnwrappedProperty.set(object, o); - } - } - for (DeserBean.DerProperty der : unwrapped.readProperties.getDerProperties()) { - boolean satisfied = false; - for (PropertyBuffer pb : buffer) { - DeserBean.DerProperty property = pb.property; - if (property == der) { - if (property.instrospection == unwrapped.introspection) { - pb.set(object, decoderContext); - der.set(object, pb.value); + } + + void init(DecoderContext decoderContext) throws SerdeException { + if (unwrappedProperties != null) { + for (UnwrappedPropertyDeserializer unwrappedProperty : unwrappedProperties) { + unwrappedProperty.beanDeserializer.init(decoderContext); + } + } + } + + boolean tryConsume(String propertyName, Decoder decoder, DecoderContext decoderContext) throws IOException { + if (allConsumed) { + return false; + } + final DeserBean.DerProperty property = creatorParameters.consume(propertyName); + if (property == null) { + if (unwrappedProperties != null) { + for (UnwrappedPropertyDeserializer unwrappedProperty : unwrappedProperties) { + if (unwrappedProperty.tryConsume(propertyName, decoder, decoderContext)) { + return true; } - satisfied = true; // belongs to another bean, that bean will handle setting the property - break; } } - if (!satisfied) { - der.setDefaultPropertyValue(decoderContext, object); + if (anyValuesDeserializer != null) { + return anyValuesDeserializer.tryConsume(propertyName, decoder, decoderContext); } + return false; } + if (property.views != null && !decoderContext.hasView(property.views)) { + decoder.skipValue(); + return true; + } + Object value; + if (property.backRef != null) { + final PropertyReference ref = decoderContext.resolveReference( + new PropertyReference<>( + property.backRef, + property.instrospection, + property.argument, + null + ) + ); + if (ref != null) { + value = ref.getReference(); + } else { + value = null; + } + } else { + value = deserializeValue(decoderContext, decoder, property); + } + if (value == null) { + property.setDefaultConstructorValue(decoderContext, values); + } else { + values[property.index] = value; + } + return true; } - return object; - } - - @Override - public void deserializeInto(Decoder decoder, DecoderContext decoderContext, Argument type, Object value) - throws IOException { - PropertiesBag.Consumer readProperties = deserBean.readProperties != null ? deserBean.readProperties.newConsumer() : null; - boolean hasProperties = readProperties != null; - boolean ignoreUnknown = this.ignoreUnknown && deserBean.ignoreUnknown; - AnyValues anyValues = deserBean.anySetter != null ? new AnyValues<>(deserBean.anySetter) : null; - final Decoder objectDecoder = decoder.decodeObject(type); - if (hasProperties) { - final PropertyBuffer propertyBuffer = decodeProperties(deserBean, - decoderContext, - value, - objectDecoder, - readProperties, - deserBean.unwrappedProperties == null, - null, - anyValues, - ignoreUnknown, - type); - // the property buffer will be non-null if there were any unwrapped - // properties in which case we need to go through and materialize unwrapped - // from the buffer - applyDefaultValuesOrFail( - value, - readProperties, - deserBean.unwrappedProperties, - propertyBuffer, - decoderContext - ); - } - finalizeObjectDecoder( - decoderContext, - type, - ignoreUnknown, - objectDecoder, - anyValues, - value - ); - } - private static final class AnyValues { - Map values; - final DeserBean.AnySetter anySetter; + boolean isAllConsumed() { + if (allConsumed) { + return true; + } + if (!creatorParameters.isAllConsumed()) { + return false; + } + if (unwrappedProperties != null) { + for (UnwrappedPropertyDeserializer unwrappedProperty : unwrappedProperties) { + if (!unwrappedProperty.isAllConsumed()) { + return false; + } - private AnyValues(DeserBean.AnySetter anySetter) { - this.anySetter = anySetter; + } + } + allConsumed = true; + return true; } - void handle(String property, Decoder objectDecoder, DecoderContext decoderContext) throws IOException { - if (values == null) { - values = new LinkedHashMap<>(); + Object[] getValues(DecoderContext decoderContext) throws IOException { + if (anyValuesDeserializer != null) { + anyValuesDeserializer.bind(values); } - if (objectDecoder.decodeNull()) { - values.put(property, null); - } else { - if (anySetter.deserializer != null) { - T deserializedValue = anySetter.deserializer.deserializeNullable( - objectDecoder, - decoderContext, - anySetter.valueType + if (unwrappedProperties != null) { + for (UnwrappedPropertyDeserializer unwrappedProperty : unwrappedProperties) { + Object value = unwrappedProperty.beanDeserializer.provideInstance(decoderContext); + DeserBean.DerProperty wrappedProperty = unwrappedProperty.wrappedProperty; + if (wrappedProperty.views != null && !decoderContext.hasView(wrappedProperty.views)) { + continue; + } + if (value == null) { + wrappedProperty.setDefaultConstructorValue(decoderContext, values); + } else { + values[wrappedProperty.index] = value; + } + } + } + DeserBean.DerProperty[] propertiesArray = parameters.getPropertiesArray(); + for (int i = 0; i < propertiesArray.length; i++) { + if (creatorParameters.isConsumed(i)) { + continue; + } + DeserBean.DerProperty property = propertiesArray[i]; + if (property.unwrapped != null) { + continue; + } + Object value = null; + if (property.backRef != null) { + final PropertyReference ref = decoderContext.resolveReference( + new PropertyReference<>( + property.backRef, + property.instrospection, + property.argument, + null + ) ); - values.put(property, deserializedValue); + if (ref != null) { + value = ref.getReference(); + } + } + if (value == null) { + property.setDefaultConstructorValue(decoderContext, values); } else { - //noinspection unchecked - values.put(property, (T) objectDecoder.decodeArbitrary()); + values[i] = value; } } + return values; } + } - void bind(Object obj) { - if (values != null) { - anySetter.bind(values, obj); + /** + * Deserializes the unwrapped properties into the wrapped bean. + * + * @author Denis Stepanov + */ + private static final class UnwrappedPropertyDeserializer { + + private final DeserBean.DerProperty wrappedProperty; + private final BeanDeserializer beanDeserializer; + + private UnwrappedPropertyDeserializer(DeserBean.DerProperty unwrappedProperty, + boolean strictNullable, + @Nullable SerdeDeserializationPreInstantiateCallback preInstantiateCallback) { + this.wrappedProperty = unwrappedProperty; + this.beanDeserializer = newBeanDeserializer(null, unwrappedProperty.unwrapped, strictNullable, preInstantiateCallback); + } + + boolean tryConsume(String propertyName, Decoder decoder, DecoderContext decoderContext) throws IOException { + if (wrappedProperty.views != null && !decoderContext.hasView(wrappedProperty.views)) { + return false; } + return beanDeserializer.tryConsume(propertyName, decoder, decoderContext); + } + + boolean isAllConsumed() { + return beanDeserializer.isAllConsumed(); } } - private static final class TokenBuffer implements Iterable { - private final String name; - private final Decoder decoder; - private final TokenBuffer next; + /** + * Deserializes a bean with a non-empty constructor. + * + * @author Denis Stepanov + */ + private static final class ArgsConstructorBeanDeserializer extends BeanDeserializer { + + private final boolean strictNullable; + @Nullable + private SerdeDeserializationPreInstantiateCallback preInstantiateCallback; + private final BeanIntrospection introspection; + private final ConstructorValuesDeserializer constructorValuesDeserializer; + @Nullable + private final CachedPropertiesValuesDeserializer propertiesConsumer; + @Nullable + private final AnyValuesDeserializer anyValuesDeserializer; + + ArgsConstructorBeanDeserializer(DeserBean db, + boolean strictNullable, + @Nullable SerdeDeserializationPreInstantiateCallback preInstantiateCallback) { + this.strictNullable = strictNullable; + this.preInstantiateCallback = preInstantiateCallback; + this.introspection = db.introspection; + constructorValuesDeserializer = new ConstructorValuesDeserializer(db, strictNullable, preInstantiateCallback); + if (db.injectProperties == null) { + propertiesConsumer = null; + } else { + propertiesConsumer = new CachedPropertiesValuesDeserializer(db, strictNullable, preInstantiateCallback); + } + if (db.anySetter == null) { + anyValuesDeserializer = null; + } else { + anyValuesDeserializer = new AnyValuesDeserializer(db.anySetter); + } + } - private TokenBuffer(@NonNull String name, @NonNull Decoder decoder, @Nullable TokenBuffer next) { - this.name = name; - this.decoder = decoder; - this.next = next; + @Override + boolean tryConsume(String propertyName, Decoder decoder, DecoderContext decoderContext) throws IOException { + if (constructorValuesDeserializer.tryConsume(propertyName, decoder, decoderContext)) { + return true; + } + if (propertiesConsumer != null && propertiesConsumer.tryConsume(propertyName, decoder, decoderContext)) { + return true; + } + return anyValuesDeserializer != null && anyValuesDeserializer.tryConsume(propertyName, decoder, decoderContext); } - TokenBuffer next(@NonNull String name, @NonNull Decoder decoder) { - return new TokenBuffer(name, decoder, this); + @Override + boolean isAllConsumed() { + return anyValuesDeserializer == null && constructorValuesDeserializer.isAllConsumed() && (propertiesConsumer == null || propertiesConsumer.isAllConsumed()); } @Override - public Iterator iterator() { - return new Iterator() { - SpecificObjectDeserializer.TokenBuffer thisBuffer = null; + void init(DecoderContext decoderContext) throws SerdeException { + constructorValuesDeserializer.init(decoderContext); + if (propertiesConsumer != null) { + propertiesConsumer.init(decoderContext); + } + } - @Override - public boolean hasNext() { - return thisBuffer == null || thisBuffer.next != null; + @Override + public Object provideInstance(DecoderContext decoderContext) throws IOException { + Object instance; + try { + Object[] values = constructorValuesDeserializer.getValues(decoderContext); + if (anyValuesDeserializer != null && anyValuesDeserializer.anySetter.constructorArgument) { + anyValuesDeserializer.bind(values); } - - @Override - public SpecificObjectDeserializer.TokenBuffer next() throws NoSuchElementException { - if (thisBuffer == null) { - thisBuffer = SpecificObjectDeserializer.TokenBuffer.this; - } else { - thisBuffer = thisBuffer.next; - } - if (thisBuffer == null) { - throw new NoSuchElementException(); - } - return thisBuffer; + if (preInstantiateCallback != null) { + preInstantiateCallback.preInstantiate(introspection, values); } - }; + instance = introspection.instantiate(strictNullable, values); + } catch (InstantiationException e) { + throw new SerdeException(PREFIX_UNABLE_TO_DESERIALIZE_TYPE + introspection.getBeanType() + "]: " + e.getMessage(), e); + } + if (propertiesConsumer != null) { + propertiesConsumer.injectProperties(instance, decoderContext); + } + if (anyValuesDeserializer != null && !anyValuesDeserializer.anySetter.constructorArgument) { + anyValuesDeserializer.bind(instance); + } + return instance; } } - private static final class PropertyBuffer implements Iterable { - - private final DeserBean.DerProperty property; - private final String name; - private Object value; - private final PropertyBuffer next; - - public PropertyBuffer(DeserBean.DerProperty derProperty, - String name, - Object val, - @Nullable PropertyBuffer next) { - //noinspection unchecked - this.property = (DeserBean.DerProperty) derProperty; - this.name = name; - this.value = val; - this.next = next; + /** + * Deserializes a bean with a no-args constructor. + * + * @author Denis Stepanov + */ + private static final class NoArgsConstructorDeserializer extends BeanDeserializer { + + @Nullable + private final SerdeDeserializationPreInstantiateCallback preInstantiateCallback; + private final BeanIntrospection introspection; + @Nullable + private final PropertiesValuesDeserializer propertiesConsumer; + @Nullable + private final AnyValuesDeserializer anyValuesDeserializer; + private Object instance; + + NoArgsConstructorDeserializer(Object instance, + DeserBean db, + boolean strictNullable, + @Nullable SerdeDeserializationPreInstantiateCallback preInstantiateCallback) { + this.instance = instance; + this.introspection = db.introspection; + this.preInstantiateCallback = preInstantiateCallback; + if (db.injectProperties != null) { + this.propertiesConsumer = new PropertiesValuesDeserializer(db, strictNullable, preInstantiateCallback); + } else { + this.propertiesConsumer = null; + } + if (db.anySetter == null) { + anyValuesDeserializer = null; + } else { + anyValuesDeserializer = new AnyValuesDeserializer(db.anySetter); + } } - PropertyBuffer next(DeserBean.DerProperty rp, String property, Object val) { - return new PropertyBuffer(rp, property, val, this); + @Override + boolean tryConsume(String propertyName, Decoder decoder, DecoderContext decoderContext) throws IOException { + if (propertiesConsumer != null && propertiesConsumer.tryConsumeAndSet(propertyName, decoder, decoderContext, instance)) { + return true; + } + return anyValuesDeserializer != null && anyValuesDeserializer.tryConsume(propertyName, decoder, decoderContext); } @Override - public Iterator iterator() { - return new Iterator<>() { - SpecificObjectDeserializer.PropertyBuffer thisBuffer = null; + boolean isAllConsumed() { + return anyValuesDeserializer == null && (propertiesConsumer == null || propertiesConsumer.isAllConsumed()); + } - @Override - public boolean hasNext() { - return thisBuffer == null || thisBuffer.next != null; + @Override + void init(DecoderContext decoderContext) throws SerdeException { + if (propertiesConsumer != null) { + propertiesConsumer.init(decoderContext); + } + if (instance == null) { + try { + if (preInstantiateCallback != null) { + preInstantiateCallback.preInstantiate(introspection, ArrayUtils.EMPTY_OBJECT_ARRAY); + } + instance = introspection.instantiate(ArrayUtils.EMPTY_OBJECT_ARRAY); + } catch (InstantiationException e) { + throw new SerdeException(PREFIX_UNABLE_TO_DESERIALIZE_TYPE + introspection.getBeanType() + "]: " + e.getMessage(), e); } + } + } - @Override - public SpecificObjectDeserializer.PropertyBuffer next() throws NoSuchElementException { - if (thisBuffer == null) { - thisBuffer = SpecificObjectDeserializer.PropertyBuffer.this; - } else { - thisBuffer = thisBuffer.next; - } + @Override + public Object provideInstance(DecoderContext decoderContext) throws IOException { + if (propertiesConsumer != null) { + propertiesConsumer.finalizeProperties(decoderContext, instance); + } + if (anyValuesDeserializer != null) { + anyValuesDeserializer.bind(instance); + } + return instance; + } + } - if (thisBuffer == null) { - throw new NoSuchElementException(); - } - return thisBuffer; - } - }; + /** + * Deserializes a bean using a builder. + * + * @author Denis Stepanov + */ + private static final class BuilderDeserializer extends BeanDeserializer { + + @Nullable + private final SerdeDeserializationPreInstantiateCallback preInstantiateCallback; + private final BeanIntrospection introspection; + private final PropertiesBag.Consumer propertiesConsumer; + private BeanIntrospection.Builder builder; + + BuilderDeserializer(DeserBean db, + @Nullable SerdeDeserializationPreInstantiateCallback preInstantiateCallback) { + this.introspection = db.introspection; + this.preInstantiateCallback = preInstantiateCallback; + this.propertiesConsumer = db.injectProperties.newConsumer(); } - public void set(Object obj, DecoderContext decoderContext) throws IOException { - if (value instanceof Decoder decoder) { - if (property.managedRef != null) { - decoderContext.pushManagedRef( - new PropertyReference<>( - property.managedRef, - property.instrospection, - property.argument, - obj - ) - ); - } - try { - value = property.deserializer.deserializeNullable( - decoder, - decoderContext, - property.argument - ); - } catch (InvalidFormatException e) { - throw new InvalidPropertyFormatException( - e, - property.argument - ); - } finally { - decoderContext.popManagedRef(); - } + @Override + boolean tryConsume(String propertyName, Decoder decoder, DecoderContext decoderContext) throws IOException { + final DeserBean.DerProperty property = propertiesConsumer.consume(propertyName); + if (property != null) { + property.deserializeAndCallBuilder(decoder, decoderContext, builder); + return true; } - property.set(obj, value); + return false; } - public void set(Object[] params, DecoderContext decoderContext) throws IOException { - if (value instanceof Decoder decoder) { - try { - value = property.deserializer.deserializeNullable( - decoder, - decoderContext, - property.argument - ); - } catch (InvalidFormatException e) { - throw new InvalidPropertyFormatException( - e, - property.argument - ); + @Override + boolean isAllConsumed() { + return propertiesConsumer.isAllConsumed(); + } + + @Override + void init(DecoderContext decoderContext) throws SerdeException { + try { + if (preInstantiateCallback != null) { + preInstantiateCallback.preInstantiate(introspection); } + builder = introspection.builder(); + } catch (InstantiationException e) { + throw new SerdeException(PREFIX_UNABLE_TO_DESERIALIZE_TYPE + introspection.getBeanType() + "]: " + e.getMessage(), e); + } + } + @Override + public Object provideInstance(DecoderContext decoderContext) throws IOException { + try { + return builder.build(); + } catch (InstantiationException e) { + throw new SerdeException(PREFIX_UNABLE_TO_DESERIALIZE_TYPE + introspection.getBeanType() + "]: " + e.getMessage(), e); } - params[property.index] = value; } } + /** + * The bean deserializes based on its shape. + * + * @author Denis Stepanov + */ + private abstract static sealed class BeanDeserializer { + + abstract boolean tryConsume(String propertyName, Decoder decoder, DecoderContext decoderContext) throws IOException; + + abstract boolean isAllConsumed(); + + abstract void init(DecoderContext decoderContext) throws SerdeException; + + abstract Object provideInstance(DecoderContext decoderContext) throws IOException; + + } + } diff --git a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SubtypedDeserBean.java b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SubtypedDeserBean.java deleted file mode 100644 index 40befd9e5..000000000 --- a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SubtypedDeserBean.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2017-2021 original authors - * - * 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 - * - * https://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 io.micronaut.serde.support.deserializers; - -import io.micronaut.context.annotation.DefaultImplementation; -import io.micronaut.core.annotation.AnnotationMetadata; -import io.micronaut.core.annotation.Internal; -import io.micronaut.core.annotation.NonNull; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.beans.BeanIntrospection; -import io.micronaut.core.type.Argument; -import io.micronaut.serde.Deserializer; -import io.micronaut.serde.config.annotation.SerdeConfig; -import io.micronaut.serde.exceptions.SerdeException; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -import static io.micronaut.serde.config.annotation.SerdeConfig.SerSubtyped.DiscriminatorValueKind.CLASS_NAME; - -/** - * Models subtype deserialization. - * - * @param The generic type - */ -@Internal -class SubtypedDeserBean extends DeserBean { - // CHECKSTYLE:OFF - @NonNull - public final Map> subtypes; - @NonNull - public final SerdeConfig.SerSubtyped.DiscriminatorType discriminatorType; - @NonNull - public final SerdeConfig.SerSubtyped.DiscriminatorValueKind discriminatorValue; - @NonNull - public final String discriminatorName; - - @Nullable - public final String defaultImpl; - // CHECKSTYLE:ON - - SubtypedDeserBean(AnnotationMetadata annotationMetadata, - BeanIntrospection introspection, - Deserializer.DecoderContext decoderContext, - DeserBeanRegistry deserBeanRegistry) throws SerdeException { - super(introspection, decoderContext, deserBeanRegistry); - this.discriminatorType = annotationMetadata.enumValue( - SerdeConfig.SerSubtyped.class, - SerdeConfig.SerSubtyped.DISCRIMINATOR_TYPE, - SerdeConfig.SerSubtyped.DiscriminatorType.class - ).orElse(SerdeConfig.SerSubtyped.DiscriminatorType.PROPERTY); - this.discriminatorValue = annotationMetadata.enumValue( - SerdeConfig.SerSubtyped.class, - SerdeConfig.SerSubtyped.DISCRIMINATOR_VALUE, - SerdeConfig.SerSubtyped.DiscriminatorValueKind.class - ).orElse(CLASS_NAME); - this.discriminatorName = annotationMetadata.stringValue( - SerdeConfig.SerSubtyped.class, - SerdeConfig.SerSubtyped.DISCRIMINATOR_PROP - ).orElse(discriminatorValue == CLASS_NAME ? "@class" : "@type"); - - final Class superType = introspection.getBeanType(); - final Collection> subtypeIntrospections = - decoderContext.getDeserializableSubtypes(superType); - this.subtypes = new HashMap<>(subtypeIntrospections.size()); - Class defaultType = annotationMetadata.classValue(DefaultImplementation.class).orElse(null); - String defaultDiscriminator = null; - for (BeanIntrospection subtypeIntrospection : subtypeIntrospections) { - Class subBeanType = subtypeIntrospection.getBeanType(); - final DeserBean deserBean = deserBeanRegistry.getDeserializableBean( - Argument.of(subBeanType), - decoderContext - ); - final String discriminatorName; - if (discriminatorValue == SerdeConfig.SerSubtyped.DiscriminatorValueKind.CLASS_NAME) { - discriminatorName = subBeanType.getName(); - } else if (discriminatorValue == SerdeConfig.SerSubtyped.DiscriminatorValueKind.CLASS_SIMPLE_NAME) { - discriminatorName = subBeanType.getSimpleName(); - } else { - discriminatorName = deserBean.introspection.stringValue(SerdeConfig.class, SerdeConfig.TYPE_NAME) - .orElse(deserBean.introspection.getBeanType().getSimpleName()); - } - this.subtypes.put( - discriminatorName, - deserBean - ); - if (defaultType != null && defaultType.equals(subBeanType)) { - defaultDiscriminator = discriminatorName; - } - - String[] names = subtypeIntrospection.stringValues(SerdeConfig.class, SerdeConfig.TYPE_NAMES); - for (String name: names) { - this.subtypes.put(name, deserBean); - } - } - this.defaultImpl = defaultDiscriminator; - } - - @Override - public boolean isSubtyped() { - return true; - } -} diff --git a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SubtypedPropertyObjectDeserializer.java b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SubtypedPropertyObjectDeserializer.java new file mode 100644 index 000000000..ce2595268 --- /dev/null +++ b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SubtypedPropertyObjectDeserializer.java @@ -0,0 +1,95 @@ +/* + * Copyright 2017-2021 original authors + * + * 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 + * + * https://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 io.micronaut.serde.support.deserializers; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.type.Argument; +import io.micronaut.serde.Decoder; +import io.micronaut.serde.Deserializer; +import io.micronaut.serde.config.annotation.SerdeConfig; + +import java.io.IOException; +import java.util.Map; + +/** + * Implementation for deserialization of objects that uses introspection metadata. + * + * @author graemerocher + * @since 1.0.0 + */ +final class SubtypedPropertyObjectDeserializer implements Deserializer { + + private final DeserBean deserBean; + private final Map> deserializers; + private final Deserializer supertypeDeserializer; + + public SubtypedPropertyObjectDeserializer(DeserBean deserBean, + Map> deserializers, + Deserializer supertypeDeserializer) { + this.deserBean = deserBean; + this.deserializers = deserializers; + this.supertypeDeserializer = supertypeDeserializer; + if (deserBean.subtypeInfo.discriminatorType() != SerdeConfig.SerSubtyped.DiscriminatorType.PROPERTY) { + throw new IllegalStateException("Unsupported discriminator type: " + deserBean.subtypeInfo.discriminatorType()); + } + } + + @Override + public Object deserialize(Decoder decoder, DecoderContext decoderContext, Argument type) + throws IOException { + try (Decoder primed = DemuxingObjectDecoder.prime(decoder)) { + Decoder typeFinder = primed.decodeObject(type); + Deserializer deserializer = findDeserializer(typeFinder); + typeFinder.finishStructure(true); + + return deserializer.deserialize( + primed, + decoderContext, + type + ); + } + } + + @NonNull + private Deserializer findDeserializer(Decoder objectDecoder) throws IOException { + final String defaultImpl = deserBean.subtypeInfo.defaultImpl(); + final String discriminatorName = deserBean.subtypeInfo.discriminatorName(); + while (true) { + final String key = objectDecoder.decodeKey(); + if (key == null) { + break; + } + + if (key.equals(discriminatorName)) { + if (!objectDecoder.decodeNull()) { + final String subtypeName = objectDecoder.decodeString(); + final Deserializer deserializer = deserializers.get(subtypeName); + if (deserializer != null) { + return deserializer; + } + } + break; + } else { + objectDecoder.skipValue(); + } + } + if (defaultImpl != null) { + return deserializers.get(defaultImpl); + } + return supertypeDeserializer; + } + +} diff --git a/serde-support/src/main/java/io/micronaut/serde/support/util/BeanDefKey.java b/serde-support/src/main/java/io/micronaut/serde/support/util/BeanDefKey.java new file mode 100644 index 000000000..cae688bfb --- /dev/null +++ b/serde-support/src/main/java/io/micronaut/serde/support/util/BeanDefKey.java @@ -0,0 +1,64 @@ +/* + * Copyright 2017-2021 original authors + * + * 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 + * + * https://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 io.micronaut.serde.support.util; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.type.Argument; + +import java.util.Objects; + +/** + * Can be used as a key for type. + */ +@Internal +public final class BeanDefKey { + private final Argument type; + @Nullable + private final String prefix; + @Nullable + private final String suffix; + private final int hashCode; + + public BeanDefKey(@NonNull Argument type, @Nullable String prefix, @Nullable String suffix) { + this.type = type; + this.hashCode = type.typeHashCode(); + this.prefix = prefix; + this.suffix = suffix; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BeanDefKey that = (BeanDefKey) o; + return type.equalsType(that.type) && Objects.equals(prefix, that.prefix) && Objects.equals(suffix, that.suffix); + } + + @Override + public int hashCode() { + return hashCode; + } + + public @NonNull Argument getType() { + return type; + } +} diff --git a/serde-support/src/main/java/io/micronaut/serde/support/util/JsonArrayNodeDecoder.java b/serde-support/src/main/java/io/micronaut/serde/support/util/JsonArrayNodeDecoder.java new file mode 100644 index 000000000..68e30e5be --- /dev/null +++ b/serde-support/src/main/java/io/micronaut/serde/support/util/JsonArrayNodeDecoder.java @@ -0,0 +1,66 @@ +/* + * Copyright 2017-2023 original authors + * + * 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 + * + * https://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 io.micronaut.serde.support.util; + +import io.micronaut.json.tree.JsonNode; + +import java.util.Iterator; + +final class JsonArrayNodeDecoder extends JsonNodeDecoder { + + private final Iterator iterator; + private JsonNode peeked; + + JsonArrayNodeDecoder(JsonNode node, RemainingLimits remainingLimits) { + super(remainingLimits); + iterator = node.values().iterator(); + skipValue(); + } + + @Override + public boolean hasNextArrayValue() { + return peeked != null; + } + + @Override + public String decodeKey() { + throw new IllegalStateException("Arrays have no keys"); + } + + @Override + public void skipValue() { + if (iterator.hasNext()) { + peeked = iterator.next(); + } else { + peeked = null; + } + } + + @Override + public void finishStructure(boolean consumeLeftElements) { + if (!consumeLeftElements && peeked != null) { + throw new IllegalStateException("Not all elements have been consumed yet"); + } + } + + @Override + protected JsonNode peekValue() { + if (peeked == null) { + throw new IllegalStateException("No more elements"); + } + return peeked; + } +} diff --git a/serde-support/src/main/java/io/micronaut/serde/support/util/JsonNodeDecoder.java b/serde-support/src/main/java/io/micronaut/serde/support/util/JsonNodeDecoder.java index a563dbbde..0a6ecfd3b 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/util/JsonNodeDecoder.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/util/JsonNodeDecoder.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.type.Argument; +import io.micronaut.core.util.CollectionUtils; import io.micronaut.json.tree.JsonNode; import io.micronaut.serde.Decoder; import io.micronaut.serde.LimitingStream; @@ -29,8 +30,6 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -39,8 +38,8 @@ * uses the {@link io.micronaut.json.tree.JsonNode} abstraction. */ @Internal -public abstract class JsonNodeDecoder extends LimitingStream implements Decoder { - private JsonNodeDecoder(LimitingStream.RemainingLimits remainingLimits) { +public abstract sealed class JsonNodeDecoder extends LimitingStream implements Decoder permits JsonArrayNodeDecoder, JsonNodeDecoder.Buffered, JsonObjectNodeDecoder { + JsonNodeDecoder(LimitingStream.RemainingLimits remainingLimits) { super(remainingLimits); } @@ -48,16 +47,16 @@ public static JsonNodeDecoder create(JsonNode node, LimitingStream.RemainingLimi return new Buffered(node, remainingLimits); } - protected abstract JsonNode peekValue(); + protected abstract JsonNode peekValue() throws IOException; @Override public Decoder decodeArray(Argument type) throws IOException { JsonNode peeked = peekValue(); if (peeked.isArray()) { skipValue(); - return new Array(peeked, childLimits()); + return new JsonArrayNodeDecoder(peeked, childLimits()); } else { - throw createDeserializationException("Not an array", null); + throw createDeserializationException("Not an array", toArbitrary(peeked)); } } @@ -66,9 +65,9 @@ public Decoder decodeObject(Argument type) throws IOException { JsonNode peeked = peekValue(); if (peeked.isObject()) { skipValue(); - return new Obj(peeked, childLimits()); + return new JsonObjectNodeDecoder(peeked, childLimits()); } else { - throw createDeserializationException("Not an array", null); + throw createDeserializationException("Not an array", toArbitrary(peeked)); } } @@ -79,7 +78,7 @@ public String decodeString() throws IOException { skipValue(); return peeked.getStringValue(); } else { - throw createDeserializationException("Not a string", toArbitrary(peekValue())); + throw createDeserializationException("Not a string", toArbitrary(peeked)); } } @@ -90,7 +89,7 @@ public boolean decodeBoolean() throws IOException { skipValue(); return peeked.getBooleanValue(); } else { - throw createDeserializationException("Not a boolean", toArbitrary(peekValue())); + throw createDeserializationException("Not a boolean", toArbitrary(peeked)); } } @@ -101,7 +100,7 @@ public byte decodeByte() throws IOException { skipValue(); return (byte) peeked.getIntValue(); } else { - throw createDeserializationException("Not a number", toArbitrary(peekValue())); + throw createDeserializationException("Not a number", toArbitrary(peeked)); } } @@ -112,7 +111,7 @@ public short decodeShort() throws IOException { skipValue(); return (short) peeked.getIntValue(); } else { - throw createDeserializationException("Not a number", toArbitrary(peekValue())); + throw createDeserializationException("Not a number", toArbitrary(peeked)); } } @@ -123,7 +122,7 @@ public char decodeChar() throws IOException { skipValue(); return (char) peeked.getIntValue(); } else { - throw createDeserializationException("Not a number", toArbitrary(peekValue())); + throw createDeserializationException("Not a number", toArbitrary(peeked)); } } @@ -134,7 +133,7 @@ public int decodeInt() throws IOException { skipValue(); return peeked.getIntValue(); } else { - throw createDeserializationException("Not a number", toArbitrary(peekValue())); + throw createDeserializationException("Not a number", toArbitrary(peeked)); } } @@ -145,7 +144,7 @@ public long decodeLong() throws IOException { skipValue(); return peeked.getLongValue(); } else { - throw createDeserializationException("Not a number", toArbitrary(peekValue())); + throw createDeserializationException("Not a number", toArbitrary(peeked)); } } @@ -156,7 +155,7 @@ public float decodeFloat() throws IOException { skipValue(); return peeked.getFloatValue(); } else { - throw createDeserializationException("Not a number", toArbitrary(peekValue())); + throw createDeserializationException("Not a number", toArbitrary(peeked)); } } @@ -167,7 +166,7 @@ public double decodeDouble() throws IOException { skipValue(); return peeked.getDoubleValue(); } else { - throw createDeserializationException("Not a number", toArbitrary(peekValue())); + throw createDeserializationException("Not a number", toArbitrary(peeked)); } } @@ -178,7 +177,7 @@ public BigInteger decodeBigInteger() throws IOException { skipValue(); return peeked.getBigIntegerValue(); } else { - throw createDeserializationException("Not a number", toArbitrary(peekValue())); + throw createDeserializationException("Not a number", toArbitrary(peeked)); } } @@ -189,7 +188,7 @@ public BigDecimal decodeBigDecimal() throws IOException { skipValue(); return peeked.getBigDecimalValue(); } else { - throw createDeserializationException("Not a number", toArbitrary(peekValue())); + throw createDeserializationException("Not a number", toArbitrary(peeked)); } } @@ -237,13 +236,13 @@ private static Object toArbitrary(JsonNode node) { } else if (node.isString()) { return node.getStringValue(); } else if (node.isArray()) { - List transformed = new ArrayList<>(); + List transformed = new ArrayList<>(node.size()); for (JsonNode value : node.values()) { transformed.add(toArbitrary(value)); } return transformed; } else if (node.isObject()) { - Map transformed = new LinkedHashMap<>(); + Map transformed = CollectionUtils.newLinkedHashMap(node.size()); for (Map.Entry entry : node.entries()) { transformed.put(entry.getKey(), toArbitrary(entry.getValue())); } @@ -269,104 +268,7 @@ public IOException createDeserializationException(String message, Object invalid } } - private static class Obj extends JsonNodeDecoder { - private final Iterator> iterator; - private JsonNode nextValue = null; - - Obj(JsonNode node, RemainingLimits remainingLimits) { - super(remainingLimits); - iterator = node.entries().iterator(); - } - - @Override - protected JsonNode peekValue() { - if (nextValue == null) { - throw new IllegalStateException("Field name not parsed yet"); - } - return nextValue; - } - - @Override - public void skipValue() { - if (nextValue == null) { - throw new IllegalStateException("Field name not parsed yet"); - } - nextValue = null; - } - - @Override - public boolean hasNextArrayValue() { - return false; - } - - @Override - public String decodeKey() { - if (nextValue != null) { - throw new IllegalStateException("Field value not parsed yet"); - } - if (iterator.hasNext()) { - Map.Entry next = iterator.next(); - nextValue = next.getValue(); - return next.getKey(); - } else { - return null; - } - } - - @Override - public void finishStructure(boolean consumeLeftElements) { - if (!consumeLeftElements && (nextValue != null || iterator.hasNext())) { - throw new IllegalStateException("Not all elements have been consumed yet"); - } - } - } - - private static class Array extends JsonNodeDecoder { - private final Iterator iterator; - private JsonNode peeked; - - Array(JsonNode node, RemainingLimits remainingLimits) { - super(remainingLimits); - iterator = node.values().iterator(); - skipValue(); - } - - @Override - public boolean hasNextArrayValue() { - return peeked != null; - } - - @Override - public String decodeKey() { - throw new IllegalStateException("Arrays have no keys"); - } - - @Override - public void skipValue() { - if (iterator.hasNext()) { - peeked = iterator.next(); - } else { - peeked = null; - } - } - - @Override - public void finishStructure(boolean consumeLeftElements) { - if (!consumeLeftElements && peeked != null) { - throw new IllegalStateException("Not all elements have been consumed yet"); - } - } - - @Override - protected JsonNode peekValue() { - if (peeked == null) { - throw new IllegalStateException("No more elements"); - } - return peeked; - } - } - - private static class Buffered extends JsonNodeDecoder { + static final class Buffered extends JsonNodeDecoder { private final JsonNode node; private boolean complete = false; diff --git a/serde-support/src/main/java/io/micronaut/serde/support/util/JsonObjectNodeDecoder.java b/serde-support/src/main/java/io/micronaut/serde/support/util/JsonObjectNodeDecoder.java new file mode 100644 index 000000000..ed8cd720e --- /dev/null +++ b/serde-support/src/main/java/io/micronaut/serde/support/util/JsonObjectNodeDecoder.java @@ -0,0 +1,73 @@ +/* + * Copyright 2017-2023 original authors + * + * 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 + * + * https://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 io.micronaut.serde.support.util; + +import io.micronaut.json.tree.JsonNode; + +import java.util.Iterator; +import java.util.Map; + +final class JsonObjectNodeDecoder extends JsonNodeDecoder { + private final Iterator> iterator; + private JsonNode nextValue = null; + + JsonObjectNodeDecoder(JsonNode node, RemainingLimits remainingLimits) { + super(remainingLimits); + iterator = node.entries().iterator(); + } + + @Override + protected JsonNode peekValue() { + if (nextValue == null) { + throw new IllegalStateException("Field name not parsed yet"); + } + return nextValue; + } + + @Override + public void skipValue() { + if (nextValue == null) { + throw new IllegalStateException("Field name not parsed yet"); + } + nextValue = null; + } + + @Override + public boolean hasNextArrayValue() { + return false; + } + + @Override + public String decodeKey() { + if (nextValue != null) { + throw new IllegalStateException("Field value not parsed yet"); + } + if (iterator.hasNext()) { + Map.Entry next = iterator.next(); + nextValue = next.getValue(); + return next.getKey(); + } else { + return null; + } + } + + @Override + public void finishStructure(boolean consumeLeftElements) { + if (!consumeLeftElements && (nextValue != null || iterator.hasNext())) { + throw new IllegalStateException("Not all elements have been consumed yet"); + } + } +} diff --git a/serde-support/src/test/groovy/io/micronaut/serde/support/deserializers/DemuxingObjectDecoderSpec.groovy b/serde-support/src/test/groovy/io/micronaut/serde/support/deserializers/DemuxingObjectDecoderSpec.groovy new file mode 100644 index 000000000..dd7fa4d72 --- /dev/null +++ b/serde-support/src/test/groovy/io/micronaut/serde/support/deserializers/DemuxingObjectDecoderSpec.groovy @@ -0,0 +1,104 @@ +package io.micronaut.serde.support.deserializers + +import io.micronaut.context.ApplicationContext +import io.micronaut.json.JsonMapper +import io.micronaut.json.tree.JsonNode +import io.micronaut.serde.Decoder +import io.micronaut.serde.LimitingStream +import io.micronaut.serde.support.util.JsonNodeDecoder +import org.intellij.lang.annotations.Language +import spock.lang.Specification + +class DemuxingObjectDecoderSpec extends Specification { + def 'simple'() { + given: + def ctx = ApplicationContext.run() + def outerDecoder = createDecoder(ctx, """{"a": 1, "b": 2, "c": 3}""") + + def primed = DemuxingObjectDecoder.prime(outerDecoder) + def demux1 = primed.decodeObject() + def demux2 = primed.decodeObject() + + expect: + demux1.decodeKey() == "a" + demux1.decodeInt() == 1 + demux1.decodeKey() == "b" + demux1.skipValue() + demux1.decodeKey() == "c" + demux1.decodeInt() == 3 + demux1.decodeKey() == null + demux1.finishStructure() + + demux2.decodeKey() == "b" + demux2.decodeInt() == 2 + demux2.finishStructure() + + cleanup: + ctx.close() + } + + def 'simple structures'() { + given: + def ctx = ApplicationContext.run() + def outerDecoder = createDecoder(ctx, """{"a": [1], "b": {"foo": "bar"}, "c": {"fizz": "buzz"}}""") + + def primed = DemuxingObjectDecoder.prime(outerDecoder) + def demux1 = primed.decodeObject() + def demux2 = primed.decodeObject() + + expect: + demux1.decodeKey() == "a" + def arr1 = demux1.decodeArray() + arr1.decodeInt() == 1 + arr1.finishStructure() + demux1.decodeKey() == "b" + demux1.skipValue() + demux1.decodeKey() == "c" + def obj3 = demux1.decodeObject() + obj3.decodeKey() == "fizz" + obj3.decodeString() == "buzz" + obj3.finishStructure() + demux1.decodeKey() == null + demux1.finishStructure() + + demux2.decodeKey() == "b" + def obj2 = demux2.decodeObject() + obj2.decodeKey() == "foo" + obj2.decodeString() == "bar" + obj2.finishStructure() + demux2.finishStructure() + + cleanup: + ctx.close() + } + + def 'interleaved'() { + given: + def ctx = ApplicationContext.run() + def outerDecoder = createDecoder(ctx, """{"a": 1, "b": 2, "c": 3}""") + + def primed = DemuxingObjectDecoder.prime(outerDecoder) + def demux1 = primed.decodeObject() + def demux2 = primed.decodeObject() + + expect: + demux1.decodeKey() == "a" + demux1.decodeInt() == 1 + demux2.decodeKey() == "b" + !demux2.decodeNull() + demux2.decodeInt() == 2 + demux1.decodeKey() == "c" + demux1.decodeInt() == 3 + demux1.decodeKey() == null + demux1.finishStructure() + demux2.decodeKey() == null + demux2.finishStructure() + + cleanup: + ctx.close() + } + + private static Decoder createDecoder(ApplicationContext ctx, @Language("json") String json) { + JsonNodeDecoder.create(ctx.getBean(JsonMapper).readValue(json, JsonNode), LimitingStream.DEFAULT_LIMITS) + } +}