Skip to content

Commit a71ffd6

Browse files
committed
Use FormattingConversionService in JSON Patch binding.
We no pipe the Spring MVC ConversionService into the JSON Patch path binding. That usually is a FormattingConversionService at runtime and also supports the conversion of dates. The ConversionServices is configured into the BindContext(Factory) we use for binding. The context then exposes the EvaluationContext set up with it. Fixes #2233.
1 parent fdb9c66 commit a71ffd6

File tree

9 files changed

+80
-17
lines changed

9 files changed

+80
-17
lines changed

spring-data-rest-tests/spring-data-rest-tests-mongodb/src/test/java/org/springframework/data/rest/webmvc/config/JsonPatchHandlerUnitTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.junit.jupiter.api.extension.ExtendWith;
3131
import org.mockito.Mock;
3232
import org.mockito.junit.jupiter.MockitoExtension;
33+
import org.springframework.core.convert.support.DefaultConversionService;
3334
import org.springframework.data.mapping.context.PersistentEntities;
3435
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
3536
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
@@ -76,7 +77,8 @@ void setUp() {
7677

7778
PersistentEntities entities = new PersistentEntities(Arrays.asList(context));
7879
Associations associations = new Associations(mappings, mock(RepositoryRestConfiguration.class));
79-
BindContextFactory factory = new PersistentEntitiesBindContextFactory(entities);
80+
BindContextFactory factory = new PersistentEntitiesBindContextFactory(entities,
81+
DefaultConversionService.getSharedInstance());
8082

8183
this.handler = new JsonPatchHandler(factory, new DomainObjectReader(entities, associations));
8284

spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ public PersistentEntityResourceHandlerMethodArgumentResolver persistentEntityArg
500500

501501
PluginRegistry<EntityLookup<?>, Class<?>> lookups = PluginRegistry.of(getEntityLookups());
502502
DomainObjectReader reader = new DomainObjectReader(entities, associationLinks);
503-
BindContextFactory factory = new PersistentEntitiesBindContextFactory(entities);
503+
BindContextFactory factory = new PersistentEntitiesBindContextFactory(entities, defaultConversionService);
504504

505505
return new PersistentEntityResourceHandlerMethodArgumentResolver(defaultMessageConverters,
506506
repoRequestArgumentResolver, backendIdHandlerMethodArgumentResolver,

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@
1717

1818
import java.util.Optional;
1919

20+
import org.springframework.core.convert.ConversionService;
2021
import org.springframework.data.mapping.PersistentProperty;
2122
import org.springframework.data.mapping.context.PersistentEntities;
2223
import org.springframework.data.rest.webmvc.json.patch.BindContext;
24+
import org.springframework.expression.EvaluationContext;
25+
import org.springframework.expression.spel.support.SimpleEvaluationContext;
2326
import org.springframework.util.Assert;
2427

2528
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -34,20 +37,24 @@ class JacksonBindContext implements BindContext {
3437

3538
private final PersistentEntities entities;
3639
private final ObjectMapper mapper;
40+
private final EvaluationContext context;
3741

3842
/**
3943
* Creates a new {@link JacksonBindContext} for the given {@link PersistentEntities} and {@link ObjectMapper}.
4044
*
4145
* @param entities must not be {@literal null}.
46+
* @param conversionService must not be {@literal null}.
4247
* @param mapper must not be {@literal null}.
4348
*/
44-
public JacksonBindContext(PersistentEntities entities, ObjectMapper mapper) {
49+
public JacksonBindContext(PersistentEntities entities, ConversionService conversionService, ObjectMapper mapper) {
4550

4651
Assert.notNull(entities, "PersistentEntities must not be null");
4752
Assert.notNull(mapper, "ObjectMapper must not be null");
53+
Assert.notNull(conversionService, "ConversionService must not be null!");
4854

4955
this.entities = entities;
5056
this.mapper = mapper;
57+
this.context = SimpleEvaluationContext.forReadWriteDataBinding().withConversionService(conversionService).build();
5158
}
5259

5360
@Override
@@ -66,6 +73,15 @@ public Optional<String> getWritableProperty(String segment, Class<?> type) {
6673
.filter(it -> it.isWritableField(segment)), segment);
6774
}
6875

76+
/*
77+
* (non-Javadoc)
78+
* @see org.springframework.data.rest.webmvc.json.patch.BindContext#getEvaluationContext()
79+
*/
80+
@Override
81+
public EvaluationContext getEvaluationContext() {
82+
return context;
83+
}
84+
6985
private static Optional<String> getProperty(Optional<MappedProperties> properties, String segment) {
7086

7187
return properties.map(it -> it.getPersistentProperty(segment))

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.rest.webmvc.json;
1717

18+
import org.springframework.core.convert.ConversionService;
1819
import org.springframework.data.mapping.context.PersistentEntities;
1920
import org.springframework.data.rest.webmvc.json.patch.BindContext;
2021
import org.springframework.util.Assert;
@@ -29,21 +30,23 @@
2930
public class PersistentEntitiesBindContextFactory implements BindContextFactory {
3031

3132
private final PersistentEntities entities;
33+
private final ConversionService conversionService;
3234

3335
/**
3436
* Creates a new {@link PersistentEntitiesBindContextFactory} for the given {@link PersistentEntities}.
3537
*
3638
* @param entities must not be {@literal null}.
3739
*/
38-
public PersistentEntitiesBindContextFactory(PersistentEntities entities) {
40+
public PersistentEntitiesBindContextFactory(PersistentEntities entities, ConversionService conversionService) {
3941

4042
Assert.notNull(entities, "PersistentEntities must not be null!");
4143

4244
this.entities = entities;
45+
this.conversionService = conversionService;
4346
}
4447

4548
@Override
4649
public BindContext getBindContextFor(ObjectMapper mapper) {
47-
return new JacksonBindContext(entities, mapper);
50+
return new JacksonBindContext(entities, conversionService, mapper);
4851
}
4952
}

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

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

1818
import java.util.Optional;
1919

20+
import org.springframework.expression.EvaluationContext;
21+
2022
/**
2123
* Contextual mapping for he translation of JSON Pointer segments into property references on persistent types.
2224
*
@@ -41,4 +43,12 @@ public interface BindContext {
4143
* @return will never be {@literal null}.
4244
*/
4345
Optional<String> getReadableProperty(String segment, Class<?> type);
46+
47+
/**
48+
* Returns the {@link EvaluationContext} to be used for evaluating the underlying SpEL expression.
49+
*
50+
* @return will never be {@literal null}.
51+
* @since 3.7.9
52+
*/
53+
EvaluationContext getEvaluationContext();
4454
}

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
import org.springframework.expression.spel.SpelEvaluationException;
3636
import org.springframework.expression.spel.SpelMessage;
3737
import org.springframework.expression.spel.standard.SpelExpressionParser;
38-
import org.springframework.expression.spel.support.SimpleEvaluationContext;
3938
import org.springframework.lang.Nullable;
4039
import org.springframework.util.Assert;
4140
import org.springframework.util.CollectionUtils;
@@ -142,7 +141,7 @@ public ReadingOperations bindForRead(Class<?> type, BindContext context) {
142141
return READ_PATHS.computeIfAbsent(CacheKey.of(type, this, context),
143142
key -> {
144143
String mapped = new JsonPointerMapping(context).forRead(key.path.path, type);
145-
return new TypedSpelPath(mapped, key.type);
144+
return new TypedSpelPath(mapped, key.type, context.getEvaluationContext());
146145
});
147146
}
148147

@@ -160,7 +159,7 @@ public WritingOperations bindForWrite(Class<?> type, BindContext context) {
160159
return WRITE_PATHS.computeIfAbsent(CacheKey.of(type, this, context),
161160
key -> {
162161
String mapped = new JsonPointerMapping(context).forWrite(key.path.path, type);
163-
return new TypedSpelPath(mapped, key.type);
162+
return new TypedSpelPath(mapped, key.type, context.getEvaluationContext());
164163
});
165164
}
166165

@@ -253,17 +252,18 @@ static class TypedSpelPath extends SpelPath implements ReadingOperations, Writin
253252

254253
private static final String INVALID_PATH_REFERENCE = "Invalid path reference %s on type %s";
255254
private static final String INVALID_COLLECTION_INDEX = "Invalid collection index %s for collection of size %s; Use '…/-' or the collection's actual size as index to append to it";
256-
private static final EvaluationContext CONTEXT = SimpleEvaluationContext.forReadWriteDataBinding().build();
257255

258256
private final Expression expression;
259257
private final Class<?> type;
258+
private final EvaluationContext context;
260259

261-
private TypedSpelPath(String path, Class<?> type) {
260+
private TypedSpelPath(String path, Class<?> type, EvaluationContext context) {
262261

263262
super(path);
264263

265264
this.type = type;
266265
this.expression = toSpel(path, type);
266+
this.context = context;
267267
}
268268

269269
/**
@@ -278,7 +278,7 @@ public <T> T getValue(Object target) {
278278
Assert.notNull(target, "Target must not be null!");
279279

280280
try {
281-
return (T) expression.getValue(CONTEXT, target);
281+
return (T) expression.getValue(context, target);
282282
} catch (ExpressionException o_O) {
283283
throw new PatchException("Unable to get value from target", o_O);
284284
}
@@ -294,7 +294,7 @@ public void setValue(Object target, @Nullable Object value) {
294294

295295
Assert.notNull(target, "Target must not be null!");
296296

297-
expression.setValue(CONTEXT, target, value);
297+
expression.setValue(context, target, value);
298298
}
299299

300300
/**
@@ -326,7 +326,7 @@ public Class<?> getType(Object root) {
326326

327327
try {
328328

329-
return expression.getValueType(CONTEXT, root);
329+
return expression.getValueType(context, root);
330330

331331
} catch (SpelEvaluationException o_O) {
332332

@@ -460,11 +460,11 @@ public String toString() {
460460
}
461461

462462
private TypedSpelPath getParent() {
463-
return new TypedSpelPath(path.substring(0, path.lastIndexOf('/')), type);
463+
return new TypedSpelPath(path.substring(0, path.lastIndexOf('/')), type, context);
464464
}
465465

466466
private TypeDescriptor getTypeDescriptor(Object target) {
467-
return expression.getValueTypeDescriptor(CONTEXT, target);
467+
return expression.getValueTypeDescriptor(context, target);
468468
}
469469

470470
private Integer getTargetListIndex() {

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.junit.jupiter.api.BeforeEach;
2222
import org.junit.jupiter.api.Test;
23+
import org.springframework.core.convert.support.DefaultConversionService;
2324
import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext;
2425
import org.springframework.data.mapping.context.PersistentEntities;
2526
import org.springframework.data.rest.webmvc.json.BindContextFactory;
@@ -45,7 +46,8 @@ void setUp() {
4546
context.getPersistentEntity(Sample.class);
4647

4748
PersistentEntities entities = new PersistentEntities(Arrays.asList(context));
48-
BindContextFactory factory = new PersistentEntitiesBindContextFactory(entities);
49+
BindContextFactory factory = new PersistentEntitiesBindContextFactory(entities,
50+
DefaultConversionService.getSharedInstance());
4951

5052
ObjectMapper mapper = new ObjectMapper();
5153
this.verifier = new JsonPointerMapping(factory.getBindContextFor(mapper));

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import lombok.Data;
2121
import lombok.Getter;
2222

23+
import java.time.LocalDate;
2324
import java.util.ArrayList;
2425
import java.util.Arrays;
2526
import java.util.List;
@@ -33,6 +34,9 @@
3334
import org.springframework.data.rest.webmvc.json.PersistentEntitiesBindContextFactory;
3435
import org.springframework.data.rest.webmvc.json.patch.SpelPath.UntypedSpelPath;
3536
import org.springframework.data.rest.webmvc.json.patch.SpelPath.WritingOperations;
37+
import org.springframework.format.annotation.DateTimeFormat;
38+
import org.springframework.format.annotation.DateTimeFormat.ISO;
39+
import org.springframework.format.support.DefaultFormattingConversionService;
3640

3741
import com.fasterxml.jackson.annotation.JsonIgnore;
3842
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -57,7 +61,8 @@ void setUp() {
5761
context.getPersistentEntity(Person.class);
5862

5963
PersistentEntities entities = new PersistentEntities(Arrays.asList(context));
60-
BindContextFactory factory = new PersistentEntitiesBindContextFactory(entities);
64+
BindContextFactory factory = new PersistentEntitiesBindContextFactory(entities,
65+
new DefaultFormattingConversionService());
6166

6267
this.context = factory.getBindContextFor(new ObjectMapper());
6368
}
@@ -167,6 +172,18 @@ void mapsRenamedProperty() {
167172
assertThat(path.getExpressionString()).isEqualTo("renamed");
168173
}
169174

175+
@Test // #2233
176+
void bindsDatesProperly() {
177+
178+
Person person = new Person();
179+
180+
SpelPath.untyped("/birthday")
181+
.bindForWrite(Person.class, context)
182+
.setValue(person, "2000-01-01");
183+
184+
assertThat(person.birthday).isEqualTo(LocalDate.of(2000, 1, 1));
185+
}
186+
170187
// DATAREST-1338
171188

172189
@Data
@@ -175,6 +192,7 @@ static class Person {
175192
@JsonIgnore String hiddenProperty;
176193
@Getter(onMethod = @__(@JsonIgnore)) String hiddenGetter;
177194
@JsonProperty("demaner") String renamed;
195+
@DateTimeFormat(iso = ISO.DATE) LocalDate birthday;
178196
}
179197

180198
@Data

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

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

1818
import java.util.Optional;
1919

20+
import org.springframework.expression.EvaluationContext;
21+
import org.springframework.expression.spel.support.SimpleEvaluationContext;
22+
2023
public class TestPropertyPathContext implements BindContext {
2124

2225
public static final BindContext INSTANCE = new TestPropertyPathContext();
@@ -38,4 +41,13 @@ public Optional<String> getReadableProperty(String segment, Class<?> type) {
3841
public Optional<String> getWritableProperty(String segment, Class<?> type) {
3942
return Optional.of(segment);
4043
}
44+
45+
/*
46+
* (non-Javadoc)
47+
* @see org.springframework.data.rest.webmvc.json.patch.BindContext#getEvaluationContext()
48+
*/
49+
@Override
50+
public EvaluationContext getEvaluationContext() {
51+
return SimpleEvaluationContext.forReadWriteDataBinding().build();
52+
}
4153
}

0 commit comments

Comments
 (0)