diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/reactive/AbstractNamedValueMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/reactive/AbstractNamedValueMethodArgumentResolver.java new file mode 100644 index 0000000000..9184e41f36 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/reactive/AbstractNamedValueMethodArgumentResolver.java @@ -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. + * + *

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. + * + *

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. + * + *

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 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; + } + } + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/reactive/DestinationVariableMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/reactive/DestinationVariableMethodArgumentResolver.java new file mode 100644 index 0000000000..b8c413072d --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/reactive/DestinationVariableMethodArgumentResolver.java @@ -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 vars = (Map) 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); + } + } + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/reactive/HeaderMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/reactive/HeaderMethodArgumentResolver.java new file mode 100644 index 0000000000..a50d38e377 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/reactive/HeaderMethodArgumentResolver.java @@ -0,0 +1,121 @@ +/* + * 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.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +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.handler.annotation.Header; +import org.springframework.messaging.support.NativeMessageHeaderAccessor; +import org.springframework.util.Assert; + +/** + * Resolver for {@link Header @Header} arguments. Headers are resolved from + * either the top-level header map or the nested + * {@link NativeMessageHeaderAccessor native} header map. + * + * @author Rossen Stoyanchev + * @since 5.2 + * + * @see HeadersMethodArgumentResolver + * @see NativeMessageHeaderAccessor + */ +public class HeaderMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + private static final Log logger = LogFactory.getLog(HeaderMethodArgumentResolver.class); + + + public HeaderMethodArgumentResolver( + ConversionService conversionService, @Nullable ConfigurableBeanFactory beanFactory) { + + super(conversionService, beanFactory); + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(Header.class); + } + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + Header annot = parameter.getParameterAnnotation(Header.class); + Assert.state(annot != null, "No Header annotation"); + return new HeaderNamedValueInfo(annot); + } + + @Override + @Nullable + protected Object resolveArgumentInternal(MethodParameter parameter, Message message, String name) { + + Object headerValue = message.getHeaders().get(name); + Object nativeHeaderValue = getNativeHeaderValue(message, name); + + if (headerValue != null && nativeHeaderValue != null) { + if (logger.isDebugEnabled()) { + logger.debug("A value was found for '" + name + "', in both the top level header map " + + "and also in the nested map for native headers. Using the value from top level map. " + + "Use 'nativeHeader.myHeader' to resolve the native header."); + } + } + + return (headerValue != null ? headerValue : nativeHeaderValue); + } + + @Nullable + private Object getNativeHeaderValue(Message message, String name) { + Map> nativeHeaders = getNativeHeaders(message); + if (name.startsWith("nativeHeaders.")) { + name = name.substring("nativeHeaders.".length()); + } + if (nativeHeaders == null || !nativeHeaders.containsKey(name)) { + return null; + } + List nativeHeaderValues = nativeHeaders.get(name); + return (nativeHeaderValues.size() == 1 ? nativeHeaderValues.get(0) : nativeHeaderValues); + } + + @SuppressWarnings("unchecked") + @Nullable + private Map> getNativeHeaders(Message message) { + return (Map>) message.getHeaders().get(NativeMessageHeaderAccessor.NATIVE_HEADERS); + } + + @Override + protected void handleMissingValue(String headerName, MethodParameter parameter, Message message) { + throw new MessageHandlingException(message, "Missing header '" + headerName + + "' for method parameter type [" + parameter.getParameterType() + "]"); + } + + + private static final class HeaderNamedValueInfo extends NamedValueInfo { + + private HeaderNamedValueInfo(Header annotation) { + super(annotation.name(), annotation.required(), annotation.defaultValue()); + } + } + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/reactive/HeadersMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/reactive/HeadersMethodArgumentResolver.java new file mode 100644 index 0000000000..e2aa858540 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/reactive/HeadersMethodArgumentResolver.java @@ -0,0 +1,82 @@ +/* + * 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.lang.reflect.Method; +import java.util.Map; + +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.handler.annotation.Headers; +import org.springframework.messaging.handler.invocation.reactive.SyncHandlerMethodArgumentResolver; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.util.ReflectionUtils; + +/** + * Argument resolver for headers. Resolves the following method parameters: + *

+ * + * @author Rossen Stoyanchev + * @since 5.2 + */ +public class HeadersMethodArgumentResolver implements SyncHandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + Class paramType = parameter.getParameterType(); + return ((parameter.hasParameterAnnotation(Headers.class) && Map.class.isAssignableFrom(paramType)) || + MessageHeaders.class == paramType || MessageHeaderAccessor.class.isAssignableFrom(paramType)); + } + + @Override + @Nullable + public Object resolveArgumentValue(MethodParameter parameter, Message message) { + Class paramType = parameter.getParameterType(); + if (Map.class.isAssignableFrom(paramType)) { + return message.getHeaders(); + } + else if (MessageHeaderAccessor.class == paramType) { + MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class); + return accessor != null ? accessor : new MessageHeaderAccessor(message); + } + else if (MessageHeaderAccessor.class.isAssignableFrom(paramType)) { + MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class); + if (accessor != null && paramType.isAssignableFrom(accessor.getClass())) { + return accessor; + } + else { + Method method = ReflectionUtils.findMethod(paramType, "wrap", Message.class); + if (method == null) { + throw new IllegalStateException( + "Cannot create accessor of type " + paramType + " for message " + message); + } + return ReflectionUtils.invokeMethod(method, null, message); + } + } + else { + throw new IllegalStateException("Unexpected parameter of type " + paramType + + " in method " + parameter.getMethod() + ". "); + } + } + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/reactive/MessageMappingMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/reactive/MessageMappingMessageHandler.java index 4d8d8ba9d6..25463e70f3 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/reactive/MessageMappingMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/reactive/MessageMappingMessageHandler.java @@ -24,9 +24,14 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.codec.Decoder; +import org.springframework.core.convert.ConversionService; +import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.handler.CompositeMessageCondition; @@ -73,6 +78,8 @@ public class MessageMappingMessageHandler extends AbstractMethodMessageHandlerBy default {@link DefaultFormattingConversionService} is used. + * @param conversionService the conversion service to use + */ + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + + /** + * Return the configured ConversionService. + */ + public ConversionService getConversionService() { + return this.conversionService; + } + @Override public void setEmbeddedValueResolver(StringValueResolver resolver) { this.valueResolver = resolver; @@ -159,6 +183,15 @@ public class MessageMappingMessageHandler extends AbstractMethodMessageHandler initArgumentResolvers() { List resolvers = new ArrayList<>(); + ApplicationContext context = getApplicationContext(); + ConfigurableBeanFactory beanFactory = (context instanceof ConfigurableApplicationContext ? + ((ConfigurableApplicationContext) context).getBeanFactory() : null); + + // Annotation-based resolvers + resolvers.add(new HeaderMethodArgumentResolver(this.conversionService, beanFactory)); + resolvers.add(new HeadersMethodArgumentResolver()); + resolvers.add(new DestinationVariableMethodArgumentResolver(this.conversionService)); + // Custom resolvers resolvers.addAll(getArgumentResolverConfigurer().getCustomResolvers()); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/SyncHandlerMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/SyncHandlerMethodArgumentResolver.java new file mode 100644 index 0000000000..01727c43de --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/SyncHandlerMethodArgumentResolver.java @@ -0,0 +1,52 @@ +/* + * 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.invocation.reactive; + +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; +import org.springframework.messaging.Message; + +/** + * An extension of {@link HandlerMethodArgumentResolver} for implementations + * that are synchronous in nature and do not block to resolve values. + * + * @author Rossen Stoyanchev + * @since 5.2 + */ +public interface SyncHandlerMethodArgumentResolver extends HandlerMethodArgumentResolver { + + /** + * {@inheritDoc} + *

By default this simply delegates to {@link #resolveArgumentValue} for + * synchronous resolution. + */ + @Override + default Mono resolveArgument(MethodParameter parameter, Message message) { + return Mono.justOrEmpty(resolveArgumentValue(parameter, message)); + } + + /** + * Resolve the value for the method parameter synchronously. + * @param parameter the method parameter + * @param message the currently processed message + * @return the resolved value, if any + */ + @Nullable + Object resolveArgumentValue(MethodParameter parameter, Message message); + +} diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/MessagingPredicates.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/MessagingPredicates.java new file mode 100644 index 0000000000..59c0100068 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/MessagingPredicates.java @@ -0,0 +1,128 @@ +/* + * 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; + +import java.util.function.Predicate; + +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; + +/** + * Predicates for messaging annotations. + * + * @author Rossen Stoyanchev + */ +public class MessagingPredicates { + + public static DestinationVariablePredicate destinationVar() { + return new DestinationVariablePredicate(); + } + + public static DestinationVariablePredicate destinationVar(String value) { + return new DestinationVariablePredicate().value(value); + } + + public static HeaderPredicate header() { + return new HeaderPredicate(); + } + + public static HeaderPredicate header(String name) { + return new HeaderPredicate().name(name); + } + + public static HeaderPredicate header(String name, String defaultValue) { + return new HeaderPredicate().name(name).defaultValue(defaultValue); + } + + public static HeaderPredicate headerPlain() { + return new HeaderPredicate().noAttributes(); + } + + + public static class DestinationVariablePredicate implements Predicate { + + @Nullable + private String value; + + + public DestinationVariablePredicate value(@Nullable String name) { + this.value = name; + return this; + } + + public DestinationVariablePredicate noValue() { + this.value = ""; + return this; + } + + @Override + public boolean test(MethodParameter parameter) { + DestinationVariable annotation = parameter.getParameterAnnotation(DestinationVariable.class); + return annotation != null && (this.value == null || annotation.value().equals(this.value)); + } + } + + + public static class HeaderPredicate implements Predicate { + + @Nullable + private String name; + + @Nullable + private Boolean required; + + @Nullable + private String defaultValue; + + + public HeaderPredicate name(@Nullable String name) { + this.name = name; + return this; + } + + public HeaderPredicate noName() { + this.name = ""; + return this; + } + + public HeaderPredicate required(boolean required) { + this.required = required; + return this; + } + + public HeaderPredicate defaultValue(@Nullable String value) { + this.defaultValue = value; + return this; + } + + public HeaderPredicate noAttributes() { + this.name = ""; + this.required = true; + this.defaultValue = ValueConstants.DEFAULT_NONE; + return this; + } + + @Override + public boolean test(MethodParameter parameter) { + Header annotation = parameter.getParameterAnnotation(Header.class); + return annotation != null && + (this.name == null || annotation.name().equals(this.name)) && + (this.required == null || annotation.required() == this.required) && + (this.defaultValue == null || annotation.defaultValue().equals(this.defaultValue)); + } + } + +} diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/reactive/DestinationVariableMethodArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/reactive/DestinationVariableMethodArgumentResolverTests.java new file mode 100644 index 0000000000..e5aee3aa95 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/reactive/DestinationVariableMethodArgumentResolverTests.java @@ -0,0 +1,91 @@ +/* + * 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.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandlingException; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.invocation.ResolvableMethod; +import org.springframework.messaging.support.MessageBuilder; + +import static org.junit.Assert.*; +import static org.springframework.messaging.handler.annotation.MessagingPredicates.*; + +/** + * Test fixture for {@link DestinationVariableMethodArgumentResolver} tests. + * @author Rossen Stoyanchev + */ +public class DestinationVariableMethodArgumentResolverTests { + + private final DestinationVariableMethodArgumentResolver resolver = + new DestinationVariableMethodArgumentResolver(new DefaultConversionService()); + + private final ResolvableMethod resolvable = + ResolvableMethod.on(getClass()).named("handleMessage").build(); + + + @Test + public void supportsParameter() { + assertTrue(resolver.supportsParameter(this.resolvable.annot(destinationVar().noValue()).arg())); + assertFalse(resolver.supportsParameter(this.resolvable.annotNotPresent(DestinationVariable.class).arg())); + } + + @Test + public void resolveArgument() { + + Map vars = new HashMap<>(); + vars.put("foo", "bar"); + vars.put("name", "value"); + + Message message = MessageBuilder.withPayload(new byte[0]).setHeader( + DestinationVariableMethodArgumentResolver.DESTINATION_TEMPLATE_VARIABLES_HEADER, vars).build(); + + Object result = resolveArgument(this.resolvable.annot(destinationVar().noValue()).arg(), message); + assertEquals("bar", result); + + result = resolveArgument(this.resolvable.annot(destinationVar("name")).arg(), message); + assertEquals("value", result); + } + + @Test(expected = MessageHandlingException.class) + public void resolveArgumentNotFound() { + Message message = MessageBuilder.withPayload(new byte[0]).build(); + resolveArgument(this.resolvable.annot(destinationVar().noValue()).arg(), message); + } + + @SuppressWarnings({"unchecked", "ConstantConditions"}) + private T resolveArgument(MethodParameter param, Message message) { + return (T) this.resolver.resolveArgument(param, message).block(Duration.ofSeconds(5)); + } + + + @SuppressWarnings("unused") + private void handleMessage( + @DestinationVariable String foo, + @DestinationVariable(value = "name") String param1, + String param3) { + } + +} diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/reactive/HeaderMethodArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/reactive/HeaderMethodArgumentResolverTests.java new file mode 100644 index 0000000000..bc2422ad27 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/reactive/HeaderMethodArgumentResolverTests.java @@ -0,0 +1,176 @@ +/* + * 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.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandlingException; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.messaging.handler.invocation.ResolvableMethod; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.messaging.support.NativeMessageHeaderAccessor; + +import static org.junit.Assert.*; +import static org.springframework.messaging.handler.annotation.MessagingPredicates.*; + +/** + * Test fixture for {@link HeaderMethodArgumentResolver} tests. + * @author Rossen Stoyanchev + */ +public class HeaderMethodArgumentResolverTests { + + private HeaderMethodArgumentResolver resolver; + + private final ResolvableMethod resolvable = ResolvableMethod.on(getClass()).named("handleMessage").build(); + + + @Before + public void setup() { + GenericApplicationContext context = new GenericApplicationContext(); + context.refresh(); + this.resolver = new HeaderMethodArgumentResolver(new DefaultConversionService(), context.getBeanFactory()); + } + + + @Test + public void supportsParameter() { + assertTrue(this.resolver.supportsParameter(this.resolvable.annot(headerPlain()).arg())); + assertFalse(this.resolver.supportsParameter(this.resolvable.annotNotPresent(Header.class).arg())); + } + + @Test + public void resolveArgument() { + Message message = MessageBuilder.withPayload(new byte[0]).setHeader("param1", "foo").build(); + Object result = resolveArgument(this.resolvable.annot(headerPlain()).arg(), message); + assertEquals("foo", result); + } + + @Test // SPR-11326 + public void resolveArgumentNativeHeader() { + TestMessageHeaderAccessor headers = new TestMessageHeaderAccessor(); + headers.setNativeHeader("param1", "foo"); + Message message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build(); + assertEquals("foo", resolveArgument(this.resolvable.annot(headerPlain()).arg(), message)); + } + + @Test + public void resolveArgumentNativeHeaderAmbiguity() { + TestMessageHeaderAccessor headers = new TestMessageHeaderAccessor(); + headers.setHeader("param1", "foo"); + headers.setNativeHeader("param1", "native-foo"); + Message message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build(); + + assertEquals("foo", resolveArgument( + this.resolvable.annot(headerPlain()).arg(), message)); + + assertEquals("native-foo", resolveArgument( + this.resolvable.annot(header("nativeHeaders.param1")).arg(), message)); + } + + @Test(expected = MessageHandlingException.class) + public void resolveArgumentNotFound() { + Message message = MessageBuilder.withPayload(new byte[0]).build(); + resolveArgument(this.resolvable.annot(headerPlain()).arg(), message); + } + + @Test + public void resolveArgumentDefaultValue() { + Message message = MessageBuilder.withPayload(new byte[0]).build(); + Object result = resolveArgument(this.resolvable.annot(header("name", "bar")).arg(), message); + assertEquals("bar", result); + } + + @Test + public void resolveDefaultValueSystemProperty() { + System.setProperty("systemProperty", "sysbar"); + try { + Message message = MessageBuilder.withPayload(new byte[0]).build(); + MethodParameter param = this.resolvable.annot(header("name", "#{systemProperties.systemProperty}")).arg(); + Object result = resolveArgument(param, message); + assertEquals("sysbar", result); + } + finally { + System.clearProperty("systemProperty"); + } + } + + @Test + public void resolveNameFromSystemProperty() { + System.setProperty("systemProperty", "sysbar"); + try { + Message message = MessageBuilder.withPayload(new byte[0]).setHeader("sysbar", "foo").build(); + MethodParameter param = this.resolvable.annot(header("#{systemProperties.systemProperty}")).arg(); + Object result = resolveArgument(param, message); + assertEquals("foo", result); + } + finally { + System.clearProperty("systemProperty"); + } + } + + @Test + public void resolveOptionalHeaderWithValue() { + Message message = MessageBuilder.withPayload("foo").setHeader("foo", "bar").build(); + MethodParameter param = this.resolvable.annot(header("foo")).arg(Optional.class, String.class); + Object result = resolveArgument(param, message); + assertEquals(Optional.of("bar"), result); + } + + @Test + public void resolveOptionalHeaderAsEmpty() { + Message message = MessageBuilder.withPayload("foo").build(); + MethodParameter param = this.resolvable.annot(header("foo")).arg(Optional.class, String.class); + Object result = resolveArgument(param, message); + assertEquals(Optional.empty(), result); + } + + @SuppressWarnings({"unchecked", "ConstantConditions"}) + private T resolveArgument(MethodParameter param, Message message) { + return (T) this.resolver.resolveArgument(param, message).block(Duration.ofSeconds(5)); + } + + + @SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) + public void handleMessage( + @Header String param1, + @Header(name = "name", defaultValue = "bar") String param2, + @Header(name = "name", defaultValue = "#{systemProperties.systemProperty}") String param3, + @Header(name = "#{systemProperties.systemProperty}") String param4, + String param5, + @Header("foo") Optional param6, + @Header("nativeHeaders.param1") String nativeHeaderParam1) { + } + + + public static class TestMessageHeaderAccessor extends NativeMessageHeaderAccessor { + + TestMessageHeaderAccessor() { + super((Map>) null); + } + } + +} diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/reactive/HeadersMethodArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/reactive/HeadersMethodArgumentResolverTests.java new file mode 100644 index 0000000000..bfbcd1e8db --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/reactive/HeadersMethodArgumentResolverTests.java @@ -0,0 +1,121 @@ +/* + * 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.time.Duration; +import java.util.Collections; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.handler.annotation.Headers; +import org.springframework.messaging.handler.invocation.ResolvableMethod; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.messaging.support.NativeMessageHeaderAccessor; + +import static org.junit.Assert.*; + +/** + * Test fixture for {@link HeadersMethodArgumentResolver} tests. + * @author Rossen Stoyanchev + */ +public class HeadersMethodArgumentResolverTests { + + private final HeadersMethodArgumentResolver resolver = new HeadersMethodArgumentResolver(); + + private Message message = + MessageBuilder.withPayload(new byte[0]).copyHeaders(Collections.singletonMap("foo", "bar")).build(); + + private final ResolvableMethod resolvable = ResolvableMethod.on(getClass()).named("handleMessage").build(); + + + @Test + public void supportsParameter() { + + assertTrue(this.resolver.supportsParameter( + this.resolvable.annotPresent(Headers.class).arg(Map.class, String.class, Object.class))); + + assertTrue(this.resolver.supportsParameter(this.resolvable.arg(MessageHeaders.class))); + assertTrue(this.resolver.supportsParameter(this.resolvable.arg(MessageHeaderAccessor.class))); + assertTrue(this.resolver.supportsParameter(this.resolvable.arg(TestMessageHeaderAccessor.class))); + + assertFalse(this.resolver.supportsParameter(this.resolvable.annotPresent(Headers.class).arg(String.class))); + } + + @Test + @SuppressWarnings("unchecked") + public void resolveArgumentAnnotated() { + MethodParameter param = this.resolvable.annotPresent(Headers.class).arg(Map.class, String.class, Object.class); + Map headers = resolveArgument(param); + assertEquals("bar", headers.get("foo")); + } + + @Test(expected = IllegalStateException.class) + public void resolveArgumentAnnotatedNotMap() { + resolveArgument(this.resolvable.annotPresent(Headers.class).arg(String.class)); + } + + @Test + public void resolveArgumentMessageHeaders() { + MessageHeaders headers = resolveArgument(this.resolvable.arg(MessageHeaders.class)); + assertEquals("bar", headers.get("foo")); + } + + @Test + public void resolveArgumentMessageHeaderAccessor() { + MessageHeaderAccessor headers = resolveArgument(this.resolvable.arg(MessageHeaderAccessor.class)); + assertEquals("bar", headers.getHeader("foo")); + } + + @Test + public void resolveArgumentMessageHeaderAccessorSubclass() { + TestMessageHeaderAccessor headers = resolveArgument(this.resolvable.arg(TestMessageHeaderAccessor.class)); + assertEquals("bar", headers.getHeader("foo")); + } + + @SuppressWarnings({"unchecked", "ConstantConditions"}) + private T resolveArgument(MethodParameter param) { + return (T) this.resolver.resolveArgument(param, this.message).block(Duration.ofSeconds(5)); + } + + + @SuppressWarnings("unused") + private void handleMessage( + @Headers Map param1, + @Headers String param2, + MessageHeaders param3, + MessageHeaderAccessor param4, + TestMessageHeaderAccessor param5) { + } + + + public static class TestMessageHeaderAccessor extends NativeMessageHeaderAccessor { + + TestMessageHeaderAccessor(Message message) { + super(message); + } + + public static TestMessageHeaderAccessor wrap(Message message) { + return new TestMessageHeaderAccessor(message); + } + } + +}