diff --git a/src/main/java/org/springframework/hateoas/mediatype/PropertyUtils.java b/src/main/java/org/springframework/hateoas/mediatype/PropertyUtils.java index ae19f97e3..96d1a7c19 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/PropertyUtils.java +++ b/src/main/java/org/springframework/hateoas/mediatype/PropertyUtils.java @@ -15,6 +15,7 @@ */ package org.springframework.hateoas.mediatype; +import com.fasterxml.jackson.annotation.JsonUnwrapped; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; @@ -85,6 +86,10 @@ static List> getTypesToUnwrap() { } public static Map extractPropertyValues(@Nullable Object object) { + return extractPropertyValues(object, false); + } + + public static Map extractPropertyValues(@Nullable Object object, boolean unwrapEligibleProperties) { if (object == null) { return Collections.emptyMap(); @@ -98,7 +103,10 @@ public static Map extractPropertyValues(@Nullable Object object) return getExposedProperties(object.getClass()).stream() // .map(PropertyMetadata::getName) - .collect(HashMap::new, (map, name) -> map.put(name, wrapper.getPropertyValue(name)), HashMap::putAll); + .map(name -> unwrapEligibleProperties ? unwrapPropertyIfNeeded(name, wrapper) : + Collections.singletonMap(name, wrapper.getPropertyValue(name))) + .flatMap(it -> it.entrySet().stream()) + .collect(HashMap::new, (map, it) -> map.put(it.getKey(), it.getValue()), HashMap::putAll); } public static T createObjectFromProperties(Class clazz, Map properties) { @@ -113,7 +121,6 @@ public static T createObjectFromProperties(Class clazz, Map unwrapPropertyIfNeeded(String propertyName, BeanWrapper wrapper) { + Field descriptorField = ReflectionUtils.findField(wrapper.getWrappedClass(), propertyName); + Method readMethod = wrapper.getPropertyDescriptor(propertyName).getReadMethod(); + + MergedAnnotation unwrappedAnnotation = + Stream.of(descriptorField, readMethod) + .filter(Objects::nonNull) + .map(MergedAnnotations::from) + .flatMap(mergedAnnotations -> mergedAnnotations.stream(JsonUnwrapped.class)) + .filter(it -> it.getBoolean("enabled")) + .findFirst() + .orElse(null); + + Object propertyValue = wrapper.getPropertyValue(propertyName); + if (unwrappedAnnotation == null) { + return Collections.singletonMap(propertyName, propertyValue); + } + + String prefix = unwrappedAnnotation.getString("prefix"); + String suffix = unwrappedAnnotation.getString("suffix"); + + Map properties = new HashMap<>(); + extractPropertyValues(propertyValue, true) + .forEach((name, value) -> properties.put(prefix + name + suffix, value)); + return properties; + } + private static ResolvableType unwrapDomainType(ResolvableType type) { if (!type.hasGenerics()) { @@ -169,7 +203,7 @@ private static ResolvableType unwrapDomainType(ResolvableType type) { * Replaces the given {@link ResolvableType} with the one produced by the given {@link Supplier} if the former is * assignable from one of the types to be unwrapped. * - * @param type must not be {@literal null}. + * @param type must not be {@literal null}. * @param mapper must not be {@literal null}. * @return * @see #TYPES_TO_UNWRAP @@ -188,8 +222,8 @@ private static Stream lookupExposedProperties(@Nullable Class< return type == null // ? Stream.empty() // : getPropertyDescriptors(type) // - .map(it -> new AnnotatedProperty(new Property(type, it.getReadMethod(), it.getWriteMethod()))) - .map(it -> JSR_303_PRESENT ? new Jsr303AwarePropertyMetadata(it) : new DefaultPropertyMetadata(it)); + .map(it -> new AnnotatedProperty(new Property(type, it.getReadMethod(), it.getWriteMethod()))) + .map(it -> JSR_303_PRESENT ? new Jsr303AwarePropertyMetadata(it) : new DefaultPropertyMetadata(it)); } /** @@ -359,7 +393,7 @@ public boolean hasWriteMethod() { /** * Returns the {@link MergedAnnotation} of the given type. * - * @param the annotation type. + * @param the annotation type. * @param type must not be {@literal null}. * @return the {@link MergedAnnotation} if available or {@link MergedAnnotation#missing()} if not. */ @@ -457,7 +491,6 @@ public ResolvableType getType() { public int compareTo(DefaultPropertyMetadata that) { return BY_NAME.compare(this, that); } - } /** diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDocument.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDocument.java index f033c234d..e71b0db26 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDocument.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDocument.java @@ -108,7 +108,7 @@ private HalFormsDocument() { */ public static HalFormsDocument forRepresentationModel(RepresentationModel model) { - Map attributes = PropertyUtils.extractPropertyValues(model); + Map attributes = PropertyUtils.extractPropertyValues(model, true); attributes.remove("links"); return new HalFormsDocument<>().withAttributes(attributes); diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java index bb4108652..83a46b46d 100644 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.*; +import com.fasterxml.jackson.annotation.JsonUnwrapped; import lombok.Getter; import net.minidev.json.JSONArray; @@ -516,6 +517,18 @@ void considersJsr303AnnotationsForTemplates() throws Exception { assertValueForPath(model, "$._templates.default.properties[0].required", true); } + /** + * @see #968 + */ + @Test + void considerJsonUnwrapped() throws Exception { + UnwrappedExample unwrappedExample = new UnwrappedExample(); + unwrappedExample.element = new UnwrappedExampleElement(); + unwrappedExample.element.firstname = "john"; + + assertValueForPath(unwrappedExample, "$.firstname", "john"); + } + private void assertThatPathDoesNotExist(Object toMarshall, String path) throws Exception { ObjectMapper mapper = getCuriedObjectMapper(); @@ -621,4 +634,18 @@ public String getFirstname() { return firstname; } } + + public static class UnwrappedExample extends RepresentationModel { + + private UnwrappedExampleElement element; + + @JsonUnwrapped + public UnwrappedExampleElement getElement(){ + return element; + } + } + + public static class UnwrappedExampleElement { + private @Getter String firstname; + } }