Skip to content

Commit 567c559

Browse files
committed
Resolvers for destination vars and headers
See gh-21987
1 parent dda40c1 commit 567c559

10 files changed

+1123
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
/*
2+
* Copyright 2002-2019 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+
17+
package org.springframework.messaging.handler.annotation.support.reactive;
18+
19+
import java.util.Map;
20+
import java.util.concurrent.ConcurrentHashMap;
21+
22+
import org.springframework.beans.factory.BeanFactory;
23+
import org.springframework.beans.factory.config.BeanExpressionContext;
24+
import org.springframework.beans.factory.config.BeanExpressionResolver;
25+
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
26+
import org.springframework.core.MethodParameter;
27+
import org.springframework.core.convert.ConversionService;
28+
import org.springframework.core.convert.TypeDescriptor;
29+
import org.springframework.lang.Nullable;
30+
import org.springframework.messaging.Message;
31+
import org.springframework.messaging.handler.annotation.ValueConstants;
32+
import org.springframework.messaging.handler.invocation.reactive.SyncHandlerMethodArgumentResolver;
33+
import org.springframework.util.ClassUtils;
34+
35+
/**
36+
* Abstract base class to resolve method arguments from a named value, e.g.
37+
* message headers or destination variables. Named values could have one or more
38+
* of a name, a required flag, and a default value.
39+
*
40+
* <p>Subclasses only need to define specific steps such as how to obtain named
41+
* value details from a method parameter, how to resolve to argument values, or
42+
* how to handle missing values.
43+
*
44+
* <p>A default value string can contain ${...} placeholders and Spring
45+
* Expression Language {@code #{...}} expressions which will be resolved if a
46+
* {@link ConfigurableBeanFactory} is supplied to the class constructor.
47+
*
48+
* <p>A {@link ConversionService} is used to to convert resolved String argument
49+
* value to the expected target method parameter type.
50+
*
51+
* @author Rossen Stoyanchev
52+
* @since 5.2
53+
*/
54+
public abstract class AbstractNamedValueMethodArgumentResolver implements SyncHandlerMethodArgumentResolver {
55+
56+
private final ConversionService conversionService;
57+
58+
@Nullable
59+
private final ConfigurableBeanFactory configurableBeanFactory;
60+
61+
@Nullable
62+
private final BeanExpressionContext expressionContext;
63+
64+
private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<>(256);
65+
66+
67+
/**
68+
* Constructor with a {@link ConversionService} and a {@link BeanFactory}.
69+
* @param conversionService conversion service for converting String values
70+
* to the target method parameter type
71+
* @param beanFactory a bean factory for resolving {@code ${...}}
72+
* placeholders and {@code #{...}} SpEL expressions in default values
73+
*/
74+
protected AbstractNamedValueMethodArgumentResolver(ConversionService conversionService,
75+
@Nullable ConfigurableBeanFactory beanFactory) {
76+
77+
this.conversionService = conversionService;
78+
this.configurableBeanFactory = beanFactory;
79+
this.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, null) : null);
80+
}
81+
82+
83+
@Override
84+
public Object resolveArgumentValue(MethodParameter parameter, Message<?> message) {
85+
86+
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
87+
MethodParameter nestedParameter = parameter.nestedIfOptional();
88+
89+
Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
90+
if (resolvedName == null) {
91+
throw new IllegalArgumentException(
92+
"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
93+
}
94+
95+
Object arg = resolveArgumentInternal(nestedParameter, message, resolvedName.toString());
96+
if (arg == null) {
97+
if (namedValueInfo.defaultValue != null) {
98+
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
99+
}
100+
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
101+
handleMissingValue(namedValueInfo.name, nestedParameter, message);
102+
}
103+
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
104+
}
105+
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
106+
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
107+
}
108+
109+
if (parameter != nestedParameter || !ClassUtils.isAssignableValue(parameter.getParameterType(), arg)) {
110+
arg = this.conversionService.convert(arg, TypeDescriptor.forObject(arg), new TypeDescriptor(parameter));
111+
}
112+
113+
return arg;
114+
}
115+
116+
/**
117+
* Obtain the named value for the given method parameter.
118+
*/
119+
private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
120+
NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
121+
if (namedValueInfo == null) {
122+
namedValueInfo = createNamedValueInfo(parameter);
123+
namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
124+
this.namedValueInfoCache.put(parameter, namedValueInfo);
125+
}
126+
return namedValueInfo;
127+
}
128+
129+
/**
130+
* Create the {@link NamedValueInfo} object for the given method parameter.
131+
* Implementations typically retrieve the method annotation by means of
132+
* {@link MethodParameter#getParameterAnnotation(Class)}.
133+
* @param parameter the method parameter
134+
* @return the named value information
135+
*/
136+
protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter);
137+
138+
/**
139+
* Fall back on the parameter name from the class file if necessary and
140+
* replace {@link ValueConstants#DEFAULT_NONE} with null.
141+
*/
142+
private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) {
143+
String name = info.name;
144+
if (info.name.isEmpty()) {
145+
name = parameter.getParameterName();
146+
if (name == null) {
147+
Class<?> type = parameter.getParameterType();
148+
throw new IllegalArgumentException(
149+
"Name for argument of type [" + type.getName() + "] not specified, " +
150+
"and parameter name information not found in class file either.");
151+
}
152+
}
153+
return new NamedValueInfo(name, info.required,
154+
ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue);
155+
}
156+
157+
/**
158+
* Resolve the given annotation-specified value,
159+
* potentially containing placeholders and expressions.
160+
*/
161+
@Nullable
162+
private Object resolveEmbeddedValuesAndExpressions(String value) {
163+
if (this.configurableBeanFactory == null || this.expressionContext == null) {
164+
return value;
165+
}
166+
String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value);
167+
BeanExpressionResolver exprResolver = this.configurableBeanFactory.getBeanExpressionResolver();
168+
if (exprResolver == null) {
169+
return value;
170+
}
171+
return exprResolver.evaluate(placeholdersResolved, this.expressionContext);
172+
}
173+
174+
/**
175+
* Resolves the given parameter type and value name into an argument value.
176+
* @param parameter the method parameter to resolve to an argument value
177+
* @param message the current request
178+
* @param name the name of the value being resolved
179+
* @return the resolved argument. May be {@code null}
180+
*/
181+
@Nullable
182+
protected abstract Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name);
183+
184+
/**
185+
* Invoked when a value is required, but {@link #resolveArgumentInternal}
186+
* returned {@code null} and there is no default value. Sub-classes can
187+
* throw an appropriate exception for this case.
188+
* @param name the name for the value
189+
* @param parameter the target method parameter
190+
* @param message the message being processed
191+
*/
192+
protected abstract void handleMissingValue(String name, MethodParameter parameter, Message<?> message);
193+
194+
/**
195+
* One last chance to handle a possible null value.
196+
* Specifically for booleans method parameters, use {@link Boolean#FALSE}.
197+
* Also raise an ISE for primitive types.
198+
*/
199+
@Nullable
200+
private Object handleNullValue(String name, @Nullable Object value, Class<?> paramType) {
201+
if (value == null) {
202+
if (Boolean.TYPE.equals(paramType)) {
203+
return Boolean.FALSE;
204+
}
205+
else if (paramType.isPrimitive()) {
206+
throw new IllegalStateException("Optional " + paramType + " parameter '" + name +
207+
"' is present but cannot be translated into a null value due to being " +
208+
"declared as a primitive type. Consider declaring it as object wrapper " +
209+
"for the corresponding primitive type.");
210+
}
211+
}
212+
return value;
213+
}
214+
215+
216+
/**
217+
* Represents a named value declaration.
218+
*/
219+
protected static class NamedValueInfo {
220+
221+
private final String name;
222+
223+
private final boolean required;
224+
225+
@Nullable
226+
private final String defaultValue;
227+
228+
protected NamedValueInfo(String name, boolean required, @Nullable String defaultValue) {
229+
this.name = name;
230+
this.required = required;
231+
this.defaultValue = defaultValue;
232+
}
233+
}
234+
235+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2002-2019 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+
17+
package org.springframework.messaging.handler.annotation.support.reactive;
18+
19+
import java.util.Map;
20+
21+
import org.springframework.core.MethodParameter;
22+
import org.springframework.core.convert.ConversionService;
23+
import org.springframework.lang.Nullable;
24+
import org.springframework.messaging.Message;
25+
import org.springframework.messaging.MessageHandlingException;
26+
import org.springframework.messaging.MessageHeaders;
27+
import org.springframework.messaging.handler.annotation.DestinationVariable;
28+
import org.springframework.messaging.handler.annotation.ValueConstants;
29+
import org.springframework.util.Assert;
30+
31+
/**
32+
* Resolve for {@link DestinationVariable @DestinationVariable} method parameters.
33+
*
34+
* @author Rossen Stoyanchev
35+
* @since 5.2
36+
*/
37+
public class DestinationVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
38+
39+
/** The name of the header used to for template variables. */
40+
public static final String DESTINATION_TEMPLATE_VARIABLES_HEADER =
41+
DestinationVariableMethodArgumentResolver.class.getSimpleName() + ".templateVariables";
42+
43+
44+
public DestinationVariableMethodArgumentResolver(ConversionService conversionService) {
45+
super(conversionService, null);
46+
}
47+
48+
49+
@Override
50+
public boolean supportsParameter(MethodParameter parameter) {
51+
return parameter.hasParameterAnnotation(DestinationVariable.class);
52+
}
53+
54+
@Override
55+
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
56+
DestinationVariable annot = parameter.getParameterAnnotation(DestinationVariable.class);
57+
Assert.state(annot != null, "No DestinationVariable annotation");
58+
return new DestinationVariableNamedValueInfo(annot);
59+
}
60+
61+
@Override
62+
@Nullable
63+
@SuppressWarnings("unchecked")
64+
protected Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name) {
65+
MessageHeaders headers = message.getHeaders();
66+
Map<String, String> vars = (Map<String, String>) headers.get(DESTINATION_TEMPLATE_VARIABLES_HEADER);
67+
return vars != null ? vars.get(name) : null;
68+
}
69+
70+
@Override
71+
protected void handleMissingValue(String name, MethodParameter parameter, Message<?> message) {
72+
throw new MessageHandlingException(message, "Missing path template variable '" + name + "' " +
73+
"for method parameter type [" + parameter.getParameterType() + "]");
74+
}
75+
76+
77+
private static final class DestinationVariableNamedValueInfo extends NamedValueInfo {
78+
79+
private DestinationVariableNamedValueInfo(DestinationVariable annotation) {
80+
super(annotation.value(), true, ValueConstants.DEFAULT_NONE);
81+
}
82+
}
83+
84+
}

0 commit comments

Comments
 (0)