Skip to content

Commit 132807c

Browse files
committed
DATAREST-965 - Switched to property based application of PUT requests.
Jacksons ObjectMapper.readerForUpdate(…) unfortunately doesn't handle nested objects properly. We already have a manual merge process in place for PATCH requests but tweaking that to also handle PUT requests gracefully caused more complexity than anticipated. We now switched to an object based merge so that we can read in the source JSON structure into a new object and then merge the objects. Related pull request: #247.
1 parent 294db99 commit 132807c

File tree

3 files changed

+242
-17
lines changed

3 files changed

+242
-17
lines changed

spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/DomainObjectReader.java

+156-7
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,28 @@
2222
import java.util.ArrayList;
2323
import java.util.Arrays;
2424
import java.util.Collection;
25+
import java.util.Collections;
2526
import java.util.Iterator;
2627
import java.util.Map;
2728
import java.util.Map.Entry;
2829

30+
import org.springframework.beans.PropertyAccessor;
31+
import org.springframework.beans.PropertyAccessorFactory;
32+
import org.springframework.core.CollectionFactory;
33+
import org.springframework.core.convert.ConversionService;
34+
import org.springframework.core.convert.support.DefaultConversionService;
2935
import org.springframework.data.mapping.PersistentEntity;
3036
import org.springframework.data.mapping.PersistentProperty;
3137
import org.springframework.data.mapping.PersistentPropertyAccessor;
3238
import org.springframework.data.mapping.SimplePropertyHandler;
3339
import org.springframework.data.mapping.context.PersistentEntities;
40+
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
3441
import org.springframework.data.rest.webmvc.mapping.Associations;
3542
import org.springframework.data.util.ClassTypeInformation;
3643
import org.springframework.data.util.TypeInformation;
3744
import org.springframework.http.converter.HttpMessageNotReadableException;
3845
import org.springframework.util.Assert;
46+
import org.springframework.util.ObjectUtils;
3947

4048
import com.fasterxml.jackson.databind.JsonNode;
4149
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -88,6 +96,7 @@ public <T> T read(InputStream source, T target, ObjectMapper mapper) {
8896
* @param mapper
8997
* @return
9098
*/
99+
@SuppressWarnings("unchecked")
91100
public <T> T readPut(final ObjectNode source, T target, final ObjectMapper mapper) {
92101

93102
Assert.notNull(source, "ObjectNode must not be null!");
@@ -100,8 +109,50 @@ public <T> T readPut(final ObjectNode source, T target, final ObjectMapper mappe
100109

101110
Assert.notNull(entity, "No PersistentEntity found for ".concat(type.getName()).concat("!"));
102111

112+
try {
113+
114+
Object intermediate = mapper.readerFor(target.getClass()).readValue(source);
115+
return (T) mergeForPut(intermediate, target, mapper);
116+
117+
} catch (Exception o_O) {
118+
throw new HttpMessageNotReadableException("Could not read payload!", o_O);
119+
}
120+
}
121+
122+
/**
123+
* Merges the state of given source object onto the target one preserving PUT semantics.
124+
*
125+
* @param source can be {@literal null}.
126+
* @param target can be {@literal null}.
127+
* @param mapper must not be {@literal null}.
128+
* @return
129+
*/
130+
private <T> T mergeForPut(T source, T target, final ObjectMapper mapper) {
131+
132+
Assert.notNull(mapper, "ObjectMapper must not be null!");
133+
134+
if (target == null || source == null) {
135+
return source;
136+
}
137+
138+
Class<? extends Object> type = target.getClass();
139+
140+
final PersistentEntity<?, ?> entity = entities.getPersistentEntity(type);
141+
142+
if (entity == null) {
143+
return source;
144+
}
145+
146+
Assert.notNull(entity, "No PersistentEntity found for ".concat(type.getName()).concat("!"));
147+
103148
final MappedProperties properties = MappedProperties.fromJacksonProperties(entity, mapper);
104149

150+
ConversionService conversionService = new DefaultConversionService();
151+
final PersistentPropertyAccessor targetAccessor = entity.getPropertyAccessor(target);
152+
final ConvertingPropertyAccessor convertingAccessor = new ConvertingPropertyAccessor(targetAccessor,
153+
conversionService);
154+
final PersistentPropertyAccessor sourceAccessor = entity.getPropertyAccessor(source);
155+
105156
entity.doWithProperties(new SimplePropertyHandler() {
106157

107158
/*
@@ -115,18 +166,50 @@ public void doWithPersistentProperty(PersistentProperty<?> property) {
115166
return;
116167
}
117168

118-
String mappedName = properties.getMappedName(property);
169+
if (!properties.isMappedProperty(property)) {
170+
return;
171+
}
119172

120-
boolean isMappedProperty = mappedName != null;
121-
boolean noValueInSource = !source.has(mappedName);
173+
Object sourceValue = sourceAccessor.getProperty(property);
174+
Object targetValue = targetAccessor.getProperty(property);
175+
Object result = null;
122176

123-
if (isMappedProperty && noValueInSource) {
124-
source.putNull(mappedName);
177+
if (property.isMap()) {
178+
result = mergeMaps(property, sourceValue, targetValue, mapper);
179+
} else if (property.isCollectionLike()) {
180+
result = mergeCollections(property, sourceValue, targetValue, mapper);
181+
} else if (property.isEntity()) {
182+
result = mergeForPut(sourceValue, targetValue, mapper);
183+
} else {
184+
result = sourceValue;
125185
}
186+
187+
convertingAccessor.setProperty(property, result);
126188
}
127189
});
128190

129-
return merge(source, target, mapper);
191+
// Need to copy unmapped properties as the PersistentProperty model currently does not contain any transient
192+
// properties
193+
copyRemainingProperties(properties, source, target);
194+
195+
return target;
196+
}
197+
198+
/**
199+
* Copies the unmapped properties of the given {@link MappedProperties} from the source object to the target instance.
200+
*
201+
* @param properties must not be {@literal null}.
202+
* @param source must not be {@literal null}.
203+
* @param target must not be {@literal null}.
204+
*/
205+
private static void copyRemainingProperties(MappedProperties properties, Object source, Object target) {
206+
207+
PropertyAccessor sourceAccessor = PropertyAccessorFactory.forDirectFieldAccess(source);
208+
PropertyAccessor targetAccessor = PropertyAccessorFactory.forDirectFieldAccess(target);
209+
210+
for (String property : properties.getSpringDataUnmappedProperties()) {
211+
targetAccessor.setPropertyValue(property, sourceAccessor.getPropertyValue(property));
212+
}
130213
}
131214

132215
public <T> T merge(ObjectNode source, T target, ObjectMapper mapper) {
@@ -148,7 +231,7 @@ public <T> T merge(ObjectNode source, T target, ObjectMapper mapper) {
148231
* @throws Exception
149232
*/
150233
@SuppressWarnings("unchecked")
151-
private <T> T doMerge(ObjectNode root, T target, ObjectMapper mapper) throws Exception {
234+
<T> T doMerge(ObjectNode root, T target, ObjectMapper mapper) throws Exception {
152235

153236
Assert.notNull(root, "Root ObjectNode must not be null!");
154237
Assert.notNull(target, "Target object instance must not be null!");
@@ -351,6 +434,72 @@ private void doMergeNestedMap(Map<Object, Object> source, ObjectNode node, Objec
351434
}
352435
}
353436

437+
@SuppressWarnings("unchecked")
438+
private Map<Object, Object> mergeMaps(PersistentProperty<?> property, Object source, Object target,
439+
ObjectMapper mapper) {
440+
441+
Map<Object, Object> sourceMap = (Map<Object, Object>) source;
442+
443+
if (sourceMap == null) {
444+
return null;
445+
}
446+
447+
Map<Object, Object> targetMap = (Map<Object, Object>) target;
448+
Map<Object, Object> result = targetMap == null ? CollectionFactory.createMap(Map.class, sourceMap.size())
449+
: CollectionFactory.createApproximateMap(targetMap, sourceMap.size());
450+
451+
for (Entry<Object, Object> entry : sourceMap.entrySet()) {
452+
453+
Object targetValue = targetMap == null ? null : targetMap.get(entry.getKey());
454+
result.put(entry.getKey(), mergeForPut(entry.getValue(), targetValue, mapper));
455+
}
456+
457+
return result;
458+
}
459+
460+
private Collection<Object> mergeCollections(PersistentProperty<?> property, Object source, Object target,
461+
ObjectMapper mapper) {
462+
463+
Collection<Object> sourceCollection = asCollection(source);
464+
465+
if (sourceCollection == null) {
466+
return null;
467+
}
468+
469+
Collection<Object> targetCollection = asCollection(target);
470+
Collection<Object> result = targetCollection == null
471+
? CollectionFactory.createCollection(Collection.class, sourceCollection.size())
472+
: CollectionFactory.createApproximateCollection(targetCollection, sourceCollection.size());
473+
474+
Iterator<Object> sourceIterator = sourceCollection.iterator();
475+
Iterator<Object> targetIterator = targetCollection == null ? Collections.emptyIterator()
476+
: targetCollection.iterator();
477+
478+
while (sourceIterator.hasNext()) {
479+
480+
Object sourceElement = sourceIterator.next();
481+
Object targetElement = targetIterator.hasNext() ? targetIterator.next() : null;
482+
483+
result.add(mergeForPut(sourceElement, targetElement, mapper));
484+
}
485+
486+
return result;
487+
}
488+
489+
@SuppressWarnings("unchecked")
490+
private static Collection<Object> asCollection(Object source) {
491+
492+
if (source == null) {
493+
return null;
494+
} else if (source instanceof Collection) {
495+
return (Collection<Object>) source;
496+
} else if (source.getClass().isArray()) {
497+
return Arrays.asList(ObjectUtils.toObjectArray(source));
498+
} else {
499+
return Collections.singleton(source);
500+
}
501+
}
502+
354503
/**
355504
* Returns the given source instance as {@link Collection} or creates a new one for the given type.
356505
*

spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/MappedProperties.java

+46-4
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@
1515
*/
1616
package org.springframework.data.rest.webmvc.json;
1717

18+
import java.util.ArrayList;
19+
import java.util.Collections;
1820
import java.util.HashMap;
21+
import java.util.HashSet;
22+
import java.util.List;
1923
import java.util.Map;
24+
import java.util.Set;
2025

2126
import org.springframework.data.mapping.PersistentEntity;
2227
import org.springframework.data.mapping.PersistentProperty;
@@ -38,8 +43,9 @@ class MappedProperties {
3843

3944
private static final ClassIntrospector INTROSPECTOR = new BasicClassIntrospector();
4045

41-
private final Map<PersistentProperty<?>, String> propertyToFieldName;
46+
private final Map<PersistentProperty<?>, BeanPropertyDefinition> propertyToFieldName;
4247
private final Map<String, PersistentProperty<?>> fieldNameToProperty;
48+
private final Set<BeanPropertyDefinition> unmappedProperties;
4349

4450
/**
4551
* Creates a new {@link MappedProperties} instance for the given {@link PersistentEntity} and {@link BeanDescription}.
@@ -52,16 +58,19 @@ private MappedProperties(PersistentEntity<?, ?> entity, BeanDescription descript
5258
Assert.notNull(entity, "Entity must not be null!");
5359
Assert.notNull(description, "BeanDescription must not be null!");
5460

55-
this.propertyToFieldName = new HashMap<PersistentProperty<?>, String>();
61+
this.propertyToFieldName = new HashMap<PersistentProperty<?>, BeanPropertyDefinition>();
5662
this.fieldNameToProperty = new HashMap<String, PersistentProperty<?>>();
63+
this.unmappedProperties = new HashSet<BeanPropertyDefinition>();
5764

5865
for (BeanPropertyDefinition property : description.findProperties()) {
5966

6067
PersistentProperty<?> persistentProperty = entity.getPersistentProperty(property.getInternalName());
6168

6269
if (persistentProperty != null) {
63-
propertyToFieldName.put(persistentProperty, property.getName());
70+
propertyToFieldName.put(persistentProperty, property);
6471
fieldNameToProperty.put(property.getName(), persistentProperty);
72+
} else {
73+
unmappedProperties.add(property);
6574
}
6675
}
6776
}
@@ -89,7 +98,7 @@ public String getMappedName(PersistentProperty<?> property) {
8998

9099
Assert.notNull(property, "PersistentProperty must not be null!");
91100

92-
return propertyToFieldName.get(property);
101+
return propertyToFieldName.get(property).getName();
93102
}
94103

95104
/**
@@ -113,4 +122,37 @@ public PersistentProperty<?> getPersistentProperty(String fieldName) {
113122

114123
return fieldNameToProperty.get(fieldName);
115124
}
125+
126+
/**
127+
* Returns all properties only known to Jackson.
128+
*
129+
* @return the names of all properties that are not known to Spring Data but appear in the Jackson metamodel.
130+
*/
131+
public Iterable<String> getSpringDataUnmappedProperties() {
132+
133+
if (unmappedProperties.isEmpty()) {
134+
return Collections.emptySet();
135+
}
136+
137+
List<String> result = new ArrayList<String>(unmappedProperties.size());
138+
139+
for (BeanPropertyDefinition definitions : unmappedProperties) {
140+
result.add(definitions.getInternalName());
141+
}
142+
143+
return result;
144+
}
145+
146+
/**
147+
* Returns whether the given {@link PersistentProperty} is mapped, i.e. known to both Jackson and Spring Data.
148+
*
149+
* @param property must not be {@literal null}.
150+
* @return
151+
*/
152+
public boolean isMappedProperty(PersistentProperty<?> property) {
153+
154+
Assert.notNull(property, "PersistentProperty must not be null!");
155+
156+
return propertyToFieldName.containsKey(property);
157+
}
116158
}

0 commit comments

Comments
 (0)