Skip to content

Commit 3182b39

Browse files
committed
Fix PUT merge handling for polymorphic properties.
We now replace the original value of polymorphic properties with the newly deserialized one if the type of the new one is different from the old one. We still copy all JSON ignored properties to the new instance to make sure that non-exposed, server-side state is retained. We apply the same handling for explicitly immutable source and/or target types. Fixes #2130.
1 parent ffde83a commit 3182b39

File tree

5 files changed

+97
-12
lines changed

5 files changed

+97
-12
lines changed

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

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.springframework.data.util.ClassTypeInformation;
4343
import org.springframework.data.util.TypeInformation;
4444
import org.springframework.http.converter.HttpMessageNotReadableException;
45+
import org.springframework.lang.Nullable;
4546
import org.springframework.util.Assert;
4647
import org.springframework.util.ObjectUtils;
4748

@@ -124,6 +125,7 @@ public <T> T readPut(final ObjectNode source, T target, final ObjectMapper mappe
124125
* @param mapper must not be {@literal null}.
125126
* @return
126127
*/
128+
@Nullable
127129
<T> T mergeForPut(T source, T target, final ObjectMapper mapper) {
128130

129131
Assert.notNull(mapper, "ObjectMapper must not be null!");
@@ -132,41 +134,53 @@ <T> T mergeForPut(T source, T target, final ObjectMapper mapper) {
132134
return source;
133135
}
134136

135-
Class<? extends Object> type = target.getClass();
137+
boolean isTypeChange = !source.getClass().isInstance(target);
136138

137-
return entities.getPersistentEntity(type) //
138-
.filter(it -> !it.isImmutable()) //
139+
boolean immutableTarget = entities.getPersistentEntity(target.getClass())
140+
.map(PersistentEntity::isImmutable)
141+
.orElse(true); // Not a Spring Data managed type -> no detailed merging
142+
143+
return entities.getPersistentEntity(isTypeChange ? source.getClass() : target.getClass()) //
139144
.map(it -> {
140145

146+
MappedProperties properties = MappedProperties.forDeserialization(it, mapper);
147+
148+
if (isTypeChange || immutableTarget || it.isImmutable()) {
149+
150+
copyRemainingProperties(properties.getIgnoredProperties(), target, source);
151+
152+
return source;
153+
}
154+
141155
MergingPropertyHandler propertyHandler = new MergingPropertyHandler(source, target, it, mapper);
142156

143157
it.doWithProperties(propertyHandler);
144158
it.doWithAssociations(new LinkedAssociationSkippingAssociationHandler(associationLinks, propertyHandler));
145159

146160
// Need to copy unmapped properties as the PersistentProperty model currently does not contain any transient
147161
// properties
148-
copyRemainingProperties(propertyHandler.getProperties(), source, target);
162+
copyRemainingProperties(properties.getSpringDataUnmappedProperties(), source, target);
149163

150164
return target;
151165

152166
}).orElse(source);
153167
}
154168

155169
/**
156-
* Copies the unmapped properties of the given {@link MappedProperties} from the source object to the target instance.
170+
* Copies the given properties from the source object to the target instance.
157171
*
158-
* @param properties must not be {@literal null}.
172+
* @param propertyNames must not be {@literal null}.
159173
* @param source must not be {@literal null}.
160174
* @param target must not be {@literal null}.
161175
*/
162-
private static void copyRemainingProperties(MappedProperties properties, Object source, Object target) {
176+
private static void copyRemainingProperties(Iterable<String> propertyNames, Object source, Object target) {
163177

164178
PropertyAccessor sourceFieldAccessor = PropertyAccessorFactory.forDirectFieldAccess(source);
165179
PropertyAccessor sourcePropertyAccessor = PropertyAccessorFactory.forBeanPropertyAccess(source);
166180
PropertyAccessor targetFieldAccessor = PropertyAccessorFactory.forDirectFieldAccess(target);
167181
PropertyAccessor targetPropertyAccessor = PropertyAccessorFactory.forBeanPropertyAccess(target);
168182

169-
for (String property : properties.getSpringDataUnmappedProperties()) {
183+
for (String property : propertyNames) {
170184

171185
// If there's a field we can just copy it.
172186
if (targetFieldAccessor.isWritableProperty(property)) {

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,16 @@ public Iterable<String> getSpringDataUnmappedProperties() {
199199
return result;
200200
}
201201

202+
/**
203+
* Returns all property names of ignored properties.
204+
*
205+
* @return will never be {@literal null}.
206+
* @since 3.5.11, 3.6.4
207+
*/
208+
public Iterable<String> getIgnoredProperties() {
209+
return ignoredPropertyNames;
210+
}
211+
202212
/**
203213
* Returns whether the given {@link PersistentProperty} is mapped, i.e. known to both Jackson and Spring Data.
204214
*

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,5 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
/**
17-
*
18-
* @author Oliver Gierke
19-
*/
16+
@org.springframework.lang.NonNullApi
2017
package org.springframework.data.rest.webmvc.json;

spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/DomainObjectReaderUnitTests.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@
5555
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
5656
import com.fasterxml.jackson.annotation.JsonIgnore;
5757
import com.fasterxml.jackson.annotation.JsonProperty;
58+
import com.fasterxml.jackson.annotation.JsonSubTypes;
59+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
60+
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
5861
import com.fasterxml.jackson.core.JsonParser;
5962
import com.fasterxml.jackson.core.JsonProcessingException;
6063
import com.fasterxml.jackson.databind.DeserializationContext;
@@ -104,6 +107,8 @@ void setUp() {
104107
mappingContext.getPersistentEntity(Note.class);
105108
mappingContext.getPersistentEntity(WithNullCollection.class);
106109
mappingContext.getPersistentEntity(ArrayHolder.class);
110+
mappingContext.getPersistentEntity(Apple.class);
111+
mappingContext.getPersistentEntity(Pear.class);
107112
mappingContext.afterPropertiesSet();
108113

109114
this.entities = new PersistentEntities(Collections.singleton(mappingContext));
@@ -587,6 +592,32 @@ void arraysCanBeResizedDuringMerge() throws Exception {
587592
assertThat(updated.array).containsExactly("new");
588593
}
589594

595+
@Test // #2130
596+
void writesPolymorphicArrayWithSwitchedItemForPut() throws Exception {
597+
598+
Apple apple = new Apple();
599+
apple.apple = "apple";
600+
apple.color = "red";
601+
apple.ignored = "ignored";
602+
603+
Pear pear = new Pear();
604+
pear.pear = "pear";
605+
606+
Fruit result = reader.mergeForPut(pear, apple, new ObjectMapper());
607+
608+
assertThat(result).isInstanceOfSatisfying(Pear.class, it -> {
609+
610+
// Exposed property is wiped as expected for PUT
611+
assertThat(it.color).isNull();
612+
613+
// Non-exposed state is transferred
614+
assertThat(it.ignored).isEqualTo("ignored");
615+
616+
// Type specific state applied, too
617+
assertThat(it.pear).isEqualTo("pear");
618+
});
619+
}
620+
590621
@SuppressWarnings("unchecked")
591622
private static <T> T as(Object source, Class<T> type) {
592623

@@ -816,4 +847,32 @@ static class WithNullCollection {
816847
static class ArrayHolder {
817848
String[] array;
818849
}
850+
851+
// DATAREST-1026
852+
853+
@JsonAutoDetect(fieldVisibility = Visibility.ANY)
854+
static class Basket {
855+
856+
@Id Long id;
857+
List<Fruit> fruits;
858+
}
859+
860+
@JsonAutoDetect(fieldVisibility = Visibility.ANY)
861+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = As.PROPERTY, property = "type")
862+
@JsonSubTypes({ @JsonSubTypes.Type(name = "Apple", value = Apple.class),
863+
@JsonSubTypes.Type(name = "Pear", value = Pear.class) })
864+
static class Fruit {
865+
String color;
866+
@JsonIgnore String ignored;
867+
}
868+
869+
@JsonAutoDetect(fieldVisibility = Visibility.ANY)
870+
static class Apple extends Fruit {
871+
String apple;
872+
}
873+
874+
@JsonAutoDetect(fieldVisibility = Visibility.ANY)
875+
static class Pear extends Fruit {
876+
String pear;
877+
}
819878
}

spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/MappedPropertiesUnitTests.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ void exposesExistanceOfCatchAllMethod() {
115115
assertThat(properties.isWritableProperty("someRandomProperty")).isTrue();
116116
}
117117

118+
@Test // #2130
119+
void exposesIgnoredProperties() {
120+
assertThat(properties.getIgnoredProperties()).contains("notExposedByJackson");
121+
}
122+
118123
static class Sample {
119124

120125
public @Transient String notExposedBySpringData;

0 commit comments

Comments
 (0)