Skip to content

Commit e7077cd

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 1bcd717 commit e7077cd

File tree

2 files changed

+241
-17
lines changed

2 files changed

+241
-17
lines changed

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

Lines changed: 201 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,32 @@
2222
import java.util.ArrayList;
2323
import java.util.Arrays;
2424
import java.util.Collection;
25+
import java.util.Collections;
2526
import java.util.HashMap;
27+
import java.util.HashSet;
2628
import java.util.Iterator;
29+
import java.util.List;
2730
import java.util.Map;
2831
import java.util.Map.Entry;
32+
import java.util.Set;
2933

34+
import org.springframework.beans.PropertyAccessor;
35+
import org.springframework.beans.PropertyAccessorFactory;
36+
import org.springframework.core.CollectionFactory;
37+
import org.springframework.core.convert.ConversionService;
38+
import org.springframework.core.convert.support.DefaultConversionService;
3039
import org.springframework.data.mapping.PersistentEntity;
3140
import org.springframework.data.mapping.PersistentProperty;
3241
import org.springframework.data.mapping.PersistentPropertyAccessor;
3342
import org.springframework.data.mapping.SimplePropertyHandler;
3443
import org.springframework.data.mapping.context.PersistentEntities;
44+
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
3545
import org.springframework.data.rest.webmvc.mapping.Associations;
3646
import org.springframework.data.util.ClassTypeInformation;
3747
import org.springframework.data.util.TypeInformation;
3848
import org.springframework.http.converter.HttpMessageNotReadableException;
3949
import org.springframework.util.Assert;
50+
import org.springframework.util.ObjectUtils;
4051

4152
import com.fasterxml.jackson.databind.BeanDescription;
4253
import com.fasterxml.jackson.databind.JsonNode;
@@ -92,6 +103,7 @@ public <T> T read(InputStream source, T target, ObjectMapper mapper) {
92103
* @param mapper
93104
* @return
94105
*/
106+
@SuppressWarnings("unchecked")
95107
public <T> T readPut(final ObjectNode source, T target, final ObjectMapper mapper) {
96108

97109
Assert.notNull(source, "ObjectNode must not be null!");
@@ -104,8 +116,50 @@ public <T> T readPut(final ObjectNode source, T target, final ObjectMapper mappe
104116

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

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

157+
ConversionService conversionService = new DefaultConversionService();
158+
final PersistentPropertyAccessor targetAccessor = entity.getPropertyAccessor(target);
159+
final ConvertingPropertyAccessor convertingAccessor = new ConvertingPropertyAccessor(targetAccessor,
160+
conversionService);
161+
final PersistentPropertyAccessor sourceAccessor = entity.getPropertyAccessor(source);
162+
109163
entity.doWithProperties(new SimplePropertyHandler() {
110164

111165
/*
@@ -119,18 +173,50 @@ public void doWithPersistentProperty(PersistentProperty<?> property) {
119173
return;
120174
}
121175

122-
String mappedName = properties.getMappedName(property);
176+
if (!properties.isMappedProperty(property)) {
177+
return;
178+
}
123179

124-
boolean isMappedProperty = mappedName != null;
125-
boolean noValueInSource = !source.has(mappedName);
180+
Object sourceValue = sourceAccessor.getProperty(property);
181+
Object targetValue = targetAccessor.getProperty(property);
182+
Object result = null;
126183

127-
if (isMappedProperty && noValueInSource) {
128-
source.putNull(mappedName);
184+
if (property.isMap()) {
185+
result = mergeMaps(property, sourceValue, targetValue, mapper);
186+
} else if (property.isCollectionLike()) {
187+
result = mergeCollections(property, sourceValue, targetValue, mapper);
188+
} else if (property.isEntity()) {
189+
result = mergeForPut(sourceValue, targetValue, mapper);
190+
} else {
191+
result = sourceValue;
129192
}
193+
194+
convertingAccessor.setProperty(property, result);
130195
}
131196
});
132197

133-
return merge(source, target, mapper);
198+
// Need to copy unmapped properties as the PersistentProperty model currently does not contain any transient
199+
// properties
200+
copyRemainingProperties(properties, source, target);
201+
202+
return target;
203+
}
204+
205+
/**
206+
* Copies the unmapped properties of the given {@link MappedProperties} from the source object to the target instance.
207+
*
208+
* @param properties must not be {@literal null}.
209+
* @param source must not be {@literal null}.
210+
* @param target must not be {@literal null}.
211+
*/
212+
private static void copyRemainingProperties(MappedProperties properties, Object source, Object target) {
213+
214+
PropertyAccessor sourceAccessor = PropertyAccessorFactory.forDirectFieldAccess(source);
215+
PropertyAccessor targetAccessor = PropertyAccessorFactory.forDirectFieldAccess(target);
216+
217+
for (String property : properties.getSpringDataUnmappedProperties()) {
218+
targetAccessor.setPropertyValue(property, sourceAccessor.getPropertyValue(property));
219+
}
134220
}
135221

136222
public <T> T merge(ObjectNode source, T target, ObjectMapper mapper) {
@@ -152,7 +238,7 @@ public <T> T merge(ObjectNode source, T target, ObjectMapper mapper) {
152238
* @throws Exception
153239
*/
154240
@SuppressWarnings("unchecked")
155-
private <T> T doMerge(ObjectNode root, T target, ObjectMapper mapper) throws Exception {
241+
<T> T doMerge(ObjectNode root, T target, ObjectMapper mapper) throws Exception {
156242

157243
Assert.notNull(root, "Root ObjectNode must not be null!");
158244
Assert.notNull(target, "Target object instance must not be null!");
@@ -355,6 +441,72 @@ private void doMergeNestedMap(Map<Object, Object> source, ObjectNode node, Objec
355441
}
356442
}
357443

444+
@SuppressWarnings("unchecked")
445+
private Map<Object, Object> mergeMaps(PersistentProperty<?> property, Object source, Object target,
446+
ObjectMapper mapper) {
447+
448+
Map<Object, Object> sourceMap = (Map<Object, Object>) source;
449+
450+
if (sourceMap == null) {
451+
return null;
452+
}
453+
454+
Map<Object, Object> targetMap = (Map<Object, Object>) target;
455+
Map<Object, Object> result = targetMap == null ? CollectionFactory.createMap(Map.class, sourceMap.size())
456+
: CollectionFactory.createApproximateMap(targetMap, sourceMap.size());
457+
458+
for (Entry<Object, Object> entry : sourceMap.entrySet()) {
459+
460+
Object targetValue = targetMap == null ? null : targetMap.get(entry.getKey());
461+
result.put(entry.getKey(), mergeForPut(entry.getValue(), targetValue, mapper));
462+
}
463+
464+
return result;
465+
}
466+
467+
private Collection<Object> mergeCollections(PersistentProperty<?> property, Object source, Object target,
468+
ObjectMapper mapper) {
469+
470+
Collection<Object> sourceCollection = asCollection(source);
471+
472+
if (sourceCollection == null) {
473+
return null;
474+
}
475+
476+
Collection<Object> targetCollection = asCollection(target);
477+
Collection<Object> result = targetCollection == null
478+
? CollectionFactory.createCollection(Collection.class, sourceCollection.size())
479+
: CollectionFactory.createApproximateCollection(targetCollection, sourceCollection.size());
480+
481+
Iterator<Object> sourceIterator = sourceCollection.iterator();
482+
Iterator<Object> targetIterator = targetCollection == null ? Collections.emptyIterator()
483+
: targetCollection.iterator();
484+
485+
while (sourceIterator.hasNext()) {
486+
487+
Object sourceElement = sourceIterator.next();
488+
Object targetElement = targetIterator.hasNext() ? targetIterator.next() : null;
489+
490+
result.add(mergeForPut(sourceElement, targetElement, mapper));
491+
}
492+
493+
return result;
494+
}
495+
496+
@SuppressWarnings("unchecked")
497+
private static Collection<Object> asCollection(Object source) {
498+
499+
if (source == null) {
500+
return null;
501+
} else if (source instanceof Collection) {
502+
return (Collection<Object>) source;
503+
} else if (source.getClass().isArray()) {
504+
return Arrays.asList(ObjectUtils.toObjectArray(source));
505+
} else {
506+
return Collections.singleton(source);
507+
}
508+
}
509+
358510
/**
359511
* Returns the given source instance as {@link Collection} or creates a new one for the given type.
360512
*
@@ -402,13 +554,15 @@ private static Class<?> typeOrObject(TypeInformation<?> type) {
402554
* Simple value object to capture a mapping of Jackson mapped field names and {@link PersistentProperty} instances.
403555
*
404556
* @author Oliver Gierke
557+
* @author Mark Paluch
405558
*/
406559
static class MappedProperties {
407560

408561
private static final ClassIntrospector INTROSPECTOR = new BasicClassIntrospector();
409562

410-
private final Map<PersistentProperty<?>, String> propertyToFieldName;
563+
private final Map<PersistentProperty<?>, BeanPropertyDefinition> propertyToFieldName;
411564
private final Map<String, PersistentProperty<?>> fieldNameToProperty;
565+
private final Set<BeanPropertyDefinition> unmappedProperties;
412566

413567
/**
414568
* Creates a new {@link MappedProperties} instance for the given {@link PersistentEntity} and
@@ -422,16 +576,19 @@ private MappedProperties(PersistentEntity<?, ?> entity, BeanDescription descript
422576
Assert.notNull(entity, "Entity must not be null!");
423577
Assert.notNull(description, "BeanDescription must not be null!");
424578

425-
this.propertyToFieldName = new HashMap<PersistentProperty<?>, String>();
579+
this.propertyToFieldName = new HashMap<PersistentProperty<?>, BeanPropertyDefinition>();
426580
this.fieldNameToProperty = new HashMap<String, PersistentProperty<?>>();
581+
this.unmappedProperties = new HashSet<BeanPropertyDefinition>();
427582

428583
for (BeanPropertyDefinition property : description.findProperties()) {
429584

430585
PersistentProperty<?> persistentProperty = entity.getPersistentProperty(property.getInternalName());
431586

432587
if (persistentProperty != null) {
433-
propertyToFieldName.put(persistentProperty, property.getName());
588+
propertyToFieldName.put(persistentProperty, property);
434589
fieldNameToProperty.put(property.getName(), persistentProperty);
590+
} else {
591+
unmappedProperties.add(property);
435592
}
436593
}
437594
}
@@ -459,7 +616,7 @@ public String getMappedName(PersistentProperty<?> property) {
459616

460617
Assert.notNull(property, "PersistentProperty must not be null!");
461618

462-
return propertyToFieldName.get(property);
619+
return propertyToFieldName.get(property).getName();
463620
}
464621

465622
/**
@@ -483,5 +640,38 @@ public PersistentProperty<?> getPersistentProperty(String fieldName) {
483640

484641
return fieldNameToProperty.get(fieldName);
485642
}
643+
644+
/**
645+
* Returns all properties only known to Jackson.
646+
*
647+
* @return the names of all properties that are not known to Spring Data but appear in the Jackson metamodel.
648+
*/
649+
public Iterable<String> getSpringDataUnmappedProperties() {
650+
651+
if (unmappedProperties.isEmpty()) {
652+
return Collections.emptySet();
653+
}
654+
655+
List<String> result = new ArrayList<String>(unmappedProperties.size());
656+
657+
for (BeanPropertyDefinition definitions : unmappedProperties) {
658+
result.add(definitions.getInternalName());
659+
}
660+
661+
return result;
662+
}
663+
664+
/**
665+
* Returns whether the given {@link PersistentProperty} is mapped, i.e. known to both Jackson and Spring Data.
666+
*
667+
* @param property must not be {@literal null}.
668+
* @return
669+
*/
670+
public boolean isMappedProperty(PersistentProperty<?> property) {
671+
672+
Assert.notNull(property, "PersistentProperty must not be null!");
673+
674+
return propertyToFieldName.containsKey(property);
675+
}
486676
}
487677
}

0 commit comments

Comments
 (0)