Skip to content

Commit 0e6f191

Browse files
committed
Refine projections.
1 parent 4c1a5fb commit 0e6f191

File tree

4 files changed

+138
-24
lines changed

4 files changed

+138
-24
lines changed

src/main/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactory.java

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,18 +131,27 @@ private static boolean hasJsonPathAnnotation(Class<?> type) {
131131
return false;
132132
}
133133

134-
private static class InputMessageProjecting implements MethodInterceptor {
135-
136-
private final DocumentContext context;
137-
138-
public InputMessageProjecting(DocumentContext context) {
139-
this.context = context;
140-
}
134+
private record InputMessageProjecting(DocumentContext context) implements MethodInterceptor {
141135

142136
@Override
143137
public @Nullable Object invoke(MethodInvocation invocation) throws Throwable {
144138

145139
Method method = invocation.getMethod();
140+
141+
switch (method.getName()) {
142+
case "equals" -> {
143+
// Only consider equal when proxies are identical.
144+
return (invocation.getThis() == invocation.getArguments()[0]);
145+
}
146+
case "hashCode" -> {
147+
// Use hashCode of EntityManager proxy.
148+
return context.hashCode();
149+
}
150+
case "toString" -> {
151+
return context.jsonString();
152+
}
153+
}
154+
146155
TypeInformation<?> returnType = TypeInformation.fromReturnTypeOf(method);
147156
ResolvableType type = ResolvableType.forMethodReturnType(method);
148157
boolean isCollectionResult = type.getRawClass() != null && Collection.class.isAssignableFrom(type.getRawClass());

src/main/java/org/springframework/data/web/ProjectingJacksonHttpMessageConverter.java

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2025 the original author or authors.
2+
* Copyright 2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -56,6 +56,7 @@
5656
* {@link HttpMessageConverter} implementation to enable projected JSON binding to interfaces annotated with
5757
* {@link ProjectedPayload}.
5858
*
59+
* @author Mark Paluch
5960
* @author Oliver Gierke
6061
* @author Christoph Strobl
6162
* @soundtrack Richard Spaven - Ice Is Nice (Spaven's 5ive)
@@ -115,28 +116,38 @@ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
115116
}
116117

117118
@Override
118-
public boolean canRead(ResolvableType type, @Nullable MediaType mediaType) {
119+
protected boolean supports(Class<?> clazz) {
119120

120-
if (!canRead(mediaType)) {
121-
return false;
122-
}
121+
if (clazz.isInterface()) {
123122

124-
Class<?> rawType = type.resolve();
123+
Boolean result = supportedTypesCache.get(clazz);
125124

126-
if (rawType == null) {
127-
return false;
128-
}
125+
if (result != null) {
126+
return result;
127+
}
129128

130-
Boolean result = supportedTypesCache.get(rawType);
129+
result = AnnotationUtils.findAnnotation(clazz, ProjectedPayload.class) != null;
130+
supportedTypesCache.put(clazz, result);
131131

132-
if (result != null) {
133132
return result;
134133
}
135134

136-
result = rawType.isInterface() && AnnotationUtils.findAnnotation(rawType, ProjectedPayload.class) != null;
137-
supportedTypesCache.put(rawType, result);
135+
return false;
136+
}
137+
138+
@Override
139+
public boolean canRead(ResolvableType type, @Nullable MediaType mediaType) {
140+
141+
if (!super.canRead(type, mediaType)) {
142+
return false;
143+
}
144+
145+
Class<?> clazz = type.resolve();
146+
if (clazz == null) {
147+
return false;
148+
}
138149

139-
return result;
150+
return supports(clazz);
140151
}
141152

142153
@Override

src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.web.config;
1717

18+
import java.util.ArrayList;
1819
import java.util.List;
1920

2021
import org.jspecify.annotations.Nullable;
@@ -39,6 +40,7 @@
3940
import org.springframework.data.web.XmlBeamHttpMessageConverter;
4041
import org.springframework.format.FormatterRegistry;
4142
import org.springframework.format.support.FormattingConversionService;
43+
import org.springframework.http.converter.HttpMessageConverter;
4244
import org.springframework.http.converter.HttpMessageConverters;
4345
import org.springframework.util.Assert;
4446
import org.springframework.util.ClassUtils;
@@ -154,6 +156,17 @@ public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentRes
154156
@Override
155157
public void configureMessageConverters(HttpMessageConverters.Builder builder) {
156158

159+
List<HttpMessageConverter<?>> converters = new ArrayList<>();
160+
configureMessageConverters(converters);
161+
162+
for (HttpMessageConverter<?> converter : converters) {
163+
builder.additionalMessageConverter(converter);
164+
}
165+
}
166+
167+
@Override
168+
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
169+
157170
if (ClassUtils.isPresent("com.jayway.jsonpath.DocumentContext", context.getClassLoader())) {
158171

159172
if (ClassUtils.isPresent("tools.jackson.databind.ObjectReader", context.getClassLoader())) {
@@ -165,7 +178,7 @@ public void configureMessageConverters(HttpMessageConverters.Builder builder) {
165178
converter.setBeanFactory(context);
166179
forwardBeanClassLoader(converter);
167180

168-
builder.additionalMessageConverter(converter);
181+
converters.add(0, converter);
169182
} else if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", context.getClassLoader())) {
170183

171184
ObjectMapper mapper = context.getBeanProvider(ObjectMapper.class).getIfUnique(ObjectMapper::new);
@@ -174,13 +187,13 @@ public void configureMessageConverters(HttpMessageConverters.Builder builder) {
174187
converter.setBeanFactory(context);
175188
forwardBeanClassLoader(converter);
176189

177-
builder.additionalMessageConverter(converter);
190+
converters.add(0, converter);
178191
}
179192
}
180193

181194
if (ClassUtils.isPresent("org.xmlbeam.XBProjector", context.getClassLoader())) {
182195

183-
builder.additionalMessageConverter(context.getBeanProvider(XmlBeamHttpMessageConverter.class) //
196+
converters.add(0, context.getBeanProvider(XmlBeamHttpMessageConverter.class) //
184197
.getIfAvailable(XmlBeamHttpMessageConverter::new));
185198
}
186199
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.web;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import org.junit.jupiter.api.Test;
21+
22+
import org.springframework.core.ResolvableType;
23+
import org.springframework.http.MediaType;
24+
25+
/**
26+
* Unit tests for {@link ProjectingJacksonHttpMessageConverter}.
27+
*
28+
* @author Oliver Gierke
29+
* @author Mark Paluch
30+
*/
31+
class ProjectingJacksonHttpMessageConverterUnitTests {
32+
33+
ProjectingJacksonHttpMessageConverter converter = new ProjectingJacksonHttpMessageConverter();
34+
MediaType ANYTHING_JSON = MediaType.parseMediaType("application/*+json");
35+
36+
@Test // DATCMNS-885
37+
void canReadJsonIntoAnnotatedInterface() {
38+
assertThat(converter.canRead(SampleInterface.class, ANYTHING_JSON)).isTrue();
39+
}
40+
41+
@Test // DATCMNS-885
42+
void cannotReadUnannotatedInterface() {
43+
assertThat(converter.canRead(UnannotatedInterface.class, ANYTHING_JSON)).isFalse();
44+
}
45+
46+
@Test // DATCMNS-885
47+
void cannotReadClass() {
48+
assertThat(converter.canRead(SampleClass.class, ANYTHING_JSON)).isFalse();
49+
}
50+
51+
@Test // DATACMNS-972
52+
void doesNotConsiderTypeVariableBoundTo() throws Throwable {
53+
54+
var method = BaseController.class.getDeclaredMethod("createEntity", AbstractDto.class);
55+
56+
assertThat(converter.canRead(ResolvableType.forMethodParameter(method, 0), ANYTHING_JSON)).isFalse();
57+
}
58+
59+
@Test // DATACMNS-972
60+
void genericTypeOnConcreteOne() throws Throwable {
61+
62+
var method = ConcreteController.class.getMethod("createEntity", AbstractDto.class);
63+
64+
assertThat(converter.canRead(ResolvableType.forMethodParameter(method, 0), ANYTHING_JSON)).isFalse();
65+
}
66+
67+
@ProjectedPayload
68+
interface SampleInterface {}
69+
70+
interface UnannotatedInterface {}
71+
72+
class SampleClass {}
73+
74+
class AbstractDto {}
75+
76+
abstract class BaseController<D extends AbstractDto> {
77+
public void createEntity(D dto) {}
78+
}
79+
80+
class ConcreteController extends BaseController<AbstractDto> {}
81+
}

0 commit comments

Comments
 (0)