Initial support for RSocket in spring-messaging
This commit is contained in:
		
						commit
						1ec3261062
					
				|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2018 the original author or authors. |  * Copyright 2002-2019 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -105,13 +105,15 @@ public abstract class AbstractDataBufferAllocatingTestCase { | ||||||
| 	 */ | 	 */ | ||||||
| 	protected void waitForDataBufferRelease(Duration duration) throws InterruptedException { | 	protected void waitForDataBufferRelease(Duration duration) throws InterruptedException { | ||||||
| 		Instant start = Instant.now(); | 		Instant start = Instant.now(); | ||||||
| 		while (Instant.now().isBefore(start.plus(duration))) { | 		while (true) { | ||||||
| 			try { | 			try { | ||||||
| 				verifyAllocations(); | 				verifyAllocations(); | ||||||
| 				break; | 				break; | ||||||
| 			} | 			} | ||||||
| 			catch (AssertionError ex) { | 			catch (AssertionError ex) { | ||||||
| 				// ignore; | 				if (Instant.now().isAfter(start.plus(duration))) { | ||||||
|  | 					throw ex; | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 			Thread.sleep(50); | 			Thread.sleep(50); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -7,12 +7,15 @@ dependencyManagement { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | def rsocketVersion = "0.11.17" | ||||||
|  | 
 | ||||||
| dependencies { | dependencies { | ||||||
| 	compile(project(":spring-beans")) | 	compile(project(":spring-beans")) | ||||||
| 	compile(project(":spring-core")) | 	compile(project(":spring-core")) | ||||||
| 	optional(project(":spring-context")) | 	optional(project(":spring-context")) | ||||||
| 	optional(project(":spring-oxm")) | 	optional(project(":spring-oxm")) | ||||||
| 	optional("io.projectreactor.netty:reactor-netty") | 	optional("io.projectreactor.netty:reactor-netty") | ||||||
|  | 	optional("io.rsocket:rsocket-core:${rsocketVersion}") | ||||||
| 	optional("com.fasterxml.jackson.core:jackson-databind:${jackson2Version}") | 	optional("com.fasterxml.jackson.core:jackson-databind:${jackson2Version}") | ||||||
| 	optional("javax.xml.bind:jaxb-api:2.3.1") | 	optional("javax.xml.bind:jaxb-api:2.3.1") | ||||||
| 	testCompile("javax.inject:javax.inject-tck:1") | 	testCompile("javax.inject:javax.inject-tck:1") | ||||||
|  | @ -24,6 +27,9 @@ dependencies { | ||||||
| 		exclude group: "org.springframework", module: "spring-context" | 		exclude group: "org.springframework", module: "spring-context" | ||||||
| 	} | 	} | ||||||
| 	testCompile("org.apache.activemq:activemq-stomp:5.8.0") | 	testCompile("org.apache.activemq:activemq-stomp:5.8.0") | ||||||
|  | 	testCompile("io.projectreactor:reactor-test") | ||||||
|  | 	testCompile "io.reactivex.rxjava2:rxjava:${rxjava2Version}" | ||||||
|  | 	testCompile("io.rsocket:rsocket-transport-netty:${rsocketVersion}") | ||||||
| 	testCompile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}") | 	testCompile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}") | ||||||
| 	testCompile("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}") | 	testCompile("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}") | ||||||
| 	testCompile("org.xmlunit:xmlunit-matchers:2.6.2") | 	testCompile("org.xmlunit:xmlunit-matchers:2.6.2") | ||||||
|  |  | ||||||
|  | @ -0,0 +1,36 @@ | ||||||
|  | /* | ||||||
|  |  * 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; | ||||||
|  | 
 | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Reactive contract for handling a {@link Message}. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | @FunctionalInterface | ||||||
|  | public interface ReactiveMessageHandler { | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Handle the given message. | ||||||
|  | 	 * @param message the message to be handled | ||||||
|  | 	 * @return a completion {@link Mono} for the result of the message handling. | ||||||
|  | 	 */ | ||||||
|  | 	Mono<Void> handleMessage(Message<?> message); | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,160 @@ | ||||||
|  | /* | ||||||
|  |  * 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; | ||||||
|  | 
 | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.stream.Collectors; | ||||||
|  | 
 | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Composite {@link MessageCondition} that delegates to other message conditions. | ||||||
|  |  * | ||||||
|  |  * <p>For {@link #combine} and {@link #compareTo} it is expected that the "other" | ||||||
|  |  * composite contains the same number, type, and order of message conditions. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | public class CompositeMessageCondition implements MessageCondition<CompositeMessageCondition> { | ||||||
|  | 
 | ||||||
|  | 	private final List<MessageCondition<?>> messageConditions; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	public CompositeMessageCondition(MessageCondition<?>... messageConditions) { | ||||||
|  | 		this(Arrays.asList(messageConditions)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private CompositeMessageCondition(List<MessageCondition<?>> messageConditions) { | ||||||
|  | 		Assert.notEmpty(messageConditions, "No message conditions"); | ||||||
|  | 		this.messageConditions = messageConditions; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	public List<MessageCondition<?>> getMessageConditions() { | ||||||
|  | 		return this.messageConditions; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("unchecked") | ||||||
|  | 	public <T extends MessageCondition<T>> T getCondition(Class<T> messageConditionType) { | ||||||
|  | 		for (MessageCondition<?> condition : this.messageConditions) { | ||||||
|  | 			if (messageConditionType.isAssignableFrom(condition.getClass())) { | ||||||
|  | 				return (T) condition; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		throw new IllegalStateException("No condition of type: " + messageConditionType); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public CompositeMessageCondition combine(CompositeMessageCondition other) { | ||||||
|  | 		checkCompatible(other); | ||||||
|  | 		List<MessageCondition<?>> result = new ArrayList<>(this.messageConditions.size()); | ||||||
|  | 		for (int i = 0; i < this.messageConditions.size(); i++) { | ||||||
|  | 			result.add(combine(getMessageConditions().get(i), other.getMessageConditions().get(i))); | ||||||
|  | 		} | ||||||
|  | 		return new CompositeMessageCondition(result); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("unchecked") | ||||||
|  | 	private <T extends MessageCondition<T>> T combine(MessageCondition<?> first, MessageCondition<?> second) { | ||||||
|  | 		return ((T) first).combine((T) second); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public CompositeMessageCondition getMatchingCondition(Message<?> message) { | ||||||
|  | 		List<MessageCondition<?>> result = new ArrayList<>(this.messageConditions.size()); | ||||||
|  | 		for (MessageCondition<?> condition : this.messageConditions) { | ||||||
|  | 			MessageCondition<?> matchingCondition = (MessageCondition<?>) condition.getMatchingCondition(message); | ||||||
|  | 			if (matchingCondition == null) { | ||||||
|  | 				return null; | ||||||
|  | 			} | ||||||
|  | 			result.add(matchingCondition); | ||||||
|  | 		} | ||||||
|  | 		return new CompositeMessageCondition(result); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public int compareTo(CompositeMessageCondition other, Message<?> message) { | ||||||
|  | 		checkCompatible(other); | ||||||
|  | 		List<MessageCondition<?>> otherConditions = other.getMessageConditions(); | ||||||
|  | 		for (int i = 0; i < this.messageConditions.size(); i++) { | ||||||
|  | 			int result = compare (this.messageConditions.get(i), otherConditions.get(i), message); | ||||||
|  | 			if (result != 0) { | ||||||
|  | 				return result; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return 0; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("unchecked") | ||||||
|  | 	private <T extends MessageCondition<T>> int compare( | ||||||
|  | 			MessageCondition<?> first, MessageCondition<?> second, Message<?> message) { | ||||||
|  | 
 | ||||||
|  | 		return ((T) first).compareTo((T) second, message); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private void checkCompatible(CompositeMessageCondition other) { | ||||||
|  | 		List<MessageCondition<?>> others = other.getMessageConditions(); | ||||||
|  | 		for (int i = 0; i < this.messageConditions.size(); i++) { | ||||||
|  | 			if (i < others.size()) { | ||||||
|  | 				if (this.messageConditions.get(i).getClass().equals(others.get(i).getClass())) { | ||||||
|  | 					continue; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			throw new IllegalArgumentException("Mismatched CompositeMessageCondition: " + | ||||||
|  | 					this.messageConditions + " vs " + others); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public boolean equals(Object other) { | ||||||
|  | 		if (this == other) { | ||||||
|  | 			return true; | ||||||
|  | 		} | ||||||
|  | 		if (!(other instanceof CompositeMessageCondition)) { | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  | 		CompositeMessageCondition otherComposite = (CompositeMessageCondition) other; | ||||||
|  | 		checkCompatible(otherComposite); | ||||||
|  | 		List<MessageCondition<?>> otherConditions = otherComposite.getMessageConditions(); | ||||||
|  | 		for (int i = 0; i < this.messageConditions.size(); i++) { | ||||||
|  | 			if (!this.messageConditions.get(i).equals(otherConditions.get(i))) { | ||||||
|  | 				return false; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return true; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public int hashCode() { | ||||||
|  | 		int hashCode = 0; | ||||||
|  | 		for (MessageCondition<?> condition : this.messageConditions) { | ||||||
|  | 			hashCode += condition.hashCode() * 31; | ||||||
|  | 		} | ||||||
|  | 		return hashCode; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public String toString() { | ||||||
|  | 		return this.messageConditions.stream().map(Object::toString).collect(Collectors.joining(",", "{", "}")); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -298,7 +298,7 @@ public class HandlerMethod { | ||||||
| 	 */ | 	 */ | ||||||
| 	public String getShortLogMessage() { | 	public String getShortLogMessage() { | ||||||
| 		int args = this.method.getParameterCount(); | 		int args = this.method.getParameterCount(); | ||||||
| 		return getBeanType().getName() + "#" + this.method.getName() + "[" + args + " args]"; | 		return getBeanType().getSimpleName() + "#" + this.method.getName() + "[" + args + " args]"; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2017 the original author or authors. |  * Copyright 2002-2019 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -26,7 +26,6 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory; | ||||||
| import org.springframework.core.MethodParameter; | import org.springframework.core.MethodParameter; | ||||||
| import org.springframework.core.convert.ConversionService; | import org.springframework.core.convert.ConversionService; | ||||||
| import org.springframework.core.convert.TypeDescriptor; | import org.springframework.core.convert.TypeDescriptor; | ||||||
| import org.springframework.core.convert.support.DefaultConversionService; |  | ||||||
| import org.springframework.lang.Nullable; | import org.springframework.lang.Nullable; | ||||||
| import org.springframework.messaging.Message; | import org.springframework.messaging.Message; | ||||||
| import org.springframework.messaging.handler.annotation.ValueConstants; | import org.springframework.messaging.handler.annotation.ValueConstants; | ||||||
|  | @ -34,24 +33,20 @@ import org.springframework.messaging.handler.invocation.HandlerMethodArgumentRes | ||||||
| import org.springframework.util.ClassUtils; | import org.springframework.util.ClassUtils; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Abstract base class for resolving method arguments from a named value. Message headers, |  * Abstract base class to resolve method arguments from a named value, e.g. | ||||||
|  * and path variables are examples of named values. Each may have a name, a required flag, |  * message headers or destination variables. Named values could have one or more | ||||||
|  * and a default value. |  * of a name, a required flag, and a default value. | ||||||
|  * |  * | ||||||
|  * <p>Subclasses define how to do the following: |  * <p>Subclasses only need to define specific steps such as how to obtain named | ||||||
|  * <ul> |  * value details from a method parameter, how to resolve to argument values, or | ||||||
|  * <li>Obtain named value information for a method parameter |  * how to handle missing values. | ||||||
|  * <li>Resolve names into argument values |  | ||||||
|  * <li>Handle missing argument values when argument values are required |  | ||||||
|  * <li>Optionally handle a resolved value |  | ||||||
|  * </ul> |  | ||||||
|  * |  * | ||||||
|  * <p>A default value string can contain ${...} placeholders and Spring Expression |  *  <p>A default value string can contain ${...} placeholders and Spring | ||||||
|  * Language {@code #{...}} expressions. For this to work a {@link ConfigurableBeanFactory} |  * Expression Language {@code #{...}} expressions which will be resolved if a | ||||||
|  * must be supplied to the class constructor. |  * {@link ConfigurableBeanFactory} is supplied to the class constructor. | ||||||
|  * |  * | ||||||
|  * <p>A {@link ConversionService} may be used to apply type conversion to the resolved |  * <p>A {@link ConversionService} is used to to convert resolved String argument | ||||||
|  * argument value if it doesn't match the method parameter type. |  * value to the expected target method parameter type. | ||||||
|  * |  * | ||||||
|  * @author Rossen Stoyanchev |  * @author Rossen Stoyanchev | ||||||
|  * @author Juergen Hoeller |  * @author Juergen Hoeller | ||||||
|  | @ -61,8 +56,10 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle | ||||||
| 
 | 
 | ||||||
| 	private final ConversionService conversionService; | 	private final ConversionService conversionService; | ||||||
| 
 | 
 | ||||||
|  | 	@Nullable | ||||||
| 	private final ConfigurableBeanFactory configurableBeanFactory; | 	private final ConfigurableBeanFactory configurableBeanFactory; | ||||||
| 
 | 
 | ||||||
|  | 	@Nullable | ||||||
| 	private final BeanExpressionContext expressionContext; | 	private final BeanExpressionContext expressionContext; | ||||||
| 
 | 
 | ||||||
| 	private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<>(256); | 	private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<>(256); | ||||||
|  | @ -70,28 +67,27 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * Constructor with a {@link ConversionService} and a {@link BeanFactory}. | 	 * Constructor with a {@link ConversionService} and a {@link BeanFactory}. | ||||||
| 	 * @param cs conversion service for converting values to match the | 	 * @param conversionService conversion service for converting String values | ||||||
| 	 * target method parameter type | 	 * to the target method parameter type | ||||||
| 	 * @param beanFactory a bean factory to use for resolving {@code ${...}} placeholder | 	 * @param beanFactory a bean factory for resolving {@code ${...}} | ||||||
| 	 * and {@code #{...}} SpEL expressions in default values, or {@code null} if default | 	 * placeholders and {@code #{...}} SpEL expressions in default values | ||||||
| 	 * values are not expected to contain expressions |  | ||||||
| 	 */ | 	 */ | ||||||
| 	protected AbstractNamedValueMethodArgumentResolver(ConversionService cs, | 	protected AbstractNamedValueMethodArgumentResolver(ConversionService conversionService, | ||||||
| 			@Nullable ConfigurableBeanFactory beanFactory) { | 			@Nullable ConfigurableBeanFactory beanFactory) { | ||||||
| 
 | 
 | ||||||
| 		this.conversionService = (cs != null ? cs : DefaultConversionService.getSharedInstance()); | 		this.conversionService = conversionService; | ||||||
| 		this.configurableBeanFactory = beanFactory; | 		this.configurableBeanFactory = beanFactory; | ||||||
| 		this.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, null) : null); | 		this.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, null) : null); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	@Override | 	@Override | ||||||
| 	@Nullable |  | ||||||
| 	public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception { | 	public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception { | ||||||
|  | 
 | ||||||
| 		NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); | 		NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); | ||||||
| 		MethodParameter nestedParameter = parameter.nestedIfOptional(); | 		MethodParameter nestedParameter = parameter.nestedIfOptional(); | ||||||
| 
 | 
 | ||||||
| 		Object resolvedName = resolveStringValue(namedValueInfo.name); | 		Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name); | ||||||
| 		if (resolvedName == null) { | 		if (resolvedName == null) { | ||||||
| 			throw new IllegalArgumentException( | 			throw new IllegalArgumentException( | ||||||
| 					"Specified name must not resolve to null: [" + namedValueInfo.name + "]"); | 					"Specified name must not resolve to null: [" + namedValueInfo.name + "]"); | ||||||
|  | @ -100,7 +96,7 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle | ||||||
| 		Object arg = resolveArgumentInternal(nestedParameter, message, resolvedName.toString()); | 		Object arg = resolveArgumentInternal(nestedParameter, message, resolvedName.toString()); | ||||||
| 		if (arg == null) { | 		if (arg == null) { | ||||||
| 			if (namedValueInfo.defaultValue != null) { | 			if (namedValueInfo.defaultValue != null) { | ||||||
| 				arg = resolveStringValue(namedValueInfo.defaultValue); | 				arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); | ||||||
| 			} | 			} | ||||||
| 			else if (namedValueInfo.required && !nestedParameter.isOptional()) { | 			else if (namedValueInfo.required && !nestedParameter.isOptional()) { | ||||||
| 				handleMissingValue(namedValueInfo.name, nestedParameter, message); | 				handleMissingValue(namedValueInfo.name, nestedParameter, message); | ||||||
|  | @ -108,7 +104,7 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle | ||||||
| 			arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); | 			arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); | ||||||
| 		} | 		} | ||||||
| 		else if ("".equals(arg) && namedValueInfo.defaultValue != null) { | 		else if ("".equals(arg) && namedValueInfo.defaultValue != null) { | ||||||
| 			arg = resolveStringValue(namedValueInfo.defaultValue); | 			arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if (parameter != nestedParameter || !ClassUtils.isAssignableValue(parameter.getParameterType(), arg)) { | 		if (parameter != nestedParameter || !ClassUtils.isAssignableValue(parameter.getParameterType(), arg)) { | ||||||
|  | @ -134,35 +130,40 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * Create the {@link NamedValueInfo} object for the given method parameter. Implementations typically | 	 * Create the {@link NamedValueInfo} object for the given method parameter. | ||||||
| 	 * retrieve the method annotation by means of {@link MethodParameter#getParameterAnnotation(Class)}. | 	 * Implementations typically retrieve the method annotation by means of | ||||||
|  | 	 * {@link MethodParameter#getParameterAnnotation(Class)}. | ||||||
| 	 * @param parameter the method parameter | 	 * @param parameter the method parameter | ||||||
| 	 * @return the named value information | 	 * @return the named value information | ||||||
| 	 */ | 	 */ | ||||||
| 	protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter); | 	protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter); | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * Create a new NamedValueInfo based on the given NamedValueInfo with sanitized values. | 	 * 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) { | 	private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) { | ||||||
| 		String name = info.name; | 		String name = info.name; | ||||||
| 		if (info.name.isEmpty()) { | 		if (info.name.isEmpty()) { | ||||||
| 			name = parameter.getParameterName(); | 			name = parameter.getParameterName(); | ||||||
| 			if (name == null) { | 			if (name == null) { | ||||||
| 				throw new IllegalArgumentException("Name for argument type [" + parameter.getParameterType().getName() + | 				Class<?> type = parameter.getParameterType(); | ||||||
| 						"] not available, and parameter name information not found in class file either."); | 				throw new IllegalArgumentException( | ||||||
|  | 						"Name for argument of type [" + type.getName() + "] not specified, " + | ||||||
|  | 								"and parameter name information not found in class file either."); | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue); | 		return new NamedValueInfo(name, info.required, | ||||||
| 		return new NamedValueInfo(name, info.required, defaultValue); | 				ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * Resolve the given annotation-specified value, | 	 * Resolve the given annotation-specified value, | ||||||
| 	 * potentially containing placeholders and expressions. | 	 * potentially containing placeholders and expressions. | ||||||
| 	 */ | 	 */ | ||||||
| 	private Object resolveStringValue(String value) { | 	@Nullable | ||||||
| 		if (this.configurableBeanFactory == null) { | 	private Object resolveEmbeddedValuesAndExpressions(String value) { | ||||||
|  | 		if (this.configurableBeanFactory == null || this.expressionContext == null) { | ||||||
| 			return value; | 			return value; | ||||||
| 		} | 		} | ||||||
| 		String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value); | 		String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value); | ||||||
|  | @ -186,19 +187,21 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle | ||||||
| 			throws Exception; | 			throws Exception; | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * Invoked when a named value is required, but | 	 * Invoked when a value is required, but {@link #resolveArgumentInternal} | ||||||
| 	 * {@link #resolveArgumentInternal(MethodParameter, Message, String)} returned {@code null} and | 	 * returned {@code null} and there is no default value. Sub-classes can | ||||||
| 	 * there is no default value. Subclasses typically throw an exception in this case. | 	 * throw an appropriate exception for this case. | ||||||
| 	 * @param name the name for the value | 	 * @param name the name for the value | ||||||
| 	 * @param parameter the method parameter | 	 * @param parameter the target method parameter | ||||||
| 	 * @param message the message being processed | 	 * @param message the message being processed | ||||||
| 	 */ | 	 */ | ||||||
| 	protected abstract void handleMissingValue(String name, MethodParameter parameter, Message<?> message); | 	protected abstract void handleMissingValue(String name, MethodParameter parameter, Message<?> message); | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * A {@code null} results in a {@code false} value for {@code boolean}s or an | 	 * One last chance to handle a possible null value. | ||||||
| 	 * exception for other primitives. | 	 * 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) { | 	private Object handleNullValue(String name, @Nullable Object value, Class<?> paramType) { | ||||||
| 		if (value == null) { | 		if (value == null) { | ||||||
| 			if (Boolean.TYPE.equals(paramType)) { | 			if (Boolean.TYPE.equals(paramType)) { | ||||||
|  | @ -221,13 +224,13 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle | ||||||
| 	 * @param parameter the argument parameter type | 	 * @param parameter the argument parameter type | ||||||
| 	 * @param message the message | 	 * @param message the message | ||||||
| 	 */ | 	 */ | ||||||
| 	protected void handleResolvedValue(Object arg, String name, MethodParameter parameter, Message<?> message) { | 	protected void handleResolvedValue( | ||||||
|  | 			@Nullable Object arg, String name, MethodParameter parameter, Message<?> message) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * Represents the information about a named value, including name, whether it's | 	 * Represents a named value declaration. | ||||||
| 	 * required and a default value. |  | ||||||
| 	 */ | 	 */ | ||||||
| 	protected static class NamedValueInfo { | 	protected static class NamedValueInfo { | ||||||
| 
 | 
 | ||||||
|  | @ -235,9 +238,10 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle | ||||||
| 
 | 
 | ||||||
| 		private final boolean required; | 		private final boolean required; | ||||||
| 
 | 
 | ||||||
|  | 		@Nullable | ||||||
| 		private final String defaultValue; | 		private final String defaultValue; | ||||||
| 
 | 
 | ||||||
| 		protected NamedValueInfo(String name, boolean required, String defaultValue) { | 		protected NamedValueInfo(String name, boolean required, @Nullable String defaultValue) { | ||||||
| 			this.name = name; | 			this.name = name; | ||||||
| 			this.required = required; | 			this.required = required; | ||||||
| 			this.defaultValue = defaultValue; | 			this.defaultValue = defaultValue; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2018 the original author or authors. |  * Copyright 2002-2019 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -26,11 +26,13 @@ import org.springframework.beans.factory.InitializingBean; | ||||||
| import org.springframework.beans.factory.config.ConfigurableBeanFactory; | import org.springframework.beans.factory.config.ConfigurableBeanFactory; | ||||||
| import org.springframework.core.convert.ConversionService; | import org.springframework.core.convert.ConversionService; | ||||||
| import org.springframework.format.support.DefaultFormattingConversionService; | import org.springframework.format.support.DefaultFormattingConversionService; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
| import org.springframework.messaging.converter.GenericMessageConverter; | import org.springframework.messaging.converter.GenericMessageConverter; | ||||||
| import org.springframework.messaging.converter.MessageConverter; | import org.springframework.messaging.converter.MessageConverter; | ||||||
| import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; | import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; | ||||||
| import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolverComposite; | import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolverComposite; | ||||||
| import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; | import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; | ||||||
|  | import org.springframework.util.Assert; | ||||||
| import org.springframework.validation.Validator; | import org.springframework.validation.Validator; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -60,15 +62,19 @@ public class DefaultMessageHandlerMethodFactory | ||||||
| 
 | 
 | ||||||
| 	private ConversionService conversionService = new DefaultFormattingConversionService(); | 	private ConversionService conversionService = new DefaultFormattingConversionService(); | ||||||
| 
 | 
 | ||||||
|  | 	@Nullable | ||||||
| 	private MessageConverter messageConverter; | 	private MessageConverter messageConverter; | ||||||
| 
 | 
 | ||||||
|  | 	@Nullable | ||||||
| 	private Validator validator; | 	private Validator validator; | ||||||
| 
 | 
 | ||||||
|  | 	@Nullable | ||||||
| 	private List<HandlerMethodArgumentResolver> customArgumentResolvers; | 	private List<HandlerMethodArgumentResolver> customArgumentResolvers; | ||||||
| 
 | 
 | ||||||
| 	private final HandlerMethodArgumentResolverComposite argumentResolvers = | 	private final HandlerMethodArgumentResolverComposite argumentResolvers = | ||||||
| 			new HandlerMethodArgumentResolverComposite(); | 			new HandlerMethodArgumentResolverComposite(); | ||||||
| 
 | 
 | ||||||
|  | 	@Nullable | ||||||
| 	private BeanFactory beanFactory; | 	private BeanFactory beanFactory; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -114,6 +120,7 @@ public class DefaultMessageHandlerMethodFactory | ||||||
| 	 * the ones configured by default. This is an advanced option. For most use cases | 	 * the ones configured by default. This is an advanced option. For most use cases | ||||||
| 	 * it should be sufficient to use {@link #setCustomArgumentResolvers(java.util.List)}. | 	 * it should be sufficient to use {@link #setCustomArgumentResolvers(java.util.List)}. | ||||||
| 	 */ | 	 */ | ||||||
|  | 	@SuppressWarnings("ConstantConditions") | ||||||
| 	public void setArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { | 	public void setArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { | ||||||
| 		if (argumentResolvers == null) { | 		if (argumentResolvers == null) { | ||||||
| 			this.argumentResolvers.clear(); | 			this.argumentResolvers.clear(); | ||||||
|  | @ -151,11 +158,11 @@ public class DefaultMessageHandlerMethodFactory | ||||||
| 
 | 
 | ||||||
| 	protected List<HandlerMethodArgumentResolver> initArgumentResolvers() { | 	protected List<HandlerMethodArgumentResolver> initArgumentResolvers() { | ||||||
| 		List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(); | 		List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(); | ||||||
| 		ConfigurableBeanFactory cbf = (this.beanFactory instanceof ConfigurableBeanFactory ? | 		ConfigurableBeanFactory beanFactory = (this.beanFactory instanceof ConfigurableBeanFactory ? | ||||||
| 				(ConfigurableBeanFactory) this.beanFactory : null); | 				(ConfigurableBeanFactory) this.beanFactory : null); | ||||||
| 
 | 
 | ||||||
| 		// Annotation-based argument resolution | 		// Annotation-based argument resolution | ||||||
| 		resolvers.add(new HeaderMethodArgumentResolver(this.conversionService, cbf)); | 		resolvers.add(new HeaderMethodArgumentResolver(this.conversionService, beanFactory)); | ||||||
| 		resolvers.add(new HeadersMethodArgumentResolver()); | 		resolvers.add(new HeadersMethodArgumentResolver()); | ||||||
| 
 | 
 | ||||||
| 		// Type-based argument resolution | 		// Type-based argument resolution | ||||||
|  | @ -164,6 +171,8 @@ public class DefaultMessageHandlerMethodFactory | ||||||
| 		if (this.customArgumentResolvers != null) { | 		if (this.customArgumentResolvers != null) { | ||||||
| 			resolvers.addAll(this.customArgumentResolvers); | 			resolvers.addAll(this.customArgumentResolvers); | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
|  | 		Assert.notNull(this.messageConverter, "MessageConverter not configured"); | ||||||
| 		resolvers.add(new PayloadArgumentResolver(this.messageConverter, this.validator)); | 		resolvers.add(new PayloadArgumentResolver(this.messageConverter, this.validator)); | ||||||
| 
 | 
 | ||||||
| 		return resolvers; | 		return resolvers; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2018 the original author or authors. |  * Copyright 2002-2019 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -23,28 +23,26 @@ import org.springframework.core.convert.ConversionService; | ||||||
| import org.springframework.lang.Nullable; | import org.springframework.lang.Nullable; | ||||||
| import org.springframework.messaging.Message; | import org.springframework.messaging.Message; | ||||||
| import org.springframework.messaging.MessageHandlingException; | import org.springframework.messaging.MessageHandlingException; | ||||||
|  | import org.springframework.messaging.MessageHeaders; | ||||||
| import org.springframework.messaging.handler.annotation.DestinationVariable; | import org.springframework.messaging.handler.annotation.DestinationVariable; | ||||||
| import org.springframework.messaging.handler.annotation.ValueConstants; | import org.springframework.messaging.handler.annotation.ValueConstants; | ||||||
| import org.springframework.util.Assert; | import org.springframework.util.Assert; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Resolves method parameters annotated with |  * Resolve for {@link DestinationVariable @DestinationVariable} method parameters. | ||||||
|  * {@link org.springframework.messaging.handler.annotation.DestinationVariable @DestinationVariable}. |  | ||||||
|  * |  * | ||||||
|  * @author Brian Clozel |  * @author Brian Clozel | ||||||
|  * @since 4.0 |  * @since 4.0 | ||||||
|  */ |  */ | ||||||
| public class DestinationVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { | public class DestinationVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** The name of the header used to for template variables. */ | ||||||
| 	 * The name of the header used to for template variables. |  | ||||||
| 	 */ |  | ||||||
| 	public static final String DESTINATION_TEMPLATE_VARIABLES_HEADER = | 	public static final String DESTINATION_TEMPLATE_VARIABLES_HEADER = | ||||||
| 			DestinationVariableMethodArgumentResolver.class.getSimpleName() + ".templateVariables"; | 			DestinationVariableMethodArgumentResolver.class.getSimpleName() + ".templateVariables"; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	public DestinationVariableMethodArgumentResolver(ConversionService cs) { | 	public DestinationVariableMethodArgumentResolver(ConversionService conversionService) { | ||||||
| 		super(cs, null); | 		super(conversionService, null); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -55,26 +53,24 @@ public class DestinationVariableMethodArgumentResolver extends AbstractNamedValu | ||||||
| 
 | 
 | ||||||
| 	@Override | 	@Override | ||||||
| 	protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { | 	protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { | ||||||
| 		DestinationVariable annotation = parameter.getParameterAnnotation(DestinationVariable.class); | 		DestinationVariable annot = parameter.getParameterAnnotation(DestinationVariable.class); | ||||||
| 		Assert.state(annotation != null, "No DestinationVariable annotation"); | 		Assert.state(annot != null, "No DestinationVariable annotation"); | ||||||
| 		return new DestinationVariableNamedValueInfo(annotation); | 		return new DestinationVariableNamedValueInfo(annot); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Override | 	@Override | ||||||
| 	@Nullable | 	@Nullable | ||||||
| 	protected Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name) |  | ||||||
| 			throws Exception { |  | ||||||
| 
 |  | ||||||
| 	@SuppressWarnings("unchecked") | 	@SuppressWarnings("unchecked") | ||||||
| 		Map<String, String> vars = | 	protected Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name) { | ||||||
| 				(Map<String, String>) message.getHeaders().get(DESTINATION_TEMPLATE_VARIABLES_HEADER); | 		MessageHeaders headers = message.getHeaders(); | ||||||
| 		return (vars != null ? vars.get(name) : null); | 		Map<String, String> vars = (Map<String, String>) headers.get(DESTINATION_TEMPLATE_VARIABLES_HEADER); | ||||||
|  | 		return vars != null ? vars.get(name) : null; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Override | 	@Override | ||||||
| 	protected void handleMissingValue(String name, MethodParameter parameter, Message<?> message) { | 	protected void handleMissingValue(String name, MethodParameter parameter, Message<?> message) { | ||||||
| 		throw new MessageHandlingException(message, "Missing path template variable '" + name + | 		throw new MessageHandlingException(message, "Missing path template variable '" + name + "' " + | ||||||
| 				"' for method parameter type [" + parameter.getParameterType() + "]"); | 				"for method parameter type [" + parameter.getParameterType() + "]"); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2018 the original author or authors. |  * Copyright 2002-2019 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -33,18 +33,25 @@ import org.springframework.messaging.support.NativeMessageHeaderAccessor; | ||||||
| import org.springframework.util.Assert; | import org.springframework.util.Assert; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Resolves method parameters annotated with {@link Header @Header}. |  * 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 |  * @author Rossen Stoyanchev | ||||||
|  * @since 4.0 |  * @since 4.0 | ||||||
|  |  * | ||||||
|  |  * @see HeadersMethodArgumentResolver | ||||||
|  |  * @see NativeMessageHeaderAccessor | ||||||
|  */ |  */ | ||||||
| public class HeaderMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { | public class HeaderMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { | ||||||
| 
 | 
 | ||||||
| 	private static final Log logger = LogFactory.getLog(HeaderMethodArgumentResolver.class); | 	private static final Log logger = LogFactory.getLog(HeaderMethodArgumentResolver.class); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	public HeaderMethodArgumentResolver(ConversionService cs, ConfigurableBeanFactory beanFactory) { | 	public HeaderMethodArgumentResolver( | ||||||
| 		super(cs, beanFactory); | 			ConversionService conversionService, @Nullable ConfigurableBeanFactory beanFactory) { | ||||||
|  | 
 | ||||||
|  | 		super(conversionService, beanFactory); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -55,9 +62,9 @@ public class HeaderMethodArgumentResolver extends AbstractNamedValueMethodArgume | ||||||
| 
 | 
 | ||||||
| 	@Override | 	@Override | ||||||
| 	protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { | 	protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { | ||||||
| 		Header annotation = parameter.getParameterAnnotation(Header.class); | 		Header annot = parameter.getParameterAnnotation(Header.class); | ||||||
| 		Assert.state(annotation != null, "No Header annotation"); | 		Assert.state(annot != null, "No Header annotation"); | ||||||
| 		return new HeaderNamedValueInfo(annotation); | 		return new HeaderNamedValueInfo(annot); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Override | 	@Override | ||||||
|  | @ -70,10 +77,9 @@ public class HeaderMethodArgumentResolver extends AbstractNamedValueMethodArgume | ||||||
| 
 | 
 | ||||||
| 		if (headerValue != null && nativeHeaderValue != null) { | 		if (headerValue != null && nativeHeaderValue != null) { | ||||||
| 			if (logger.isDebugEnabled()) { | 			if (logger.isDebugEnabled()) { | ||||||
| 				logger.debug("Message headers contain two values for the same header '" + name + "', " + | 				logger.debug("A value was found for '" + name + "', in both the top level header map " + | ||||||
| 						"one in the top level header map and a second in the nested map with native headers. " + | 						"and also in the nested map for native headers. Using the value from top level map. " + | ||||||
| 						"Using the value from top level map. " + | 						"Use 'nativeHeader.myHeader' to resolve the native header."); | ||||||
| 						"Use 'nativeHeader.myHeader' to resolve to the value from the nested native header map."); |  | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | @ -94,9 +100,9 @@ public class HeaderMethodArgumentResolver extends AbstractNamedValueMethodArgume | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@SuppressWarnings("unchecked") | 	@SuppressWarnings("unchecked") | ||||||
|  | 	@Nullable | ||||||
| 	private Map<String, List<String>> getNativeHeaders(Message<?> message) { | 	private Map<String, List<String>> getNativeHeaders(Message<?> message) { | ||||||
| 		return (Map<String, List<String>>) message.getHeaders().get( | 		return (Map<String, List<String>>) message.getHeaders().get(NativeMessageHeaderAccessor.NATIVE_HEADERS); | ||||||
| 				NativeMessageHeaderAccessor.NATIVE_HEADERS); |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Override | 	@Override | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2018 the original author or authors. |  * Copyright 2002-2019 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -29,12 +29,11 @@ import org.springframework.messaging.support.MessageHeaderAccessor; | ||||||
| import org.springframework.util.ReflectionUtils; | import org.springframework.util.ReflectionUtils; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * {@link HandlerMethodArgumentResolver} for header method parameters. Resolves the |  * Argument resolver for headers. Resolves the following method parameters: | ||||||
|  * following method parameters: |  | ||||||
|  * <ul> |  * <ul> | ||||||
|  * <li>Parameters assignable to {@link Map} annotated with {@link Headers @Headers} |  * <li>{@link Headers @Headers} {@link Map} | ||||||
|  * <li>Parameters of type {@link MessageHeaders} |  * <li>{@link MessageHeaders} | ||||||
|  * <li>Parameters assignable to {@link MessageHeaderAccessor} |  * <li>{@link MessageHeaderAccessor} | ||||||
|  * </ul> |  * </ul> | ||||||
|  * |  * | ||||||
|  * @author Rossen Stoyanchev |  * @author Rossen Stoyanchev | ||||||
|  | @ -58,7 +57,7 @@ public class HeadersMethodArgumentResolver implements HandlerMethodArgumentResol | ||||||
| 		} | 		} | ||||||
| 		else if (MessageHeaderAccessor.class == paramType) { | 		else if (MessageHeaderAccessor.class == paramType) { | ||||||
| 			MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class); | 			MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class); | ||||||
| 			return (accessor != null ? accessor : new MessageHeaderAccessor(message)); | 			return accessor != null ? accessor : new MessageHeaderAccessor(message); | ||||||
| 		} | 		} | ||||||
| 		else if (MessageHeaderAccessor.class.isAssignableFrom(paramType)) { | 		else if (MessageHeaderAccessor.class.isAssignableFrom(paramType)) { | ||||||
| 			MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class); | 			MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class); | ||||||
|  | @ -75,9 +74,8 @@ public class HeadersMethodArgumentResolver implements HandlerMethodArgumentResol | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		else { | 		else { | ||||||
| 			throw new IllegalStateException( | 			throw new IllegalStateException("Unexpected parameter of type " + paramType + | ||||||
| 					"Unexpected method parameter type " + paramType + "in method " + parameter.getMethod() + ". " | 					" in method " + parameter.getMethod() + ". "); | ||||||
| 					+ "@Headers method arguments must be assignable to java.util.Map."); |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2018 the original author or authors. |  * Copyright 2002-2019 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -43,6 +43,7 @@ import org.springframework.util.StringUtils; | ||||||
|  */ |  */ | ||||||
| public class MessageMethodArgumentResolver implements HandlerMethodArgumentResolver { | public class MessageMethodArgumentResolver implements HandlerMethodArgumentResolver { | ||||||
| 
 | 
 | ||||||
|  | 	@Nullable | ||||||
| 	private final MessageConverter converter; | 	private final MessageConverter converter; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -55,6 +55,7 @@ public class PayloadArgumentResolver implements HandlerMethodArgumentResolver { | ||||||
| 
 | 
 | ||||||
| 	private final MessageConverter converter; | 	private final MessageConverter converter; | ||||||
| 
 | 
 | ||||||
|  | 	@Nullable | ||||||
| 	private final Validator validator; | 	private final Validator validator; | ||||||
| 
 | 
 | ||||||
| 	private final boolean useDefaultResolution; | 	private final boolean useDefaultResolution; | ||||||
|  | @ -76,7 +77,7 @@ public class PayloadArgumentResolver implements HandlerMethodArgumentResolver { | ||||||
| 	 * @param messageConverter the MessageConverter to use (required) | 	 * @param messageConverter the MessageConverter to use (required) | ||||||
| 	 * @param validator the Validator to use (optional) | 	 * @param validator the Validator to use (optional) | ||||||
| 	 */ | 	 */ | ||||||
| 	public PayloadArgumentResolver(MessageConverter messageConverter, Validator validator) { | 	public PayloadArgumentResolver(MessageConverter messageConverter, @Nullable Validator validator) { | ||||||
| 		this(messageConverter, validator, true); | 		this(messageConverter, validator, true); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -89,7 +90,7 @@ public class PayloadArgumentResolver implements HandlerMethodArgumentResolver { | ||||||
| 	 * all parameters; if "false" then only arguments with the {@code @Payload} | 	 * all parameters; if "false" then only arguments with the {@code @Payload} | ||||||
| 	 * annotation are supported. | 	 * annotation are supported. | ||||||
| 	 */ | 	 */ | ||||||
| 	public PayloadArgumentResolver(MessageConverter messageConverter, Validator validator, | 	public PayloadArgumentResolver(MessageConverter messageConverter, @Nullable Validator validator, | ||||||
| 			boolean useDefaultResolution) { | 			boolean useDefaultResolution) { | ||||||
| 
 | 
 | ||||||
| 		Assert.notNull(messageConverter, "MessageConverter must not be null"); | 		Assert.notNull(messageConverter, "MessageConverter must not be null"); | ||||||
|  |  | ||||||
|  | @ -1,4 +1,9 @@ | ||||||
| /** | /** | ||||||
|  * Support classes for working with annotated message-handling methods. |  * Support classes for working with annotated message-handling methods. | ||||||
|  */ |  */ | ||||||
|  | @NonNullApi | ||||||
|  | @NonNullFields | ||||||
| package org.springframework.messaging.handler.annotation.support; | package org.springframework.messaging.handler.annotation.support; | ||||||
|  | 
 | ||||||
|  | import org.springframework.lang.NonNullApi; | ||||||
|  | import org.springframework.lang.NonNullFields; | ||||||
|  |  | ||||||
|  | @ -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; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -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); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -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<String, List<String>> 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<String, List<String>> getNativeHeaders(Message<?> message) { | ||||||
|  | 		return (Map<String, List<String>>) 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()); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -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: | ||||||
|  |  * <ul> | ||||||
|  |  * <li>{@link Headers @Headers} {@link Map} | ||||||
|  |  * <li>{@link MessageHeaders} | ||||||
|  |  * <li>{@link MessageHeaderAccessor} | ||||||
|  |  * </ul> | ||||||
|  |  * | ||||||
|  |  * @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() + ". "); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,313 @@ | ||||||
|  | /* | ||||||
|  |  * 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.AnnotatedElement; | ||||||
|  | import java.lang.reflect.Method; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.Comparator; | ||||||
|  | import java.util.LinkedHashSet; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Set; | ||||||
|  | import java.util.function.Predicate; | ||||||
|  | 
 | ||||||
|  | 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; | ||||||
|  | import org.springframework.messaging.handler.DestinationPatternsMessageCondition; | ||||||
|  | import org.springframework.messaging.handler.annotation.MessageMapping; | ||||||
|  | import org.springframework.messaging.handler.annotation.support.AnnotationExceptionHandlerMethodResolver; | ||||||
|  | import org.springframework.messaging.handler.invocation.AbstractExceptionHandlerMethodResolver; | ||||||
|  | import org.springframework.messaging.handler.invocation.reactive.AbstractEncoderMethodReturnValueHandler; | ||||||
|  | import org.springframework.messaging.handler.invocation.reactive.AbstractMethodMessageHandler; | ||||||
|  | import org.springframework.messaging.handler.invocation.reactive.HandlerMethodArgumentResolver; | ||||||
|  | import org.springframework.messaging.handler.invocation.reactive.HandlerMethodReturnValueHandler; | ||||||
|  | import org.springframework.stereotype.Controller; | ||||||
|  | import org.springframework.util.AntPathMatcher; | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | import org.springframework.util.PathMatcher; | ||||||
|  | import org.springframework.util.StringValueResolver; | ||||||
|  | import org.springframework.validation.Validator; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Extension of {@link AbstractMethodMessageHandler} for reactive, non-blocking | ||||||
|  |  * handling of messages via {@link MessageMapping @MessageMapping} methods. | ||||||
|  |  * By default such methods are detected in {@code @Controller} Spring beans but | ||||||
|  |  * that can be changed via {@link #setHandlerPredicate(Predicate)}. | ||||||
|  |  * | ||||||
|  |  * <p>Payloads for incoming messages are decoded through the configured | ||||||
|  |  * {@link #setDecoders(List)} decoders, with the help of | ||||||
|  |  * {@link PayloadMethodArgumentResolver}. | ||||||
|  |  * | ||||||
|  |  * <p>There is no default handling for return values but | ||||||
|  |  * {@link #setReturnValueHandlerConfigurer} can be used to configure custom | ||||||
|  |  * return value handlers. Sub-classes may also override | ||||||
|  |  * {@link #initReturnValueHandlers()} to set up default return value handlers. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  * @see AbstractEncoderMethodReturnValueHandler | ||||||
|  |  */ | ||||||
|  | public class MessageMappingMessageHandler extends AbstractMethodMessageHandler<CompositeMessageCondition> | ||||||
|  | 		implements EmbeddedValueResolverAware { | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	private Predicate<Class<?>> handlerPredicate = | ||||||
|  | 			beanType -> AnnotatedElementUtils.hasAnnotation(beanType, Controller.class); | ||||||
|  | 
 | ||||||
|  | 	private final List<Decoder<?>> decoders = new ArrayList<>(); | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	private Validator validator; | ||||||
|  | 
 | ||||||
|  | 	private PathMatcher pathMatcher; | ||||||
|  | 
 | ||||||
|  | 	private ConversionService conversionService = new DefaultFormattingConversionService(); | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	private StringValueResolver valueResolver; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	public MessageMappingMessageHandler() { | ||||||
|  | 		this.pathMatcher = new AntPathMatcher(); | ||||||
|  | 		((AntPathMatcher) this.pathMatcher).setPathSeparator("."); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Manually configure handlers to check for {@code @MessageMapping} methods. | ||||||
|  | 	 * <p><strong>Note:</strong> the given handlers are not required to be | ||||||
|  | 	 * annotated with {@code @Controller}. Consider also using | ||||||
|  | 	 * {@link #setAutoDetectDisabled()} if the intent is to use these handlers | ||||||
|  | 	 * instead of, and not in addition to {@code @Controller} classes. Or | ||||||
|  | 	 * alternatively use {@link #setHandlerPredicate(Predicate)} to select a | ||||||
|  | 	 * different set of beans based on a different criteria. | ||||||
|  | 	 * @param handlers the handlers to register | ||||||
|  | 	 * @see #setAutoDetectDisabled() | ||||||
|  | 	 * @see #setHandlerPredicate(Predicate) | ||||||
|  | 	 */ | ||||||
|  | 	public void setHandlers(List<Object> handlers) { | ||||||
|  | 		for (Object handler : handlers) { | ||||||
|  | 			detectHandlerMethods(handler); | ||||||
|  | 		} | ||||||
|  | 		// Disable auto-detection.. | ||||||
|  | 		this.handlerPredicate = null; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Configure the predicate to use for selecting which Spring beans to check | ||||||
|  | 	 * for {@code @MessageMapping} methods. When set to {@code null}, | ||||||
|  | 	 * auto-detection is turned off which is what | ||||||
|  | 	 * {@link #setAutoDetectDisabled()} does internally. | ||||||
|  | 	 * <p>The predicate used by default selects {@code @Controller} classes. | ||||||
|  | 	 * @see #setHandlers(List) | ||||||
|  | 	 * @see #setAutoDetectDisabled() | ||||||
|  | 	 */ | ||||||
|  | 	public void setHandlerPredicate(@Nullable Predicate<Class<?>> handlerPredicate) { | ||||||
|  | 		this.handlerPredicate = handlerPredicate; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the {@link #setHandlerPredicate configured} handler predicate. | ||||||
|  | 	 */ | ||||||
|  | 	@Nullable | ||||||
|  | 	public Predicate<Class<?>> getHandlerPredicate() { | ||||||
|  | 		return this.handlerPredicate; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Disable auto-detection of {@code @MessageMapping} methods, e.g. in | ||||||
|  | 	 * {@code @Controller}s, by setting {@link #setHandlerPredicate(Predicate) | ||||||
|  | 	 * setHandlerPredicate(null)}. | ||||||
|  | 	 */ | ||||||
|  | 	public void setAutoDetectDisabled() { | ||||||
|  | 		this.handlerPredicate = null; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Configure the decoders to use for incoming payloads. | ||||||
|  | 	 */ | ||||||
|  | 	public void setDecoders(List<? extends Decoder<?>> decoders) { | ||||||
|  | 		this.decoders.addAll(decoders); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the configured decoders. | ||||||
|  | 	 */ | ||||||
|  | 	public List<? extends Decoder<?>> getDecoders() { | ||||||
|  | 		return this.decoders; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Set the Validator instance used for validating {@code @Payload} arguments. | ||||||
|  | 	 * @see org.springframework.validation.annotation.Validated | ||||||
|  | 	 * @see PayloadMethodArgumentResolver | ||||||
|  | 	 */ | ||||||
|  | 	public void setValidator(@Nullable Validator validator) { | ||||||
|  | 		this.validator = validator; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the configured Validator instance. | ||||||
|  | 	 */ | ||||||
|  | 	@Nullable | ||||||
|  | 	public Validator getValidator() { | ||||||
|  | 		return this.validator; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Set the PathMatcher implementation to use for matching destinations | ||||||
|  | 	 * against configured destination patterns. | ||||||
|  | 	 * <p>By default, {@link AntPathMatcher} is used with separator set to ".". | ||||||
|  | 	 */ | ||||||
|  | 	public void setPathMatcher(PathMatcher pathMatcher) { | ||||||
|  | 		Assert.notNull(pathMatcher, "PathMatcher must not be null"); | ||||||
|  | 		this.pathMatcher = pathMatcher; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the PathMatcher implementation to use for matching destinations. | ||||||
|  | 	 */ | ||||||
|  | 	public PathMatcher getPathMatcher() { | ||||||
|  | 		return this.pathMatcher; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Configure a {@link ConversionService} to use for type conversion of | ||||||
|  | 	 * String based values, e.g. in destination variables or headers. | ||||||
|  | 	 * <p>By 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; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	protected List<? extends HandlerMethodArgumentResolver> initArgumentResolvers() { | ||||||
|  | 		List<HandlerMethodArgumentResolver> 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()); | ||||||
|  | 
 | ||||||
|  | 		// Catch-all | ||||||
|  | 		resolvers.add(new PayloadMethodArgumentResolver( | ||||||
|  | 				this.decoders, this.validator, getReactiveAdapterRegistry(), true)); | ||||||
|  | 
 | ||||||
|  | 		return resolvers; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	protected List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers() { | ||||||
|  | 		return Collections.emptyList(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	protected Predicate<Class<?>> initHandlerPredicate() { | ||||||
|  | 		return this.handlerPredicate; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	protected CompositeMessageCondition getMappingForMethod(Method method, Class<?> handlerType) { | ||||||
|  | 		CompositeMessageCondition methodCondition = getCondition(method); | ||||||
|  | 		if (methodCondition != null) { | ||||||
|  | 			CompositeMessageCondition typeCondition = getCondition(handlerType); | ||||||
|  | 			if (typeCondition != null) { | ||||||
|  | 				return typeCondition.combine(methodCondition); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return methodCondition; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	private CompositeMessageCondition getCondition(AnnotatedElement element) { | ||||||
|  | 		MessageMapping annot = AnnotatedElementUtils.findMergedAnnotation(element, MessageMapping.class); | ||||||
|  | 		if (annot == null || annot.value().length == 0) { | ||||||
|  | 			return null; | ||||||
|  | 		} | ||||||
|  | 		String[] destinations = annot.value(); | ||||||
|  | 		if (this.valueResolver != null) { | ||||||
|  | 			destinations = Arrays.stream(annot.value()) | ||||||
|  | 					.map(s -> this.valueResolver.resolveStringValue(s)) | ||||||
|  | 					.toArray(String[]::new); | ||||||
|  | 		} | ||||||
|  | 		return new CompositeMessageCondition(new DestinationPatternsMessageCondition(destinations, this.pathMatcher)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	protected Set<String> getDirectLookupMappings(CompositeMessageCondition mapping) { | ||||||
|  | 		Set<String> result = new LinkedHashSet<>(); | ||||||
|  | 		for (String pattern : mapping.getCondition(DestinationPatternsMessageCondition.class).getPatterns()) { | ||||||
|  | 			if (!this.pathMatcher.isPattern(pattern)) { | ||||||
|  | 				result.add(pattern); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return result; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	protected String getDestination(Message<?> message) { | ||||||
|  | 		return (String) message.getHeaders().get(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	protected CompositeMessageCondition getMatchingMapping(CompositeMessageCondition mapping, Message<?> message) { | ||||||
|  | 		return mapping.getMatchingCondition(message); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	protected Comparator<CompositeMessageCondition> getMappingComparator(Message<?> message) { | ||||||
|  | 		return (info1, info2) -> info1.compareTo(info2, message); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	protected AbstractExceptionHandlerMethodResolver createExceptionMethodResolverFor(Class<?> beanType) { | ||||||
|  | 		return new AnnotationExceptionHandlerMethodResolver(beanType); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,302 @@ | ||||||
|  | /* | ||||||
|  |  * 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.annotation.Annotation; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.function.Consumer; | ||||||
|  | 
 | ||||||
|  | import org.apache.commons.logging.Log; | ||||||
|  | import org.apache.commons.logging.LogFactory; | ||||||
|  | import org.reactivestreams.Publisher; | ||||||
|  | import reactor.core.publisher.Flux; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.Conventions; | ||||||
|  | import org.springframework.core.MethodParameter; | ||||||
|  | import org.springframework.core.ReactiveAdapter; | ||||||
|  | import org.springframework.core.ReactiveAdapterRegistry; | ||||||
|  | import org.springframework.core.ResolvableType; | ||||||
|  | import org.springframework.core.annotation.AnnotationUtils; | ||||||
|  | import org.springframework.core.codec.Decoder; | ||||||
|  | import org.springframework.core.codec.DecodingException; | ||||||
|  | import org.springframework.core.io.buffer.DataBuffer; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | import org.springframework.messaging.MessageHeaders; | ||||||
|  | import org.springframework.messaging.handler.annotation.Payload; | ||||||
|  | import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException; | ||||||
|  | import org.springframework.messaging.handler.invocation.MethodArgumentResolutionException; | ||||||
|  | import org.springframework.messaging.handler.invocation.reactive.HandlerMethodArgumentResolver; | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | import org.springframework.util.CollectionUtils; | ||||||
|  | import org.springframework.util.MimeType; | ||||||
|  | import org.springframework.util.MimeTypeUtils; | ||||||
|  | import org.springframework.util.ObjectUtils; | ||||||
|  | import org.springframework.util.StringUtils; | ||||||
|  | import org.springframework.validation.BeanPropertyBindingResult; | ||||||
|  | import org.springframework.validation.SmartValidator; | ||||||
|  | import org.springframework.validation.Validator; | ||||||
|  | import org.springframework.validation.annotation.Validated; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A resolver to extract and decode the payload of a message using a | ||||||
|  |  * {@link Decoder}, where the payload is expected to be a {@link Publisher} of | ||||||
|  |  * {@link DataBuffer DataBuffer}. | ||||||
|  |  * | ||||||
|  |  * <p>Validation is applied if the method argument is annotated with | ||||||
|  |  * {@code @javax.validation.Valid} or | ||||||
|  |  * {@link org.springframework.validation.annotation.Validated}. Validation | ||||||
|  |  * failure results in an {@link MethodArgumentNotValidException}. | ||||||
|  |  * | ||||||
|  |  * <p>This resolver should be ordered last if {@link #useDefaultResolution} is | ||||||
|  |  * set to {@code true} since in that case it supports all types and does not | ||||||
|  |  * require the presence of {@link Payload}. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | public class PayloadMethodArgumentResolver implements HandlerMethodArgumentResolver { | ||||||
|  | 
 | ||||||
|  | 	protected final Log logger = LogFactory.getLog(getClass()); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private final List<Decoder<?>> decoders; | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	private final Validator validator; | ||||||
|  | 
 | ||||||
|  | 	private final ReactiveAdapterRegistry adapterRegistry; | ||||||
|  | 
 | ||||||
|  | 	private final boolean useDefaultResolution; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	public PayloadMethodArgumentResolver(List<? extends Decoder<?>> decoders, @Nullable Validator validator, | ||||||
|  | 			@Nullable ReactiveAdapterRegistry registry, boolean useDefaultResolution) { | ||||||
|  | 
 | ||||||
|  | 		Assert.isTrue(!CollectionUtils.isEmpty(decoders), "At least one Decoder is required."); | ||||||
|  | 		this.decoders = Collections.unmodifiableList(new ArrayList<>(decoders)); | ||||||
|  | 		this.validator = validator; | ||||||
|  | 		this.adapterRegistry = registry != null ? registry : ReactiveAdapterRegistry.getSharedInstance(); | ||||||
|  | 		this.useDefaultResolution = useDefaultResolution; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return a read-only list of the configured decoders. | ||||||
|  | 	 */ | ||||||
|  | 	public List<Decoder<?>> getDecoders() { | ||||||
|  | 		return this.decoders; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the configured validator, if any. | ||||||
|  | 	 */ | ||||||
|  | 	@Nullable | ||||||
|  | 	public Validator getValidator() { | ||||||
|  | 		return this.validator; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the configured {@link ReactiveAdapterRegistry}. | ||||||
|  | 	 */ | ||||||
|  | 	public ReactiveAdapterRegistry getAdapterRegistry() { | ||||||
|  | 		return this.adapterRegistry; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Whether this resolver is configured to use default resolution, i.e. | ||||||
|  | 	 * works for any argument type regardless of whether {@code @Payload} is | ||||||
|  | 	 * present or not. | ||||||
|  | 	 */ | ||||||
|  | 	public boolean isUseDefaultResolution() { | ||||||
|  | 		return this.useDefaultResolution; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public boolean supportsParameter(MethodParameter parameter) { | ||||||
|  | 		return parameter.hasParameterAnnotation(Payload.class) || this.useDefaultResolution; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Decode the content of the given message payload through a compatible | ||||||
|  | 	 * {@link Decoder}. | ||||||
|  | 	 * | ||||||
|  | 	 * <p>Validation is applied if the method argument is annotated with | ||||||
|  | 	 * {@code @javax.validation.Valid} or | ||||||
|  | 	 * {@link org.springframework.validation.annotation.Validated}. Validation | ||||||
|  | 	 * failure results in an {@link MethodArgumentNotValidException}. | ||||||
|  | 	 * | ||||||
|  | 	 * @param parameter the target method argument that we are decoding to | ||||||
|  | 	 * @param message the message from which the content was extracted | ||||||
|  | 	 * @return a Mono with the result of argument resolution | ||||||
|  | 	 * | ||||||
|  | 	 * @see #extractContent(MethodParameter, Message) | ||||||
|  | 	 * @see #getMimeType(Message) | ||||||
|  | 	 */ | ||||||
|  | 	@Override | ||||||
|  | 	public final Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message) { | ||||||
|  | 
 | ||||||
|  | 		Payload ann = parameter.getParameterAnnotation(Payload.class); | ||||||
|  | 		if (ann != null && StringUtils.hasText(ann.expression())) { | ||||||
|  | 			throw new IllegalStateException("@Payload SpEL expressions not supported by this resolver"); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		MimeType mimeType = getMimeType(message); | ||||||
|  | 		mimeType = mimeType != null ? mimeType : MimeTypeUtils.APPLICATION_OCTET_STREAM; | ||||||
|  | 
 | ||||||
|  | 		Flux<DataBuffer> content = extractContent(parameter, message); | ||||||
|  | 		return decodeContent(parameter, message, ann == null || ann.required(), content, mimeType); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("unchecked") | ||||||
|  | 	private Flux<DataBuffer> extractContent(MethodParameter parameter, Message<?> message) { | ||||||
|  | 		Object payload = message.getPayload(); | ||||||
|  | 		if (payload instanceof DataBuffer) { | ||||||
|  | 			return Flux.just((DataBuffer) payload); | ||||||
|  | 		} | ||||||
|  | 		if (payload instanceof Publisher) { | ||||||
|  | 			return Flux.from((Publisher<?>) payload).map(value -> { | ||||||
|  | 				if (value instanceof DataBuffer) { | ||||||
|  | 					return (DataBuffer) value; | ||||||
|  | 				} | ||||||
|  | 				String className = value.getClass().getName(); | ||||||
|  | 				throw getUnexpectedPayloadError(message, parameter, "Publisher<" + className + ">"); | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 		return Flux.error(getUnexpectedPayloadError(message, parameter, payload.getClass().getName())); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private MethodArgumentResolutionException getUnexpectedPayloadError( | ||||||
|  | 			Message<?> message, MethodParameter parameter, String actualType) { | ||||||
|  | 
 | ||||||
|  | 		return new MethodArgumentResolutionException(message, parameter, | ||||||
|  | 				"Expected DataBuffer or Publisher<DataBuffer> for the Message payload, actual: " + actualType); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the mime type for the content. By default this method checks the | ||||||
|  | 	 * {@link MessageHeaders#CONTENT_TYPE} header expecting to find a | ||||||
|  | 	 * {@link MimeType} value or a String to parse to a {@link MimeType}. | ||||||
|  | 	 * @param message the input message | ||||||
|  | 	 */ | ||||||
|  | 	@Nullable | ||||||
|  | 	protected MimeType getMimeType(Message<?> message) { | ||||||
|  | 		Object headerValue = message.getHeaders().get(MessageHeaders.CONTENT_TYPE); | ||||||
|  | 		if (headerValue == null) { | ||||||
|  | 			return null; | ||||||
|  | 		} | ||||||
|  | 		else if (headerValue instanceof String) { | ||||||
|  | 			return MimeTypeUtils.parseMimeType((String) headerValue); | ||||||
|  | 		} | ||||||
|  | 		else if (headerValue instanceof MimeType) { | ||||||
|  | 			return (MimeType) headerValue; | ||||||
|  | 		} | ||||||
|  | 		else { | ||||||
|  | 			throw new IllegalArgumentException("Unexpected MimeType value: " + headerValue); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private Mono<Object> decodeContent(MethodParameter parameter, Message<?> message, | ||||||
|  | 			boolean isContentRequired, Flux<DataBuffer> content, MimeType mimeType) { | ||||||
|  | 
 | ||||||
|  | 		ResolvableType targetType = ResolvableType.forMethodParameter(parameter); | ||||||
|  | 		Class<?> resolvedType = targetType.resolve(); | ||||||
|  | 		ReactiveAdapter adapter = (resolvedType != null ? getAdapterRegistry().getAdapter(resolvedType) : null); | ||||||
|  | 		ResolvableType elementType = (adapter != null ? targetType.getGeneric() : targetType); | ||||||
|  | 		isContentRequired = isContentRequired || (adapter != null && !adapter.supportsEmpty()); | ||||||
|  | 		Consumer<Object> validator = getValidator(message, parameter); | ||||||
|  | 
 | ||||||
|  | 		Map<String, Object> hints = Collections.emptyMap(); | ||||||
|  | 
 | ||||||
|  | 		for (Decoder<?> decoder : this.decoders) { | ||||||
|  | 			if (decoder.canDecode(elementType, mimeType)) { | ||||||
|  | 				if (adapter != null && adapter.isMultiValue()) { | ||||||
|  | 					Flux<?> flux = content | ||||||
|  | 							.concatMap(buffer -> decoder.decode(Mono.just(buffer), elementType, mimeType, hints)) | ||||||
|  | 							.onErrorResume(ex -> Flux.error(handleReadError(parameter, message, ex))); | ||||||
|  | 					if (isContentRequired) { | ||||||
|  | 						flux = flux.switchIfEmpty(Flux.error(() -> handleMissingBody(parameter, message))); | ||||||
|  | 					} | ||||||
|  | 					if (validator != null) { | ||||||
|  | 						flux = flux.doOnNext(validator::accept); | ||||||
|  | 					} | ||||||
|  | 					return Mono.just(adapter.fromPublisher(flux)); | ||||||
|  | 				} | ||||||
|  | 				else { | ||||||
|  | 					// Single-value (with or without reactive type wrapper) | ||||||
|  | 					Mono<?> mono = decoder | ||||||
|  | 							.decodeToMono(content.next(), targetType, mimeType, hints) | ||||||
|  | 							.onErrorResume(ex -> Mono.error(handleReadError(parameter, message, ex))); | ||||||
|  | 					if (isContentRequired) { | ||||||
|  | 						mono = mono.switchIfEmpty(Mono.error(() -> handleMissingBody(parameter, message))); | ||||||
|  | 					} | ||||||
|  | 					if (validator != null) { | ||||||
|  | 						mono = mono.doOnNext(validator::accept); | ||||||
|  | 					} | ||||||
|  | 					return (adapter != null ? Mono.just(adapter.fromPublisher(mono)) : Mono.from(mono)); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return Mono.error(new MethodArgumentResolutionException( | ||||||
|  | 				message, parameter, "Cannot decode to [" + targetType + "]" + message)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private Throwable handleReadError(MethodParameter parameter, Message<?> message, Throwable ex) { | ||||||
|  | 		return ex instanceof DecodingException ? | ||||||
|  | 				new MethodArgumentResolutionException(message, parameter, "Failed to read HTTP message", ex) : ex; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private MethodArgumentResolutionException handleMissingBody(MethodParameter param, Message<?> message) { | ||||||
|  | 		return new MethodArgumentResolutionException(message, param, | ||||||
|  | 				"Payload content is missing: " + param.getExecutable().toGenericString()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	private Consumer<Object> getValidator(Message<?> message, MethodParameter parameter) { | ||||||
|  | 		if (this.validator == null) { | ||||||
|  | 			return null; | ||||||
|  | 		} | ||||||
|  | 		for (Annotation ann : parameter.getParameterAnnotations()) { | ||||||
|  | 			Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); | ||||||
|  | 			if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { | ||||||
|  | 				Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); | ||||||
|  | 				Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); | ||||||
|  | 				String name = Conventions.getVariableNameForParameter(parameter); | ||||||
|  | 				return target -> { | ||||||
|  | 					BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(target, name); | ||||||
|  | 					if (!ObjectUtils.isEmpty(validationHints) && this.validator instanceof SmartValidator) { | ||||||
|  | 						((SmartValidator) this.validator).validate(target, bindingResult, validationHints); | ||||||
|  | 					} | ||||||
|  | 					else { | ||||||
|  | 						this.validator.validate(target, bindingResult); | ||||||
|  | 					} | ||||||
|  | 					if (bindingResult.hasErrors()) { | ||||||
|  | 						throw new MethodArgumentNotValidException(message, parameter, bindingResult); | ||||||
|  | 					} | ||||||
|  | 				}; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return null; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,10 @@ | ||||||
|  | /** | ||||||
|  |  * Support classes for working with annotated message-handling methods with | ||||||
|  |  * non-blocking, reactive contracts. | ||||||
|  |  */ | ||||||
|  | @NonNullApi | ||||||
|  | @NonNullFields | ||||||
|  | package org.springframework.messaging.handler.annotation.support.reactive; | ||||||
|  | 
 | ||||||
|  | import org.springframework.lang.NonNullApi; | ||||||
|  | import org.springframework.lang.NonNullFields; | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2018 the original author or authors. |  * Copyright 2002-2019 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -85,7 +85,7 @@ public abstract class AbstractExceptionHandlerMethodResolver { | ||||||
| 	 * @return a Method to handle the exception, or {@code null} if none found | 	 * @return a Method to handle the exception, or {@code null} if none found | ||||||
| 	 */ | 	 */ | ||||||
| 	@Nullable | 	@Nullable | ||||||
| 	public Method resolveMethod(Exception exception) { | 	public Method resolveMethod(Throwable exception) { | ||||||
| 		Method method = resolveMethodByExceptionType(exception.getClass()); | 		Method method = resolveMethodByExceptionType(exception.getClass()); | ||||||
| 		if (method == null) { | 		if (method == null) { | ||||||
| 			Throwable cause = exception.getCause(); | 			Throwable cause = exception.getCause(); | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ | ||||||
| package org.springframework.messaging.handler.invocation; | package org.springframework.messaging.handler.invocation; | ||||||
| 
 | 
 | ||||||
| import org.springframework.core.MethodParameter; | import org.springframework.core.MethodParameter; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
| import org.springframework.messaging.Message; | import org.springframework.messaging.Message; | ||||||
| import org.springframework.messaging.MessagingException; | import org.springframework.messaging.MessagingException; | ||||||
| 
 | 
 | ||||||
|  | @ -51,6 +52,17 @@ public class MethodArgumentResolutionException extends MessagingException { | ||||||
| 		this.parameter = parameter; | 		this.parameter = parameter; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Create a new instance providing the invalid {@code MethodParameter}, | ||||||
|  | 	 * prepared description, and a cause. | ||||||
|  | 	 */ | ||||||
|  | 	public MethodArgumentResolutionException( | ||||||
|  | 			Message<?> message, MethodParameter parameter, String description, @Nullable Throwable cause) { | ||||||
|  | 
 | ||||||
|  | 		super(message, getMethodParameterMessage(parameter) + ": " + description, cause); | ||||||
|  | 		this.parameter = parameter; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * Return the MethodParameter that was rejected. | 	 * Return the MethodParameter that was rejected. | ||||||
|  |  | ||||||
|  | @ -0,0 +1,218 @@ | ||||||
|  | /* | ||||||
|  |  * 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 java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
|  | 
 | ||||||
|  | import org.apache.commons.logging.Log; | ||||||
|  | import org.apache.commons.logging.LogFactory; | ||||||
|  | import org.reactivestreams.Publisher; | ||||||
|  | import reactor.core.publisher.Flux; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.MethodParameter; | ||||||
|  | import org.springframework.core.ReactiveAdapter; | ||||||
|  | import org.springframework.core.ReactiveAdapterRegistry; | ||||||
|  | import org.springframework.core.ResolvableType; | ||||||
|  | import org.springframework.core.codec.Encoder; | ||||||
|  | import org.springframework.core.io.buffer.DataBuffer; | ||||||
|  | import org.springframework.core.io.buffer.DataBufferFactory; | ||||||
|  | import org.springframework.core.io.buffer.DataBufferUtils; | ||||||
|  | import org.springframework.core.io.buffer.DefaultDataBufferFactory; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | import org.springframework.messaging.MessageHeaders; | ||||||
|  | import org.springframework.messaging.MessagingException; | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | import org.springframework.util.MimeType; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Base class for a return value handler that encodes return values to | ||||||
|  |  * {@code Flux<DataBuffer>} through the configured {@link Encoder}s. | ||||||
|  |  * | ||||||
|  |  * <p>Sub-classes must implement the abstract method | ||||||
|  |  * {@link #handleEncodedContent} to handle the resulting encoded content. | ||||||
|  |  * | ||||||
|  |  * <p>This handler should be ordered last since its {@link #supportsReturnType} | ||||||
|  |  * returns {@code true} for any method parameter type. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | public abstract class AbstractEncoderMethodReturnValueHandler implements HandlerMethodReturnValueHandler { | ||||||
|  | 
 | ||||||
|  | 	private static final ResolvableType VOID_RESOLVABLE_TYPE = ResolvableType.forClass(Void.class); | ||||||
|  | 
 | ||||||
|  | 	private static final ResolvableType OBJECT_RESOLVABLE_TYPE = ResolvableType.forClass(Object.class); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	protected final Log logger = LogFactory.getLog(getClass()); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private final List<Encoder<?>> encoders; | ||||||
|  | 
 | ||||||
|  | 	private final ReactiveAdapterRegistry adapterRegistry; | ||||||
|  | 
 | ||||||
|  | 	private DataBufferFactory defaultBufferFactory = new DefaultDataBufferFactory(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	protected AbstractEncoderMethodReturnValueHandler(List<Encoder<?>> encoders, ReactiveAdapterRegistry registry) { | ||||||
|  | 		Assert.notEmpty(encoders, "At least one Encoder is required"); | ||||||
|  | 		Assert.notNull(registry, "ReactiveAdapterRegistry is required"); | ||||||
|  | 		this.encoders = Collections.unmodifiableList(encoders); | ||||||
|  | 		this.adapterRegistry = registry; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * The configured encoders. | ||||||
|  | 	 */ | ||||||
|  | 	public List<Encoder<?>> getEncoders() { | ||||||
|  | 		return this.encoders; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * The configured adapter registry. | ||||||
|  | 	 */ | ||||||
|  | 	public ReactiveAdapterRegistry getAdapterRegistry() { | ||||||
|  | 		return this.adapterRegistry; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public boolean supportsReturnType(MethodParameter returnType) { | ||||||
|  | 		// We could check canEncode but we're probably last in order anyway | ||||||
|  | 		return true; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public Mono<Void> handleReturnValue( | ||||||
|  | 			@Nullable Object returnValue, MethodParameter returnType, Message<?> message) { | ||||||
|  | 
 | ||||||
|  | 		if (returnValue == null) { | ||||||
|  | 			return handleNoContent(returnType, message); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		DataBufferFactory bufferFactory = (DataBufferFactory) message.getHeaders() | ||||||
|  | 				.getOrDefault(HandlerMethodReturnValueHandler.DATA_BUFFER_FACTORY_HEADER, this.defaultBufferFactory); | ||||||
|  | 
 | ||||||
|  | 		MimeType mimeType = (MimeType) message.getHeaders().get(MessageHeaders.CONTENT_TYPE); | ||||||
|  | 
 | ||||||
|  | 		Flux<DataBuffer> encodedContent = encodeContent( | ||||||
|  | 				returnValue, returnType, bufferFactory, mimeType, Collections.emptyMap()); | ||||||
|  | 
 | ||||||
|  | 		return new ChannelSendOperator<>(encodedContent, publisher -> | ||||||
|  | 				handleEncodedContent(Flux.from(publisher), returnType, message)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("unchecked") | ||||||
|  | 	private Flux<DataBuffer> encodeContent( | ||||||
|  | 			@Nullable Object content, MethodParameter returnType, DataBufferFactory bufferFactory, | ||||||
|  | 			@Nullable MimeType mimeType, Map<String, Object> hints) { | ||||||
|  | 
 | ||||||
|  | 		ResolvableType returnValueType = ResolvableType.forMethodParameter(returnType); | ||||||
|  | 		ReactiveAdapter adapter = getAdapterRegistry().getAdapter(returnValueType.resolve(), content); | ||||||
|  | 
 | ||||||
|  | 		Publisher<?> publisher; | ||||||
|  | 		ResolvableType elementType; | ||||||
|  | 		if (adapter != null) { | ||||||
|  | 			publisher = adapter.toPublisher(content); | ||||||
|  | 			ResolvableType genericType = returnValueType.getGeneric(); | ||||||
|  | 			elementType = getElementType(adapter, genericType); | ||||||
|  | 		} | ||||||
|  | 		else { | ||||||
|  | 			publisher = Mono.justOrEmpty(content); | ||||||
|  | 			elementType = returnValueType.toClass() == Object.class && content != null ? | ||||||
|  | 					ResolvableType.forInstance(content) : returnValueType; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (elementType.resolve() == void.class || elementType.resolve() == Void.class) { | ||||||
|  | 			return Flux.from(publisher).cast(DataBuffer.class); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		Encoder<?> encoder = getEncoder(elementType, mimeType); | ||||||
|  | 
 | ||||||
|  | 		return Flux.from((Publisher) publisher).concatMap(value -> | ||||||
|  | 				encodeValue(value, elementType, encoder, bufferFactory, mimeType, hints)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private ResolvableType getElementType(ReactiveAdapter adapter, ResolvableType type) { | ||||||
|  | 		if (adapter.isNoValue()) { | ||||||
|  | 			return VOID_RESOLVABLE_TYPE; | ||||||
|  | 		} | ||||||
|  | 		else if (type != ResolvableType.NONE) { | ||||||
|  | 			return type; | ||||||
|  | 		} | ||||||
|  | 		else { | ||||||
|  | 			return OBJECT_RESOLVABLE_TYPE; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	@SuppressWarnings("unchecked") | ||||||
|  | 	private <T> Encoder<T> getEncoder(ResolvableType elementType, @Nullable MimeType mimeType) { | ||||||
|  | 		for (Encoder<?> encoder : getEncoders()) { | ||||||
|  | 			if (encoder.canEncode(elementType, mimeType)) { | ||||||
|  | 				return (Encoder<T>) encoder; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return null; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("unchecked") | ||||||
|  | 	private <T> Mono<DataBuffer> encodeValue( | ||||||
|  | 			Object element, ResolvableType elementType, @Nullable Encoder<T> encoder, | ||||||
|  | 			DataBufferFactory bufferFactory, @Nullable MimeType mimeType, | ||||||
|  | 			@Nullable Map<String, Object> hints) { | ||||||
|  | 
 | ||||||
|  | 		if (encoder == null) { | ||||||
|  | 			encoder = getEncoder(ResolvableType.forInstance(element), mimeType); | ||||||
|  | 			if (encoder == null) { | ||||||
|  | 				return Mono.error(new MessagingException( | ||||||
|  | 						"No encoder for " + elementType + ", current value type is " + element.getClass())); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		Mono<T> mono = Mono.just((T) element); | ||||||
|  | 		Flux<DataBuffer> dataBuffers = encoder.encode(mono, bufferFactory, elementType, mimeType, hints); | ||||||
|  | 		return DataBufferUtils.join(dataBuffers); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Sub-classes implement this method to handle encoded values in some way | ||||||
|  | 	 * such as creating and sending messages. | ||||||
|  | 	 * | ||||||
|  | 	 * @param encodedContent the encoded content; each {@code DataBuffer} | ||||||
|  | 	 * represents the fully-aggregated, encoded content for one value | ||||||
|  | 	 * (i.e. payload) returned from the HandlerMethod. | ||||||
|  | 	 * @param returnType return type of the handler method that produced the data | ||||||
|  | 	 * @param message the input message handled by the handler method | ||||||
|  | 	 * @return completion {@code Mono<Void>} for the handling | ||||||
|  | 	 */ | ||||||
|  | 	protected abstract Mono<Void> handleEncodedContent( | ||||||
|  | 			Flux<DataBuffer> encodedContent, MethodParameter returnType, Message<?> message); | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Invoked for a {@code null} return value, which could mean a void method | ||||||
|  | 	 * or method returning an async type parameterized by void. | ||||||
|  | 	 * @param returnType return type of the handler method that produced the data | ||||||
|  | 	 * @param message the input message handled by the handler method | ||||||
|  | 	 * @return completion {@code Mono<Void>} for the handling | ||||||
|  | 	 */ | ||||||
|  | 	protected abstract Mono<Void> handleNoContent(MethodParameter returnType, Message<?> message); | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,517 @@ | ||||||
|  | /* | ||||||
|  |  * 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 java.lang.reflect.Method; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Collection; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.Comparator; | ||||||
|  | import java.util.LinkedHashMap; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.Set; | ||||||
|  | import java.util.function.Predicate; | ||||||
|  | 
 | ||||||
|  | import org.apache.commons.logging.Log; | ||||||
|  | import org.apache.commons.logging.LogFactory; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | 
 | ||||||
|  | import org.springframework.beans.factory.BeanNameAware; | ||||||
|  | import org.springframework.beans.factory.InitializingBean; | ||||||
|  | import org.springframework.context.ApplicationContext; | ||||||
|  | import org.springframework.context.ApplicationContextAware; | ||||||
|  | import org.springframework.core.MethodIntrospector; | ||||||
|  | import org.springframework.core.ReactiveAdapterRegistry; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | import org.springframework.messaging.MessagingException; | ||||||
|  | import org.springframework.messaging.ReactiveMessageHandler; | ||||||
|  | import org.springframework.messaging.handler.HandlerMethod; | ||||||
|  | import org.springframework.messaging.handler.MessagingAdviceBean; | ||||||
|  | import org.springframework.messaging.handler.invocation.AbstractExceptionHandlerMethodResolver; | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | import org.springframework.util.ClassUtils; | ||||||
|  | import org.springframework.util.CollectionUtils; | ||||||
|  | import org.springframework.util.LinkedMultiValueMap; | ||||||
|  | import org.springframework.util.MultiValueMap; | ||||||
|  | import org.springframework.util.ObjectUtils; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Abstract base class for reactive HandlerMethod-based message handling. | ||||||
|  |  * Provides most of the logic required to discover handler methods at startup, | ||||||
|  |  * find a matching handler method at runtime for a given message and invoke it. | ||||||
|  |  * | ||||||
|  |  * <p>Also supports discovering and invoking exception handling methods to process | ||||||
|  |  * exceptions raised during message handling. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  * @param <T> the type of the Object that contains information mapping information | ||||||
|  |  */ | ||||||
|  | public abstract class AbstractMethodMessageHandler<T> | ||||||
|  | 		implements ReactiveMessageHandler, ApplicationContextAware, InitializingBean, BeanNameAware { | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Bean name prefix for target beans behind scoped proxies. Used to exclude those | ||||||
|  | 	 * targets from handler method detection, in favor of the corresponding proxies. | ||||||
|  | 	 * <p>We're not checking the autowire-candidate status here, which is how the | ||||||
|  | 	 * proxy target filtering problem is being handled at the autowiring level, | ||||||
|  | 	 * since autowire-candidate may have been turned to {@code false} for other | ||||||
|  | 	 * reasons, while still expecting the bean to be eligible for handler methods. | ||||||
|  | 	 * <p>Originally defined in {@link org.springframework.aop.scope.ScopedProxyUtils} | ||||||
|  | 	 * but duplicated here to avoid a hard dependency on the spring-aop module. | ||||||
|  | 	 */ | ||||||
|  | 	private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget."; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	protected final Log logger = LogFactory.getLog(getClass()); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private ArgumentResolverConfigurer argumentResolverConfigurer = new ArgumentResolverConfigurer(); | ||||||
|  | 
 | ||||||
|  | 	private ReturnValueHandlerConfigurer returnValueHandlerConfigurer = new ReturnValueHandlerConfigurer(); | ||||||
|  | 
 | ||||||
|  | 	private final InvocableHelper invocableHelper = new InvocableHelper(this::createExceptionMethodResolverFor); | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	private ApplicationContext applicationContext; | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	private String beanName; | ||||||
|  | 
 | ||||||
|  | 	private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<>(64); | ||||||
|  | 
 | ||||||
|  | 	private final MultiValueMap<String, T> destinationLookup = new LinkedMultiValueMap<>(64); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Configure custom resolvers for handler method arguments. | ||||||
|  | 	 */ | ||||||
|  | 	public void setArgumentResolverConfigurer(ArgumentResolverConfigurer configurer) { | ||||||
|  | 		Assert.notNull(configurer, "HandlerMethodArgumentResolver is required."); | ||||||
|  | 		this.argumentResolverConfigurer = configurer; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the configured custom resolvers for handler method arguments. | ||||||
|  | 	 */ | ||||||
|  | 	public ArgumentResolverConfigurer getArgumentResolverConfigurer() { | ||||||
|  | 		return this.argumentResolverConfigurer; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Configure custom return value handlers for handler metohds. | ||||||
|  | 	 */ | ||||||
|  | 	public void setReturnValueHandlerConfigurer(ReturnValueHandlerConfigurer configurer) { | ||||||
|  | 		Assert.notNull(configurer, "ReturnValueHandlerConfigurer is required."); | ||||||
|  | 		this.returnValueHandlerConfigurer = configurer; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the configured return value handlers. | ||||||
|  | 	 */ | ||||||
|  | 	public ReturnValueHandlerConfigurer getReturnValueHandlerConfigurer() { | ||||||
|  | 		return this.returnValueHandlerConfigurer; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Configure the registry for adapting various reactive types. | ||||||
|  | 	 * <p>By default this is an instance of {@link ReactiveAdapterRegistry} with | ||||||
|  | 	 * default settings. | ||||||
|  | 	 */ | ||||||
|  | 	public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) { | ||||||
|  | 		this.invocableHelper.setReactiveAdapterRegistry(registry); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the configured registry for adapting reactive types. | ||||||
|  | 	 */ | ||||||
|  | 	public ReactiveAdapterRegistry getReactiveAdapterRegistry() { | ||||||
|  | 		return this.invocableHelper.getReactiveAdapterRegistry(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public void setApplicationContext(@Nullable ApplicationContext applicationContext) { | ||||||
|  | 		this.applicationContext = applicationContext; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	public ApplicationContext getApplicationContext() { | ||||||
|  | 		return this.applicationContext; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public void setBeanName(String name) { | ||||||
|  | 		this.beanName = name; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public String getBeanName() { | ||||||
|  | 		return this.beanName != null ? this.beanName : | ||||||
|  | 				getClass().getSimpleName() + "@" + ObjectUtils.getIdentityHexString(this); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Subclasses can invoke this method to populate the MessagingAdviceBean cache | ||||||
|  | 	 * (e.g. to support "global" {@code @MessageExceptionHandler}). | ||||||
|  | 	 */ | ||||||
|  | 	protected void registerExceptionHandlerAdvice( | ||||||
|  | 			MessagingAdviceBean bean, AbstractExceptionHandlerMethodResolver resolver) { | ||||||
|  | 
 | ||||||
|  | 		this.invocableHelper.registerExceptionHandlerAdvice(bean, resolver); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return a read-only map with all handler methods and their mappings. | ||||||
|  | 	 */ | ||||||
|  | 	public Map<T, HandlerMethod> getHandlerMethods() { | ||||||
|  | 		return Collections.unmodifiableMap(this.handlerMethods); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return a read-only multi-value map with a direct lookup of mappings, | ||||||
|  | 	 * (e.g. for non-pattern destinations). | ||||||
|  | 	 */ | ||||||
|  | 	public MultiValueMap<String, T> getDestinationLookup() { | ||||||
|  | 		return CollectionUtils.unmodifiableMultiValueMap(this.destinationLookup); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public void afterPropertiesSet() { | ||||||
|  | 
 | ||||||
|  | 		List<? extends HandlerMethodArgumentResolver> resolvers = initArgumentResolvers(); | ||||||
|  | 		if (resolvers.isEmpty()) { | ||||||
|  | 			resolvers = new ArrayList<>(this.argumentResolverConfigurer.getCustomResolvers()); | ||||||
|  | 		} | ||||||
|  | 		this.invocableHelper.addArgumentResolvers(resolvers); | ||||||
|  | 
 | ||||||
|  | 		List<? extends HandlerMethodReturnValueHandler> handlers = initReturnValueHandlers(); | ||||||
|  | 		if (handlers.isEmpty()) { | ||||||
|  | 			handlers = new ArrayList<>(this.returnValueHandlerConfigurer.getCustomHandlers()); | ||||||
|  | 		} | ||||||
|  | 		this.invocableHelper.addReturnValueHandlers(handlers); | ||||||
|  | 
 | ||||||
|  | 		initHandlerMethods(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the list of argument resolvers to use. | ||||||
|  | 	 * <p>Subclasses should also take into account custom argument types configured via | ||||||
|  | 	 * {@link #setArgumentResolverConfigurer}. | ||||||
|  | 	 */ | ||||||
|  | 	protected abstract List<? extends HandlerMethodArgumentResolver> initArgumentResolvers(); | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the list of return value handlers to use. | ||||||
|  | 	 * <p>Subclasses should also take into account custom return value types configured | ||||||
|  | 	 * via {@link #setReturnValueHandlerConfigurer}. | ||||||
|  | 	 */ | ||||||
|  | 	protected abstract List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private void initHandlerMethods() { | ||||||
|  | 		if (this.applicationContext == null) { | ||||||
|  | 			logger.warn("No ApplicationContext available for detecting beans with message handling methods."); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		Predicate<Class<?>> handlerPredicate = initHandlerPredicate(); | ||||||
|  | 		if (handlerPredicate == null) { | ||||||
|  | 			logger.warn("[" + getBeanName() + "] No auto-detection of handler methods (e.g. in @Controller)."); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		for (String beanName : this.applicationContext.getBeanNamesForType(Object.class)) { | ||||||
|  | 			if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { | ||||||
|  | 				Class<?> beanType = null; | ||||||
|  | 				try { | ||||||
|  | 					beanType = this.applicationContext.getType(beanName); | ||||||
|  | 				} | ||||||
|  | 				catch (Throwable ex) { | ||||||
|  | 					// An unresolvable bean type, probably from a lazy bean - let's ignore it. | ||||||
|  | 					if (logger.isDebugEnabled()) { | ||||||
|  | 						logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				if (beanType != null && handlerPredicate.test(beanType)) { | ||||||
|  | 					detectHandlerMethods(beanName); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the predicate to use to check whether a given Spring bean should | ||||||
|  | 	 * be introspected for message handling methods. If {@code null} is | ||||||
|  | 	 * returned, auto-detection is effectively disabled. | ||||||
|  | 	 */ | ||||||
|  | 	@Nullable | ||||||
|  | 	protected abstract Predicate<Class<?>> initHandlerPredicate(); | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Detect if the given handler has any methods that can handle messages and if | ||||||
|  | 	 * so register it with the extracted mapping information. | ||||||
|  | 	 * <p><strong>Note:</strong> This method is protected and can be invoked by | ||||||
|  | 	 * sub-classes, but this should be done on startup only as documented in | ||||||
|  | 	 * {@link #registerHandlerMethod}. | ||||||
|  | 	 * @param handler the handler to check, either an instance of a Spring bean name | ||||||
|  | 	 */ | ||||||
|  | 	protected final void detectHandlerMethods(Object handler) { | ||||||
|  | 		Class<?> handlerType; | ||||||
|  | 		if (handler instanceof String) { | ||||||
|  | 			ApplicationContext context = getApplicationContext(); | ||||||
|  | 			Assert.state(context != null, "ApplicationContext is required for resolving handler bean names"); | ||||||
|  | 			handlerType = context.getType((String) handler); | ||||||
|  | 		} | ||||||
|  | 		else { | ||||||
|  | 			handlerType = handler.getClass(); | ||||||
|  | 		} | ||||||
|  | 		if (handlerType != null) { | ||||||
|  | 			final Class<?> userType = ClassUtils.getUserClass(handlerType); | ||||||
|  | 			Map<Method, T> methods = MethodIntrospector.selectMethods(userType, | ||||||
|  | 					(MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType)); | ||||||
|  | 			if (logger.isDebugEnabled()) { | ||||||
|  | 				logger.debug(methods.size() + " message handler methods found on " + userType + ": " + methods); | ||||||
|  | 			} | ||||||
|  | 			methods.forEach((key, value) -> registerHandlerMethod(handler, key, value)); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Obtain the mapping for the given method, if any. | ||||||
|  | 	 * @param method the method to check | ||||||
|  | 	 * @param handlerType the handler type, possibly a sub-type of the method's declaring class | ||||||
|  | 	 * @return the mapping, or {@code null} if the method is not mapped | ||||||
|  | 	 */ | ||||||
|  | 	@Nullable | ||||||
|  | 	protected abstract T getMappingForMethod(Method method, Class<?> handlerType); | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Register a handler method and its unique mapping. | ||||||
|  | 	 * <p><strong>Note:</strong> This method is protected and can be invoked by | ||||||
|  | 	 * sub-classes. Keep in mind however that the registration is not protected | ||||||
|  | 	 * for concurrent use, and is expected to be done on startup. | ||||||
|  | 	 * @param handler the bean name of the handler or the handler instance | ||||||
|  | 	 * @param method the method to register | ||||||
|  | 	 * @param mapping the mapping conditions associated with the handler method | ||||||
|  | 	 * @throws IllegalStateException if another method was already registered | ||||||
|  | 	 * under the same mapping | ||||||
|  | 	 */ | ||||||
|  | 	protected final void registerHandlerMethod(Object handler, Method method, T mapping) { | ||||||
|  | 		Assert.notNull(mapping, "Mapping must not be null"); | ||||||
|  | 		HandlerMethod newHandlerMethod = createHandlerMethod(handler, method); | ||||||
|  | 		HandlerMethod oldHandlerMethod = this.handlerMethods.get(mapping); | ||||||
|  | 
 | ||||||
|  | 		if (oldHandlerMethod != null && !oldHandlerMethod.equals(newHandlerMethod)) { | ||||||
|  | 			throw new IllegalStateException("Ambiguous mapping found. Cannot map '" + newHandlerMethod.getBean() + | ||||||
|  | 					"' bean method \n" + newHandlerMethod + "\nto " + mapping + ": There is already '" + | ||||||
|  | 					oldHandlerMethod.getBean() + "' bean method\n" + oldHandlerMethod + " mapped."); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		this.handlerMethods.put(mapping, newHandlerMethod); | ||||||
|  | 		if (logger.isTraceEnabled()) { | ||||||
|  | 			logger.trace("Mapped \"" + mapping + "\" onto " + newHandlerMethod); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		for (String pattern : getDirectLookupMappings(mapping)) { | ||||||
|  | 			this.destinationLookup.add(pattern, mapping); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Create a HandlerMethod instance from an Object handler that is either a handler | ||||||
|  | 	 * instance or a String-based bean name. | ||||||
|  | 	 */ | ||||||
|  | 	private HandlerMethod createHandlerMethod(Object handler, Method method) { | ||||||
|  | 		HandlerMethod handlerMethod; | ||||||
|  | 		if (handler instanceof String) { | ||||||
|  | 			ApplicationContext context = getApplicationContext(); | ||||||
|  | 			Assert.state(context != null, "ApplicationContext is required for resolving handler bean names"); | ||||||
|  | 			String beanName = (String) handler; | ||||||
|  | 			handlerMethod = new HandlerMethod(beanName, context.getAutowireCapableBeanFactory(), method); | ||||||
|  | 		} | ||||||
|  | 		else { | ||||||
|  | 			handlerMethod = new HandlerMethod(handler, method); | ||||||
|  | 		} | ||||||
|  | 		return handlerMethod; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return String-based destinations for the given mapping, if any, that can | ||||||
|  | 	 * be used to find matches with a direct lookup (i.e. non-patterns). | ||||||
|  | 	 * <p><strong>Note:</strong> This is completely optional. The mapping | ||||||
|  | 	 * metadata for a sub-class may support neither direct lookups, nor String | ||||||
|  | 	 * based destinations. | ||||||
|  | 	 */ | ||||||
|  | 	protected abstract Set<String> getDirectLookupMappings(T mapping); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public Mono<Void> handleMessage(Message<?> message) throws MessagingException { | ||||||
|  | 		Match<T> match = getHandlerMethod(message); | ||||||
|  | 		if (match == null) { | ||||||
|  | 			// handleNoMatch would have been invoked already | ||||||
|  | 			return Mono.empty(); | ||||||
|  | 		} | ||||||
|  | 		HandlerMethod handlerMethod = match.getHandlerMethod().createWithResolvedBean(); | ||||||
|  | 		return this.invocableHelper.handleMessage(handlerMethod, message); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	private Match<T> getHandlerMethod(Message<?> message) { | ||||||
|  | 		List<Match<T>> matches = new ArrayList<>(); | ||||||
|  | 
 | ||||||
|  | 		String destination = getDestination(message); | ||||||
|  | 		List<T> mappingsByUrl = destination != null ? this.destinationLookup.get(destination) : null; | ||||||
|  | 		if (mappingsByUrl != null) { | ||||||
|  | 			addMatchesToCollection(mappingsByUrl, message, matches); | ||||||
|  | 		} | ||||||
|  | 		if (matches.isEmpty()) { | ||||||
|  | 			// No direct hits, go through all mappings | ||||||
|  | 			Set<T> allMappings = this.handlerMethods.keySet(); | ||||||
|  | 			addMatchesToCollection(allMappings, message, matches); | ||||||
|  | 		} | ||||||
|  | 		if (matches.isEmpty()) { | ||||||
|  | 			handleNoMatch(destination, message); | ||||||
|  | 			return null; | ||||||
|  | 		} | ||||||
|  | 		Comparator<Match<T>> comparator = new MatchComparator(getMappingComparator(message)); | ||||||
|  | 		matches.sort(comparator); | ||||||
|  | 		if (logger.isTraceEnabled()) { | ||||||
|  | 			logger.trace("Found " + matches.size() + " handler methods: " + matches); | ||||||
|  | 		} | ||||||
|  | 		Match<T> bestMatch = matches.get(0); | ||||||
|  | 		if (matches.size() > 1) { | ||||||
|  | 			Match<T> secondBestMatch = matches.get(1); | ||||||
|  | 			if (comparator.compare(bestMatch, secondBestMatch) == 0) { | ||||||
|  | 				HandlerMethod m1 = bestMatch.handlerMethod; | ||||||
|  | 				HandlerMethod m2 = secondBestMatch.handlerMethod; | ||||||
|  | 				throw new IllegalStateException("Ambiguous handler methods mapped for destination '" + | ||||||
|  | 						destination + "': {" + m1.getShortLogMessage() + ", " + m2.getShortLogMessage() + "}"); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return bestMatch; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Extract a String-based destination, if any, that can be used to perform | ||||||
|  | 	 * a direct look up into the registered mappings. | ||||||
|  | 	 * <p><strong>Note:</strong> This is completely optional. The mapping | ||||||
|  | 	 * metadata for a sub-class may support neither direct lookups, nor String | ||||||
|  | 	 * based destinations. | ||||||
|  | 	 * @see #getDirectLookupMappings(Object) | ||||||
|  | 	 */ | ||||||
|  | 	@Nullable | ||||||
|  | 	protected abstract String getDestination(Message<?> message); | ||||||
|  | 
 | ||||||
|  | 	private void addMatchesToCollection( | ||||||
|  | 			Collection<T> mappingsToCheck, Message<?> message, List<Match<T>> matches) { | ||||||
|  | 
 | ||||||
|  | 		for (T mapping : mappingsToCheck) { | ||||||
|  | 			T match = getMatchingMapping(mapping, message); | ||||||
|  | 			if (match != null) { | ||||||
|  | 				matches.add(new Match<T>(match, this.handlerMethods.get(mapping))); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Check if a mapping matches the current message and return a possibly | ||||||
|  | 	 * new mapping with conditions relevant to the current request. | ||||||
|  | 	 * @param mapping the mapping to get a match for | ||||||
|  | 	 * @param message the message being handled | ||||||
|  | 	 * @return the match or {@code null} if there is no match | ||||||
|  | 	 */ | ||||||
|  | 	@Nullable | ||||||
|  | 	protected abstract T getMatchingMapping(T mapping, Message<?> message); | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return a comparator for sorting matching mappings. | ||||||
|  | 	 * The returned comparator should sort 'better' matches higher. | ||||||
|  | 	 * @param message the current Message | ||||||
|  | 	 * @return the comparator, never {@code null} | ||||||
|  | 	 */ | ||||||
|  | 	protected abstract Comparator<T> getMappingComparator(Message<?> message); | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Invoked when no matching handler is found. | ||||||
|  | 	 * @param destination the destination | ||||||
|  | 	 * @param message the message | ||||||
|  | 	 */ | ||||||
|  | 	@Nullable | ||||||
|  | 	protected void handleNoMatch(@Nullable String destination, Message<?> message) { | ||||||
|  | 		logger.debug("No handlers for destination '" + destination + "'"); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Create a concrete instance of {@link AbstractExceptionHandlerMethodResolver} | ||||||
|  | 	 * that finds exception handling methods based on some criteria, e.g. based | ||||||
|  | 	 * on the presence of {@code @MessageExceptionHandler}. | ||||||
|  | 	 * @param beanType the class in which an exception occurred during handling | ||||||
|  | 	 * @return the resolver to use | ||||||
|  | 	 */ | ||||||
|  | 	protected abstract AbstractExceptionHandlerMethodResolver createExceptionMethodResolverFor(Class<?> beanType); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Container for matched mapping and HandlerMethod. Used for best match | ||||||
|  | 	 * comparison and for access to mapping information. | ||||||
|  | 	 */ | ||||||
|  | 	private static class Match<T> { | ||||||
|  | 
 | ||||||
|  | 		private final T mapping; | ||||||
|  | 
 | ||||||
|  | 		private final HandlerMethod handlerMethod; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		Match(T mapping, HandlerMethod handlerMethod) { | ||||||
|  | 			this.mapping = mapping; | ||||||
|  | 			this.handlerMethod = handlerMethod; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		public T getMapping() { | ||||||
|  | 			return this.mapping; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		public HandlerMethod getHandlerMethod() { | ||||||
|  | 			return this.handlerMethod; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public String toString() { | ||||||
|  | 			return this.mapping.toString(); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private class MatchComparator implements Comparator<Match<T>> { | ||||||
|  | 
 | ||||||
|  | 		private final Comparator<T> comparator; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		MatchComparator(Comparator<T> comparator) { | ||||||
|  | 			this.comparator = comparator; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public int compare(Match<T> match1, Match<T> match2) { | ||||||
|  | 			return this.comparator.compare(match1.mapping, match2.mapping); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,50 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright 2002-2018 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 java.util.ArrayList; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Assist with configuration for handler method argument resolvers. | ||||||
|  |  * At present, it supports only providing a list of custom resolvers. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | public class ArgumentResolverConfigurer { | ||||||
|  | 
 | ||||||
|  | 	private final List<HandlerMethodArgumentResolver> customResolvers = new ArrayList<>(8); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Configure resolvers for custom handler method arguments. | ||||||
|  | 	 * @param resolver the resolvers to add | ||||||
|  | 	 */ | ||||||
|  | 	public void addCustomResolver(HandlerMethodArgumentResolver... resolver) { | ||||||
|  | 		Assert.notNull(resolver, "'resolvers' must not be null"); | ||||||
|  | 		this.customResolvers.addAll(Arrays.asList(resolver)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	public List<HandlerMethodArgumentResolver> getCustomResolvers() { | ||||||
|  | 		return this.customResolvers; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,410 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright 2002-2018 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 java.util.function.Function; | ||||||
|  | 
 | ||||||
|  | import org.reactivestreams.Publisher; | ||||||
|  | import org.reactivestreams.Subscriber; | ||||||
|  | import org.reactivestreams.Subscription; | ||||||
|  | import reactor.core.CoreSubscriber; | ||||||
|  | import reactor.core.Scannable; | ||||||
|  | import reactor.core.publisher.Flux; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | import reactor.core.publisher.Operators; | ||||||
|  | import reactor.util.context.Context; | ||||||
|  | 
 | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * ---------------------- | ||||||
|  |  * <p><strong>NOTE:</strong> This class was copied from | ||||||
|  |  * {@code org.springframework.http.server.reactive.ChannelSendOperator} and is | ||||||
|  |  * identical to it. It's used for the same purpose, i.e. the ability to switch to | ||||||
|  |  * alternate handling via annotated exception handler methods if the output | ||||||
|  |  * publisher starts with an error. | ||||||
|  |  * <p>----------------------<br> | ||||||
|  |  * | ||||||
|  |  * <p>Given a write function that accepts a source {@code Publisher<T>} to write | ||||||
|  |  * with and returns {@code Publisher<Void>} for the result, this operator helps | ||||||
|  |  * to defer the invocation of the write function, until we know if the source | ||||||
|  |  * publisher will begin publishing without an error. If the first emission is | ||||||
|  |  * an error, the write function is bypassed, and the error is sent directly | ||||||
|  |  * through the result publisher. Otherwise the write function is invoked. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @author Stephane Maldini | ||||||
|  |  * @since 5.2 | ||||||
|  |  * @param <T> the type of element signaled | ||||||
|  |  */ | ||||||
|  | class ChannelSendOperator<T> extends Mono<Void> implements Scannable { | ||||||
|  | 
 | ||||||
|  | 	private final Function<Publisher<T>, Publisher<Void>> writeFunction; | ||||||
|  | 
 | ||||||
|  | 	private final Flux<T> source; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	public ChannelSendOperator(Publisher<? extends T> source, Function<Publisher<T>, Publisher<Void>> writeFunction) { | ||||||
|  | 		this.source = Flux.from(source); | ||||||
|  | 		this.writeFunction = writeFunction; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	@Nullable | ||||||
|  | 	@SuppressWarnings("rawtypes") | ||||||
|  | 	public Object scanUnsafe(Attr key) { | ||||||
|  | 		if (key == Attr.PREFETCH) { | ||||||
|  | 			return Integer.MAX_VALUE; | ||||||
|  | 		} | ||||||
|  | 		if (key == Attr.PARENT) { | ||||||
|  | 			return this.source; | ||||||
|  | 		} | ||||||
|  | 		return null; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public void subscribe(CoreSubscriber<? super Void> actual) { | ||||||
|  | 		this.source.subscribe(new WriteBarrier(actual)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private enum State { | ||||||
|  | 
 | ||||||
|  | 		/** No emissions from the upstream source yet. */ | ||||||
|  | 		NEW, | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * At least one signal of any kind has been received; we're ready to | ||||||
|  | 		 * call the write function and proceed with actual writing. | ||||||
|  | 		 */ | ||||||
|  | 		FIRST_SIGNAL_RECEIVED, | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * The write subscriber has subscribed and requested; we're going to | ||||||
|  | 		 * emit the cached signals. | ||||||
|  | 		 */ | ||||||
|  | 		EMITTING_CACHED_SIGNALS, | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * The write subscriber has subscribed, and cached signals have been | ||||||
|  | 		 * emitted to it; we're ready to switch to a simple pass-through mode | ||||||
|  | 		 * for all remaining signals. | ||||||
|  | 		 **/ | ||||||
|  | 		READY_TO_WRITE | ||||||
|  | 
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * A barrier inserted between the write source and the write subscriber | ||||||
|  | 	 * (i.e. the HTTP server adapter) that pre-fetches and waits for the first | ||||||
|  | 	 * signal before deciding whether to hook in to the write subscriber. | ||||||
|  | 	 * | ||||||
|  | 	 * <p>Acts as: | ||||||
|  | 	 * <ul> | ||||||
|  | 	 * <li>Subscriber to the write source. | ||||||
|  | 	 * <li>Subscription to the write subscriber. | ||||||
|  | 	 * <li>Publisher to the write subscriber. | ||||||
|  | 	 * </ul> | ||||||
|  | 	 * | ||||||
|  | 	 * <p>Also uses {@link WriteCompletionBarrier} to communicate completion | ||||||
|  | 	 * and detect cancel signals from the completion subscriber. | ||||||
|  | 	 */ | ||||||
|  | 	private class WriteBarrier implements CoreSubscriber<T>, Subscription, Publisher<T> { | ||||||
|  | 
 | ||||||
|  | 		/* Bridges signals to and from the completionSubscriber */ | ||||||
|  | 		private final WriteCompletionBarrier writeCompletionBarrier; | ||||||
|  | 
 | ||||||
|  | 		/* Upstream write source subscription */ | ||||||
|  | 		@Nullable | ||||||
|  | 		private Subscription subscription; | ||||||
|  | 
 | ||||||
|  | 		/** Cached data item before readyToWrite. */ | ||||||
|  | 		@Nullable | ||||||
|  | 		private T item; | ||||||
|  | 
 | ||||||
|  | 		/** Cached error signal before readyToWrite. */ | ||||||
|  | 		@Nullable | ||||||
|  | 		private Throwable error; | ||||||
|  | 
 | ||||||
|  | 		/** Cached onComplete signal before readyToWrite. */ | ||||||
|  | 		private boolean completed = false; | ||||||
|  | 
 | ||||||
|  | 		/** Recursive demand while emitting cached signals. */ | ||||||
|  | 		private long demandBeforeReadyToWrite; | ||||||
|  | 
 | ||||||
|  | 		/** Current state. */ | ||||||
|  | 		private State state = State.NEW; | ||||||
|  | 
 | ||||||
|  | 		/** The actual writeSubscriber from the HTTP server adapter. */ | ||||||
|  | 		@Nullable | ||||||
|  | 		private Subscriber<? super T> writeSubscriber; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		WriteBarrier(CoreSubscriber<? super Void> completionSubscriber) { | ||||||
|  | 			this.writeCompletionBarrier = new WriteCompletionBarrier(completionSubscriber, this); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		// Subscriber<T> methods (we're the subscriber to the write source).. | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public final void onSubscribe(Subscription s) { | ||||||
|  | 			if (Operators.validate(this.subscription, s)) { | ||||||
|  | 				this.subscription = s; | ||||||
|  | 				this.writeCompletionBarrier.connect(); | ||||||
|  | 				s.request(1); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public final void onNext(T item) { | ||||||
|  | 			if (this.state == State.READY_TO_WRITE) { | ||||||
|  | 				requiredWriteSubscriber().onNext(item); | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 			//FIXME revisit in case of reentrant sync deadlock | ||||||
|  | 			synchronized (this) { | ||||||
|  | 				if (this.state == State.READY_TO_WRITE) { | ||||||
|  | 					requiredWriteSubscriber().onNext(item); | ||||||
|  | 				} | ||||||
|  | 				else if (this.state == State.NEW) { | ||||||
|  | 					this.item = item; | ||||||
|  | 					this.state = State.FIRST_SIGNAL_RECEIVED; | ||||||
|  | 					writeFunction.apply(this).subscribe(this.writeCompletionBarrier); | ||||||
|  | 				} | ||||||
|  | 				else { | ||||||
|  | 					if (this.subscription != null) { | ||||||
|  | 						this.subscription.cancel(); | ||||||
|  | 					} | ||||||
|  | 					this.writeCompletionBarrier.onError(new IllegalStateException("Unexpected item.")); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private Subscriber<? super T> requiredWriteSubscriber() { | ||||||
|  | 			Assert.state(this.writeSubscriber != null, "No write subscriber"); | ||||||
|  | 			return this.writeSubscriber; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public final void onError(Throwable ex) { | ||||||
|  | 			if (this.state == State.READY_TO_WRITE) { | ||||||
|  | 				requiredWriteSubscriber().onError(ex); | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 			synchronized (this) { | ||||||
|  | 				if (this.state == State.READY_TO_WRITE) { | ||||||
|  | 					requiredWriteSubscriber().onError(ex); | ||||||
|  | 				} | ||||||
|  | 				else if (this.state == State.NEW) { | ||||||
|  | 					this.state = State.FIRST_SIGNAL_RECEIVED; | ||||||
|  | 					this.writeCompletionBarrier.onError(ex); | ||||||
|  | 				} | ||||||
|  | 				else { | ||||||
|  | 					this.error = ex; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public final void onComplete() { | ||||||
|  | 			if (this.state == State.READY_TO_WRITE) { | ||||||
|  | 				requiredWriteSubscriber().onComplete(); | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 			synchronized (this) { | ||||||
|  | 				if (this.state == State.READY_TO_WRITE) { | ||||||
|  | 					requiredWriteSubscriber().onComplete(); | ||||||
|  | 				} | ||||||
|  | 				else if (this.state == State.NEW) { | ||||||
|  | 					this.completed = true; | ||||||
|  | 					this.state = State.FIRST_SIGNAL_RECEIVED; | ||||||
|  | 					writeFunction.apply(this).subscribe(this.writeCompletionBarrier); | ||||||
|  | 				} | ||||||
|  | 				else { | ||||||
|  | 					this.completed = true; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public Context currentContext() { | ||||||
|  | 			return this.writeCompletionBarrier.currentContext(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		// Subscription methods (we're the Subscription to the writeSubscriber).. | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public void request(long n) { | ||||||
|  | 			Subscription s = this.subscription; | ||||||
|  | 			if (s == null) { | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 			if (this.state == State.READY_TO_WRITE) { | ||||||
|  | 				s.request(n); | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 			synchronized (this) { | ||||||
|  | 				if (this.writeSubscriber != null) { | ||||||
|  | 					if (this.state == State.EMITTING_CACHED_SIGNALS) { | ||||||
|  | 						this.demandBeforeReadyToWrite = n; | ||||||
|  | 						return; | ||||||
|  | 					} | ||||||
|  | 					try { | ||||||
|  | 						this.state = State.EMITTING_CACHED_SIGNALS; | ||||||
|  | 						if (emitCachedSignals()) { | ||||||
|  | 							return; | ||||||
|  | 						} | ||||||
|  | 						n = n + this.demandBeforeReadyToWrite - 1; | ||||||
|  | 						if (n == 0) { | ||||||
|  | 							return; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 					finally { | ||||||
|  | 						this.state = State.READY_TO_WRITE; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			s.request(n); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private boolean emitCachedSignals() { | ||||||
|  | 			if (this.item != null) { | ||||||
|  | 				requiredWriteSubscriber().onNext(this.item); | ||||||
|  | 			} | ||||||
|  | 			if (this.error != null) { | ||||||
|  | 				requiredWriteSubscriber().onError(this.error); | ||||||
|  | 				return true; | ||||||
|  | 			} | ||||||
|  | 			if (this.completed) { | ||||||
|  | 				requiredWriteSubscriber().onComplete(); | ||||||
|  | 				return true; | ||||||
|  | 			} | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public void cancel() { | ||||||
|  | 			Subscription s = this.subscription; | ||||||
|  | 			if (s != null) { | ||||||
|  | 				this.subscription = null; | ||||||
|  | 				s.cancel(); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		// Publisher<T> methods (we're the Publisher to the writeSubscriber).. | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public void subscribe(Subscriber<? super T> writeSubscriber) { | ||||||
|  | 			synchronized (this) { | ||||||
|  | 				Assert.state(this.writeSubscriber == null, "Only one write subscriber supported"); | ||||||
|  | 				this.writeSubscriber = writeSubscriber; | ||||||
|  | 				if (this.error != null || this.completed) { | ||||||
|  | 					this.writeSubscriber.onSubscribe(Operators.emptySubscription()); | ||||||
|  | 					emitCachedSignals(); | ||||||
|  | 				} | ||||||
|  | 				else { | ||||||
|  | 					this.writeSubscriber.onSubscribe(this); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * We need an extra barrier between the WriteBarrier itself and the actual | ||||||
|  | 	 * completion subscriber. | ||||||
|  | 	 * | ||||||
|  | 	 * <p>The completionSubscriber is subscribed initially to the WriteBarrier. | ||||||
|  | 	 * Later after the first signal is received, we need one more subscriber | ||||||
|  | 	 * instance (per spec can only subscribe once) to subscribe to the write | ||||||
|  | 	 * function and switch to delegating completion signals from it. | ||||||
|  | 	 */ | ||||||
|  | 	private class WriteCompletionBarrier implements CoreSubscriber<Void>, Subscription { | ||||||
|  | 
 | ||||||
|  | 		/* Downstream write completion subscriber */ | ||||||
|  | 		private final CoreSubscriber<? super Void> completionSubscriber; | ||||||
|  | 
 | ||||||
|  | 		private final WriteBarrier writeBarrier; | ||||||
|  | 
 | ||||||
|  | 		@Nullable | ||||||
|  | 		private Subscription subscription; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		public WriteCompletionBarrier(CoreSubscriber<? super Void> subscriber, WriteBarrier writeBarrier) { | ||||||
|  | 			this.completionSubscriber = subscriber; | ||||||
|  | 			this.writeBarrier = writeBarrier; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * Connect the underlying completion subscriber to this barrier in order | ||||||
|  | 		 * to track cancel signals and pass them on to the write barrier. | ||||||
|  | 		 */ | ||||||
|  | 		public void connect() { | ||||||
|  | 			this.completionSubscriber.onSubscribe(this); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Subscriber methods (we're the subscriber to the write function).. | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public void onSubscribe(Subscription subscription) { | ||||||
|  | 			this.subscription = subscription; | ||||||
|  | 			subscription.request(Long.MAX_VALUE); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public void onNext(Void aVoid) { | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public void onError(Throwable ex) { | ||||||
|  | 			this.completionSubscriber.onError(ex); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public void onComplete() { | ||||||
|  | 			this.completionSubscriber.onComplete(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public Context currentContext() { | ||||||
|  | 			return this.completionSubscriber.currentContext(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public void request(long n) { | ||||||
|  | 			// Ignore: we don't produce data | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public void cancel() { | ||||||
|  | 			this.writeBarrier.cancel(); | ||||||
|  | 			Subscription subscription = this.subscription; | ||||||
|  | 			if (subscription != null) { | ||||||
|  | 				subscription.cancel(); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,52 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright 2002-2018 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.messaging.Message; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Strategy interface for resolving method parameters into argument values | ||||||
|  |  * in the context of a given {@link Message}. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | public interface HandlerMethodArgumentResolver { | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Whether the given {@linkplain MethodParameter method parameter} is | ||||||
|  | 	 * supported by this resolver. | ||||||
|  | 	 * @param parameter the method parameter to check | ||||||
|  | 	 * @return {@code true} if this resolver supports the supplied parameter; | ||||||
|  | 	 * {@code false} otherwise | ||||||
|  | 	 */ | ||||||
|  | 	boolean supportsParameter(MethodParameter parameter); | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Resolves a method parameter into an argument value from a given message. | ||||||
|  | 	 * @param parameter the method parameter to resolve. | ||||||
|  | 	 * This parameter must have previously been passed to | ||||||
|  | 	 * {@link #supportsParameter(org.springframework.core.MethodParameter)} | ||||||
|  | 	 * which must have returned {@code true}. | ||||||
|  | 	 * @param message the currently processed message | ||||||
|  | 	 * @return {@code Mono} for the argument value, possibly empty | ||||||
|  | 	 */ | ||||||
|  | 	Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message); | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,142 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright 2002-2018 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 java.util.Collections; | ||||||
|  | import java.util.LinkedList; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.concurrent.ConcurrentHashMap; | ||||||
|  | 
 | ||||||
|  | import org.apache.commons.logging.Log; | ||||||
|  | import org.apache.commons.logging.LogFactory; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.MethodParameter; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Resolves method parameters by delegating to a list of registered | ||||||
|  |  * {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}. | ||||||
|  |  * Previously resolved method parameters are cached for faster lookups. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver { | ||||||
|  | 
 | ||||||
|  | 	protected final Log logger = LogFactory.getLog(getClass()); | ||||||
|  | 
 | ||||||
|  | 	private final List<HandlerMethodArgumentResolver> argumentResolvers = new LinkedList<>(); | ||||||
|  | 
 | ||||||
|  | 	private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache = | ||||||
|  | 			new ConcurrentHashMap<>(256); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Add the given {@link HandlerMethodArgumentResolver}. | ||||||
|  | 	 */ | ||||||
|  | 	public HandlerMethodArgumentResolverComposite addResolver(HandlerMethodArgumentResolver resolver) { | ||||||
|  | 		this.argumentResolvers.add(resolver); | ||||||
|  | 		return this; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Add the given {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}. | ||||||
|  | 	 */ | ||||||
|  | 	public HandlerMethodArgumentResolverComposite addResolvers(@Nullable HandlerMethodArgumentResolver... resolvers) { | ||||||
|  | 		if (resolvers != null) { | ||||||
|  | 			Collections.addAll(this.argumentResolvers, resolvers); | ||||||
|  | 		} | ||||||
|  | 		return this; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Add the given {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}. | ||||||
|  | 	 */ | ||||||
|  | 	public HandlerMethodArgumentResolverComposite addResolvers( | ||||||
|  | 			@Nullable List<? extends HandlerMethodArgumentResolver> resolvers) { | ||||||
|  | 
 | ||||||
|  | 		if (resolvers != null) { | ||||||
|  | 			this.argumentResolvers.addAll(resolvers); | ||||||
|  | 		} | ||||||
|  | 		return this; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return a read-only list with the contained resolvers, or an empty list. | ||||||
|  | 	 */ | ||||||
|  | 	public List<HandlerMethodArgumentResolver> getResolvers() { | ||||||
|  | 		return Collections.unmodifiableList(this.argumentResolvers); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Clear the list of configured resolvers. | ||||||
|  | 	 */ | ||||||
|  | 	public void clear() { | ||||||
|  | 		this.argumentResolvers.clear(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Whether the given {@linkplain MethodParameter method parameter} is | ||||||
|  | 	 * supported by any registered {@link HandlerMethodArgumentResolver}. | ||||||
|  | 	 */ | ||||||
|  | 	@Override | ||||||
|  | 	public boolean supportsParameter(MethodParameter parameter) { | ||||||
|  | 		return getArgumentResolver(parameter) != null; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Iterate over registered | ||||||
|  | 	 * {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers} and | ||||||
|  | 	 * invoke the one that supports it. | ||||||
|  | 	 * @throws IllegalStateException if no suitable | ||||||
|  | 	 * {@link HandlerMethodArgumentResolver} is found. | ||||||
|  | 	 */ | ||||||
|  | 	@Override | ||||||
|  | 	public Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message) { | ||||||
|  | 		HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); | ||||||
|  | 		if (resolver == null) { | ||||||
|  | 			throw new IllegalArgumentException( | ||||||
|  | 					"Unsupported parameter type [" + parameter.getParameterType().getName() + "]." + | ||||||
|  | 							" supportsParameter should be called first."); | ||||||
|  | 		} | ||||||
|  | 		return resolver.resolveArgument(parameter, message); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Find a registered {@link HandlerMethodArgumentResolver} that supports | ||||||
|  | 	 * the given method parameter. | ||||||
|  | 	 */ | ||||||
|  | 	@Nullable | ||||||
|  | 	private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { | ||||||
|  | 		HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); | ||||||
|  | 		if (result == null) { | ||||||
|  | 			for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) { | ||||||
|  | 				if (methodArgumentResolver.supportsParameter(parameter)) { | ||||||
|  | 					result = methodArgumentResolver; | ||||||
|  | 					this.argumentResolverCache.put(parameter, result); | ||||||
|  | 					break; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return result; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,57 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright 2002-2017 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; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Handle the return value from the invocation of an annotated {@link Message} | ||||||
|  |  * handling method. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | public interface HandlerMethodReturnValueHandler { | ||||||
|  | 
 | ||||||
|  | 	/** Header containing a DataBufferFactory for use in return value handling. */ | ||||||
|  | 	String DATA_BUFFER_FACTORY_HEADER = "dataBufferFactory"; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Whether the given {@linkplain MethodParameter method return type} is | ||||||
|  | 	 * supported by this handler. | ||||||
|  | 	 * @param returnType the method return type to check | ||||||
|  | 	 * @return {@code true} if this handler supports the supplied return type; | ||||||
|  | 	 * {@code false} otherwise | ||||||
|  | 	 */ | ||||||
|  | 	boolean supportsReturnType(MethodParameter returnType); | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Handle the given return value. | ||||||
|  | 	 * @param returnValue the value returned from the handler method | ||||||
|  | 	 * @param returnType the type of the return value. This type must have previously | ||||||
|  | 	 * been passed to {@link #supportsReturnType(MethodParameter)} | ||||||
|  | 	 * and it must have returned {@code true}. | ||||||
|  | 	 * @return {@code Mono<Void>} to indicate when handling is complete. | ||||||
|  | 	 */ | ||||||
|  | 	Mono<Void> handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, Message<?> message); | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,108 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright 2002-2018 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 java.util.ArrayList; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | import org.apache.commons.logging.Log; | ||||||
|  | import org.apache.commons.logging.LogFactory; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.MethodParameter; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A HandlerMethodReturnValueHandler that wraps and delegates to others. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler { | ||||||
|  | 
 | ||||||
|  | 	protected final Log logger = LogFactory.getLog(getClass()); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private final List<HandlerMethodReturnValueHandler> returnValueHandlers = new ArrayList<>(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return a read-only list with the configured handlers. | ||||||
|  | 	 */ | ||||||
|  | 	public List<HandlerMethodReturnValueHandler> getReturnValueHandlers() { | ||||||
|  | 		return Collections.unmodifiableList(this.returnValueHandlers); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Clear the list of configured handlers. | ||||||
|  | 	 */ | ||||||
|  | 	public void clear() { | ||||||
|  | 		this.returnValueHandlers.clear(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Add the given {@link HandlerMethodReturnValueHandler}. | ||||||
|  | 	 */ | ||||||
|  | 	public HandlerMethodReturnValueHandlerComposite addHandler(HandlerMethodReturnValueHandler returnValueHandler) { | ||||||
|  | 		this.returnValueHandlers.add(returnValueHandler); | ||||||
|  | 		return this; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Add the given {@link HandlerMethodReturnValueHandler HandlerMethodReturnValueHandlers}. | ||||||
|  | 	 */ | ||||||
|  | 	public HandlerMethodReturnValueHandlerComposite addHandlers( | ||||||
|  | 			@Nullable List<? extends HandlerMethodReturnValueHandler> handlers) { | ||||||
|  | 
 | ||||||
|  | 		if (handlers != null) { | ||||||
|  | 			this.returnValueHandlers.addAll(handlers); | ||||||
|  | 		} | ||||||
|  | 		return this; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public boolean supportsReturnType(MethodParameter returnType) { | ||||||
|  | 		return getReturnValueHandler(returnType) != null; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public Mono<Void> handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, Message<?> message) { | ||||||
|  | 		HandlerMethodReturnValueHandler handler = getReturnValueHandler(returnType); | ||||||
|  | 		if (handler == null) { | ||||||
|  | 			throw new IllegalStateException("No handler for return value type: " + returnType.getParameterType()); | ||||||
|  | 		} | ||||||
|  | 		if (logger.isTraceEnabled()) { | ||||||
|  | 			logger.trace("Processing return value with " + handler); | ||||||
|  | 		} | ||||||
|  | 		return handler.handleReturnValue(returnValue, returnType, message); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("ForLoopReplaceableByForEach") | ||||||
|  | 	@Nullable | ||||||
|  | 	private HandlerMethodReturnValueHandler getReturnValueHandler(MethodParameter returnType) { | ||||||
|  | 		for (int i = 0; i < this.returnValueHandlers.size(); i++) { | ||||||
|  | 			HandlerMethodReturnValueHandler handler = this.returnValueHandlers.get(i); | ||||||
|  | 			if (handler.supportsReturnType(returnType)) { | ||||||
|  | 				return handler; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return null; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,213 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright 2002-2018 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 java.lang.reflect.InvocationTargetException; | ||||||
|  | import java.lang.reflect.Method; | ||||||
|  | import java.lang.reflect.ParameterizedType; | ||||||
|  | import java.lang.reflect.Type; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.stream.Stream; | ||||||
|  | 
 | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.DefaultParameterNameDiscoverer; | ||||||
|  | import org.springframework.core.MethodParameter; | ||||||
|  | import org.springframework.core.ParameterNameDiscoverer; | ||||||
|  | import org.springframework.core.ReactiveAdapter; | ||||||
|  | import org.springframework.core.ReactiveAdapterRegistry; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | import org.springframework.messaging.handler.HandlerMethod; | ||||||
|  | import org.springframework.messaging.handler.invocation.MethodArgumentResolutionException; | ||||||
|  | import org.springframework.util.ObjectUtils; | ||||||
|  | import org.springframework.util.ReflectionUtils; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Extension of {@link HandlerMethod} that invokes the underlying method with | ||||||
|  |  * argument values resolved from the current HTTP request through a list of | ||||||
|  |  * {@link HandlerMethodArgumentResolver}. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | public class InvocableHandlerMethod extends HandlerMethod { | ||||||
|  | 
 | ||||||
|  | 	private static final Mono<Object[]> EMPTY_ARGS = Mono.just(new Object[0]); | ||||||
|  | 
 | ||||||
|  | 	private static final Object NO_ARG_VALUE = new Object(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite(); | ||||||
|  | 
 | ||||||
|  | 	private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); | ||||||
|  | 
 | ||||||
|  | 	private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Create an instance from a {@code HandlerMethod}. | ||||||
|  | 	 */ | ||||||
|  | 	public InvocableHandlerMethod(HandlerMethod handlerMethod) { | ||||||
|  | 		super(handlerMethod); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Create an instance from a bean instance and a method. | ||||||
|  | 	 */ | ||||||
|  | 	public InvocableHandlerMethod(Object bean, Method method) { | ||||||
|  | 		super(bean, method); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Configure the argument resolvers to use to use for resolving method | ||||||
|  | 	 * argument values against a {@code ServerWebExchange}. | ||||||
|  | 	 */ | ||||||
|  | 	public void setArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { | ||||||
|  | 		this.resolvers.addResolvers(resolvers); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the configured argument resolvers. | ||||||
|  | 	 */ | ||||||
|  | 	public List<HandlerMethodArgumentResolver> getResolvers() { | ||||||
|  | 		return this.resolvers.getResolvers(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Set the ParameterNameDiscoverer for resolving parameter names when needed | ||||||
|  | 	 * (e.g. default request attribute name). | ||||||
|  | 	 * <p>Default is a {@link DefaultParameterNameDiscoverer}. | ||||||
|  | 	 */ | ||||||
|  | 	public void setParameterNameDiscoverer(ParameterNameDiscoverer nameDiscoverer) { | ||||||
|  | 		this.parameterNameDiscoverer = nameDiscoverer; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the configured parameter name discoverer. | ||||||
|  | 	 */ | ||||||
|  | 	public ParameterNameDiscoverer getParameterNameDiscoverer() { | ||||||
|  | 		return this.parameterNameDiscoverer; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Configure a reactive registry. This is needed for cases where the response | ||||||
|  | 	 * is fully handled within the controller in combination with an async void | ||||||
|  | 	 * return value. | ||||||
|  | 	 * <p>By default this is an instance of {@link ReactiveAdapterRegistry} with | ||||||
|  | 	 * default settings. | ||||||
|  | 	 * @param registry the registry to use | ||||||
|  | 	 */ | ||||||
|  | 	public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) { | ||||||
|  | 		this.reactiveAdapterRegistry = registry; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Invoke the method for the given exchange. | ||||||
|  | 	 * @param message the current message | ||||||
|  | 	 * @param providedArgs optional list of argument values to match by type | ||||||
|  | 	 * @return a Mono with the result from the invocation. | ||||||
|  | 	 */ | ||||||
|  | 	public Mono<Object> invoke(Message<?> message, Object... providedArgs) { | ||||||
|  | 
 | ||||||
|  | 		return getMethodArgumentValues(message, providedArgs).flatMap(args -> { | ||||||
|  | 			Object value; | ||||||
|  | 			try { | ||||||
|  | 				ReflectionUtils.makeAccessible(getBridgedMethod()); | ||||||
|  | 				value = getBridgedMethod().invoke(getBean(), args); | ||||||
|  | 			} | ||||||
|  | 			catch (IllegalArgumentException ex) { | ||||||
|  | 				assertTargetBean(getBridgedMethod(), getBean(), args); | ||||||
|  | 				String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument"); | ||||||
|  | 				return Mono.error(new IllegalStateException(formatInvokeError(text, args), ex)); | ||||||
|  | 			} | ||||||
|  | 			catch (InvocationTargetException ex) { | ||||||
|  | 				return Mono.error(ex.getTargetException()); | ||||||
|  | 			} | ||||||
|  | 			catch (Throwable ex) { | ||||||
|  | 				// Unlikely to ever get here, but it must be handled... | ||||||
|  | 				return Mono.error(new IllegalStateException(formatInvokeError("Invocation failure", args), ex)); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			MethodParameter returnType = getReturnType(); | ||||||
|  | 			ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(returnType.getParameterType()); | ||||||
|  | 			return isAsyncVoidReturnType(returnType, adapter) ? | ||||||
|  | 					Mono.from(adapter.toPublisher(value)) : Mono.justOrEmpty(value); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private Mono<Object[]> getMethodArgumentValues(Message<?> message, Object... providedArgs) { | ||||||
|  | 		if (ObjectUtils.isEmpty(getMethodParameters())) { | ||||||
|  | 			return EMPTY_ARGS; | ||||||
|  | 		} | ||||||
|  | 		MethodParameter[] parameters = getMethodParameters(); | ||||||
|  | 		List<Mono<Object>> argMonos = new ArrayList<>(parameters.length); | ||||||
|  | 		for (MethodParameter parameter : parameters) { | ||||||
|  | 			parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); | ||||||
|  | 			Object providedArg = findProvidedArgument(parameter, providedArgs); | ||||||
|  | 			if (providedArg != null) { | ||||||
|  | 				argMonos.add(Mono.just(providedArg)); | ||||||
|  | 				continue; | ||||||
|  | 			} | ||||||
|  | 			if (!this.resolvers.supportsParameter(parameter)) { | ||||||
|  | 				return Mono.error(new MethodArgumentResolutionException( | ||||||
|  | 						message, parameter, formatArgumentError(parameter, "No suitable resolver"))); | ||||||
|  | 			} | ||||||
|  | 			try { | ||||||
|  | 				argMonos.add(this.resolvers.resolveArgument(parameter, message) | ||||||
|  | 						.defaultIfEmpty(NO_ARG_VALUE) | ||||||
|  | 						.doOnError(cause -> logArgumentErrorIfNecessary(parameter, cause))); | ||||||
|  | 			} | ||||||
|  | 			catch (Exception ex) { | ||||||
|  | 				logArgumentErrorIfNecessary(parameter, ex); | ||||||
|  | 				argMonos.add(Mono.error(ex)); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return Mono.zip(argMonos, values -> | ||||||
|  | 				Stream.of(values).map(o -> o != NO_ARG_VALUE ? o : null).toArray()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private void logArgumentErrorIfNecessary(MethodParameter parameter, Throwable cause) { | ||||||
|  | 		// Leave stack trace for later, if error is not handled.. | ||||||
|  | 		String causeMessage = cause.getMessage(); | ||||||
|  | 		if (!causeMessage.contains(parameter.getExecutable().toGenericString())) { | ||||||
|  | 			if (logger.isDebugEnabled()) { | ||||||
|  | 				logger.debug(formatArgumentError(parameter, causeMessage)); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private boolean isAsyncVoidReturnType(MethodParameter returnType, @Nullable ReactiveAdapter reactiveAdapter) { | ||||||
|  | 		if (reactiveAdapter != null && reactiveAdapter.supportsEmpty()) { | ||||||
|  | 			if (reactiveAdapter.isNoValue()) { | ||||||
|  | 				return true; | ||||||
|  | 			} | ||||||
|  | 			Type parameterType = returnType.getGenericParameterType(); | ||||||
|  | 			if (parameterType instanceof ParameterizedType) { | ||||||
|  | 				ParameterizedType type = (ParameterizedType) parameterType; | ||||||
|  | 				if (type.getActualTypeArguments().length == 1) { | ||||||
|  | 					return Void.class.equals(type.getActualTypeArguments()[0]); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return false; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,208 @@ | ||||||
|  | /* | ||||||
|  |  * 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 java.lang.reflect.Method; | ||||||
|  | import java.util.LinkedHashMap; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.concurrent.ConcurrentHashMap; | ||||||
|  | import java.util.function.Function; | ||||||
|  | 
 | ||||||
|  | import org.apache.commons.logging.Log; | ||||||
|  | import org.apache.commons.logging.LogFactory; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.MethodParameter; | ||||||
|  | import org.springframework.core.ReactiveAdapterRegistry; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | import org.springframework.messaging.handler.HandlerMethod; | ||||||
|  | import org.springframework.messaging.handler.MessagingAdviceBean; | ||||||
|  | import org.springframework.messaging.handler.invocation.AbstractExceptionHandlerMethodResolver; | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Help to initialize and invoke an {@link InvocableHandlerMethod}, and to then | ||||||
|  |  * apply return value handling and exception handling. Holds all necessary | ||||||
|  |  * configuration necessary to do so. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | class InvocableHelper { | ||||||
|  | 
 | ||||||
|  | 	private static Log logger = LogFactory.getLog(InvocableHelper.class); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private final HandlerMethodArgumentResolverComposite argumentResolvers = | ||||||
|  | 			new HandlerMethodArgumentResolverComposite(); | ||||||
|  | 
 | ||||||
|  | 	private final HandlerMethodReturnValueHandlerComposite returnValueHandlers = | ||||||
|  | 			new HandlerMethodReturnValueHandlerComposite(); | ||||||
|  | 
 | ||||||
|  | 	private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); | ||||||
|  | 
 | ||||||
|  | 	private final Function<Class<?>, AbstractExceptionHandlerMethodResolver> exceptionMethodResolverFactory; | ||||||
|  | 
 | ||||||
|  | 	private final Map<Class<?>, AbstractExceptionHandlerMethodResolver> exceptionHandlerCache = | ||||||
|  | 			new ConcurrentHashMap<>(64); | ||||||
|  | 
 | ||||||
|  | 	private final Map<MessagingAdviceBean, AbstractExceptionHandlerMethodResolver> exceptionHandlerAdviceCache = | ||||||
|  | 			new LinkedHashMap<>(64); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	public InvocableHelper( | ||||||
|  | 			Function<Class<?>, AbstractExceptionHandlerMethodResolver> exceptionMethodResolverFactory) { | ||||||
|  | 
 | ||||||
|  | 		this.exceptionMethodResolverFactory = exceptionMethodResolverFactory; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Add the arguments resolvers to use for message handling and exception | ||||||
|  | 	 * handling methods. | ||||||
|  | 	 */ | ||||||
|  | 	public void addArgumentResolvers(List<? extends HandlerMethodArgumentResolver> resolvers) { | ||||||
|  | 		this.argumentResolvers.addResolvers(resolvers); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Add the return value handlers to use for message handling and exception | ||||||
|  | 	 * handling methods. | ||||||
|  | 	 */ | ||||||
|  | 	public void addReturnValueHandlers(List<? extends HandlerMethodReturnValueHandler> handlers) { | ||||||
|  | 		this.returnValueHandlers.addHandlers(handlers); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Configure the registry for adapting various reactive types. | ||||||
|  | 	 * <p>By default this is an instance of {@link ReactiveAdapterRegistry} with | ||||||
|  | 	 * default settings. | ||||||
|  | 	 */ | ||||||
|  | 	public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) { | ||||||
|  | 		Assert.notNull(registry, "ReactiveAdapterRegistry is required"); | ||||||
|  | 		this.reactiveAdapterRegistry = registry; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the configured registry for adapting reactive types. | ||||||
|  | 	 */ | ||||||
|  | 	public ReactiveAdapterRegistry getReactiveAdapterRegistry() { | ||||||
|  | 		return this.reactiveAdapterRegistry; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Method to populate the MessagingAdviceBean cache (e.g. to support "global" | ||||||
|  | 	 * {@code @MessageExceptionHandler}). | ||||||
|  | 	 */ | ||||||
|  | 	public void registerExceptionHandlerAdvice( | ||||||
|  | 			MessagingAdviceBean bean, AbstractExceptionHandlerMethodResolver resolver) { | ||||||
|  | 
 | ||||||
|  | 		this.exceptionHandlerAdviceCache.put(bean, resolver); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Create {@link InvocableHandlerMethod} with the configured arg resolvers. | ||||||
|  | 	 * @param handlerMethod the target handler method to invoke | ||||||
|  | 	 * @return the created instance | ||||||
|  | 	 */ | ||||||
|  | 
 | ||||||
|  | 	public InvocableHandlerMethod initMessageMappingMethod(HandlerMethod handlerMethod) { | ||||||
|  | 		InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod); | ||||||
|  | 		invocable.setArgumentResolvers(this.argumentResolvers.getResolvers()); | ||||||
|  | 		return invocable; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Find an exception handling method for the given exception. | ||||||
|  | 	 * <p>The default implementation searches methods in the class hierarchy of | ||||||
|  | 	 * the HandlerMethod first and if not found, it continues searching for | ||||||
|  | 	 * additional handling methods registered via | ||||||
|  | 	 * {@link #registerExceptionHandlerAdvice}. | ||||||
|  | 	 * @param handlerMethod the method where the exception was raised | ||||||
|  | 	 * @param ex the exception raised or signaled | ||||||
|  | 	 * @return a method to handle the exception, or {@code null} | ||||||
|  | 	 */ | ||||||
|  | 	@Nullable | ||||||
|  | 	public InvocableHandlerMethod initExceptionHandlerMethod(HandlerMethod handlerMethod, Throwable ex) { | ||||||
|  | 		if (logger.isDebugEnabled()) { | ||||||
|  | 			logger.debug("Searching for methods to handle " + ex.getClass().getSimpleName()); | ||||||
|  | 		} | ||||||
|  | 		Class<?> beanType = handlerMethod.getBeanType(); | ||||||
|  | 		AbstractExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(beanType); | ||||||
|  | 		if (resolver == null) { | ||||||
|  | 			resolver = this.exceptionMethodResolverFactory.apply(beanType); | ||||||
|  | 			this.exceptionHandlerCache.put(beanType, resolver); | ||||||
|  | 		} | ||||||
|  | 		InvocableHandlerMethod exceptionHandlerMethod = null; | ||||||
|  | 		Method method = resolver.resolveMethod(ex); | ||||||
|  | 		if (method != null) { | ||||||
|  | 			exceptionHandlerMethod = new InvocableHandlerMethod(handlerMethod.getBean(), method); | ||||||
|  | 		} | ||||||
|  | 		else { | ||||||
|  | 			for (MessagingAdviceBean advice : this.exceptionHandlerAdviceCache.keySet()) { | ||||||
|  | 				if (advice.isApplicableToBeanType(beanType)) { | ||||||
|  | 					resolver = this.exceptionHandlerAdviceCache.get(advice); | ||||||
|  | 					method = resolver.resolveMethod(ex); | ||||||
|  | 					if (method != null) { | ||||||
|  | 						exceptionHandlerMethod = new InvocableHandlerMethod(advice.resolveBean(), method); | ||||||
|  | 						break; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if (exceptionHandlerMethod != null) { | ||||||
|  | 			logger.debug("Found exception handler " + exceptionHandlerMethod.getShortLogMessage()); | ||||||
|  | 			exceptionHandlerMethod.setArgumentResolvers(this.argumentResolvers.getResolvers()); | ||||||
|  | 		} | ||||||
|  | 		else { | ||||||
|  | 			logger.error("No exception handling method", ex); | ||||||
|  | 		} | ||||||
|  | 		return exceptionHandlerMethod; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	public Mono<Void> handleMessage(HandlerMethod handlerMethod, Message<?> message) { | ||||||
|  | 		InvocableHandlerMethod invocable = initMessageMappingMethod(handlerMethod); | ||||||
|  | 		if (logger.isDebugEnabled()) { | ||||||
|  | 			logger.debug("Invoking " + invocable.getShortLogMessage()); | ||||||
|  | 		} | ||||||
|  | 		return invocable.invoke(message) | ||||||
|  | 				.switchIfEmpty(Mono.defer(() -> handleReturnValue(null, invocable, message))) | ||||||
|  | 				.flatMap(returnValue -> handleReturnValue(returnValue, invocable, message)) | ||||||
|  | 				.onErrorResume(ex -> { | ||||||
|  | 					InvocableHandlerMethod exHandler = initExceptionHandlerMethod(handlerMethod, ex); | ||||||
|  | 					if (exHandler == null) { | ||||||
|  | 						return Mono.error(ex); | ||||||
|  | 					} | ||||||
|  | 					if (logger.isDebugEnabled()) { | ||||||
|  | 						logger.debug("Invoking " + exHandler.getShortLogMessage()); | ||||||
|  | 					} | ||||||
|  | 					return exHandler.invoke(message, ex) | ||||||
|  | 							.switchIfEmpty(Mono.defer(() -> handleReturnValue(null, exHandler, message))) | ||||||
|  | 							.flatMap(returnValue -> handleReturnValue(returnValue, exHandler, message)); | ||||||
|  | 				}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private Mono<Void> handleReturnValue( | ||||||
|  | 			@Nullable Object returnValue, HandlerMethod handlerMethod, Message<?> message) { | ||||||
|  | 
 | ||||||
|  | 		MethodParameter returnType = handlerMethod.getReturnType(); | ||||||
|  | 		return this.returnValueHandlers.handleReturnValue(returnValue, returnType, message); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,50 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright 2002-2018 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 java.util.ArrayList; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Assist with configuration for handler method return value handlers. | ||||||
|  |  * At present, it supports only providing a list of custom handlers. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | public class ReturnValueHandlerConfigurer { | ||||||
|  | 
 | ||||||
|  | 	private final List<HandlerMethodReturnValueHandler> customHandlers = new ArrayList<>(8); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Configure custom return value handlers for handler methods. | ||||||
|  | 	 * @param handlers the handlers to add | ||||||
|  | 	 */ | ||||||
|  | 	public void addCustomHandler(HandlerMethodReturnValueHandler... handlers) { | ||||||
|  | 		Assert.notNull(handlers, "'handlers' must not be null"); | ||||||
|  | 		this.customHandlers.addAll(Arrays.asList(handlers)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	public List<HandlerMethodReturnValueHandler> getCustomHandlers() { | ||||||
|  | 		return this.customHandlers; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -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} | ||||||
|  | 	 * <p>By default this simply delegates to {@link #resolveArgumentValue} for | ||||||
|  | 	 * synchronous resolution. | ||||||
|  | 	 */ | ||||||
|  | 	@Override | ||||||
|  | 	default Mono<Object> 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); | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,10 @@ | ||||||
|  | /** | ||||||
|  |  * Common infrastructure for invoking message handler methods with non-blocking, | ||||||
|  |  * and reactive contracts. | ||||||
|  |  */ | ||||||
|  | @NonNullApi | ||||||
|  | @NonNullFields | ||||||
|  | package org.springframework.messaging.handler.invocation.reactive; | ||||||
|  | 
 | ||||||
|  | import org.springframework.lang.NonNullApi; | ||||||
|  | import org.springframework.lang.NonNullFields; | ||||||
|  | @ -0,0 +1,272 @@ | ||||||
|  | /* | ||||||
|  |  * 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.rsocket; | ||||||
|  | 
 | ||||||
|  | import java.nio.charset.StandardCharsets; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.Map; | ||||||
|  | 
 | ||||||
|  | import io.rsocket.Payload; | ||||||
|  | import io.rsocket.RSocket; | ||||||
|  | import org.reactivestreams.Publisher; | ||||||
|  | import reactor.core.publisher.Flux; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.ParameterizedTypeReference; | ||||||
|  | import org.springframework.core.ReactiveAdapter; | ||||||
|  | import org.springframework.core.ResolvableType; | ||||||
|  | import org.springframework.core.codec.Decoder; | ||||||
|  | import org.springframework.core.codec.Encoder; | ||||||
|  | import org.springframework.core.io.buffer.DataBuffer; | ||||||
|  | import org.springframework.core.io.buffer.DataBufferUtils; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | import org.springframework.util.MimeType; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Default, package-private {@link RSocketRequester} implementation. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | final class DefaultRSocketRequester implements RSocketRequester { | ||||||
|  | 
 | ||||||
|  | 	private static final Map<String, Object> EMPTY_HINTS = Collections.emptyMap(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private final RSocket rsocket; | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	private final MimeType dataMimeType; | ||||||
|  | 
 | ||||||
|  | 	private final RSocketStrategies strategies; | ||||||
|  | 
 | ||||||
|  | 	private DataBuffer emptyDataBuffer; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	DefaultRSocketRequester(RSocket rsocket, @Nullable MimeType dataMimeType, RSocketStrategies strategies) { | ||||||
|  | 		Assert.notNull(rsocket, "RSocket is required"); | ||||||
|  | 		Assert.notNull(strategies, "RSocketStrategies is required"); | ||||||
|  | 		this.rsocket = rsocket; | ||||||
|  | 		this.dataMimeType = dataMimeType; | ||||||
|  | 		this.strategies = strategies; | ||||||
|  | 		this.emptyDataBuffer = this.strategies.dataBufferFactory().wrap(new byte[0]); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public RSocket rsocket() { | ||||||
|  | 		return this.rsocket; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public RequestSpec route(String route) { | ||||||
|  | 		return new DefaultRequestSpec(route); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private static boolean isVoid(ResolvableType elementType) { | ||||||
|  | 		return Void.class.equals(elementType.resolve()) || void.class.equals(elementType.resolve()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private class DefaultRequestSpec implements RequestSpec { | ||||||
|  | 
 | ||||||
|  | 		private final String route; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		DefaultRequestSpec(String route) { | ||||||
|  | 			this.route = route; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public ResponseSpec data(Object data) { | ||||||
|  | 			Assert.notNull(data, "'data' must not be null"); | ||||||
|  | 			return toResponseSpec(data, ResolvableType.NONE); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public <T, P extends Publisher<T>> ResponseSpec data(P publisher, Class<T> dataType) { | ||||||
|  | 			Assert.notNull(publisher, "'publisher' must not be null"); | ||||||
|  | 			Assert.notNull(dataType, "'dataType' must not be null"); | ||||||
|  | 			return toResponseSpec(publisher, ResolvableType.forClass(dataType)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public <T, P extends Publisher<T>> ResponseSpec data(P publisher, ParameterizedTypeReference<T> dataTypeRef) { | ||||||
|  | 			Assert.notNull(publisher, "'publisher' must not be null"); | ||||||
|  | 			Assert.notNull(dataTypeRef, "'dataTypeRef' must not be null"); | ||||||
|  | 			return toResponseSpec(publisher, ResolvableType.forType(dataTypeRef)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private ResponseSpec toResponseSpec(Object input, ResolvableType dataType) { | ||||||
|  | 			ReactiveAdapter adapter = strategies.reactiveAdapterRegistry().getAdapter(input.getClass()); | ||||||
|  | 			Publisher<?> publisher; | ||||||
|  | 			if (input instanceof Publisher) { | ||||||
|  | 				publisher = (Publisher<?>) input; | ||||||
|  | 			} | ||||||
|  | 			else if (adapter != null) { | ||||||
|  | 				publisher = adapter.toPublisher(input); | ||||||
|  | 			} | ||||||
|  | 			else { | ||||||
|  | 				Mono<Payload> payloadMono = encodeValue(input, ResolvableType.forInstance(input), null) | ||||||
|  | 						.map(this::firstPayload) | ||||||
|  | 						.switchIfEmpty(emptyPayload()); | ||||||
|  | 				return new DefaultResponseSpec(payloadMono); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if (isVoid(dataType) || (adapter != null && adapter.isNoValue())) { | ||||||
|  | 				Mono<Payload> payloadMono = Mono.when(publisher).then(emptyPayload()); | ||||||
|  | 				return new DefaultResponseSpec(payloadMono); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			Encoder<?> encoder = dataType != ResolvableType.NONE && !Object.class.equals(dataType.resolve()) ? | ||||||
|  | 					strategies.encoder(dataType, dataMimeType) : null; | ||||||
|  | 
 | ||||||
|  | 			if (adapter != null && !adapter.isMultiValue()) { | ||||||
|  | 				Mono<Payload> payloadMono = Mono.from(publisher) | ||||||
|  | 						.flatMap(value -> encodeValue(value, dataType, encoder)) | ||||||
|  | 						.map(this::firstPayload) | ||||||
|  | 						.switchIfEmpty(emptyPayload()); | ||||||
|  | 				return new DefaultResponseSpec(payloadMono); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			Flux<Payload> payloadFlux = Flux.from(publisher) | ||||||
|  | 					.concatMap(value -> encodeValue(value, dataType, encoder)) | ||||||
|  | 					.switchOnFirst((signal, inner) -> { | ||||||
|  | 						DataBuffer data = signal.get(); | ||||||
|  | 						if (data != null) { | ||||||
|  | 							return Flux.concat( | ||||||
|  | 									Mono.just(firstPayload(data)), | ||||||
|  | 									inner.skip(1).map(PayloadUtils::createPayload)); | ||||||
|  | 						} | ||||||
|  | 						else { | ||||||
|  | 							return inner.map(PayloadUtils::createPayload); | ||||||
|  | 						} | ||||||
|  | 					}) | ||||||
|  | 					.switchIfEmpty(emptyPayload()); | ||||||
|  | 			return new DefaultResponseSpec(payloadFlux); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@SuppressWarnings("unchecked") | ||||||
|  | 		private <T> Mono<DataBuffer> encodeValue(T value, ResolvableType valueType, @Nullable Encoder<?> encoder) { | ||||||
|  | 			if (encoder == null) { | ||||||
|  | 				encoder = strategies.encoder(ResolvableType.forInstance(value), dataMimeType); | ||||||
|  | 			} | ||||||
|  | 			return DataBufferUtils.join(((Encoder<T>) encoder).encode( | ||||||
|  | 					Mono.just(value), strategies.dataBufferFactory(), valueType, dataMimeType, EMPTY_HINTS)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private Payload firstPayload(DataBuffer data) { | ||||||
|  | 			return PayloadUtils.createPayload(getMetadata(), data); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private Mono<Payload> emptyPayload() { | ||||||
|  | 			return Mono.fromCallable(() -> firstPayload(emptyDataBuffer)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private DataBuffer getMetadata() { | ||||||
|  | 			return strategies.dataBufferFactory().wrap(this.route.getBytes(StandardCharsets.UTF_8)); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private class DefaultResponseSpec implements ResponseSpec { | ||||||
|  | 
 | ||||||
|  | 		@Nullable | ||||||
|  | 		private final Mono<Payload> payloadMono; | ||||||
|  | 
 | ||||||
|  | 		@Nullable | ||||||
|  | 		private final Flux<Payload> payloadFlux; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		DefaultResponseSpec(Mono<Payload> payloadMono) { | ||||||
|  | 			this.payloadMono = payloadMono; | ||||||
|  | 			this.payloadFlux = null; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		DefaultResponseSpec(Flux<Payload> payloadFlux) { | ||||||
|  | 			this.payloadMono = null; | ||||||
|  | 			this.payloadFlux = payloadFlux; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public Mono<Void> send() { | ||||||
|  | 			Assert.notNull(this.payloadMono, "No RSocket interaction model for one-way send with Flux."); | ||||||
|  | 			return this.payloadMono.flatMap(rsocket::fireAndForget); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public <T> Mono<T> retrieveMono(Class<T> dataType) { | ||||||
|  | 			return retrieveMono(ResolvableType.forClass(dataType)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public <T> Mono<T> retrieveMono(ParameterizedTypeReference<T> dataTypeRef) { | ||||||
|  | 			return retrieveMono(ResolvableType.forType(dataTypeRef)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public <T> Flux<T> retrieveFlux(Class<T> dataType) { | ||||||
|  | 			return retrieveFlux(ResolvableType.forClass(dataType)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public <T> Flux<T> retrieveFlux(ParameterizedTypeReference<T> dataTypeRef) { | ||||||
|  | 			return retrieveFlux(ResolvableType.forType(dataTypeRef)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@SuppressWarnings("unchecked") | ||||||
|  | 		private <T> Mono<T> retrieveMono(ResolvableType elementType) { | ||||||
|  | 			Assert.notNull(this.payloadMono, | ||||||
|  | 					"No RSocket interaction model for Flux request to Mono response."); | ||||||
|  | 
 | ||||||
|  | 			Mono<Payload> payloadMono = this.payloadMono.flatMap(rsocket::requestResponse); | ||||||
|  | 
 | ||||||
|  | 			if (isVoid(elementType)) { | ||||||
|  | 				return (Mono<T>) payloadMono.then(); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			Decoder<?> decoder = strategies.decoder(elementType, dataMimeType); | ||||||
|  | 			return (Mono<T>) decoder.decodeToMono( | ||||||
|  | 					payloadMono.map(this::retainDataAndReleasePayload), elementType, dataMimeType, EMPTY_HINTS); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@SuppressWarnings("unchecked") | ||||||
|  | 		private <T> Flux<T> retrieveFlux(ResolvableType elementType) { | ||||||
|  | 
 | ||||||
|  | 			Flux<Payload> payloadFlux = this.payloadMono != null ? | ||||||
|  | 					this.payloadMono.flatMapMany(rsocket::requestStream) : | ||||||
|  | 					rsocket.requestChannel(this.payloadFlux); | ||||||
|  | 
 | ||||||
|  | 			if (isVoid(elementType)) { | ||||||
|  | 				return payloadFlux.thenMany(Flux.empty()); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			Decoder<?> decoder = strategies.decoder(elementType, dataMimeType); | ||||||
|  | 
 | ||||||
|  | 			return payloadFlux.map(this::retainDataAndReleasePayload).concatMap(dataBuffer -> | ||||||
|  | 					(Mono<T>) decoder.decodeToMono(Mono.just(dataBuffer), elementType, dataMimeType, EMPTY_HINTS)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private DataBuffer retainDataAndReleasePayload(Payload payload) { | ||||||
|  | 			return PayloadUtils.retainDataAndReleasePayload(payload, strategies.dataBufferFactory()); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,140 @@ | ||||||
|  | /* | ||||||
|  |  * 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.rsocket; | ||||||
|  | 
 | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.function.Consumer; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.ReactiveAdapterRegistry; | ||||||
|  | import org.springframework.core.codec.Decoder; | ||||||
|  | import org.springframework.core.codec.Encoder; | ||||||
|  | import org.springframework.core.io.buffer.DataBufferFactory; | ||||||
|  | import org.springframework.core.io.buffer.DefaultDataBufferFactory; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Default, package-private {@link RSocketStrategies} implementation. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | final class DefaultRSocketStrategies implements RSocketStrategies { | ||||||
|  | 
 | ||||||
|  | 	private final List<Encoder<?>> encoders; | ||||||
|  | 
 | ||||||
|  | 	private final List<Decoder<?>> decoders; | ||||||
|  | 
 | ||||||
|  | 	private final ReactiveAdapterRegistry adapterRegistry; | ||||||
|  | 
 | ||||||
|  | 	private final DataBufferFactory bufferFactory; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private DefaultRSocketStrategies( | ||||||
|  | 			List<Encoder<?>> encoders, List<Decoder<?>> decoders, | ||||||
|  | 			ReactiveAdapterRegistry adapterRegistry, DataBufferFactory bufferFactory) { | ||||||
|  | 
 | ||||||
|  | 		this.encoders = Collections.unmodifiableList(encoders); | ||||||
|  | 		this.decoders = Collections.unmodifiableList(decoders); | ||||||
|  | 		this.adapterRegistry = adapterRegistry; | ||||||
|  | 		this.bufferFactory = bufferFactory; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public List<Encoder<?>> encoders() { | ||||||
|  | 		return this.encoders; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public List<Decoder<?>> decoders() { | ||||||
|  | 		return this.decoders; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public ReactiveAdapterRegistry reactiveAdapterRegistry() { | ||||||
|  | 		return this.adapterRegistry; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public DataBufferFactory dataBufferFactory() { | ||||||
|  | 		return this.bufferFactory; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Default RSocketStrategies.Builder implementation. | ||||||
|  | 	 */ | ||||||
|  | 	static class DefaultRSocketStrategiesBuilder implements RSocketStrategies.Builder { | ||||||
|  | 
 | ||||||
|  | 		private final List<Encoder<?>> encoders = new ArrayList<>(); | ||||||
|  | 
 | ||||||
|  | 		private final List<Decoder<?>> decoders = new ArrayList<>(); | ||||||
|  | 
 | ||||||
|  | 		private ReactiveAdapterRegistry adapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); | ||||||
|  | 
 | ||||||
|  | 		@Nullable | ||||||
|  | 		private DataBufferFactory dataBufferFactory; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public Builder encoder(Encoder<?>... encoders) { | ||||||
|  | 			this.encoders.addAll(Arrays.asList(encoders)); | ||||||
|  | 			return this; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public Builder decoder(Decoder<?>... decoder) { | ||||||
|  | 			this.decoders.addAll(Arrays.asList(decoder)); | ||||||
|  | 			return this; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public Builder encoders(Consumer<List<Encoder<?>>> consumer) { | ||||||
|  | 			consumer.accept(this.encoders); | ||||||
|  | 			return this; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public Builder decoders(Consumer<List<Decoder<?>>> consumer) { | ||||||
|  | 			consumer.accept(this.decoders); | ||||||
|  | 			return this; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public Builder reactiveAdapterStrategy(ReactiveAdapterRegistry registry) { | ||||||
|  | 			Assert.notNull(registry, "ReactiveAdapterRegistry is required"); | ||||||
|  | 			this.adapterRegistry = registry; | ||||||
|  | 			return this; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public Builder dataBufferFactory(DataBufferFactory bufferFactory) { | ||||||
|  | 			this.dataBufferFactory = bufferFactory; | ||||||
|  | 			return this; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public RSocketStrategies build() { | ||||||
|  | 			return new DefaultRSocketStrategies(this.encoders, this.decoders, this.adapterRegistry, | ||||||
|  | 					this.dataBufferFactory != null ? this.dataBufferFactory : new DefaultDataBufferFactory()); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,77 @@ | ||||||
|  | /* | ||||||
|  |  * 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.rsocket; | ||||||
|  | 
 | ||||||
|  | import java.util.function.Function; | ||||||
|  | 
 | ||||||
|  | import io.rsocket.ConnectionSetupPayload; | ||||||
|  | import io.rsocket.RSocket; | ||||||
|  | import io.rsocket.SocketAcceptor; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | 
 | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | import org.springframework.util.MimeType; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Extension of {@link RSocketMessageHandler} that can be plugged directly into | ||||||
|  |  * RSocket to receive connections either on the | ||||||
|  |  * {@link io.rsocket.RSocketFactory.ClientRSocketFactory#acceptor(Function) client} or on the | ||||||
|  |  * {@link io.rsocket.RSocketFactory.ServerRSocketFactory#acceptor(SocketAcceptor) server} | ||||||
|  |  * side. Requests are handled by delegating to the "super" {@link #handleMessage(Message)}. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | public final class MessageHandlerAcceptor extends RSocketMessageHandler | ||||||
|  | 		implements SocketAcceptor, Function<RSocket, RSocket> { | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	private MimeType defaultDataMimeType; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Configure the default content type to use for data payloads. | ||||||
|  | 	 * <p>By default this is not set. However a server acceptor will use the | ||||||
|  | 	 * content type from the {@link ConnectionSetupPayload}, so this is typically | ||||||
|  | 	 * required for clients but can also be used on servers as a fallback. | ||||||
|  | 	 * @param defaultDataMimeType the MimeType to use | ||||||
|  | 	 */ | ||||||
|  | 	public void setDefaultDataMimeType(@Nullable MimeType defaultDataMimeType) { | ||||||
|  | 		this.defaultDataMimeType = defaultDataMimeType; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public Mono<RSocket> accept(ConnectionSetupPayload setupPayload, RSocket sendingRSocket) { | ||||||
|  | 		MessagingRSocket rsocket = createRSocket(sendingRSocket); | ||||||
|  | 		// Allow handling of the ConnectionSetupPayload via @MessageMapping methods. | ||||||
|  | 		// However, if the handling is to make requests to the client, it's expected | ||||||
|  | 		// it will do so decoupled from the handling, e.g. via .subscribe(). | ||||||
|  | 		return rsocket.handleConnectionSetupPayload(setupPayload).then(Mono.just(rsocket)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public RSocket apply(RSocket sendingRSocket) { | ||||||
|  | 		return createRSocket(sendingRSocket); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private MessagingRSocket createRSocket(RSocket rsocket) { | ||||||
|  | 		return new MessagingRSocket( | ||||||
|  | 				this::handleMessage, rsocket, this.defaultDataMimeType, getRSocketStrategies()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,195 @@ | ||||||
|  | /* | ||||||
|  |  * 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.rsocket; | ||||||
|  | 
 | ||||||
|  | import java.util.concurrent.atomic.AtomicBoolean; | ||||||
|  | import java.util.function.Function; | ||||||
|  | 
 | ||||||
|  | import io.rsocket.AbstractRSocket; | ||||||
|  | import io.rsocket.ConnectionSetupPayload; | ||||||
|  | import io.rsocket.Payload; | ||||||
|  | import io.rsocket.RSocket; | ||||||
|  | import org.reactivestreams.Publisher; | ||||||
|  | import reactor.core.publisher.Flux; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | import reactor.core.publisher.MonoProcessor; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.io.buffer.DataBuffer; | ||||||
|  | import org.springframework.core.io.buffer.DataBufferFactory; | ||||||
|  | import org.springframework.core.io.buffer.DataBufferUtils; | ||||||
|  | import org.springframework.core.io.buffer.NettyDataBuffer; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | import org.springframework.messaging.MessageHeaders; | ||||||
|  | import org.springframework.messaging.handler.DestinationPatternsMessageCondition; | ||||||
|  | import org.springframework.messaging.handler.invocation.reactive.HandlerMethodReturnValueHandler; | ||||||
|  | import org.springframework.messaging.support.MessageBuilder; | ||||||
|  | import org.springframework.messaging.support.MessageHeaderAccessor; | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | import org.springframework.util.MimeType; | ||||||
|  | import org.springframework.util.MimeTypeUtils; | ||||||
|  | import org.springframework.util.StringUtils; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Implementation of {@link RSocket} that wraps incoming requests with a | ||||||
|  |  * {@link Message}, delegates to a {@link Function} for handling, and then | ||||||
|  |  * obtains the response from a "reply" header. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | class MessagingRSocket extends AbstractRSocket { | ||||||
|  | 
 | ||||||
|  | 	private final Function<Message<?>, Mono<Void>> handler; | ||||||
|  | 
 | ||||||
|  | 	private final RSocketRequester requester; | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	private MimeType dataMimeType; | ||||||
|  | 
 | ||||||
|  | 	private final RSocketStrategies strategies; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	MessagingRSocket(Function<Message<?>, Mono<Void>> handler, RSocket sendingRSocket, | ||||||
|  | 			@Nullable MimeType defaultDataMimeType, RSocketStrategies strategies) { | ||||||
|  | 
 | ||||||
|  | 		Assert.notNull(handler, "'handler' is required"); | ||||||
|  | 		Assert.notNull(sendingRSocket, "'sendingRSocket' is required"); | ||||||
|  | 		this.handler = handler; | ||||||
|  | 		this.requester = RSocketRequester.create(sendingRSocket, defaultDataMimeType, strategies); | ||||||
|  | 		this.dataMimeType = defaultDataMimeType; | ||||||
|  | 		this.strategies = strategies; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Wrap the {@link ConnectionSetupPayload} with a {@link Message} and | ||||||
|  | 	 * delegate to {@link #handle(Payload)} for handling. | ||||||
|  | 	 * @param payload the connection payload | ||||||
|  | 	 * @return completion handle for success or error | ||||||
|  | 	 */ | ||||||
|  | 	public Mono<Void> handleConnectionSetupPayload(ConnectionSetupPayload payload) { | ||||||
|  | 		if (StringUtils.hasText(payload.dataMimeType())) { | ||||||
|  | 			this.dataMimeType = MimeTypeUtils.parseMimeType(payload.dataMimeType()); | ||||||
|  | 		} | ||||||
|  | 		// frameDecoder does not apply to connectionSetupPayload | ||||||
|  | 		// so retain here since handle expects it.. | ||||||
|  | 		payload.retain(); | ||||||
|  | 		return handle(payload); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public Mono<Void> fireAndForget(Payload payload) { | ||||||
|  | 		return handle(payload); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public Mono<Payload> requestResponse(Payload payload) { | ||||||
|  | 		return handleAndReply(payload, Flux.just(payload)).next(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public Flux<Payload> requestStream(Payload payload) { | ||||||
|  | 		return handleAndReply(payload, Flux.just(payload)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public Flux<Payload> requestChannel(Publisher<Payload> payloads) { | ||||||
|  | 		return Flux.from(payloads) | ||||||
|  | 				.switchOnFirst((signal, innerFlux) -> { | ||||||
|  | 					Payload firstPayload = signal.get(); | ||||||
|  | 					return firstPayload == null ? innerFlux : handleAndReply(firstPayload, innerFlux); | ||||||
|  | 				}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public Mono<Void> metadataPush(Payload payload) { | ||||||
|  | 		// Not very useful until createHeaders does more with metadata | ||||||
|  | 		return handle(payload); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private Mono<Void> handle(Payload payload) { | ||||||
|  | 		String destination = getDestination(payload); | ||||||
|  | 		MessageHeaders headers = createHeaders(destination, null); | ||||||
|  | 		DataBuffer dataBuffer = retainDataAndReleasePayload(payload); | ||||||
|  | 		int refCount = refCount(dataBuffer); | ||||||
|  | 		Message<?> message = MessageBuilder.createMessage(dataBuffer, headers); | ||||||
|  | 		return Mono.defer(() -> this.handler.apply(message)) | ||||||
|  | 				.doFinally(s -> { | ||||||
|  | 					if (refCount(dataBuffer) == refCount) { | ||||||
|  | 						DataBufferUtils.release(dataBuffer); | ||||||
|  | 					} | ||||||
|  | 				}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private int refCount(DataBuffer dataBuffer) { | ||||||
|  | 		return dataBuffer instanceof NettyDataBuffer ? | ||||||
|  | 				((NettyDataBuffer) dataBuffer).getNativeBuffer().refCnt() : 1; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private Flux<Payload> handleAndReply(Payload firstPayload, Flux<Payload> payloads) { | ||||||
|  | 		MonoProcessor<Flux<Payload>> replyMono = MonoProcessor.create(); | ||||||
|  | 		String destination = getDestination(firstPayload); | ||||||
|  | 		MessageHeaders headers = createHeaders(destination, replyMono); | ||||||
|  | 
 | ||||||
|  | 		AtomicBoolean read = new AtomicBoolean(); | ||||||
|  | 		Flux<DataBuffer> buffers = payloads.map(this::retainDataAndReleasePayload).doOnSubscribe(s -> read.set(true)); | ||||||
|  | 		Message<Flux<DataBuffer>> message = MessageBuilder.createMessage(buffers, headers); | ||||||
|  | 
 | ||||||
|  | 		return Mono.defer(() -> this.handler.apply(message)) | ||||||
|  | 				.doFinally(s -> { | ||||||
|  | 					// Subscription should have happened by now due to ChannelSendOperator | ||||||
|  | 					if (!read.get()) { | ||||||
|  | 						buffers.subscribe(DataBufferUtils::release); | ||||||
|  | 					} | ||||||
|  | 				}) | ||||||
|  | 				.thenMany(Flux.defer(() -> replyMono.isTerminated() ? | ||||||
|  | 						replyMono.flatMapMany(Function.identity()) : | ||||||
|  | 						Mono.error(new IllegalStateException("Something went wrong: reply Mono not set")))); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private String getDestination(Payload payload) { | ||||||
|  | 
 | ||||||
|  | 		// TODO: | ||||||
|  | 		// For now treat the metadata as a simple string with routing information. | ||||||
|  | 		// We'll have to get more sophisticated once the routing extension is completed. | ||||||
|  | 		// https://github.com/rsocket/rsocket-java/issues/568 | ||||||
|  | 
 | ||||||
|  | 		return payload.getMetadataUtf8(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private DataBuffer retainDataAndReleasePayload(Payload payload) { | ||||||
|  | 		return PayloadUtils.retainDataAndReleasePayload(payload, this.strategies.dataBufferFactory()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private MessageHeaders createHeaders(String destination, @Nullable MonoProcessor<?> replyMono) { | ||||||
|  | 		MessageHeaderAccessor headers = new MessageHeaderAccessor(); | ||||||
|  | 		headers.setHeader(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, destination); | ||||||
|  | 		if (this.dataMimeType != null) { | ||||||
|  | 			headers.setContentType(this.dataMimeType); | ||||||
|  | 		} | ||||||
|  | 		headers.setHeader(RSocketRequesterMethodArgumentResolver.RSOCKET_REQUESTER_HEADER, this.requester); | ||||||
|  | 		if (replyMono != null) { | ||||||
|  | 			headers.setHeader(RSocketPayloadReturnValueHandler.RESPONSE_HEADER, replyMono); | ||||||
|  | 		} | ||||||
|  | 		DataBufferFactory bufferFactory = this.strategies.dataBufferFactory(); | ||||||
|  | 		headers.setHeader(HandlerMethodReturnValueHandler.DATA_BUFFER_FACTORY_HEADER, bufferFactory); | ||||||
|  | 		return headers.getMessageHeaders(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,107 @@ | ||||||
|  | /* | ||||||
|  |  * 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.rsocket; | ||||||
|  | 
 | ||||||
|  | import io.netty.buffer.ByteBuf; | ||||||
|  | import io.rsocket.Frame; | ||||||
|  | import io.rsocket.Payload; | ||||||
|  | import io.rsocket.util.ByteBufPayload; | ||||||
|  | import io.rsocket.util.DefaultPayload; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.io.buffer.DataBuffer; | ||||||
|  | import org.springframework.core.io.buffer.DataBufferFactory; | ||||||
|  | import org.springframework.core.io.buffer.DefaultDataBuffer; | ||||||
|  | import org.springframework.core.io.buffer.NettyDataBuffer; | ||||||
|  | import org.springframework.core.io.buffer.NettyDataBufferFactory; | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Static utility methods to create {@link Payload} from {@link DataBuffer}s | ||||||
|  |  * and vice versa. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | abstract class PayloadUtils { | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Use this method to slice, retain and wrap the data portion of the | ||||||
|  | 	 * {@code Payload}, and also to release the {@code Payload}. This assumes | ||||||
|  | 	 * the Payload metadata has been read by now and ensures downstream code | ||||||
|  | 	 * need only be aware of {@code DataBuffer}s. | ||||||
|  | 	 * @param payload the payload to process | ||||||
|  | 	 * @param bufferFactory the DataBufferFactory to wrap with | ||||||
|  | 	 * @return the created {@code DataBuffer} instance | ||||||
|  | 	 */ | ||||||
|  | 	public static DataBuffer retainDataAndReleasePayload(Payload payload, DataBufferFactory bufferFactory) { | ||||||
|  | 		try { | ||||||
|  | 			if (bufferFactory instanceof NettyDataBufferFactory) { | ||||||
|  | 				ByteBuf byteBuf = payload.sliceData().retain(); | ||||||
|  | 				return ((NettyDataBufferFactory) bufferFactory).wrap(byteBuf); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			Assert.isTrue(!(payload instanceof ByteBufPayload) && !(payload instanceof Frame), | ||||||
|  | 					"NettyDataBufferFactory expected, actual: " + bufferFactory.getClass().getSimpleName()); | ||||||
|  | 
 | ||||||
|  | 			return bufferFactory.wrap(payload.getData()); | ||||||
|  | 		} | ||||||
|  | 		finally { | ||||||
|  | 			if (payload.refCnt() > 0) { | ||||||
|  | 				payload.release(); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Create a Payload from the given metadata and data. | ||||||
|  | 	 * @param metadata the metadata part for the payload | ||||||
|  | 	 * @param data the data part for the payload | ||||||
|  | 	 * @return the created Payload | ||||||
|  | 	 */ | ||||||
|  | 	public static Payload createPayload(DataBuffer metadata, DataBuffer data) { | ||||||
|  | 		if (metadata instanceof NettyDataBuffer && data instanceof NettyDataBuffer) { | ||||||
|  | 			return ByteBufPayload.create( | ||||||
|  | 					((NettyDataBuffer) data).getNativeBuffer(), | ||||||
|  | 					((NettyDataBuffer) metadata).getNativeBuffer()); | ||||||
|  | 		} | ||||||
|  | 		else if (metadata instanceof DefaultDataBuffer && data instanceof DefaultDataBuffer) { | ||||||
|  | 			return DefaultPayload.create( | ||||||
|  | 					((DefaultDataBuffer) data).getNativeBuffer(), | ||||||
|  | 					((DefaultDataBuffer) metadata).getNativeBuffer()); | ||||||
|  | 		} | ||||||
|  | 		else { | ||||||
|  | 			return DefaultPayload.create(data.asByteBuffer(), metadata.asByteBuffer()); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Create a Payload from the given data. | ||||||
|  | 	 * @param data the data part for the payload | ||||||
|  | 	 * @return the created Payload | ||||||
|  | 	 */ | ||||||
|  | 	public static Payload createPayload(DataBuffer data) { | ||||||
|  | 		if (data instanceof NettyDataBuffer) { | ||||||
|  | 			return ByteBufPayload.create(((NettyDataBuffer) data).getNativeBuffer()); | ||||||
|  | 		} | ||||||
|  | 		else if (data instanceof DefaultDataBuffer) { | ||||||
|  | 			return DefaultPayload.create(((DefaultDataBuffer) data).getNativeBuffer()); | ||||||
|  | 		} | ||||||
|  | 		else { | ||||||
|  | 			return DefaultPayload.create(data.asByteBuffer()); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,125 @@ | ||||||
|  | /* | ||||||
|  |  * 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.rsocket; | ||||||
|  | 
 | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.codec.Decoder; | ||||||
|  | import org.springframework.core.codec.Encoder; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | import org.springframework.messaging.MessageDeliveryException; | ||||||
|  | import org.springframework.messaging.handler.annotation.support.reactive.MessageMappingMessageHandler; | ||||||
|  | import org.springframework.messaging.handler.invocation.reactive.HandlerMethodReturnValueHandler; | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | import org.springframework.util.StringUtils; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * RSocket-specific extension of {@link MessageMappingMessageHandler}. | ||||||
|  |  * | ||||||
|  |  * <p>The configured {@link #setEncoders(List) encoders} are used to encode the | ||||||
|  |  * return values from handler methods, with the help of | ||||||
|  |  * {@link RSocketPayloadReturnValueHandler}. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | public class RSocketMessageHandler extends MessageMappingMessageHandler { | ||||||
|  | 
 | ||||||
|  | 	private final List<Encoder<?>> encoders = new ArrayList<>(); | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	private RSocketStrategies rsocketStrategies; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Configure the encoders to use for encoding handler method return values. | ||||||
|  | 	 */ | ||||||
|  | 	public void setEncoders(List<? extends Encoder<?>> encoders) { | ||||||
|  | 		this.encoders.addAll(encoders); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the configured {@link #setEncoders(List) encoders}. | ||||||
|  | 	 */ | ||||||
|  | 	public List<? extends Encoder<?>> getEncoders() { | ||||||
|  | 		return this.encoders; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Provide configuration in the form of {@link RSocketStrategies}. This is | ||||||
|  | 	 * an alternative to using {@link #setEncoders(List)}, | ||||||
|  | 	 * {@link #setDecoders(List)}, and others directly. It is convenient when | ||||||
|  | 	 * you also configuring an {@link RSocketRequester} in which case the | ||||||
|  | 	 * {@link RSocketStrategies} encapsulates required configuration for re-use. | ||||||
|  | 	 * @param rsocketStrategies the strategies to use | ||||||
|  | 	 */ | ||||||
|  | 	public void setRSocketStrategies(RSocketStrategies rsocketStrategies) { | ||||||
|  | 		Assert.notNull(rsocketStrategies, "RSocketStrategies must not be null"); | ||||||
|  | 		this.rsocketStrategies = rsocketStrategies; | ||||||
|  | 		setDecoders(rsocketStrategies.decoders()); | ||||||
|  | 		setEncoders(rsocketStrategies.encoders()); | ||||||
|  | 		setReactiveAdapterRegistry(rsocketStrategies.reactiveAdapterRegistry()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the {@code RSocketStrategies} instance provided via | ||||||
|  | 	 * {@link #setRSocketStrategies rsocketStrategies}, or | ||||||
|  | 	 * otherwise initialize it with the configured {@link #setEncoders(List) | ||||||
|  | 	 * encoders}, {@link #setDecoders(List) decoders}, and others. | ||||||
|  | 	 */ | ||||||
|  | 	public RSocketStrategies getRSocketStrategies() { | ||||||
|  | 		if (this.rsocketStrategies == null) { | ||||||
|  | 			this.rsocketStrategies = RSocketStrategies.builder() | ||||||
|  | 					.decoder(getDecoders().toArray(new Decoder<?>[0])) | ||||||
|  | 					.encoder(getEncoders().toArray(new Encoder<?>[0])) | ||||||
|  | 					.reactiveAdapterStrategy(getReactiveAdapterRegistry()) | ||||||
|  | 					.build(); | ||||||
|  | 		} | ||||||
|  | 		return this.rsocketStrategies; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public void afterPropertiesSet() { | ||||||
|  | 		getArgumentResolverConfigurer().addCustomResolver(new RSocketRequesterMethodArgumentResolver()); | ||||||
|  | 		super.afterPropertiesSet(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	protected List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers() { | ||||||
|  | 		List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(); | ||||||
|  | 		handlers.add(new RSocketPayloadReturnValueHandler(this.encoders, getReactiveAdapterRegistry())); | ||||||
|  | 		handlers.addAll(getReturnValueHandlerConfigurer().getCustomHandlers()); | ||||||
|  | 		return handlers; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	protected void handleNoMatch(@Nullable String destination, Message<?> message) { | ||||||
|  | 
 | ||||||
|  | 		// MessagingRSocket will raise an error anyway if reply Mono is expected | ||||||
|  | 		// Here we raise a more helpful message a destination is present | ||||||
|  | 
 | ||||||
|  | 		// It is OK if some messages (ConnectionSetupPayload, metadataPush) are not handled | ||||||
|  | 		// We need a better way to avoid raising errors for those | ||||||
|  | 
 | ||||||
|  | 		if (StringUtils.hasText(destination)) { | ||||||
|  | 			throw new MessageDeliveryException("No handler for destination '" + destination + "'"); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,86 @@ | ||||||
|  | /* | ||||||
|  |  * 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.rsocket; | ||||||
|  | 
 | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | import io.rsocket.Payload; | ||||||
|  | import reactor.core.publisher.Flux; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | import reactor.core.publisher.MonoProcessor; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.MethodParameter; | ||||||
|  | import org.springframework.core.ReactiveAdapterRegistry; | ||||||
|  | import org.springframework.core.codec.Encoder; | ||||||
|  | import org.springframework.core.io.buffer.DataBuffer; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | import org.springframework.messaging.handler.invocation.reactive.AbstractEncoderMethodReturnValueHandler; | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Extension of {@link AbstractEncoderMethodReturnValueHandler} that | ||||||
|  |  * {@link #handleEncodedContent handles} encoded content by wrapping data buffers | ||||||
|  |  * as RSocket payloads and by passing those to the {@link MonoProcessor} | ||||||
|  |  * from the {@link #RESPONSE_HEADER} header. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | public class RSocketPayloadReturnValueHandler extends AbstractEncoderMethodReturnValueHandler { | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Message header name that is expected to have a {@link MonoProcessor} | ||||||
|  | 	 * which will receive the {@code Flux<Payload>} that represents the response. | ||||||
|  | 	 */ | ||||||
|  | 	public static final String RESPONSE_HEADER = "rsocketResponse"; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	public RSocketPayloadReturnValueHandler(List<Encoder<?>> encoders, ReactiveAdapterRegistry registry) { | ||||||
|  | 		super(encoders, registry); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	@SuppressWarnings("unchecked") | ||||||
|  | 	protected Mono<Void> handleEncodedContent( | ||||||
|  | 			Flux<DataBuffer> encodedContent, MethodParameter returnType, Message<?> message) { | ||||||
|  | 
 | ||||||
|  | 		MonoProcessor<Flux<Payload>> replyMono = getReplyMono(message); | ||||||
|  | 		Assert.notNull(replyMono, "Missing '" + RESPONSE_HEADER + "'"); | ||||||
|  | 		replyMono.onNext(encodedContent.map(PayloadUtils::createPayload)); | ||||||
|  | 		replyMono.onComplete(); | ||||||
|  | 		return Mono.empty(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	protected Mono<Void> handleNoContent(MethodParameter returnType, Message<?> message) { | ||||||
|  | 		MonoProcessor<Flux<Payload>> replyMono = getReplyMono(message); | ||||||
|  | 		if (replyMono != null) { | ||||||
|  | 			replyMono.onComplete(); | ||||||
|  | 		} | ||||||
|  | 		return Mono.empty(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	@SuppressWarnings("unchecked") | ||||||
|  | 	private MonoProcessor<Flux<Payload>> getReplyMono(Message<?> message) { | ||||||
|  | 		Object headerValue = message.getHeaders().get(RESPONSE_HEADER); | ||||||
|  | 		Assert.state(headerValue == null || headerValue instanceof MonoProcessor, "Expected MonoProcessor"); | ||||||
|  | 		return (MonoProcessor<Flux<Payload>>) headerValue; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,166 @@ | ||||||
|  | /* | ||||||
|  |  * 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.rsocket; | ||||||
|  | 
 | ||||||
|  | import io.rsocket.RSocket; | ||||||
|  | import org.reactivestreams.Publisher; | ||||||
|  | import reactor.core.publisher.Flux; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.ParameterizedTypeReference; | ||||||
|  | import org.springframework.core.ReactiveAdapterRegistry; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.util.MimeType; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A thin wrapper around a sending {@link RSocket} with a fluent API accepting | ||||||
|  |  * and returning higher level Objects for input and for output, along with | ||||||
|  |  * methods specify routing and other metadata. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | public interface RSocketRequester { | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the underlying RSocket used to make requests. | ||||||
|  | 	 */ | ||||||
|  | 	RSocket rsocket(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Create a new {@code RSocketRequester} from the given {@link RSocket} and | ||||||
|  | 	 * strategies for encoding and decoding request and response payloads. | ||||||
|  | 	 * @param rsocket the sending RSocket to use | ||||||
|  | 	 * @param dataMimeType the MimeType for data (from the SETUP frame) | ||||||
|  | 	 * @param strategies encoders, decoders, and others | ||||||
|  | 	 * @return the created RSocketRequester wrapper | ||||||
|  | 	 */ | ||||||
|  | 	static RSocketRequester create(RSocket rsocket, @Nullable MimeType dataMimeType, RSocketStrategies strategies) { | ||||||
|  | 		return new DefaultRSocketRequester(rsocket, dataMimeType, strategies); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	// For now we treat metadata as a simple string that is the route. | ||||||
|  | 	// This will change after the resolution of: | ||||||
|  | 	// https://github.com/rsocket/rsocket-java/issues/568 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Entry point to prepare a new request to the given route. | ||||||
|  | 	 * | ||||||
|  | 	 * <p>For requestChannel interactions, i.e. Flux-to-Flux the metadata is | ||||||
|  | 	 * attached to the first request payload. | ||||||
|  | 	 * | ||||||
|  | 	 * @param route the routing destination | ||||||
|  | 	 * @return a spec for further defining and executing the reuqest | ||||||
|  | 	 */ | ||||||
|  | 	RequestSpec route(String route); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Contract to provide input data for an RSocket request. | ||||||
|  | 	 */ | ||||||
|  | 	interface RequestSpec { | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * Provide request payload data. The given Object may be a synchronous | ||||||
|  | 		 * value, or a {@link Publisher} of values, or another async type that's | ||||||
|  | 		 * registered in the configured {@link ReactiveAdapterRegistry}. | ||||||
|  | 		 * <p>For multivalued Publishers, prefer using | ||||||
|  | 		 * {@link #data(Publisher, Class)} or | ||||||
|  | 		 * {@link #data(Publisher, ParameterizedTypeReference)} since that makes | ||||||
|  | 		 * it possible to find a compatible {@code Encoder} up front vs looking | ||||||
|  | 		 * it up on every value. | ||||||
|  | 		 * @param data the Object to use for payload data | ||||||
|  | 		 * @return spec for declaring the expected response | ||||||
|  | 		 */ | ||||||
|  | 		ResponseSpec data(Object data); | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * Provide a {@link Publisher} of value(s) for request payload data. | ||||||
|  | 		 * <p>Publisher semantics determined through the configured | ||||||
|  | 		 * {@link ReactiveAdapterRegistry} influence which of the 4 RSocket | ||||||
|  | 		 * interactions to use. Publishers with unknown semantics are treated | ||||||
|  | 		 * as multivalued. Consider registering a reactive type adapter, or | ||||||
|  | 		 * passing {@code Mono.from(publisher)}. | ||||||
|  | 		 * <p>If the publisher completes empty, possibly {@code Publisher<Void>}, | ||||||
|  | 		 * the request will have an empty data Payload. | ||||||
|  | 		 * @param publisher source of payload data value(s) | ||||||
|  | 		 * @param dataType the type of values to be published | ||||||
|  | 		 * @param <T> the type of element values | ||||||
|  | 		 * @param <P> the type of publisher | ||||||
|  | 		 * @return spec for declaring the expected response | ||||||
|  | 		 */ | ||||||
|  | 		<T, P extends Publisher<T>> ResponseSpec data(P publisher, Class<T> dataType); | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * Variant of {@link #data(Publisher, Class)} for when the dataType has | ||||||
|  | 		 * to have a generic type. See {@link ParameterizedTypeReference}. | ||||||
|  | 		 */ | ||||||
|  | 		<T, P extends Publisher<T>> ResponseSpec data(P publisher, ParameterizedTypeReference<T> dataTypeRef); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Contract to declare the expected RSocket response. | ||||||
|  | 	 */ | ||||||
|  | 	interface ResponseSpec { | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * Perform {@link RSocket#fireAndForget fireAndForget}. | ||||||
|  | 		 */ | ||||||
|  | 		Mono<Void> send(); | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * Perform {@link RSocket#requestResponse requestResponse}. If the | ||||||
|  | 		 * expected data type is {@code Void.class}, the returned {@code Mono} | ||||||
|  | 		 * will complete after all data is consumed. | ||||||
|  | 		 * <p><strong>Note:</strong> Use of this method will raise an error if | ||||||
|  | 		 * the request payload is a multivalued {@link Publisher} as | ||||||
|  | 		 * determined through the configured {@link ReactiveAdapterRegistry}. | ||||||
|  | 		 * @param dataType the expected data type for the response | ||||||
|  | 		 * @param <T> parameter for the expected data type | ||||||
|  | 		 * @return the decoded response | ||||||
|  | 		 */ | ||||||
|  | 		<T> Mono<T> retrieveMono(Class<T> dataType); | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * Variant of {@link #retrieveMono(Class)} for when the dataType has | ||||||
|  | 		 * to have a generic type. See {@link ParameterizedTypeReference}. | ||||||
|  | 		 */ | ||||||
|  | 		<T> Mono<T> retrieveMono(ParameterizedTypeReference<T> dataTypeRef); | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * Perform {@link RSocket#requestStream requestStream} or | ||||||
|  | 		 * {@link RSocket#requestChannel requestChannel} depending on whether | ||||||
|  | 		 * the request input consists of a single or multiple payloads. | ||||||
|  | 		 * If the expected data type is {@code Void.class}, the returned | ||||||
|  | 		 * {@code Flux} will complete after all data is consumed. | ||||||
|  | 		 * @param dataType the expected type for values in the response | ||||||
|  | 		 * @param <T> parameterize the expected type of values | ||||||
|  | 		 * @return the decoded response | ||||||
|  | 		 */ | ||||||
|  | 		<T> Flux<T> retrieveFlux(Class<T> dataType); | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * Variant of {@link #retrieveFlux(Class)} for when the dataType has | ||||||
|  | 		 * to have a generic type. See {@link ParameterizedTypeReference}. | ||||||
|  | 		 */ | ||||||
|  | 		<T> Flux<T> retrieveFlux(ParameterizedTypeReference<T> dataTypeRef); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,70 @@ | ||||||
|  | /* | ||||||
|  |  * 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.rsocket; | ||||||
|  | 
 | ||||||
|  | import io.rsocket.RSocket; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.MethodParameter; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | import org.springframework.messaging.handler.invocation.reactive.HandlerMethodArgumentResolver; | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Resolves arguments of type {@link RSocket} that can be used for making | ||||||
|  |  * requests to the remote peer. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | public class RSocketRequesterMethodArgumentResolver implements HandlerMethodArgumentResolver { | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Message header name that is expected to have the {@link RSocket} to | ||||||
|  | 	 * initiate new interactions to the remote peer with. | ||||||
|  | 	 */ | ||||||
|  | 	public static final String RSOCKET_REQUESTER_HEADER = "rsocketRequester"; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public boolean supportsParameter(MethodParameter parameter) { | ||||||
|  | 		Class<?> type = parameter.getParameterType(); | ||||||
|  | 		return RSocketRequester.class.equals(type) || RSocket.class.isAssignableFrom(type); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message) { | ||||||
|  | 
 | ||||||
|  | 		Object headerValue = message.getHeaders().get(RSOCKET_REQUESTER_HEADER); | ||||||
|  | 		Assert.notNull(headerValue, "Missing '" + RSOCKET_REQUESTER_HEADER + "'"); | ||||||
|  | 		Assert.isInstanceOf(RSocketRequester.class, headerValue, "Expected header value of type RSocketRequester"); | ||||||
|  | 
 | ||||||
|  | 		RSocketRequester requester = (RSocketRequester) headerValue; | ||||||
|  | 
 | ||||||
|  | 		Class<?> type = parameter.getParameterType(); | ||||||
|  | 		if (RSocketRequester.class.equals(type)) { | ||||||
|  | 			return Mono.just(requester); | ||||||
|  | 		} | ||||||
|  | 		else if (RSocket.class.isAssignableFrom(type)) { | ||||||
|  | 			return Mono.just(requester.rsocket()); | ||||||
|  | 		} | ||||||
|  | 		else { | ||||||
|  | 			return Mono.error(new IllegalArgumentException("Unexpected parameter type: " + parameter)); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,171 @@ | ||||||
|  | /* | ||||||
|  |  * 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.rsocket; | ||||||
|  | 
 | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.function.Consumer; | ||||||
|  | 
 | ||||||
|  | import io.netty.buffer.PooledByteBufAllocator; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.ReactiveAdapterRegistry; | ||||||
|  | import org.springframework.core.ResolvableType; | ||||||
|  | import org.springframework.core.codec.Decoder; | ||||||
|  | import org.springframework.core.codec.Encoder; | ||||||
|  | import org.springframework.core.io.buffer.DataBufferFactory; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.util.MimeType; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Access to strategies for use by RSocket requester and responder components. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @since 5.2 | ||||||
|  |  */ | ||||||
|  | public interface RSocketStrategies { | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the configured {@link Builder#encoder(Encoder[]) encoders}. | ||||||
|  | 	 * @see #encoder(ResolvableType, MimeType) | ||||||
|  | 	 */ | ||||||
|  | 	List<Encoder<?>> encoders(); | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Find a compatible Encoder for the given element type. | ||||||
|  | 	 * @param elementType the element type to match | ||||||
|  | 	 * @param mimeType the MimeType to match | ||||||
|  | 	 * @param <T> for casting the Encoder to the expected element type | ||||||
|  | 	 * @return the matching Encoder | ||||||
|  | 	 * @throws IllegalArgumentException if no matching Encoder is found | ||||||
|  | 	 */ | ||||||
|  | 	@SuppressWarnings("unchecked") | ||||||
|  | 	default <T> Encoder<T> encoder(ResolvableType elementType, @Nullable MimeType mimeType) { | ||||||
|  | 		for (Encoder<?> encoder : encoders()) { | ||||||
|  | 			if (encoder.canEncode(elementType, mimeType)) { | ||||||
|  | 				return (Encoder<T>) encoder; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		throw new IllegalArgumentException("No encoder for " + elementType); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the configured {@link Builder#decoder(Decoder[]) decoders}. | ||||||
|  | 	 * @see #decoder(ResolvableType, MimeType) | ||||||
|  | 	 */ | ||||||
|  | 	List<Decoder<?>> decoders(); | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Find a compatible Decoder for the given element type. | ||||||
|  | 	 * @param elementType the element type to match | ||||||
|  | 	 * @param mimeType the MimeType to match | ||||||
|  | 	 * @param <T> for casting the Decoder to the expected element type | ||||||
|  | 	 * @return the matching Decoder | ||||||
|  | 	 * @throws IllegalArgumentException if no matching Decoder is found | ||||||
|  | 	 */ | ||||||
|  | 	@SuppressWarnings("unchecked") | ||||||
|  | 	default <T> Decoder<T> decoder(ResolvableType elementType, @Nullable MimeType mimeType) { | ||||||
|  | 		for (Decoder<?> decoder : decoders()) { | ||||||
|  | 			if (decoder.canDecode(elementType, mimeType)) { | ||||||
|  | 				return (Decoder<T>) decoder; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		throw new IllegalArgumentException("No decoder for " + elementType); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the configured | ||||||
|  | 	 * {@link Builder#reactiveAdapterStrategy(ReactiveAdapterRegistry) reactiveAdapterRegistry}. | ||||||
|  | 	 */ | ||||||
|  | 	ReactiveAdapterRegistry reactiveAdapterRegistry(); | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return the configured | ||||||
|  | 	 * {@link Builder#dataBufferFactory(DataBufferFactory) dataBufferFactory}. | ||||||
|  | 	 */ | ||||||
|  | 	DataBufferFactory dataBufferFactory(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Return a builder to build a new {@code RSocketStrategies} instance. | ||||||
|  | 	 */ | ||||||
|  | 	static Builder builder() { | ||||||
|  | 		return new DefaultRSocketStrategies.DefaultRSocketStrategiesBuilder(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * The builder options for creating {@code RSocketStrategies}. | ||||||
|  | 	 */ | ||||||
|  | 	interface Builder { | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * Add encoders to use for serializing Objects. | ||||||
|  | 		 * <p>By default this is empty. | ||||||
|  | 		 */ | ||||||
|  | 		Builder encoder(Encoder<?>... encoder); | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * Add decoders for de-serializing Objects. | ||||||
|  | 		 * <p>By default this is empty. | ||||||
|  | 		 */ | ||||||
|  | 		Builder decoder(Decoder<?>... decoder); | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * Access and manipulate the list of configured {@link #encoder encoders}. | ||||||
|  | 		 */ | ||||||
|  | 		Builder encoders(Consumer<List<Encoder<?>>> consumer); | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * Access and manipulate the list of configured {@link #encoder decoders}. | ||||||
|  | 		 */ | ||||||
|  | 		Builder decoders(Consumer<List<Decoder<?>>> consumer); | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * Configure the registry for reactive type support. This can be used to | ||||||
|  | 		 * to adapt to, and/or determine the semantics of a given | ||||||
|  | 		 * {@link org.reactivestreams.Publisher Publisher}. | ||||||
|  | 		 * <p>By default this {@link ReactiveAdapterRegistry#sharedInstance}. | ||||||
|  | 		 * @param registry the registry to use | ||||||
|  | 		 */ | ||||||
|  | 		Builder reactiveAdapterStrategy(ReactiveAdapterRegistry registry); | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * Configure the DataBufferFactory to use for allocating buffers, for | ||||||
|  | 		 * example when preparing requests or when responding. The choice here | ||||||
|  | 		 * must be aligned with the frame decoder configured in | ||||||
|  | 		 * {@link io.rsocket.RSocketFactory}. | ||||||
|  | 		 * <p>By default this property is an instance of | ||||||
|  | 		 * {@link org.springframework.core.io.buffer.DefaultDataBufferFactory | ||||||
|  | 		 * DefaultDataBufferFactory} matching to the default frame decoder in | ||||||
|  | 		 * {@link io.rsocket.RSocketFactory} which copies the payload. This | ||||||
|  | 		 * comes at cost to performance but does not require reference counting | ||||||
|  | 		 * and eliminates possibility for memory leaks. | ||||||
|  | 		 * <p>To switch to a zero-copy strategy, | ||||||
|  | 		 * <a href="https://github.com/rsocket/rsocket-java#zero-copy">configure RSocket</a> | ||||||
|  | 		 * accordingly, and then configure this property with an instance of | ||||||
|  | 		 * {@link org.springframework.core.io.buffer.NettyDataBufferFactory | ||||||
|  | 		 * NettyDataBufferFactory} with a pooled allocator such as | ||||||
|  | 		 * {@link PooledByteBufAllocator#DEFAULT}. | ||||||
|  | 		 * @param bufferFactory the DataBufferFactory to use | ||||||
|  | 		 */ | ||||||
|  | 		Builder dataBufferFactory(DataBufferFactory bufferFactory); | ||||||
|  | 
 | ||||||
|  | 		/** | ||||||
|  | 		 * Builder the {@code RSocketStrategies} instance. | ||||||
|  | 		 */ | ||||||
|  | 		RSocketStrategies build(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,9 @@ | ||||||
|  | /** | ||||||
|  |  * Support for the RSocket protocol. | ||||||
|  |  */ | ||||||
|  | @NonNullApi | ||||||
|  | @NonNullFields | ||||||
|  | package org.springframework.messaging.rsocket; | ||||||
|  | 
 | ||||||
|  | import org.springframework.lang.NonNullApi; | ||||||
|  | import org.springframework.lang.NonNullFields; | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2018 the original author or authors. |  * Copyright 2002-2019 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -18,6 +18,7 @@ package org.springframework.messaging.simp; | ||||||
| 
 | 
 | ||||||
| import org.springframework.lang.Nullable; | import org.springframework.lang.Nullable; | ||||||
| import org.springframework.messaging.Message; | import org.springframework.messaging.Message; | ||||||
|  | import org.springframework.messaging.handler.CompositeMessageCondition; | ||||||
| import org.springframework.messaging.handler.DestinationPatternsMessageCondition; | import org.springframework.messaging.handler.DestinationPatternsMessageCondition; | ||||||
| import org.springframework.messaging.handler.MessageCondition; | import org.springframework.messaging.handler.MessageCondition; | ||||||
| 
 | 
 | ||||||
|  | @ -34,62 +35,44 @@ import org.springframework.messaging.handler.MessageCondition; | ||||||
|  */ |  */ | ||||||
| public class SimpMessageMappingInfo implements MessageCondition<SimpMessageMappingInfo> { | public class SimpMessageMappingInfo implements MessageCondition<SimpMessageMappingInfo> { | ||||||
| 
 | 
 | ||||||
| 	private final SimpMessageTypeMessageCondition messageTypeMessageCondition; | 	private final CompositeMessageCondition delegate; | ||||||
| 
 |  | ||||||
| 	private final DestinationPatternsMessageCondition destinationConditions; |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	public SimpMessageMappingInfo(SimpMessageTypeMessageCondition messageTypeMessageCondition, | 	public SimpMessageMappingInfo(SimpMessageTypeMessageCondition messageTypeMessageCondition, | ||||||
| 			DestinationPatternsMessageCondition destinationConditions) { | 			DestinationPatternsMessageCondition destinationConditions) { | ||||||
| 
 | 
 | ||||||
| 		this.messageTypeMessageCondition = messageTypeMessageCondition; | 		this.delegate = new CompositeMessageCondition(messageTypeMessageCondition, destinationConditions); | ||||||
| 		this.destinationConditions = destinationConditions; | 	} | ||||||
|  | 
 | ||||||
|  | 	private SimpMessageMappingInfo(CompositeMessageCondition delegate) { | ||||||
|  | 		this.delegate = delegate; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	public SimpMessageTypeMessageCondition getMessageTypeMessageCondition() { | 	public SimpMessageTypeMessageCondition getMessageTypeMessageCondition() { | ||||||
| 		return this.messageTypeMessageCondition; | 		return this.delegate.getCondition(SimpMessageTypeMessageCondition.class); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	public DestinationPatternsMessageCondition getDestinationConditions() { | 	public DestinationPatternsMessageCondition getDestinationConditions() { | ||||||
| 		return this.destinationConditions; | 		return this.delegate.getCondition(DestinationPatternsMessageCondition.class); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	@Override | 	@Override | ||||||
| 	public SimpMessageMappingInfo combine(SimpMessageMappingInfo other) { | 	public SimpMessageMappingInfo combine(SimpMessageMappingInfo other) { | ||||||
| 		SimpMessageTypeMessageCondition typeCond = | 		return new SimpMessageMappingInfo(this.delegate.combine(other.delegate)); | ||||||
| 				this.getMessageTypeMessageCondition().combine(other.getMessageTypeMessageCondition()); |  | ||||||
| 		DestinationPatternsMessageCondition destCond = |  | ||||||
| 				this.destinationConditions.combine(other.getDestinationConditions()); |  | ||||||
| 		return new SimpMessageMappingInfo(typeCond, destCond); |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Override | 	@Override | ||||||
| 	@Nullable | 	@Nullable | ||||||
| 	public SimpMessageMappingInfo getMatchingCondition(Message<?> message) { | 	public SimpMessageMappingInfo getMatchingCondition(Message<?> message) { | ||||||
| 		SimpMessageTypeMessageCondition typeCond = this.messageTypeMessageCondition.getMatchingCondition(message); | 		CompositeMessageCondition condition = this.delegate.getMatchingCondition(message); | ||||||
| 		if (typeCond == null) { | 		return condition != null ? new SimpMessageMappingInfo(condition) : null; | ||||||
| 			return null; |  | ||||||
| 		} |  | ||||||
| 		DestinationPatternsMessageCondition destCond = this.destinationConditions.getMatchingCondition(message); |  | ||||||
| 		if (destCond == null) { |  | ||||||
| 			return null; |  | ||||||
| 		} |  | ||||||
| 		return new SimpMessageMappingInfo(typeCond, destCond); |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Override | 	@Override | ||||||
| 	public int compareTo(SimpMessageMappingInfo other, Message<?> message) { | 	public int compareTo(SimpMessageMappingInfo other, Message<?> message) { | ||||||
| 		int result = this.messageTypeMessageCondition.compareTo(other.messageTypeMessageCondition, message); | 		return this.delegate.compareTo(other.delegate, message); | ||||||
| 		if (result != 0) { |  | ||||||
| 			return result; |  | ||||||
| 		} |  | ||||||
| 		result = this.destinationConditions.compareTo(other.destinationConditions, message); |  | ||||||
| 		if (result != 0) { |  | ||||||
| 			return result; |  | ||||||
| 		} |  | ||||||
| 		return 0; |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -101,19 +84,17 @@ public class SimpMessageMappingInfo implements MessageCondition<SimpMessageMappi | ||||||
| 		if (!(other instanceof SimpMessageMappingInfo)) { | 		if (!(other instanceof SimpMessageMappingInfo)) { | ||||||
| 			return false; | 			return false; | ||||||
| 		} | 		} | ||||||
| 		SimpMessageMappingInfo otherInfo = (SimpMessageMappingInfo) other; | 		return this.delegate.equals(((SimpMessageMappingInfo) other).delegate); | ||||||
| 		return (this.destinationConditions.equals(otherInfo.destinationConditions) && |  | ||||||
| 				this.messageTypeMessageCondition.equals(otherInfo.messageTypeMessageCondition)); |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Override | 	@Override | ||||||
| 	public int hashCode() { | 	public int hashCode() { | ||||||
| 		return (this.destinationConditions.hashCode() * 31 + this.messageTypeMessageCondition.hashCode()); | 		return this.delegate.hashCode(); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Override | 	@Override | ||||||
| 	public String toString() { | 	public String toString() { | ||||||
| 		return "{" + this.destinationConditions + ",messageType=" + this.messageTypeMessageCondition + '}'; | 		return this.delegate.toString(); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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<MethodParameter> { | ||||||
|  | 
 | ||||||
|  | 		@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<MethodParameter> { | ||||||
|  | 
 | ||||||
|  | 		@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)); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2018 the original author or authors. |  * Copyright 2002-2019 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -16,23 +16,21 @@ | ||||||
| 
 | 
 | ||||||
| package org.springframework.messaging.handler.annotation.support; | package org.springframework.messaging.handler.annotation.support; | ||||||
| 
 | 
 | ||||||
| import java.lang.reflect.Method; |  | ||||||
| import java.util.HashMap; | import java.util.HashMap; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
| 
 | 
 | ||||||
| import org.junit.Before; |  | ||||||
| import org.junit.Test; | import org.junit.Test; | ||||||
| 
 | 
 | ||||||
| import org.springframework.core.DefaultParameterNameDiscoverer; |  | ||||||
| import org.springframework.core.GenericTypeResolver; |  | ||||||
| import org.springframework.core.MethodParameter; | import org.springframework.core.MethodParameter; | ||||||
| import org.springframework.core.convert.support.DefaultConversionService; | import org.springframework.core.convert.support.DefaultConversionService; | ||||||
| import org.springframework.messaging.Message; | import org.springframework.messaging.Message; | ||||||
| import org.springframework.messaging.MessageHandlingException; | import org.springframework.messaging.MessageHandlingException; | ||||||
| import org.springframework.messaging.handler.annotation.DestinationVariable; | import org.springframework.messaging.handler.annotation.DestinationVariable; | ||||||
|  | import org.springframework.messaging.handler.invocation.ResolvableMethod; | ||||||
| import org.springframework.messaging.support.MessageBuilder; | import org.springframework.messaging.support.MessageBuilder; | ||||||
| 
 | 
 | ||||||
| import static org.junit.Assert.*; | import static org.junit.Assert.*; | ||||||
|  | import static org.springframework.messaging.handler.annotation.MessagingPredicates.*; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Test fixture for {@link DestinationVariableMethodArgumentResolver} tests. |  * Test fixture for {@link DestinationVariableMethodArgumentResolver} tests. | ||||||
|  | @ -41,33 +39,17 @@ import static org.junit.Assert.*; | ||||||
|  */ |  */ | ||||||
| public class DestinationVariableMethodArgumentResolverTests { | public class DestinationVariableMethodArgumentResolverTests { | ||||||
| 
 | 
 | ||||||
| 	private DestinationVariableMethodArgumentResolver resolver; | 	private final DestinationVariableMethodArgumentResolver resolver = | ||||||
|  | 			new DestinationVariableMethodArgumentResolver(new DefaultConversionService()); | ||||||
| 
 | 
 | ||||||
| 	private MethodParameter paramAnnotated; | 	private final ResolvableMethod resolvable = | ||||||
| 	private MethodParameter paramAnnotatedValue; | 			ResolvableMethod.on(getClass()).named("handleMessage").build(); | ||||||
| 	private MethodParameter paramNotAnnotated; |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	@Before |  | ||||||
| 	public void setup() throws Exception { |  | ||||||
| 		this.resolver = new DestinationVariableMethodArgumentResolver(new DefaultConversionService()); |  | ||||||
| 
 |  | ||||||
| 		Method method = getClass().getDeclaredMethod("handleMessage", String.class, String.class, String.class); |  | ||||||
| 		this.paramAnnotated = new MethodParameter(method, 0); |  | ||||||
| 		this.paramAnnotatedValue = new MethodParameter(method, 1); |  | ||||||
| 		this.paramNotAnnotated = new MethodParameter(method, 2); |  | ||||||
| 
 |  | ||||||
| 		this.paramAnnotated.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); |  | ||||||
| 		GenericTypeResolver.resolveParameterType(this.paramAnnotated, DestinationVariableMethodArgumentResolver.class); |  | ||||||
| 		this.paramAnnotatedValue.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); |  | ||||||
| 		GenericTypeResolver.resolveParameterType(this.paramAnnotatedValue, DestinationVariableMethodArgumentResolver.class); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	@Test | 	@Test | ||||||
| 	public void supportsParameter() { | 	public void supportsParameter() { | ||||||
| 		assertTrue(resolver.supportsParameter(paramAnnotated)); | 		assertTrue(resolver.supportsParameter(this.resolvable.annot(destinationVar().noValue()).arg())); | ||||||
| 		assertTrue(resolver.supportsParameter(paramAnnotatedValue)); | 		assertFalse(resolver.supportsParameter(this.resolvable.annotNotPresent(DestinationVariable.class).arg())); | ||||||
| 		assertFalse(resolver.supportsParameter(paramNotAnnotated)); |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test | 	@Test | ||||||
|  | @ -80,17 +62,19 @@ public class DestinationVariableMethodArgumentResolverTests { | ||||||
| 		Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeader( | 		Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeader( | ||||||
| 			DestinationVariableMethodArgumentResolver.DESTINATION_TEMPLATE_VARIABLES_HEADER, vars).build(); | 			DestinationVariableMethodArgumentResolver.DESTINATION_TEMPLATE_VARIABLES_HEADER, vars).build(); | ||||||
| 
 | 
 | ||||||
| 		Object result = this.resolver.resolveArgument(this.paramAnnotated, message); | 		MethodParameter param = this.resolvable.annot(destinationVar().noValue()).arg(); | ||||||
|  | 		Object result = this.resolver.resolveArgument(param, message); | ||||||
| 		assertEquals("bar", result); | 		assertEquals("bar", result); | ||||||
| 
 | 
 | ||||||
| 		result = this.resolver.resolveArgument(this.paramAnnotatedValue, message); | 		param = this.resolvable.annot(destinationVar("name")).arg(); | ||||||
|  | 		result = this.resolver.resolveArgument(param, message); | ||||||
| 		assertEquals("value", result); | 		assertEquals("value", result); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test(expected = MessageHandlingException.class) | 	@Test(expected = MessageHandlingException.class) | ||||||
| 	public void resolveArgumentNotFound() throws Exception { | 	public void resolveArgumentNotFound() throws Exception { | ||||||
| 		Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build(); | 		Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build(); | ||||||
| 		this.resolver.resolveArgument(this.paramAnnotated, message); | 		this.resolver.resolveArgument(this.resolvable.annot(destinationVar().noValue()).arg(), message); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@SuppressWarnings("unused") | 	@SuppressWarnings("unused") | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2018 the original author or authors. |  * Copyright 2002-2019 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -16,7 +16,6 @@ | ||||||
| 
 | 
 | ||||||
| package org.springframework.messaging.handler.annotation.support; | package org.springframework.messaging.handler.annotation.support; | ||||||
| 
 | 
 | ||||||
| import java.lang.reflect.Method; |  | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
| import java.util.Optional; | import java.util.Optional; | ||||||
|  | @ -25,19 +24,17 @@ import org.junit.Before; | ||||||
| import org.junit.Test; | import org.junit.Test; | ||||||
| 
 | 
 | ||||||
| import org.springframework.context.support.GenericApplicationContext; | import org.springframework.context.support.GenericApplicationContext; | ||||||
| import org.springframework.core.DefaultParameterNameDiscoverer; |  | ||||||
| import org.springframework.core.GenericTypeResolver; |  | ||||||
| import org.springframework.core.MethodParameter; | import org.springframework.core.MethodParameter; | ||||||
| import org.springframework.core.annotation.SynthesizingMethodParameter; |  | ||||||
| import org.springframework.core.convert.support.DefaultConversionService; | import org.springframework.core.convert.support.DefaultConversionService; | ||||||
| import org.springframework.messaging.Message; | import org.springframework.messaging.Message; | ||||||
| import org.springframework.messaging.MessageHandlingException; | import org.springframework.messaging.MessageHandlingException; | ||||||
| import org.springframework.messaging.handler.annotation.Header; | 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.MessageBuilder; | ||||||
| import org.springframework.messaging.support.NativeMessageHeaderAccessor; | import org.springframework.messaging.support.NativeMessageHeaderAccessor; | ||||||
| import org.springframework.util.ReflectionUtils; |  | ||||||
| 
 | 
 | ||||||
| import static org.junit.Assert.*; | import static org.junit.Assert.*; | ||||||
|  | import static org.springframework.messaging.handler.annotation.MessagingPredicates.*; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Test fixture for {@link HeaderMethodArgumentResolver} tests. |  * Test fixture for {@link HeaderMethodArgumentResolver} tests. | ||||||
|  | @ -50,46 +47,27 @@ public class HeaderMethodArgumentResolverTests { | ||||||
| 
 | 
 | ||||||
| 	private HeaderMethodArgumentResolver resolver; | 	private HeaderMethodArgumentResolver resolver; | ||||||
| 
 | 
 | ||||||
| 	private MethodParameter paramRequired; | 	private final ResolvableMethod resolvable = ResolvableMethod.on(getClass()).named("handleMessage").build(); | ||||||
| 	private MethodParameter paramNamedDefaultValueStringHeader; |  | ||||||
| 	private MethodParameter paramSystemPropertyDefaultValue; |  | ||||||
| 	private MethodParameter paramSystemPropertyName; |  | ||||||
| 	private MethodParameter paramNotAnnotated; |  | ||||||
| 	private MethodParameter paramOptional; |  | ||||||
| 	private MethodParameter paramNativeHeader; |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	@Before | 	@Before | ||||||
| 	public void setup() { | 	public void setup() { | ||||||
| 		@SuppressWarnings("resource") | 		GenericApplicationContext context = new GenericApplicationContext(); | ||||||
| 		GenericApplicationContext cxt = new GenericApplicationContext(); | 		context.refresh(); | ||||||
| 		cxt.refresh(); | 		this.resolver = new HeaderMethodArgumentResolver(new DefaultConversionService(), context.getBeanFactory()); | ||||||
| 		this.resolver = new HeaderMethodArgumentResolver(new DefaultConversionService(), cxt.getBeanFactory()); |  | ||||||
| 
 |  | ||||||
| 		Method method = ReflectionUtils.findMethod(getClass(), "handleMessage", (Class<?>[]) null); |  | ||||||
| 		this.paramRequired = new SynthesizingMethodParameter(method, 0); |  | ||||||
| 		this.paramNamedDefaultValueStringHeader = new SynthesizingMethodParameter(method, 1); |  | ||||||
| 		this.paramSystemPropertyDefaultValue = new SynthesizingMethodParameter(method, 2); |  | ||||||
| 		this.paramSystemPropertyName = new SynthesizingMethodParameter(method, 3); |  | ||||||
| 		this.paramNotAnnotated = new SynthesizingMethodParameter(method, 4); |  | ||||||
| 		this.paramOptional = new SynthesizingMethodParameter(method, 5); |  | ||||||
| 		this.paramNativeHeader = new SynthesizingMethodParameter(method, 6); |  | ||||||
| 
 |  | ||||||
| 		this.paramRequired.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); |  | ||||||
| 		GenericTypeResolver.resolveParameterType(this.paramRequired, HeaderMethodArgumentResolver.class); |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	@Test | 	@Test | ||||||
| 	public void supportsParameter() { | 	public void supportsParameter() { | ||||||
| 		assertTrue(resolver.supportsParameter(paramNamedDefaultValueStringHeader)); | 		assertTrue(this.resolver.supportsParameter(this.resolvable.annot(headerPlain()).arg())); | ||||||
| 		assertFalse(resolver.supportsParameter(paramNotAnnotated)); | 		assertFalse(this.resolver.supportsParameter(this.resolvable.annotNotPresent(Header.class).arg())); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test | 	@Test | ||||||
| 	public void resolveArgument() throws Exception { | 	public void resolveArgument() throws Exception { | ||||||
| 		Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeader("param1", "foo").build(); | 		Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeader("param1", "foo").build(); | ||||||
| 		Object result = this.resolver.resolveArgument(this.paramRequired, message); | 		Object result = this.resolver.resolveArgument(this.resolvable.annot(headerPlain()).arg(), message); | ||||||
| 		assertEquals("foo", result); | 		assertEquals("foo", result); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -98,7 +76,7 @@ public class HeaderMethodArgumentResolverTests { | ||||||
| 		TestMessageHeaderAccessor headers = new TestMessageHeaderAccessor(); | 		TestMessageHeaderAccessor headers = new TestMessageHeaderAccessor(); | ||||||
| 		headers.setNativeHeader("param1", "foo"); | 		headers.setNativeHeader("param1", "foo"); | ||||||
| 		Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build(); | 		Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build(); | ||||||
| 		assertEquals("foo", this.resolver.resolveArgument(this.paramRequired, message)); | 		assertEquals("foo", this.resolver.resolveArgument(this.resolvable.annot(headerPlain()).arg(), message)); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test | 	@Test | ||||||
|  | @ -108,20 +86,23 @@ public class HeaderMethodArgumentResolverTests { | ||||||
| 		headers.setNativeHeader("param1", "native-foo"); | 		headers.setNativeHeader("param1", "native-foo"); | ||||||
| 		Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build(); | 		Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build(); | ||||||
| 
 | 
 | ||||||
| 		assertEquals("foo", this.resolver.resolveArgument(this.paramRequired, message)); | 		assertEquals("foo", this.resolver.resolveArgument( | ||||||
| 		assertEquals("native-foo", this.resolver.resolveArgument(this.paramNativeHeader, message)); | 				this.resolvable.annot(headerPlain()).arg(), message)); | ||||||
|  | 
 | ||||||
|  | 		assertEquals("native-foo", this.resolver.resolveArgument( | ||||||
|  | 				this.resolvable.annot(header("nativeHeaders.param1")).arg(), message)); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test(expected = MessageHandlingException.class) | 	@Test(expected = MessageHandlingException.class) | ||||||
| 	public void resolveArgumentNotFound() throws Exception { | 	public void resolveArgumentNotFound() throws Exception { | ||||||
| 		Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build(); | 		Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build(); | ||||||
| 		this.resolver.resolveArgument(this.paramRequired, message); | 		this.resolver.resolveArgument(this.resolvable.annot(headerPlain()).arg(), message); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test | 	@Test | ||||||
| 	public void resolveArgumentDefaultValue() throws Exception { | 	public void resolveArgumentDefaultValue() throws Exception { | ||||||
| 		Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build(); | 		Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build(); | ||||||
| 		Object result = this.resolver.resolveArgument(this.paramNamedDefaultValueStringHeader, message); | 		Object result = this.resolver.resolveArgument(this.resolvable.annot(header("name", "bar")).arg(), message); | ||||||
| 		assertEquals("bar", result); | 		assertEquals("bar", result); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -130,7 +111,8 @@ public class HeaderMethodArgumentResolverTests { | ||||||
| 		System.setProperty("systemProperty", "sysbar"); | 		System.setProperty("systemProperty", "sysbar"); | ||||||
| 		try { | 		try { | ||||||
| 			Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build(); | 			Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build(); | ||||||
| 			Object result = resolver.resolveArgument(paramSystemPropertyDefaultValue, message); | 			MethodParameter param = this.resolvable.annot(header("name", "#{systemProperties.systemProperty}")).arg(); | ||||||
|  | 			Object result = resolver.resolveArgument(param, message); | ||||||
| 			assertEquals("sysbar", result); | 			assertEquals("sysbar", result); | ||||||
| 		} | 		} | ||||||
| 		finally { | 		finally { | ||||||
|  | @ -143,7 +125,8 @@ public class HeaderMethodArgumentResolverTests { | ||||||
| 		System.setProperty("systemProperty", "sysbar"); | 		System.setProperty("systemProperty", "sysbar"); | ||||||
| 		try { | 		try { | ||||||
| 			Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeader("sysbar", "foo").build(); | 			Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeader("sysbar", "foo").build(); | ||||||
| 			Object result = resolver.resolveArgument(paramSystemPropertyName, message); | 			MethodParameter param = this.resolvable.annot(header("#{systemProperties.systemProperty}")).arg(); | ||||||
|  | 			Object result = resolver.resolveArgument(param, message); | ||||||
| 			assertEquals("foo", result); | 			assertEquals("foo", result); | ||||||
| 		} | 		} | ||||||
| 		finally { | 		finally { | ||||||
|  | @ -153,31 +136,22 @@ public class HeaderMethodArgumentResolverTests { | ||||||
| 
 | 
 | ||||||
| 	@Test | 	@Test | ||||||
| 	public void resolveOptionalHeaderWithValue() throws Exception { | 	public void resolveOptionalHeaderWithValue() throws Exception { | ||||||
| 		GenericApplicationContext cxt = new GenericApplicationContext(); |  | ||||||
| 		cxt.refresh(); |  | ||||||
| 
 |  | ||||||
| 		HeaderMethodArgumentResolver resolver = |  | ||||||
| 				new HeaderMethodArgumentResolver(new DefaultConversionService(), cxt.getBeanFactory()); |  | ||||||
| 
 |  | ||||||
| 		Message<String> message = MessageBuilder.withPayload("foo").setHeader("foo", "bar").build(); | 		Message<String> message = MessageBuilder.withPayload("foo").setHeader("foo", "bar").build(); | ||||||
| 		Object result = resolver.resolveArgument(paramOptional, message); | 		MethodParameter param = this.resolvable.annot(header("foo")).arg(Optional.class, String.class); | ||||||
|  | 		Object result = resolver.resolveArgument(param, message); | ||||||
| 		assertEquals(Optional.of("bar"), result); | 		assertEquals(Optional.of("bar"), result); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test | 	@Test | ||||||
| 	public void resolveOptionalHeaderAsEmpty() throws Exception { | 	public void resolveOptionalHeaderAsEmpty() throws Exception { | ||||||
| 		GenericApplicationContext cxt = new GenericApplicationContext(); |  | ||||||
| 		cxt.refresh(); |  | ||||||
| 
 |  | ||||||
| 		HeaderMethodArgumentResolver resolver = |  | ||||||
| 				new HeaderMethodArgumentResolver(new DefaultConversionService(), cxt.getBeanFactory()); |  | ||||||
| 
 |  | ||||||
| 		Message<String> message = MessageBuilder.withPayload("foo").build(); | 		Message<String> message = MessageBuilder.withPayload("foo").build(); | ||||||
| 		Object result = resolver.resolveArgument(paramOptional, message); | 		MethodParameter param = this.resolvable.annot(header("foo")).arg(Optional.class, String.class); | ||||||
|  | 		Object result = resolver.resolveArgument(param, message); | ||||||
| 		assertEquals(Optional.empty(), result); | 		assertEquals(Optional.empty(), result); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | 	@SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) | ||||||
| 	public void handleMessage( | 	public void handleMessage( | ||||||
| 			@Header String param1, | 			@Header String param1, | ||||||
| 			@Header(name = "name", defaultValue = "bar") String param2, | 			@Header(name = "name", defaultValue = "bar") String param2, | ||||||
|  | @ -191,7 +165,7 @@ public class HeaderMethodArgumentResolverTests { | ||||||
| 
 | 
 | ||||||
| 	public static class TestMessageHeaderAccessor extends NativeMessageHeaderAccessor { | 	public static class TestMessageHeaderAccessor extends NativeMessageHeaderAccessor { | ||||||
| 
 | 
 | ||||||
| 		protected TestMessageHeaderAccessor() { | 		TestMessageHeaderAccessor() { | ||||||
| 			super((Map<String, List<String>>) null); | 			super((Map<String, List<String>>) null); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
|  * Copyright 2002-2016 the original author or authors. |  * Copyright 2002-2019 the original author or authors. | ||||||
|  * |  * | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  * you may not use this file except in compliance with the License. |  * you may not use this file except in compliance with the License. | ||||||
|  | @ -16,17 +16,16 @@ | ||||||
| 
 | 
 | ||||||
| package org.springframework.messaging.handler.annotation.support; | package org.springframework.messaging.handler.annotation.support; | ||||||
| 
 | 
 | ||||||
| import java.lang.reflect.Method; | import java.util.Collections; | ||||||
| import java.util.HashMap; |  | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
| 
 | 
 | ||||||
| import org.junit.Before; |  | ||||||
| import org.junit.Test; | import org.junit.Test; | ||||||
| 
 | 
 | ||||||
| import org.springframework.core.MethodParameter; | import org.springframework.core.MethodParameter; | ||||||
| import org.springframework.messaging.Message; | import org.springframework.messaging.Message; | ||||||
| import org.springframework.messaging.MessageHeaders; | import org.springframework.messaging.MessageHeaders; | ||||||
| import org.springframework.messaging.handler.annotation.Headers; | 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.MessageBuilder; | ||||||
| import org.springframework.messaging.support.MessageHeaderAccessor; | import org.springframework.messaging.support.MessageHeaderAccessor; | ||||||
| import org.springframework.messaging.support.NativeMessageHeaderAccessor; | import org.springframework.messaging.support.NativeMessageHeaderAccessor; | ||||||
|  | @ -41,47 +40,31 @@ import static org.junit.Assert.*; | ||||||
|  */ |  */ | ||||||
| public class HeadersMethodArgumentResolverTests { | public class HeadersMethodArgumentResolverTests { | ||||||
| 
 | 
 | ||||||
| 	private HeadersMethodArgumentResolver resolver; | 	private final HeadersMethodArgumentResolver resolver = new HeadersMethodArgumentResolver(); | ||||||
| 
 | 
 | ||||||
| 	private MethodParameter paramAnnotated; | 	private Message<byte[]> message = | ||||||
| 	private MethodParameter paramAnnotatedNotMap; | 			MessageBuilder.withPayload(new byte[0]).copyHeaders(Collections.singletonMap("foo", "bar")).build(); | ||||||
| 	private MethodParameter paramMessageHeaders; |  | ||||||
| 	private MethodParameter paramMessageHeaderAccessor; |  | ||||||
| 	private MethodParameter paramMessageHeaderAccessorSubclass; |  | ||||||
| 
 | 
 | ||||||
| 	private Message<byte[]> message; | 	private final ResolvableMethod resolvable = ResolvableMethod.on(getClass()).named("handleMessage").build(); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	@Before |  | ||||||
| 	public void setup() throws Exception { |  | ||||||
| 		this.resolver = new HeadersMethodArgumentResolver(); |  | ||||||
| 
 |  | ||||||
| 		Method method = getClass().getDeclaredMethod("handleMessage", Map.class, String.class, |  | ||||||
| 				MessageHeaders.class, MessageHeaderAccessor.class, TestMessageHeaderAccessor.class); |  | ||||||
| 
 |  | ||||||
| 		this.paramAnnotated = new MethodParameter(method, 0); |  | ||||||
| 		this.paramAnnotatedNotMap = new MethodParameter(method, 1); |  | ||||||
| 		this.paramMessageHeaders = new MethodParameter(method, 2); |  | ||||||
| 		this.paramMessageHeaderAccessor = new MethodParameter(method, 3); |  | ||||||
| 		this.paramMessageHeaderAccessorSubclass = new MethodParameter(method, 4); |  | ||||||
| 
 |  | ||||||
| 		Map<String, Object> headers = new HashMap<>(); |  | ||||||
| 		headers.put("foo", "bar"); |  | ||||||
| 		this.message = MessageBuilder.withPayload(new byte[0]).copyHeaders(headers).build(); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	@Test | 	@Test | ||||||
| 	public void supportsParameter() { | 	public void supportsParameter() { | ||||||
| 		assertTrue(this.resolver.supportsParameter(this.paramAnnotated)); | 
 | ||||||
| 		assertFalse(this.resolver.supportsParameter(this.paramAnnotatedNotMap)); | 		assertTrue(this.resolver.supportsParameter( | ||||||
| 		assertTrue(this.resolver.supportsParameter(this.paramMessageHeaders)); | 				this.resolvable.annotPresent(Headers.class).arg(Map.class, String.class, Object.class))); | ||||||
| 		assertTrue(this.resolver.supportsParameter(this.paramMessageHeaderAccessor)); | 
 | ||||||
| 		assertTrue(this.resolver.supportsParameter(this.paramMessageHeaderAccessorSubclass)); | 		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 | 	@Test | ||||||
| 	public void resolveArgumentAnnotated() throws Exception { | 	public void resolveArgumentAnnotated() throws Exception { | ||||||
| 		Object resolved = this.resolver.resolveArgument(this.paramAnnotated, this.message); | 		MethodParameter param = this.resolvable.annotPresent(Headers.class).arg(Map.class, String.class, Object.class); | ||||||
|  | 		Object resolved = this.resolver.resolveArgument(param, this.message); | ||||||
| 
 | 
 | ||||||
| 		assertTrue(resolved instanceof Map); | 		assertTrue(resolved instanceof Map); | ||||||
| 		@SuppressWarnings("unchecked") | 		@SuppressWarnings("unchecked") | ||||||
|  | @ -91,12 +74,12 @@ public class HeadersMethodArgumentResolverTests { | ||||||
| 
 | 
 | ||||||
| 	@Test(expected = IllegalStateException.class) | 	@Test(expected = IllegalStateException.class) | ||||||
| 	public void resolveArgumentAnnotatedNotMap() throws Exception { | 	public void resolveArgumentAnnotatedNotMap() throws Exception { | ||||||
| 		this.resolver.resolveArgument(this.paramAnnotatedNotMap, this.message); | 		this.resolver.resolveArgument(this.resolvable.annotPresent(Headers.class).arg(String.class), this.message); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test | 	@Test | ||||||
| 	public void resolveArgumentMessageHeaders() throws Exception { | 	public void resolveArgumentMessageHeaders() throws Exception { | ||||||
| 		Object resolved = this.resolver.resolveArgument(this.paramMessageHeaders, this.message); | 		Object resolved = this.resolver.resolveArgument(this.resolvable.arg(MessageHeaders.class), this.message); | ||||||
| 
 | 
 | ||||||
| 		assertTrue(resolved instanceof MessageHeaders); | 		assertTrue(resolved instanceof MessageHeaders); | ||||||
| 		MessageHeaders headers = (MessageHeaders) resolved; | 		MessageHeaders headers = (MessageHeaders) resolved; | ||||||
|  | @ -105,7 +88,8 @@ public class HeadersMethodArgumentResolverTests { | ||||||
| 
 | 
 | ||||||
| 	@Test | 	@Test | ||||||
| 	public void resolveArgumentMessageHeaderAccessor() throws Exception { | 	public void resolveArgumentMessageHeaderAccessor() throws Exception { | ||||||
| 		Object resolved = this.resolver.resolveArgument(this.paramMessageHeaderAccessor, this.message); | 		MethodParameter param = this.resolvable.arg(MessageHeaderAccessor.class); | ||||||
|  | 		Object resolved = this.resolver.resolveArgument(param, this.message); | ||||||
| 
 | 
 | ||||||
| 		assertTrue(resolved instanceof MessageHeaderAccessor); | 		assertTrue(resolved instanceof MessageHeaderAccessor); | ||||||
| 		MessageHeaderAccessor headers = (MessageHeaderAccessor) resolved; | 		MessageHeaderAccessor headers = (MessageHeaderAccessor) resolved; | ||||||
|  | @ -114,7 +98,8 @@ public class HeadersMethodArgumentResolverTests { | ||||||
| 
 | 
 | ||||||
| 	@Test | 	@Test | ||||||
| 	public void resolveArgumentMessageHeaderAccessorSubclass() throws Exception { | 	public void resolveArgumentMessageHeaderAccessorSubclass() throws Exception { | ||||||
| 		Object resolved = this.resolver.resolveArgument(this.paramMessageHeaderAccessorSubclass, this.message); | 		MethodParameter param = this.resolvable.arg(TestMessageHeaderAccessor.class); | ||||||
|  | 		Object resolved = this.resolver.resolveArgument(param, this.message); | ||||||
| 
 | 
 | ||||||
| 		assertTrue(resolved instanceof TestMessageHeaderAccessor); | 		assertTrue(resolved instanceof TestMessageHeaderAccessor); | ||||||
| 		TestMessageHeaderAccessor headers = (TestMessageHeaderAccessor) resolved; | 		TestMessageHeaderAccessor headers = (TestMessageHeaderAccessor) resolved; | ||||||
|  | @ -124,7 +109,7 @@ public class HeadersMethodArgumentResolverTests { | ||||||
| 
 | 
 | ||||||
| 	@SuppressWarnings("unused") | 	@SuppressWarnings("unused") | ||||||
| 	private void handleMessage( | 	private void handleMessage( | ||||||
| 			@Headers Map<String, ?> param1, | 			@Headers Map<String, Object> param1, | ||||||
| 			@Headers String param2, | 			@Headers String param2, | ||||||
| 			MessageHeaders param3, | 			MessageHeaders param3, | ||||||
| 			MessageHeaderAccessor param4, | 			MessageHeaderAccessor param4, | ||||||
|  | @ -134,7 +119,7 @@ public class HeadersMethodArgumentResolverTests { | ||||||
| 
 | 
 | ||||||
| 	public static class TestMessageHeaderAccessor extends NativeMessageHeaderAccessor { | 	public static class TestMessageHeaderAccessor extends NativeMessageHeaderAccessor { | ||||||
| 
 | 
 | ||||||
| 		protected TestMessageHeaderAccessor(Message<?> message) { | 		TestMessageHeaderAccessor(Message<?> message) { | ||||||
| 			super(message); | 			super(message); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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<String, Object> vars = new HashMap<>(); | ||||||
|  | 		vars.put("foo", "bar"); | ||||||
|  | 		vars.put("name", "value"); | ||||||
|  | 
 | ||||||
|  | 		Message<byte[]> 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<byte[]> message = MessageBuilder.withPayload(new byte[0]).build(); | ||||||
|  | 		resolveArgument(this.resolvable.annot(destinationVar().noValue()).arg(), message); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings({"unchecked", "ConstantConditions"}) | ||||||
|  | 	private <T> 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) { | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -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<byte[]> 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<byte[]> 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<byte[]> 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<byte[]> message = MessageBuilder.withPayload(new byte[0]).build(); | ||||||
|  | 		resolveArgument(this.resolvable.annot(headerPlain()).arg(), message); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void resolveArgumentDefaultValue() { | ||||||
|  | 		Message<byte[]> 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<byte[]> 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<byte[]> 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<String> 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<String> 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> 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<String> param6, | ||||||
|  | 			@Header("nativeHeaders.param1") String nativeHeaderParam1) { | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	public static class TestMessageHeaderAccessor extends NativeMessageHeaderAccessor { | ||||||
|  | 
 | ||||||
|  | 		TestMessageHeaderAccessor() { | ||||||
|  | 			super((Map<String, List<String>>) null); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -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<byte[]> 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<String, Object> 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> T resolveArgument(MethodParameter param) { | ||||||
|  | 		return (T) this.resolver.resolveArgument(param, this.message).block(Duration.ofSeconds(5)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("unused") | ||||||
|  | 	private void handleMessage( | ||||||
|  | 			@Headers Map<String, Object> 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); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,201 @@ | ||||||
|  | /* | ||||||
|  |  * 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.Arrays; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | import org.junit.Test; | ||||||
|  | import reactor.core.publisher.Flux; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | import reactor.test.StepVerifier; | ||||||
|  | 
 | ||||||
|  | import org.springframework.beans.factory.config.EmbeddedValueResolver; | ||||||
|  | import org.springframework.context.support.StaticApplicationContext; | ||||||
|  | import org.springframework.core.ReactiveAdapterRegistry; | ||||||
|  | import org.springframework.core.codec.CharSequenceEncoder; | ||||||
|  | import org.springframework.core.codec.Decoder; | ||||||
|  | import org.springframework.core.codec.Encoder; | ||||||
|  | import org.springframework.core.codec.StringDecoder; | ||||||
|  | import org.springframework.core.env.MapPropertySource; | ||||||
|  | import org.springframework.core.env.PropertySource; | ||||||
|  | import org.springframework.core.io.buffer.DataBuffer; | ||||||
|  | import org.springframework.core.io.buffer.DataBufferFactory; | ||||||
|  | import org.springframework.core.io.buffer.DefaultDataBufferFactory; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | import org.springframework.messaging.handler.DestinationPatternsMessageCondition; | ||||||
|  | import org.springframework.messaging.handler.annotation.MessageExceptionHandler; | ||||||
|  | import org.springframework.messaging.handler.annotation.MessageMapping; | ||||||
|  | import org.springframework.messaging.handler.invocation.reactive.TestEncoderMethodReturnValueHandler; | ||||||
|  | import org.springframework.messaging.support.GenericMessage; | ||||||
|  | import org.springframework.stereotype.Controller; | ||||||
|  | 
 | ||||||
|  | import static java.nio.charset.StandardCharsets.*; | ||||||
|  | import static org.junit.Assert.*; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Unit tests for {@link MessageMappingMessageHandler}. | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  */ | ||||||
|  | @SuppressWarnings("ALL") | ||||||
|  | public class MessageMappingMessageHandlerTests { | ||||||
|  | 
 | ||||||
|  | 	private static final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private TestEncoderMethodReturnValueHandler returnValueHandler; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void handleString() { | ||||||
|  | 		MessageMappingMessageHandler messsageHandler = initMesssageHandler(); | ||||||
|  | 		messsageHandler.handleMessage(message("string", "abcdef")).block(Duration.ofSeconds(5)); | ||||||
|  | 		verifyOutputContent(Collections.singletonList("abcdef::response")); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void handleMonoString() { | ||||||
|  | 		MessageMappingMessageHandler messsageHandler = initMesssageHandler(); | ||||||
|  | 		messsageHandler.handleMessage(message("monoString", "abcdef")).block(Duration.ofSeconds(5)); | ||||||
|  | 		verifyOutputContent(Collections.singletonList("abcdef::response")); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void handleFluxString() { | ||||||
|  | 		MessageMappingMessageHandler messsageHandler = initMesssageHandler(); | ||||||
|  | 		messsageHandler.handleMessage(message("fluxString", "abc\ndef\nghi")).block(Duration.ofSeconds(5)); | ||||||
|  | 		verifyOutputContent(Arrays.asList("abc::response", "def::response", "ghi::response")); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void handleWithPlaceholderInMapping() { | ||||||
|  | 		MessageMappingMessageHandler messsageHandler = initMesssageHandler(); | ||||||
|  | 		messsageHandler.handleMessage(message("path123", "abcdef")).block(Duration.ofSeconds(5)); | ||||||
|  | 		verifyOutputContent(Collections.singletonList("abcdef::response")); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void handleException() { | ||||||
|  | 		MessageMappingMessageHandler messsageHandler = initMesssageHandler(); | ||||||
|  | 		messsageHandler.handleMessage(message("exception", "abc")).block(Duration.ofSeconds(5)); | ||||||
|  | 		verifyOutputContent(Collections.singletonList("rejected::handled")); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void handleErrorSignal() { | ||||||
|  | 		MessageMappingMessageHandler messsageHandler = initMesssageHandler(); | ||||||
|  | 		messsageHandler.handleMessage(message("errorSignal", "abc")).block(Duration.ofSeconds(5)); | ||||||
|  | 		verifyOutputContent(Collections.singletonList("rejected::handled")); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void unhandledExceptionShouldFlowThrough() { | ||||||
|  | 
 | ||||||
|  | 		GenericMessage<?> message = new GenericMessage<>(new Object(), | ||||||
|  | 				Collections.singletonMap(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, "string")); | ||||||
|  | 
 | ||||||
|  | 		StepVerifier.create(initMesssageHandler().handleMessage(message)) | ||||||
|  | 				.expectErrorSatisfies(ex -> assertTrue( | ||||||
|  | 						"Actual: " + ex.getMessage(), | ||||||
|  | 						ex.getMessage().startsWith("Could not resolve method parameter at index 0"))) | ||||||
|  | 				.verify(Duration.ofSeconds(5)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private MessageMappingMessageHandler initMesssageHandler() { | ||||||
|  | 
 | ||||||
|  | 		List<Decoder<?>> decoders = Collections.singletonList(StringDecoder.allMimeTypes()); | ||||||
|  | 		List<Encoder<?>> encoders = Collections.singletonList(CharSequenceEncoder.allMimeTypes()); | ||||||
|  | 
 | ||||||
|  | 		ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance(); | ||||||
|  | 		this.returnValueHandler = new TestEncoderMethodReturnValueHandler(encoders, registry); | ||||||
|  | 
 | ||||||
|  | 		PropertySource<?> source = new MapPropertySource("test", Collections.singletonMap("path", "path123")); | ||||||
|  | 
 | ||||||
|  | 		StaticApplicationContext context = new StaticApplicationContext(); | ||||||
|  | 		context.getEnvironment().getPropertySources().addFirst(source); | ||||||
|  | 		context.registerSingleton("testController", TestController.class); | ||||||
|  | 		context.refresh(); | ||||||
|  | 
 | ||||||
|  | 		MessageMappingMessageHandler messageHandler = new MessageMappingMessageHandler(); | ||||||
|  | 		messageHandler.getReturnValueHandlerConfigurer().addCustomHandler(this.returnValueHandler); | ||||||
|  | 		messageHandler.setApplicationContext(context); | ||||||
|  | 		messageHandler.setEmbeddedValueResolver(new EmbeddedValueResolver(context.getBeanFactory())); | ||||||
|  | 		messageHandler.setDecoders(decoders); | ||||||
|  | 		messageHandler.afterPropertiesSet(); | ||||||
|  | 
 | ||||||
|  | 		return messageHandler; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private Message<?> message(String destination, String... content) { | ||||||
|  | 		return new GenericMessage<>( | ||||||
|  | 				Flux.fromIterable(Arrays.asList(content)).map(payload -> toDataBuffer(payload)), | ||||||
|  | 				Collections.singletonMap(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, destination)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private DataBuffer toDataBuffer(String payload) { | ||||||
|  | 		return bufferFactory.wrap(payload.getBytes(UTF_8)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private void verifyOutputContent(List<String> expected) { | ||||||
|  | 		Flux<String> result = this.returnValueHandler.getContentAsStrings(); | ||||||
|  | 		StepVerifier.create(result.collectList()).expectNext(expected).verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Controller | ||||||
|  | 	static class TestController { | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("string") | ||||||
|  | 		String handleString(String payload) { | ||||||
|  | 			return payload + "::response"; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("monoString") | ||||||
|  | 		Mono<String> handleMonoString(Mono<String> payload) { | ||||||
|  | 			return payload.map(s -> s + "::response").delayElement(Duration.ofMillis(10)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("fluxString") | ||||||
|  | 		Flux<String> handleFluxString(Flux<String> payload) { | ||||||
|  | 			return payload.map(s -> s + "::response").delayElements(Duration.ofMillis(10)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("${path}") | ||||||
|  | 		String handleWithPlaceholder(String payload) { | ||||||
|  | 			return payload + "::response"; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("exception") | ||||||
|  | 		String handleAndThrow() { | ||||||
|  | 			throw new IllegalArgumentException("rejected"); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("errorSignal") | ||||||
|  | 		Mono<String> handleAndSignalError() { | ||||||
|  | 			return Mono.delay(Duration.ofMillis(10)) | ||||||
|  | 					.flatMap(aLong -> Mono.error(new IllegalArgumentException("rejected"))); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageExceptionHandler | ||||||
|  | 		Mono<String> handleException(IllegalArgumentException ex) { | ||||||
|  | 			return Mono.delay(Duration.ofMillis(10)).map(aLong -> ex.getMessage() + "::handled"); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,208 @@ | ||||||
|  | /* | ||||||
|  |  * 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.nio.charset.StandardCharsets; | ||||||
|  | import java.time.Duration; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | import org.junit.Test; | ||||||
|  | import org.reactivestreams.Publisher; | ||||||
|  | import reactor.core.publisher.Flux; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | import reactor.test.StepVerifier; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.MethodParameter; | ||||||
|  | import org.springframework.core.ResolvableType; | ||||||
|  | import org.springframework.core.codec.Decoder; | ||||||
|  | import org.springframework.core.codec.StringDecoder; | ||||||
|  | import org.springframework.core.io.buffer.DataBuffer; | ||||||
|  | import org.springframework.core.io.buffer.DefaultDataBufferFactory; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | import org.springframework.messaging.MessageHeaders; | ||||||
|  | import org.springframework.messaging.handler.annotation.Payload; | ||||||
|  | import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException; | ||||||
|  | import org.springframework.messaging.handler.invocation.MethodArgumentResolutionException; | ||||||
|  | import org.springframework.messaging.handler.invocation.ResolvableMethod; | ||||||
|  | import org.springframework.messaging.support.GenericMessage; | ||||||
|  | import org.springframework.util.MimeTypeUtils; | ||||||
|  | import org.springframework.validation.Errors; | ||||||
|  | import org.springframework.validation.Validator; | ||||||
|  | import org.springframework.validation.annotation.Validated; | ||||||
|  | 
 | ||||||
|  | import static org.junit.Assert.*; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Unit tests for {@link PayloadMethodArgumentResolver}. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  */ | ||||||
|  | public class PayloadMethodArgumentResolverTests { | ||||||
|  | 
 | ||||||
|  | 	private final List<Decoder<?>> decoders = new ArrayList<>(); | ||||||
|  | 
 | ||||||
|  | 	private final ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void supportsParameter() { | ||||||
|  | 
 | ||||||
|  | 		boolean useDefaultResolution = true; | ||||||
|  | 		PayloadMethodArgumentResolver resolver = createResolver(null, useDefaultResolution); | ||||||
|  | 
 | ||||||
|  | 		assertTrue(resolver.supportsParameter(this.testMethod.annotPresent(Payload.class).arg())); | ||||||
|  | 		assertTrue(resolver.supportsParameter(this.testMethod.annotNotPresent(Payload.class).arg(String.class))); | ||||||
|  | 
 | ||||||
|  | 		useDefaultResolution = false; | ||||||
|  | 		resolver = createResolver(null, useDefaultResolution); | ||||||
|  | 
 | ||||||
|  | 		assertTrue(resolver.supportsParameter(this.testMethod.annotPresent(Payload.class).arg())); | ||||||
|  | 		assertFalse(resolver.supportsParameter(this.testMethod.annotNotPresent(Payload.class).arg(String.class))); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void emptyBodyWhenRequired() { | ||||||
|  | 		MethodParameter param = this.testMethod.arg(ResolvableType.forClassWithGenerics(Mono.class, String.class)); | ||||||
|  | 		Mono<Object> mono = resolveValue(param, Mono.empty(), null); | ||||||
|  | 
 | ||||||
|  | 		StepVerifier.create(mono) | ||||||
|  | 				.consumeErrorWith(ex -> { | ||||||
|  | 					assertEquals(MethodArgumentResolutionException.class, ex.getClass()); | ||||||
|  | 					assertTrue(ex.getMessage(), ex.getMessage().contains("Payload content is missing")); | ||||||
|  | 				}) | ||||||
|  | 				.verify(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void emptyBodyWhenNotRequired() { | ||||||
|  | 		MethodParameter param = this.testMethod.annotPresent(Payload.class).arg(); | ||||||
|  | 		assertNull(resolveValue(param, Mono.empty(), null)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void stringMono() { | ||||||
|  | 		String body = "foo"; | ||||||
|  | 		MethodParameter param = this.testMethod.arg(ResolvableType.forClassWithGenerics(Mono.class, String.class)); | ||||||
|  | 		Mono<Object> mono = resolveValue(param, | ||||||
|  | 				Mono.delay(Duration.ofMillis(10)).map(aLong -> toDataBuffer(body)), null); | ||||||
|  | 
 | ||||||
|  | 		assertEquals(body, mono.block()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void stringFlux() { | ||||||
|  | 		List<String> body = Arrays.asList("foo", "bar"); | ||||||
|  | 		ResolvableType type = ResolvableType.forClassWithGenerics(Flux.class, String.class); | ||||||
|  | 		MethodParameter param = this.testMethod.arg(type); | ||||||
|  | 		Flux<Object> flux = resolveValue(param, | ||||||
|  | 				Flux.fromIterable(body).delayElements(Duration.ofMillis(10)).map(this::toDataBuffer), null); | ||||||
|  | 
 | ||||||
|  | 		assertEquals(body, flux.collectList().block()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void string() { | ||||||
|  | 		String body = "foo"; | ||||||
|  | 		MethodParameter param = this.testMethod.annotNotPresent(Payload.class).arg(String.class); | ||||||
|  | 		Object value = resolveValue(param, Mono.just(toDataBuffer(body)), null); | ||||||
|  | 
 | ||||||
|  | 		assertEquals(body, value); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void validateStringMono() { | ||||||
|  | 		ResolvableType type = ResolvableType.forClassWithGenerics(Mono.class, String.class); | ||||||
|  | 		MethodParameter param = this.testMethod.arg(type); | ||||||
|  | 		Mono<Object> mono = resolveValue(param, Mono.just(toDataBuffer("12345")), new TestValidator()); | ||||||
|  | 
 | ||||||
|  | 		StepVerifier.create(mono).expectNextCount(0) | ||||||
|  | 				.expectError(MethodArgumentNotValidException.class).verify(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void validateStringFlux() { | ||||||
|  | 		ResolvableType type = ResolvableType.forClassWithGenerics(Flux.class, String.class); | ||||||
|  | 		MethodParameter param = this.testMethod.arg(type); | ||||||
|  | 		Flux<Object> flux = resolveValue(param, Mono.just(toDataBuffer("12345678\n12345")), new TestValidator()); | ||||||
|  | 
 | ||||||
|  | 		StepVerifier.create(flux) | ||||||
|  | 				.expectNext("12345678") | ||||||
|  | 				.expectError(MethodArgumentNotValidException.class) | ||||||
|  | 				.verify(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private DataBuffer toDataBuffer(String value) { | ||||||
|  | 		return new DefaultDataBufferFactory().wrap(value.getBytes(StandardCharsets.UTF_8)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("unchecked") | ||||||
|  | 	@Nullable | ||||||
|  | 	private <T> T resolveValue(MethodParameter param, Publisher<DataBuffer> content, Validator validator) { | ||||||
|  | 
 | ||||||
|  | 		Message<?> message = new GenericMessage<>(content, | ||||||
|  | 				Collections.singletonMap(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN)); | ||||||
|  | 
 | ||||||
|  | 		Mono<Object> result = createResolver(validator, true).resolveArgument(param, message); | ||||||
|  | 
 | ||||||
|  | 		Object value = result.block(Duration.ofSeconds(5)); | ||||||
|  | 		if (value != null) { | ||||||
|  | 			Class<?> expectedType = param.getParameterType(); | ||||||
|  | 			assertTrue("Unexpected return value type: " + value, expectedType.isAssignableFrom(value.getClass())); | ||||||
|  | 		} | ||||||
|  | 		return (T) value; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private PayloadMethodArgumentResolver createResolver(@Nullable Validator validator, boolean useDefaultResolution) { | ||||||
|  | 		if (this.decoders.isEmpty()) { | ||||||
|  | 			this.decoders.add(StringDecoder.allMimeTypes()); | ||||||
|  | 		} | ||||||
|  | 		List<StringDecoder> decoders = Collections.singletonList(StringDecoder.allMimeTypes()); | ||||||
|  | 		return new PayloadMethodArgumentResolver(decoders, validator, null, useDefaultResolution) {}; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("unused") | ||||||
|  | 	private void handle( | ||||||
|  | 			@Validated Mono<String> valueMono, | ||||||
|  | 			@Validated Flux<String> valueFlux, | ||||||
|  | 			@Payload(required = false) String optionalValue, | ||||||
|  | 			String value) { | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private static class TestValidator implements Validator { | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public boolean supports(Class<?> clazz) { | ||||||
|  | 			return clazz.equals(String.class); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public void validate(@Nullable Object target, Errors errors) { | ||||||
|  | 			if (target instanceof String && ((String) target).length() < 8) { | ||||||
|  | 				errors.reject("Invalid length"); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -23,7 +23,6 @@ import org.junit.Test; | ||||||
| import org.springframework.core.MethodParameter; | import org.springframework.core.MethodParameter; | ||||||
| import org.springframework.lang.Nullable; | import org.springframework.lang.Nullable; | ||||||
| import org.springframework.messaging.Message; | import org.springframework.messaging.Message; | ||||||
| import org.springframework.messaging.handler.ResolvableMethod; |  | ||||||
| 
 | 
 | ||||||
| import static org.hamcrest.Matchers.*; | import static org.hamcrest.Matchers.*; | ||||||
| import static org.junit.Assert.*; | import static org.junit.Assert.*; | ||||||
|  |  | ||||||
|  | @ -20,7 +20,6 @@ import java.lang.reflect.Method; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.Arrays; | import java.util.Arrays; | ||||||
| import java.util.Comparator; | import java.util.Comparator; | ||||||
| import java.util.HashMap; |  | ||||||
| import java.util.LinkedHashMap; | import java.util.LinkedHashMap; | ||||||
| import java.util.LinkedHashSet; | import java.util.LinkedHashSet; | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  | @ -32,7 +31,6 @@ import org.junit.Before; | ||||||
| import org.junit.Test; | import org.junit.Test; | ||||||
| 
 | 
 | ||||||
| import org.springframework.context.support.StaticApplicationContext; | import org.springframework.context.support.StaticApplicationContext; | ||||||
| import org.springframework.core.MethodIntrospector; |  | ||||||
| import org.springframework.messaging.Message; | import org.springframework.messaging.Message; | ||||||
| import org.springframework.messaging.converter.SimpleMessageConverter; | import org.springframework.messaging.converter.SimpleMessageConverter; | ||||||
| import org.springframework.messaging.handler.DestinationPatternsMessageCondition; | import org.springframework.messaging.handler.DestinationPatternsMessageCondition; | ||||||
|  | @ -40,8 +38,8 @@ import org.springframework.messaging.handler.HandlerMethod; | ||||||
| import org.springframework.messaging.handler.annotation.support.MessageMethodArgumentResolver; | import org.springframework.messaging.handler.annotation.support.MessageMethodArgumentResolver; | ||||||
| import org.springframework.messaging.support.MessageBuilder; | import org.springframework.messaging.support.MessageBuilder; | ||||||
| import org.springframework.util.AntPathMatcher; | import org.springframework.util.AntPathMatcher; | ||||||
|  | import org.springframework.util.Assert; | ||||||
| import org.springframework.util.PathMatcher; | import org.springframework.util.PathMatcher; | ||||||
| import org.springframework.util.ReflectionUtils.MethodFilter; |  | ||||||
| 
 | 
 | ||||||
| import static org.junit.Assert.*; | import static org.junit.Assert.*; | ||||||
| 
 | 
 | ||||||
|  | @ -90,7 +88,7 @@ public class MethodMessageHandlerTests { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test | 	@Test | ||||||
| 	public void antPatchMatchWildcard() throws Exception { | 	public void patternMatch() throws Exception { | ||||||
| 
 | 
 | ||||||
| 		Method method = this.testController.getClass().getMethod("handlerPathMatchWildcard"); | 		Method method = this.testController.getClass().getMethod("handlerPathMatchWildcard"); | ||||||
| 		this.messageHandler.registerHandlerMethod(this.testController, method, "/handlerPathMatch*"); | 		this.messageHandler.registerHandlerMethod(this.testController, method, "/handlerPathMatch*"); | ||||||
|  | @ -101,7 +99,7 @@ public class MethodMessageHandlerTests { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test | 	@Test | ||||||
| 	public void bestMatchWildcard() throws Exception { | 	public void bestMatch() throws Exception { | ||||||
| 
 | 
 | ||||||
| 		Method method = this.testController.getClass().getMethod("bestMatch"); | 		Method method = this.testController.getClass().getMethod("bestMatch"); | ||||||
| 		this.messageHandler.registerHandlerMethod(this.testController, method, "/bestmatch/{foo}/path"); | 		this.messageHandler.registerHandlerMethod(this.testController, method, "/bestmatch/{foo}/path"); | ||||||
|  | @ -124,7 +122,7 @@ public class MethodMessageHandlerTests { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Test | 	@Test | ||||||
| 	public void exceptionHandled() { | 	public void handleException() { | ||||||
| 
 | 
 | ||||||
| 		this.messageHandler.handleMessage(toDestination("/test/handlerThrowsExc")); | 		this.messageHandler.handleMessage(toDestination("/test/handlerThrowsExc")); | ||||||
| 
 | 
 | ||||||
|  | @ -166,7 +164,7 @@ public class MethodMessageHandlerTests { | ||||||
| 			this.method = "secondBestMatch"; | 			this.method = "secondBestMatch"; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		public void illegalStateException(IllegalStateException exception) { | 		public void handleIllegalStateException(IllegalStateException exception) { | ||||||
| 			this.method = "illegalStateException"; | 			this.method = "illegalStateException"; | ||||||
| 			this.arguments.put("exception", exception); | 			this.arguments.put("exception", exception); | ||||||
| 		} | 		} | ||||||
|  | @ -186,6 +184,7 @@ public class MethodMessageHandlerTests { | ||||||
| 
 | 
 | ||||||
| 		private PathMatcher pathMatcher = new AntPathMatcher(); | 		private PathMatcher pathMatcher = new AntPathMatcher(); | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| 		public void registerHandler(Object handler) { | 		public void registerHandler(Object handler) { | ||||||
| 			super.detectHandlerMethods(handler); | 			super.detectHandlerMethods(handler); | ||||||
| 		} | 		} | ||||||
|  | @ -239,55 +238,24 @@ public class MethodMessageHandlerTests { | ||||||
| 
 | 
 | ||||||
| 		@Override | 		@Override | ||||||
| 		protected String getMatchingMapping(String mapping, Message<?> message) { | 		protected String getMatchingMapping(String mapping, Message<?> message) { | ||||||
| 
 |  | ||||||
| 			String destination = getLookupDestination(getDestination(message)); | 			String destination = getLookupDestination(getDestination(message)); | ||||||
| 			if (mapping.equals(destination) || this.pathMatcher.match(mapping, destination)) { | 			Assert.notNull(destination, "No destination"); | ||||||
| 				return mapping; | 			return mapping.equals(destination) || this.pathMatcher.match(mapping, destination) ? mapping : null; | ||||||
| 			} |  | ||||||
| 			return null; |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		@Override | 		@Override | ||||||
| 		protected Comparator<String> getMappingComparator(final Message<?> message) { | 		protected Comparator<String> getMappingComparator(final Message<?> message) { | ||||||
| 			return new Comparator<String>() { | 			return (info1, info2) -> { | ||||||
| 				@Override |  | ||||||
| 				public int compare(String info1, String info2) { |  | ||||||
| 				DestinationPatternsMessageCondition cond1 = new DestinationPatternsMessageCondition(info1); | 				DestinationPatternsMessageCondition cond1 = new DestinationPatternsMessageCondition(info1); | ||||||
| 				DestinationPatternsMessageCondition cond2 = new DestinationPatternsMessageCondition(info2); | 				DestinationPatternsMessageCondition cond2 = new DestinationPatternsMessageCondition(info2); | ||||||
| 				return cond1.compareTo(cond2, message); | 				return cond1.compareTo(cond2, message); | ||||||
| 				} |  | ||||||
| 			}; | 			}; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		@Override | 		@Override | ||||||
| 		protected AbstractExceptionHandlerMethodResolver createExceptionHandlerMethodResolverFor(Class<?> beanType) { | 		protected AbstractExceptionHandlerMethodResolver createExceptionHandlerMethodResolverFor(Class<?> beanType) { | ||||||
| 			return new TestExceptionHandlerMethodResolver(beanType); | 			return new TestExceptionResolver(beanType); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| 	private static class TestExceptionHandlerMethodResolver extends AbstractExceptionHandlerMethodResolver { |  | ||||||
| 
 |  | ||||||
| 		public TestExceptionHandlerMethodResolver(Class<?> handlerType) { |  | ||||||
| 			super(initExceptionMappings(handlerType)); |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		private static Map<Class<? extends Throwable>, Method> initExceptionMappings(Class<?> handlerType) { |  | ||||||
| 			Map<Class<? extends Throwable>, Method> result = new HashMap<>(); |  | ||||||
| 			for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHOD_FILTER)) { |  | ||||||
| 				for (Class<? extends Throwable> exception : getExceptionsFromMethodSignature(method)) { |  | ||||||
| 					result.put(exception, method); |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 			return result; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		public final static MethodFilter EXCEPTION_HANDLER_METHOD_FILTER = new MethodFilter() { |  | ||||||
| 			@Override |  | ||||||
| 			public boolean matches(Method method) { |  | ||||||
| 				return method.getName().contains("Exception"); |  | ||||||
| 			} |  | ||||||
| 		}; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ | ||||||
|  * limitations under the License. |  * limitations under the License. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| package org.springframework.messaging.handler; | package org.springframework.messaging.handler.invocation; | ||||||
| 
 | 
 | ||||||
| import java.lang.annotation.Annotation; | import java.lang.annotation.Annotation; | ||||||
| import java.lang.reflect.Method; | import java.lang.reflect.Method; | ||||||
|  | @ -57,7 +57,10 @@ import org.springframework.util.ReflectionUtils; | ||||||
| import static java.util.stream.Collectors.*; | import static java.util.stream.Collectors.*; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Convenience class to resolve method parameters from hints. |  * NOTE: This class is a replica of the same class in spring-web so it can | ||||||
|  |  * be used for tests in spring-messaging. | ||||||
|  |  * | ||||||
|  |  * <p>Convenience class to resolve method parameters from hints. | ||||||
|  * |  * | ||||||
|  * <h1>Background</h1> |  * <h1>Background</h1> | ||||||
|  * |  * | ||||||
|  | @ -120,7 +123,7 @@ import static java.util.stream.Collectors.*; | ||||||
|  * </pre> |  * </pre> | ||||||
|  * |  * | ||||||
|  * @author Rossen Stoyanchev |  * @author Rossen Stoyanchev | ||||||
|  * @since 5.0 |  * @since 5.2 | ||||||
|  */ |  */ | ||||||
| public class ResolvableMethod { | public class ResolvableMethod { | ||||||
| 
 | 
 | ||||||
|  | @ -186,6 +189,7 @@ public class ResolvableMethod { | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * Filter on method arguments with annotation. | 	 * Filter on method arguments with annotation. | ||||||
|  | 	 * See {@link org.springframework.web.method.MvcAnnotationPredicates}. | ||||||
| 	 */ | 	 */ | ||||||
| 	@SafeVarargs | 	@SafeVarargs | ||||||
| 	public final ArgResolver annot(Predicate<MethodParameter>... filter) { | 	public final ArgResolver annot(Predicate<MethodParameter>... filter) { | ||||||
|  | @ -298,6 +302,7 @@ public class ResolvableMethod { | ||||||
| 
 | 
 | ||||||
| 		/** | 		/** | ||||||
| 		 * Filter on annotated methods. | 		 * Filter on annotated methods. | ||||||
|  | 		 * See {@link org.springframework.web.method.MvcAnnotationPredicates}. | ||||||
| 		 */ | 		 */ | ||||||
| 		@SafeVarargs | 		@SafeVarargs | ||||||
| 		public final Builder<T> annot(Predicate<Method>... filters) { | 		public final Builder<T> annot(Predicate<Method>... filters) { | ||||||
|  | @ -308,6 +313,7 @@ public class ResolvableMethod { | ||||||
| 		/** | 		/** | ||||||
| 		 * Filter on methods annotated with the given annotation type. | 		 * Filter on methods annotated with the given annotation type. | ||||||
| 		 * @see #annot(Predicate[]) | 		 * @see #annot(Predicate[]) | ||||||
|  | 		 * See {@link org.springframework.web.method.MvcAnnotationPredicates}. | ||||||
| 		 */ | 		 */ | ||||||
| 		@SafeVarargs | 		@SafeVarargs | ||||||
| 		public final Builder<T> annotPresent(Class<? extends Annotation>... annotationTypes) { | 		public final Builder<T> annotPresent(Class<? extends Annotation>... annotationTypes) { | ||||||
|  | @ -524,6 +530,7 @@ public class ResolvableMethod { | ||||||
| 
 | 
 | ||||||
| 		/** | 		/** | ||||||
| 		 * Filter on method arguments with annotations. | 		 * Filter on method arguments with annotations. | ||||||
|  | 		 * See {@link org.springframework.web.method.MvcAnnotationPredicates}. | ||||||
| 		 */ | 		 */ | ||||||
| 		@SafeVarargs | 		@SafeVarargs | ||||||
| 		public final ArgResolver annot(Predicate<MethodParameter>... filters) { | 		public final ArgResolver annot(Predicate<MethodParameter>... filters) { | ||||||
|  | @ -535,6 +542,7 @@ public class ResolvableMethod { | ||||||
| 		 * Filter on method arguments that have the given annotations. | 		 * Filter on method arguments that have the given annotations. | ||||||
| 		 * @param annotationTypes the annotation types | 		 * @param annotationTypes the annotation types | ||||||
| 		 * @see #annot(Predicate[]) | 		 * @see #annot(Predicate[]) | ||||||
|  | 		 * See {@link org.springframework.web.method.MvcAnnotationPredicates}. | ||||||
| 		 */ | 		 */ | ||||||
| 		@SafeVarargs | 		@SafeVarargs | ||||||
| 		public final ArgResolver annotPresent(Class<? extends Annotation>... annotationTypes) { | 		public final ArgResolver annotPresent(Class<? extends Annotation>... annotationTypes) { | ||||||
|  | @ -0,0 +1,49 @@ | ||||||
|  | /* | ||||||
|  |  * 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; | ||||||
|  | 
 | ||||||
|  | import java.lang.reflect.Method; | ||||||
|  | import java.util.HashMap; | ||||||
|  | import java.util.Map; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.MethodIntrospector; | ||||||
|  | import org.springframework.util.ReflectionUtils; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Sub-class for {@link AbstractExceptionHandlerMethodResolver} for testing. | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  */ | ||||||
|  | public class TestExceptionResolver extends AbstractExceptionHandlerMethodResolver { | ||||||
|  | 
 | ||||||
|  | 	private final static ReflectionUtils.MethodFilter EXCEPTION_HANDLER_METHOD_FILTER = | ||||||
|  | 			method -> method.getName().matches("handle[\\w]*Exception"); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	public TestExceptionResolver(Class<?> handlerType) { | ||||||
|  | 		super(initExceptionMappings(handlerType)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private static Map<Class<? extends Throwable>, Method> initExceptionMappings(Class<?> handlerType) { | ||||||
|  | 		Map<Class<? extends Throwable>, Method> result = new HashMap<>(); | ||||||
|  | 		for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHOD_FILTER)) { | ||||||
|  | 			for (Class<? extends Throwable> exception : getExceptionsFromMethodSignature(method)) { | ||||||
|  | 				result.put(exception, method); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return result; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,125 @@ | ||||||
|  | /* | ||||||
|  |  * 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 java.util.Collections; | ||||||
|  | 
 | ||||||
|  | import io.reactivex.Completable; | ||||||
|  | import org.junit.Test; | ||||||
|  | import reactor.core.publisher.Flux; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | import reactor.test.StepVerifier; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.MethodParameter; | ||||||
|  | import org.springframework.core.ReactiveAdapterRegistry; | ||||||
|  | import org.springframework.core.codec.CharSequenceEncoder; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | import org.springframework.messaging.support.GenericMessage; | ||||||
|  | 
 | ||||||
|  | import static org.springframework.messaging.handler.invocation.ResolvableMethod.*; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Unit tests for {@link AbstractEncoderMethodReturnValueHandler}. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  */ | ||||||
|  | public class EncoderMethodReturnValueHandlerTests { | ||||||
|  | 
 | ||||||
|  | 	private final TestEncoderMethodReturnValueHandler handler = new TestEncoderMethodReturnValueHandler( | ||||||
|  | 			Collections.singletonList(CharSequenceEncoder.textPlainOnly()), | ||||||
|  | 			ReactiveAdapterRegistry.getSharedInstance()); | ||||||
|  | 
 | ||||||
|  | 	private final Message<?> message = new GenericMessage<>("shouldn't matter"); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void stringReturnValue() { | ||||||
|  | 		MethodParameter parameter = on(TestController.class).resolveReturnType(String.class); | ||||||
|  | 		this.handler.handleReturnValue("foo", parameter, this.message).block(); | ||||||
|  | 		Flux<String> result = this.handler.getContentAsStrings(); | ||||||
|  | 
 | ||||||
|  | 		StepVerifier.create(result).expectNext("foo").verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void objectReturnValue() { | ||||||
|  | 		MethodParameter parameter = on(TestController.class).resolveReturnType(Object.class); | ||||||
|  | 		this.handler.handleReturnValue("foo", parameter, this.message).block(); | ||||||
|  | 		Flux<String> result = this.handler.getContentAsStrings(); | ||||||
|  | 
 | ||||||
|  | 		StepVerifier.create(result).expectNext("foo").verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void fluxStringReturnValue() { | ||||||
|  | 		MethodParameter parameter = on(TestController.class).resolveReturnType(Flux.class, String.class); | ||||||
|  | 		this.handler.handleReturnValue(Flux.just("foo", "bar"), parameter, this.message).block(); | ||||||
|  | 		Flux<String> result = this.handler.getContentAsStrings(); | ||||||
|  | 
 | ||||||
|  | 		StepVerifier.create(result).expectNext("foo").expectNext("bar").verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void fluxObjectReturnValue() { | ||||||
|  | 		MethodParameter parameter = on(TestController.class).resolveReturnType(Flux.class, Object.class); | ||||||
|  | 		this.handler.handleReturnValue(Flux.just("foo", "bar"), parameter, this.message).block(); | ||||||
|  | 		Flux<String> result = this.handler.getContentAsStrings(); | ||||||
|  | 
 | ||||||
|  | 		StepVerifier.create(result).expectNext("foo").expectNext("bar").verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void voidReturnValue() { | ||||||
|  | 		testVoidReturnType(null, on(TestController.class).resolveReturnType(void.class)); | ||||||
|  | 		testVoidReturnType(Mono.empty(), on(TestController.class).resolveReturnType(Mono.class, Void.class)); | ||||||
|  | 		testVoidReturnType(Completable.complete(), on(TestController.class).resolveReturnType(Completable.class)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private void testVoidReturnType(@Nullable Object value, MethodParameter bodyParameter) { | ||||||
|  | 		this.handler.handleReturnValue(value, bodyParameter, this.message).block(); | ||||||
|  | 		Flux<String> result = this.handler.getContentAsStrings(); | ||||||
|  | 		StepVerifier.create(result).expectComplete().verify(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void noEncoder() { | ||||||
|  | 		MethodParameter parameter = on(TestController.class).resolveReturnType(Object.class); | ||||||
|  | 		StepVerifier.create(this.handler.handleReturnValue(new Object(), parameter, this.message)) | ||||||
|  | 				.expectErrorMessage("No encoder for java.lang.Object, current value type is class java.lang.Object") | ||||||
|  | 				.verify(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings({"unused", "ConstantConditions"}) | ||||||
|  | 	private static class TestController { | ||||||
|  | 
 | ||||||
|  | 		String string() { return null; } | ||||||
|  | 
 | ||||||
|  | 		Object object() { return null; } | ||||||
|  | 
 | ||||||
|  | 		Flux<String> fluxString() { return null; } | ||||||
|  | 
 | ||||||
|  | 		Flux<Object> fluxObject() { return null; } | ||||||
|  | 
 | ||||||
|  | 		void voidReturn() { } | ||||||
|  | 
 | ||||||
|  | 		Mono<Void> monoVoid() { return null; } | ||||||
|  | 
 | ||||||
|  | 		Completable completable() { return null; } | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,237 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright 2002-2018 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 java.lang.reflect.Method; | ||||||
|  | import java.time.Duration; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.concurrent.atomic.AtomicReference; | ||||||
|  | 
 | ||||||
|  | import org.junit.Test; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | import reactor.test.StepVerifier; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.MethodParameter; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | import org.springframework.messaging.handler.invocation.MethodArgumentResolutionException; | ||||||
|  | import org.springframework.messaging.handler.invocation.ResolvableMethod; | ||||||
|  | 
 | ||||||
|  | import static org.junit.Assert.*; | ||||||
|  | import static org.mockito.Mockito.*; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Unit tests for {@link InvocableHandlerMethod}. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  * @author Juergen Hoeller | ||||||
|  |  */ | ||||||
|  | public class InvocableHandlerMethodTests { | ||||||
|  | 
 | ||||||
|  | 	private final Message<?> message = mock(Message.class); | ||||||
|  | 
 | ||||||
|  | 	private final List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void resolveArg() { | ||||||
|  | 		this.resolvers.add(new StubArgumentResolver(99)); | ||||||
|  | 		this.resolvers.add(new StubArgumentResolver("value")); | ||||||
|  | 		Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); | ||||||
|  | 		Object value = invokeAndBlock(new Handler(), method); | ||||||
|  | 
 | ||||||
|  | 		assertEquals(1, getStubResolver(0).getResolvedParameters().size()); | ||||||
|  | 		assertEquals(1, getStubResolver(1).getResolvedParameters().size()); | ||||||
|  | 		assertEquals("99-value", value); | ||||||
|  | 		assertEquals("intArg", getStubResolver(0).getResolvedParameters().get(0).getParameterName()); | ||||||
|  | 		assertEquals("stringArg", getStubResolver(1).getResolvedParameters().get(0).getParameterName()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void resolveNoArgValue() { | ||||||
|  | 		this.resolvers.add(new StubArgumentResolver(Integer.class)); | ||||||
|  | 		this.resolvers.add(new StubArgumentResolver(String.class)); | ||||||
|  | 		Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); | ||||||
|  | 		Object value = invokeAndBlock(new Handler(), method); | ||||||
|  | 
 | ||||||
|  | 		assertEquals(1, getStubResolver(0).getResolvedParameters().size()); | ||||||
|  | 		assertEquals(1, getStubResolver(1).getResolvedParameters().size()); | ||||||
|  | 		assertEquals("null-null", value); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void cannotResolveArg() { | ||||||
|  | 		try { | ||||||
|  | 			Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); | ||||||
|  | 			invokeAndBlock(new Handler(), method); | ||||||
|  | 			fail("Expected exception"); | ||||||
|  | 		} | ||||||
|  | 		catch (MethodArgumentResolutionException ex) { | ||||||
|  | 			assertNotNull(ex.getMessage()); | ||||||
|  | 			assertTrue(ex.getMessage().contains("Could not resolve parameter [0]")); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void resolveProvidedArg() { | ||||||
|  | 		Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); | ||||||
|  | 		Object value = invokeAndBlock(new Handler(), method, 99, "value"); | ||||||
|  | 
 | ||||||
|  | 		assertNotNull(value); | ||||||
|  | 		assertEquals(String.class, value.getClass()); | ||||||
|  | 		assertEquals("99-value", value); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void resolveProvidedArgFirst() { | ||||||
|  | 		this.resolvers.add(new StubArgumentResolver(1)); | ||||||
|  | 		this.resolvers.add(new StubArgumentResolver("value1")); | ||||||
|  | 		Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); | ||||||
|  | 		Object value = invokeAndBlock(new Handler(), method, 2, "value2"); | ||||||
|  | 
 | ||||||
|  | 		assertEquals("2-value2", value); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void exceptionInResolvingArg() { | ||||||
|  | 		this.resolvers.add(new InvocableHandlerMethodTests.ExceptionRaisingArgumentResolver()); | ||||||
|  | 		try { | ||||||
|  | 			Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); | ||||||
|  | 			invokeAndBlock(new Handler(), method); | ||||||
|  | 			fail("Expected exception"); | ||||||
|  | 		} | ||||||
|  | 		catch (IllegalArgumentException ex) { | ||||||
|  | 			// expected -  allow HandlerMethodArgumentResolver exceptions to propagate | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void illegalArgumentException() { | ||||||
|  | 		this.resolvers.add(new StubArgumentResolver(Integer.class, "__not_an_int__")); | ||||||
|  | 		this.resolvers.add(new StubArgumentResolver("value")); | ||||||
|  | 		try { | ||||||
|  | 			Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); | ||||||
|  | 			invokeAndBlock(new Handler(), method); | ||||||
|  | 			fail("Expected exception"); | ||||||
|  | 		} | ||||||
|  | 		catch (IllegalStateException ex) { | ||||||
|  | 			assertNotNull("Exception not wrapped", ex.getCause()); | ||||||
|  | 			assertTrue(ex.getCause() instanceof IllegalArgumentException); | ||||||
|  | 			assertTrue(ex.getMessage().contains("Endpoint [")); | ||||||
|  | 			assertTrue(ex.getMessage().contains("Method [")); | ||||||
|  | 			assertTrue(ex.getMessage().contains("with argument values:")); | ||||||
|  | 			assertTrue(ex.getMessage().contains("[0] [type=java.lang.String] [value=__not_an_int__]")); | ||||||
|  | 			assertTrue(ex.getMessage().contains("[1] [type=java.lang.String] [value=value")); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void invocationTargetException() { | ||||||
|  | 		Method method = ResolvableMethod.on(Handler.class).argTypes(Throwable.class).resolveMethod(); | ||||||
|  | 
 | ||||||
|  | 		Throwable expected = new Throwable("error"); | ||||||
|  | 		Mono<Object> result = invoke(new Handler(), method, expected); | ||||||
|  | 		StepVerifier.create(result).expectErrorSatisfies(actual -> assertSame(expected, actual)).verify(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void voidMethod() { | ||||||
|  | 		this.resolvers.add(new StubArgumentResolver(double.class, 5.25)); | ||||||
|  | 		Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0.0d)).method(); | ||||||
|  | 		Handler handler = new Handler(); | ||||||
|  | 		Object value = invokeAndBlock(handler, method); | ||||||
|  | 
 | ||||||
|  | 		assertNull(value); | ||||||
|  | 		assertEquals(1, getStubResolver(0).getResolvedParameters().size()); | ||||||
|  | 		assertEquals("5.25", handler.getResult()); | ||||||
|  | 		assertEquals("amount", getStubResolver(0).getResolvedParameters().get(0).getParameterName()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void voidMonoMethod() { | ||||||
|  | 		Method method = ResolvableMethod.on(Handler.class).mockCall(Handler::handleAsync).method(); | ||||||
|  | 		Handler handler = new Handler(); | ||||||
|  | 		Object value = invokeAndBlock(handler, method); | ||||||
|  | 
 | ||||||
|  | 		assertNull(value); | ||||||
|  | 		assertEquals("success", handler.getResult()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	private Object invokeAndBlock(Object handler, Method method, Object... providedArgs) { | ||||||
|  | 		return invoke(handler, method, providedArgs).block(Duration.ofSeconds(5)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private Mono<Object> invoke(Object handler, Method method, Object... providedArgs) { | ||||||
|  | 		InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(handler, method); | ||||||
|  | 		handlerMethod.setArgumentResolvers(this.resolvers); | ||||||
|  | 		return handlerMethod.invoke(this.message, providedArgs); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private StubArgumentResolver getStubResolver(int index) { | ||||||
|  | 		return (StubArgumentResolver) this.resolvers.get(index); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings({"unused", "UnusedReturnValue", "SameParameterValue"}) | ||||||
|  | 	private static class Handler { | ||||||
|  | 
 | ||||||
|  | 		private AtomicReference<String> result = new AtomicReference<>(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		public String getResult() { | ||||||
|  | 			return this.result.get(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		String handle(Integer intArg, String stringArg) { | ||||||
|  | 			return intArg + "-" + stringArg; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		void handle(double amount) { | ||||||
|  | 			this.result.set(String.valueOf(amount)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		void handleWithException(Throwable ex) throws Throwable { | ||||||
|  | 			throw ex; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		Mono<Void> handleAsync() { | ||||||
|  | 			return Mono.delay(Duration.ofMillis(100)).thenEmpty(Mono.defer(() -> { | ||||||
|  | 				this.result.set("success"); | ||||||
|  | 				return Mono.empty(); | ||||||
|  | 			})); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public boolean supportsParameter(MethodParameter parameter) { | ||||||
|  | 			return true; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message) { | ||||||
|  | 			return Mono.error(new IllegalArgumentException("oops, can't read")); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,263 @@ | ||||||
|  | /* | ||||||
|  |  * 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 java.lang.reflect.Method; | ||||||
|  | import java.time.Duration; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.Comparator; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.Set; | ||||||
|  | import java.util.function.Consumer; | ||||||
|  | import java.util.function.Predicate; | ||||||
|  | 
 | ||||||
|  | import org.hamcrest.Matchers; | ||||||
|  | import org.junit.Test; | ||||||
|  | import org.reactivestreams.Publisher; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | import reactor.test.StepVerifier; | ||||||
|  | 
 | ||||||
|  | import org.springframework.context.support.StaticApplicationContext; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | import org.springframework.messaging.handler.DestinationPatternsMessageCondition; | ||||||
|  | import org.springframework.messaging.handler.HandlerMethod; | ||||||
|  | import org.springframework.messaging.handler.invocation.AbstractExceptionHandlerMethodResolver; | ||||||
|  | import org.springframework.messaging.handler.invocation.TestExceptionResolver; | ||||||
|  | import org.springframework.messaging.support.GenericMessage; | ||||||
|  | import org.springframework.util.AntPathMatcher; | ||||||
|  | import org.springframework.util.Assert; | ||||||
|  | import org.springframework.util.ClassUtils; | ||||||
|  | import org.springframework.util.PathMatcher; | ||||||
|  | 
 | ||||||
|  | import static org.junit.Assert.*; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Unit tests for {@link AbstractMethodMessageHandler}. | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  */ | ||||||
|  | public class MethodMessageHandlerTests { | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Test(expected = IllegalStateException.class) | ||||||
|  | 	public void duplicateMapping() { | ||||||
|  | 		initMethodMessageHandler(DuplicateMappingsController.class); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void registeredMappings() { | ||||||
|  | 		TestMethodMessageHandler messageHandler = initMethodMessageHandler(TestController.class); | ||||||
|  | 		Map<String, HandlerMethod> mappings = messageHandler.getHandlerMethods(); | ||||||
|  | 
 | ||||||
|  | 		assertEquals(5, mappings.keySet().size()); | ||||||
|  | 		assertThat(mappings.keySet(), Matchers.containsInAnyOrder( | ||||||
|  | 				"/handleMessage", "/handleMessageWithArgument", "/handleMessageWithError", | ||||||
|  | 				"/handleMessageMatch1", "/handleMessageMatch2")); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void bestMatch() throws NoSuchMethodException { | ||||||
|  | 		TestMethodMessageHandler handler = new TestMethodMessageHandler(); | ||||||
|  | 		TestController controller = new TestController(); | ||||||
|  | 		handler.register(controller, TestController.class.getMethod("handleMessageMatch1"), "/bestmatch/{foo}/path"); | ||||||
|  | 		handler.register(controller, TestController.class.getMethod("handleMessageMatch2"), "/bestmatch/*/*"); | ||||||
|  | 		handler.afterPropertiesSet(); | ||||||
|  | 
 | ||||||
|  | 		Message<?> message = new GenericMessage<>("body", Collections.singletonMap( | ||||||
|  | 				DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, "/bestmatch/bar/path")); | ||||||
|  | 
 | ||||||
|  | 		handler.handleMessage(message).block(Duration.ofSeconds(5)); | ||||||
|  | 
 | ||||||
|  | 		StepVerifier.create((Publisher<Object>) handler.getLastReturnValue()) | ||||||
|  | 				.expectNext("handleMessageMatch1") | ||||||
|  | 				.verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void argumentResolution() { | ||||||
|  | 
 | ||||||
|  | 		ArgumentResolverConfigurer configurer = new ArgumentResolverConfigurer(); | ||||||
|  | 		configurer.addCustomResolver(new StubArgumentResolver(String.class, "foo")); | ||||||
|  | 
 | ||||||
|  | 		TestMethodMessageHandler handler = initMethodMessageHandler( | ||||||
|  | 				theHandler -> theHandler.setArgumentResolverConfigurer(configurer), | ||||||
|  | 				TestController.class); | ||||||
|  | 
 | ||||||
|  | 		Message<?> message = new GenericMessage<>("body", Collections.singletonMap( | ||||||
|  | 				DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, "/handleMessageWithArgument")); | ||||||
|  | 
 | ||||||
|  | 		handler.handleMessage(message).block(Duration.ofSeconds(5)); | ||||||
|  | 
 | ||||||
|  | 		StepVerifier.create((Publisher<Object>) handler.getLastReturnValue()) | ||||||
|  | 				.expectNext("handleMessageWithArgument,payload=foo") | ||||||
|  | 				.verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void handleException() { | ||||||
|  | 
 | ||||||
|  | 		TestMethodMessageHandler handler = initMethodMessageHandler(TestController.class); | ||||||
|  | 
 | ||||||
|  | 		Message<?> message = new GenericMessage<>("body", Collections.singletonMap( | ||||||
|  | 				DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, "/handleMessageWithError")); | ||||||
|  | 
 | ||||||
|  | 		handler.handleMessage(message).block(Duration.ofSeconds(5)); | ||||||
|  | 
 | ||||||
|  | 		StepVerifier.create((Publisher<Object>) handler.getLastReturnValue()) | ||||||
|  | 				.expectNext("handleIllegalStateException,ex=rejected") | ||||||
|  | 				.verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private TestMethodMessageHandler initMethodMessageHandler(Class<?>... handlerTypes) { | ||||||
|  | 		return initMethodMessageHandler(handler -> {}, handlerTypes); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private TestMethodMessageHandler initMethodMessageHandler( | ||||||
|  | 			Consumer<TestMethodMessageHandler> customizer, Class<?>... handlerTypes) { | ||||||
|  | 
 | ||||||
|  | 		StaticApplicationContext context = new StaticApplicationContext(); | ||||||
|  | 		for (Class<?> handlerType : handlerTypes) { | ||||||
|  | 			String beanName = ClassUtils.getShortNameAsProperty(handlerType); | ||||||
|  | 			context.registerPrototype(beanName, handlerType); | ||||||
|  | 		} | ||||||
|  | 		TestMethodMessageHandler messageHandler = new TestMethodMessageHandler(); | ||||||
|  | 		messageHandler.setApplicationContext(context); | ||||||
|  | 		customizer.accept(messageHandler); | ||||||
|  | 		messageHandler.afterPropertiesSet(); | ||||||
|  | 		return messageHandler; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("unused") | ||||||
|  | 	private static class TestController { | ||||||
|  | 
 | ||||||
|  | 		public Mono<String> handleMessage() { | ||||||
|  | 			return delay("handleMessage"); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@SuppressWarnings("rawtypes") | ||||||
|  | 		public Mono<String> handleMessageWithArgument(String payload) { | ||||||
|  | 			return delay("handleMessageWithArgument,payload=" + payload); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		public Mono<String> handleMessageWithError() { | ||||||
|  | 			return Mono.delay(Duration.ofMillis(10)) | ||||||
|  | 					.flatMap(aLong -> Mono.error(new IllegalStateException("rejected"))); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		public Mono<String> handleMessageMatch1() { | ||||||
|  | 			return delay("handleMessageMatch1"); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		public Mono<String> handleMessageMatch2() { | ||||||
|  | 			return delay("handleMessageMatch2"); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		public Mono<String> handleIllegalStateException(IllegalStateException ex) { | ||||||
|  | 			return delay("handleIllegalStateException,ex=" + ex.getMessage()); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private Mono<String> delay(String value) { | ||||||
|  | 			return Mono.delay(Duration.ofMillis(10)).map(aLong -> value); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("unused") | ||||||
|  | 	private static class DuplicateMappingsController { | ||||||
|  | 
 | ||||||
|  | 		void handleMessageFoo() { } | ||||||
|  | 
 | ||||||
|  | 		void handleMessageFoo(String foo) { } | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private static class TestMethodMessageHandler extends AbstractMethodMessageHandler<String> { | ||||||
|  | 
 | ||||||
|  | 		private final TestReturnValueHandler returnValueHandler = new TestReturnValueHandler(); | ||||||
|  | 
 | ||||||
|  | 		private PathMatcher pathMatcher = new AntPathMatcher(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		protected List<? extends HandlerMethodArgumentResolver> initArgumentResolvers() { | ||||||
|  | 			return Collections.emptyList(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		protected List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers() { | ||||||
|  | 			return Collections.singletonList(this.returnValueHandler); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		protected Predicate<Class<?>> initHandlerPredicate() { | ||||||
|  | 			return handlerType -> handlerType.getName().endsWith("Controller"); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Nullable | ||||||
|  | 		public Object getLastReturnValue() { | ||||||
|  | 			return this.returnValueHandler.getLastReturnValue(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		public void register(Object handler, Method method, String mapping) { | ||||||
|  | 			super.registerHandlerMethod(handler, method, mapping); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		protected String getMappingForMethod(Method method, Class<?> handlerType) { | ||||||
|  | 			String methodName = method.getName(); | ||||||
|  | 			if (methodName.startsWith("handleMessage")) { | ||||||
|  | 				return "/" + methodName; | ||||||
|  | 			} | ||||||
|  | 			return null; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		protected Set<String> getDirectLookupMappings(String mapping) { | ||||||
|  | 			return Collections.singleton(mapping); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		@Nullable | ||||||
|  | 		protected String getDestination(Message<?> message) { | ||||||
|  | 			return (String) message.getHeaders().get(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		protected String getMatchingMapping(String mapping, Message<?> message) { | ||||||
|  | 			String destination = getDestination(message); | ||||||
|  | 			Assert.notNull(destination, "No destination"); | ||||||
|  | 			return mapping.equals(destination) || this.pathMatcher.match(mapping, destination) ? mapping : null; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		protected Comparator<String> getMappingComparator(Message<?> message) { | ||||||
|  | 			return (info1, info2) -> { | ||||||
|  | 				DestinationPatternsMessageCondition cond1 = new DestinationPatternsMessageCondition(info1); | ||||||
|  | 				DestinationPatternsMessageCondition cond2 = new DestinationPatternsMessageCondition(info2); | ||||||
|  | 				return cond1.compareTo(cond2, message); | ||||||
|  | 			}; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		protected AbstractExceptionHandlerMethodResolver createExceptionMethodResolverFor(Class<?> beanType) { | ||||||
|  | 			return new TestExceptionResolver(beanType); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,74 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright 2002-2018 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 java.util.ArrayList; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.MethodParameter; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Stub resolver for a fixed value type and/or value. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  */ | ||||||
|  | public class StubArgumentResolver implements HandlerMethodArgumentResolver { | ||||||
|  | 
 | ||||||
|  | 	private final Class<?> valueType; | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	private final Object value; | ||||||
|  | 
 | ||||||
|  | 	private List<MethodParameter> resolvedParameters = new ArrayList<>(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	public StubArgumentResolver(Object value) { | ||||||
|  | 		this(value.getClass(), value); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public StubArgumentResolver(Class<?> valueType) { | ||||||
|  | 		this(valueType, null); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public StubArgumentResolver(Class<?> valueType, Object value) { | ||||||
|  | 		this.valueType = valueType; | ||||||
|  | 		this.value = value; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	public List<MethodParameter> getResolvedParameters() { | ||||||
|  | 		return resolvedParameters; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public boolean supportsParameter(MethodParameter parameter) { | ||||||
|  | 		return parameter.getParameterType().equals(this.valueType); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@SuppressWarnings("unchecked") | ||||||
|  | 	@Override | ||||||
|  | 	public Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message) { | ||||||
|  | 		this.resolvedParameters.add(parameter); | ||||||
|  | 		return Mono.justOrEmpty(this.value); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,69 @@ | ||||||
|  | /* | ||||||
|  |  * 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 java.util.List; | ||||||
|  | 
 | ||||||
|  | import reactor.core.publisher.Flux; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.MethodParameter; | ||||||
|  | import org.springframework.core.ReactiveAdapterRegistry; | ||||||
|  | import org.springframework.core.codec.Encoder; | ||||||
|  | import org.springframework.core.io.buffer.DataBuffer; | ||||||
|  | import org.springframework.core.io.buffer.support.DataBufferTestUtils; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | 
 | ||||||
|  | import static java.nio.charset.StandardCharsets.*; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Implementation of {@link AbstractEncoderMethodReturnValueHandler} for tests. | ||||||
|  |  * "Handles" by storing encoded return values. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  */ | ||||||
|  | public class TestEncoderMethodReturnValueHandler extends AbstractEncoderMethodReturnValueHandler { | ||||||
|  | 
 | ||||||
|  | 	private Flux<DataBuffer> encodedContent; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	public TestEncoderMethodReturnValueHandler(List<Encoder<?>> encoders, ReactiveAdapterRegistry registry) { | ||||||
|  | 		super(encoders, registry); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	public Flux<DataBuffer> getContent() { | ||||||
|  | 		return this.encodedContent; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public Flux<String> getContentAsStrings() { | ||||||
|  | 		return this.encodedContent.map(buffer -> DataBufferTestUtils.dumpString(buffer, UTF_8)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	protected Mono<Void> handleEncodedContent( | ||||||
|  | 			Flux<DataBuffer> encodedContent, MethodParameter returnType, Message<?> message) { | ||||||
|  | 
 | ||||||
|  | 		this.encodedContent = encodedContent.cache(); | ||||||
|  | 		return this.encodedContent.then(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	protected Mono<Void> handleNoContent(MethodParameter returnType, Message<?> message) { | ||||||
|  | 		this.encodedContent = Flux.empty(); | ||||||
|  | 		return Mono.empty(); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,59 @@ | ||||||
|  | /* | ||||||
|  |  * 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 org.reactivestreams.Publisher; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.MethodParameter; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.messaging.Message; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Return value handler that simply stores the last return value. | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  */ | ||||||
|  | public class TestReturnValueHandler implements HandlerMethodReturnValueHandler { | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	private Object lastReturnValue; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Nullable | ||||||
|  | 	public Object getLastReturnValue() { | ||||||
|  | 		return this.lastReturnValue; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public boolean supportsReturnType(MethodParameter returnType) { | ||||||
|  | 		return true; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	@SuppressWarnings("unchecked") | ||||||
|  | 	public Mono<Void> handleReturnValue(@Nullable Object value, MethodParameter returnType, Message<?> message) { | ||||||
|  | 		return value instanceof Publisher ? | ||||||
|  | 				new ChannelSendOperator((Publisher) value, this::saveValue) : | ||||||
|  | 				saveValue(value); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private Mono<Void> saveValue(@Nullable Object value) { | ||||||
|  | 		this.lastReturnValue = value; | ||||||
|  | 		return Mono.empty(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,275 @@ | ||||||
|  | /* | ||||||
|  |  * 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.rsocket; | ||||||
|  | 
 | ||||||
|  | import java.nio.charset.StandardCharsets; | ||||||
|  | import java.time.Duration; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.concurrent.atomic.AtomicBoolean; | ||||||
|  | import java.util.function.Function; | ||||||
|  | 
 | ||||||
|  | import io.reactivex.Completable; | ||||||
|  | import io.reactivex.Observable; | ||||||
|  | import io.reactivex.Single; | ||||||
|  | import io.rsocket.AbstractRSocket; | ||||||
|  | import io.rsocket.Payload; | ||||||
|  | import org.junit.Before; | ||||||
|  | import org.junit.Test; | ||||||
|  | import org.reactivestreams.Publisher; | ||||||
|  | import reactor.core.publisher.Flux; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | import reactor.test.StepVerifier; | ||||||
|  | 
 | ||||||
|  | import org.springframework.core.codec.CharSequenceEncoder; | ||||||
|  | import org.springframework.core.codec.StringDecoder; | ||||||
|  | import org.springframework.core.io.buffer.DefaultDataBufferFactory; | ||||||
|  | import org.springframework.lang.Nullable; | ||||||
|  | import org.springframework.messaging.rsocket.RSocketRequester.RequestSpec; | ||||||
|  | import org.springframework.messaging.rsocket.RSocketRequester.ResponseSpec; | ||||||
|  | import org.springframework.util.MimeTypeUtils; | ||||||
|  | 
 | ||||||
|  | import static java.util.concurrent.TimeUnit.*; | ||||||
|  | import static org.junit.Assert.*; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Unit tests for {@link DefaultRSocketRequester}. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  */ | ||||||
|  | public class DefaultRSocketRequesterTests { | ||||||
|  | 
 | ||||||
|  | 	private static final Duration MILLIS_10 = Duration.ofMillis(10); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private TestRSocket rsocket; | ||||||
|  | 
 | ||||||
|  | 	private RSocketRequester requester; | ||||||
|  | 
 | ||||||
|  | 	private final DefaultDataBufferFactory bufferFactory = new DefaultDataBufferFactory(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Before | ||||||
|  | 	public void setUp() { | ||||||
|  | 		RSocketStrategies strategies = RSocketStrategies.builder() | ||||||
|  | 				.decoder(StringDecoder.allMimeTypes()) | ||||||
|  | 				.encoder(CharSequenceEncoder.allMimeTypes()) | ||||||
|  | 				.build(); | ||||||
|  | 		this.rsocket = new TestRSocket(); | ||||||
|  | 		this.requester = RSocketRequester.create(rsocket, MimeTypeUtils.TEXT_PLAIN, strategies); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void singlePayload() { | ||||||
|  | 
 | ||||||
|  | 		// data(Object) | ||||||
|  | 		testSinglePayload(spec -> spec.data("bodyA"), "bodyA"); | ||||||
|  | 		testSinglePayload(spec -> spec.data(Mono.delay(MILLIS_10).map(l -> "bodyA")), "bodyA"); | ||||||
|  | 		testSinglePayload(spec -> spec.data(Mono.delay(MILLIS_10).then()), ""); | ||||||
|  | 		testSinglePayload(spec -> spec.data(Single.timer(10, MILLISECONDS).map(l -> "bodyA")), "bodyA"); | ||||||
|  | 		testSinglePayload(spec -> spec.data(Completable.complete()), ""); | ||||||
|  | 
 | ||||||
|  | 		// data(Publisher<T>, Class<T>) | ||||||
|  | 		testSinglePayload(spec -> spec.data(Mono.delay(MILLIS_10).map(l -> "bodyA"), String.class), "bodyA"); | ||||||
|  | 		testSinglePayload(spec -> spec.data(Mono.delay(MILLIS_10).map(l -> "bodyA"), Object.class), "bodyA"); | ||||||
|  | 		testSinglePayload(spec -> spec.data(Mono.delay(MILLIS_10).then(), Void.class), ""); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private void testSinglePayload(Function<RequestSpec, ResponseSpec> mapper, String expectedValue) { | ||||||
|  | 		mapper.apply(this.requester.route("toA")).send().block(Duration.ofSeconds(5)); | ||||||
|  | 
 | ||||||
|  | 		assertEquals("fireAndForget", this.rsocket.getSavedMethodName()); | ||||||
|  | 		assertEquals("toA", this.rsocket.getSavedPayload().getMetadataUtf8()); | ||||||
|  | 		assertEquals(expectedValue, this.rsocket.getSavedPayload().getDataUtf8()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void multiPayload() { | ||||||
|  | 		String[] values = new String[] {"bodyA", "bodyB", "bodyC"}; | ||||||
|  | 		Flux<String> stringFlux = Flux.fromArray(values).delayElements(MILLIS_10); | ||||||
|  | 
 | ||||||
|  | 		// data(Object) | ||||||
|  | 		testMultiPayload(spec -> spec.data(stringFlux), values); | ||||||
|  | 		testMultiPayload(spec -> spec.data(Flux.empty()), ""); | ||||||
|  | 		testMultiPayload(spec -> spec.data(Observable.fromArray(values).delay(10, MILLISECONDS)), values); | ||||||
|  | 		testMultiPayload(spec -> spec.data(Observable.empty()), ""); | ||||||
|  | 
 | ||||||
|  | 		// data(Publisher<T>, Class<T>) | ||||||
|  | 		testMultiPayload(spec -> spec.data(stringFlux, String.class), values); | ||||||
|  | 		testMultiPayload(spec -> spec.data(stringFlux.cast(Object.class), Object.class), values); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private void testMultiPayload(Function<RequestSpec, ResponseSpec> mapper, String... expectedValues) { | ||||||
|  | 		this.rsocket.reset(); | ||||||
|  | 		mapper.apply(this.requester.route("toA")).retrieveFlux(String.class).blockLast(Duration.ofSeconds(5)); | ||||||
|  | 
 | ||||||
|  | 		assertEquals("requestChannel", this.rsocket.getSavedMethodName()); | ||||||
|  | 		List<Payload> payloads = this.rsocket.getSavedPayloadFlux().collectList().block(Duration.ofSeconds(5)); | ||||||
|  | 		assertNotNull(payloads); | ||||||
|  | 
 | ||||||
|  | 		if (Arrays.equals(new String[] {""}, expectedValues)) { | ||||||
|  | 			assertEquals(1, payloads.size()); | ||||||
|  | 			assertEquals("toA", payloads.get(0).getMetadataUtf8()); | ||||||
|  | 			assertEquals("", payloads.get(0).getDataUtf8()); | ||||||
|  | 		} | ||||||
|  | 		else { | ||||||
|  | 			assertArrayEquals(new String[] {"toA", "", ""}, | ||||||
|  | 					payloads.stream().map(Payload::getMetadataUtf8).toArray(String[]::new)); | ||||||
|  | 			assertArrayEquals(expectedValues, | ||||||
|  | 					payloads.stream().map(Payload::getDataUtf8).toArray(String[]::new)); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void send() { | ||||||
|  | 		String value = "bodyA"; | ||||||
|  | 		this.requester.route("toA").data(value).send().block(Duration.ofSeconds(5)); | ||||||
|  | 
 | ||||||
|  | 		assertEquals("fireAndForget", this.rsocket.getSavedMethodName()); | ||||||
|  | 		assertEquals("toA", this.rsocket.getSavedPayload().getMetadataUtf8()); | ||||||
|  | 		assertEquals("bodyA", this.rsocket.getSavedPayload().getDataUtf8()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void retrieveMono() { | ||||||
|  | 		String value = "bodyA"; | ||||||
|  | 		this.rsocket.setPayloadMonoToReturn(Mono.delay(MILLIS_10).thenReturn(toPayload(value))); | ||||||
|  | 		Mono<String> response = this.requester.route("").data("").retrieveMono(String.class); | ||||||
|  | 
 | ||||||
|  | 		StepVerifier.create(response).expectNext(value).expectComplete().verify(Duration.ofSeconds(5)); | ||||||
|  | 		assertEquals("requestResponse", this.rsocket.getSavedMethodName()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void retrieveMonoVoid() { | ||||||
|  | 		AtomicBoolean consumed = new AtomicBoolean(false); | ||||||
|  | 		Mono<Payload> mono = Mono.delay(MILLIS_10).thenReturn(toPayload("bodyA")).doOnSuccess(p -> consumed.set(true)); | ||||||
|  | 		this.rsocket.setPayloadMonoToReturn(mono); | ||||||
|  | 		this.requester.route("").data("").retrieveMono(Void.class).block(Duration.ofSeconds(5)); | ||||||
|  | 
 | ||||||
|  | 		assertTrue(consumed.get()); | ||||||
|  | 		assertEquals("requestResponse", this.rsocket.getSavedMethodName()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void retrieveFlux() { | ||||||
|  | 		String[] values = new String[] {"bodyA", "bodyB", "bodyC"}; | ||||||
|  | 		this.rsocket.setPayloadFluxToReturn(Flux.fromArray(values).delayElements(MILLIS_10).map(this::toPayload)); | ||||||
|  | 		Flux<String> response = this.requester.route("").data("").retrieveFlux(String.class); | ||||||
|  | 
 | ||||||
|  | 		StepVerifier.create(response).expectNext(values).expectComplete().verify(Duration.ofSeconds(5)); | ||||||
|  | 		assertEquals("requestStream", this.rsocket.getSavedMethodName()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void retrieveFluxVoid() { | ||||||
|  | 		AtomicBoolean consumed = new AtomicBoolean(false); | ||||||
|  | 		Flux<Payload> flux = Flux.just("bodyA", "bodyB") | ||||||
|  | 				.delayElements(MILLIS_10).map(this::toPayload).doOnComplete(() -> consumed.set(true)); | ||||||
|  | 		this.rsocket.setPayloadFluxToReturn(flux); | ||||||
|  | 		this.requester.route("").data("").retrieveFlux(Void.class).blockLast(Duration.ofSeconds(5)); | ||||||
|  | 
 | ||||||
|  | 		assertTrue(consumed.get()); | ||||||
|  | 		assertEquals("requestStream", this.rsocket.getSavedMethodName()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void rejectFluxToMono() { | ||||||
|  | 		try { | ||||||
|  | 			this.requester.route("").data(Flux.just("a", "b")).retrieveMono(String.class); | ||||||
|  | 			fail(); | ||||||
|  | 		} | ||||||
|  | 		catch (IllegalArgumentException ex) { | ||||||
|  | 			assertEquals("No RSocket interaction model for Flux request to Mono response.", ex.getMessage()); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private Payload toPayload(String value) { | ||||||
|  | 		return PayloadUtils.createPayload(bufferFactory.wrap(value.getBytes(StandardCharsets.UTF_8))); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private static class TestRSocket extends AbstractRSocket { | ||||||
|  | 
 | ||||||
|  | 		private Mono<Payload> payloadMonoToReturn = Mono.empty(); | ||||||
|  | 		private Flux<Payload> payloadFluxToReturn = Flux.empty(); | ||||||
|  | 
 | ||||||
|  | 		@Nullable private volatile String savedMethodName; | ||||||
|  | 		@Nullable private volatile Payload savedPayload; | ||||||
|  | 		@Nullable private volatile Flux<Payload> savedPayloadFlux; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		void setPayloadMonoToReturn(Mono<Payload> payloadMonoToReturn) { | ||||||
|  | 			this.payloadMonoToReturn = payloadMonoToReturn; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		void setPayloadFluxToReturn(Flux<Payload> payloadFluxToReturn) { | ||||||
|  | 			this.payloadFluxToReturn = payloadFluxToReturn; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Nullable | ||||||
|  | 		String getSavedMethodName() { | ||||||
|  | 			return this.savedMethodName; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Nullable | ||||||
|  | 		Payload getSavedPayload() { | ||||||
|  | 			return this.savedPayload; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Nullable | ||||||
|  | 		Flux<Payload> getSavedPayloadFlux() { | ||||||
|  | 			return this.savedPayloadFlux; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		public void reset() { | ||||||
|  | 			this.savedMethodName = null; | ||||||
|  | 			this.savedPayload = null; | ||||||
|  | 			this.savedPayloadFlux = null; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public Mono<Void> fireAndForget(Payload payload) { | ||||||
|  | 			this.savedMethodName = "fireAndForget"; | ||||||
|  | 			this.savedPayload = payload; | ||||||
|  | 			return Mono.empty(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public Mono<Payload> requestResponse(Payload payload) { | ||||||
|  | 			this.savedMethodName = "requestResponse"; | ||||||
|  | 			this.savedPayload = payload; | ||||||
|  | 			return this.payloadMonoToReturn; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public Flux<Payload> requestStream(Payload payload) { | ||||||
|  | 			this.savedMethodName = "requestStream"; | ||||||
|  | 			this.savedPayload = payload; | ||||||
|  | 			return this.payloadFluxToReturn; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public Flux<Payload> requestChannel(Publisher<Payload> publisher) { | ||||||
|  | 			this.savedMethodName = "requestChannel"; | ||||||
|  | 			this.savedPayloadFlux = Flux.from(publisher); | ||||||
|  | 			return this.payloadFluxToReturn; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,78 @@ | ||||||
|  | /* | ||||||
|  |  * 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.rsocket; | ||||||
|  | 
 | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.concurrent.CopyOnWriteArrayList; | ||||||
|  | import java.util.concurrent.atomic.AtomicInteger; | ||||||
|  | 
 | ||||||
|  | import io.rsocket.AbstractRSocket; | ||||||
|  | import io.rsocket.Payload; | ||||||
|  | import io.rsocket.RSocket; | ||||||
|  | import io.rsocket.plugins.RSocketInterceptor; | ||||||
|  | import io.rsocket.util.RSocketProxy; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Intercept received RSockets and count successfully completed requests seen | ||||||
|  |  * on the server side. This is useful for verifying fire-and-forget | ||||||
|  |  * interactions. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  */ | ||||||
|  | class FireAndForgetCountingInterceptor extends AbstractRSocket implements RSocketInterceptor { | ||||||
|  | 
 | ||||||
|  | 	private final List<CountingDecorator> rsockets = new CopyOnWriteArrayList<>(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	public int getRSocketCount() { | ||||||
|  | 		return this.rsockets.size(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public int getFireAndForgetCount(int index) { | ||||||
|  | 		return this.rsockets.get(index).getFireAndForgetCount(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public RSocket apply(RSocket rsocket) { | ||||||
|  | 		CountingDecorator decorator = new CountingDecorator(rsocket); | ||||||
|  | 		this.rsockets.add(decorator); | ||||||
|  | 		return decorator; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private static class CountingDecorator extends RSocketProxy { | ||||||
|  | 
 | ||||||
|  | 		private final AtomicInteger fireAndForget = new AtomicInteger(0); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		CountingDecorator(RSocket delegate) { | ||||||
|  | 			super(delegate); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		public int getFireAndForgetCount() { | ||||||
|  | 			return this.fireAndForget.get(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public Mono<Void> fireAndForget(Payload payload) { | ||||||
|  | 			return super.fireAndForget(payload).doOnSuccess(aVoid -> this.fireAndForget.incrementAndGet()); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,466 @@ | ||||||
|  | /* | ||||||
|  |  * 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.rsocket; | ||||||
|  | 
 | ||||||
|  | import java.time.Duration; | ||||||
|  | import java.time.Instant; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.concurrent.CopyOnWriteArrayList; | ||||||
|  | 
 | ||||||
|  | import io.netty.buffer.ByteBuf; | ||||||
|  | import io.netty.buffer.ByteBufAllocator; | ||||||
|  | import io.netty.buffer.PooledByteBufAllocator; | ||||||
|  | import io.netty.buffer.Unpooled; | ||||||
|  | import io.netty.util.ReferenceCounted; | ||||||
|  | import io.rsocket.AbstractRSocket; | ||||||
|  | import io.rsocket.Frame; | ||||||
|  | import io.rsocket.RSocket; | ||||||
|  | import io.rsocket.RSocketFactory; | ||||||
|  | import io.rsocket.plugins.RSocketInterceptor; | ||||||
|  | import io.rsocket.transport.netty.client.TcpClientTransport; | ||||||
|  | import io.rsocket.transport.netty.server.CloseableChannel; | ||||||
|  | import io.rsocket.transport.netty.server.TcpServerTransport; | ||||||
|  | import org.junit.After; | ||||||
|  | import org.junit.AfterClass; | ||||||
|  | import org.junit.Before; | ||||||
|  | import org.junit.BeforeClass; | ||||||
|  | import org.junit.Test; | ||||||
|  | import org.reactivestreams.Publisher; | ||||||
|  | import reactor.core.publisher.Flux; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | import reactor.core.publisher.ReplayProcessor; | ||||||
|  | import reactor.test.StepVerifier; | ||||||
|  | 
 | ||||||
|  | import org.springframework.context.annotation.AnnotationConfigApplicationContext; | ||||||
|  | import org.springframework.context.annotation.Bean; | ||||||
|  | import org.springframework.context.annotation.Configuration; | ||||||
|  | import org.springframework.core.codec.CharSequenceEncoder; | ||||||
|  | import org.springframework.core.codec.StringDecoder; | ||||||
|  | import org.springframework.core.io.Resource; | ||||||
|  | import org.springframework.core.io.buffer.DataBuffer; | ||||||
|  | import org.springframework.core.io.buffer.NettyDataBuffer; | ||||||
|  | import org.springframework.core.io.buffer.NettyDataBufferFactory; | ||||||
|  | import org.springframework.core.io.buffer.PooledDataBuffer; | ||||||
|  | import org.springframework.messaging.handler.annotation.MessageExceptionHandler; | ||||||
|  | import org.springframework.messaging.handler.annotation.MessageMapping; | ||||||
|  | import org.springframework.messaging.handler.annotation.Payload; | ||||||
|  | import org.springframework.stereotype.Controller; | ||||||
|  | import org.springframework.util.MimeTypeUtils; | ||||||
|  | import org.springframework.util.ObjectUtils; | ||||||
|  | 
 | ||||||
|  | import static org.junit.Assert.*; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Tests for scenarios that could lead to Payload and/or DataBuffer leaks. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  */ | ||||||
|  | public class RSocketBufferLeakTests { | ||||||
|  | 
 | ||||||
|  | 	private static AnnotationConfigApplicationContext context; | ||||||
|  | 
 | ||||||
|  | 	private static final PayloadInterceptor payloadInterceptor = new PayloadInterceptor(); | ||||||
|  | 
 | ||||||
|  | 	private static CloseableChannel server; | ||||||
|  | 
 | ||||||
|  | 	private static RSocket client; | ||||||
|  | 
 | ||||||
|  | 	private static RSocketRequester requester; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@BeforeClass | ||||||
|  | 	@SuppressWarnings("ConstantConditions") | ||||||
|  | 	public static void setupOnce() { | ||||||
|  | 
 | ||||||
|  | 		context = new AnnotationConfigApplicationContext(ServerConfig.class); | ||||||
|  | 
 | ||||||
|  | 		server = RSocketFactory.receive() | ||||||
|  | 				.frameDecoder(Frame::retain) // zero copy | ||||||
|  | 				.addServerPlugin(payloadInterceptor) // intercept responding | ||||||
|  | 				.acceptor(context.getBean(MessageHandlerAcceptor.class)) | ||||||
|  | 				.transport(TcpServerTransport.create("localhost", 7000)) | ||||||
|  | 				.start() | ||||||
|  | 				.block(); | ||||||
|  | 
 | ||||||
|  | 		client = RSocketFactory.connect() | ||||||
|  | 				.frameDecoder(Frame::retain) // zero copy | ||||||
|  | 				.addClientPlugin(payloadInterceptor) // intercept outgoing requests | ||||||
|  | 				.dataMimeType(MimeTypeUtils.TEXT_PLAIN_VALUE) | ||||||
|  | 				.transport(TcpClientTransport.create("localhost", 7000)) | ||||||
|  | 				.start() | ||||||
|  | 				.block(); | ||||||
|  | 
 | ||||||
|  | 		requester = RSocketRequester.create( | ||||||
|  | 				client, MimeTypeUtils.TEXT_PLAIN, context.getBean(RSocketStrategies.class)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@AfterClass | ||||||
|  | 	public static void tearDownOnce() { | ||||||
|  | 		client.dispose(); | ||||||
|  | 		server.dispose(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Before | ||||||
|  | 	public void setUp() { | ||||||
|  | 		getLeakAwareNettyDataBufferFactory().reset(); | ||||||
|  | 		payloadInterceptor.reset(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@After | ||||||
|  | 	public void tearDown() throws InterruptedException { | ||||||
|  | 		getLeakAwareNettyDataBufferFactory().checkForLeaks(Duration.ofSeconds(5)); | ||||||
|  | 		payloadInterceptor.checkForLeaks(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private LeakAwareNettyDataBufferFactory getLeakAwareNettyDataBufferFactory() { | ||||||
|  | 		return (LeakAwareNettyDataBufferFactory) context.getBean(RSocketStrategies.class).dataBufferFactory(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void assemblyTimeErrorForHandleAndReply() { | ||||||
|  | 		Mono<String> result = requester.route("A.B").data("foo").retrieveMono(String.class); | ||||||
|  | 		StepVerifier.create(result).expectErrorMatches(ex -> { | ||||||
|  | 			String prefix = "Ambiguous handler methods mapped for destination 'A.B':"; | ||||||
|  | 			return ex.getMessage().startsWith(prefix); | ||||||
|  | 		}).verify(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void subscriptionTimeErrorForHandleAndReply() { | ||||||
|  | 		Mono<String> result = requester.route("not-decodable").data("foo").retrieveMono(String.class); | ||||||
|  | 		StepVerifier.create(result).expectErrorMatches(ex -> { | ||||||
|  | 			String prefix = "Cannot decode to [org.springframework.core.io.Resource]"; | ||||||
|  | 			return ex.getMessage().contains(prefix); | ||||||
|  | 		}).verify(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void errorSignalWithExceptionHandler() { | ||||||
|  | 		Mono<String> result = requester.route("error-signal").data("foo").retrieveMono(String.class); | ||||||
|  | 		StepVerifier.create(result).expectNext("Handled 'bad input'").verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void ignoreInput() { | ||||||
|  | 		Flux<String> result = requester.route("ignore-input").data("a").retrieveFlux(String.class); | ||||||
|  | 		StepVerifier.create(result).expectNext("bar").verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void retrieveMonoFromFluxResponderMethod() { | ||||||
|  | 		Mono<String> result = requester.route("request-stream").data("foo").retrieveMono(String.class); | ||||||
|  | 		StepVerifier.create(result).expectNext("foo-1").verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Controller | ||||||
|  | 	static class ServerController { | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("A.*") | ||||||
|  | 		void ambiguousMatchA(String payload) { | ||||||
|  | 			throw new IllegalStateException("Unexpected call"); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("*.B") | ||||||
|  | 		void ambiguousMatchB(String payload) { | ||||||
|  | 			throw new IllegalStateException("Unexpected call"); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("not-decodable") | ||||||
|  | 		void notDecodable(@Payload Resource resource) { | ||||||
|  | 			throw new IllegalStateException("Unexpected call"); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("error-signal") | ||||||
|  | 		public Flux<String> errorSignal(String payload) { | ||||||
|  | 			return Flux.error(new IllegalArgumentException("bad input")) | ||||||
|  | 					.delayElements(Duration.ofMillis(10)) | ||||||
|  | 					.cast(String.class); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageExceptionHandler | ||||||
|  | 		public String handleIllegalArgument(IllegalArgumentException ex) { | ||||||
|  | 			return "Handled '" + ex.getMessage() + "'"; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("ignore-input") | ||||||
|  | 		Mono<String> ignoreInput() { | ||||||
|  | 			return Mono.delay(Duration.ofMillis(10)).map(l -> "bar"); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("request-stream") | ||||||
|  | 		Flux<String> stream(String payload) { | ||||||
|  | 			return Flux.range(1,100).delayElements(Duration.ofMillis(10)).map(idx -> payload + "-" + idx); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Configuration | ||||||
|  | 	static class ServerConfig { | ||||||
|  | 
 | ||||||
|  | 		@Bean | ||||||
|  | 		public ServerController controller() { | ||||||
|  | 			return new ServerController(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Bean | ||||||
|  | 		public MessageHandlerAcceptor messageHandlerAcceptor() { | ||||||
|  | 			MessageHandlerAcceptor acceptor = new MessageHandlerAcceptor(); | ||||||
|  | 			acceptor.setRSocketStrategies(rsocketStrategies()); | ||||||
|  | 			return acceptor; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Bean | ||||||
|  | 		public RSocketStrategies rsocketStrategies() { | ||||||
|  | 			return RSocketStrategies.builder() | ||||||
|  | 					.decoder(StringDecoder.allMimeTypes()) | ||||||
|  | 					.encoder(CharSequenceEncoder.allMimeTypes()) | ||||||
|  | 					.dataBufferFactory(new LeakAwareNettyDataBufferFactory(PooledByteBufAllocator.DEFAULT)) | ||||||
|  | 					.build(); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Similar {@link org.springframework.core.io.buffer.LeakAwareDataBufferFactory} | ||||||
|  | 	 * but extends {@link NettyDataBufferFactory} rather than rely on | ||||||
|  | 	 * decoration, since {@link PayloadUtils} does instanceof checks. | ||||||
|  | 	 */ | ||||||
|  | 	private static class LeakAwareNettyDataBufferFactory extends NettyDataBufferFactory { | ||||||
|  | 
 | ||||||
|  | 		private final List<DataBufferLeakInfo> created = new ArrayList<>(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		LeakAwareNettyDataBufferFactory(ByteBufAllocator byteBufAllocator) { | ||||||
|  | 			super(byteBufAllocator); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		void checkForLeaks(Duration duration) throws InterruptedException { | ||||||
|  | 			Instant start = Instant.now(); | ||||||
|  | 			while (true) { | ||||||
|  | 				try { | ||||||
|  | 					this.created.forEach(info -> { | ||||||
|  | 						if (((PooledDataBuffer) info.getDataBuffer()).isAllocated()) { | ||||||
|  | 							throw info.getError(); | ||||||
|  | 						} | ||||||
|  | 					}); | ||||||
|  | 					break; | ||||||
|  | 				} | ||||||
|  | 				catch (AssertionError ex) { | ||||||
|  | 					if (Instant.now().isAfter(start.plus(duration))) { | ||||||
|  | 						throw ex; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				Thread.sleep(50); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		void reset() { | ||||||
|  | 			this.created.clear(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public NettyDataBuffer allocateBuffer() { | ||||||
|  | 			return (NettyDataBuffer) record(super.allocateBuffer()); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public NettyDataBuffer allocateBuffer(int initialCapacity) { | ||||||
|  | 			return (NettyDataBuffer) record(super.allocateBuffer(initialCapacity)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public NettyDataBuffer wrap(ByteBuf byteBuf) { | ||||||
|  | 			NettyDataBuffer dataBuffer = super.wrap(byteBuf); | ||||||
|  | 			if (byteBuf != Unpooled.EMPTY_BUFFER) { | ||||||
|  | 				record(dataBuffer); | ||||||
|  | 			} | ||||||
|  | 			return dataBuffer; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public DataBuffer join(List<? extends DataBuffer> dataBuffers) { | ||||||
|  | 			return record(super.join(dataBuffers)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private DataBuffer record(DataBuffer buffer) { | ||||||
|  | 			this.created.add(new DataBufferLeakInfo(buffer, new AssertionError(String.format( | ||||||
|  | 					"DataBuffer leak: {%s} {%s} not released.%nStacktrace at buffer creation: ", buffer, | ||||||
|  | 					ObjectUtils.getIdentityHexString(((NettyDataBuffer) buffer).getNativeBuffer()))))); | ||||||
|  | 			return buffer; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private static class DataBufferLeakInfo { | ||||||
|  | 
 | ||||||
|  | 		private final DataBuffer dataBuffer; | ||||||
|  | 
 | ||||||
|  | 		private final AssertionError error; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		DataBufferLeakInfo(DataBuffer dataBuffer, AssertionError error) { | ||||||
|  | 			this.dataBuffer = dataBuffer; | ||||||
|  | 			this.error = error; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		DataBuffer getDataBuffer() { | ||||||
|  | 			return this.dataBuffer; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		AssertionError getError() { | ||||||
|  | 			return this.error; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * Store all intercepted incoming and outgoing payloads and then use | ||||||
|  | 	 * {@link #checkForLeaks()} at the end to check reference counts. | ||||||
|  | 	 */ | ||||||
|  | 	private static class PayloadInterceptor extends AbstractRSocket implements RSocketInterceptor { | ||||||
|  | 
 | ||||||
|  | 		private final List<PayloadSavingDecorator> rsockets = new CopyOnWriteArrayList<>(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		void checkForLeaks() { | ||||||
|  | 			this.rsockets.stream().map(PayloadSavingDecorator::getPayloads) | ||||||
|  | 					.forEach(payloadInfoProcessor -> { | ||||||
|  | 						payloadInfoProcessor.onComplete(); | ||||||
|  | 						payloadInfoProcessor | ||||||
|  | 								.doOnNext(this::checkForLeak) | ||||||
|  | 								.blockLast(); | ||||||
|  | 					}); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private void checkForLeak(PayloadLeakInfo info) { | ||||||
|  | 			Instant start = Instant.now(); | ||||||
|  | 			while (true) { | ||||||
|  | 				try { | ||||||
|  | 					int count = info.getReferenceCount(); | ||||||
|  | 					assertTrue("Leaked payload (refCnt=" + count + "): " + info, count == 0); | ||||||
|  | 					break; | ||||||
|  | 				} | ||||||
|  | 				catch (AssertionError ex) { | ||||||
|  | 					if (Instant.now().isAfter(start.plus(Duration.ofSeconds(5)))) { | ||||||
|  | 						throw ex; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				try { | ||||||
|  | 					Thread.sleep(50); | ||||||
|  | 				} | ||||||
|  | 				catch (InterruptedException ex) { | ||||||
|  | 					// ignore | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		public void reset() { | ||||||
|  | 			this.rsockets.forEach(PayloadSavingDecorator::reset); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public RSocket apply(RSocket rsocket) { | ||||||
|  | 			PayloadSavingDecorator decorator = new PayloadSavingDecorator(rsocket); | ||||||
|  | 			this.rsockets.add(decorator); | ||||||
|  | 			return decorator; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		private static class PayloadSavingDecorator extends AbstractRSocket { | ||||||
|  | 
 | ||||||
|  | 			private final RSocket delegate; | ||||||
|  | 
 | ||||||
|  | 			private ReplayProcessor<PayloadLeakInfo> payloads = ReplayProcessor.create(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 			PayloadSavingDecorator(RSocket delegate) { | ||||||
|  | 				this.delegate = delegate; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 			ReplayProcessor<PayloadLeakInfo> getPayloads() { | ||||||
|  | 				return this.payloads; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			void reset() { | ||||||
|  | 				this.payloads = ReplayProcessor.create(); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			@Override | ||||||
|  | 			public Mono<Void> fireAndForget(io.rsocket.Payload payload) { | ||||||
|  | 				return this.delegate.fireAndForget(addPayload(payload)); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			@Override | ||||||
|  | 			public Mono<io.rsocket.Payload> requestResponse(io.rsocket.Payload payload) { | ||||||
|  | 				return this.delegate.requestResponse(addPayload(payload)).doOnSuccess(this::addPayload); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			@Override | ||||||
|  | 			public Flux<io.rsocket.Payload> requestStream(io.rsocket.Payload payload) { | ||||||
|  | 				return this.delegate.requestStream(addPayload(payload)).doOnNext(this::addPayload); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			@Override | ||||||
|  | 			public Flux<io.rsocket.Payload> requestChannel(Publisher<io.rsocket.Payload> payloads) { | ||||||
|  | 				return this.delegate | ||||||
|  | 						.requestChannel(Flux.from(payloads).doOnNext(this::addPayload)) | ||||||
|  | 						.doOnNext(this::addPayload); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			private io.rsocket.Payload addPayload(io.rsocket.Payload payload) { | ||||||
|  | 				this.payloads.onNext(new PayloadLeakInfo(payload)); | ||||||
|  | 				return payload; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			@Override | ||||||
|  | 			public Mono<Void> metadataPush(io.rsocket.Payload payload) { | ||||||
|  | 				return this.delegate.metadataPush(addPayload(payload)); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private static class PayloadLeakInfo { | ||||||
|  | 
 | ||||||
|  | 		private final String description; | ||||||
|  | 
 | ||||||
|  | 		private final ReferenceCounted referenceCounted; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		PayloadLeakInfo(io.rsocket.Payload payload) { | ||||||
|  | 			this.description = payload.toString(); | ||||||
|  | 			this.referenceCounted = payload; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		int getReferenceCount() { | ||||||
|  | 			return this.referenceCounted.refCnt(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Override | ||||||
|  | 		public String toString() { | ||||||
|  | 			return this.description; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,274 @@ | ||||||
|  | /* | ||||||
|  |  * 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.rsocket; | ||||||
|  | 
 | ||||||
|  | import java.time.Duration; | ||||||
|  | 
 | ||||||
|  | import io.netty.buffer.PooledByteBufAllocator; | ||||||
|  | import io.rsocket.Frame; | ||||||
|  | import io.rsocket.RSocket; | ||||||
|  | import io.rsocket.RSocketFactory; | ||||||
|  | import io.rsocket.transport.netty.client.TcpClientTransport; | ||||||
|  | import io.rsocket.transport.netty.server.CloseableChannel; | ||||||
|  | import io.rsocket.transport.netty.server.TcpServerTransport; | ||||||
|  | import org.junit.AfterClass; | ||||||
|  | import org.junit.BeforeClass; | ||||||
|  | import org.junit.Test; | ||||||
|  | import reactor.core.publisher.Flux; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | import reactor.core.publisher.ReplayProcessor; | ||||||
|  | import reactor.test.StepVerifier; | ||||||
|  | 
 | ||||||
|  | import org.springframework.context.annotation.AnnotationConfigApplicationContext; | ||||||
|  | import org.springframework.context.annotation.Bean; | ||||||
|  | import org.springframework.context.annotation.Configuration; | ||||||
|  | import org.springframework.core.codec.CharSequenceEncoder; | ||||||
|  | import org.springframework.core.codec.StringDecoder; | ||||||
|  | import org.springframework.core.io.buffer.NettyDataBufferFactory; | ||||||
|  | import org.springframework.messaging.handler.annotation.MessageExceptionHandler; | ||||||
|  | import org.springframework.messaging.handler.annotation.MessageMapping; | ||||||
|  | import org.springframework.stereotype.Controller; | ||||||
|  | import org.springframework.util.MimeTypeUtils; | ||||||
|  | 
 | ||||||
|  | import static org.junit.Assert.*; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Server-side handling of RSocket requests. | ||||||
|  |  * | ||||||
|  |  * @author Rossen Stoyanchev | ||||||
|  |  */ | ||||||
|  | public class RSocketClientToServerIntegrationTests { | ||||||
|  | 
 | ||||||
|  | 	private static AnnotationConfigApplicationContext context; | ||||||
|  | 
 | ||||||
|  | 	private static CloseableChannel server; | ||||||
|  | 
 | ||||||
|  | 	private static FireAndForgetCountingInterceptor interceptor = new FireAndForgetCountingInterceptor(); | ||||||
|  | 
 | ||||||
|  | 	private static RSocket client; | ||||||
|  | 
 | ||||||
|  | 	private static RSocketRequester requester; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@BeforeClass | ||||||
|  | 	@SuppressWarnings("ConstantConditions") | ||||||
|  | 	public static void setupOnce() { | ||||||
|  | 
 | ||||||
|  | 		context = new AnnotationConfigApplicationContext(ServerConfig.class); | ||||||
|  | 
 | ||||||
|  | 		server = RSocketFactory.receive() | ||||||
|  | 				.addServerPlugin(interceptor) | ||||||
|  | 				.frameDecoder(Frame::retain)  // as per https://github.com/rsocket/rsocket-java#zero-copy | ||||||
|  | 				.acceptor(context.getBean(MessageHandlerAcceptor.class)) | ||||||
|  | 				.transport(TcpServerTransport.create("localhost", 7000)) | ||||||
|  | 				.start() | ||||||
|  | 				.block(); | ||||||
|  | 
 | ||||||
|  | 		client = RSocketFactory.connect() | ||||||
|  | 				.dataMimeType(MimeTypeUtils.TEXT_PLAIN_VALUE) | ||||||
|  | 				.frameDecoder(Frame::retain)  // as per https://github.com/rsocket/rsocket-java#zero-copy | ||||||
|  | 				.transport(TcpClientTransport.create("localhost", 7000)) | ||||||
|  | 				.start() | ||||||
|  | 				.block(); | ||||||
|  | 
 | ||||||
|  | 		requester = RSocketRequester.create( | ||||||
|  | 				client, MimeTypeUtils.TEXT_PLAIN, context.getBean(RSocketStrategies.class)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@AfterClass | ||||||
|  | 	public static void tearDownOnce() { | ||||||
|  | 		client.dispose(); | ||||||
|  | 		server.dispose(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void fireAndForget() { | ||||||
|  | 
 | ||||||
|  | 		Flux.range(1, 3) | ||||||
|  | 				.concatMap(i -> requester.route("receive").data("Hello " + i).send()) | ||||||
|  | 				.blockLast(); | ||||||
|  | 
 | ||||||
|  | 		StepVerifier.create(context.getBean(ServerController.class).fireForgetPayloads) | ||||||
|  | 				.expectNext("Hello 1") | ||||||
|  | 				.expectNext("Hello 2") | ||||||
|  | 				.expectNext("Hello 3") | ||||||
|  | 				.thenCancel() | ||||||
|  | 				.verify(Duration.ofSeconds(5)); | ||||||
|  | 
 | ||||||
|  | 		assertEquals(1, interceptor.getRSocketCount()); | ||||||
|  | 		assertEquals("Fire and forget requests did not actually complete handling on the server side", | ||||||
|  | 				3, interceptor.getFireAndForgetCount(0)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void echo() { | ||||||
|  | 		Flux<String> result = Flux.range(1, 3).concatMap(i -> | ||||||
|  | 				requester.route("echo").data("Hello " + i).retrieveMono(String.class)); | ||||||
|  | 
 | ||||||
|  | 		StepVerifier.create(result) | ||||||
|  | 				.expectNext("Hello 1").expectNext("Hello 2").expectNext("Hello 3") | ||||||
|  | 				.verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void echoAsync() { | ||||||
|  | 		Flux<String> result = Flux.range(1, 3).concatMap(i -> | ||||||
|  | 				requester.route("echo-async").data("Hello " + i).retrieveMono(String.class)); | ||||||
|  | 
 | ||||||
|  | 		StepVerifier.create(result) | ||||||
|  | 				.expectNext("Hello 1 async").expectNext("Hello 2 async").expectNext("Hello 3 async") | ||||||
|  | 				.verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void echoStream() { | ||||||
|  | 		Flux<String> result = requester.route("echo-stream").data("Hello").retrieveFlux(String.class); | ||||||
|  | 
 | ||||||
|  | 		StepVerifier.create(result) | ||||||
|  | 				.expectNext("Hello 0").expectNextCount(6).expectNext("Hello 7") | ||||||
|  | 				.thenCancel() | ||||||
|  | 				.verify(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void echoChannel() { | ||||||
|  | 		Flux<String> result = requester.route("echo-channel") | ||||||
|  | 				.data(Flux.range(1, 10).map(i -> "Hello " + i), String.class) | ||||||
|  | 				.retrieveFlux(String.class); | ||||||
|  | 
 | ||||||
|  | 		StepVerifier.create(result) | ||||||
|  | 				.expectNext("Hello 1 async").expectNextCount(8).expectNext("Hello 10 async") | ||||||
|  | 				.verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void voidReturnValue() { | ||||||
|  | 		Flux<String> result = requester.route("void-return-value").data("Hello").retrieveFlux(String.class); | ||||||
|  | 		StepVerifier.create(result).verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void voidReturnValueFromExceptionHandler() { | ||||||
|  | 		Flux<String> result = requester.route("void-return-value").data("bad").retrieveFlux(String.class); | ||||||
|  | 		StepVerifier.create(result).verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void handleWithThrownException() { | ||||||
|  | 		Mono<String> result = requester.route("thrown-exception").data("a").retrieveMono(String.class); | ||||||
|  | 		StepVerifier.create(result).expectNext("Invalid input error handled").verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void handleWithErrorSignal() { | ||||||
|  | 		Mono<String> result = requester.route("error-signal").data("a").retrieveMono(String.class); | ||||||
|  | 		StepVerifier.create(result).expectNext("Invalid input error handled").verifyComplete(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void noMatchingRoute() { | ||||||
|  | 		Mono<String> result = requester.route("invalid").data("anything").retrieveMono(String.class); | ||||||
|  | 		StepVerifier.create(result).verifyErrorMessage("No handler for destination 'invalid'"); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Controller | ||||||
|  | 	static class ServerController { | ||||||
|  | 
 | ||||||
|  | 		final ReplayProcessor<String> fireForgetPayloads = ReplayProcessor.create(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("receive") | ||||||
|  | 		void receive(String payload) { | ||||||
|  | 			this.fireForgetPayloads.onNext(payload); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("echo") | ||||||
|  | 		String echo(String payload) { | ||||||
|  | 			return payload; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("echo-async") | ||||||
|  | 		Mono<String> echoAsync(String payload) { | ||||||
|  | 			return Mono.delay(Duration.ofMillis(10)).map(aLong -> payload + " async"); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("echo-stream") | ||||||
|  | 		Flux<String> echoStream(String payload) { | ||||||
|  | 			return Flux.interval(Duration.ofMillis(10)).map(aLong -> payload + " " + aLong); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("echo-channel") | ||||||
|  | 		Flux<String> echoChannel(Flux<String> payloads) { | ||||||
|  | 			return payloads.delayElements(Duration.ofMillis(10)).map(payload -> payload + " async"); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("thrown-exception") | ||||||
|  | 		Mono<String> handleAndThrow(String payload) { | ||||||
|  | 			throw new IllegalArgumentException("Invalid input error"); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("error-signal") | ||||||
|  | 		Mono<String> handleAndReturnError(String payload) { | ||||||
|  | 			return Mono.error(new IllegalArgumentException("Invalid input error")); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("void-return-value") | ||||||
|  | 		Mono<Void> voidReturnValue(String payload) { | ||||||
|  | 			return !payload.equals("bad") ? | ||||||
|  | 					Mono.delay(Duration.ofMillis(10)).then(Mono.empty()) : | ||||||
|  | 					Mono.error(new IllegalStateException("bad")); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageExceptionHandler | ||||||
|  | 		Mono<String> handleException(IllegalArgumentException ex) { | ||||||
|  | 			return Mono.delay(Duration.ofMillis(10)).map(aLong -> ex.getMessage() + " handled"); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageExceptionHandler | ||||||
|  | 		Mono<Void> handleExceptionWithVoidReturnValue(IllegalStateException ex) { | ||||||
|  | 			return Mono.delay(Duration.ofMillis(10)).then(Mono.empty()); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Configuration | ||||||
|  | 	static class ServerConfig { | ||||||
|  | 
 | ||||||
|  | 		@Bean | ||||||
|  | 		public ServerController controller() { | ||||||
|  | 			return new ServerController(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Bean | ||||||
|  | 		public MessageHandlerAcceptor messageHandlerAcceptor() { | ||||||
|  | 			MessageHandlerAcceptor acceptor = new MessageHandlerAcceptor(); | ||||||
|  | 			acceptor.setRSocketStrategies(rsocketStrategies()); | ||||||
|  | 			return acceptor; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Bean | ||||||
|  | 		public RSocketStrategies rsocketStrategies() { | ||||||
|  | 			return RSocketStrategies.builder() | ||||||
|  | 					.decoder(StringDecoder.allMimeTypes()) | ||||||
|  | 					.encoder(CharSequenceEncoder.allMimeTypes()) | ||||||
|  | 					.dataBufferFactory(new NettyDataBufferFactory(PooledByteBufAllocator.DEFAULT)) | ||||||
|  | 					.build(); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,285 @@ | ||||||
|  | /* | ||||||
|  |  * 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.rsocket; | ||||||
|  | 
 | ||||||
|  | import java.time.Duration; | ||||||
|  | import java.util.Collections; | ||||||
|  | 
 | ||||||
|  | import io.netty.buffer.PooledByteBufAllocator; | ||||||
|  | import io.rsocket.Closeable; | ||||||
|  | import io.rsocket.Frame; | ||||||
|  | import io.rsocket.RSocket; | ||||||
|  | import io.rsocket.RSocketFactory; | ||||||
|  | import io.rsocket.transport.netty.client.TcpClientTransport; | ||||||
|  | import io.rsocket.transport.netty.server.TcpServerTransport; | ||||||
|  | import io.rsocket.util.DefaultPayload; | ||||||
|  | import org.junit.AfterClass; | ||||||
|  | import org.junit.BeforeClass; | ||||||
|  | import org.junit.Test; | ||||||
|  | import reactor.core.publisher.Flux; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
|  | import reactor.core.publisher.MonoProcessor; | ||||||
|  | import reactor.core.publisher.ReplayProcessor; | ||||||
|  | import reactor.core.scheduler.Schedulers; | ||||||
|  | import reactor.test.StepVerifier; | ||||||
|  | 
 | ||||||
|  | import org.springframework.context.annotation.AnnotationConfigApplicationContext; | ||||||
|  | import org.springframework.context.annotation.Bean; | ||||||
|  | import org.springframework.context.annotation.Configuration; | ||||||
|  | import org.springframework.core.codec.CharSequenceEncoder; | ||||||
|  | import org.springframework.core.codec.StringDecoder; | ||||||
|  | import org.springframework.core.io.buffer.NettyDataBufferFactory; | ||||||
|  | import org.springframework.messaging.handler.annotation.MessageMapping; | ||||||
|  | import org.springframework.stereotype.Controller; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Client-side handling of requests initiated from the server side. | ||||||
|  |  * | ||||||
|  |  *  @author Rossen Stoyanchev | ||||||
|  |  */ | ||||||
|  | public class RSocketServerToClientIntegrationTests { | ||||||
|  | 
 | ||||||
|  | 	private static AnnotationConfigApplicationContext context; | ||||||
|  | 
 | ||||||
|  | 	private static Closeable server; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@BeforeClass | ||||||
|  | 	@SuppressWarnings("ConstantConditions") | ||||||
|  | 	public static void setupOnce() { | ||||||
|  | 
 | ||||||
|  | 		context = new AnnotationConfigApplicationContext(RSocketConfig.class); | ||||||
|  | 
 | ||||||
|  | 		server = RSocketFactory.receive() | ||||||
|  | 				.frameDecoder(Frame::retain)  // as per https://github.com/rsocket/rsocket-java#zero-copy | ||||||
|  | 				.acceptor(context.getBean("serverAcceptor", MessageHandlerAcceptor.class)) | ||||||
|  | 				.transport(TcpServerTransport.create("localhost", 7000)) | ||||||
|  | 				.start() | ||||||
|  | 				.block(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@AfterClass | ||||||
|  | 	public static void tearDownOnce() { | ||||||
|  | 		server.dispose(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void echo() { | ||||||
|  | 		connectAndVerify("connect.echo"); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void echoAsync() { | ||||||
|  | 		connectAndVerify("connect.echo-async"); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void echoStream() { | ||||||
|  | 		connectAndVerify("connect.echo-stream"); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Test | ||||||
|  | 	public void echoChannel() { | ||||||
|  | 		connectAndVerify("connect.echo-channel"); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private static void connectAndVerify(String destination) { | ||||||
|  | 
 | ||||||
|  | 		ServerController serverController = context.getBean(ServerController.class); | ||||||
|  | 		serverController.reset(); | ||||||
|  | 
 | ||||||
|  | 		RSocket rsocket = null; | ||||||
|  | 		try { | ||||||
|  | 			rsocket = RSocketFactory.connect() | ||||||
|  | 					.setupPayload(DefaultPayload.create("", destination)) | ||||||
|  | 					.dataMimeType("text/plain") | ||||||
|  | 					.frameDecoder(Frame::retain)  // as per https://github.com/rsocket/rsocket-java#zero-copy | ||||||
|  | 					.acceptor(context.getBean("clientAcceptor", MessageHandlerAcceptor.class)) | ||||||
|  | 					.transport(TcpClientTransport.create("localhost", 7000)) | ||||||
|  | 					.start() | ||||||
|  | 					.block(); | ||||||
|  | 
 | ||||||
|  | 			serverController.await(Duration.ofSeconds(5)); | ||||||
|  | 		} | ||||||
|  | 		finally { | ||||||
|  | 			if (rsocket != null) { | ||||||
|  | 				rsocket.dispose(); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Controller | ||||||
|  | 	@SuppressWarnings({"unused", "NullableProblems"}) | ||||||
|  | 	static class ServerController { | ||||||
|  | 
 | ||||||
|  | 		// Must be initialized by @Test method... | ||||||
|  | 		volatile MonoProcessor<Void> result; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		public void reset() { | ||||||
|  | 			this.result = MonoProcessor.create(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		public void await(Duration duration) { | ||||||
|  | 			this.result.block(duration); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("connect.echo") | ||||||
|  | 		void echo(RSocketRequester requester) { | ||||||
|  | 			runTest(() -> { | ||||||
|  | 				Flux<String> flux = Flux.range(1, 3).concatMap(i -> | ||||||
|  | 						requester.route("echo").data("Hello " + i).retrieveMono(String.class)); | ||||||
|  | 
 | ||||||
|  | 				StepVerifier.create(flux) | ||||||
|  | 						.expectNext("Hello 1") | ||||||
|  | 						.expectNext("Hello 2") | ||||||
|  | 						.expectNext("Hello 3") | ||||||
|  | 						.verifyComplete(); | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("connect.echo-async") | ||||||
|  | 		void echoAsync(RSocketRequester requester) { | ||||||
|  | 			runTest(() -> { | ||||||
|  | 				Flux<String> flux = Flux.range(1, 3).concatMap(i -> | ||||||
|  | 						requester.route("echo-async").data("Hello " + i).retrieveMono(String.class)); | ||||||
|  | 
 | ||||||
|  | 				StepVerifier.create(flux) | ||||||
|  | 						.expectNext("Hello 1 async") | ||||||
|  | 						.expectNext("Hello 2 async") | ||||||
|  | 						.expectNext("Hello 3 async") | ||||||
|  | 						.verifyComplete(); | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("connect.echo-stream") | ||||||
|  | 		void echoStream(RSocketRequester requester) { | ||||||
|  | 			runTest(() -> { | ||||||
|  | 				Flux<String> flux = requester.route("echo-stream").data("Hello").retrieveFlux(String.class); | ||||||
|  | 
 | ||||||
|  | 				StepVerifier.create(flux) | ||||||
|  | 						.expectNext("Hello 0") | ||||||
|  | 						.expectNextCount(5) | ||||||
|  | 						.expectNext("Hello 6") | ||||||
|  | 						.expectNext("Hello 7") | ||||||
|  | 						.thenCancel() | ||||||
|  | 						.verify(); | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("connect.echo-channel") | ||||||
|  | 		void echoChannel(RSocketRequester requester) { | ||||||
|  | 			runTest(() -> { | ||||||
|  | 				Flux<String> flux = requester.route("echo-channel") | ||||||
|  | 						.data(Flux.range(1, 10).map(i -> "Hello " + i), String.class) | ||||||
|  | 						.retrieveFlux(String.class); | ||||||
|  | 
 | ||||||
|  | 				StepVerifier.create(flux) | ||||||
|  | 						.expectNext("Hello 1 async") | ||||||
|  | 						.expectNextCount(7) | ||||||
|  | 						.expectNext("Hello 9 async") | ||||||
|  | 						.expectNext("Hello 10 async") | ||||||
|  | 						.verifyComplete(); | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		private void runTest(Runnable testEcho) { | ||||||
|  | 			Mono.fromRunnable(testEcho) | ||||||
|  | 					.doOnError(ex -> result.onError(ex)) | ||||||
|  | 					.doOnSuccess(o -> result.onComplete()) | ||||||
|  | 					.subscribeOn(Schedulers.elastic()) // StepVerifier will block | ||||||
|  | 					.subscribe(); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	private static class ClientHandler { | ||||||
|  | 
 | ||||||
|  | 		final ReplayProcessor<String> fireForgetPayloads = ReplayProcessor.create(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("receive") | ||||||
|  | 		void receive(String payload) { | ||||||
|  | 			this.fireForgetPayloads.onNext(payload); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("echo") | ||||||
|  | 		String echo(String payload) { | ||||||
|  | 			return payload; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("echo-async") | ||||||
|  | 		Mono<String> echoAsync(String payload) { | ||||||
|  | 			return Mono.delay(Duration.ofMillis(10)).map(aLong -> payload + " async"); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("echo-stream") | ||||||
|  | 		Flux<String> echoStream(String payload) { | ||||||
|  | 			return Flux.interval(Duration.ofMillis(10)).map(aLong -> payload + " " + aLong); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@MessageMapping("echo-channel") | ||||||
|  | 		Flux<String> echoChannel(Flux<String> payloads) { | ||||||
|  | 			return payloads.delayElements(Duration.ofMillis(10)).map(payload -> payload + " async"); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@Configuration | ||||||
|  | 	static class RSocketConfig { | ||||||
|  | 
 | ||||||
|  | 		@Bean | ||||||
|  | 		public ClientHandler clientHandler() { | ||||||
|  | 			return new ClientHandler(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Bean | ||||||
|  | 		public ServerController serverController() { | ||||||
|  | 			return new ServerController(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Bean | ||||||
|  | 		public MessageHandlerAcceptor clientAcceptor() { | ||||||
|  | 			MessageHandlerAcceptor acceptor = new MessageHandlerAcceptor(); | ||||||
|  | 			acceptor.setHandlers(Collections.singletonList(clientHandler())); | ||||||
|  | 			acceptor.setAutoDetectDisabled(); | ||||||
|  | 			acceptor.setRSocketStrategies(rsocketStrategies()); | ||||||
|  | 			return acceptor; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Bean | ||||||
|  | 		public MessageHandlerAcceptor serverAcceptor() { | ||||||
|  | 			MessageHandlerAcceptor handler = new MessageHandlerAcceptor(); | ||||||
|  | 			handler.setRSocketStrategies(rsocketStrategies()); | ||||||
|  | 			return handler; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		@Bean | ||||||
|  | 		public RSocketStrategies rsocketStrategies() { | ||||||
|  | 			return RSocketStrategies.builder() | ||||||
|  | 					.decoder(StringDecoder.allMimeTypes()) | ||||||
|  | 					.encoder(CharSequenceEncoder.allMimeTypes()) | ||||||
|  | 					.dataBufferFactory(new NettyDataBufferFactory(PooledByteBufAllocator.DEFAULT)) | ||||||
|  | 					.build(); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -57,7 +57,9 @@ import org.springframework.util.ReflectionUtils; | ||||||
| import static java.util.stream.Collectors.*; | import static java.util.stream.Collectors.*; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Convenience class to resolve method parameters from hints. |  * Convenience class to resolve to a Method and method parameters. | ||||||
|  |  * | ||||||
|  |  * <p>Note that a replica of this class also exists in spring-messaging. | ||||||
|  * |  * | ||||||
|  * <h1>Background</h1> |  * <h1>Background</h1> | ||||||
|  * |  * | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue