Skip to content

Commit fb19242

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 2a88cc7 commit fb19242

File tree

9 files changed

+78
-17
lines changed

9 files changed

+78
-17
lines changed

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

+3-1
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

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ public PersistentEntityResourceHandlerMethodArgumentResolver persistentEntityArg
496496

497497
PluginRegistry<EntityLookup<?>, Class<?>> lookups = PluginRegistry.of(getEntityLookups());
498498
DomainObjectReader reader = new DomainObjectReader(entities, associationLinks);
499-
BindContextFactory factory = new PersistentEntitiesBindContextFactory(entities);
499+
BindContextFactory factory = new PersistentEntitiesBindContextFactory(entities, defaultConversionService);
500500

501501
return new PersistentEntityResourceHandlerMethodArgumentResolver(defaultMessageConverters,
502502
repoRequestArgumentResolver, backendIdHandlerMethodArgumentResolver,

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

+17-1
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))

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

+5-2
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
}

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

+10
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
}

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

+10-10
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;
@@ -130,7 +129,7 @@ public ReadingOperations bindForRead(Class<?> type, BindContext context) {
130129
return READ_PATHS.computeIfAbsent(CacheKey.of(type, this, context),
131130
key -> {
132131
String mapped = new JsonPointerMapping(context).forRead(key.path.path, type);
133-
return new TypedSpelPath(mapped, key.type);
132+
return new TypedSpelPath(mapped, key.type, context.getEvaluationContext());
134133
});
135134
}
136135

@@ -148,7 +147,7 @@ public WritingOperations bindForWrite(Class<?> type, BindContext context) {
148147
return WRITE_PATHS.computeIfAbsent(CacheKey.of(type, this, context),
149148
key -> {
150149
String mapped = new JsonPointerMapping(context).forWrite(key.path.path, type);
151-
return new TypedSpelPath(mapped, key.type);
150+
return new TypedSpelPath(mapped, key.type, context.getEvaluationContext());
152151
});
153152
}
154153

@@ -233,17 +232,18 @@ static class TypedSpelPath extends SpelPath implements ReadingOperations, Writin
233232

234233
private static final String INVALID_PATH_REFERENCE = "Invalid path reference %s on type %s";
235234
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";
236-
private static final EvaluationContext CONTEXT = SimpleEvaluationContext.forReadWriteDataBinding().build();
237235

238236
private final Expression expression;
239237
private final Class<?> type;
238+
private final EvaluationContext context;
240239

241-
private TypedSpelPath(String path, Class<?> type) {
240+
private TypedSpelPath(String path, Class<?> type, EvaluationContext context) {
242241

243242
super(path);
244243

245244
this.type = type;
246245
this.expression = toSpel(path, type);
246+
this.context = context;
247247
}
248248

249249
/**
@@ -258,7 +258,7 @@ public <T> T getValue(Object target) {
258258
Assert.notNull(target, "Target must not be null");
259259

260260
try {
261-
return (T) expression.getValue(CONTEXT, target);
261+
return (T) expression.getValue(context, target);
262262
} catch (ExpressionException o_O) {
263263
throw new PatchException("Unable to get value from target", o_O);
264264
}
@@ -274,7 +274,7 @@ public void setValue(Object target, @Nullable Object value) {
274274

275275
Assert.notNull(target, "Target must not be null");
276276

277-
expression.setValue(CONTEXT, target, value);
277+
expression.setValue(context, target, value);
278278
}
279279

280280
/**
@@ -306,7 +306,7 @@ public Class<?> getType(Object root) {
306306

307307
try {
308308

309-
return expression.getValueType(CONTEXT, root);
309+
return expression.getValueType(context, root);
310310

311311
} catch (SpelEvaluationException o_O) {
312312

@@ -436,11 +436,11 @@ public String toString() {
436436
}
437437

438438
private TypedSpelPath getParent() {
439-
return new TypedSpelPath(path.substring(0, path.lastIndexOf('/')), type);
439+
return new TypedSpelPath(path.substring(0, path.lastIndexOf('/')), type, context);
440440
}
441441

442442
private TypeDescriptor getTypeDescriptor(Object target) {
443-
return expression.getValueTypeDescriptor(CONTEXT, target);
443+
return expression.getValueTypeDescriptor(context, target);
444444
}
445445

446446
private Integer getTargetListIndex() {

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

+3-1
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));

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

+17-1
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,7 @@
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.support.DefaultFormattingConversionService;
3638

3739
import com.fasterxml.jackson.annotation.JsonIgnore;
3840
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -57,7 +59,8 @@ void setUp() {
5759
context.getPersistentEntity(Person.class);
5860

5961
PersistentEntities entities = new PersistentEntities(Arrays.asList(context));
60-
BindContextFactory factory = new PersistentEntitiesBindContextFactory(entities);
62+
BindContextFactory factory = new PersistentEntitiesBindContextFactory(entities,
63+
new DefaultFormattingConversionService());
6164

6265
this.context = factory.getBindContextFor(new ObjectMapper());
6366
}
@@ -167,6 +170,18 @@ void mapsRenamedProperty() {
167170
assertThat(path.getExpressionString()).isEqualTo("renamed");
168171
}
169172

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

172187
@Data
@@ -175,6 +190,7 @@ static class Person {
175190
@JsonIgnore String hiddenProperty;
176191
@Getter(onMethod = @__(@JsonIgnore)) String hiddenGetter;
177192
@JsonProperty("demaner") String renamed;
193+
LocalDate birthday;
178194
}
179195

180196
@Data

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

+12
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)