Skip to content

Commit 1736b11

Browse files
Consider Jackson customizations are considered when building
a Querydsl Predicate instance. Closes GH-2572.
1 parent 9b60b99 commit 1736b11

5 files changed

Lines changed: 121 additions & 5 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: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,25 @@
1919
import static org.mockito.ArgumentMatchers.*;
2020
import static org.mockito.Mockito.*;
2121

22+
import tools.jackson.databind.json.JsonMapper;
23+
2224
import java.util.Collections;
25+
import java.util.List;
2326
import java.util.Map;
2427
import java.util.Optional;
28+
import java.util.function.Function;
2529

2630
import org.junit.jupiter.api.BeforeEach;
2731
import org.junit.jupiter.api.Test;
2832
import org.junit.jupiter.api.extension.ExtendWith;
33+
import org.mockito.ArgumentCaptor;
2934
import org.mockito.Mock;
3035
import org.mockito.Mockito;
3136
import org.mockito.junit.jupiter.MockitoExtension;
3237
import org.springframework.core.MethodParameter;
38+
import org.springframework.data.mapping.context.PersistentEntities;
39+
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
40+
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
3341
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
3442
import org.springframework.data.querydsl.QuerydslRepositoryInvokerAdapter;
3543
import org.springframework.data.querydsl.SimpleEntityPathResolver;
@@ -41,11 +49,14 @@
4149
import org.springframework.data.repository.support.Repositories;
4250
import org.springframework.data.repository.support.RepositoryInvoker;
4351
import org.springframework.data.repository.support.RepositoryInvokerFactory;
52+
import org.springframework.data.rest.tests.mongodb.Profile;
4453
import org.springframework.data.rest.tests.mongodb.QUser;
4554
import org.springframework.data.rest.tests.mongodb.Receipt;
4655
import org.springframework.data.rest.tests.mongodb.ReceiptRepository;
4756
import org.springframework.data.rest.tests.mongodb.User;
57+
import org.springframework.data.rest.webmvc.json.MappedJacksonProperties;
4858
import org.springframework.test.util.ReflectionTestUtils;
59+
import org.springframework.util.MultiValueMap;
4960

5061
import com.querydsl.core.types.Predicate;
5162

@@ -68,15 +79,28 @@ class QuerydslAwareRootResourceInformationHandlerMethodArgumentResolverUnitTests
6879
@Mock MethodParameter parameter;
6980

7081
QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver resolver;
82+
Function<Class<?>, MappedJacksonProperties> jacksonPropertiesLookup;
7183

7284
@BeforeEach
7385
void setUp() {
7486

7587
QuerydslBindingsFactory factory = new QuerydslBindingsFactory(SimpleEntityPathResolver.INSTANCE);
7688
ReflectionTestUtils.setField(factory, "repositories", Optional.of(repositories));
7789

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

81105
when(builder.getPredicate(any(), any(), any())).thenReturn(mock(Predicate.class));
82106
when(parameter.hasParameterAnnotation(QuerydslPredicate.class)).thenReturn(true);
@@ -116,8 +140,50 @@ void invokesCustomizationOnRepositoryIfItImplementsCustomizer() {
116140
verify(repository, times(1)).customize(Mockito.any(QuerydslBindings.class), Mockito.any(QUser.class));
117141
}
118142

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

185+
interface QuerydslProfileRepository extends QuerydslPredicateExecutor<Profile> {}
186+
121187
interface QuerydslCustomizingUserRepository
122188
extends QuerydslPredicateExecutor<User>, QuerydslBinderCustomizer<QUser> {}
123189
}

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

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
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;
24+
25+
import org.jspecify.annotations.Nullable;
2226

2327
import org.springframework.core.MethodParameter;
2428
import org.springframework.data.core.TypeInformation;
29+
import org.springframework.data.mapping.PersistentProperty;
2530
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
2631
import org.springframework.data.querydsl.QuerydslRepositoryInvokerAdapter;
2732
import org.springframework.data.querydsl.binding.QuerydslBindings;
@@ -32,6 +37,7 @@
3237
import org.springframework.data.repository.support.RepositoryInvoker;
3338
import org.springframework.data.repository.support.RepositoryInvokerFactory;
3439
import org.springframework.data.rest.webmvc.RootResourceInformation;
40+
import org.springframework.data.rest.webmvc.json.MappedJacksonProperties;
3541
import org.springframework.data.util.Pair;
3642
import org.springframework.util.LinkedMultiValueMap;
3743
import org.springframework.util.MultiValueMap;
@@ -53,6 +59,7 @@ class QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver
5359
private final Repositories repositories;
5460
private final QuerydslPredicateBuilder predicateBuilder;
5561
private final QuerydslBindingsFactory factory;
62+
private final Function<Class<?>, @Nullable MappedJacksonProperties> jacksonPropertiesLookup;
5663

5764
/**
5865
* Creates a new {@link QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver} using the given
@@ -61,16 +68,21 @@ class QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver
6168
* @param repositories must not be {@literal null}.
6269
* @param invokerFactory must not be {@literal null}.
6370
* @param resourceMetadataResolver must not be {@literal null}.
71+
* @param jacksonPropertiesLookup must not be {@literal null}. May return {@literal null} for domain types that are
72+
* not managed as persistent entities, in which case the request parameter map is forwarded to Querydsl
73+
* unfiltered.
6474
*/
6575
public QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver(Repositories repositories,
6676
RepositoryInvokerFactory invokerFactory, ResourceMetadataHandlerMethodArgumentResolver resourceMetadataResolver,
67-
QuerydslPredicateBuilder predicateBuilder, QuerydslBindingsFactory factory) {
77+
QuerydslPredicateBuilder predicateBuilder, QuerydslBindingsFactory factory,
78+
Function<Class<?>, @Nullable MappedJacksonProperties> jacksonPropertiesLookup) {
6879

6980
super(repositories, invokerFactory, resourceMetadataResolver);
7081

7182
this.repositories = repositories;
7283
this.predicateBuilder = predicateBuilder;
7384
this.factory = factory;
85+
this.jacksonPropertiesLookup = jacksonPropertiesLookup;
7486
}
7587

7688
@Override
@@ -92,14 +104,45 @@ protected RepositoryInvoker postProcess(MethodParameter parameter, RepositoryInv
92104
private Optional<Pair<QuerydslPredicateExecutor<?>, Predicate>> getRepositoryAndPredicate(
93105
QuerydslPredicateExecutor<?> repository, Class<?> domainType, Map<String, String[]> parameters) {
94106

107+
Map<String, String[]> filteredParameters = filterByJacksonVisibility(domainType, parameters);
108+
95109
TypeInformation<?> type = TypeInformation.of(domainType);
96110

97111
QuerydslBindings bindings = factory.createBindingsFor(type);
98-
Predicate predicate = predicateBuilder.getPredicate(type, toMultiValueMap(parameters), bindings);
112+
Predicate predicate = predicateBuilder.getPredicate(type, toMultiValueMap(filteredParameters), bindings);
99113

100114
return Optional.ofNullable(predicate).map(it -> Pair.of(repository, it));
101115
}
102116

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

432+
JsonMapper mapper = objectMapper();
433+
PersistentEntities entities = persistentEntities();
434+
432435
return new QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver(repositories,
433-
repositoryInvokerFactory, resourceMetadataHandlerMethodArgumentResolver, predicateBuilder, factory);
436+
repositoryInvokerFactory, resourceMetadataHandlerMethodArgumentResolver, predicateBuilder, factory,
437+
type -> entities.getPersistentEntity(type)
438+
.map(entity -> MappedJacksonProperties.forSerialization(entity, mapper)).orElse(null));
434439
}
435440

436441
return new RootResourceInformationHandlerMethodArgumentResolver(repositories, repositoryInvokerFactory,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
* @author Mathias Düsterhöft
5151
* @since 5.0
5252
*/
53-
class MappedJacksonProperties {
53+
public class MappedJacksonProperties {
5454

5555
private final Map<PersistentProperty<?>, BeanPropertyDefinition> propertyToFieldName;
5656
private final Map<String, PersistentProperty<?>> fieldNameToProperty;

0 commit comments

Comments
 (0)