Skip to content

Commit 924fab2

Browse files
committed
Fix spring-projects#968 @JsonUnwrapped ignored with (embedded) CollectionModel and HAL Forms
1 parent c388a85 commit 924fab2

File tree

3 files changed

+77
-24
lines changed

3 files changed

+77
-24
lines changed

src/main/java/org/springframework/hateoas/mediatype/PropertyUtils.java

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@
1515
*/
1616
package org.springframework.hateoas.mediatype;
1717

18-
import lombok.AccessLevel;
19-
import lombok.RequiredArgsConstructor;
20-
18+
import javax.validation.constraints.NotNull;
19+
import javax.validation.constraints.Pattern;
2120
import java.beans.PropertyDescriptor;
2221
import java.lang.annotation.Annotation;
2322
import java.lang.reflect.Field;
@@ -28,9 +27,13 @@
2827
import java.util.stream.Collectors;
2928
import java.util.stream.Stream;
3029

31-
import javax.validation.constraints.NotNull;
32-
import javax.validation.constraints.Pattern;
33-
30+
import com.fasterxml.jackson.annotation.JsonIgnore;
31+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
32+
import com.fasterxml.jackson.annotation.JsonProperty;
33+
import com.fasterxml.jackson.annotation.JsonProperty.Access;
34+
import com.fasterxml.jackson.annotation.JsonUnwrapped;
35+
import lombok.AccessLevel;
36+
import lombok.RequiredArgsConstructor;
3437
import org.reactivestreams.Publisher;
3538
import org.springframework.beans.BeanUtils;
3639
import org.springframework.beans.BeanWrapper;
@@ -45,16 +48,7 @@
4548
import org.springframework.hateoas.EntityModel;
4649
import org.springframework.http.HttpEntity;
4750
import org.springframework.lang.Nullable;
48-
import org.springframework.util.Assert;
49-
import org.springframework.util.ClassUtils;
50-
import org.springframework.util.ConcurrentReferenceHashMap;
51-
import org.springframework.util.ReflectionUtils;
52-
import org.springframework.util.StringUtils;
53-
54-
import com.fasterxml.jackson.annotation.JsonIgnore;
55-
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
56-
import com.fasterxml.jackson.annotation.JsonProperty;
57-
import com.fasterxml.jackson.annotation.JsonProperty.Access;
51+
import org.springframework.util.*;
5852

5953
/**
6054
* @author Greg Turnquist
@@ -85,6 +79,10 @@ static List<Class<?>> getTypesToUnwrap() {
8579
}
8680

8781
public static Map<String, Object> extractPropertyValues(@Nullable Object object) {
82+
return extractPropertyValues(object, false);
83+
}
84+
85+
public static Map<String, Object> extractPropertyValues(@Nullable Object object, boolean unwrapEligibleProperties) {
8886

8987
if (object == null) {
9088
return Collections.emptyMap();
@@ -98,7 +96,10 @@ public static Map<String, Object> extractPropertyValues(@Nullable Object object)
9896

9997
return getExposedProperties(object.getClass()).stream() //
10098
.map(PropertyMetadata::getName)
101-
.collect(HashMap::new, (map, name) -> map.put(name, wrapper.getPropertyValue(name)), HashMap::putAll);
99+
.map(name -> unwrapEligibleProperties ? unwrapPropertyIfNeeded(name, wrapper) :
100+
Collections.singletonMap(name, wrapper.getPropertyValue(name)))
101+
.flatMap(it -> it.entrySet().stream())
102+
.collect(HashMap::new, (map, it) -> map.put(it.getKey(), it.getValue()), HashMap::putAll);
102103
}
103104

104105
public static <T> T createObjectFromProperties(Class<T> clazz, Map<String, Object> properties) {
@@ -113,7 +114,6 @@ public static <T> T createObjectFromProperties(Class<T> clazz, Map<String, Objec
113114
Method writeMethod = property.getWriteMethod();
114115
ReflectionUtils.makeAccessible(writeMethod);
115116
writeMethod.invoke(obj, value);
116-
117117
} catch (IllegalAccessException | InvocationTargetException e) {
118118

119119
throw new RuntimeException(e);
@@ -151,6 +151,33 @@ public static InputPayloadMetadata getExposedProperties(@Nullable ResolvableType
151151
});
152152
}
153153

154+
private static Map<String, Object> unwrapPropertyIfNeeded(String propertyName, BeanWrapper wrapper) {
155+
Field descriptorField = ReflectionUtils.findField(wrapper.getWrappedClass(), propertyName);
156+
Method readMethod = wrapper.getPropertyDescriptor(propertyName).getReadMethod();
157+
158+
MergedAnnotation<JsonUnwrapped> unwrappedAnnotation =
159+
Stream.of(descriptorField, readMethod)
160+
.filter(Objects::nonNull)
161+
.map(MergedAnnotations::from)
162+
.flatMap(mergedAnnotations -> mergedAnnotations.stream(JsonUnwrapped.class))
163+
.filter(it -> it.getBoolean("enabled"))
164+
.findFirst()
165+
.orElse(null);
166+
167+
Object propertyValue = wrapper.getPropertyValue(propertyName);
168+
if (unwrappedAnnotation == null) {
169+
return Collections.singletonMap(propertyName, propertyValue);
170+
}
171+
172+
String prefix = unwrappedAnnotation.getString("prefix");
173+
String suffix = unwrappedAnnotation.getString("suffix");
174+
175+
Map<String, Object> properties = new HashMap<>();
176+
extractPropertyValues(propertyValue, true)
177+
.forEach((name, value) -> properties.put(prefix + name + suffix, value));
178+
return properties;
179+
}
180+
154181
private static ResolvableType unwrapDomainType(ResolvableType type) {
155182

156183
if (!type.hasGenerics()) {
@@ -169,7 +196,7 @@ private static ResolvableType unwrapDomainType(ResolvableType type) {
169196
* Replaces the given {@link ResolvableType} with the one produced by the given {@link Supplier} if the former is
170197
* assignable from one of the types to be unwrapped.
171198
*
172-
* @param type must not be {@literal null}.
199+
* @param type must not be {@literal null}.
173200
* @param mapper must not be {@literal null}.
174201
* @return
175202
* @see #TYPES_TO_UNWRAP
@@ -188,8 +215,8 @@ private static Stream<PropertyMetadata> lookupExposedProperties(@Nullable Class<
188215
return type == null //
189216
? Stream.empty() //
190217
: getPropertyDescriptors(type) //
191-
.map(it -> new AnnotatedProperty(new Property(type, it.getReadMethod(), it.getWriteMethod())))
192-
.map(it -> JSR_303_PRESENT ? new Jsr303AwarePropertyMetadata(it) : new DefaultPropertyMetadata(it));
218+
.map(it -> new AnnotatedProperty(new Property(type, it.getReadMethod(), it.getWriteMethod())))
219+
.map(it -> JSR_303_PRESENT ? new Jsr303AwarePropertyMetadata(it) : new DefaultPropertyMetadata(it));
193220
}
194221

195222
/**
@@ -359,7 +386,7 @@ public boolean hasWriteMethod() {
359386
/**
360387
* Returns the {@link MergedAnnotation} of the given type.
361388
*
362-
* @param <T> the annotation type.
389+
* @param <T> the annotation type.
363390
* @param type must not be {@literal null}.
364391
* @return the {@link MergedAnnotation} if available or {@link MergedAnnotation#missing()} if not.
365392
*/
@@ -457,7 +484,6 @@ public ResolvableType getType() {
457484
public int compareTo(DefaultPropertyMetadata that) {
458485
return BY_NAME.compare(this, that);
459486
}
460-
461487
}
462488

463489
/**

src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDocument.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ private HalFormsDocument() {
108108
*/
109109
public static HalFormsDocument<?> forRepresentationModel(RepresentationModel<?> model) {
110110

111-
Map<String, Object> attributes = PropertyUtils.extractPropertyValues(model);
111+
Map<String, Object> attributes = PropertyUtils.extractPropertyValues(model, true);
112112
attributes.remove("links");
113113

114114
return new HalFormsDocument<>().withAttributes(attributes);

src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import static org.assertj.core.api.Assertions.*;
1919

20+
import com.fasterxml.jackson.annotation.JsonUnwrapped;
2021
import lombok.Getter;
2122
import net.minidev.json.JSONArray;
2223

@@ -516,6 +517,18 @@ void considersJsr303AnnotationsForTemplates() throws Exception {
516517
assertValueForPath(model, "$._templates.default.properties[0].required", true);
517518
}
518519

520+
/**
521+
* @see #968
522+
*/
523+
@Test
524+
void considerJsonUnwrapped() throws Exception {
525+
UnwrappedExample unwrappedExample = new UnwrappedExample();
526+
unwrappedExample.element = new UnwrappedExampleElement();
527+
unwrappedExample.element.firstname = "john";
528+
529+
assertValueForPath(unwrappedExample, "$.firstname", "john");
530+
}
531+
519532
private void assertThatPathDoesNotExist(Object toMarshall, String path) throws Exception {
520533

521534
ObjectMapper mapper = getCuriedObjectMapper();
@@ -621,4 +634,18 @@ public String getFirstname() {
621634
return firstname;
622635
}
623636
}
637+
638+
public static class UnwrappedExample extends RepresentationModel<UnwrappedExample> {
639+
640+
private UnwrappedExampleElement element;
641+
642+
@JsonUnwrapped
643+
public UnwrappedExampleElement getElement(){
644+
return element;
645+
}
646+
}
647+
648+
public static class UnwrappedExampleElement {
649+
private @Getter String firstname;
650+
}
624651
}

0 commit comments

Comments
 (0)