diff --git a/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/JsonUnwrappedSpec.groovy b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/JsonUnwrappedSpec.groovy index ee394b29d..b1676831b 100644 --- a/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/JsonUnwrappedSpec.groovy +++ b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/JsonUnwrappedSpec.groovy @@ -2,6 +2,9 @@ package io.micronaut.serde.jackson.annotation import io.micronaut.core.type.Argument import io.micronaut.serde.jackson.JsonCompileSpec +import io.micronaut.serde.jackson.nested.Address +import io.micronaut.serde.jackson.nested.NestedEntity +import io.micronaut.serde.jackson.nested.NestedEntityId import spock.lang.Requires class JsonUnwrappedSpec extends JsonCompileSpec { @@ -596,4 +599,145 @@ class Name { cleanup: context.close() } + + void "test @JsonUnwrapped - levels"() { + given: + def context = buildContext(""" +package unwrapped; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +class Foo { + + @JsonUnwrapped(prefix = "hk_", suffix = "_out") + private ComplexFooId hashKey; + + private String value; + + public ComplexFooId getHashKey() { + return hashKey; + } + + public void setHashKey(ComplexFooId hashKey) { + this.hashKey = hashKey; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} +@Serdeable +class ComplexFooId { + + private Integer theInt; + + @JsonUnwrapped(prefix = "foo_", suffix = "_in") + private InnerFooId nested; + + public Integer getTheInt() { + return theInt; + } + + public void setTheInt(Integer theInt) { + this.theInt = theInt; + } + + public InnerFooId getNested() { + return nested; + } + + public void setNested(InnerFooId nested) { + this.nested = nested; + } +} +@Serdeable +class InnerFooId { + + private Long theLong; + + private String theString; + + public Long getTheLong() { + return theLong; + } + + public void setTheLong(Long theLong) { + this.theLong = theLong; + } + + public String getTheString() { + return theString; + } + + public void setTheString(String theString) { + this.theString = theString; + } +} +""") + + when: + def foo = newInstance(context, 'unwrapped.Foo', [value: "TheValue", hashKey: newInstance(context, 'unwrapped.ComplexFooId', [theInt: 10, + nested: newInstance(context, 'unwrapped.InnerFooId', [theLong: 200L, theString: 'MyString'])])]) + + def result = writeJson(jsonMapper, foo) + + then: + result == '{"hk_theInt_out":10,"hk_foo_theLong_in_out":200,"hk_foo_theString_in_out":"MyString","value":"TheValue"}' + + when: + def read = jsonMapper.readValue(result, Argument.of(context.classLoader.loadClass('unwrapped.Foo'))) + + then: + read + read.value == 'TheValue' + read.hashKey.theInt == 10 + read.hashKey.nested.theLong == 200 + read.hashKey.nested.theString == 'MyString' + + cleanup: + context.close() + } + + + void "test @JsonUnwrapped - levels 2"() { + given: + def ctx = buildContext("") + + when: + def nestedEntity = new NestedEntity(); + nestedEntity.setValue("test1"); + NestedEntityId hashKey = new NestedEntityId(); + hashKey.setTheInt(100); + hashKey.setTheString("MyString"); + nestedEntity.setHashKey(hashKey); + Address address = new Address(); + address.getCityData().setCity("NY"); + address.getCityData().setZipCode("22000"); + address.setStreet("Blvd 11"); + nestedEntity.setAddress(address); + def nestedJsonStr = writeJson(jsonMapper, nestedEntity) + + then: + nestedJsonStr == '{"hk_theInt":100,"hk_theString":"MyString","value":"test1","addr_street":"Blvd 11","addr_cd_zipCode":"22000","addr_cd_city":"NY","version":1,"dateCreated":"1970-01-01T00:00:00Z","dateUpdated":"1970-01-01T00:00:00Z"}' + + when: + def deserNestedEntity = jsonMapper.readValue(nestedJsonStr, NestedEntity.class) + + then: + deserNestedEntity + deserNestedEntity.hashKey.theInt == nestedEntity.hashKey.theInt + deserNestedEntity.value == nestedEntity.value + deserNestedEntity.audit.dateCreated == nestedEntity.audit.dateCreated + deserNestedEntity.address.cityData.zipCode == nestedEntity.address.cityData.zipCode + deserNestedEntity.address.street == nestedEntity.address.street + + cleanup: + ctx.close() + } } diff --git a/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/Address.java b/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/Address.java new file mode 100644 index 000000000..f777fa4ba --- /dev/null +++ b/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/Address.java @@ -0,0 +1,29 @@ +package io.micronaut.serde.jackson.nested; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +public class Address { + + @JsonUnwrapped(prefix = "cd_") + private CityData cityData = new CityData(); + + private String street; + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public CityData getCityData() { + return cityData; + } + + public void setCityData(CityData cityData) { + this.cityData = cityData; + } +} diff --git a/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/Audit.java b/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/Audit.java new file mode 100644 index 000000000..e9831d8ac --- /dev/null +++ b/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/Audit.java @@ -0,0 +1,43 @@ +package io.micronaut.serde.jackson.nested; + +import io.micronaut.serde.annotation.Serdeable; + +import java.sql.Timestamp; +import java.util.Date; + +@Serdeable +public class Audit { + + static final Timestamp MIN_TIMESTAMP = new Timestamp(new Date(0).getTime()); + + private Long version = 1L; + + // Init manually because cannot be nullable and not getting populated by the event + private Timestamp dateCreated = MIN_TIMESTAMP; + + private Timestamp dateUpdated = MIN_TIMESTAMP; + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public Timestamp getDateCreated() { + return dateCreated; + } + + public void setDateCreated(Timestamp dateCreated) { + this.dateCreated = dateCreated; + } + + public Timestamp getDateUpdated() { + return dateUpdated; + } + + public void setDateUpdated(Timestamp dateUpdated) { + this.dateUpdated = dateUpdated; + } +} diff --git a/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/CityData.java b/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/CityData.java new file mode 100644 index 000000000..bc710a552 --- /dev/null +++ b/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/CityData.java @@ -0,0 +1,29 @@ +package io.micronaut.serde.jackson.nested; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +public class CityData { + + @NonNull + private String zipCode; + + private String city; + + public String getZipCode() { + return zipCode; + } + + public void setZipCode(String zipCode) { + this.zipCode = zipCode; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } +} diff --git a/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/NestedEntity.java b/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/NestedEntity.java new file mode 100644 index 000000000..b02d286d8 --- /dev/null +++ b/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/NestedEntity.java @@ -0,0 +1,52 @@ +package io.micronaut.serde.jackson.nested; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +public class NestedEntity { + + @JsonUnwrapped(prefix = "hk_") + private NestedEntityId hashKey; + + private String value; + + @JsonUnwrapped(prefix = "addr_") + private Address address; + + @JsonUnwrapped + private Audit audit = new Audit(); + + public NestedEntityId getHashKey() { + return hashKey; + } + + public void setHashKey(NestedEntityId hashKey) { + this.hashKey = hashKey; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + + public Audit getAudit() { + return audit; + } + + public void setAudit(Audit audit) { + this.audit = audit; + } + +} diff --git a/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/NestedEntityId.java b/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/NestedEntityId.java new file mode 100644 index 000000000..9ebf909b4 --- /dev/null +++ b/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/NestedEntityId.java @@ -0,0 +1,27 @@ +package io.micronaut.serde.jackson.nested; + +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +public class NestedEntityId { + + private Integer theInt; + + private String theString; + + public Integer getTheInt() { + return theInt; + } + + public void setTheInt(Integer theInt) { + this.theInt = theInt; + } + + public String getTheString() { + return theString; + } + + public void setTheString(String theString) { + this.theString = theString; + } +} 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 8cd8c4e6d..6006f08c4 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 @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -101,6 +102,11 @@ private int probe(String key) { } } + /** + * Get the properties in this bag with their property names. + * + * @return All properties in this bag + */ public List>> getProperties() { Stream>> originalProperties = Arrays.stream(originalNameToPropertiesMapping) .filter(index -> index != -1) @@ -118,6 +124,15 @@ public List>> getProperties() .collect(Collectors.toList()); } + /** + * Get the properties in this bag. + * + * @return All properties in this bag + */ + public List> getDerProperties() { + return Collections.unmodifiableList(Arrays.asList(properties)); + } + public int propertyIndexOf(@NonNull String name) { int i = probe(name); return i < 0 ? -1 : indexTable[i]; 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 674ff67a0..3cdcd3370 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 @@ -30,7 +30,10 @@ import io.micronaut.serde.reference.PropertyReference; import java.io.IOException; -import java.util.*; +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. @@ -307,7 +310,7 @@ public Object deserialize(Decoder decoder, DecoderContext decoderContext, Argume obj, objectDecoder, readProperties, - db.unwrappedProperties, + db.unwrappedProperties == null, buffer, anyValues, ignoreUnknown, @@ -344,7 +347,7 @@ public Object deserialize(Decoder decoder, DecoderContext decoderContext, Argume obj, objectDecoder, readProperties, - db.unwrappedProperties, + db.unwrappedProperties == null, existingBuffer, anyValues, ignoreUnknown, @@ -526,7 +529,7 @@ private PropertyBuffer decodeProperties( Object obj, Decoder objectDecoder, PropertiesBag.Consumer readProperties, - DeserBean.DerProperty[] unwrappedProperties, + boolean hasNoUnwrapped, PropertyBuffer propertyBuffer, @Nullable AnyValues anyValues, boolean ignoreUnknown, @@ -559,7 +562,7 @@ private PropertyBuffer decodeProperties( } else { propertyBuffer = initBuffer(propertyBuffer, property, prop, val); } - if (readProperties.isAllConsumed() && unwrappedProperties == null && introspection.anySetter == null) { + if (readProperties.isAllConsumed() && hasNoUnwrapped && introspection.anySetter == null) { break; } } else { @@ -656,60 +659,74 @@ private void applyDefaultValuesOrFail( DecoderContext decoderContext) throws IOException { final DeserBean unwrapped = property.unwrapped; if (unwrapped != null) { - Object object; - if (unwrapped.creatorParams != null) { - final PropertiesBag.Consumer creatorParams = unwrapped.creatorParams.newConsumer(); - Object[] params = new Object[unwrapped.creatorSize]; - // handle construction - for (DeserBean.DerProperty der : creatorParams.getNotConsumed()) { - boolean satisfied = false; - for (PropertyBuffer pb : buffer) { - if (pb.property == der) { - pb.set(params, decoderContext); - satisfied = true; - break; - } - } - 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 materializeUnwrapped(buffer, decoderContext, unwrapped); + } + 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; } } - if (preInstantiateCallback != null) { - preInstantiateCallback.preInstantiate(unwrapped.introspection, params); - } - object = unwrapped.introspection.instantiate(strictNullable, params); - } else { - if (preInstantiateCallback != null) { - preInstantiateCallback.preInstantiate(unwrapped.introspection); + 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"); + + } } - object = unwrapped.introspection.instantiate(strictNullable, ArrayUtils.EMPTY_OBJECT_ARRAY); } + if (preInstantiateCallback != null) { + preInstantiateCallback.preInstantiate(unwrapped.introspection, params); + } + 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) { - final PropertiesBag.Consumer readProperties = unwrapped.readProperties.newConsumer(); - for (DeserBean.DerProperty der : readProperties.getNotConsumed()) { - boolean satisfied = false; - for (PropertyBuffer pb : buffer) { - if (pb.property == der) { + 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); - satisfied = true; - break; } + satisfied = true; // belongs to another bean, that bean will handle setting the property + break; } - if (!satisfied) { - der.setDefaultPropertyValue(decoderContext, object); - } + } + if (!satisfied) { + der.setDefaultPropertyValue(decoderContext, object); } } - return object; } - return null; + return object; } @Override @@ -726,7 +743,7 @@ public void deserializeInto(Decoder decoder, DecoderContext decoderContext, Argu value, objectDecoder, readProperties, - deserBean.unwrappedProperties, + deserBean.unwrappedProperties == null, null, anyValues, ignoreUnknown, diff --git a/serde-support/src/main/java/io/micronaut/serde/support/serializers/SerBean.java b/serde-support/src/main/java/io/micronaut/serde/support/serializers/SerBean.java index 30a1b8180..1617a45f1 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/serializers/SerBean.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/serializers/SerBean.java @@ -33,6 +33,7 @@ import io.micronaut.core.order.OrderUtil; import io.micronaut.core.order.Ordered; import io.micronaut.core.type.Argument; +import io.micronaut.core.util.ArrayUtils; import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.serde.PropertyFilter; @@ -217,32 +218,14 @@ public int getOrder() { boolean unwrapped = propertyAnnotationMetadata.hasAnnotation(SerdeConfig.SerUnwrapped.class); PropertyNamingStrategy propertyNamingStrategy = getPropertyNamingStrategy(property.getAnnotationMetadata(), encoderContext, entityPropertyNamingStrategy); if (unwrapped) { - BeanIntrospection propertyIntrospection = introspections.getSerializableIntrospection(property.asArgument()); - Set ignoredProperties = Arrays.stream(argument.getAnnotationMetadata().stringValues(SerdeConfig.SerIgnored.class)).collect(Collectors.toSet()); - for (BeanProperty unwrappedProperty : propertyIntrospection.getBeanProperties()) { - if (!ignoredProperties.contains(unwrappedProperty.getName())) { - Argument unwrappedPropertyArgument = unwrappedProperty.asArgument(); - String n = resolveName(propertyAnnotationMetadata, - unwrappedProperty.getAnnotationMetadata(), - unwrappedPropertyArgument.getName(), - true, propertyNamingStrategy); - final AnnotationMetadataHierarchy combinedMetadata = - new AnnotationMetadataHierarchy( - argument.getAnnotationMetadata(), - unwrappedProperty.getAnnotationMetadata() - ); - if (!combinedMetadata.booleanValue(SerdeConfig.class, SerdeConfig.IGNORED).orElse(false) && - !combinedMetadata.booleanValue(SerdeConfig.class, SerdeConfig.READ_ONLY).orElse(false)) { - CustomSerProperty prop = new CustomSerProperty<>(SerBean.this, n, - unwrappedPropertyArgument, - combinedMetadata, - bean -> unwrappedProperty.get(property.get(bean)) - ); - writeProperties.add(prop); - initializers.add(ctx -> initProperty(prop, ctx)); - } - } - } + processUnwrapped( + introspections, + property, + argument, + propertyAnnotationMetadata, + propertyNamingStrategy, + null + ); } else { String n = resolveName(annotationMetadata, propertyAnnotationMetadata, defaultPropertyName, false, propertyNamingStrategy); final SerProperty serProperty = new PropSerProperty<>(SerBean.this, @@ -293,6 +276,65 @@ public int getOrder() { subtyped = isAbstractIntrospection || introspection.getAnnotationMetadata().hasDeclaredAnnotation(SerdeConfig.SerSubtyped.class); } + private void processUnwrapped( + SerdeIntrospections introspections, + BeanProperty property, + Argument argument, + AnnotationMetadata propertyAnnotationMetadata, + PropertyNamingStrategy propertyNamingStrategy, + Function nestedValueResolver) { + BeanIntrospection propertyIntrospection = introspections.getSerializableIntrospection(property.asArgument()); + Set ignoredProperties = Arrays.stream(argument.getAnnotationMetadata().stringValues(SerdeConfig.SerIgnored.class)).collect(Collectors.toSet()); + for (BeanProperty unwrappedProperty : propertyIntrospection.getBeanProperties()) { + if (!ignoredProperties.contains(unwrappedProperty.getName())) { + Argument unwrappedPropertyArgument = unwrappedProperty.asArgument(); + AnnotationMetadata unwrappedPropertyAnnotationMetadata = unwrappedProperty.getAnnotationMetadata(); + Function valueResolver; + + if (nestedValueResolver != null) { + valueResolver = bean -> unwrappedProperty.get(nestedValueResolver.apply(bean)); + } else { + valueResolver = bean -> unwrappedProperty.get(property.get(bean)); + } + + String n = resolveName(propertyAnnotationMetadata, + unwrappedPropertyAnnotationMetadata, + unwrappedPropertyArgument.getName(), + true, propertyNamingStrategy); + final AnnotationMetadataHierarchy combinedMetadata = + new AnnotationMetadataHierarchy( + argument.getAnnotationMetadata(), + unwrappedPropertyAnnotationMetadata + ); + + if (unwrappedPropertyAnnotationMetadata.hasDeclaredAnnotation(SerdeConfig.SerUnwrapped.class)) { + // nested unwrapped + processUnwrapped( + introspections, + (BeanProperty) unwrappedProperty, + unwrappedPropertyArgument, + combinedMetadata, + propertyNamingStrategy, + valueResolver + ); + } else { + + if (!combinedMetadata.booleanValue(SerdeConfig.class, SerdeConfig.IGNORED).orElse(false) && + !combinedMetadata.booleanValue(SerdeConfig.class, SerdeConfig.READ_ONLY).orElse(false)) { + + CustomSerProperty prop = new CustomSerProperty<>(SerBean.this, n, + unwrappedPropertyArgument, + combinedMetadata, + valueResolver + ); + writeProperties.add(prop); + initializers.add(ctx -> initProperty(prop, ctx)); + } + } + } + } + } + public void initialize(Serializer.EncoderContext encoderContext) throws SerdeException { if (!initialized) { synchronized (this) { @@ -383,9 +425,14 @@ public AnnotationMetadata getAnnotationMetadata() { .orElse(defaultPropertyName); } if (unwrapped) { - n = annotationMetadata.stringValue(SerdeConfig.SerUnwrapped.class, SerdeConfig.SerUnwrapped.PREFIX) - .orElse("") + n + annotationMetadata.stringValue(SerdeConfig.SerUnwrapped.class, SerdeConfig.SerUnwrapped.SUFFIX) - .orElse(""); + @NonNull String[] prefixes = annotationMetadata.stringValues(SerdeConfig.SerUnwrapped.class, SerdeConfig.SerUnwrapped.PREFIX); + @NonNull String[] suffixes = annotationMetadata.stringValues(SerdeConfig.SerUnwrapped.class, SerdeConfig.SerUnwrapped.SUFFIX); + if (ArrayUtils.isNotEmpty(prefixes) || ArrayUtils.isNotEmpty(suffixes)) { + List<@NonNull String> prefixList = Arrays.asList(prefixes); + Collections.reverse(prefixList); + List<@NonNull String> suffixList = Arrays.asList(suffixes); + return String.join("", prefixList) + n + String.join("", suffixList); + } } return n; }