From 6a0f4d992598abac391723cc280f59e6e7a3d331 Mon Sep 17 00:00:00 2001 From: radovanradic Date: Thu, 16 Mar 2023 13:40:25 +0100 Subject: [PATCH] Support for missing Oracle JSON types serde (#398) * Added missing serde support for Oracle JSON --------- Co-authored-by: Graeme Rocher --- .../serde/bson/AbstractBsonMapper.java | 6 +- .../serde/jackson/JacksonJsonMapper.java | 6 +- .../jackson/annotation/JsonIgnoreSpec.groovy | 4 +- .../serde/json/stream/JsonStreamMapper.java | 10 +- .../json/OracleJdbcJsonGeneratorEncoder.java | 24 +- .../json/OracleJdbcJsonParserDecoder.java | 197 +++++++++++----- .../json/serde/AbstractOracleJsonSerde.java | 88 ++++++++ .../json/serde/OracleJsonBinarySerde.java | 57 +++++ .../json/serde/OracleJsonDurationSerde.java | 56 +++++ .../json/serde/OracleJsonLocaleDateSerde.java | 62 +++++ .../serde/OracleJsonLocaleDateTimeSerde.java | 57 +++++ .../serde/OracleJsonOffsetDateTimeSerde.java | 57 +++++ .../serde/OracleJsonZonedDateTimeSerde.java | 57 +++++ .../oracle/jdbc/json/serde/package-info.java | 22 ++ .../OracleJdbcJsonBinaryBasicSerdeSpec.groovy | 90 ++++++++ .../io/micronaut/serde/bson/SampleData.java | 165 ++++++++++++++ serde-support/build.gradle.kts | 5 + .../serde/support/DefaultSerdeRegistry.java | 211 +++++++++++++----- .../deserializers/CoreDeserializers.java | 27 --- .../support/deserializers/DeserBean.java | 2 +- .../SimpleObjectDeserializer.java | 10 +- .../serde/support/serdes/CoreSerdes.java | 85 ++++--- 22 files changed, 1126 insertions(+), 172 deletions(-) create mode 100644 serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/AbstractOracleJsonSerde.java create mode 100644 serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonBinarySerde.java create mode 100644 serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonDurationSerde.java create mode 100644 serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonLocaleDateSerde.java create mode 100644 serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonLocaleDateTimeSerde.java create mode 100644 serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonOffsetDateTimeSerde.java create mode 100644 serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonZonedDateTimeSerde.java create mode 100644 serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/package-info.java create mode 100644 serde-oracle-jdbc-json/src/test/java/io/micronaut/serde/bson/SampleData.java diff --git a/serde-bson/src/main/java/io/micronaut/serde/bson/AbstractBsonMapper.java b/serde-bson/src/main/java/io/micronaut/serde/bson/AbstractBsonMapper.java index 1690e905f..e2cedf741 100644 --- a/serde-bson/src/main/java/io/micronaut/serde/bson/AbstractBsonMapper.java +++ b/serde-bson/src/main/java/io/micronaut/serde/bson/AbstractBsonMapper.java @@ -91,7 +91,7 @@ public byte[] writeValueAsBytes(Argument type, T object) throws IOExcepti @Override public T readValueFromTree(JsonNode tree, Argument type) throws IOException { - final Deserializer deserializer = this.registry.findDeserializer(type).createSpecific(decoderContext, type); + final Deserializer deserializer = this.decoderContext.findDeserializer(type).createSpecific(decoderContext, type); return deserializer.deserialize(JsonNodeDecoder.create(tree), decoderContext, type); } @@ -112,7 +112,7 @@ private T readValue(ByteBuffer byteBuffer, Argument type) throws IOExcept } private T readValue(BsonReader bsonReader, Argument type) throws IOException { - return registry.findDeserializer(type) + return decoderContext.findDeserializer(type) .createSpecific(decoderContext, type) .deserialize(new BsonReaderDecoder(bsonReader), decoderContext, type); } @@ -167,7 +167,7 @@ private void serialize(Encoder encoder, Object object) throws IOException { } private void serialize(Encoder encoder, Object object, Argument type) throws IOException { - final Serializer serializer = registry.findSerializer(type).createSpecific(encoderContext, type); + final Serializer serializer = encoderContext.findSerializer(type).createSpecific(encoderContext, type); serializer.serialize(encoder, encoderContext, type, object); } diff --git a/serde-jackson/src/main/java/io/micronaut/serde/jackson/JacksonJsonMapper.java b/serde-jackson/src/main/java/io/micronaut/serde/jackson/JacksonJsonMapper.java index e095c4d18..0285d77d8 100644 --- a/serde-jackson/src/main/java/io/micronaut/serde/jackson/JacksonJsonMapper.java +++ b/serde-jackson/src/main/java/io/micronaut/serde/jackson/JacksonJsonMapper.java @@ -106,7 +106,7 @@ private void writeValue0(JsonGenerator gen, T value, Class type) throws I private void writeValue(JsonGenerator gen, T value, Argument argument) throws IOException { gen.setCodec(objectCodecImpl); - Serializer serializer = registry.findSerializer(argument) + Serializer serializer = encoderContext.findSerializer(argument) .createSpecific(encoderContext, argument); final Encoder encoder = JacksonEncoder.create(gen); serializer.serialize( @@ -123,7 +123,7 @@ private T readValue(JsonParser parser, Argument type) throws IOException @SuppressWarnings({"rawtypes", "unchecked"}) private T readValue0(JsonParser parser, Argument type) throws IOException { parser.setCodec(objectCodecImpl); - Deserializer deserializer = registry.findDeserializer(type).createSpecific(decoderContext, (Argument) type); + Deserializer deserializer = decoderContext.findDeserializer(type).createSpecific(decoderContext, (Argument) type); if (!parser.hasCurrentToken()) { parser.nextToken(); } @@ -248,7 +248,7 @@ public JsonMapper cloneWithViewClass(@NonNull Class viewClass) { public void updateValueFromTree(Object value, JsonNode tree) throws IOException { if (tree != null && value != null) { Argument type = (Argument) Argument.of(value.getClass()); - Deserializer deserializer = registry.findDeserializer(type).createSpecific(decoderContext, type); + Deserializer deserializer = decoderContext.findDeserializer(type).createSpecific(decoderContext, type); if (deserializer instanceof UpdatingDeserializer) { try (JsonParser parser = treeCodec.treeAsTokens(tree)) { diff --git a/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/JsonIgnoreSpec.groovy b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/JsonIgnoreSpec.groovy index ebacdc89d..2de905b32 100644 --- a/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/JsonIgnoreSpec.groovy +++ b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/JsonIgnoreSpec.groovy @@ -389,7 +389,7 @@ class Test { } - void "test @JsonIgnore without @Inherited on interface method is not inherited"() { + void "test @JsonIgnore without @Inherited on interface method is inherited"() { given: def context = buildContext('test.Test', """ package test; @@ -428,7 +428,7 @@ interface MyInterface { """, [value:'test']) expect: - writeJson(jsonMapper, beanUnderTest) == '{"value":"test","ignored":false}' + writeJson(jsonMapper, beanUnderTest) == '{"value":"test"}' cleanup: context.close() diff --git a/serde-jsonp/src/main/java/io/micronaut/serde/json/stream/JsonStreamMapper.java b/serde-jsonp/src/main/java/io/micronaut/serde/json/stream/JsonStreamMapper.java index 1081bdf99..81fcc4f30 100644 --- a/serde-jsonp/src/main/java/io/micronaut/serde/json/stream/JsonStreamMapper.java +++ b/serde-jsonp/src/main/java/io/micronaut/serde/json/stream/JsonStreamMapper.java @@ -21,6 +21,7 @@ import io.micronaut.json.JsonMapper; import io.micronaut.json.JsonStreamConfig; import io.micronaut.json.tree.JsonNode; +import io.micronaut.serde.Decoder; import io.micronaut.serde.Deserializer; import io.micronaut.serde.Encoder; import io.micronaut.serde.ObjectMapper; @@ -72,7 +73,7 @@ public JsonMapper cloneWithViewClass(Class viewClass) { @Override public T readValueFromTree(JsonNode tree, Argument type) throws IOException { Deserializer.DecoderContext context = registry.newDecoderContext(view); - final Deserializer deserializer = this.registry.findDeserializer(type).createSpecific(context, type); + final Deserializer deserializer = context.findDeserializer(type).createSpecific(context, type); return deserializer.deserialize( JsonNodeDecoder.create(tree), context, @@ -95,10 +96,11 @@ public T readValue(byte[] byteArray, Argument type) throws IOException { } private T readValue(JsonParser parser, Argument type) throws IOException { + Decoder decoder = new JsonParserDecoder(parser); Deserializer.DecoderContext context = registry.newDecoderContext(view); - final Deserializer deserializer = this.registry.findDeserializer(type).createSpecific(context, type); + final Deserializer deserializer = context.findDeserializer(type).createSpecific(context, type); return deserializer.deserialize( - new JsonParserDecoder(parser), + decoder, context, type ); @@ -166,7 +168,7 @@ private void serialize(Encoder encoder, Object object) throws IOException { private void serialize(Encoder encoder, Object object, Argument type) throws IOException { Serializer.EncoderContext context = registry.newEncoderContext(view); - final Serializer serializer = registry.findSerializer(type).createSpecific(context, type); + final Serializer serializer = context.findSerializer(type).createSpecific(context, type); serializer.serialize( encoder, context, diff --git a/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/OracleJdbcJsonGeneratorEncoder.java b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/OracleJdbcJsonGeneratorEncoder.java index f8eef6631..6c125aa3f 100644 --- a/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/OracleJdbcJsonGeneratorEncoder.java +++ b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/OracleJdbcJsonGeneratorEncoder.java @@ -23,6 +23,8 @@ import java.math.BigDecimal; import java.math.BigInteger; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; /** * Implementation of the {@link Encoder} interface for Oracle JDBC JSON. @@ -31,7 +33,7 @@ * @since 1.2.0 */ @Internal -final class OracleJdbcJsonGeneratorEncoder implements Encoder { +public final class OracleJdbcJsonGeneratorEncoder implements Encoder { private final OracleJsonGenerator jsonGenerator; private final OracleJdbcJsonGeneratorEncoder parent; private String currentKey; @@ -170,4 +172,24 @@ public String currentPath() { } return builder.toString(); } + + /** + * Encodes local date time. + * + * @param localDateTime the local date time + */ + public void encodeLocalDateTime(LocalDateTime localDateTime) { + jsonGenerator.write(localDateTime.toString()); + postEncodeValue(); + } + + /** + * Encodes offset date time. + * + * @param offsetDateTime the offset date time + */ + public void encodeOffsetDateTime(OffsetDateTime offsetDateTime) { + jsonGenerator.write(offsetDateTime.toString()); + postEncodeValue(); + } } diff --git a/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/OracleJdbcJsonParserDecoder.java b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/OracleJdbcJsonParserDecoder.java index f8966052b..434259390 100644 --- a/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/OracleJdbcJsonParserDecoder.java +++ b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/OracleJdbcJsonParserDecoder.java @@ -16,15 +16,21 @@ package io.micronaut.serde.oracle.jdbc.json; import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; import io.micronaut.core.util.StringUtils; import io.micronaut.serde.exceptions.InvalidFormatException; import io.micronaut.serde.exceptions.SerdeException; import io.micronaut.serde.support.AbstractStreamDecoder; +import oracle.sql.json.OracleJsonArray; import oracle.sql.json.OracleJsonParser; import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; /** * Implementation of the {@link io.micronaut.serde.Decoder} interface for Oracle JDBC JSON. @@ -33,7 +39,10 @@ * @since 1.2.0 */ @Internal -final class OracleJdbcJsonParserDecoder extends AbstractStreamDecoder { +public final class OracleJdbcJsonParserDecoder extends AbstractStreamDecoder { + + private static final String METHOD_CALLED_IN_WRONG_CONTEXT = "Method called in wrong context "; + private final OracleJsonParser jsonParser; private OracleJsonParser.Event currentEvent; @@ -53,31 +62,18 @@ final class OracleJdbcJsonParserDecoder extends AbstractStreamDecoder { @Override protected TokenType currentToken() { - switch (currentEvent) { - case START_ARRAY: - return TokenType.START_ARRAY; - case START_OBJECT: - return TokenType.START_OBJECT; - case KEY_NAME: - return TokenType.KEY; - case VALUE_STRING: - return TokenType.STRING; - case VALUE_DECIMAL: - case VALUE_DOUBLE: - case VALUE_FLOAT: - return TokenType.NUMBER; - case VALUE_TRUE: - case VALUE_FALSE: - return TokenType.BOOLEAN; - case VALUE_NULL: - return TokenType.NULL; - case END_OBJECT: - return TokenType.END_OBJECT; - case END_ARRAY: - return TokenType.END_ARRAY; - default: - return TokenType.OTHER; - } + return switch (currentEvent) { + case START_ARRAY -> TokenType.START_ARRAY; + case START_OBJECT -> TokenType.START_OBJECT; + case KEY_NAME -> TokenType.KEY; + case VALUE_STRING -> TokenType.STRING; + case VALUE_DECIMAL, VALUE_DOUBLE, VALUE_FLOAT -> TokenType.NUMBER; + case VALUE_TRUE, VALUE_FALSE -> TokenType.BOOLEAN; + case VALUE_NULL -> TokenType.NULL; + case END_OBJECT -> TokenType.END_OBJECT; + case END_ARRAY -> TokenType.END_ARRAY; + default -> TokenType.OTHER; + }; } @Override @@ -97,20 +93,22 @@ protected String getCurrentKey() { @Override protected String coerceScalarToString() { - switch (currentEvent) { - case VALUE_STRING: - case VALUE_DECIMAL: - case VALUE_DOUBLE: - case VALUE_FLOAT: - // only allowed for string and number - return jsonParser.getString(); - case VALUE_TRUE: - return StringUtils.TRUE; - case VALUE_FALSE: - return StringUtils.FALSE; - default: - throw new IllegalStateException("Method called in wrong context " + currentEvent); - } + return switch (currentEvent) { + case VALUE_STRING, VALUE_DECIMAL, VALUE_DOUBLE, VALUE_FLOAT, VALUE_INTERVALDS, VALUE_INTERVALYM -> + // only allowed for string, number + // additionally for processing string values from VALUE_INTERVALDS, VALUE_INTERVALYM + // in combination with custom de/serializers configured for Oracle Json parsing + // which is needed to transform from VALUE_INTERVALDS to java.time.Duration + jsonParser.getString(); + case VALUE_BINARY -> + // VALUE_BINARY will return Base16 encoded string and when serializing just write back the same string value + // which should work fine + jsonParser.getValue().asJsonBinary().getString(); + case VALUE_TRUE -> StringUtils.TRUE; + case VALUE_FALSE -> StringUtils.FALSE; + default -> + throw new IllegalStateException(METHOD_CALLED_IN_WRONG_CONTEXT + currentEvent); + }; } @Override @@ -145,16 +143,13 @@ protected BigDecimal getBigDecimal() { @Override protected Number getBestNumber() { - switch (currentEvent) { - case VALUE_DECIMAL: - return jsonParser.getLong(); - case VALUE_DOUBLE: - return jsonParser.getDouble(); - case VALUE_FLOAT: - return jsonParser.getFloat(); - default: - throw new IllegalStateException("Method called in wrong context " + currentEvent); - } + return switch (currentEvent) { + case VALUE_DECIMAL -> jsonParser.getLong(); + case VALUE_DOUBLE -> jsonParser.getDouble(); + case VALUE_FLOAT -> jsonParser.getFloat(); + default -> + throw new IllegalStateException(METHOD_CALLED_IN_WRONG_CONTEXT + currentEvent); + }; } @Override @@ -167,11 +162,111 @@ protected void skipChildren() { } @Override - public IOException createDeserializationException(String message, Object invalidValue) { + @NonNull + public IOException createDeserializationException(@NonNull String message, @Nullable Object invalidValue) { if (invalidValue != null) { return new InvalidFormatException(message, null, invalidValue); } else { return new SerdeException(message); } } + + /** + * Decodes Oracle JSON binary data as byte array. + * + * @return the byte array for Oracle JSON binary + */ + public byte[] decodeBinary() { + if (currentEvent == OracleJsonParser.Event.VALUE_BINARY) { + byte[] bytes = jsonParser.getBytes(); + nextToken(); + return bytes; + } + if (currentEvent == OracleJsonParser.Event.START_ARRAY) { + OracleJsonArray oracleJsonArray = jsonParser.getArray(); + int size = oracleJsonArray.size(); + byte[] bytes = new byte[size]; + for (int i = 0; i < size; i++) { + if (oracleJsonArray.isNull(i)) { + bytes[i] = 0; + } else { + bytes[i] = (byte) oracleJsonArray.getInt(i); + } + } + nextToken(); + return bytes; + } + if (currentEvent == OracleJsonParser.Event.VALUE_STRING) { + String str = jsonParser.getString(); + nextToken(); + // string binary representation is Base16 encoded, so we should decode it + return decodeBase16(str); + } + throw new IllegalStateException(METHOD_CALLED_IN_WRONG_CONTEXT + currentEvent); + } + + /** + * Decodes Oracle JSON value as {@link LocalDateTime}. + * + * @return the {@link LocalDateTime} value being decoded + */ + public LocalDateTime decodeLocalDateTime() { + LocalDateTime value = + switch (currentEvent) { + case VALUE_DATE, VALUE_TIMESTAMP -> jsonParser.getLocalDateTime(); + case VALUE_STRING -> LocalDateTime.parse(jsonParser.getString()); + default -> throw new IllegalStateException(METHOD_CALLED_IN_WRONG_CONTEXT + currentEvent); + }; + nextToken(); + return value; + } + + /** + * Decodes Oracle JSON value as {@link OffsetDateTime}. + * + * @return the {@link OffsetDateTime} value being decoded + */ + public OffsetDateTime decodeOffsetDateTime() { + OffsetDateTime value = + switch (currentEvent) { + case VALUE_TIMESTAMPTZ -> jsonParser.getOffsetDateTime(); + case VALUE_STRING -> OffsetDateTime.parse(jsonParser.getString()); + default -> throw new IllegalStateException(METHOD_CALLED_IN_WRONG_CONTEXT + currentEvent); + }; + nextToken(); + return value; + } + + /** + * Decodes Oracle JSON value as {@link ZonedDateTime}. + * + * @return the {@link ZonedDateTime} value being decoded + */ + public ZonedDateTime decodeZonedDateTime() { + ZonedDateTime value = + switch (currentEvent) { + case VALUE_TIMESTAMPTZ -> jsonParser.getOffsetDateTime().toZonedDateTime(); + case VALUE_STRING -> ZonedDateTime.parse(jsonParser.getString()); + default -> throw new IllegalStateException(METHOD_CALLED_IN_WRONG_CONTEXT + currentEvent); + }; + nextToken(); + return value; + } + + private static byte[] decodeBase16(CharSequence cs) { + final int len = cs.length(); + if ((len % 2) != 0) { + throw new IllegalArgumentException("Encoded string must have an even length"); + } + byte[] bytes = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + int hi = Character.digit(cs.charAt(i), 16); + int lo = Character.digit(cs.charAt(i + 1), 16); + if ((hi | lo) < 0) { + throw new IllegalArgumentException("Encoded string " + cs + " contains non-hex characters"); + } + bytes[i / 2] = (byte) (hi << 4 | lo); + } + return bytes; + } } diff --git a/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/AbstractOracleJsonSerde.java b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/AbstractOracleJsonSerde.java new file mode 100644 index 000000000..0a1f3edfd --- /dev/null +++ b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/AbstractOracleJsonSerde.java @@ -0,0 +1,88 @@ +/* + * 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.oracle.jdbc.json.serde; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.type.Argument; +import io.micronaut.serde.Decoder; +import io.micronaut.serde.Encoder; +import io.micronaut.serde.oracle.jdbc.json.OracleJdbcJsonGeneratorEncoder; +import io.micronaut.serde.oracle.jdbc.json.OracleJdbcJsonParserDecoder; +import io.micronaut.serde.util.NullableSerde; + +import java.io.IOException; + +/** + * Abstract serializer/deserializer that needs to access Oracle JSON decoder. + * + * @param the type being deserialized + */ +@Internal +public abstract class AbstractOracleJsonSerde implements NullableSerde { + + @Override + @NonNull + public final T deserializeNonNull(@NonNull Decoder decoder, @NonNull DecoderContext decoderContext, @NonNull Argument type) throws IOException { + if (decoder instanceof OracleJdbcJsonParserDecoder oracleJdbcJsonParserDecoder) { + return doDeserializeNonNull(oracleJdbcJsonParserDecoder, decoderContext, type); + } else { + return getDefault().deserializeNonNull(decoder, decoderContext, type); + } + } + + @Override + public void serialize(@NonNull Encoder encoder, @NonNull EncoderContext context, @NonNull Argument type, T value) throws IOException { + if (encoder instanceof OracleJdbcJsonGeneratorEncoder oracleEncoder) { + if (value == null) { + encoder.encodeNull(); + } else { + doSerializeNonNull(oracleEncoder, context, type, value); + } + } else { + getDefault().serialize(encoder, context, type, value); + } + } + + /** + * Deserializes object using {@link OracleJdbcJsonParserDecoder}. + * + * @param decoder the Oracle JSON decoder + * @param decoderContext the decoder context + * @param type the type being deserialized + * @return the deserialized instance of given type + * @throws IOException if an unrecoverable error occurs + */ + @NonNull + protected abstract T doDeserializeNonNull(@NonNull OracleJdbcJsonParserDecoder decoder, @NonNull DecoderContext decoderContext, + @NonNull Argument type) throws IOException; + + /** + * Serializes non null value. + * + * @param encoder the encoder + * @param context the encoder context + * @param type the type of object being serialized + * @param value the value being serialized + * @throws IOException if an unrecoverable error occurs + */ + protected abstract void doSerializeNonNull(OracleJdbcJsonGeneratorEncoder encoder, EncoderContext context, Argument type, @NonNull T value) throws IOException; + + /** + * @return The default behaviour + */ + protected abstract NullableSerde getDefault(); +} diff --git a/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonBinarySerde.java b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonBinarySerde.java new file mode 100644 index 000000000..fd67bdb4a --- /dev/null +++ b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonBinarySerde.java @@ -0,0 +1,57 @@ +/* + * 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.oracle.jdbc.json.serde; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Order; +import io.micronaut.core.type.Argument; +import io.micronaut.serde.oracle.jdbc.json.OracleJdbcJsonGeneratorEncoder; +import io.micronaut.serde.oracle.jdbc.json.OracleJdbcJsonParserDecoder; +import io.micronaut.serde.support.DefaultSerdeRegistry; +import io.micronaut.serde.util.NullableSerde; +import jakarta.inject.Singleton; +import oracle.jdbc.driver.json.tree.OracleJsonBinaryImpl; + + +/** + * The custom serde for binary values for Oracle JSON. + * + * @author radovanradic + * @since 2.0.0 + */ +@Singleton +@Order(-100) +public class OracleJsonBinarySerde extends AbstractOracleJsonSerde { + + @Override + @NonNull + protected byte[] doDeserializeNonNull(@NonNull OracleJdbcJsonParserDecoder decoder, @NonNull DecoderContext decoderContext, + @NonNull Argument type) { + return decoder.decodeBinary(); + } + + @Override + protected void doSerializeNonNull(@NonNull OracleJdbcJsonGeneratorEncoder encoder, @NonNull EncoderContext context, + @NonNull Argument type, @NonNull byte[] value) { + encoder.encodeString(OracleJsonBinaryImpl.getString(value, false)); + } + + @Override + protected NullableSerde getDefault() { + return DefaultSerdeRegistry.BYTE_ARRAY_SERDE; + } + +} diff --git a/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonDurationSerde.java b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonDurationSerde.java new file mode 100644 index 000000000..0c03e3f18 --- /dev/null +++ b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonDurationSerde.java @@ -0,0 +1,56 @@ +/* + * 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.oracle.jdbc.json.serde; + +import java.io.IOException; +import java.time.Duration; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Order; +import io.micronaut.core.type.Argument; +import io.micronaut.serde.Decoder; +import io.micronaut.serde.Encoder; +import io.micronaut.serde.util.NullableSerde; +import jakarta.inject.Singleton; + +/** + * The custom serde for {@link Duration} for Oracle JSON. Needed because default serde in Micronaut expects number (nanos) + * to deserialize from, but we are getting it as String from Oracle JSON parser. + * + * @author radovanradic + * @since 2.0.0 + */ +@Singleton +@Order(-100) +public class OracleJsonDurationSerde implements NullableSerde { + + @Override + @NonNull + public Duration deserializeNonNull(Decoder decoder, DecoderContext decoderContext, Argument type) throws IOException { + String duration = decoder.decodeString(); + return Duration.parse(duration); + } + + @Override + public void serialize(@NonNull Encoder encoder, @NonNull EncoderContext context, + @NonNull Argument type, Duration value) throws IOException { + if (value == null) { + encoder.encodeNull(); + } else { + encoder.encodeString(value.toString()); + } + } +} diff --git a/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonLocaleDateSerde.java b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonLocaleDateSerde.java new file mode 100644 index 000000000..f8c054739 --- /dev/null +++ b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonLocaleDateSerde.java @@ -0,0 +1,62 @@ +/* + * 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.oracle.jdbc.json.serde; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Order; +import io.micronaut.core.type.Argument; +import io.micronaut.serde.oracle.jdbc.json.OracleJdbcJsonGeneratorEncoder; +import io.micronaut.serde.oracle.jdbc.json.OracleJdbcJsonParserDecoder; +import io.micronaut.serde.support.serdes.LocalDateSerde; +import io.micronaut.serde.util.NullableSerde; +import jakarta.inject.Singleton; + +import java.time.LocalDate; + +/** + * Serde for {@link LocalDate} from Oracle JSON. + * + * @author radovanradic + * @since 2.0.0 + */ +@Singleton +@Order(-100) +public class OracleJsonLocaleDateSerde extends AbstractOracleJsonSerde { + + private final LocalDateSerde localDateSerde; + + public OracleJsonLocaleDateSerde(LocalDateSerde localDateSerde) { + this.localDateSerde = localDateSerde; + } + + @Override + @NonNull + protected LocalDate doDeserializeNonNull(@NonNull OracleJdbcJsonParserDecoder decoder, + @NonNull DecoderContext decoderContext, + @NonNull Argument type) { + return decoder.decodeLocalDateTime().toLocalDate(); + } + + @Override + protected void doSerializeNonNull(OracleJdbcJsonGeneratorEncoder encoder, EncoderContext context, Argument type, LocalDate value) { + encoder.encodeLocalDateTime(value.atStartOfDay()); + } + + @Override + protected NullableSerde getDefault() { + return localDateSerde; + } +} diff --git a/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonLocaleDateTimeSerde.java b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonLocaleDateTimeSerde.java new file mode 100644 index 000000000..d44c2a738 --- /dev/null +++ b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonLocaleDateTimeSerde.java @@ -0,0 +1,57 @@ +/* + * 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.oracle.jdbc.json.serde; + +import io.micronaut.core.annotation.Order; +import io.micronaut.core.type.Argument; +import io.micronaut.serde.oracle.jdbc.json.OracleJdbcJsonGeneratorEncoder; +import io.micronaut.serde.oracle.jdbc.json.OracleJdbcJsonParserDecoder; +import io.micronaut.serde.support.serdes.LocalDateTimeSerde; +import io.micronaut.serde.util.NullableSerde; +import jakarta.inject.Singleton; + +import java.time.LocalDateTime; + +/** + * Serde for {@link LocalDateTime} from Oracle JSON. + * + * @author radovanradic + * @since 2.0.0 + */ +@Singleton +@Order(-100) +public class OracleJsonLocaleDateTimeSerde extends AbstractOracleJsonSerde { + private final LocalDateTimeSerde dateTimeSerde; + + public OracleJsonLocaleDateTimeSerde(LocalDateTimeSerde dateTimeSerde) { + this.dateTimeSerde = dateTimeSerde; + } + + @Override + protected LocalDateTime doDeserializeNonNull(OracleJdbcJsonParserDecoder decoder, DecoderContext decoderContext, Argument type) { + return decoder.decodeLocalDateTime(); + } + + @Override + protected void doSerializeNonNull(OracleJdbcJsonGeneratorEncoder encoder, EncoderContext context, Argument type, LocalDateTime value) { + encoder.encodeLocalDateTime(value); + } + + @Override + protected NullableSerde getDefault() { + return dateTimeSerde; + } +} diff --git a/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonOffsetDateTimeSerde.java b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonOffsetDateTimeSerde.java new file mode 100644 index 000000000..c9a23fb99 --- /dev/null +++ b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonOffsetDateTimeSerde.java @@ -0,0 +1,57 @@ +/* + * 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.oracle.jdbc.json.serde; + +import io.micronaut.core.annotation.Order; +import io.micronaut.core.type.Argument; +import io.micronaut.serde.oracle.jdbc.json.OracleJdbcJsonGeneratorEncoder; +import io.micronaut.serde.oracle.jdbc.json.OracleJdbcJsonParserDecoder; +import io.micronaut.serde.support.serdes.OffsetDateTimeSerde; +import io.micronaut.serde.util.NullableSerde; +import jakarta.inject.Singleton; + +import java.time.OffsetDateTime; + +/** + * Serde for {@link OffsetDateTime} from Oracle JSON. + * + * @author radovanradic + * @since 2.0.0 + */ +@Singleton +@Order(-100) +public class OracleJsonOffsetDateTimeSerde extends AbstractOracleJsonSerde { + private final OffsetDateTimeSerde offsetDateTimeSerde; + + public OracleJsonOffsetDateTimeSerde(OffsetDateTimeSerde offsetDateTimeSerde) { + this.offsetDateTimeSerde = offsetDateTimeSerde; + } + + @Override + protected OffsetDateTime doDeserializeNonNull(OracleJdbcJsonParserDecoder decoder, DecoderContext decoderContext, Argument type) { + return decoder.decodeOffsetDateTime(); + } + + @Override + protected void doSerializeNonNull(OracleJdbcJsonGeneratorEncoder encoder, EncoderContext context, Argument type, OffsetDateTime value) { + encoder.encodeOffsetDateTime(value); + } + + @Override + protected NullableSerde getDefault() { + return offsetDateTimeSerde; + } +} diff --git a/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonZonedDateTimeSerde.java b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonZonedDateTimeSerde.java new file mode 100644 index 000000000..4b295b014 --- /dev/null +++ b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/OracleJsonZonedDateTimeSerde.java @@ -0,0 +1,57 @@ +/* + * 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.oracle.jdbc.json.serde; + +import io.micronaut.core.annotation.Order; +import io.micronaut.core.type.Argument; +import io.micronaut.serde.oracle.jdbc.json.OracleJdbcJsonGeneratorEncoder; +import io.micronaut.serde.oracle.jdbc.json.OracleJdbcJsonParserDecoder; +import io.micronaut.serde.support.serdes.ZonedDateTimeSerde; +import io.micronaut.serde.util.NullableSerde; +import jakarta.inject.Singleton; + +import java.time.ZonedDateTime; + +/** + * Serde for {@link ZonedDateTime} from Oracle JSON. + * + * @author radovanradic + * @since 2.0.0 + */ +@Singleton +@Order(-100) +public class OracleJsonZonedDateTimeSerde extends AbstractOracleJsonSerde { + private final ZonedDateTimeSerde zonedDateTimeSerde; + + public OracleJsonZonedDateTimeSerde(ZonedDateTimeSerde zonedDateTimeSerde) { + this.zonedDateTimeSerde = zonedDateTimeSerde; + } + + @Override + protected ZonedDateTime doDeserializeNonNull(OracleJdbcJsonParserDecoder decoder, DecoderContext decoderContext, Argument type) { + return decoder.decodeZonedDateTime(); + } + + @Override + protected void doSerializeNonNull(OracleJdbcJsonGeneratorEncoder encoder, EncoderContext context, Argument type, ZonedDateTime value) { + encoder.encodeOffsetDateTime(value.toOffsetDateTime()); + } + + @Override + protected NullableSerde getDefault() { + return zonedDateTimeSerde; + } +} diff --git a/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/package-info.java b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/package-info.java new file mode 100644 index 000000000..976635809 --- /dev/null +++ b/serde-oracle-jdbc-json/src/main/java/io/micronaut/serde/oracle/jdbc/json/serde/package-info.java @@ -0,0 +1,22 @@ +/* + * 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. + */ +/** + * Custom serializers and deserializers that can be used to serde given types from Oracle JSON. + * + * @author radovanradic + * @since 2.0.0 + */ +package io.micronaut.serde.oracle.jdbc.json.serde; diff --git a/serde-oracle-jdbc-json/src/test/groovy/io/micronaut/serde/oracle/jdbc/json/OracleJdbcJsonBinaryBasicSerdeSpec.groovy b/serde-oracle-jdbc-json/src/test/groovy/io/micronaut/serde/oracle/jdbc/json/OracleJdbcJsonBinaryBasicSerdeSpec.groovy index 000fb0407..85fc011d1 100644 --- a/serde-oracle-jdbc-json/src/test/groovy/io/micronaut/serde/oracle/jdbc/json/OracleJdbcJsonBinaryBasicSerdeSpec.groovy +++ b/serde-oracle-jdbc-json/src/test/groovy/io/micronaut/serde/oracle/jdbc/json/OracleJdbcJsonBinaryBasicSerdeSpec.groovy @@ -3,11 +3,28 @@ package io.micronaut.serde.oracle.jdbc.json import io.micronaut.core.type.Argument import io.micronaut.json.JsonMapper import io.micronaut.serde.AbstractBasicSerdeSpec +import io.micronaut.serde.bson.Address +import io.micronaut.serde.bson.SampleData import io.micronaut.test.extensions.spock.annotation.MicronautTest import jakarta.inject.Inject +import oracle.jdbc.driver.json.tree.OracleJsonArrayImpl +import oracle.jdbc.driver.json.tree.OracleJsonBinaryImpl +import oracle.jdbc.driver.json.tree.OracleJsonDateImpl +import oracle.jdbc.driver.json.tree.OracleJsonDoubleImpl +import oracle.jdbc.driver.json.tree.OracleJsonIntervalDSImpl +import oracle.jdbc.driver.json.tree.OracleJsonIntervalYMImpl +import oracle.jdbc.driver.json.tree.OracleJsonStringImpl +import oracle.jdbc.driver.json.tree.OracleJsonTimestampImpl +import oracle.jdbc.driver.json.tree.OracleJsonTimestampTZImpl +import oracle.sql.json.OracleJsonFactory import oracle.sql.json.OracleJsonObject +import java.nio.charset.Charset import java.nio.charset.StandardCharsets +import java.time.Duration +import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.time.Period @MicronautTest class OracleJdbcJsonBinaryBasicSerdeSpec extends AbstractBasicSerdeSpec { @@ -41,4 +58,77 @@ class OracleJdbcJsonBinaryBasicSerdeSpec extends AbstractBasicSerdeSpec { assert obj == expected obj == expected } + + void 'test parsing various types'() { + given: + def etag = UUID.randomUUID().toString() + def memo = "some long content" + def uuid = UUID.randomUUID() + def duration = Duration.ofMinutes(15) + def period = Period.of(2, 3, 0) + + def localDateTime = LocalDateTime.now() + def offsetDateTime = OffsetDateTime.now() + + def address = new Address("1", "Main St", "Someville", "11122") + + // simple props + def description = "data description" + int grade = 9 + double rating = 2.4 + def rates = List.of(109.5f, 107.0f, 111.85f) + def active = true + + def jsonFactory = new OracleJsonFactory() + def oson = jsonFactory.createObject() + + // Add manually value to the object to mimic how Oracle DB would return it + oson.put("etag", new OracleJsonBinaryImpl(etag.getBytes(Charset.defaultCharset()), false)) + oson.put("memo", new OracleJsonBinaryImpl(memo.getBytes(Charset.defaultCharset()), false)) + oson.put("uuid", new OracleJsonStringImpl(uuid.toString())) + oson.put("duration", new OracleJsonIntervalDSImpl(duration)) + oson.put("period", new OracleJsonIntervalYMImpl(period)) + oson.put("localDateTime", new OracleJsonTimestampImpl(localDateTime)) + oson.put("offsetDateTime", new OracleJsonTimestampTZImpl(offsetDateTime)) + oson.put("date", new OracleJsonDateImpl(localDateTime)) + oson.put("description", new OracleJsonStringImpl(description)) + oson.put("grade", new OracleJsonDoubleImpl(grade)) + oson.put("rating", new OracleJsonDoubleImpl(rating)) + def oracleJsonArrayRates = new OracleJsonArrayImpl() + rates.forEach {oracleJsonArrayRates.add(it.doubleValue())} + oson.put("rates", oracleJsonArrayRates) + def oracleJsonObjectAddress = jsonFactory.createObject() + oracleJsonObjectAddress.put("address", address.getAddress()) + oracleJsonObjectAddress.put("street", address.getStreet()) + oracleJsonObjectAddress.put("postcode", address.getPostcode()) + oracleJsonObjectAddress.put("town", address.getTown()) + oson.put("address", oracleJsonObjectAddress) + oson.put("active", new OracleJsonStringImpl(active.toString())) + + def bytes = osonMapper.writeValueAsBytes(oson) + when: + def sampleData = osonMapper.readValue(bytes, SampleData) + then: + sampleData.etag == OracleJsonBinaryImpl.getString(etag.getBytes(Charset.defaultCharset()), false) + sampleData.memo == memo.getBytes(Charset.defaultCharset()) + sampleData.uuid == uuid + sampleData.duration == duration + sampleData.period == period + sampleData.localDateTime == localDateTime + sampleData.offsetDateTime == offsetDateTime + sampleData.date == localDateTime.toLocalDate() + sampleData.description == description + sampleData.grade == grade + sampleData.rating == rating + sampleData.rates == rates + sampleData.address == address + !sampleData.person + sampleData.active == active + when: + def json = textJsonMapper.writeValueAsString(sampleData) + then: + json != '' + // Just simple validation, no need to parse + json.contains("\"etag\":\"" + OracleJsonBinaryImpl.getString(etag.getBytes(Charset.defaultCharset()), false) + "\"") + } } diff --git a/serde-oracle-jdbc-json/src/test/java/io/micronaut/serde/bson/SampleData.java b/serde-oracle-jdbc-json/src/test/java/io/micronaut/serde/bson/SampleData.java new file mode 100644 index 000000000..087e1351e --- /dev/null +++ b/serde-oracle-jdbc-json/src/test/java/io/micronaut/serde/bson/SampleData.java @@ -0,0 +1,165 @@ +package io.micronaut.serde.bson; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.Period; +import java.util.List; +import java.util.UUID; + +import io.micronaut.core.annotation.Introspected; + +@Introspected +public class SampleData { + + private LocalDateTime localDateTime; + + private OffsetDateTime offsetDateTime; + + private LocalDate date; + + private UUID uuid; + + private String etag; + + private byte[] memo; + + private Period period; + + private Duration duration; + + private String description; + + private int grade; + + private Double rating; + + private List rates; + + private Address address; + + private Person person; + + private boolean active; + + public LocalDateTime getLocalDateTime() { + return localDateTime; + } + + public void setLocalDateTime(LocalDateTime localDateTime) { + this.localDateTime = localDateTime; + } + + public OffsetDateTime getOffsetDateTime() { + return offsetDateTime; + } + + public void setOffsetDateTime(OffsetDateTime offsetDateTime) { + this.offsetDateTime = offsetDateTime; + } + + public LocalDate getDate() { + return date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public UUID getUuid() { + return uuid; + } + + public void setUuid(UUID uuid) { + this.uuid = uuid; + } + + public String getEtag() { + return etag; + } + + public void setEtag(String etag) { + this.etag = etag; + } + + public byte[] getMemo() { + return memo; + } + + public void setMemo(byte[] memo) { + this.memo = memo; + } + + public Period getPeriod() { + return period; + } + + public void setPeriod(Period period) { + this.period = period; + } + + public Duration getDuration() { + return duration; + } + + public void setDuration(Duration duration) { + this.duration = duration; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public int getGrade() { + return grade; + } + + public void setGrade(int grade) { + this.grade = grade; + } + + public Double getRating() { + return rating; + } + + public void setRating(Double rating) { + this.rating = rating; + } + + public List getRates() { + return rates; + } + + public void setRates(List rates) { + this.rates = rates; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + + public Person getPerson() { + return person; + } + + public void setPerson(Person person) { + this.person = person; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } +} diff --git a/serde-support/build.gradle.kts b/serde-support/build.gradle.kts index 516933e7c..ffc145476 100644 --- a/serde-support/build.gradle.kts +++ b/serde-support/build.gradle.kts @@ -27,3 +27,8 @@ dependencies { testImplementation(mn.micronaut.jackson.databind) testImplementation(libs.jetbrains.annotations) } +micronautBuild { + binaryCompatibility { + enabled.set(false) + } +} diff --git a/serde-support/src/main/java/io/micronaut/serde/support/DefaultSerdeRegistry.java b/serde-support/src/main/java/io/micronaut/serde/support/DefaultSerdeRegistry.java index 0b90cd5cb..bf3d09874 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/DefaultSerdeRegistry.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/DefaultSerdeRegistry.java @@ -38,7 +38,6 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; -import java.util.stream.Stream; import io.micronaut.context.BeanContext; import io.micronaut.context.BeanRegistration; @@ -70,7 +69,6 @@ import io.micronaut.serde.support.serdes.NumberSerde; import io.micronaut.serde.support.serializers.ObjectSerializer; import io.micronaut.serde.support.util.TypeKey; -import io.micronaut.serde.util.NullableDeserializer; import io.micronaut.serde.util.NullableSerde; import jakarta.inject.Singleton; @@ -81,6 +79,66 @@ @BootstrapContextCompatible public class DefaultSerdeRegistry implements SerdeRegistry { + public static final IntegerSerde INTEGER_SERDE = new IntegerSerde(); + public static final LongSerde LONG_SERDE = new LongSerde(); + public static final ShortSerde SHORT_SERDE = new ShortSerde(); + public static final FloatSerde FLOAT_SERDE = new FloatSerde(); + public static final ByteSerde BYTE_SERDE = new ByteSerde(); + public static final DoubleSerde DOUBLE_SERDE = new DoubleSerde(); + public static final OptionalIntSerde OPTIONAL_INT_SERDE = new OptionalIntSerde(); + public static final OptionalDoubleSerde OPTIONAL_DOUBLE_SERDE = new OptionalDoubleSerde(); + public static final OptionalLongSerde OPTIONAL_LONG_SERDE = new OptionalLongSerde(); + public static final BigDecimalSerde BIG_DECIMAL_SERDE = new BigDecimalSerde(); + public static final BigIntegerSerde BIG_INTEGER_SERDE = new BigIntegerSerde(); + public static final UUIDSerde UUID_SERDE = new UUIDSerde(); + public static final URLSerde URL_SERDE = new URLSerde(); + public static final URISerde URI_SERDE = new URISerde(); + public static final CharsetSerde CHARSET_SERDE = new CharsetSerde(); + public static final TimeZoneSerde TIME_ZONE_SERDE = new TimeZoneSerde(); + public static final LocaleSerde LOCALE_SERDE = new LocaleSerde(); + public static final IntArraySerde INT_ARRAY_SERDE = new IntArraySerde(); + public static final LongArraySerde LONG_ARRAY_SERDE = new LongArraySerde(); + public static final FloatArraySerde FLOAT_ARRAY_SERDE = new FloatArraySerde(); + public static final ShortArraySerde SHORT_ARRAY_SERDE = new ShortArraySerde(); + public static final DoubleArraySerde DOUBLE_ARRAY_SERDE = new DoubleArraySerde(); + public static final BooleanArraySerde BOOLEAN_ARRAY_SERDE = new BooleanArraySerde(); + public static final ByteArraySerde BYTE_ARRAY_SERDE = new ByteArraySerde(); + public static final CharArraySerde CHAR_ARRAY_SERDE = new CharArraySerde(); + + public static final StringSerde STRING_SERDE = new StringSerde(); + + public static final BooleanSerde BOOLEAN_SERDE = new BooleanSerde(); + public static final CharSerde CHAR_SERDE = new CharSerde(); + public static final List> DEFAULT_SERDES = List.of( + BOOLEAN_SERDE, + BYTE_SERDE, + CHAR_SERDE, + DOUBLE_SERDE, + FLOAT_SERDE, + INTEGER_SERDE, + LONG_SERDE, + SHORT_SERDE, + STRING_SERDE, + OPTIONAL_INT_SERDE, + OPTIONAL_DOUBLE_SERDE, + OPTIONAL_LONG_SERDE, + BIG_DECIMAL_SERDE, + BIG_INTEGER_SERDE, + UUID_SERDE, + URL_SERDE, + URI_SERDE, + CHARSET_SERDE, + TIME_ZONE_SERDE, + LOCALE_SERDE, + INT_ARRAY_SERDE, + LONG_ARRAY_SERDE, + FLOAT_ARRAY_SERDE, + SHORT_ARRAY_SERDE, + DOUBLE_ARRAY_SERDE, + BOOLEAN_ARRAY_SERDE, + BYTE_ARRAY_SERDE, + CHAR_ARRAY_SERDE + ); private final Serializer objectSerializer; private final Map, List>> serializerDefMap; private final Map, List>> deserializerDefMap; @@ -159,68 +217,25 @@ public DefaultSerdeRegistry( } registerBuiltInSerdes(); - registerPrimitiveSerdes(); this.objectSerializer = objectSerializer; this.objectDeserializer = objectDeserializer; this.conversionService = conversionService; } - private void registerPrimitiveSerdes() { - this.deserializerMap.put( - new TypeKey(Argument.BOOLEAN), - (decoder, decoderContext, type) -> decoder.decodeBoolean() - ); - this.deserializerMap.put( - new TypeKey(Argument.of(Boolean.class)), - (NullableDeserializer) (decoder, decoderContext, type) -> decoder.decodeBoolean() - ); - this.deserializerMap.put( - new TypeKey(Argument.CHAR), - (decoder, decoderContext, type) -> decoder.decodeChar() - ); - this.deserializerMap.put( - new TypeKey(Argument.of(Character.class)), - (NullableDeserializer) (decoder, decoderContext, type) -> decoder.decodeChar() - ); - } - private void registerBuiltInSerdes() { - this.deserializerMap.put(new TypeKey(Argument.STRING), - (NullableDeserializer) (decoder, decoderContext, type) -> decoder.decodeString()); - Stream.of( - new IntegerSerde(), - new LongSerde(), - new ShortSerde(), - new FloatSerde(), - new ByteSerde(), - new DoubleSerde(), - new OptionalIntSerde(), - new OptionalDoubleSerde(), - new OptionalLongSerde(), - new BigDecimalSerde(), - new BigIntegerSerde(), - new UUIDSerde(), - new URLSerde(), - new URISerde(), - new CharsetSerde(), - new TimeZoneSerde(), - new LocaleSerde(), - new IntArraySerde(), - new LongArraySerde(), - new FloatArraySerde(), - new ShortArraySerde(), - new DoubleArraySerde(), - new BooleanArraySerde(), - new ByteArraySerde(), - new CharArraySerde() - ).forEach(this::register); + DEFAULT_SERDES.forEach(this::register); } private void register(SerdeRegistrar serdeRegistrar) { for (Argument type : serdeRegistrar.getTypes()) { final TypeKey typeEntry = new TypeKey(type); - DefaultSerdeRegistry.this.deserializerMap.put(typeEntry, serdeRegistrar); - DefaultSerdeRegistry.this.serializerMap.put(typeEntry, serdeRegistrar); + // if it hasn't been overridden by a bean + if (!deserializerDefMap.containsKey(type.getType())) { + DefaultSerdeRegistry.this.deserializerMap.put(typeEntry, serdeRegistrar); + } + if (!serializerDefMap.containsKey(type.getType())) { + DefaultSerdeRegistry.this.serializerMap.put(typeEntry, serdeRegistrar); + } } } @@ -612,6 +627,74 @@ public Integer getDefaultValue(@NonNull DecoderContext context, @NonNull Argumen } } + private static final class CharSerde extends SerdeRegistrar implements NullableSerde { + @Override + public Character deserializeNonNull(Decoder decoder, + DecoderContext decoderContext, + Argument type) throws IOException { + return decoder.decodeChar(); + } + + @Override + public void serialize(Encoder encoder, + EncoderContext context, + Argument type, Character value) throws IOException { + encoder.encodeChar(value); + } + + @Override + Argument getType() { + return Argument.of(Character.class); + } + + @Override + protected Iterable> getTypes() { + return Arrays.asList( + getType(), Argument.CHAR + ); + } + + @Nullable + @Override + public Character getDefaultValue(@NonNull DecoderContext context, @NonNull Argument type) { + return type.isPrimitive() ? (char) 0 : null; + } + } + + private static final class BooleanSerde extends SerdeRegistrar implements NullableSerde { + @Override + public Boolean deserializeNonNull(Decoder decoder, + DecoderContext decoderContext, + Argument type) throws IOException { + return decoder.decodeBoolean(); + } + + @Override + public void serialize(Encoder encoder, + EncoderContext context, + Argument type, Boolean value) throws IOException { + encoder.encodeBoolean(value); + } + + @Override + Argument getType() { + return Argument.of(Boolean.class); + } + + @Override + protected Iterable> getTypes() { + return Arrays.asList( + getType(), Argument.BOOLEAN + ); + } + + @Nullable + @Override + public Boolean getDefaultValue(@NonNull DecoderContext context, @NonNull Argument type) { + return type.isPrimitive() ? false : null; + } + } + private static final class LongSerde extends SerdeRegistrar implements NumberSerde { @Override public Long deserializeNonNull(Decoder decoder, @@ -1016,6 +1099,28 @@ public BigDecimal deserializeNonNull(Decoder decoder, DecoderContext decoderCont } } + private static final class StringSerde + extends SerdeRegistrar + implements NullableSerde { + + @Override + Argument getType() { + return Argument.of(String.class); + } + + @Override + public void serialize(Encoder encoder, EncoderContext context, Argument type, String value) + throws IOException { + encoder.encodeString(value); + } + + @Override + public String deserializeNonNull(Decoder decoder, DecoderContext decoderContext, Argument type) + throws IOException { + return decoder.decodeString(); + } + } + private static final class URLSerde extends SerdeRegistrar implements NullableSerde { @Override diff --git a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/CoreDeserializers.java b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/CoreDeserializers.java index 5bf53cef1..45cf7188f 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/CoreDeserializers.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/CoreDeserializers.java @@ -50,17 +50,6 @@ @BootstrapContextCompatible public class CoreDeserializers { - /** - * Deserializes string types. - * - * @return The string deserializer - */ - @Singleton - @NonNull - protected Deserializer stringDeserializer() { - return new StringDeserializer(); - } - /** * Deserializes array lists. * @@ -191,22 +180,6 @@ protected Deserializer> optionalDeserializer() { return new OptionalDeserializer<>(); } - private static class StringDeserializer implements Deserializer { - - @Override - public String deserialize(Decoder decoder, DecoderContext decoderContext, Argument type) throws IOException { - if (decoder.decodeNull()) { - return null; - } - return decoder.decodeString(); - } - - @Override - public boolean allowNull() { - return true; - } - } - private static class OptionalDeserializer implements CustomizableDeserializer> { @Override 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 7146d82e3..4e7e8d2d5 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 @@ -499,7 +499,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 { 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 a26de4cc5..64740681e 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 @@ -92,10 +92,16 @@ private void readProperties(Decoder decoder, DecoderContext decoderContext, Argu } } else { Object val; + Argument argument = consumedProperty.argument; try { - val = consumedProperty.deserializer.deserialize(objectDecoder, decoderContext, consumedProperty.argument); + val = consumedProperty + .deserializer + .createSpecific(decoderContext, argument) + .deserialize(objectDecoder, decoderContext, argument); } catch (InvalidFormatException e) { - throw new InvalidPropertyFormatException(e, consumedProperty.argument); + throw new InvalidPropertyFormatException(e, argument); + } catch (Exception e) { + throw new SerdeException("Error decoding property [" + argument + "] of type [" + deserBean.introspection.getBeanType() + "]: " + e.getMessage(), e); } consumedProperty.set(obj, val); diff --git a/serde-support/src/main/java/io/micronaut/serde/support/serdes/CoreSerdes.java b/serde-support/src/main/java/io/micronaut/serde/support/serdes/CoreSerdes.java index 77d7b1963..511eb2ac5 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/serdes/CoreSerdes.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/serdes/CoreSerdes.java @@ -25,6 +25,7 @@ import io.micronaut.context.annotation.BootstrapContextCompatible; import io.micronaut.context.annotation.Factory; import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Order; import io.micronaut.core.type.Argument; import io.micronaut.json.tree.JsonNode; import io.micronaut.serde.Decoder; @@ -39,6 +40,11 @@ @Factory @BootstrapContextCompatible public class CoreSerdes { + + public static final NullableSerde DURATION_SERDE = new DurationSerde(); + public static final NullableSerde PERIOD_SERDE = new PeriodSerde(); + public static final CharSequenceSerde CHAR_SEQUENCE_SERDE = new CharSequenceSerde(); + /** * Serde used for object arrays. * @return The serde @@ -58,19 +64,7 @@ protected Serde objectArraySerde() { @NonNull @BootstrapContextCompatible protected NullableSerde durationSerde() { - return new NullableSerde() { - @Override - public void serialize(Encoder encoder, EncoderContext context, Argument type, Duration value) - throws IOException { - encoder.encodeLong(value.toNanos()); - } - - @Override - public Duration deserializeNonNull(Decoder decoder, DecoderContext decoderContext, Argument type) - throws IOException { - return Duration.ofNanos(decoder.decodeLong()); - } - }; + return DURATION_SERDE; } /** @@ -81,19 +75,19 @@ public Duration deserializeNonNull(Decoder decoder, DecoderContext decoderContex @NonNull @BootstrapContextCompatible protected NullableSerde periodSerde() { - return new NullableSerde() { - @Override - public void serialize(Encoder encoder, EncoderContext context, Argument type, Period value) - throws IOException { - encoder.encodeString(value.toString()); - } + return PERIOD_SERDE; + } - @Override - public Period deserializeNonNull(Decoder decoder, DecoderContext decoderContext, Argument type) - throws IOException { - return Period.parse(decoder.decodeString()); - } - }; + /** + * Serde for CharSequence. + * @return CharSequence serde + */ + @Singleton + @NonNull + @BootstrapContextCompatible + @Order(100) // lower priority than string + protected NullableSerde charSequenceSerde() { + return CHAR_SEQUENCE_SERDE; } /** @@ -164,4 +158,45 @@ public JsonNode deserializeNonNull(Decoder decoder, DecoderContext decoderContex } }; } + + private static class DurationSerde implements NullableSerde { + @Override + public void serialize(Encoder encoder, EncoderContext context, Argument type, Duration value) + throws IOException { + encoder.encodeLong(value.toNanos()); + } + + @Override + public Duration deserializeNonNull(Decoder decoder, DecoderContext decoderContext, Argument type) + throws IOException { + return Duration.ofNanos(decoder.decodeLong()); + } + } + + private static class PeriodSerde implements NullableSerde { + @Override + public void serialize(Encoder encoder, EncoderContext context, Argument type, Period value) + throws IOException { + encoder.encodeString(value.toString()); + } + + @Override + public Period deserializeNonNull(Decoder decoder, DecoderContext decoderContext, Argument type) + throws IOException { + return Period.parse(decoder.decodeString()); + } + } + + private static final class CharSequenceSerde + implements NullableSerde { + @Override + public void serialize(Encoder encoder, EncoderContext context, Argument type, CharSequence value) throws IOException { + encoder.encodeString(value.toString()); + } + + @Override + public CharSequence deserializeNonNull(Decoder decoder, DecoderContext decoderContext, Argument type) throws IOException { + return decoder.decodeString(); + } + } }