Skip to content

Commit 57a42ab

Browse files
committed
DATACMNS-630 - Added HandlerMethodArgumentResolver to create proxies for interfaces.
We now ship a ProxyingHandlerMethodArgumentResolver that gets registered when @EnableSpringDataWebSupport is activated. It creates Map-based proxy instances for interfaces used as Spring MVC controller method parameters.
1 parent 127747f commit 57a42ab

File tree

7 files changed

+650
-5
lines changed

7 files changed

+650
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
/*
2+
* Copyright 2015 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+
* http://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 java.beans.PropertyDescriptor;
19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
import org.springframework.beans.AbstractPropertyAccessor;
24+
import org.springframework.beans.BeanUtils;
25+
import org.springframework.beans.BeansException;
26+
import org.springframework.beans.ConfigurablePropertyAccessor;
27+
import org.springframework.beans.NotWritablePropertyException;
28+
import org.springframework.beans.PropertyAccessor;
29+
import org.springframework.context.expression.MapAccessor;
30+
import org.springframework.core.CollectionFactory;
31+
import org.springframework.core.MethodParameter;
32+
import org.springframework.core.convert.ConversionService;
33+
import org.springframework.core.convert.TypeDescriptor;
34+
import org.springframework.core.convert.support.DefaultConversionService;
35+
import org.springframework.data.mapping.PropertyPath;
36+
import org.springframework.data.mapping.PropertyReferenceException;
37+
import org.springframework.data.util.TypeInformation;
38+
import org.springframework.expression.AccessException;
39+
import org.springframework.expression.EvaluationContext;
40+
import org.springframework.expression.Expression;
41+
import org.springframework.expression.TypedValue;
42+
import org.springframework.expression.spel.SpelParserConfiguration;
43+
import org.springframework.expression.spel.standard.SpelExpressionParser;
44+
import org.springframework.expression.spel.support.StandardEvaluationContext;
45+
import org.springframework.expression.spel.support.StandardTypeConverter;
46+
import org.springframework.util.Assert;
47+
import org.springframework.web.bind.WebDataBinder;
48+
49+
/**
50+
* A {@link WebDataBinder} that automatically binds all properties exposed in the given type using a {@link Map}.
51+
*
52+
* @author Oliver Gierke
53+
* @since 1.10
54+
*/
55+
class MapDataBinder extends WebDataBinder {
56+
57+
private final Class<?> type;
58+
private final ConversionService conversionService;
59+
60+
/**
61+
* Creates a new {@link MapDataBinder} for the given type and {@link ConversionService}.
62+
*
63+
* @param type target type to detect property that need to be bound.
64+
* @param conversionService the {@link ConversionService} to be used to preprocess values.
65+
*/
66+
public MapDataBinder(Class<?> type, ConversionService conversionService) {
67+
68+
super(new HashMap<String, Object>());
69+
70+
this.type = type;
71+
this.conversionService = conversionService;
72+
}
73+
74+
/*
75+
* (non-Javadoc)
76+
* @see org.springframework.validation.DataBinder#getTarget()
77+
*/
78+
@Override
79+
@SuppressWarnings("unchecked")
80+
public Map<String, Object> getTarget() {
81+
return (Map<String, Object>) super.getTarget();
82+
}
83+
84+
/*
85+
* (non-Javadoc)
86+
* @see org.springframework.validation.DataBinder#getPropertyAccessor()
87+
*/
88+
@Override
89+
protected ConfigurablePropertyAccessor getPropertyAccessor() {
90+
return new MapPropertyAccessor(type, getTarget(), conversionService);
91+
}
92+
93+
/**
94+
* {@link PropertyAccessor} to store and retrieve values in a {@link Map}. Uses Spring Expression language to create
95+
* deeply nested Map structures.
96+
*
97+
* @author Oliver Gierke
98+
* @since 1.10
99+
*/
100+
private static class MapPropertyAccessor extends AbstractPropertyAccessor {
101+
102+
private static final SpelExpressionParser PARSER = new SpelExpressionParser(
103+
new SpelParserConfiguration(false, true));
104+
105+
private final Class<?> type;
106+
private final Map<String, Object> map;
107+
private final ConversionService conversionService;
108+
109+
/**
110+
* Creates a new {@link MapPropertyAccessor} for the given type, map and {@link ConversionService}.
111+
*
112+
* @param type must not be {@literal null}.
113+
* @param map must not be {@literal null}.
114+
* @param conversionService must not be {@literal null}.
115+
*/
116+
public MapPropertyAccessor(Class<?> type, Map<String, Object> map, ConversionService conversionService) {
117+
118+
Assert.notNull(type, "Type must not be null!");
119+
Assert.notNull(map, "Map must not be null!");
120+
Assert.notNull(conversionService, "ConversionService must not be null!");
121+
122+
this.type = type;
123+
this.map = map;
124+
this.conversionService = conversionService;
125+
}
126+
127+
/*
128+
* (non-Javadoc)
129+
* @see org.springframework.beans.PropertyAccessor#isReadableProperty(java.lang.String)
130+
*/
131+
@Override
132+
public boolean isReadableProperty(String propertyName) {
133+
throw new UnsupportedOperationException();
134+
}
135+
136+
/*
137+
* (non-Javadoc)
138+
* @see org.springframework.beans.PropertyAccessor#isWritableProperty(java.lang.String)
139+
*/
140+
@Override
141+
public boolean isWritableProperty(String propertyName) {
142+
143+
try {
144+
return getPropertyPath(propertyName) != null;
145+
} catch (PropertyReferenceException o_O) {
146+
return false;
147+
}
148+
}
149+
150+
/*
151+
* (non-Javadoc)
152+
* @see org.springframework.beans.PropertyAccessor#getPropertyTypeDescriptor(java.lang.String)
153+
*/
154+
@Override
155+
public TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException {
156+
throw new UnsupportedOperationException();
157+
}
158+
159+
/*
160+
* (non-Javadoc)
161+
* @see org.springframework.beans.AbstractPropertyAccessor#getPropertyValue(java.lang.String)
162+
*/
163+
@Override
164+
public Object getPropertyValue(String propertyName) throws BeansException {
165+
throw new UnsupportedOperationException();
166+
}
167+
168+
/*
169+
* (non-Javadoc)
170+
* @see org.springframework.beans.AbstractPropertyAccessor#setPropertyValue(java.lang.String, java.lang.Object)
171+
*/
172+
@Override
173+
public void setPropertyValue(String propertyName, Object value) throws BeansException {
174+
175+
if (!isWritableProperty(propertyName)) {
176+
throw new NotWritablePropertyException(type, propertyName);
177+
}
178+
179+
StandardEvaluationContext context = new StandardEvaluationContext();
180+
context.addPropertyAccessor(new PropertyTraversingMapAccessor(type, new DefaultConversionService()));
181+
context.setTypeConverter(new StandardTypeConverter(conversionService));
182+
context.setRootObject(map);
183+
184+
Expression expression = PARSER.parseExpression(propertyName);
185+
186+
PropertyPath leafProperty = getPropertyPath(propertyName).getLeafProperty();
187+
TypeInformation<?> owningType = leafProperty.getOwningType();
188+
TypeInformation<?> propertyType = owningType.getProperty(leafProperty.getSegment());
189+
190+
propertyType = propertyName.endsWith("]") ? propertyType.getActualType() : propertyType;
191+
192+
if (conversionRequired(value, propertyType.getType())) {
193+
194+
PropertyDescriptor descriptor = BeanUtils
195+
.getPropertyDescriptor(owningType.getType(), leafProperty.getSegment());
196+
MethodParameter methodParameter = new MethodParameter(descriptor.getReadMethod(), -1);
197+
TypeDescriptor typeDescriptor = TypeDescriptor.nested(methodParameter, 0);
198+
199+
value = conversionService.convert(value, TypeDescriptor.forObject(value), typeDescriptor);
200+
}
201+
202+
expression.setValue(context, value);
203+
}
204+
205+
private boolean conversionRequired(Object source, Class<?> targetType) {
206+
207+
if (targetType.isInstance(source)) {
208+
return false;
209+
}
210+
211+
return conversionService.canConvert(source.getClass(), targetType);
212+
}
213+
214+
private PropertyPath getPropertyPath(String propertyName) {
215+
216+
String plainPropertyPath = propertyName.replaceAll("\\[.*?\\]", "");
217+
return PropertyPath.from(plainPropertyPath, type);
218+
}
219+
220+
/**
221+
* A special {@link MapAccessor} that traverses properties on the configured type to automatically create nested Map
222+
* and collection values as necessary.
223+
*
224+
* @author Oliver Gierke
225+
* @since 1.10
226+
*/
227+
private static final class PropertyTraversingMapAccessor extends MapAccessor {
228+
229+
private final ConversionService conversionService;
230+
private Class<?> type;
231+
232+
/**
233+
* Creates a new {@link PropertyTraversingMapAccessor} for the given type and {@link ConversionService}.
234+
*
235+
* @param type must not be {@literal null}.
236+
* @param conversionService must not be {@literal null}.
237+
*/
238+
public PropertyTraversingMapAccessor(Class<?> type, ConversionService conversionService) {
239+
240+
Assert.notNull(type, "Type must not be null!");
241+
Assert.notNull(conversionService, "ConversionService must not be null!");
242+
243+
this.type = type;
244+
this.conversionService = conversionService;
245+
}
246+
247+
/*
248+
* (non-Javadoc)
249+
* @see org.springframework.context.expression.MapAccessor#canRead(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String)
250+
*/
251+
@Override
252+
public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
253+
return true;
254+
}
255+
256+
/*
257+
* (non-Javadoc)
258+
* @see org.springframework.context.expression.MapAccessor#read(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String)
259+
*/
260+
@Override
261+
@SuppressWarnings("unchecked")
262+
public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException {
263+
264+
PropertyPath path = PropertyPath.from(name, type);
265+
266+
try {
267+
return super.read(context, target, name);
268+
} catch (AccessException o_O) {
269+
270+
Object emptyResult = path.isCollection() ? CollectionFactory.createCollection(List.class, 0)
271+
: CollectionFactory.createMap(Map.class, 0);
272+
273+
((Map<String, Object>) target).put(name, emptyResult);
274+
275+
return new TypedValue(emptyResult, getDescriptor(path, emptyResult));
276+
} finally {
277+
this.type = path.getType();
278+
}
279+
}
280+
281+
/**
282+
* Returns the type descriptor for the given {@link PropertyPath} and empty value for that path.
283+
*
284+
* @param path must not be {@literal null}.
285+
* @param emptyValue must not be {@literal null}.
286+
* @return
287+
*/
288+
private TypeDescriptor getDescriptor(PropertyPath path, Object emptyValue) {
289+
290+
Class<?> actualPropertyType = path.getType();
291+
292+
TypeDescriptor valueDescriptor = conversionService.canConvert(String.class, actualPropertyType) ? TypeDescriptor
293+
.valueOf(String.class) : TypeDescriptor.valueOf(HashMap.class);
294+
295+
return path.isCollection() ? TypeDescriptor.collection(emptyValue.getClass(), valueDescriptor) : TypeDescriptor
296+
.map(emptyValue.getClass(), TypeDescriptor.valueOf(String.class), valueDescriptor);
297+
298+
}
299+
}
300+
}
301+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2015 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+
* http://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 org.springframework.beans.BeansException;
19+
import org.springframework.beans.MutablePropertyValues;
20+
import org.springframework.beans.factory.BeanFactory;
21+
import org.springframework.beans.factory.BeanFactoryAware;
22+
import org.springframework.core.MethodParameter;
23+
import org.springframework.core.convert.ConversionService;
24+
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
25+
import org.springframework.web.bind.WebDataBinder;
26+
import org.springframework.web.bind.support.WebDataBinderFactory;
27+
import org.springframework.web.context.request.NativeWebRequest;
28+
import org.springframework.web.method.annotation.ModelAttributeMethodProcessor;
29+
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
30+
31+
/**
32+
* {@link HandlerMethodArgumentResolver} to create Proxy instances for interface based controller method parameters.
33+
*
34+
* @author Oliver Gierke
35+
* @since 1.10
36+
*/
37+
public class ProxyingHandlerMethodArgumentResolver extends ModelAttributeMethodProcessor implements BeanFactoryAware {
38+
39+
private final SpelAwareProxyProjectionFactory proxyFactory;
40+
private final ConversionService conversionService;
41+
42+
/**
43+
* Creates a new {@link PageableHandlerMethodArgumentResolver} using the given {@link ConversionService}.
44+
*
45+
* @param conversionService must not be {@literal null}.
46+
*/
47+
public ProxyingHandlerMethodArgumentResolver(ConversionService conversionService) {
48+
49+
super(true);
50+
51+
this.proxyFactory = new SpelAwareProxyProjectionFactory();
52+
this.conversionService = conversionService;
53+
}
54+
55+
/*
56+
* (non-Javadoc)
57+
* @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
58+
*/
59+
@Override
60+
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
61+
this.proxyFactory.setBeanFactory(beanFactory);
62+
}
63+
64+
/*
65+
* (non-Javadoc)
66+
* @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter)
67+
*/
68+
@Override
69+
public boolean supportsParameter(MethodParameter parameter) {
70+
return parameter.getParameterType().isInterface();
71+
}
72+
73+
/*
74+
* (non-Javadoc)
75+
* @see org.springframework.web.method.annotation.ModelAttributeMethodProcessor#createAttribute(java.lang.String, org.springframework.core.MethodParameter, org.springframework.web.bind.support.WebDataBinderFactory, org.springframework.web.context.request.NativeWebRequest)
76+
*/
77+
@Override
78+
protected Object createAttribute(String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory,
79+
NativeWebRequest request) throws Exception {
80+
81+
MapDataBinder binder = new MapDataBinder(parameter.getParameterType(), conversionService);
82+
binder.bind(new MutablePropertyValues(request.getParameterMap()));
83+
84+
return proxyFactory.createProjection(parameter.getParameterType(), binder.getTarget());
85+
}
86+
87+
/*
88+
* (non-Javadoc)
89+
* @see org.springframework.web.method.annotation.ModelAttributeMethodProcessor#bindRequestParameters(org.springframework.web.bind.WebDataBinder, org.springframework.web.context.request.NativeWebRequest)
90+
*/
91+
@Override
92+
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {}
93+
}

0 commit comments

Comments
 (0)