Skip to content

Fix #968 @JsonUnwrapped ignored with (embedded) CollectionModel and HAL Forms #1269

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package org.springframework.hateoas.mediatype;

import com.fasterxml.jackson.annotation.JsonUnwrapped;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;

Expand Down Expand Up @@ -85,6 +86,10 @@ static List<Class<?>> getTypesToUnwrap() {
}

public static Map<String, Object> extractPropertyValues(@Nullable Object object) {
return extractPropertyValues(object, false);
}

public static Map<String, Object> extractPropertyValues(@Nullable Object object, boolean unwrapEligibleProperties) {

if (object == null) {
return Collections.emptyMap();
Expand All @@ -98,7 +103,10 @@ public static Map<String, Object> 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> T createObjectFromProperties(Class<T> clazz, Map<String, Object> properties) {
Expand All @@ -113,7 +121,6 @@ public static <T> T createObjectFromProperties(Class<T> clazz, Map<String, Objec
Method writeMethod = property.getWriteMethod();
ReflectionUtils.makeAccessible(writeMethod);
writeMethod.invoke(obj, value);

} catch (IllegalAccessException | InvocationTargetException e) {

throw new RuntimeException(e);
Expand Down Expand Up @@ -151,6 +158,33 @@ public static InputPayloadMetadata getExposedProperties(@Nullable ResolvableType
});
}

private static Map<String, Object> unwrapPropertyIfNeeded(String propertyName, BeanWrapper wrapper) {
Field descriptorField = ReflectionUtils.findField(wrapper.getWrappedClass(), propertyName);
Method readMethod = wrapper.getPropertyDescriptor(propertyName).getReadMethod();

MergedAnnotation<JsonUnwrapped> 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<String, Object> 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()) {
Expand All @@ -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
Expand All @@ -188,8 +222,8 @@ private static Stream<PropertyMetadata> 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));
}

/**
Expand Down Expand Up @@ -359,7 +393,7 @@ public boolean hasWriteMethod() {
/**
* Returns the {@link MergedAnnotation} of the given type.
*
* @param <T> the annotation type.
* @param <T> the annotation type.
* @param type must not be {@literal null}.
* @return the {@link MergedAnnotation} if available or {@link MergedAnnotation#missing()} if not.
*/
Expand Down Expand Up @@ -457,7 +491,6 @@ public ResolvableType getType() {
public int compareTo(DefaultPropertyMetadata that) {
return BY_NAME.compare(this, that);
}

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ private HalFormsDocument() {
*/
public static HalFormsDocument<?> forRepresentationModel(RepresentationModel<?> model) {

Map<String, Object> attributes = PropertyUtils.extractPropertyValues(model);
Map<String, Object> attributes = PropertyUtils.extractPropertyValues(model, true);
attributes.remove("links");

return new HalFormsDocument<>().withAttributes(attributes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -621,4 +634,18 @@ public String getFirstname() {
return firstname;
}
}

public static class UnwrappedExample extends RepresentationModel<UnwrappedExample> {

private UnwrappedExampleElement element;

@JsonUnwrapped
public UnwrappedExampleElement getElement(){
return element;
}
}

public static class UnwrappedExampleElement {
private @Getter String firstname;
}
}