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"); | ||||
|  * 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 { | ||||
| 		Instant start = Instant.now(); | ||||
| 		while (Instant.now().isBefore(start.plus(duration))) { | ||||
| 		while (true) { | ||||
| 			try { | ||||
| 				verifyAllocations(); | ||||
| 				break; | ||||
| 			} | ||||
| 			catch (AssertionError ex) { | ||||
| 				// ignore; | ||||
| 				if (Instant.now().isAfter(start.plus(duration))) { | ||||
| 					throw ex; | ||||
| 				} | ||||
| 			} | ||||
| 			Thread.sleep(50); | ||||
| 		} | ||||
|  |  | |||
|  | @ -7,12 +7,15 @@ dependencyManagement { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| def rsocketVersion = "0.11.17" | ||||
| 
 | ||||
| dependencies { | ||||
| 	compile(project(":spring-beans")) | ||||
| 	compile(project(":spring-core")) | ||||
| 	optional(project(":spring-context")) | ||||
| 	optional(project(":spring-oxm")) | ||||
| 	optional("io.projectreactor.netty:reactor-netty") | ||||
| 	optional("io.rsocket:rsocket-core:${rsocketVersion}") | ||||
| 	optional("com.fasterxml.jackson.core:jackson-databind:${jackson2Version}") | ||||
| 	optional("javax.xml.bind:jaxb-api:2.3.1") | ||||
| 	testCompile("javax.inject:javax.inject-tck:1") | ||||
|  | @ -24,6 +27,9 @@ dependencies { | |||
| 		exclude group: "org.springframework", module: "spring-context" | ||||
| 	} | ||||
| 	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-stdlib:${kotlinVersion}") | ||||
| 	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() { | ||||
| 		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"); | ||||
|  * 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.convert.ConversionService; | ||||
| import org.springframework.core.convert.TypeDescriptor; | ||||
| import org.springframework.core.convert.support.DefaultConversionService; | ||||
| import org.springframework.lang.Nullable; | ||||
| import org.springframework.messaging.Message; | ||||
| import org.springframework.messaging.handler.annotation.ValueConstants; | ||||
|  | @ -34,24 +33,20 @@ import org.springframework.messaging.handler.invocation.HandlerMethodArgumentRes | |||
| import org.springframework.util.ClassUtils; | ||||
| 
 | ||||
| /** | ||||
|  * Abstract base class for resolving method arguments from a named value. Message headers, | ||||
|  * and path variables are examples of named values. Each may have a name, a required flag, | ||||
|  * and a default value. | ||||
|  * 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 define how to do the following: | ||||
|  * <ul> | ||||
|  * <li>Obtain named value information for a method parameter | ||||
|  * <li>Resolve names into argument values | ||||
|  * <li>Handle missing argument values when argument values are required | ||||
|  * <li>Optionally handle a resolved value | ||||
|  * </ul> | ||||
|  * <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. For this to work a {@link ConfigurableBeanFactory} | ||||
|  * must be supplied to the class constructor. | ||||
|  *  <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} may be used to apply type conversion to the resolved | ||||
|  * argument value if it doesn't match the method parameter type. | ||||
|  * <p>A {@link ConversionService} is used to to convert resolved String argument | ||||
|  * value to the expected target method parameter type. | ||||
|  * | ||||
|  * @author Rossen Stoyanchev | ||||
|  * @author Juergen Hoeller | ||||
|  | @ -61,8 +56,10 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle | |||
| 
 | ||||
| 	private final ConversionService conversionService; | ||||
| 
 | ||||
| 	@Nullable | ||||
| 	private final ConfigurableBeanFactory configurableBeanFactory; | ||||
| 
 | ||||
| 	@Nullable | ||||
| 	private final BeanExpressionContext expressionContext; | ||||
| 
 | ||||
| 	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}. | ||||
| 	 * @param cs conversion service for converting values to match the | ||||
| 	 * target method parameter type | ||||
| 	 * @param beanFactory a bean factory to use for resolving {@code ${...}} placeholder | ||||
| 	 * and {@code #{...}} SpEL expressions in default values, or {@code null} if default | ||||
| 	 * values are not expected to contain expressions | ||||
| 	 * @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 cs, | ||||
| 	protected AbstractNamedValueMethodArgumentResolver(ConversionService conversionService, | ||||
| 			@Nullable ConfigurableBeanFactory beanFactory) { | ||||
| 
 | ||||
| 		this.conversionService = (cs != null ? cs : DefaultConversionService.getSharedInstance()); | ||||
| 		this.conversionService = conversionService; | ||||
| 		this.configurableBeanFactory = beanFactory; | ||||
| 		this.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, null) : null); | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	@Override | ||||
| 	@Nullable | ||||
| 	public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception { | ||||
| 
 | ||||
| 		NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); | ||||
| 		MethodParameter nestedParameter = parameter.nestedIfOptional(); | ||||
| 
 | ||||
| 		Object resolvedName = resolveStringValue(namedValueInfo.name); | ||||
| 		Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name); | ||||
| 		if (resolvedName == null) { | ||||
| 			throw new IllegalArgumentException( | ||||
| 					"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()); | ||||
| 		if (arg == null) { | ||||
| 			if (namedValueInfo.defaultValue != null) { | ||||
| 				arg = resolveStringValue(namedValueInfo.defaultValue); | ||||
| 				arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); | ||||
| 			} | ||||
| 			else if (namedValueInfo.required && !nestedParameter.isOptional()) { | ||||
| 				handleMissingValue(namedValueInfo.name, nestedParameter, message); | ||||
|  | @ -108,7 +104,7 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle | |||
| 			arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); | ||||
| 		} | ||||
| 		else if ("".equals(arg) && namedValueInfo.defaultValue != null) { | ||||
| 			arg = resolveStringValue(namedValueInfo.defaultValue); | ||||
| 			arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); | ||||
| 		} | ||||
| 
 | ||||
| 		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 | ||||
| 	 * retrieve the method annotation by means of {@link MethodParameter#getParameterAnnotation(Class)}. | ||||
| 	 * 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); | ||||
| 
 | ||||
| 	/** | ||||
| 	 * 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) { | ||||
| 		String name = info.name; | ||||
| 		if (info.name.isEmpty()) { | ||||
| 			name = parameter.getParameterName(); | ||||
| 			if (name == null) { | ||||
| 				throw new IllegalArgumentException("Name for argument type [" + parameter.getParameterType().getName() + | ||||
| 						"] not available, and parameter name information not found in class file either."); | ||||
| 				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."); | ||||
| 			} | ||||
| 		} | ||||
| 		String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue); | ||||
| 		return new NamedValueInfo(name, info.required, defaultValue); | ||||
| 		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. | ||||
| 	 */ | ||||
| 	private Object resolveStringValue(String value) { | ||||
| 		if (this.configurableBeanFactory == null) { | ||||
| 	@Nullable | ||||
| 	private Object resolveEmbeddedValuesAndExpressions(String value) { | ||||
| 		if (this.configurableBeanFactory == null || this.expressionContext == null) { | ||||
| 			return value; | ||||
| 		} | ||||
| 		String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value); | ||||
|  | @ -186,19 +187,21 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle | |||
| 			throws Exception; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Invoked when a named value is required, but | ||||
| 	 * {@link #resolveArgumentInternal(MethodParameter, Message, String)} returned {@code null} and | ||||
| 	 * there is no default value. Subclasses typically throw an exception in this case. | ||||
| 	 * 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 method parameter | ||||
| 	 * @param parameter the target method parameter | ||||
| 	 * @param message the message being processed | ||||
| 	 */ | ||||
| 	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 | ||||
| 	 * exception for other primitives. | ||||
| 	 * 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)) { | ||||
|  | @ -221,13 +224,13 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle | |||
| 	 * @param parameter the argument parameter type | ||||
| 	 * @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 | ||||
| 	 * required and a default value. | ||||
| 	 * Represents a named value declaration. | ||||
| 	 */ | ||||
| 	protected static class NamedValueInfo { | ||||
| 
 | ||||
|  | @ -235,9 +238,10 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle | |||
| 
 | ||||
| 		private final boolean required; | ||||
| 
 | ||||
| 		@Nullable | ||||
| 		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.required = required; | ||||
| 			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"); | ||||
|  * 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.core.convert.ConversionService; | ||||
| import org.springframework.format.support.DefaultFormattingConversionService; | ||||
| import org.springframework.lang.Nullable; | ||||
| import org.springframework.messaging.converter.GenericMessageConverter; | ||||
| import org.springframework.messaging.converter.MessageConverter; | ||||
| import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; | ||||
| import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolverComposite; | ||||
| import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; | ||||
| import org.springframework.util.Assert; | ||||
| import org.springframework.validation.Validator; | ||||
| 
 | ||||
| /** | ||||
|  | @ -60,15 +62,19 @@ public class DefaultMessageHandlerMethodFactory | |||
| 
 | ||||
| 	private ConversionService conversionService = new DefaultFormattingConversionService(); | ||||
| 
 | ||||
| 	@Nullable | ||||
| 	private MessageConverter messageConverter; | ||||
| 
 | ||||
| 	@Nullable | ||||
| 	private Validator validator; | ||||
| 
 | ||||
| 	@Nullable | ||||
| 	private List<HandlerMethodArgumentResolver> customArgumentResolvers; | ||||
| 
 | ||||
| 	private final HandlerMethodArgumentResolverComposite argumentResolvers = | ||||
| 			new HandlerMethodArgumentResolverComposite(); | ||||
| 
 | ||||
| 	@Nullable | ||||
| 	private BeanFactory beanFactory; | ||||
| 
 | ||||
| 
 | ||||
|  | @ -114,6 +120,7 @@ public class DefaultMessageHandlerMethodFactory | |||
| 	 * 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)}. | ||||
| 	 */ | ||||
| 	@SuppressWarnings("ConstantConditions") | ||||
| 	public void setArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { | ||||
| 		if (argumentResolvers == null) { | ||||
| 			this.argumentResolvers.clear(); | ||||
|  | @ -151,11 +158,11 @@ public class DefaultMessageHandlerMethodFactory | |||
| 
 | ||||
| 	protected List<HandlerMethodArgumentResolver> initArgumentResolvers() { | ||||
| 		List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(); | ||||
| 		ConfigurableBeanFactory cbf = (this.beanFactory instanceof ConfigurableBeanFactory ? | ||||
| 		ConfigurableBeanFactory beanFactory = (this.beanFactory instanceof ConfigurableBeanFactory ? | ||||
| 				(ConfigurableBeanFactory) this.beanFactory : null); | ||||
| 
 | ||||
| 		// Annotation-based argument resolution | ||||
| 		resolvers.add(new HeaderMethodArgumentResolver(this.conversionService, cbf)); | ||||
| 		resolvers.add(new HeaderMethodArgumentResolver(this.conversionService, beanFactory)); | ||||
| 		resolvers.add(new HeadersMethodArgumentResolver()); | ||||
| 
 | ||||
| 		// Type-based argument resolution | ||||
|  | @ -164,6 +171,8 @@ public class DefaultMessageHandlerMethodFactory | |||
| 		if (this.customArgumentResolvers != null) { | ||||
| 			resolvers.addAll(this.customArgumentResolvers); | ||||
| 		} | ||||
| 
 | ||||
| 		Assert.notNull(this.messageConverter, "MessageConverter not configured"); | ||||
| 		resolvers.add(new PayloadArgumentResolver(this.messageConverter, this.validator)); | ||||
| 
 | ||||
| 		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"); | ||||
|  * 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.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; | ||||
| 
 | ||||
| /** | ||||
|  * Resolves method parameters annotated with | ||||
|  * {@link org.springframework.messaging.handler.annotation.DestinationVariable @DestinationVariable}. | ||||
|  * Resolve for {@link DestinationVariable @DestinationVariable} method parameters. | ||||
|  * | ||||
|  * @author Brian Clozel | ||||
|  * @since 4.0 | ||||
|  */ | ||||
| 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 = | ||||
| 			DestinationVariableMethodArgumentResolver.class.getSimpleName() + ".templateVariables"; | ||||
| 
 | ||||
| 
 | ||||
| 	public DestinationVariableMethodArgumentResolver(ConversionService cs) { | ||||
| 		super(cs, null); | ||||
| 	public DestinationVariableMethodArgumentResolver(ConversionService conversionService) { | ||||
| 		super(conversionService, null); | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
|  | @ -55,26 +53,24 @@ public class DestinationVariableMethodArgumentResolver extends AbstractNamedValu | |||
| 
 | ||||
| 	@Override | ||||
| 	protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { | ||||
| 		DestinationVariable annotation = parameter.getParameterAnnotation(DestinationVariable.class); | ||||
| 		Assert.state(annotation != null, "No DestinationVariable annotation"); | ||||
| 		return new DestinationVariableNamedValueInfo(annotation); | ||||
| 		DestinationVariable annot = parameter.getParameterAnnotation(DestinationVariable.class); | ||||
| 		Assert.state(annot != null, "No DestinationVariable annotation"); | ||||
| 		return new DestinationVariableNamedValueInfo(annot); | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	@Nullable | ||||
| 	protected Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name) | ||||
| 			throws Exception { | ||||
| 
 | ||||
| 		@SuppressWarnings("unchecked") | ||||
| 		Map<String, String> vars = | ||||
| 				(Map<String, String>) message.getHeaders().get(DESTINATION_TEMPLATE_VARIABLES_HEADER); | ||||
| 		return (vars != null ? vars.get(name) : null); | ||||
| 	@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() + "]"); | ||||
| 		throw new MessageHandlingException(message, "Missing path template variable '" + name + "' " + | ||||
| 				"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"); | ||||
|  * 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; | ||||
| 
 | ||||
| /** | ||||
|  * 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 | ||||
|  * @since 4.0 | ||||
|  * | ||||
|  * @see HeadersMethodArgumentResolver | ||||
|  * @see NativeMessageHeaderAccessor | ||||
|  */ | ||||
| public class HeaderMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { | ||||
| 
 | ||||
| 	private static final Log logger = LogFactory.getLog(HeaderMethodArgumentResolver.class); | ||||
| 
 | ||||
| 
 | ||||
| 	public HeaderMethodArgumentResolver(ConversionService cs, ConfigurableBeanFactory beanFactory) { | ||||
| 		super(cs, beanFactory); | ||||
| 	public HeaderMethodArgumentResolver( | ||||
| 			ConversionService conversionService, @Nullable ConfigurableBeanFactory beanFactory) { | ||||
| 
 | ||||
| 		super(conversionService, beanFactory); | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
|  | @ -55,9 +62,9 @@ public class HeaderMethodArgumentResolver extends AbstractNamedValueMethodArgume | |||
| 
 | ||||
| 	@Override | ||||
| 	protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { | ||||
| 		Header annotation = parameter.getParameterAnnotation(Header.class); | ||||
| 		Assert.state(annotation != null, "No Header annotation"); | ||||
| 		return new HeaderNamedValueInfo(annotation); | ||||
| 		Header annot = parameter.getParameterAnnotation(Header.class); | ||||
| 		Assert.state(annot != null, "No Header annotation"); | ||||
| 		return new HeaderNamedValueInfo(annot); | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
|  | @ -70,10 +77,9 @@ public class HeaderMethodArgumentResolver extends AbstractNamedValueMethodArgume | |||
| 
 | ||||
| 		if (headerValue != null && nativeHeaderValue != null) { | ||||
| 			if (logger.isDebugEnabled()) { | ||||
| 				logger.debug("Message headers contain two values for the same header '" + name + "', " + | ||||
| 						"one in the top level header map and a second in the nested map with native headers. " + | ||||
| 						"Using the value from top level map. " + | ||||
| 						"Use 'nativeHeader.myHeader' to resolve to the value from the nested native header map."); | ||||
| 				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."); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|  | @ -94,9 +100,9 @@ public class HeaderMethodArgumentResolver extends AbstractNamedValueMethodArgume | |||
| 	} | ||||
| 
 | ||||
| 	@SuppressWarnings("unchecked") | ||||
| 	@Nullable | ||||
| 	private Map<String, List<String>> getNativeHeaders(Message<?> message) { | ||||
| 		return (Map<String, List<String>>) message.getHeaders().get( | ||||
| 				NativeMessageHeaderAccessor.NATIVE_HEADERS); | ||||
| 		return (Map<String, List<String>>) message.getHeaders().get(NativeMessageHeaderAccessor.NATIVE_HEADERS); | ||||
| 	} | ||||
| 
 | ||||
| 	@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"); | ||||
|  * 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; | ||||
| 
 | ||||
| /** | ||||
|  * {@link HandlerMethodArgumentResolver} for header method parameters. Resolves the | ||||
|  * following method parameters: | ||||
|  * Argument resolver for headers. Resolves the following method parameters: | ||||
|  * <ul> | ||||
|  * <li>Parameters assignable to {@link Map} annotated with {@link Headers @Headers} | ||||
|  * <li>Parameters of type {@link MessageHeaders} | ||||
|  * <li>Parameters assignable to {@link MessageHeaderAccessor} | ||||
|  * <li>{@link Headers @Headers} {@link Map} | ||||
|  * <li>{@link MessageHeaders} | ||||
|  * <li>{@link MessageHeaderAccessor} | ||||
|  * </ul> | ||||
|  * | ||||
|  * @author Rossen Stoyanchev | ||||
|  | @ -58,7 +57,7 @@ public class HeadersMethodArgumentResolver implements HandlerMethodArgumentResol | |||
| 		} | ||||
| 		else if (MessageHeaderAccessor.class == paramType) { | ||||
| 			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)) { | ||||
| 			MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class); | ||||
|  | @ -75,9 +74,8 @@ public class HeadersMethodArgumentResolver implements HandlerMethodArgumentResol | |||
| 			} | ||||
| 		} | ||||
| 		else { | ||||
| 			throw new IllegalStateException( | ||||
| 					"Unexpected method parameter type " + paramType + "in method " + parameter.getMethod() + ". " | ||||
| 					+ "@Headers method arguments must be assignable to java.util.Map."); | ||||
| 			throw new IllegalStateException("Unexpected parameter of type " + paramType + | ||||
| 					" in method " + parameter.getMethod() + ". "); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -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"); | ||||
|  * 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 { | ||||
| 
 | ||||
| 	@Nullable | ||||
| 	private final MessageConverter converter; | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -55,6 +55,7 @@ public class PayloadArgumentResolver implements HandlerMethodArgumentResolver { | |||
| 
 | ||||
| 	private final MessageConverter converter; | ||||
| 
 | ||||
| 	@Nullable | ||||
| 	private final Validator validator; | ||||
| 
 | ||||
| 	private final boolean useDefaultResolution; | ||||
|  | @ -76,7 +77,7 @@ public class PayloadArgumentResolver implements HandlerMethodArgumentResolver { | |||
| 	 * @param messageConverter the MessageConverter to use (required) | ||||
| 	 * @param validator the Validator to use (optional) | ||||
| 	 */ | ||||
| 	public PayloadArgumentResolver(MessageConverter messageConverter, Validator validator) { | ||||
| 	public PayloadArgumentResolver(MessageConverter messageConverter, @Nullable Validator validator) { | ||||
| 		this(messageConverter, validator, true); | ||||
| 	} | ||||
| 
 | ||||
|  | @ -89,7 +90,7 @@ public class PayloadArgumentResolver implements HandlerMethodArgumentResolver { | |||
| 	 * all parameters; if "false" then only arguments with the {@code @Payload} | ||||
| 	 * annotation are supported. | ||||
| 	 */ | ||||
| 	public PayloadArgumentResolver(MessageConverter messageConverter, Validator validator, | ||||
| 	public PayloadArgumentResolver(MessageConverter messageConverter, @Nullable Validator validator, | ||||
| 			boolean useDefaultResolution) { | ||||
| 
 | ||||
| 		Assert.notNull(messageConverter, "MessageConverter must not be null"); | ||||
|  |  | |||
|  | @ -1,4 +1,9 @@ | |||
| /** | ||||
|  * Support classes for working with annotated message-handling methods. | ||||
|  */ | ||||
| @NonNullApi | ||||
| @NonNullFields | ||||
| 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"); | ||||
|  * 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 | ||||
| 	 */ | ||||
| 	@Nullable | ||||
| 	public Method resolveMethod(Exception exception) { | ||||
| 	public Method resolveMethod(Throwable exception) { | ||||
| 		Method method = resolveMethodByExceptionType(exception.getClass()); | ||||
| 		if (method == null) { | ||||
| 			Throwable cause = exception.getCause(); | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ | |||
| package org.springframework.messaging.handler.invocation; | ||||
| 
 | ||||
| import org.springframework.core.MethodParameter; | ||||
| import org.springframework.lang.Nullable; | ||||
| import org.springframework.messaging.Message; | ||||
| import org.springframework.messaging.MessagingException; | ||||
| 
 | ||||
|  | @ -51,6 +52,17 @@ public class MethodArgumentResolutionException extends MessagingException { | |||
| 		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. | ||||
|  |  | |||
|  | @ -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"); | ||||
|  * 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.messaging.Message; | ||||
| import org.springframework.messaging.handler.CompositeMessageCondition; | ||||
| import org.springframework.messaging.handler.DestinationPatternsMessageCondition; | ||||
| import org.springframework.messaging.handler.MessageCondition; | ||||
| 
 | ||||
|  | @ -34,62 +35,44 @@ import org.springframework.messaging.handler.MessageCondition; | |||
|  */ | ||||
| public class SimpMessageMappingInfo implements MessageCondition<SimpMessageMappingInfo> { | ||||
| 
 | ||||
| 	private final SimpMessageTypeMessageCondition messageTypeMessageCondition; | ||||
| 
 | ||||
| 	private final DestinationPatternsMessageCondition destinationConditions; | ||||
| 	private final CompositeMessageCondition delegate; | ||||
| 
 | ||||
| 
 | ||||
| 	public SimpMessageMappingInfo(SimpMessageTypeMessageCondition messageTypeMessageCondition, | ||||
| 			DestinationPatternsMessageCondition destinationConditions) { | ||||
| 
 | ||||
| 		this.messageTypeMessageCondition = messageTypeMessageCondition; | ||||
| 		this.destinationConditions = destinationConditions; | ||||
| 		this.delegate = new CompositeMessageCondition(messageTypeMessageCondition, destinationConditions); | ||||
| 	} | ||||
| 
 | ||||
| 	private SimpMessageMappingInfo(CompositeMessageCondition delegate) { | ||||
| 		this.delegate = delegate; | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	public SimpMessageTypeMessageCondition getMessageTypeMessageCondition() { | ||||
| 		return this.messageTypeMessageCondition; | ||||
| 		return this.delegate.getCondition(SimpMessageTypeMessageCondition.class); | ||||
| 	} | ||||
| 
 | ||||
| 	public DestinationPatternsMessageCondition getDestinationConditions() { | ||||
| 		return this.destinationConditions; | ||||
| 		return this.delegate.getCondition(DestinationPatternsMessageCondition.class); | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	@Override | ||||
| 	public SimpMessageMappingInfo combine(SimpMessageMappingInfo other) { | ||||
| 		SimpMessageTypeMessageCondition typeCond = | ||||
| 				this.getMessageTypeMessageCondition().combine(other.getMessageTypeMessageCondition()); | ||||
| 		DestinationPatternsMessageCondition destCond = | ||||
| 				this.destinationConditions.combine(other.getDestinationConditions()); | ||||
| 		return new SimpMessageMappingInfo(typeCond, destCond); | ||||
| 		return new SimpMessageMappingInfo(this.delegate.combine(other.delegate)); | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	@Nullable | ||||
| 	public SimpMessageMappingInfo getMatchingCondition(Message<?> message) { | ||||
| 		SimpMessageTypeMessageCondition typeCond = this.messageTypeMessageCondition.getMatchingCondition(message); | ||||
| 		if (typeCond == null) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		DestinationPatternsMessageCondition destCond = this.destinationConditions.getMatchingCondition(message); | ||||
| 		if (destCond == null) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		return new SimpMessageMappingInfo(typeCond, destCond); | ||||
| 		CompositeMessageCondition condition = this.delegate.getMatchingCondition(message); | ||||
| 		return condition != null ? new SimpMessageMappingInfo(condition) : null; | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public int compareTo(SimpMessageMappingInfo other, Message<?> message) { | ||||
| 		int result = this.messageTypeMessageCondition.compareTo(other.messageTypeMessageCondition, message); | ||||
| 		if (result != 0) { | ||||
| 			return result; | ||||
| 		} | ||||
| 		result = this.destinationConditions.compareTo(other.destinationConditions, message); | ||||
| 		if (result != 0) { | ||||
| 			return result; | ||||
| 		} | ||||
| 		return 0; | ||||
| 		return this.delegate.compareTo(other.delegate, message); | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
|  | @ -101,19 +84,17 @@ public class SimpMessageMappingInfo implements MessageCondition<SimpMessageMappi | |||
| 		if (!(other instanceof SimpMessageMappingInfo)) { | ||||
| 			return false; | ||||
| 		} | ||||
| 		SimpMessageMappingInfo otherInfo = (SimpMessageMappingInfo) other; | ||||
| 		return (this.destinationConditions.equals(otherInfo.destinationConditions) && | ||||
| 				this.messageTypeMessageCondition.equals(otherInfo.messageTypeMessageCondition)); | ||||
| 		return this.delegate.equals(((SimpMessageMappingInfo) other).delegate); | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public int hashCode() { | ||||
| 		return (this.destinationConditions.hashCode() * 31 + this.messageTypeMessageCondition.hashCode()); | ||||
| 		return this.delegate.hashCode(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	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"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  | @ -16,23 +16,21 @@ | |||
| 
 | ||||
| package org.springframework.messaging.handler.annotation.support; | ||||
| 
 | ||||
| import java.lang.reflect.Method; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| import org.junit.Before; | ||||
| import org.junit.Test; | ||||
| 
 | ||||
| import org.springframework.core.DefaultParameterNameDiscoverer; | ||||
| import org.springframework.core.GenericTypeResolver; | ||||
| 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. | ||||
|  | @ -41,33 +39,17 @@ import static org.junit.Assert.*; | |||
|  */ | ||||
| public class DestinationVariableMethodArgumentResolverTests { | ||||
| 
 | ||||
| 	private DestinationVariableMethodArgumentResolver resolver; | ||||
| 	private final DestinationVariableMethodArgumentResolver resolver = | ||||
| 			new DestinationVariableMethodArgumentResolver(new DefaultConversionService()); | ||||
| 
 | ||||
| 	private MethodParameter paramAnnotated; | ||||
| 	private MethodParameter paramAnnotatedValue; | ||||
| 	private MethodParameter paramNotAnnotated; | ||||
| 	private final ResolvableMethod resolvable = | ||||
| 			ResolvableMethod.on(getClass()).named("handleMessage").build(); | ||||
| 
 | ||||
| 
 | ||||
| 	@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 | ||||
| 	public void supportsParameter() { | ||||
| 		assertTrue(resolver.supportsParameter(paramAnnotated)); | ||||
| 		assertTrue(resolver.supportsParameter(paramAnnotatedValue)); | ||||
| 		assertFalse(resolver.supportsParameter(paramNotAnnotated)); | ||||
| 		assertTrue(resolver.supportsParameter(this.resolvable.annot(destinationVar().noValue()).arg())); | ||||
| 		assertFalse(resolver.supportsParameter(this.resolvable.annotNotPresent(DestinationVariable.class).arg())); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
|  | @ -80,17 +62,19 @@ public class DestinationVariableMethodArgumentResolverTests { | |||
| 		Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeader( | ||||
| 			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); | ||||
| 
 | ||||
| 		result = this.resolver.resolveArgument(this.paramAnnotatedValue, message); | ||||
| 		param = this.resolvable.annot(destinationVar("name")).arg(); | ||||
| 		result = this.resolver.resolveArgument(param, message); | ||||
| 		assertEquals("value", result); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test(expected = MessageHandlingException.class) | ||||
| 	public void resolveArgumentNotFound() throws Exception { | ||||
| 		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") | ||||
|  |  | |||
|  | @ -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"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  | @ -16,7 +16,6 @@ | |||
| 
 | ||||
| package org.springframework.messaging.handler.annotation.support; | ||||
| 
 | ||||
| import java.lang.reflect.Method; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Optional; | ||||
|  | @ -25,19 +24,17 @@ import org.junit.Before; | |||
| import org.junit.Test; | ||||
| 
 | ||||
| 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.annotation.SynthesizingMethodParameter; | ||||
| 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 org.springframework.util.ReflectionUtils; | ||||
| 
 | ||||
| import static org.junit.Assert.*; | ||||
| import static org.springframework.messaging.handler.annotation.MessagingPredicates.*; | ||||
| 
 | ||||
| /** | ||||
|  * Test fixture for {@link HeaderMethodArgumentResolver} tests. | ||||
|  | @ -50,46 +47,27 @@ public class HeaderMethodArgumentResolverTests { | |||
| 
 | ||||
| 	private HeaderMethodArgumentResolver resolver; | ||||
| 
 | ||||
| 	private MethodParameter paramRequired; | ||||
| 	private MethodParameter paramNamedDefaultValueStringHeader; | ||||
| 	private MethodParameter paramSystemPropertyDefaultValue; | ||||
| 	private MethodParameter paramSystemPropertyName; | ||||
| 	private MethodParameter paramNotAnnotated; | ||||
| 	private MethodParameter paramOptional; | ||||
| 	private MethodParameter paramNativeHeader; | ||||
| 	private final ResolvableMethod resolvable = ResolvableMethod.on(getClass()).named("handleMessage").build(); | ||||
| 
 | ||||
| 
 | ||||
| 	@Before | ||||
| 	public void setup() { | ||||
| 		@SuppressWarnings("resource") | ||||
| 		GenericApplicationContext cxt = new GenericApplicationContext(); | ||||
| 		cxt.refresh(); | ||||
| 		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); | ||||
| 		GenericApplicationContext context = new GenericApplicationContext(); | ||||
| 		context.refresh(); | ||||
| 		this.resolver = new HeaderMethodArgumentResolver(new DefaultConversionService(), context.getBeanFactory()); | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void supportsParameter() { | ||||
| 		assertTrue(resolver.supportsParameter(paramNamedDefaultValueStringHeader)); | ||||
| 		assertFalse(resolver.supportsParameter(paramNotAnnotated)); | ||||
| 		assertTrue(this.resolver.supportsParameter(this.resolvable.annot(headerPlain()).arg())); | ||||
| 		assertFalse(this.resolver.supportsParameter(this.resolvable.annotNotPresent(Header.class).arg())); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void resolveArgument() throws Exception { | ||||
| 		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); | ||||
| 	} | ||||
| 
 | ||||
|  | @ -98,7 +76,7 @@ public class HeaderMethodArgumentResolverTests { | |||
| 		TestMessageHeaderAccessor headers = new TestMessageHeaderAccessor(); | ||||
| 		headers.setNativeHeader("param1", "foo"); | ||||
| 		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 | ||||
|  | @ -108,20 +86,23 @@ public class HeaderMethodArgumentResolverTests { | |||
| 		headers.setNativeHeader("param1", "native-foo"); | ||||
| 		Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build(); | ||||
| 
 | ||||
| 		assertEquals("foo", this.resolver.resolveArgument(this.paramRequired, message)); | ||||
| 		assertEquals("native-foo", this.resolver.resolveArgument(this.paramNativeHeader, message)); | ||||
| 		assertEquals("foo", this.resolver.resolveArgument( | ||||
| 				this.resolvable.annot(headerPlain()).arg(), message)); | ||||
| 
 | ||||
| 		assertEquals("native-foo", this.resolver.resolveArgument( | ||||
| 				this.resolvable.annot(header("nativeHeaders.param1")).arg(), message)); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test(expected = MessageHandlingException.class) | ||||
| 	public void resolveArgumentNotFound() throws Exception { | ||||
| 		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 | ||||
| 	public void resolveArgumentDefaultValue() throws Exception { | ||||
| 		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); | ||||
| 	} | ||||
| 
 | ||||
|  | @ -130,7 +111,8 @@ public class HeaderMethodArgumentResolverTests { | |||
| 		System.setProperty("systemProperty", "sysbar"); | ||||
| 		try { | ||||
| 			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); | ||||
| 		} | ||||
| 		finally { | ||||
|  | @ -143,7 +125,8 @@ public class HeaderMethodArgumentResolverTests { | |||
| 		System.setProperty("systemProperty", "sysbar"); | ||||
| 		try { | ||||
| 			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); | ||||
| 		} | ||||
| 		finally { | ||||
|  | @ -153,31 +136,22 @@ public class HeaderMethodArgumentResolverTests { | |||
| 
 | ||||
| 	@Test | ||||
| 	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(); | ||||
| 		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); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	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(); | ||||
| 		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); | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	@SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) | ||||
| 	public void handleMessage( | ||||
| 			@Header String param1, | ||||
| 			@Header(name = "name", defaultValue = "bar") String param2, | ||||
|  | @ -191,7 +165,7 @@ public class HeaderMethodArgumentResolverTests { | |||
| 
 | ||||
| 	public static class TestMessageHeaderAccessor extends NativeMessageHeaderAccessor { | ||||
| 
 | ||||
| 		protected TestMessageHeaderAccessor() { | ||||
| 		TestMessageHeaderAccessor() { | ||||
| 			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"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  | @ -16,17 +16,16 @@ | |||
| 
 | ||||
| package org.springframework.messaging.handler.annotation.support; | ||||
| 
 | ||||
| import java.lang.reflect.Method; | ||||
| import java.util.HashMap; | ||||
| import java.util.Collections; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| import org.junit.Before; | ||||
| 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; | ||||
|  | @ -41,47 +40,31 @@ import static org.junit.Assert.*; | |||
|  */ | ||||
| public class HeadersMethodArgumentResolverTests { | ||||
| 
 | ||||
| 	private HeadersMethodArgumentResolver resolver; | ||||
| 	private final HeadersMethodArgumentResolver resolver = new HeadersMethodArgumentResolver(); | ||||
| 
 | ||||
| 	private MethodParameter paramAnnotated; | ||||
| 	private MethodParameter paramAnnotatedNotMap; | ||||
| 	private MethodParameter paramMessageHeaders; | ||||
| 	private MethodParameter paramMessageHeaderAccessor; | ||||
| 	private MethodParameter paramMessageHeaderAccessorSubclass; | ||||
| 	private Message<byte[]> message = | ||||
| 			MessageBuilder.withPayload(new byte[0]).copyHeaders(Collections.singletonMap("foo", "bar")).build(); | ||||
| 
 | ||||
| 	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 | ||||
| 	public void supportsParameter() { | ||||
| 		assertTrue(this.resolver.supportsParameter(this.paramAnnotated)); | ||||
| 		assertFalse(this.resolver.supportsParameter(this.paramAnnotatedNotMap)); | ||||
| 		assertTrue(this.resolver.supportsParameter(this.paramMessageHeaders)); | ||||
| 		assertTrue(this.resolver.supportsParameter(this.paramMessageHeaderAccessor)); | ||||
| 		assertTrue(this.resolver.supportsParameter(this.paramMessageHeaderAccessorSubclass)); | ||||
| 
 | ||||
| 		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 | ||||
| 	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); | ||||
| 		@SuppressWarnings("unchecked") | ||||
|  | @ -91,12 +74,12 @@ public class HeadersMethodArgumentResolverTests { | |||
| 
 | ||||
| 	@Test(expected = IllegalStateException.class) | ||||
| 	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 | ||||
| 	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); | ||||
| 		MessageHeaders headers = (MessageHeaders) resolved; | ||||
|  | @ -105,7 +88,8 @@ public class HeadersMethodArgumentResolverTests { | |||
| 
 | ||||
| 	@Test | ||||
| 	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); | ||||
| 		MessageHeaderAccessor headers = (MessageHeaderAccessor) resolved; | ||||
|  | @ -114,7 +98,8 @@ public class HeadersMethodArgumentResolverTests { | |||
| 
 | ||||
| 	@Test | ||||
| 	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); | ||||
| 		TestMessageHeaderAccessor headers = (TestMessageHeaderAccessor) resolved; | ||||
|  | @ -124,7 +109,7 @@ public class HeadersMethodArgumentResolverTests { | |||
| 
 | ||||
| 	@SuppressWarnings("unused") | ||||
| 	private void handleMessage( | ||||
| 			@Headers Map<String, ?> param1, | ||||
| 			@Headers Map<String, Object> param1, | ||||
| 			@Headers String param2, | ||||
| 			MessageHeaders param3, | ||||
| 			MessageHeaderAccessor param4, | ||||
|  | @ -134,7 +119,7 @@ public class HeadersMethodArgumentResolverTests { | |||
| 
 | ||||
| 	public static class TestMessageHeaderAccessor extends NativeMessageHeaderAccessor { | ||||
| 
 | ||||
| 		protected TestMessageHeaderAccessor(Message<?> message) { | ||||
| 		TestMessageHeaderAccessor(Message<?> 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.lang.Nullable; | ||||
| import org.springframework.messaging.Message; | ||||
| import org.springframework.messaging.handler.ResolvableMethod; | ||||
| 
 | ||||
| import static org.hamcrest.Matchers.*; | ||||
| import static org.junit.Assert.*; | ||||
|  |  | |||
|  | @ -20,7 +20,6 @@ import java.lang.reflect.Method; | |||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Comparator; | ||||
| import java.util.HashMap; | ||||
| import java.util.LinkedHashMap; | ||||
| import java.util.LinkedHashSet; | ||||
| import java.util.List; | ||||
|  | @ -32,7 +31,6 @@ import org.junit.Before; | |||
| import org.junit.Test; | ||||
| 
 | ||||
| import org.springframework.context.support.StaticApplicationContext; | ||||
| import org.springframework.core.MethodIntrospector; | ||||
| import org.springframework.messaging.Message; | ||||
| import org.springframework.messaging.converter.SimpleMessageConverter; | ||||
| 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.support.MessageBuilder; | ||||
| import org.springframework.util.AntPathMatcher; | ||||
| import org.springframework.util.Assert; | ||||
| import org.springframework.util.PathMatcher; | ||||
| import org.springframework.util.ReflectionUtils.MethodFilter; | ||||
| 
 | ||||
| import static org.junit.Assert.*; | ||||
| 
 | ||||
|  | @ -90,7 +88,7 @@ public class MethodMessageHandlerTests { | |||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void antPatchMatchWildcard() throws Exception { | ||||
| 	public void patternMatch() throws Exception { | ||||
| 
 | ||||
| 		Method method = this.testController.getClass().getMethod("handlerPathMatchWildcard"); | ||||
| 		this.messageHandler.registerHandlerMethod(this.testController, method, "/handlerPathMatch*"); | ||||
|  | @ -101,7 +99,7 @@ public class MethodMessageHandlerTests { | |||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void bestMatchWildcard() throws Exception { | ||||
| 	public void bestMatch() throws Exception { | ||||
| 
 | ||||
| 		Method method = this.testController.getClass().getMethod("bestMatch"); | ||||
| 		this.messageHandler.registerHandlerMethod(this.testController, method, "/bestmatch/{foo}/path"); | ||||
|  | @ -124,7 +122,7 @@ public class MethodMessageHandlerTests { | |||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void exceptionHandled() { | ||||
| 	public void handleException() { | ||||
| 
 | ||||
| 		this.messageHandler.handleMessage(toDestination("/test/handlerThrowsExc")); | ||||
| 
 | ||||
|  | @ -166,7 +164,7 @@ public class MethodMessageHandlerTests { | |||
| 			this.method = "secondBestMatch"; | ||||
| 		} | ||||
| 
 | ||||
| 		public void illegalStateException(IllegalStateException exception) { | ||||
| 		public void handleIllegalStateException(IllegalStateException exception) { | ||||
| 			this.method = "illegalStateException"; | ||||
| 			this.arguments.put("exception", exception); | ||||
| 		} | ||||
|  | @ -186,6 +184,7 @@ public class MethodMessageHandlerTests { | |||
| 
 | ||||
| 		private PathMatcher pathMatcher = new AntPathMatcher(); | ||||
| 
 | ||||
| 
 | ||||
| 		public void registerHandler(Object handler) { | ||||
| 			super.detectHandlerMethods(handler); | ||||
| 		} | ||||
|  | @ -239,55 +238,24 @@ public class MethodMessageHandlerTests { | |||
| 
 | ||||
| 		@Override | ||||
| 		protected String getMatchingMapping(String mapping, Message<?> message) { | ||||
| 
 | ||||
| 			String destination = getLookupDestination(getDestination(message)); | ||||
| 			if (mapping.equals(destination) || this.pathMatcher.match(mapping, destination)) { | ||||
| 				return mapping; | ||||
| 			} | ||||
| 			return null; | ||||
| 			Assert.notNull(destination, "No destination"); | ||||
| 			return mapping.equals(destination) || this.pathMatcher.match(mapping, destination) ? mapping : null; | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		protected Comparator<String> getMappingComparator(final Message<?> message) { | ||||
| 			return new Comparator<String>() { | ||||
| 				@Override | ||||
| 				public int compare(String info1, String info2) { | ||||
| 					DestinationPatternsMessageCondition cond1 = new DestinationPatternsMessageCondition(info1); | ||||
| 					DestinationPatternsMessageCondition cond2 = new DestinationPatternsMessageCondition(info2); | ||||
| 					return cond1.compareTo(cond2, message); | ||||
| 				} | ||||
| 			return (info1, info2) -> { | ||||
| 				DestinationPatternsMessageCondition cond1 = new DestinationPatternsMessageCondition(info1); | ||||
| 				DestinationPatternsMessageCondition cond2 = new DestinationPatternsMessageCondition(info2); | ||||
| 				return cond1.compareTo(cond2, message); | ||||
| 			}; | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		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. | ||||
|  */ | ||||
| 
 | ||||
| package org.springframework.messaging.handler; | ||||
| package org.springframework.messaging.handler.invocation; | ||||
| 
 | ||||
| import java.lang.annotation.Annotation; | ||||
| import java.lang.reflect.Method; | ||||
|  | @ -57,7 +57,10 @@ import org.springframework.util.ReflectionUtils; | |||
| 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> | ||||
|  * | ||||
|  | @ -120,7 +123,7 @@ import static java.util.stream.Collectors.*; | |||
|  * </pre> | ||||
|  * | ||||
|  * @author Rossen Stoyanchev | ||||
|  * @since 5.0 | ||||
|  * @since 5.2 | ||||
|  */ | ||||
| public class ResolvableMethod { | ||||
| 
 | ||||
|  | @ -186,6 +189,7 @@ public class ResolvableMethod { | |||
| 
 | ||||
| 	/** | ||||
| 	 * Filter on method arguments with annotation. | ||||
| 	 * See {@link org.springframework.web.method.MvcAnnotationPredicates}. | ||||
| 	 */ | ||||
| 	@SafeVarargs | ||||
| 	public final ArgResolver annot(Predicate<MethodParameter>... filter) { | ||||
|  | @ -298,6 +302,7 @@ public class ResolvableMethod { | |||
| 
 | ||||
| 		/** | ||||
| 		 * Filter on annotated methods. | ||||
| 		 * See {@link org.springframework.web.method.MvcAnnotationPredicates}. | ||||
| 		 */ | ||||
| 		@SafeVarargs | ||||
| 		public final Builder<T> annot(Predicate<Method>... filters) { | ||||
|  | @ -308,6 +313,7 @@ public class ResolvableMethod { | |||
| 		/** | ||||
| 		 * Filter on methods annotated with the given annotation type. | ||||
| 		 * @see #annot(Predicate[]) | ||||
| 		 * See {@link org.springframework.web.method.MvcAnnotationPredicates}. | ||||
| 		 */ | ||||
| 		@SafeVarargs | ||||
| 		public final Builder<T> annotPresent(Class<? extends Annotation>... annotationTypes) { | ||||
|  | @ -524,6 +530,7 @@ public class ResolvableMethod { | |||
| 
 | ||||
| 		/** | ||||
| 		 * Filter on method arguments with annotations. | ||||
| 		 * See {@link org.springframework.web.method.MvcAnnotationPredicates}. | ||||
| 		 */ | ||||
| 		@SafeVarargs | ||||
| 		public final ArgResolver annot(Predicate<MethodParameter>... filters) { | ||||
|  | @ -535,6 +542,7 @@ public class ResolvableMethod { | |||
| 		 * Filter on method arguments that have the given annotations. | ||||
| 		 * @param annotationTypes the annotation types | ||||
| 		 * @see #annot(Predicate[]) | ||||
| 		 * See {@link org.springframework.web.method.MvcAnnotationPredicates}. | ||||
| 		 */ | ||||
| 		@SafeVarargs | ||||
| 		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.*; | ||||
| 
 | ||||
| /** | ||||
|  * 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> | ||||
|  * | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue