diff --git a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValuesMap.java b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValuesMap.java index 7354bbdb7a5..c32deebe903 100644 --- a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValuesMap.java +++ b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValuesMap.java @@ -25,6 +25,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -137,4 +138,20 @@ protected Map> wrapValues(Map> value return Collections.unmodifiableMap(values); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConvertibleMultiValuesMap that = (ConvertibleMultiValuesMap) o; + return values.equals(that.values); + } + + @Override + public int hashCode() { + return Objects.hash(values); + } } diff --git a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValuesMap.java b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValuesMap.java index aa63a3f0ae2..bf0d80485f7 100644 --- a/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValuesMap.java +++ b/core/src/main/java/io/micronaut/core/convert/value/ConvertibleValuesMap.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -104,4 +105,21 @@ public Collection values() { public static ConvertibleValues empty() { return EMPTY; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConvertibleValuesMap that = (ConvertibleValuesMap) o; + return map.equals(that.map); + } + + @Override + public int hashCode() { + return Objects.hash(map); + } } diff --git a/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java b/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java index 3e8b0ba0b37..5559ed42fc0 100644 --- a/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java +++ b/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java @@ -422,7 +422,7 @@ public P get(@NonNull B bean) { throw new IllegalArgumentException("Invalid bean [" + bean + "] for type: " + beanType); } if (isWriteOnly()) { - throw new UnsupportedOperationException("Cannot read from a write-only property"); + throw new UnsupportedOperationException("Cannot read from a write-only property: " + getName()); } return dispatchOne(ref.getMethodIndex, bean, null); } diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java b/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java index f374f801583..b31184b09f6 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/modules/BeanIntrospectionModule.java @@ -347,7 +347,9 @@ public JsonSerializer build() { final List newProperties = new ArrayList<>(properties); Map> named = new LinkedHashMap<>(); for (BeanProperty beanProperty : beanProperties) { - named.put(getName(config, namingStrategy, beanProperty), beanProperty); + if (!beanProperty.isWriteOnly()) { + named.put(getName(config, namingStrategy, beanProperty), beanProperty); + } } for (int i = 0; i < properties.size(); i++) { final BeanPropertyWriter existing = properties.get(i); diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/serialize/ConvertibleMultiValuesSerializer.java b/jackson-databind/src/main/java/io/micronaut/jackson/serialize/ConvertibleMultiValuesSerializer.java index 9626980c5d1..455412cd895 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/serialize/ConvertibleMultiValuesSerializer.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/serialize/ConvertibleMultiValuesSerializer.java @@ -50,12 +50,12 @@ public void serialize(ConvertibleMultiValues value, JsonGenerator gen, Serial if (len > 0) { gen.writeFieldName(fieldName); if (len == 1) { - gen.writeObject(v.get(0)); + serializers.defaultSerializeValue(v.get(0), gen); } else { gen.writeStartArray(); for (Object o : v) { - gen.writeObject(o); + serializers.defaultSerializeValue(o, gen); } gen.writeEndArray(); } diff --git a/jackson-databind/src/main/java/io/micronaut/jackson/serialize/ConvertibleValuesSerializer.java b/jackson-databind/src/main/java/io/micronaut/jackson/serialize/ConvertibleValuesSerializer.java index b495d828249..3ebadecf0c9 100644 --- a/jackson-databind/src/main/java/io/micronaut/jackson/serialize/ConvertibleValuesSerializer.java +++ b/jackson-databind/src/main/java/io/micronaut/jackson/serialize/ConvertibleValuesSerializer.java @@ -47,7 +47,7 @@ public void serialize(ConvertibleValues value, JsonGenerator gen, SerializerP Object v = entry.getValue(); if (v != null) { gen.writeFieldName(fieldName); - gen.writeObject(v); + serializers.defaultSerializeValue(v, gen); } } gen.writeEndObject(); diff --git a/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleSpec.groovy b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleSpec.groovy index 0e09ea561d4..ce054da2a81 100644 --- a/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleSpec.groovy +++ b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/BeanIntrospectionModuleSpec.groovy @@ -32,16 +32,145 @@ import io.micronaut.http.hateoas.JsonError import io.micronaut.jackson.JacksonConfiguration import io.micronaut.jackson.modules.testcase.EmailTemplate import io.micronaut.jackson.modules.testcase.Notification -import io.micronaut.jackson.modules.wrappers.* +import io.micronaut.jackson.modules.testclasses.HTTPCheck +import io.micronaut.jackson.modules.testclasses.InstanceInfo +import io.micronaut.jackson.modules.wrappers.BooleanWrapper +import io.micronaut.jackson.modules.wrappers.DoubleWrapper +import io.micronaut.jackson.modules.wrappers.IntWrapper +import io.micronaut.jackson.modules.wrappers.IntegerWrapper +import io.micronaut.jackson.modules.wrappers.LongWrapper +import io.micronaut.jackson.modules.wrappers.StringWrapper import spock.lang.Issue -import spock.lang.Unroll import spock.lang.Specification +import spock.lang.Unroll import java.beans.ConstructorProperties import java.time.LocalDateTime class BeanIntrospectionModuleSpec extends Specification { + void "test serialize/deserialize wrap/unwrap - simple"() { + given: + ApplicationContext ctx = ApplicationContext.run( + 'jackson.deserialization.UNWRAP_ROOT_VALUE': true, + 'jackson.serialization.WRAP_ROOT_VALUE': true + ) + ObjectMapper objectMapper = ctx.getBean(ObjectMapper) + + when: + Author author = new Author(name:"Bob") + + def result = objectMapper.writeValueAsString(author) + + then: + result == '{"Author":{"name":"Bob"}}' + + when: + def read = objectMapper.readValue(result, Author) + + then: + author == read + + } + + void "test serialize/deserialize wrap/unwrap -* complex"() { + given: + ApplicationContext ctx = ApplicationContext.run( + 'jackson.deserialization.UNWRAP_ROOT_VALUE': true, + 'jackson.serialization.WRAP_ROOT_VALUE': true + ) + ObjectMapper objectMapper = ctx.getBean(ObjectMapper) + + when: + HTTPCheck check = new HTTPCheck(headers:[ + Accept:['application/json', 'application/xml'] + ] ) + + def result = objectMapper.writeValueAsString(check) + + then: + result == '{"HTTPCheck":{"Header":{"Accept":["application/json","application/xml"]}}}' + + when: + def read = objectMapper.readValue(result, HTTPCheck) + + then: + check == read + + } + + void "test serialize/deserialize wrap/unwrap -* constructors"() { + given: + ApplicationContext ctx = ApplicationContext.run( + 'jackson.deserialization.UNWRAP_ROOT_VALUE': true, + 'jackson.serialization.WRAP_ROOT_VALUE': true + ) + ObjectMapper objectMapper = ctx.getBean(ObjectMapper) + + when: + IntrospectionCreator check = new IntrospectionCreator("test") + + def result = objectMapper.writeValueAsString(check) + + then: + result == '{"IntrospectionCreator":{"label":"TEST"}}' + + when: + def read = objectMapper.readValue(result, IntrospectionCreator) + + then: + check == read + + } + + void "test serialize/deserialize wrap/unwrap -* constructors & JsonRootName"() { + given: + ApplicationContext ctx = ApplicationContext.run( + 'jackson.deserialization.UNWRAP_ROOT_VALUE': true, + 'jackson.serialization.WRAP_ROOT_VALUE': true + ) + ObjectMapper objectMapper = ctx.getBean(ObjectMapper) + + when: + InstanceInfo check = new InstanceInfo("test") + + def result = objectMapper.writeValueAsString(check) + + then: + result == '{"instance":{"hostName":"test"}}' + + when: + def read = objectMapper.readValue(result, InstanceInfo) + + then: + check == read + + } + + + void "test serialize/deserialize convertible values"() { + given: + ApplicationContext ctx = ApplicationContext.run() + ObjectMapper objectMapper = ctx.getBean(ObjectMapper) + + when: + HTTPCheck check = new HTTPCheck(headers:[ + Accept:['application/json', 'application/xml'] + ] ) + + def result = objectMapper.writeValueAsString(check) + + then: + result == '{"Header":{"Accept":["application/json","application/xml"]}}' + + when: + def read = objectMapper.readValue(result, HTTPCheck) + + then: + check.header.getAll("Accept") == read.header.getAll("Accept") + + } + void "Bean introspection works with a bean without JsonIgnore annotations"() { given: ApplicationContext ctx = ApplicationContext.run() @@ -598,6 +727,7 @@ class BeanIntrospectionModuleSpec extends Specification { } @Introspected + @EqualsAndHashCode static class Author { String name } @@ -826,6 +956,7 @@ class BeanIntrospectionModuleSpec extends Specification { } @Introspected + @EqualsAndHashCode static class IntrospectionCreator { private final String name diff --git a/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/testclasses/HTTPCheck.java b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/testclasses/HTTPCheck.java new file mode 100644 index 00000000000..a011da667b3 --- /dev/null +++ b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/testclasses/HTTPCheck.java @@ -0,0 +1,46 @@ +package io.micronaut.jackson.modules.testclasses; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.convert.value.ConvertibleMultiValues; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) +@Introspected +public class HTTPCheck { + private ConvertibleMultiValues headers = ConvertibleMultiValues.empty(); + + public ConvertibleMultiValues getHeader() { + return headers; + } + + /** + * @param headers The headers + */ + @JsonProperty("Header") + public void setHeaders(Map> headers) { + if (headers == null) { + this.headers = ConvertibleMultiValues.empty(); + } else { + this.headers = ConvertibleMultiValues.of(headers); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + HTTPCheck httpCheck = (HTTPCheck) o; + return headers.equals(httpCheck.headers); + } + + @Override + public int hashCode() { + return Objects.hash(headers); + } +} diff --git a/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/testclasses/InstanceInfo.java b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/testclasses/InstanceInfo.java new file mode 100644 index 00000000000..57509ffc3e0 --- /dev/null +++ b/jackson-databind/src/test/groovy/io/micronaut/jackson/modules/testclasses/InstanceInfo.java @@ -0,0 +1,37 @@ +package io.micronaut.jackson.modules.testclasses; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; +import io.micronaut.core.annotation.Introspected; + +import java.util.Objects; + +@JsonRootName("instance") +@Introspected +public class InstanceInfo { + private final String hostName; + + @JsonCreator + InstanceInfo( + @JsonProperty("hostName") String hostName) { + this.hostName = hostName; + } + + public String getHostName() { + return hostName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + InstanceInfo that = (InstanceInfo) o; + return hostName.equals(that.hostName); + } + + @Override + public int hashCode() { + return Objects.hash(hostName); + } +}