Skip to content

Commit f1bcf06

Browse files
Consider Jackson customizations are considered when building a Querydsl Predicate instance.
Fixes GH-2572.
1 parent 1515a5b commit f1bcf06

5 files changed

Lines changed: 119 additions & 6 deletions

File tree

spring-data-rest-tests/spring-data-rest-tests-mongodb/src/main/java/org/springframework/data/rest/tests/mongodb/User.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.data.mongodb.core.mapping.DBRef;
2727
import org.springframework.data.mongodb.core.mapping.Document;
2828

29+
import com.fasterxml.jackson.annotation.JsonIgnore;
2930
import com.fasterxml.jackson.annotation.JsonValue;
3031

3132
/**
@@ -51,6 +52,7 @@ public static enum Gender {
5152
public @DBRef(lazy = true) User manager;
5253
public @DBRef(lazy = true) Map<String, User> map;
5354
public Map<String, Nested> colleaguesMap = new HashMap<String, Nested>();
55+
public @JsonIgnore String ignored;
5456

5557
public static class EmailAddress {
5658

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

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,22 @@
2020
import static org.mockito.Mockito.*;
2121

2222
import java.util.Collections;
23+
import java.util.List;
2324
import java.util.Map;
2425
import java.util.Optional;
26+
import java.util.function.Function;
2527

2628
import org.junit.jupiter.api.BeforeEach;
2729
import org.junit.jupiter.api.Test;
2830
import org.junit.jupiter.api.extension.ExtendWith;
31+
import org.mockito.ArgumentCaptor;
2932
import org.mockito.Mock;
3033
import org.mockito.Mockito;
3134
import org.mockito.junit.jupiter.MockitoExtension;
3235
import org.springframework.core.MethodParameter;
36+
import org.springframework.data.mapping.context.PersistentEntities;
37+
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
38+
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
3339
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
3440
import org.springframework.data.querydsl.QuerydslRepositoryInvokerAdapter;
3541
import org.springframework.data.querydsl.SimpleEntityPathResolver;
@@ -41,12 +47,16 @@
4147
import org.springframework.data.repository.support.Repositories;
4248
import org.springframework.data.repository.support.RepositoryInvoker;
4349
import org.springframework.data.repository.support.RepositoryInvokerFactory;
50+
import org.springframework.data.rest.tests.mongodb.Profile;
4451
import org.springframework.data.rest.tests.mongodb.QUser;
4552
import org.springframework.data.rest.tests.mongodb.Receipt;
4653
import org.springframework.data.rest.tests.mongodb.ReceiptRepository;
4754
import org.springframework.data.rest.tests.mongodb.User;
55+
import org.springframework.data.rest.webmvc.json.MappedProperties;
4856
import org.springframework.test.util.ReflectionTestUtils;
57+
import org.springframework.util.MultiValueMap;
4958

59+
import com.fasterxml.jackson.databind.ObjectMapper;
5060
import com.querydsl.core.types.Predicate;
5161

5262
/**
@@ -68,15 +78,28 @@ class QuerydslAwareRootResourceInformationHandlerMethodArgumentResolverUnitTests
6878
@Mock MethodParameter parameter;
6979

7080
QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver resolver;
81+
Function<Class<?>, MappedProperties> jacksonPropertiesLookup;
7182

7283
@BeforeEach
7384
void setUp() {
7485

7586
QuerydslBindingsFactory factory = new QuerydslBindingsFactory(SimpleEntityPathResolver.INSTANCE);
7687
ReflectionTestUtils.setField(factory, "repositories", Optional.of(repositories));
7788

89+
MongoCustomConversions conversions = new MongoCustomConversions(Collections.emptyList());
90+
MongoMappingContext mappingContext = new MongoMappingContext();
91+
mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder());
92+
mappingContext.getPersistentEntity(User.class);
93+
mappingContext.getPersistentEntity(Receipt.class);
94+
mappingContext.getPersistentEntity(Profile.class);
95+
PersistentEntities entities = new PersistentEntities(List.of(mappingContext));
96+
97+
ObjectMapper mapper = new ObjectMapper();
98+
this.jacksonPropertiesLookup = type -> entities.getPersistentEntity(type)
99+
.map(entity -> MappedProperties.forSerialization(entity, mapper)).orElse(null);
100+
78101
this.resolver = new QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver(repositories, invokerFactory,
79-
resourceMetadataResolver, builder, factory);
102+
resourceMetadataResolver, builder, factory, jacksonPropertiesLookup);
80103

81104
when(builder.getPredicate(any(), any(), any())).thenReturn(mock(Predicate.class));
82105
when(parameter.hasParameterAnnotation(QuerydslPredicate.class)).thenReturn(true);
@@ -116,8 +139,50 @@ void invokesCustomizationOnRepositoryIfItImplementsCustomizer() {
116139
verify(repository, times(1)).customize(Mockito.any(QuerydslBindings.class), Mockito.any(QUser.class));
117140
}
118141

142+
@Test // GH-2572
143+
void doesNotExposeJsonIgnoredPropertiesAsFilterKeys() {
144+
145+
Object repository = mock(QuerydslUserRepository.class);
146+
when(repositories.getRepositoryFor(User.class)).thenReturn(Optional.of(repository));
147+
148+
Map<String, String[]> parameters = Map.of("ignored", new String[] { "candidate-value" });
149+
150+
ArgumentCaptor<MultiValueMap<String, String>> captor = ArgumentCaptor.captor();
151+
when(builder.getPredicate(any(), captor.capture(), any())).thenReturn(mock(Predicate.class));
152+
153+
resolver.postProcess(parameter, invoker, User.class, parameters);
154+
155+
// Querydsl never receives a parameter that maps to a @JsonIgnore-annotated property: it could otherwise be used
156+
// as a server-side filter key (and as an existence oracle) for a value the framework refuses to serialize.
157+
assertThat(captor.getValue()).doesNotContainKey("ignored");
158+
}
159+
160+
@Test // GH-2572
161+
void translatesJacksonRenamedPropertyToPersistentPropertyName() {
162+
163+
Object repository = mock(QuerydslProfileRepository.class);
164+
when(repositories.getRepositoryFor(Profile.class)).thenReturn(Optional.of(repository));
165+
166+
// Profile.aliased is exposed as "renamed" via @JsonProperty("renamed"). A request that addresses the public alias
167+
// must reach Querydsl under the underlying domain property name; the bare Java field name must not be accepted.
168+
Map<String, String[]> parameters = Map.of(
169+
"renamed", new String[] { "value" },
170+
"aliased", new String[] { "ignored" });
171+
172+
ArgumentCaptor<MultiValueMap<String, String>> captor = ArgumentCaptor.captor();
173+
when(builder.getPredicate(any(), captor.capture(), any())).thenReturn(mock(Predicate.class));
174+
175+
resolver.postProcess(parameter, invoker, Profile.class, parameters);
176+
177+
MultiValueMap<String, String> forwarded = captor.getValue();
178+
assertThat(forwarded).doesNotContainKey("renamed");
179+
assertThat(forwarded.get("aliased")).containsExactly("value");
180+
}
181+
119182
interface QuerydslUserRepository extends QuerydslPredicateExecutor<User> {}
120183

184+
interface QuerydslProfileRepository extends QuerydslPredicateExecutor<Profile> {}
185+
121186
interface QuerydslCustomizingUserRepository
122187
extends QuerydslPredicateExecutor<User>, QuerydslBinderCustomizer<QUser> {}
123188
}

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

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616
package org.springframework.data.rest.webmvc.config;
1717

1818
import java.util.Arrays;
19+
import java.util.LinkedHashMap;
1920
import java.util.Map;
2021
import java.util.Map.Entry;
2122
import java.util.Optional;
23+
import java.util.function.Function;
2224

2325
import org.springframework.core.MethodParameter;
26+
import org.springframework.data.mapping.PersistentProperty;
2427
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
2528
import org.springframework.data.querydsl.QuerydslRepositoryInvokerAdapter;
2629
import org.springframework.data.querydsl.binding.QuerydslBindings;
@@ -31,8 +34,9 @@
3134
import org.springframework.data.repository.support.RepositoryInvoker;
3235
import org.springframework.data.repository.support.RepositoryInvokerFactory;
3336
import org.springframework.data.rest.webmvc.RootResourceInformation;
34-
import org.springframework.data.util.Pair;
37+
import org.springframework.data.rest.webmvc.json.MappedProperties;
3538
import org.springframework.data.util.ClassTypeInformation;
39+
import org.springframework.data.util.Pair;
3640
import org.springframework.util.LinkedMultiValueMap;
3741
import org.springframework.util.MultiValueMap;
3842
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
@@ -53,6 +57,7 @@ class QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver
5357
private final Repositories repositories;
5458
private final QuerydslPredicateBuilder predicateBuilder;
5559
private final QuerydslBindingsFactory factory;
60+
private final Function<Class<?>, MappedProperties> jacksonPropertiesLookup;
5661

5762
/**
5863
* Creates a new {@link QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver} using the given
@@ -61,16 +66,21 @@ class QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver
6166
* @param repositories must not be {@literal null}.
6267
* @param invokerFactory must not be {@literal null}.
6368
* @param resourceMetadataResolver must not be {@literal null}.
69+
* @param jacksonPropertiesLookup must not be {@literal null}. May return {@literal null} for domain types that are
70+
* not managed as persistent entities, in which case the request parameter map is forwarded to Querydsl
71+
* unfiltered.
6472
*/
6573
public QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver(Repositories repositories,
6674
RepositoryInvokerFactory invokerFactory, ResourceMetadataHandlerMethodArgumentResolver resourceMetadataResolver,
67-
QuerydslPredicateBuilder predicateBuilder, QuerydslBindingsFactory factory) {
75+
QuerydslPredicateBuilder predicateBuilder, QuerydslBindingsFactory factory,
76+
Function<Class<?>, MappedProperties> jacksonPropertiesLookup) {
6877

6978
super(repositories, invokerFactory, resourceMetadataResolver);
7079

7180
this.repositories = repositories;
7281
this.predicateBuilder = predicateBuilder;
7382
this.factory = factory;
83+
this.jacksonPropertiesLookup = jacksonPropertiesLookup;
7484
}
7585

7686
@Override
@@ -92,14 +102,45 @@ protected RepositoryInvoker postProcess(MethodParameter parameter, RepositoryInv
92102
private Optional<Pair<QuerydslPredicateExecutor<?>, Predicate>> getRepositoryAndPredicate(
93103
QuerydslPredicateExecutor<?> repository, Class<?> domainType, Map<String, String[]> parameters) {
94104

105+
Map<String, String[]> filteredParameters = filterByJacksonVisibility(domainType, parameters);
106+
95107
ClassTypeInformation<?> type = ClassTypeInformation.from(domainType);
96108

97109
QuerydslBindings bindings = factory.createBindingsFor(type);
98-
Predicate predicate = predicateBuilder.getPredicate(type, toMultiValueMap(parameters), bindings);
110+
Predicate predicate = predicateBuilder.getPredicate(type, toMultiValueMap(filteredParameters), bindings);
99111

100112
return Optional.ofNullable(predicate).map(it -> Pair.of(repository, it));
101113
}
102114

115+
/**
116+
* Reduces the request parameter map to entries that map to properties Jackson would expose in serialized responses
117+
* for {@code domainType}, translating Jackson field names to the underlying persistent property names that Querydsl
118+
* operates on. Without this gate, properties hidden from serialization (e.g. via {@code @JsonIgnore}) would still
119+
* be available as server-side filter keys via Querydsl's default permit-all bindings, and Jackson-renamed
120+
* properties (e.g. {@code @JsonProperty("renamed")}) would not be addressable under their public alias.
121+
*/
122+
private Map<String, String[]> filterByJacksonVisibility(Class<?> domainType, Map<String, String[]> parameters) {
123+
124+
MappedProperties properties = jacksonPropertiesLookup.apply(domainType);
125+
126+
if (properties == null) {
127+
return parameters;
128+
}
129+
130+
Map<String, String[]> filtered = new LinkedHashMap<>(parameters.size());
131+
132+
for (Entry<String, String[]> entry : parameters.entrySet()) {
133+
134+
PersistentProperty<?> property = properties.getPersistentProperty(entry.getKey());
135+
136+
if (property != null) {
137+
filtered.put(property.getName(), entry.getValue());
138+
}
139+
}
140+
141+
return filtered;
142+
}
143+
103144
@SuppressWarnings("unchecked")
104145
private static RepositoryInvoker getQuerydslAdapter(RepositoryInvoker invoker,
105146
QuerydslPredicateExecutor<?> repository, Predicate predicate) {

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,8 +427,13 @@ public RootResourceInformationHandlerMethodArgumentResolver repoRequestArgumentR
427427
QuerydslPredicateBuilder predicateBuilder = new QuerydslPredicateBuilder(defaultConversionService,
428428
factory.getEntityPathResolver());
429429

430+
ObjectMapper mapper = objectMapper();
431+
PersistentEntities entities = persistentEntities();
432+
430433
return new QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver(repositories,
431-
repositoryInvokerFactory, resourceMetadataHandlerMethodArgumentResolver, predicateBuilder, factory);
434+
repositoryInvokerFactory, resourceMetadataHandlerMethodArgumentResolver, predicateBuilder, factory,
435+
type -> entities.getPersistentEntity(type)
436+
.map(entity -> MappedProperties.forSerialization(entity, mapper)).orElse(null));
432437
}
433438

434439
return new RootResourceInformationHandlerMethodArgumentResolver(repositories, repositoryInvokerFactory,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
* @author Mark Paluch
4646
* @author Mathias Düsterhöft
4747
*/
48-
class MappedProperties {
48+
public class MappedProperties {
4949

5050
private final Map<PersistentProperty<?>, BeanPropertyDefinition> propertyToFieldName;
5151
private final Map<String, PersistentProperty<?>> fieldNameToProperty;

0 commit comments

Comments
 (0)