-
Notifications
You must be signed in to change notification settings - Fork 38k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Resolvers for destination vars and headers
See gh-21987
- Loading branch information
1 parent
dda40c1
commit 567c559
Showing
10 changed files
with
1,123 additions
and
0 deletions.
There are no files selected for viewing
235 changes: 235 additions & 0 deletions
235
...ssaging/handler/annotation/support/reactive/AbstractNamedValueMethodArgumentResolver.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,235 @@ | ||
/* | ||
* Copyright 2002-2019 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.springframework.messaging.handler.annotation.support.reactive; | ||
|
||
import java.util.Map; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
|
||
import org.springframework.beans.factory.BeanFactory; | ||
import org.springframework.beans.factory.config.BeanExpressionContext; | ||
import org.springframework.beans.factory.config.BeanExpressionResolver; | ||
import org.springframework.beans.factory.config.ConfigurableBeanFactory; | ||
import org.springframework.core.MethodParameter; | ||
import org.springframework.core.convert.ConversionService; | ||
import org.springframework.core.convert.TypeDescriptor; | ||
import org.springframework.lang.Nullable; | ||
import org.springframework.messaging.Message; | ||
import org.springframework.messaging.handler.annotation.ValueConstants; | ||
import org.springframework.messaging.handler.invocation.reactive.SyncHandlerMethodArgumentResolver; | ||
import org.springframework.util.ClassUtils; | ||
|
||
/** | ||
* Abstract base class to resolve method arguments from a named value, e.g. | ||
* message headers or destination variables. Named values could have one or more | ||
* of a name, a required flag, and a default value. | ||
* | ||
* <p>Subclasses only need to define specific steps such as how to obtain named | ||
* value details from a method parameter, how to resolve to argument values, or | ||
* how to handle missing values. | ||
* | ||
* <p>A default value string can contain ${...} placeholders and Spring | ||
* Expression Language {@code #{...}} expressions which will be resolved if a | ||
* {@link ConfigurableBeanFactory} is supplied to the class constructor. | ||
* | ||
* <p>A {@link ConversionService} is used to to convert resolved String argument | ||
* value to the expected target method parameter type. | ||
* | ||
* @author Rossen Stoyanchev | ||
* @since 5.2 | ||
*/ | ||
public abstract class AbstractNamedValueMethodArgumentResolver implements SyncHandlerMethodArgumentResolver { | ||
|
||
private final ConversionService conversionService; | ||
|
||
@Nullable | ||
private final ConfigurableBeanFactory configurableBeanFactory; | ||
|
||
@Nullable | ||
private final BeanExpressionContext expressionContext; | ||
|
||
private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<>(256); | ||
|
||
|
||
/** | ||
* Constructor with a {@link ConversionService} and a {@link BeanFactory}. | ||
* @param conversionService conversion service for converting String values | ||
* to the target method parameter type | ||
* @param beanFactory a bean factory for resolving {@code ${...}} | ||
* placeholders and {@code #{...}} SpEL expressions in default values | ||
*/ | ||
protected AbstractNamedValueMethodArgumentResolver(ConversionService conversionService, | ||
@Nullable ConfigurableBeanFactory beanFactory) { | ||
|
||
this.conversionService = conversionService; | ||
this.configurableBeanFactory = beanFactory; | ||
this.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, null) : null); | ||
} | ||
|
||
|
||
@Override | ||
public Object resolveArgumentValue(MethodParameter parameter, Message<?> message) { | ||
|
||
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); | ||
MethodParameter nestedParameter = parameter.nestedIfOptional(); | ||
|
||
Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name); | ||
if (resolvedName == null) { | ||
throw new IllegalArgumentException( | ||
"Specified name must not resolve to null: [" + namedValueInfo.name + "]"); | ||
} | ||
|
||
Object arg = resolveArgumentInternal(nestedParameter, message, resolvedName.toString()); | ||
if (arg == null) { | ||
if (namedValueInfo.defaultValue != null) { | ||
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); | ||
} | ||
else if (namedValueInfo.required && !nestedParameter.isOptional()) { | ||
handleMissingValue(namedValueInfo.name, nestedParameter, message); | ||
} | ||
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); | ||
} | ||
else if ("".equals(arg) && namedValueInfo.defaultValue != null) { | ||
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); | ||
} | ||
|
||
if (parameter != nestedParameter || !ClassUtils.isAssignableValue(parameter.getParameterType(), arg)) { | ||
arg = this.conversionService.convert(arg, TypeDescriptor.forObject(arg), new TypeDescriptor(parameter)); | ||
} | ||
|
||
return arg; | ||
} | ||
|
||
/** | ||
* Obtain the named value for the given method parameter. | ||
*/ | ||
private NamedValueInfo getNamedValueInfo(MethodParameter parameter) { | ||
NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter); | ||
if (namedValueInfo == null) { | ||
namedValueInfo = createNamedValueInfo(parameter); | ||
namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo); | ||
this.namedValueInfoCache.put(parameter, namedValueInfo); | ||
} | ||
return namedValueInfo; | ||
} | ||
|
||
/** | ||
* Create the {@link NamedValueInfo} object for the given method parameter. | ||
* Implementations typically retrieve the method annotation by means of | ||
* {@link MethodParameter#getParameterAnnotation(Class)}. | ||
* @param parameter the method parameter | ||
* @return the named value information | ||
*/ | ||
protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter); | ||
|
||
/** | ||
* Fall back on the parameter name from the class file if necessary and | ||
* replace {@link ValueConstants#DEFAULT_NONE} with null. | ||
*/ | ||
private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) { | ||
String name = info.name; | ||
if (info.name.isEmpty()) { | ||
name = parameter.getParameterName(); | ||
if (name == null) { | ||
Class<?> type = parameter.getParameterType(); | ||
throw new IllegalArgumentException( | ||
"Name for argument of type [" + type.getName() + "] not specified, " + | ||
"and parameter name information not found in class file either."); | ||
} | ||
} | ||
return new NamedValueInfo(name, info.required, | ||
ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue); | ||
} | ||
|
||
/** | ||
* Resolve the given annotation-specified value, | ||
* potentially containing placeholders and expressions. | ||
*/ | ||
@Nullable | ||
private Object resolveEmbeddedValuesAndExpressions(String value) { | ||
if (this.configurableBeanFactory == null || this.expressionContext == null) { | ||
return value; | ||
} | ||
String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value); | ||
BeanExpressionResolver exprResolver = this.configurableBeanFactory.getBeanExpressionResolver(); | ||
if (exprResolver == null) { | ||
return value; | ||
} | ||
return exprResolver.evaluate(placeholdersResolved, this.expressionContext); | ||
} | ||
|
||
/** | ||
* Resolves the given parameter type and value name into an argument value. | ||
* @param parameter the method parameter to resolve to an argument value | ||
* @param message the current request | ||
* @param name the name of the value being resolved | ||
* @return the resolved argument. May be {@code null} | ||
*/ | ||
@Nullable | ||
protected abstract Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name); | ||
|
||
/** | ||
* Invoked when a value is required, but {@link #resolveArgumentInternal} | ||
* returned {@code null} and there is no default value. Sub-classes can | ||
* throw an appropriate exception for this case. | ||
* @param name the name for the value | ||
* @param parameter the target method parameter | ||
* @param message the message being processed | ||
*/ | ||
protected abstract void handleMissingValue(String name, MethodParameter parameter, Message<?> message); | ||
|
||
/** | ||
* One last chance to handle a possible null value. | ||
* Specifically for booleans method parameters, use {@link Boolean#FALSE}. | ||
* Also raise an ISE for primitive types. | ||
*/ | ||
@Nullable | ||
private Object handleNullValue(String name, @Nullable Object value, Class<?> paramType) { | ||
if (value == null) { | ||
if (Boolean.TYPE.equals(paramType)) { | ||
return Boolean.FALSE; | ||
} | ||
else if (paramType.isPrimitive()) { | ||
throw new IllegalStateException("Optional " + paramType + " parameter '" + name + | ||
"' is present but cannot be translated into a null value due to being " + | ||
"declared as a primitive type. Consider declaring it as object wrapper " + | ||
"for the corresponding primitive type."); | ||
} | ||
} | ||
return value; | ||
} | ||
|
||
|
||
/** | ||
* Represents a named value declaration. | ||
*/ | ||
protected static class NamedValueInfo { | ||
|
||
private final String name; | ||
|
||
private final boolean required; | ||
|
||
@Nullable | ||
private final String defaultValue; | ||
|
||
protected NamedValueInfo(String name, boolean required, @Nullable String defaultValue) { | ||
this.name = name; | ||
this.required = required; | ||
this.defaultValue = defaultValue; | ||
} | ||
} | ||
|
||
} |
84 changes: 84 additions & 0 deletions
84
...saging/handler/annotation/support/reactive/DestinationVariableMethodArgumentResolver.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
/* | ||
* Copyright 2002-2019 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.springframework.messaging.handler.annotation.support.reactive; | ||
|
||
import java.util.Map; | ||
|
||
import org.springframework.core.MethodParameter; | ||
import org.springframework.core.convert.ConversionService; | ||
import org.springframework.lang.Nullable; | ||
import org.springframework.messaging.Message; | ||
import org.springframework.messaging.MessageHandlingException; | ||
import org.springframework.messaging.MessageHeaders; | ||
import org.springframework.messaging.handler.annotation.DestinationVariable; | ||
import org.springframework.messaging.handler.annotation.ValueConstants; | ||
import org.springframework.util.Assert; | ||
|
||
/** | ||
* Resolve for {@link DestinationVariable @DestinationVariable} method parameters. | ||
* | ||
* @author Rossen Stoyanchev | ||
* @since 5.2 | ||
*/ | ||
public class DestinationVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { | ||
|
||
/** The name of the header used to for template variables. */ | ||
public static final String DESTINATION_TEMPLATE_VARIABLES_HEADER = | ||
DestinationVariableMethodArgumentResolver.class.getSimpleName() + ".templateVariables"; | ||
|
||
|
||
public DestinationVariableMethodArgumentResolver(ConversionService conversionService) { | ||
super(conversionService, null); | ||
} | ||
|
||
|
||
@Override | ||
public boolean supportsParameter(MethodParameter parameter) { | ||
return parameter.hasParameterAnnotation(DestinationVariable.class); | ||
} | ||
|
||
@Override | ||
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { | ||
DestinationVariable annot = parameter.getParameterAnnotation(DestinationVariable.class); | ||
Assert.state(annot != null, "No DestinationVariable annotation"); | ||
return new DestinationVariableNamedValueInfo(annot); | ||
} | ||
|
||
@Override | ||
@Nullable | ||
@SuppressWarnings("unchecked") | ||
protected Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name) { | ||
MessageHeaders headers = message.getHeaders(); | ||
Map<String, String> vars = (Map<String, String>) headers.get(DESTINATION_TEMPLATE_VARIABLES_HEADER); | ||
return vars != null ? vars.get(name) : null; | ||
} | ||
|
||
@Override | ||
protected void handleMissingValue(String name, MethodParameter parameter, Message<?> message) { | ||
throw new MessageHandlingException(message, "Missing path template variable '" + name + "' " + | ||
"for method parameter type [" + parameter.getParameterType() + "]"); | ||
} | ||
|
||
|
||
private static final class DestinationVariableNamedValueInfo extends NamedValueInfo { | ||
|
||
private DestinationVariableNamedValueInfo(DestinationVariable annotation) { | ||
super(annotation.value(), true, ValueConstants.DEFAULT_NONE); | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.