INT-4300: Add WebFlux Server Support

JIRA: https://jira.spring.io/browse/INT-4300

* Add `ReactiveHttpInboundEndpoint` based on the WebFlux foundation
* Extract `BaseHttpInboundEndpoint` for common
HTTP Inbound Channel Adapters options
* Make `spring-webmvc` and `spring-webflux` as `optional` dependencies
to let end-user to choose
* Refactor `HttpContextUtils` to include constants
for newly added WebFlux support
* Introduce `BaseHttpInboundEndpoint.setRequestPayloadTypeClass()`
for raw `Class<?>` and modify existing `setRequestPayloadType()`
for the `ResolvableType`
* Refactor existing MVC tests and XML components parsers to use
new `setRequestPayloadTypeClass()`
* Add `MessagingGatewaySupport.sendAndReceiveMessageReactive()` to get
a reply from downstream flow reactive back-pressure manner
* Add `IntegrationHandlerResultHandler` implementation to let WebFlux
infrastructure to handle the `Mono<Void>` from the `ReactiveHttpInboundEndpoint`
properly
* Fix `JdbcLockRegistryLeaderInitiatorTests` race condition to assert
the `initiator1` is elected eventually after yielding when the `initiator2`
is stopped

* Fix JavaDocs issue in the `HttpRequestHandlingMessagingGateway`
* Move all the "hard" logic in the `MessagingGatewaySupport#doSendAndReceiveMessageReactive`
to the `Mono` chain ensuring back-pressure when `sendAndReceiveMessageReactive()` is
called not from the Reactive Stream

Add test-case to demonstrate SSE

JIRA: https://jira.spring.io/browse/INT-3625

Some polishing and optimization for the
`MessagingGatewaySupport.doSendAndReceiveMessageReactive()`

More optimization for `MessagingGatewaySupport`

* Upgrade to Reactor 3.1 M3
* Document WebFlux-based components

Minor Doc Polishing
This commit is contained in:
Artem Bilan
2017-06-20 10:14:57 -04:00
committed by Gary Russell
parent e475d9c695
commit d4a99919ed
26 changed files with 2268 additions and 745 deletions

View File

@@ -106,7 +106,7 @@ subprojects { subproject ->
hibernateVersion = '5.2.10.Final'
hsqldbVersion = '2.4.0'
h2Version = '1.4.194'
jackson2Version = '2.9.0.pr3'
jackson2Version = '2.9.0.pr4'
javaxActivationVersion = '1.1.1'
javaxMailVersion = '1.6.0-rc1'
jedisVersion = '2.9.0'
@@ -124,8 +124,8 @@ subprojects { subproject ->
mysqlVersion = '5.1.41'
pahoMqttClientVersion = '1.1.1'
postgresVersion = '42.0.0'
reactorNettyVersion = '0.7.0.BUILD-SNAPSHOT'
reactorVersion = '3.1.0.BUILD-SNAPSHOT'
reactorNettyVersion = '0.7.0.M1'
reactorVersion = '3.1.0.M3'
romeToolsVersion = '1.7.2'
servletApiVersion = '3.1.0'
slf4jVersion = "1.7.25"
@@ -152,7 +152,7 @@ subprojects { subproject ->
}
jacoco {
toolVersion = "0.7.8"
toolVersion = "0.7.9"
}
// dependencies that are common across all java projects
@@ -396,8 +396,9 @@ project('spring-integration-http') {
description = 'Spring Integration HTTP Support'
dependencies {
compile project(":spring-integration-core")
compile "org.springframework:spring-webmvc:$springVersion"
compile "org.springframework:spring-webflux:$springVersion"
compile "org.springframework:spring-web:$springVersion"
compile ("org.springframework:spring-webmvc:$springVersion", optional)
compile ("org.springframework:spring-webflux:$springVersion", optional)
compile ("javax.servlet:javax.servlet-api:$servletApiVersion", provided)
compile ("com.rometools:rome:$romeToolsVersion", optional)
compile ("io.projectreactor.ipc:reactor-netty:$reactorNettyVersion" , optional)

View File

@@ -16,35 +16,48 @@
package org.springframework.integration.gateway;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong;
import org.reactivestreams.Subscriber;
import org.springframework.core.AttributeAccessor;
import org.springframework.integration.MessageTimeoutException;
import org.springframework.integration.channel.ReactiveStreamsSubscribableChannel;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.endpoint.AbstractEndpoint;
import org.springframework.integration.endpoint.EventDrivenConsumer;
import org.springframework.integration.endpoint.PollingConsumer;
import org.springframework.integration.endpoint.ReactiveStreamsConsumer;
import org.springframework.integration.handler.BridgeHandler;
import org.springframework.integration.history.HistoryWritingMessagePostProcessor;
import org.springframework.integration.mapping.InboundMessageMapper;
import org.springframework.integration.mapping.MessageMappingException;
import org.springframework.integration.mapping.OutboundMessageMapper;
import org.springframework.integration.support.DefaultErrorMessageStrategy;
import org.springframework.integration.support.DefaultMessageBuilderFactory;
import org.springframework.integration.support.ErrorMessageStrategy;
import org.springframework.integration.support.ErrorMessageUtils;
import org.springframework.integration.support.MessageBuilderFactory;
import org.springframework.integration.support.MutableMessageBuilder;
import org.springframework.integration.support.converter.SimpleMessageConverter;
import org.springframework.integration.support.management.IntegrationManagedResource;
import org.springframework.integration.support.management.MessageSourceMetrics;
import org.springframework.integration.support.management.TrackableComponent;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
/**
* A convenient base class for connecting application code to
* {@link MessageChannel}s for sending, receiving, or request-reply operations.
@@ -450,10 +463,9 @@ public abstract class MessagingGatewaySupport extends AbstractEndpoint
if (requestChannel == null) {
throw new MessagingException("No request channel available. Cannot send request message.");
}
MessageChannel replyChannel = getReplyChannel();
if (replyChannel != null && this.replyMessageCorrelator == null) {
this.registerReplyMessageCorrelator();
}
registerReplyMessageCorrelatorIfNecessary();
Object reply = null;
Throwable error = null;
Message<?> requestMessage = null;
@@ -533,6 +545,129 @@ public abstract class MessagingGatewaySupport extends AbstractEndpoint
return reply;
}
protected Mono<Message<?>> sendAndReceiveMessageReactive(Object object) {
initializeIfNecessary();
Assert.notNull(object, "request must not be null");
MessageChannel requestChannel = getRequestChannel();
if (requestChannel == null) {
throw new MessagingException("No request channel available. Cannot send request message.");
}
registerReplyMessageCorrelatorIfNecessary();
return doSendAndReceiveMessageReactive(requestChannel, object, false);
}
@SuppressWarnings("unchecked")
private Mono<Message<?>> doSendAndReceiveMessageReactive(MessageChannel requestChannel, Object object,
boolean error) {
return Mono.defer(() -> {
Message<?> message;
try {
message = object instanceof Message<?>
? (Message<?>) object
: this.requestMapper.toMessage(object);
message = this.historyWritingPostProcessor.postProcessMessage(message);
}
catch (Exception e) {
throw new MessageMappingException("Cannot map to message: " + object, e);
}
Object originalReplyChannelHeader = message.getHeaders().getReplyChannel();
Object originalErrorChannelHeader = message.getHeaders().getErrorChannel();
FutureReplyChannel replyChannel = new FutureReplyChannel();
Message<?> requestMessage = MutableMessageBuilder.fromMessage(message)
.setReplyChannel(replyChannel)
.setHeader(this.messagingTemplate.getSendTimeoutHeader(), null)
.setHeader(this.messagingTemplate.getReceiveTimeoutHeader(), null)
.setErrorChannel(replyChannel)
.build();
if (requestChannel instanceof ReactiveStreamsSubscribableChannel) {
((ReactiveStreamsSubscribableChannel) requestChannel)
.subscribeTo(Mono.just(requestMessage));
}
else {
long sendTimeout = sendTimeout(requestMessage);
boolean sent =
sendTimeout >= 0
? requestChannel.send(requestMessage, sendTimeout)
: requestChannel.send(requestMessage);
if (!sent) {
throw new MessageDeliveryException(requestMessage,
"Failed to send message to channel '" + requestChannel +
"' within timeout: " + sendTimeout);
}
}
return Mono.fromFuture(replyChannel.messageFuture)
.doOnSubscribe(s -> {
if (!error && this.countsEnabled) {
this.messageCount.incrementAndGet();
}
})
.<Message<?>>map(replyMessage ->
MessageBuilder.fromMessage(replyMessage)
.setHeader(MessageHeaders.REPLY_CHANNEL, originalReplyChannelHeader)
.setHeader(MessageHeaders.ERROR_CHANNEL, originalErrorChannelHeader)
.build())
.onErrorResume(t -> error ? Mono.error(t) : handleSendError(requestMessage, t));
});
}
private Mono<Message<?>> handleSendError(Message<?> requestMessage, Throwable exception) {
if (logger.isDebugEnabled()) {
logger.debug("failure occurred in gateway sendAndReceiveReactive: " + exception.getMessage());
}
MessageChannel errorChannel = getErrorChannel();
if (errorChannel != null) {
ErrorMessage errorMessage = buildErrorMessage(requestMessage, exception);
try {
return doSendAndReceiveMessageReactive(errorChannel, errorMessage, true);
}
catch (Exception errorFlowFailure) {
throw new MessagingException(errorMessage, "failure occurred in error-handling flow", errorFlowFailure);
}
}
else {
// no errorChannel so we'll propagate
throw wrapExceptionIfNecessary(exception, "gateway received checked Exception");
}
}
private long sendTimeout(Message<?> requestMessage) {
Long sendTimeout = headerToLong(requestMessage.getHeaders()
.get(this.messagingTemplate.getSendTimeoutHeader()));
return (sendTimeout != null ? sendTimeout : this.messagingTemplate.getSendTimeout());
}
private long receiveTimeout(Message<?> requestMessage) {
Long receiveTimeout = headerToLong(requestMessage.getHeaders()
.get(this.messagingTemplate.getReceiveTimeoutHeader()));
return (receiveTimeout != null ? receiveTimeout : this.messagingTemplate.getReceiveTimeout());
}
@Nullable
private Long headerToLong(@Nullable Object headerValue) {
if (headerValue instanceof Number) {
return ((Number) headerValue).longValue();
}
else if (headerValue instanceof String) {
return Long.parseLong((String) headerValue);
}
else {
return null;
}
}
/**
* Build an error message for the message and throwable using the configured
* {@link ErrorMessageStrategy}.
@@ -542,9 +677,7 @@ public abstract class MessagingGatewaySupport extends AbstractEndpoint
* @since 4.3.10
*/
protected final ErrorMessage buildErrorMessage(Message<?> requestMessage, Throwable throwable) {
ErrorMessage errorMessage = this.errorMessageStrategy.buildErrorMessage(throwable,
getErrorMessageAttributes(requestMessage));
return errorMessage;
return this.errorMessageStrategy.buildErrorMessage(throwable, getErrorMessageAttributes(requestMessage));
}
/**
@@ -560,42 +693,60 @@ public abstract class MessagingGatewaySupport extends AbstractEndpoint
}
private void rethrow(Throwable t, String description) {
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
}
throw new MessagingException(description, t);
throw wrapExceptionIfNecessary(t, description);
}
private void registerReplyMessageCorrelator() {
synchronized (this.replyMessageCorrelatorMonitor) {
if (this.replyMessageCorrelator != null) {
return;
private RuntimeException wrapExceptionIfNecessary(Throwable t, String description) {
if (t instanceof RuntimeException) {
return (RuntimeException) t;
}
else {
return new MessagingException(description, t);
}
}
protected void registerReplyMessageCorrelatorIfNecessary() {
MessageChannel replyChannel = getReplyChannel();
if (replyChannel != null && this.replyMessageCorrelator == null) {
boolean shouldStartCorrelator;
synchronized (this.replyMessageCorrelatorMonitor) {
if (this.replyMessageCorrelator != null) {
return;
}
AbstractEndpoint correlator;
BridgeHandler handler = new BridgeHandler();
if (getBeanFactory() != null) {
handler.setBeanFactory(getBeanFactory());
}
handler.afterPropertiesSet();
if (replyChannel instanceof SubscribableChannel) {
correlator = new EventDrivenConsumer((SubscribableChannel) replyChannel, handler);
}
else if (replyChannel instanceof PollableChannel) {
PollingConsumer endpoint = new PollingConsumer((PollableChannel) replyChannel, handler);
endpoint.setBeanFactory(getBeanFactory());
endpoint.setReceiveTimeout(this.replyTimeout);
endpoint.afterPropertiesSet();
correlator = endpoint;
}
else if (replyChannel instanceof ReactiveStreamsSubscribableChannel) {
ReactiveStreamsConsumer endpoint =
new ReactiveStreamsConsumer(replyChannel, (Subscriber<Message<?>>) handler);
endpoint.afterPropertiesSet();
correlator = endpoint;
}
else {
throw new MessagingException("Unsupported 'replyChannel' type [" + replyChannel.getClass() + "]."
+ "SubscribableChannel or PollableChannel type are supported.");
}
this.replyMessageCorrelator = correlator;
shouldStartCorrelator = true;
}
AbstractEndpoint correlator = null;
BridgeHandler handler = new BridgeHandler();
if (this.getBeanFactory() != null) {
handler.setBeanFactory(this.getBeanFactory());
if (shouldStartCorrelator && isRunning()) {
if (isRunning()) {
this.replyMessageCorrelator.start();
}
}
handler.afterPropertiesSet();
MessageChannel replyChannel = getReplyChannel();
if (replyChannel instanceof SubscribableChannel) {
correlator = new EventDrivenConsumer((SubscribableChannel) replyChannel, handler);
}
else if (replyChannel instanceof PollableChannel) {
PollingConsumer endpoint = new PollingConsumer((PollableChannel) replyChannel, handler);
endpoint.setBeanFactory(this.getBeanFactory());
endpoint.setReceiveTimeout(this.replyTimeout);
endpoint.afterPropertiesSet();
correlator = endpoint;
}
else {
throw new MessagingException("Unsupported 'replyChannel' type [" + replyChannel.getClass() + "]."
+ "SubscribableChannel or PollableChannel type are supported.");
}
if (this.isRunning()) {
correlator.start();
}
this.replyMessageCorrelator = correlator;
}
}
@@ -641,4 +792,15 @@ public abstract class MessagingGatewaySupport extends AbstractEndpoint
}
private static class FutureReplyChannel implements MessageChannel {
private final CompletableFuture<Message<?>> messageFuture = new CompletableFuture<>();
@Override
public boolean send(Message<?> message, long timeout) {
return this.messageFuture.complete(message);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -191,7 +191,7 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse
builder.addPropertyValue("crossOrigin", crossOriginBuilder.getBeanDefinition());
}
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "request-payload-type", "requestPayloadType");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "request-payload-type", "requestPayloadTypeClass");
BeanDefinition statusCodeExpressionDef =
IntegrationNamespaceUtils.createExpressionDefIfAttributeDefined("status-code-expression", element);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 the original author or authors.
* Copyright 2014-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.
@@ -23,16 +23,21 @@ import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.integration.config.IntegrationConfigurationInitializer;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.http.inbound.IntegrationHandlerResultHandler;
import org.springframework.integration.http.inbound.IntegrationRequestMappingHandlerMapping;
import org.springframework.integration.http.inbound.ReactiveIntegrationRequestMappingHandlerMapping;
import org.springframework.integration.http.support.HttpContextUtils;
/**
* The HTTP Integration infrastructure {@code beanFactory} initializer.
*
* @author Artem Bilan
*
* @since 4.0
*/
public class HttpIntegrationConfigurationInitializer implements IntegrationConfigurationInitializer {
@@ -42,7 +47,8 @@ public class HttpIntegrationConfigurationInitializer implements IntegrationConfi
@Override
public void initialize(ConfigurableListableBeanFactory beanFactory) throws BeansException {
if (beanFactory instanceof BeanDefinitionRegistry) {
this.registerRequestMappingHandlerMappingIfNecessary((BeanDefinitionRegistry) beanFactory);
registerRequestMappingHandlerMappingIfNecessary((BeanDefinitionRegistry) beanFactory);
registerReactiveRequestMappingHandlerMappingIfNecessary((BeanDefinitionRegistry) beanFactory);
}
else {
logger.warn("'IntegrationRequestMappingHandlerMapping' isn't registered because 'beanFactory'" +
@@ -61,7 +67,7 @@ public class HttpIntegrationConfigurationInitializer implements IntegrationConfi
* the HTTP server components.
*/
private void registerRequestMappingHandlerMappingIfNecessary(BeanDefinitionRegistry registry) {
if (HttpContextUtils.SERVLET_PRESENT &&
if (HttpContextUtils.WEB_MVC_PRESENT &&
!registry.containsBeanDefinition(HttpContextUtils.HANDLER_MAPPING_BEAN_NAME)) {
BeanDefinitionBuilder requestMappingBuilder =
BeanDefinitionBuilder.genericBeanDefinition(IntegrationRequestMappingHandlerMapping.class);
@@ -72,4 +78,30 @@ public class HttpIntegrationConfigurationInitializer implements IntegrationConfi
}
}
/**
* Registers a {@link ReactiveIntegrationRequestMappingHandlerMapping}
* which could also be overridden by the user by simply registering
* a {@link ReactiveIntegrationRequestMappingHandlerMapping} {@code <bean>} with 'id'
* {@link HttpContextUtils#REACTIVE_HANDLER_MAPPING_BEAN_NAME}.
* <p>
* In addition, checks if the {@code org.springframework.web.reactive.result.method.RequestMappingInfo}
* class is present on the classpath.
* When Spring Integration HTTP is used only as an HTTP client, there is no reason to use and register
* the HTTP server components.
*/
private void registerReactiveRequestMappingHandlerMappingIfNecessary(BeanDefinitionRegistry registry) {
if (HttpContextUtils.WEB_FLUX_PRESENT &&
!registry.containsBeanDefinition(HttpContextUtils.REACTIVE_HANDLER_MAPPING_BEAN_NAME)) {
BeanDefinitionBuilder requestMappingBuilder =
BeanDefinitionBuilder.genericBeanDefinition(ReactiveIntegrationRequestMappingHandlerMapping.class);
requestMappingBuilder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
requestMappingBuilder.addPropertyValue(IntegrationNamespaceUtils.ORDER, 0);
registry.registerBeanDefinition(HttpContextUtils.REACTIVE_HANDLER_MAPPING_BEAN_NAME,
requestMappingBuilder.getBeanDefinition());
BeanDefinitionReaderUtils.registerWithGeneratedName(
new RootBeanDefinition(IntegrationHandlerResultHandler.class), registry);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016 the original author or authors.
* Copyright 2016-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.
@@ -40,7 +40,7 @@ public class IntegrationGraphControllerParser implements BeanDefinitionParser {
@Override
public BeanDefinition parse(final Element element, ParserContext parserContext) {
if (HttpContextUtils.SERVLET_PRESENT) {
if (HttpContextUtils.WEB_MVC_PRESENT) {
this.graphControllerRegistrar.registerBeanDefinitions(
new StandardAnnotationMetadata(IntegrationGraphControllerParser.class) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016 the original author or authors.
* Copyright 2016-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.
@@ -25,6 +25,7 @@ import org.springframework.integration.http.support.HttpContextUtils;
/**
* @author Artem Bilan
*
* @since 4.3
*/
class IntegrationGraphControllerRegistrarImportSelector implements ImportSelector {
@@ -33,7 +34,7 @@ class IntegrationGraphControllerRegistrarImportSelector implements ImportSelecto
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
if (HttpContextUtils.SERVLET_PRESENT) {
if (HttpContextUtils.WEB_MVC_PRESENT) {
return new String[] { IntegrationGraphControllerRegistrar.class.getName() };
}
else {

View File

@@ -17,27 +17,10 @@
package org.springframework.integration.http.dsl;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import org.springframework.expression.Expression;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.integration.dsl.ComponentsRegistration;
import org.springframework.integration.dsl.MessagingGatewaySpec;
import org.springframework.integration.expression.FunctionExpression;
import org.springframework.integration.http.inbound.CrossOrigin;
import org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport;
import org.springframework.integration.http.inbound.RequestMapping;
import org.springframework.integration.http.support.DefaultHttpHeaderMapper;
import org.springframework.integration.mapping.HeaderMapper;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartResolver;
/**
@@ -52,126 +35,10 @@ import org.springframework.web.multipart.MultipartResolver;
*/
public abstract class BaseHttpInboundEndpointSpec<S extends BaseHttpInboundEndpointSpec<S, E>,
E extends HttpRequestHandlingEndpointSupport>
extends MessagingGatewaySpec<S, E> implements ComponentsRegistration {
private final RequestMapping requestMapping = new RequestMapping();
private final Map<String, Expression> headerExpressions = new HashMap<>();
private final HeaderMapper<HttpHeaders> headerMapper = DefaultHttpHeaderMapper.inboundMapper();
private HeaderMapper<HttpHeaders> explicitHeaderMapper;
extends HttpInboundEndpointSupportSpec<S, E> {
BaseHttpInboundEndpointSpec(E endpoint, String... path) {
super(endpoint);
this.requestMapping.setPathPatterns(path);
this.target.setRequestMapping(this.requestMapping);
this.target.setHeaderExpressions(this.headerExpressions);
this.target.setHeaderMapper(this.headerMapper);
}
/**
* Provide a {@link Consumer} for configuring {@link RequestMapping} via {@link RequestMappingSpec}
* @param requestMapping the {@link Consumer} to configure {@link RequestMappingSpec}.
* @return the spec
* @see RequestMapping
*/
public S requestMapping(Consumer<RequestMappingSpec> requestMapping) {
requestMapping.accept(new RequestMappingSpec(this.requestMapping));
return _this();
}
/**
* Provide a {@link Consumer} for configuring {@link CrossOrigin} via {@link CrossOriginSpec}
* @param crossOrigin the {@link Consumer} to configure {@link CrossOriginSpec}.
* @return the spec
* @see CrossOrigin
*/
public S crossOrigin(Consumer<CrossOriginSpec> crossOrigin) {
CrossOriginSpec originSpec = new CrossOriginSpec();
crossOrigin.accept(originSpec);
this.target.setCrossOrigin(originSpec.crossOrigin);
return _this();
}
/**
* Specify a SpEL expression to evaluate in order to generate the Message payload.
* @param payloadExpression The payload expression.
* @return the spec
* @see HttpRequestHandlingEndpointSupport#setPayloadExpression(Expression)
*/
public S payloadExpression(String payloadExpression) {
return payloadExpression(PARSER.parseExpression(payloadExpression));
}
/**
* Specify a SpEL expression to evaluate in order to generate the Message payload.
* @param payloadExpression The payload expression.
* @return the spec
* @see HttpRequestHandlingEndpointSupport#setPayloadExpression(Expression)
*/
public S payloadExpression(Expression payloadExpression) {
this.target.setPayloadExpression(payloadExpression);
return _this();
}
/**
* Specify a {@link Function} to evaluate in order to generate the Message payload.
* @param payloadFunction The payload {@link Function}.
* @param <P> the expected HTTP request body type.
* @return the spec
* @see HttpRequestHandlingEndpointSupport#setPayloadExpression(Expression)
*/
public <P> S payloadFunction(Function<HttpEntity<P>, ?> payloadFunction) {
return payloadExpression(new FunctionExpression<>(payloadFunction));
}
/**
* Specify a Map of SpEL expressions to evaluate in order to generate the Message headers.
* @param headerExpressions The {@link Map} of SpEL expressions for headers.
* @return the spec
* @see HttpRequestHandlingEndpointSupport#setHeaderExpressions(Map)
*/
public S headerExpressions(Map<String, Expression> headerExpressions) {
Assert.notNull(headerExpressions, "'headerExpressions' must not be null");
this.headerExpressions.clear();
this.headerExpressions.putAll(headerExpressions);
return _this();
}
/**
* Specify SpEL expression for provided header to populate.
* @param header the header name to populate.
* @param expression the SpEL expression for the header.
* @return the spec
* @see HttpRequestHandlingEndpointSupport#setHeaderExpressions(Map)
*/
public S headerExpression(String header, String expression) {
return headerExpression(header, PARSER.parseExpression(expression));
}
/**
* Specify SpEL expression for provided header to populate.
* @param header the header name to populate.
* @param expression the SpEL expression for the header.
* @return the spec
* @see HttpRequestHandlingEndpointSupport#setHeaderExpressions(Map)
*/
public S headerExpression(String header, Expression expression) {
this.headerExpressions.put(header, expression);
return _this();
}
/**
* Specify a {@link Function} for provided header to populate.
* @param header the header name to add.
* @param headerFunction the function to evaluate the header value against {@link HttpEntity}.
* @param <P> the expected HTTP body type.
* @return the current Spec.
* @see HttpRequestHandlingEndpointSupport#setHeaderExpressions(Map)
*/
public <P> S headerFunction(String header, Function<HttpEntity<P>, ?> headerFunction) {
return headerExpression(header, new FunctionExpression<>(headerFunction));
super(endpoint, path);
}
/**
@@ -195,70 +62,6 @@ public abstract class BaseHttpInboundEndpointSpec<S extends BaseHttpInboundEndpo
return _this();
}
/**
* Set the {@link HeaderMapper} to use when mapping between HTTP headers and MessageHeaders.
* @param headerMapper The header mapper.
* @return the current Spec.
*/
public S headerMapper(HeaderMapper<HttpHeaders> headerMapper) {
this.target.setHeaderMapper(headerMapper);
this.explicitHeaderMapper = headerMapper;
return _this();
}
/**
* Provide the pattern array for request headers to map.
* @param patterns the patterns for request headers to map.
* @return the current Spec.
* @see DefaultHttpHeaderMapper#setOutboundHeaderNames(String[])
*/
public S mappedRequestHeaders(String... patterns) {
Assert.isNull(this.explicitHeaderMapper,
"The 'mappedRequestHeaders' must be specified on the provided 'headerMapper': "
+ this.explicitHeaderMapper);
((DefaultHttpHeaderMapper) this.headerMapper).setInboundHeaderNames(patterns);
return _this();
}
/**
* Provide the pattern array for response headers to map.
* @param patterns the patterns for response headers to map.
* @return the current Spec.
* @see DefaultHttpHeaderMapper#setInboundHeaderNames(String[])
*/
public S mappedResponseHeaders(String... patterns) {
Assert.isNull(this.explicitHeaderMapper,
"The 'mappedRequestHeaders' must be specified on the provided 'headerMapper': "
+ this.explicitHeaderMapper);
((DefaultHttpHeaderMapper) this.headerMapper).setOutboundHeaderNames(patterns);
return _this();
}
/**
* Specify the type of payload to be generated when the inbound HTTP request content is read by the
* {@link HttpMessageConverter}s.
* By default this value is null which means at runtime any "text" Content-Type will
* result in String while all others default to <code>byte[].class</code>.
* @param requestPayloadType The payload type.
* @return the current Spec.
*/
public S requestPayloadType(Class<?> requestPayloadType) {
this.target.setRequestPayloadType(requestPayloadType);
return _this();
}
/**
* Specify whether only the reply Message's payload should be passed in the response.
* If this is set to {@code false}, the entire Message will be used to generate the response.
* The default is {@code true}.
* @param extractReplyPayload true to extract the reply payload.
* @return the current Spec.
*/
public S extractReplyPayload(boolean extractReplyPayload) {
this.target.setExtractReplyPayload(extractReplyPayload);
return _this();
}
/**
* Specify the {@link MultipartResolver} to use when checking requests.
* @param multipartResolver The multipart resolver.
@@ -269,199 +72,4 @@ public abstract class BaseHttpInboundEndpointSpec<S extends BaseHttpInboundEndpo
return _this();
}
/**
* Specify the {@link Expression} to resolve a status code for Response to override
* the default '200 OK' or '500 Internal Server Error' for a timeout.
* @param statusCodeExpression The status code Expression.
* @return the current Spec.
* @see HttpRequestHandlingEndpointSupport#setStatusCodeExpression(Expression)
*/
public S statusCodeExpression(String statusCodeExpression) {
this.target.setStatusCodeExpressionString(statusCodeExpression);
return _this();
}
/**
* Specify the {@link Expression} to resolve a status code for Response to override
* the default '200 OK' or '500 Internal Server Error' for a timeout.
* @param statusCodeExpression The status code Expression.
* @return the current Spec.
* @see HttpRequestHandlingEndpointSupport#setStatusCodeExpression(Expression)
*/
public S statusCodeExpression(Expression statusCodeExpression) {
this.target.setStatusCodeExpression(statusCodeExpression);
return _this();
}
/**
* Specify the {@link Function} to resolve a status code for Response to override
* the default '200 OK' or '500 Internal Server Error' for a timeout.
* @param statusCodeFunction The status code {@link Function}.
* @return the current Spec.
* @see HttpRequestHandlingEndpointSupport#setStatusCodeExpression(Expression)
*/
public S statusCodeFunction(Function<Void, ?> statusCodeFunction) {
return statusCodeExpression(new FunctionExpression<>(statusCodeFunction));
}
@Override
public Map<Object, String> getComponentsToRegister() {
HeaderMapper<HttpHeaders> headerMapperToRegister =
(this.explicitHeaderMapper != null ? this.explicitHeaderMapper : this.headerMapper);
return Collections.singletonMap(headerMapperToRegister, null);
}
/**
* A fluent API for the {@link RequestMapping}.
*/
public static final class RequestMappingSpec {
private final RequestMapping requestMapping;
RequestMappingSpec(RequestMapping requestMapping) {
this.requestMapping = requestMapping;
}
/**
* The HTTP request methods to map to, narrowing the primary mapping:
* GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE.
* @param supportedMethods the {@link HttpMethod}s to use.
* @return the spec
*/
public RequestMappingSpec methods(HttpMethod... supportedMethods) {
this.requestMapping.setMethods(supportedMethods);
return this;
}
/**
* The parameters of the mapped request, narrowing the primary mapping.
* @param params the request params to map to.
* @return the spec
*/
public RequestMappingSpec params(String... params) {
this.requestMapping.setParams(params);
return this;
}
/**
* The headers of the mapped request, narrowing the primary mapping.
* @param headers the request headers to map to.
* @return the spec
*/
public RequestMappingSpec headers(String... headers) {
this.requestMapping.setHeaders(headers);
return this;
}
/**
* The consumable media types of the mapped request, narrowing the primary mapping.
* @param consumes the the media types for {@code Content-Type} header.
* @return the spec
*/
public RequestMappingSpec consumes(String... consumes) {
this.requestMapping.setConsumes(consumes);
return this;
}
/**
* The producible media types of the mapped request, narrowing the primary mapping.
* @param produces the the media types for {@code Accept} header.
* @return the spec
*/
public RequestMappingSpec produces(String... produces) {
this.requestMapping.setProduces(produces);
return this;
}
}
/**
* A fluent API for the {@link CrossOrigin}.
*/
public static final class CrossOriginSpec {
private final CrossOrigin crossOrigin = new CrossOrigin();
CrossOriginSpec() {
super();
}
/**
* List of allowed origins, e.g. {@code "http://domain1.com"}.
* <p>These values are placed in the {@code Access-Control-Allow-Origin}
* header of both the pre-flight response and the actual response.
* {@code "*"} means that all origins are allowed.
* <p>If undefined, all origins are allowed.
* @param origin the list of allowed origins.
* @return the spec
*/
public CrossOriginSpec origin(String... origin) {
this.crossOrigin.setOrigin(origin);
return this;
}
/**
* List of request headers that can be used during the actual request.
* <p>This property controls the value of the pre-flight response's
* {@code Access-Control-Allow-Headers} header.
* {@code "*"} means that all headers requested by the client are allowed.
* @param allowedHeaders the list of request headers.
* @return the spec
*/
public CrossOriginSpec allowedHeaders(String... allowedHeaders) {
this.crossOrigin.setAllowedHeaders(allowedHeaders);
return this;
}
/**
* List of response headers that the user-agent will allow the client to access.
* <p>This property controls the value of actual response's
* {@code Access-Control-Expose-Headers} header.
* @param exposedHeaders the list of response headers.
* @return the spec
*/
public CrossOriginSpec exposedHeaders(String... exposedHeaders) {
this.crossOrigin.setExposedHeaders(exposedHeaders);
return this;
}
/**
* List of supported HTTP request methods, e.g.
* {@code "{RequestMethod.GET, RequestMethod.POST}"}.
* <p>Methods specified here override those specified via {@code RequestMapping}.
* @param method the list of supported HTTP request methods
* @return the spec
*/
public CrossOriginSpec method(RequestMethod... method) {
this.crossOrigin.setMethod(method);
return this;
}
/**
* Whether the browser should include any cookies associated with the
* domain of the request being annotated.
* <p>Set to {@code "false"} if such cookies should not included.
* @param allowCredentials the {@code boolean} flag to include
* {@code Access-Control-Allow-Credentials=true} in pre-flight response or not
* @return the spec
*/
public CrossOriginSpec allowCredentials(Boolean allowCredentials) {
this.crossOrigin.setAllowCredentials(allowCredentials);
return this;
}
/**
* The maximum age (in seconds) of the cache duration for pre-flight responses.
* <p>This property controls the value of the {@code Access-Control-Max-Age}
* header in the pre-flight response.
* @param maxAge the maximum age (in seconds) of the cache duration for pre-flight responses.
* @return the spec
*/
public CrossOriginSpec maxAge(long maxAge) {
this.crossOrigin.setMaxAge(maxAge);
return this;
}
}
}

View File

@@ -24,6 +24,7 @@ import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.expression.FunctionExpression;
import org.springframework.integration.http.inbound.HttpRequestHandlingController;
import org.springframework.integration.http.inbound.HttpRequestHandlingMessagingGateway;
import org.springframework.integration.http.inbound.ReactiveHttpInboundEndpoint;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -467,6 +468,28 @@ public final class Http {
return new HttpRequestHandlerEndpointSpec(new HttpRequestHandlingMessagingGateway(), path);
}
/**
* Create an {@link ReactiveHttpInboundEndpointSpec} builder for one-way reactive adapter
* based on the provided {@code path} array for mapping.
* @param path the path mapping URIs (e.g. "/myPath.do").
* @return the ReactiveHttpInboundEndpointSpec instance
*/
public static ReactiveHttpInboundEndpointSpec inboundReactiveChannelAdapter(String... path) {
ReactiveHttpInboundEndpoint httpInboundChannelAdapter = new ReactiveHttpInboundEndpoint(false);
return new ReactiveHttpInboundEndpointSpec(httpInboundChannelAdapter, path);
}
/**
* Create an {@link ReactiveHttpInboundEndpointSpec} builder for request-reply reactive gateway
* based on the provided {@code path} array for mapping.
* @param path the path mapping URIs (e.g. "/myPath.do").
* @return the ReactiveHttpInboundEndpointSpec instance
*/
public static ReactiveHttpInboundEndpointSpec inboundReactiveGateway(String... path) {
return new ReactiveHttpInboundEndpointSpec(new ReactiveHttpInboundEndpoint(), path);
}
private Http() {
super();
}

View File

@@ -0,0 +1,444 @@
/*
* Copyright 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.integration.http.dsl;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import org.springframework.core.ResolvableType;
import org.springframework.expression.Expression;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.integration.dsl.ComponentsRegistration;
import org.springframework.integration.dsl.MessagingGatewaySpec;
import org.springframework.integration.expression.FunctionExpression;
import org.springframework.integration.http.inbound.BaseHttpInboundEndpoint;
import org.springframework.integration.http.inbound.CrossOrigin;
import org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport;
import org.springframework.integration.http.inbound.RequestMapping;
import org.springframework.integration.http.support.DefaultHttpHeaderMapper;
import org.springframework.integration.mapping.HeaderMapper;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* @author Artem Bilan
*
* @since 5.0
*/
public abstract class HttpInboundEndpointSupportSpec<S extends HttpInboundEndpointSupportSpec<S, E>, E extends BaseHttpInboundEndpoint>
extends MessagingGatewaySpec<S, E>
implements ComponentsRegistration {
protected final RequestMapping requestMapping = new RequestMapping();
protected final Map<String, Expression> headerExpressions = new HashMap<>();
protected final HeaderMapper<HttpHeaders> headerMapper = DefaultHttpHeaderMapper.inboundMapper();
private HeaderMapper<HttpHeaders> explicitHeaderMapper;
HttpInboundEndpointSupportSpec(E gateway, String... path) {
super(gateway);
this.requestMapping.setPathPatterns(path);
this.target.setRequestMapping(this.requestMapping);
this.target.setHeaderExpressions(this.headerExpressions);
this.target.setHeaderMapper(this.headerMapper);
}
/**
* Provide a {@link Consumer} for configuring {@link RequestMapping} via {@link RequestMappingSpec}
* @param requestMapping the {@link Consumer} to configure {@link RequestMappingSpec}.
* @return the spec
* @see RequestMapping
*/
public S requestMapping(Consumer<RequestMappingSpec> requestMapping) {
requestMapping.accept(new RequestMappingSpec(this.requestMapping));
return _this();
}
/**
* Provide a {@link Consumer} for configuring {@link CrossOrigin} via {@link CrossOriginSpec}
* @param crossOrigin the {@link Consumer} to configure {@link CrossOriginSpec}.
* @return the spec
* @see CrossOrigin
*/
public S crossOrigin(Consumer<CrossOriginSpec> crossOrigin) {
CrossOriginSpec originSpec = new CrossOriginSpec();
crossOrigin.accept(originSpec);
this.target.setCrossOrigin(originSpec.crossOrigin);
return _this();
}
/**
* Specify a SpEL expression to evaluate in order to generate the Message payload.
* @param payloadExpression The payload expression.
* @return the spec
* @see HttpRequestHandlingEndpointSupport#setPayloadExpression(Expression)
*/
public S payloadExpression(String payloadExpression) {
return payloadExpression(PARSER.parseExpression(payloadExpression));
}
/**
* Specify a SpEL expression to evaluate in order to generate the Message payload.
* @param payloadExpression The payload expression.
* @return the spec
* @see HttpRequestHandlingEndpointSupport#setPayloadExpression(Expression)
*/
public S payloadExpression(Expression payloadExpression) {
this.target.setPayloadExpression(payloadExpression);
return _this();
}
/**
* Specify a {@link Function} to evaluate in order to generate the Message payload.
* @param payloadFunction The payload {@link Function}.
* @param <P> the expected HTTP request body type.
* @return the spec
* @see HttpRequestHandlingEndpointSupport#setPayloadExpression(Expression)
*/
public <P> S payloadFunction(Function<HttpEntity<P>, ?> payloadFunction) {
return payloadExpression(new FunctionExpression<>(payloadFunction));
}
/**
* Specify a Map of SpEL expressions to evaluate in order to generate the Message headers.
* @param headerExpressions The {@link Map} of SpEL expressions for headers.
* @return the spec
* @see HttpRequestHandlingEndpointSupport#setHeaderExpressions(Map)
*/
public S headerExpressions(Map<String, Expression> headerExpressions) {
Assert.notNull(headerExpressions, "'headerExpressions' must not be null");
this.headerExpressions.clear();
this.headerExpressions.putAll(headerExpressions);
return _this();
}
/**
* Specify SpEL expression for provided header to populate.
* @param header the header name to populate.
* @param expression the SpEL expression for the header.
* @return the spec
* @see HttpRequestHandlingEndpointSupport#setHeaderExpressions(Map)
*/
public S headerExpression(String header, String expression) {
return headerExpression(header, PARSER.parseExpression(expression));
}
/**
* Specify SpEL expression for provided header to populate.
* @param header the header name to populate.
* @param expression the SpEL expression for the header.
* @return the spec
* @see HttpRequestHandlingEndpointSupport#setHeaderExpressions(Map)
*/
public S headerExpression(String header, Expression expression) {
this.headerExpressions.put(header, expression);
return _this();
}
/**
* Specify a {@link Function} for provided header to populate.
* @param header the header name to add.
* @param headerFunction the function to evaluate the header value against {@link HttpEntity}.
* @param <P> the expected HTTP body type.
* @return the current Spec.
* @see HttpRequestHandlingEndpointSupport#setHeaderExpressions(Map)
*/
public <P> S headerFunction(String header, Function<HttpEntity<P>, ?> headerFunction) {
return headerExpression(header, new FunctionExpression<>(headerFunction));
}
/**
* Set the {@link HeaderMapper} to use when mapping between HTTP headers and MessageHeaders.
* @param headerMapper The header mapper.
* @return the current Spec.
*/
public S headerMapper(HeaderMapper<HttpHeaders> headerMapper) {
this.target.setHeaderMapper(headerMapper);
this.explicitHeaderMapper = headerMapper;
return _this();
}
/**
* Provide the pattern array for request headers to map.
* @param patterns the patterns for request headers to map.
* @return the current Spec.
* @see DefaultHttpHeaderMapper#setOutboundHeaderNames(String[])
*/
public S mappedRequestHeaders(String... patterns) {
Assert.isNull(this.explicitHeaderMapper,
"The 'mappedRequestHeaders' must be specified on the provided 'headerMapper': "
+ this.explicitHeaderMapper);
((DefaultHttpHeaderMapper) this.headerMapper).setInboundHeaderNames(patterns);
return _this();
}
/**
* Provide the pattern array for response headers to map.
* @param patterns the patterns for response headers to map.
* @return the current Spec.
* @see DefaultHttpHeaderMapper#setInboundHeaderNames(String[])
*/
public S mappedResponseHeaders(String... patterns) {
Assert.isNull(this.explicitHeaderMapper,
"The 'mappedRequestHeaders' must be specified on the provided 'headerMapper': "
+ this.explicitHeaderMapper);
((DefaultHttpHeaderMapper) this.headerMapper).setOutboundHeaderNames(patterns);
return _this();
}
/**
* Specify the type of payload to be generated when the inbound HTTP request content is read by the
* {@link HttpMessageConverter}s.
* By default this value is null which means at runtime any "text" Content-Type will
* result in String while all others default to <code>byte[].class</code>.
* @param requestPayloadType The payload type.
* @return the current Spec.
*/
public S requestPayloadType(Class<?> requestPayloadType) {
this.target.setRequestPayloadTypeClass(requestPayloadType);
return _this();
}
/**
* Specify the type of payload to be generated when the inbound HTTP request content is read by the
* {@link HttpMessageConverter}s.
* By default this value is null which means at runtime any "text" Content-Type will
* result in String while all others default to <code>byte[].class</code>.
* @param requestPayloadType The payload type.
* @return the current Spec.
*/
public S requestPayloadType(ResolvableType requestPayloadType) {
this.target.setRequestPayloadType(requestPayloadType);
return _this();
}
/**
* Specify whether only the reply Message's payload should be passed in the response.
* If this is set to {@code false}, the entire Message will be used to generate the response.
* The default is {@code true}.
* @param extractReplyPayload true to extract the reply payload.
* @return the current Spec.
*/
public S extractReplyPayload(boolean extractReplyPayload) {
this.target.setExtractReplyPayload(extractReplyPayload);
return _this();
}
/**
* Specify the {@link Expression} to resolve a status code for Response to override
* the default '200 OK' or '500 Internal Server Error' for a timeout.
* @param statusCodeExpression The status code Expression.
* @return the current Spec.
* @see HttpRequestHandlingEndpointSupport#setStatusCodeExpression(Expression)
*/
public S statusCodeExpression(String statusCodeExpression) {
this.target.setStatusCodeExpressionString(statusCodeExpression);
return _this();
}
/**
* Specify the {@link Expression} to resolve a status code for Response to override
* the default '200 OK' or '500 Internal Server Error' for a timeout.
* @param statusCodeExpression The status code Expression.
* @return the current Spec.
* @see HttpRequestHandlingEndpointSupport#setStatusCodeExpression(Expression)
*/
public S statusCodeExpression(Expression statusCodeExpression) {
this.target.setStatusCodeExpression(statusCodeExpression);
return _this();
}
/**
* Specify the {@link Function} to resolve a status code for Response to override
* the default '200 OK' or '500 Internal Server Error' for a timeout.
* @param statusCodeFunction The status code {@link Function}.
* @return the current Spec.
* @see HttpRequestHandlingEndpointSupport#setStatusCodeExpression(Expression)
*/
public S statusCodeFunction(Function<Void, ?> statusCodeFunction) {
return statusCodeExpression(new FunctionExpression<>(statusCodeFunction));
}
@Override
public Map<Object, String> getComponentsToRegister() {
HeaderMapper<HttpHeaders> headerMapperToRegister =
(this.explicitHeaderMapper != null ? this.explicitHeaderMapper : this.headerMapper);
return Collections.singletonMap(headerMapperToRegister, null);
}
/**
* A fluent API for the {@link RequestMapping}.
*/
public static final class RequestMappingSpec {
private final RequestMapping requestMapping;
RequestMappingSpec(RequestMapping requestMapping) {
this.requestMapping = requestMapping;
}
/**
* The HTTP request methods to map to, narrowing the primary mapping:
* GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE.
* @param supportedMethods the {@link HttpMethod}s to use.
* @return the spec
*/
public RequestMappingSpec methods(HttpMethod... supportedMethods) {
this.requestMapping.setMethods(supportedMethods);
return this;
}
/**
* The parameters of the mapped request, narrowing the primary mapping.
* @param params the request params to map to.
* @return the spec
*/
public RequestMappingSpec params(String... params) {
this.requestMapping.setParams(params);
return this;
}
/**
* The headers of the mapped request, narrowing the primary mapping.
* @param headers the request headers to map to.
* @return the spec
*/
public RequestMappingSpec headers(String... headers) {
this.requestMapping.setHeaders(headers);
return this;
}
/**
* The consumable media types of the mapped request, narrowing the primary mapping.
* @param consumes the the media types for {@code Content-Type} header.
* @return the spec
*/
public RequestMappingSpec consumes(String... consumes) {
this.requestMapping.setConsumes(consumes);
return this;
}
/**
* The producible media types of the mapped request, narrowing the primary mapping.
* @param produces the the media types for {@code Accept} header.
* @return the spec
*/
public RequestMappingSpec produces(String... produces) {
this.requestMapping.setProduces(produces);
return this;
}
}
/**
* A fluent API for the {@link CrossOrigin}.
*/
public static final class CrossOriginSpec {
private final CrossOrigin crossOrigin = new CrossOrigin();
CrossOriginSpec() {
super();
}
/**
* List of allowed origins, e.g. {@code "http://domain1.com"}.
* <p>These values are placed in the {@code Access-Control-Allow-Origin}
* header of both the pre-flight response and the actual response.
* {@code "*"} means that all origins are allowed.
* <p>If undefined, all origins are allowed.
* @param origin the list of allowed origins.
* @return the spec
*/
public CrossOriginSpec origin(String... origin) {
this.crossOrigin.setOrigin(origin);
return this;
}
/**
* List of request headers that can be used during the actual request.
* <p>This property controls the value of the pre-flight response's
* {@code Access-Control-Allow-Headers} header.
* {@code "*"} means that all headers requested by the client are allowed.
* @param allowedHeaders the list of request headers.
* @return the spec
*/
public CrossOriginSpec allowedHeaders(String... allowedHeaders) {
this.crossOrigin.setAllowedHeaders(allowedHeaders);
return this;
}
/**
* List of response headers that the user-agent will allow the client to access.
* <p>This property controls the value of actual response's
* {@code Access-Control-Expose-Headers} header.
* @param exposedHeaders the list of response headers.
* @return the spec
*/
public CrossOriginSpec exposedHeaders(String... exposedHeaders) {
this.crossOrigin.setExposedHeaders(exposedHeaders);
return this;
}
/**
* List of supported HTTP request methods, e.g.
* {@code "{RequestMethod.GET, RequestMethod.POST}"}.
* <p>Methods specified here override those specified via {@code RequestMapping}.
* @param method the list of supported HTTP request methods
* @return the spec
*/
public CrossOriginSpec method(RequestMethod... method) {
this.crossOrigin.setMethod(method);
return this;
}
/**
* Whether the browser should include any cookies associated with the
* domain of the request being annotated.
* <p>Set to {@code "false"} if such cookies should not included.
* @param allowCredentials the {@code boolean} flag to include
* {@code Access-Control-Allow-Credentials=true} in pre-flight response or not
* @return the spec
*/
public CrossOriginSpec allowCredentials(Boolean allowCredentials) {
this.crossOrigin.setAllowCredentials(allowCredentials);
return this;
}
/**
* The maximum age (in seconds) of the cache duration for pre-flight responses.
* <p>This property controls the value of the {@code Access-Control-Max-Age}
* header in the pre-flight response.
* @param maxAge the maximum age (in seconds) of the cache duration for pre-flight responses.
* @return the spec
*/
public CrossOriginSpec maxAge(long maxAge) {
this.crossOrigin.setMaxAge(maxAge);
return this;
}
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 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.integration.http.dsl;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.integration.http.inbound.ReactiveHttpInboundEndpoint;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
/**
* The {@link HttpInboundEndpointSupportSpec} implementation for the {@link ReactiveHttpInboundEndpoint}.
*
* @author Artem Bilan
*
* @since 5.0
*/
public class ReactiveHttpInboundEndpointSpec
extends HttpInboundEndpointSupportSpec<ReactiveHttpInboundEndpointSpec, ReactiveHttpInboundEndpoint> {
ReactiveHttpInboundEndpointSpec(ReactiveHttpInboundEndpoint gateway, String... path) {
super(gateway, path);
}
public ReactiveHttpInboundEndpointSpec codecConfigurer(ServerCodecConfigurer codecConfigurer) {
this.target.setCodecConfigurer(codecConfigurer);
return this;
}
public ReactiveHttpInboundEndpointSpec requestedContentTypeResolver(
RequestedContentTypeResolver requestedContentTypeResolver) {
this.target.setRequestedContentTypeResolver(requestedContentTypeResolver);
return this;
}
public ReactiveHttpInboundEndpointSpec reactiveAdapterRegistry(ReactiveAdapterRegistry adapterRegistry) {
this.target.setReactiveAdapterRegistry(adapterRegistry);
return this;
}
}

View File

@@ -0,0 +1,330 @@
/*
* Copyright 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.integration.http.inbound;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.core.ResolvableType;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.integration.context.OrderlyShutdownCapable;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.gateway.MessagingGatewaySupport;
import org.springframework.integration.http.support.DefaultHttpHeaderMapper;
import org.springframework.integration.mapping.HeaderMapper;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
/**
* The {@link MessagingGatewaySupport} extension for HTTP Inbound endpoints
* with basic properties.
*
* @author Artem Bilan
*
* @since 5.0
*/
public class BaseHttpInboundEndpoint extends MessagingGatewaySupport implements OrderlyShutdownCapable {
protected static final boolean jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder",
HttpRequestHandlingEndpointSupport.class.getClassLoader());
protected static final boolean romeToolsPresent = ClassUtils.isPresent("com.rometools.rome.feed.atom.Feed",
HttpRequestHandlingEndpointSupport.class.getClassLoader());
protected static final List<HttpMethod> nonReadableBodyHttpMethods =
Arrays.asList(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS);
protected final boolean expectReply;
protected final AtomicInteger activeCount = new AtomicInteger();
private volatile ResolvableType requestPayloadType = null;
private volatile HeaderMapper<HttpHeaders> headerMapper = DefaultHttpHeaderMapper.inboundMapper();
private volatile boolean extractReplyPayload = true;
private volatile Expression statusCodeExpression;
private volatile EvaluationContext evaluationContext;
private volatile RequestMapping requestMapping = new RequestMapping();
private volatile Expression payloadExpression;
private volatile Map<String, Expression> headerExpressions;
private volatile CrossOrigin crossOrigin;
public BaseHttpInboundEndpoint(boolean expectReply) {
super(expectReply);
this.expectReply = expectReply;
}
/**
* Specifies a SpEL expression to evaluate in order to generate the Message payload.
* The EvaluationContext will be populated with an HttpEntity instance as the root object,
* and it may contain variables:
* <ul>
* <li><code>#pathVariables</code></li>
* <li><code>#requestParams</code></li>
* <li><code>#requestAttributes</code></li>
* <li><code>#requestHeaders</code></li>
* <li><code>#matrixVariables</code></li>
* <li><code>#cookies</code>
* </ul>
* @param payloadExpression The payload expression.
*/
public void setPayloadExpression(Expression payloadExpression) {
this.payloadExpression = payloadExpression;
}
/**
* Specifies a Map of SpEL expressions to evaluate in order to generate the Message headers.
* The keys in the map will be used as the header names. When evaluating the expression,
* the EvaluationContext will be populated with an HttpEntity instance as the root object,
* and it may contain variables:
* <ul>
* <li><code>#pathVariables</code></li>
* <li><code>#requestParams</code></li>
* <li><code>#requestAttributes</code></li>
* <li><code>#requestHeaders</code></li>
* <li><code>#matrixVariables</code></li>
* <li><code>#cookies</code>
* </ul>
* @param headerExpressions The header expressions.
*/
public void setHeaderExpressions(Map<String, Expression> headerExpressions) {
this.headerExpressions = headerExpressions;
}
/**
* Set the {@link RequestMapping} which allows you to specify a flexible RESTFul-mapping for this endpoint.
* @param requestMapping The request mapping.
*/
public void setRequestMapping(RequestMapping requestMapping) {
Assert.notNull(requestMapping, "requestMapping must not be null");
this.requestMapping = requestMapping;
}
public final RequestMapping getRequestMapping() {
return this.requestMapping;
}
/**
* Set the {@link CrossOrigin} to permit cross origin requests for this endpoint.
* @param crossOrigin the CrossOrigin config.
* @since 4.2
*/
public void setCrossOrigin(CrossOrigin crossOrigin) {
this.crossOrigin = crossOrigin;
}
public CrossOrigin getCrossOrigin() {
return this.crossOrigin;
}
protected Expression getPayloadExpression() {
return this.payloadExpression;
}
protected Map<String, Expression> getHeaderExpressions() {
return this.headerExpressions;
}
/**
* @return Whether to expect a reply.
*/
protected boolean isExpectReply() {
return this.expectReply;
}
/**
* Set the {@link HeaderMapper} to use when mapping between HTTP headers and MessageHeaders.
* @param headerMapper The header mapper.
*/
public void setHeaderMapper(HeaderMapper<HttpHeaders> headerMapper) {
Assert.notNull(headerMapper, "headerMapper must not be null");
this.headerMapper = headerMapper;
}
protected HeaderMapper<HttpHeaders> getHeaderMapper() {
return this.headerMapper;
}
/**
* Specify the type of payload to be generated when the inbound HTTP request
* content is read by the converters/encoders.
* By default this value is null which means at runtime any "text" Content-Type will
* result in String while all others default to <code>byte[].class</code>.
* @param requestPayloadType The payload type.
*/
public void setRequestPayloadTypeClass(Class<?> requestPayloadType) {
setRequestPayloadType(ResolvableType.forClass(requestPayloadType));
}
/**
* Specify the type of payload to be generated when the inbound HTTP request
* content is read by the converters/encoders.
* By default this value is null which means at runtime any "text" Content-Type will
* result in String while all others default to <code>byte[].class</code>.
* @param requestPayloadType The payload type.
*/
public void setRequestPayloadType(ResolvableType requestPayloadType) {
this.requestPayloadType = requestPayloadType;
}
protected ResolvableType getRequestPayloadType() {
return this.requestPayloadType;
}
/**
* Specify whether only the reply Message's payload should be passed in the response.
* If this is set to 'false', the entire Message will be used to generate the response.
* The default is 'true'.
* @param extractReplyPayload true to extract the reply payload.
*/
public void setExtractReplyPayload(boolean extractReplyPayload) {
this.extractReplyPayload = extractReplyPayload;
}
protected boolean getExtractReplyPayload() {
return this.extractReplyPayload;
}
/**
* Specify the {@link Expression} to resolve a status code for Response to override
* the default '200 OK' or '500 Internal Server Error' for a timeout.
* @param statusCodeExpression The status code Expression.
* @since 5.0
* @see #setStatusCodeExpression(Expression)
*/
public void setStatusCodeExpressionString(String statusCodeExpression) {
setStatusCodeExpression(EXPRESSION_PARSER.parseExpression(statusCodeExpression));
}
/**
* Specify the {@link Expression} to resolve a status code for Response to override
* the default '200 OK' or '500 Internal Server Error' for a timeout.
* <p>The {@link #statusCodeExpression} is applied only for the one-way
* {@code <http:inbound-channel-adapter/>} or when no reply (timeout) is received for
* a gateway. The {@code <http:inbound-gateway/>} (or whenever
* {@link #BaseHttpInboundEndpoint(boolean) expectReply} is true) resolves
* an {@link HttpStatus} from the
* {@link org.springframework.integration.http.HttpHeaders#STATUS_CODE} reply
* {@link Message} header.
* @param statusCodeExpression The status code Expression.
* @since 4.1
* @see #setReplyTimeout(long)
* @see HttpRequestHandlingEndpointSupport#HttpRequestHandlingEndpointSupport(boolean)
*/
public void setStatusCodeExpression(Expression statusCodeExpression) {
this.statusCodeExpression = statusCodeExpression;
}
protected Expression getStatusCodeExpression() {
return this.statusCodeExpression;
}
@Override
protected void onInit() throws Exception {
super.onInit();
validateSupportedMethods();
if (this.statusCodeExpression != null) {
this.evaluationContext = createEvaluationContext();
}
getRequestMapping().setName(getComponentName());
}
private void validateSupportedMethods() {
if (this.requestPayloadType != null
&& CollectionUtils.containsAny(nonReadableBodyHttpMethods,
Arrays.asList(getRequestMapping().getMethods()))) {
if (logger.isWarnEnabled()) {
logger.warn("The 'requestPayloadType' attribute will have no relevance for one " +
"of the specified HTTP methods '" + nonReadableBodyHttpMethods + "'");
}
}
}
protected HttpStatus evaluateHttpStatus() {
Object value = this.statusCodeExpression.getValue(this.evaluationContext);
return buildHttpStatus(value);
}
protected HttpStatus resolveHttpStatusFromHeaders(MessageHeaders headers) {
Object httpStatusFromHeader = headers.get(org.springframework.integration.http.HttpHeaders.STATUS_CODE);
return buildHttpStatus(httpStatusFromHeader);
}
private HttpStatus buildHttpStatus(Object httpStatusValue) {
HttpStatus httpStatus = null;
if (httpStatusValue instanceof HttpStatus) {
httpStatus = (HttpStatus) httpStatusValue;
}
else if (httpStatusValue instanceof Integer) {
httpStatus = HttpStatus.valueOf((Integer) httpStatusValue);
}
else if (httpStatusValue instanceof String) {
httpStatus = HttpStatus.valueOf(Integer.parseInt((String) httpStatusValue));
}
return httpStatus;
}
protected StandardEvaluationContext createEvaluationContext() {
return ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
}
@Override
public int beforeShutdown() {
stop();
return this.activeCount.get();
}
@Override
public int afterShutdown() {
return this.activeCount.get();
}
@Override
public String getComponentType() {
return (this.expectReply) ? "http:inbound-gateway" : "http:inbound-channel-adapter";
}
/**
* Checks if the request has a readable body (not a GET, HEAD, or OPTIONS request).
* @param request the HTTP request to check the method
* @return true or false if HTTP request can contain the body
*/
protected boolean isReadable(HttpRequest request) {
return !(CollectionUtils.containsInstance(nonReadableBodyHttpMethods, request.getMethod()));
}
}

View File

@@ -18,12 +18,10 @@ package org.springframework.integration.http.inbound;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
@@ -32,12 +30,10 @@ import javax.xml.transform.Source;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.expression.EvaluationContext;
import org.springframework.core.ResolvableType;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
@@ -49,23 +45,18 @@ import org.springframework.http.converter.feed.RssChannelHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.integration.MessageTimeoutException;
import org.springframework.integration.context.OrderlyShutdownCapable;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.gateway.MessagingGatewaySupport;
import org.springframework.integration.http.converter.MultipartAwareFormHttpMessageConverter;
import org.springframework.integration.http.multipart.MultipartHttpInputMessage;
import org.springframework.integration.http.support.DefaultHttpHeaderMapper;
import org.springframework.integration.mapping.HeaderMapper;
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
import org.springframework.integration.support.json.JacksonPresent;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.MessagingException;
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;
@@ -108,52 +99,21 @@ import org.springframework.web.servlet.HandlerMapping;
* @author Gary Russell
* @author Artem Bilan
* @author Biju Kunjummen
*
* @since 2.0
*/
public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewaySupport
implements OrderlyShutdownCapable {
public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInboundEndpoint {
private static final boolean jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder",
HttpRequestHandlingEndpointSupport.class.getClassLoader());
private final List<HttpMessageConverter<?>> defaultMessageConverters = new ArrayList<>();
private static final boolean romeToolsPresent = ClassUtils.isPresent("com.rometools.rome.feed.atom.Feed",
HttpRequestHandlingEndpointSupport.class.getClassLoader());
private static final List<HttpMethod> nonReadableBodyHttpMethods =
Arrays.asList(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS);
private final List<HttpMessageConverter<?>> defaultMessageConverters = new ArrayList<HttpMessageConverter<?>>();
private final boolean expectReply;
private volatile List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
private volatile RequestMapping requestMapping = new RequestMapping();
private volatile CrossOrigin crossOrigin;
private volatile Class<?> requestPayloadType = null;
private volatile List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
private volatile boolean convertersMerged;
private volatile boolean mergeWithDefaultConverters = false;
private volatile HeaderMapper<HttpHeaders> headerMapper = DefaultHttpHeaderMapper.inboundMapper();
private volatile boolean extractReplyPayload = true;
private volatile MultipartResolver multipartResolver;
private volatile Expression payloadExpression;
private volatile Map<String, Expression> headerExpressions;
private volatile Expression statusCodeExpression;
private volatile EvaluationContext evaluationContext;
private final AtomicInteger activeCount = new AtomicInteger();
/**
* Construct a gateway that will wait for the {@link #setReplyTimeout(long)
* replyTimeout} for a reply; if the timeout is exceeded a '500 Internal Server Error'
@@ -179,7 +139,6 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
*/
public HttpRequestHandlingEndpointSupport(boolean expectReply) {
super(expectReply);
this.expectReply = expectReply;
this.defaultMessageConverters.add(new MultipartAwareFormHttpMessageConverter());
this.defaultMessageConverters.add(new ByteArrayHttpMessageConverter());
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
@@ -210,50 +169,6 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
}
}
/**
* @return Whether to expect a reply.
*/
protected boolean isExpectReply() {
return this.expectReply;
}
/**
* Specifies a SpEL expression to evaluate in order to generate the Message payload.
* The EvaluationContext will be populated with an HttpEntity instance as the root object,
* and it may contain variables:
* <ul>
* <li><code>#pathVariables</code></li>
* <li><code>#requestParams</code></li>
* <li><code>#requestAttributes</code></li>
* <li><code>#requestHeaders</code></li>
* <li><code>#matrixVariables</code></li>
* <li><code>#cookies</code>
* </ul>
* @param payloadExpression The payload expression.
*/
public void setPayloadExpression(Expression payloadExpression) {
this.payloadExpression = payloadExpression;
}
/**
* Specifies a Map of SpEL expressions to evaluate in order to generate the Message headers.
* The keys in the map will be used as the header names. When evaluating the expression,
* the EvaluationContext will be populated with an HttpEntity instance as the root object,
* and it may contain variables:
* <ul>
* <li><code>#pathVariables</code></li>
* <li><code>#requestParams</code></li>
* <li><code>#requestAttributes</code></li>
* <li><code>#requestHeaders</code></li>
* <li><code>#matrixVariables</code></li>
* <li><code>#cookies</code>
* </ul>
* @param headerExpressions The header expressions.
*/
public void setHeaderExpressions(Map<String, Expression> headerExpressions) {
this.headerExpressions = headerExpressions;
}
/**
* Set the message body converters to use. These converters are used to convert from and to HTTP requests and
* responses.
@@ -283,60 +198,6 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
this.mergeWithDefaultConverters = mergeWithDefaultConverters;
}
/**
* Set the {@link HeaderMapper} to use when mapping between HTTP headers and MessageHeaders.
* @param headerMapper The header mapper.
*/
public void setHeaderMapper(HeaderMapper<HttpHeaders> headerMapper) {
Assert.notNull(headerMapper, "headerMapper must not be null");
this.headerMapper = headerMapper;
}
/**
* Set the {@link RequestMapping} which allows you to specify a flexible RESTFul-mapping for this endpoint.
* @param requestMapping The request mapping.
*/
public void setRequestMapping(RequestMapping requestMapping) {
Assert.notNull(requestMapping, "requestMapping must not be null");
this.requestMapping = requestMapping;
}
public final RequestMapping getRequestMapping() {
return this.requestMapping;
}
/**
* Set the {@link CrossOrigin} to permit cross origin requests for this endpoint.
* @param crossOrigin the CrossOrigin config.
* @since 4.2
*/
public void setCrossOrigin(CrossOrigin crossOrigin) {
this.crossOrigin = crossOrigin;
}
public CrossOrigin getCrossOrigin() {
return this.crossOrigin;
}
/**
* Specify the type of payload to be generated when the inbound HTTP request content is read by the
* {@link HttpMessageConverter}s. By default this value is null which means at runtime any "text" Content-Type will
* result in String while all others default to <code>byte[].class</code>.
* @param requestPayloadType The payload type.
*/
public void setRequestPayloadType(Class<?> requestPayloadType) {
this.requestPayloadType = requestPayloadType;
}
/**
* Specify whether only the reply Message's payload should be passed in the response. If this is set to 'false', the
* entire Message will be used to generate the response. The default is 'true'.
* @param extractReplyPayload true to extract the reply payload.
*/
public void setExtractReplyPayload(boolean extractReplyPayload) {
this.extractReplyPayload = extractReplyPayload;
}
/**
* Specify the {@link MultipartResolver} to use when checking requests. If no resolver is provided, the
@@ -347,40 +208,6 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
public void setMultipartResolver(MultipartResolver multipartResolver) {
this.multipartResolver = multipartResolver;
}
/**
* Specify the {@link Expression} to resolve a status code for Response to override
* the default '200 OK' or '500 Internal Server Error' for a timeout.
* @param statusCodeExpression The status code Expression.
* @since 5.0
* @see #setStatusCodeExpression(Expression)
*/
public void setStatusCodeExpressionString(String statusCodeExpression) {
setStatusCodeExpression(EXPRESSION_PARSER.parseExpression(statusCodeExpression));
}
/**
* Specify the {@link Expression} to resolve a status code for Response to override
* the default '200 OK' or '500 Internal Server Error' for a timeout.
* <p>The {@link #statusCodeExpression} is applied only for the one-way
* {@code <http:inbound-channel-adapter/>} or when no reply (timeout) is received for
* a gateway. The {@code <http:inbound-gateway/>} (or whenever
* {@link #HttpRequestHandlingEndpointSupport(boolean) expectReply} is true) resolves
* an {@link HttpStatus} from the
* {@link org.springframework.integration.http.HttpHeaders#STATUS_CODE} reply
* {@link Message} header.
* @param statusCodeExpression The status code Expression.
* @since 4.1
* @see #setReplyTimeout(long)
* @see HttpRequestHandlingEndpointSupport#HttpRequestHandlingEndpointSupport(boolean)
*/
public void setStatusCodeExpression(Expression statusCodeExpression) {
this.statusCodeExpression = statusCodeExpression;
}
@Override
public String getComponentType() {
return (this.expectReply) ? "http:inbound-gateway" : "http:inbound-channel-adapter";
}
/**
* Locates the {@link MultipartResolver} bean based on the default name defined by the
@@ -391,7 +218,7 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
@Override
protected void onInit() throws Exception {
super.onInit();
BeanFactory beanFactory = this.getBeanFactory();
BeanFactory beanFactory = getBeanFactory();
if (this.multipartResolver == null && beanFactory != null) {
try {
MultipartResolver multipartResolver = beanFactory.getBean(
@@ -412,11 +239,6 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
if (this.messageConverters.size() == 0 || (this.mergeWithDefaultConverters && !this.convertersMerged)) {
this.messageConverters.addAll(this.defaultMessageConverters);
}
this.validateSupportedMethods();
if (this.statusCodeExpression != null) {
this.evaluationContext = createEvaluationContext();
}
}
/**
@@ -437,7 +259,7 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
}
}
@SuppressWarnings({"rawtypes", "unchecked"})
@SuppressWarnings({ "rawtypes", "unchecked" })
private Message<?> actualDoHandleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
throws IOException {
this.activeCount.incrementAndGet();
@@ -445,8 +267,8 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
ServletServerHttpRequest request = this.prepareRequest(servletRequest);
Object requestBody = null;
if (this.isReadable(request)) {
requestBody = this.extractRequestBody(request);
if (isReadable(request)) {
requestBody = extractRequestBody(request);
}
HttpEntity httpEntity = new HttpEntity(requestBody, request.getHeaders());
@@ -490,14 +312,14 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
evaluationContext.setVariable("matrixVariables", matrixVariables);
}
Map<String, Object> headers = this.headerMapper.toHeaders(request.getHeaders());
Map<String, Object> headers = getHeaderMapper().toHeaders(request.getHeaders());
Object payload = null;
if (this.payloadExpression != null) {
if (getPayloadExpression() != null) {
// create payload based on SpEL
payload = this.payloadExpression.getValue(evaluationContext);
payload = getPayloadExpression().getValue(evaluationContext);
}
if (!CollectionUtils.isEmpty(this.headerExpressions)) {
for (Entry<String, Expression> entry : this.headerExpressions.entrySet()) {
if (!CollectionUtils.isEmpty(getHeaderExpressions())) {
for (Entry<String, Expression> entry : getHeaderExpressions().entrySet()) {
String headerName = entry.getKey();
Expression headerExpression = entry.getValue();
Object headerValue = headerExpression.getValue(evaluationContext);
@@ -541,11 +363,11 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
reply = this.sendAndReceiveMessage(message);
}
catch (MessageTimeoutException e) {
if (this.statusCodeExpression != null) {
if (getStatusCodeExpression() != null) {
reply = getMessageBuilderFactory().withPayload(e.getMessage())
.setHeader(org.springframework.integration.http.HttpHeaders.STATUS_CODE,
evaluateHttpStatus())
.build();
.setHeader(org.springframework.integration.http.HttpHeaders.STATUS_CODE,
evaluateHttpStatus())
.build();
}
else {
reply = getMessageBuilderFactory().withPayload(e.getMessage())
@@ -583,23 +405,22 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
* @return The message payload (if {@link #extractReplyPayload}) otherwise the message.
*/
protected final Object setupResponseAndConvertReply(ServletServerHttpResponse response, Message<?> replyMessage) {
this.headerMapper.fromHeaders(replyMessage.getHeaders(), response.getHeaders());
getHeaderMapper().fromHeaders(replyMessage.getHeaders(), response.getHeaders());
HttpStatus httpStatus = this.resolveHttpStatusFromHeaders(replyMessage.getHeaders());
if (httpStatus != null) {
response.setStatusCode(httpStatus);
}
Object reply = replyMessage;
if (this.extractReplyPayload) {
if (getExtractReplyPayload()) {
reply = replyMessage.getPayload();
}
return reply;
}
protected void setStatusCodeIfNeeded(ServletServerHttpResponse response) {
if (this.statusCodeExpression != null) {
protected void setStatusCodeIfNeeded(ServerHttpResponse response) {
if (getStatusCodeExpression() != null) {
HttpStatus httpStatus = evaluateHttpStatus();
if (httpStatus != null) {
response.setStatusCode(httpStatus);
@@ -607,14 +428,6 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
}
}
private HttpStatus evaluateHttpStatus() {
if (this.evaluationContext == null) {
this.evaluationContext = createEvaluationContext();
}
Object value = this.statusCodeExpression.getValue(this.evaluationContext);
return buildHttpStatus(value);
}
/**
* Prepares an instance of {@link ServletServerHttpRequest} from the raw
* {@link HttpServletRequest}. Also converts the request into a multipart request to
@@ -634,13 +447,6 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
return new ServletServerHttpRequest(servletRequest);
}
/**
* Checks if the request has a readable body (not a GET, HEAD, or OPTIONS request).
*/
private boolean isReadable(ServletServerHttpRequest request) {
return !(CollectionUtils.containsInstance(nonReadableBodyHttpMethods, request.getMethod()));
}
/**
* Clean up any resources used by the given multipart request (if any).
* @param request current HTTP request
@@ -666,16 +472,21 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
return convertedMap;
}
@SuppressWarnings({"unchecked", "rawtypes"})
@SuppressWarnings({ "unchecked", "rawtypes" })
private Object extractRequestBody(ServletServerHttpRequest request) throws IOException {
MediaType contentType = request.getHeaders().getContentType();
if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
Class<?> expectedType = this.requestPayloadType;
if (expectedType == null) {
expectedType = ("text".equals(contentType.getType())) ? String.class : byte[].class;
ResolvableType requestPayloadType = getRequestPayloadType();
Class<?> expectedType;
if (requestPayloadType == null) {
expectedType = "text".equals(contentType.getType()) ? String.class : byte[].class;
}
else {
expectedType = requestPayloadType.resolve();
}
for (HttpMessageConverter<?> converter : this.messageConverters) {
if (converter.canRead(expectedType, contentType)) {
return converter.read((Class) expectedType, request);
@@ -686,50 +497,4 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
+ expectedType.getName() + "] and content type [" + contentType + "]");
}
private HttpStatus resolveHttpStatusFromHeaders(MessageHeaders headers) {
Object httpStatusFromHeader = headers.get(org.springframework.integration.http.HttpHeaders.STATUS_CODE);
return buildHttpStatus(httpStatusFromHeader);
}
private HttpStatus buildHttpStatus(Object httpStatusValue) {
HttpStatus httpStatus = null;
if (httpStatusValue instanceof HttpStatus) {
httpStatus = (HttpStatus) httpStatusValue;
}
else if (httpStatusValue instanceof Integer) {
httpStatus = HttpStatus.valueOf((Integer) httpStatusValue);
}
else if (httpStatusValue instanceof String) {
httpStatus = HttpStatus.valueOf(Integer.parseInt((String) httpStatusValue));
}
return httpStatus;
}
protected StandardEvaluationContext createEvaluationContext() {
return ExpressionUtils.createStandardEvaluationContext(this.getBeanFactory());
}
private void validateSupportedMethods() {
if (this.requestPayloadType != null
&& CollectionUtils.containsAny(nonReadableBodyHttpMethods,
Arrays.asList(this.requestMapping.getMethods()))) {
if (logger.isWarnEnabled()) {
logger.warn("The 'requestPayloadType' attribute will have no relevance for one " +
"of the specified HTTP methods '" + nonReadableBodyHttpMethods + "'");
}
}
}
@Override
public int beforeShutdown() {
stop();
return this.activeCount.get();
}
@Override
public int afterShutdown() {
return this.activeCount.get();
}
}

View File

@@ -46,7 +46,7 @@ import org.springframework.web.HttpRequestHandler;
* The default supported request methods are GET and POST, but the list of values can be configured with the
* {@link RequestMapping#methods} property. The payload generated from a GET request (or HEAD or OPTIONS if supported) will
* be a {@link MultiValueMap} containing the parameter values. For a request containing a body (e.g. a POST), the type
* of the payload is determined by the {@link #setRequestPayloadType(Class) request payload type}.
* of the payload is determined by the {@link #setRequestPayloadTypeClass(Class)} request payload type}.
* <p>
* If the HTTP request is a multipart and a "multipartResolver" bean has been defined in the context, then it will be
* converted by the {@link MultipartAwareFormHttpMessageConverter} as long as the default message converters have not

View File

@@ -0,0 +1,58 @@
/*
* Copyright 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.integration.http.inbound;
import org.springframework.core.Ordered;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.HandlerResultHandler;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* A {@link HandlerResultHandler} implementation to handle the result of the
* {@link ReactiveHttpInboundEndpoint} execution. Actually just return the
* {@code result.getReturnValue()} which essentially is expected {@code Mono<Void>}.
*
* @author Artem Bilan
*
* @since 5.0
*
* @see ReactiveHttpInboundEndpoint
*/
public class IntegrationHandlerResultHandler implements HandlerResultHandler, Ordered {
@Override
public boolean supports(HandlerResult result) {
Object handler = result.getHandler();
return handler instanceof HandlerMethod
&& ReactiveHttpInboundEndpoint.class.isAssignableFrom(((HandlerMethod) handler).getBeanType());
}
@Override
@SuppressWarnings("unchecked")
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
return (Mono<Void>) result.getReturnValue();
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2016 the original author or authors.
* Copyright 2013-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.
@@ -50,7 +50,7 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl
* elements, if there is none registered, yet. However it can be configured as a regular
* bean with appropriate configuration for {@link RequestMappingHandlerMapping}.
* It is recommended to have only one similar bean in the application context using the 'id'
* {@link org.springframework.integration.http.support.HttpContextUtils#HANDLER_MAPPING_BEAN_NAME}.
* {@link org.springframework.integration.http.support.HttpContextUtils#REACTIVE_HANDLER_MAPPING_BEAN_NAME}.
* <p>
* In most cases, Spring MVC offers to configure Request Mapping via
* {@code org.springframework.stereotype.Controller} and
@@ -69,7 +69,9 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl
* (e.g. Spring Integration XML) configurations.
*
* @author Artem Bilan
*
* @since 3.0
*
* @see RequestMapping
* @see RequestMappingHandlerMapping
*/
@@ -113,7 +115,7 @@ public final class IntegrationRequestMappingHandlerMapping extends RequestMappin
if (handler instanceof String) {
handler = this.getApplicationContext().getBean((String) handler);
}
RequestMappingInfo mapping = this.getMappingForEndpoint((HttpRequestHandlingEndpointSupport) handler);
RequestMappingInfo mapping = this.getMappingForEndpoint((BaseHttpInboundEndpoint) handler);
if (mapping != null) {
registerMapping(mapping, handler, HANDLE_REQUEST_METHOD);
}
@@ -121,7 +123,7 @@ public final class IntegrationRequestMappingHandlerMapping extends RequestMappin
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {
CrossOrigin crossOrigin = ((HttpRequestHandlingEndpointSupport) handler).getCrossOrigin();
CrossOrigin crossOrigin = ((BaseHttpInboundEndpoint) handler).getCrossOrigin();
if (crossOrigin != null) {
CorsConfiguration config = new CorsConfiguration();
for (String origin : crossOrigin.getOrigin()) {
@@ -154,7 +156,7 @@ public final class IntegrationRequestMappingHandlerMapping extends RequestMappin
}
}
}
return config;
return config.applyPermitDefaultValues();
}
return null;
}
@@ -164,7 +166,7 @@ public final class IntegrationRequestMappingHandlerMapping extends RequestMappin
* 'Spring Integration HTTP Inbound Endpoint' {@link RequestMapping}.
* @see RequestMappingHandlerMapping#getMappingForMethod
*/
private RequestMappingInfo getMappingForEndpoint(HttpRequestHandlingEndpointSupport endpoint) {
private RequestMappingInfo getMappingForEndpoint(BaseHttpInboundEndpoint endpoint) {
final RequestMapping requestMapping = endpoint.getRequestMapping();
if (ObjectUtils.isEmpty(requestMapping.getPathPatterns())) {

View File

@@ -0,0 +1,461 @@
/*
* Copyright 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.integration.http.inbound;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.reactivestreams.Publisher;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.integration.gateway.MessagingGatewaySupport;
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.server.NotAcceptableStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
import org.springframework.web.server.WebHandler;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* A {@link MessagingGatewaySupport} implementation for Spring WebFlux
* HTTP requests execution.
*
* @author Artem Bilan
*
* @since 5.0
*
* @see org.springframework.web.reactive.result.HandlerResultHandlerSupport
* @see org.springframework.web.reactive.config.EnableWebFlux
*/
public class ReactiveHttpInboundEndpoint extends BaseHttpInboundEndpoint implements WebHandler {
private static final MediaType MEDIA_TYPE_APPLICATION_ALL = new MediaType("application");
private ServerCodecConfigurer codecConfigurer = ServerCodecConfigurer.create();
private RequestedContentTypeResolver requestedContentTypeResolver = new HeaderContentTypeResolver();
private ReactiveAdapterRegistry adapterRegistry = new ReactiveAdapterRegistry();
public ReactiveHttpInboundEndpoint() {
this(true);
}
public ReactiveHttpInboundEndpoint(boolean expectReply) {
super(expectReply);
}
/**
* A {@link ServerCodecConfigurer} for the request readers and response writers.
* By default the {@link ServerCodecConfigurer#create()} factory is used.
* @param codecConfigurer the {@link ServerCodecConfigurer} to use.
*/
public void setCodecConfigurer(ServerCodecConfigurer codecConfigurer) {
Assert.notNull(codecConfigurer, "'codecConfigurer' must not be null");
this.codecConfigurer = codecConfigurer;
}
/**
* A strategy to resolve the requested media types for a {@code ServerWebExchange}.
* A {@link HeaderContentTypeResolver} is used by default.
* @param requestedContentTypeResolver the {@link RequestedContentTypeResolver} to use.
*/
public void setRequestedContentTypeResolver(RequestedContentTypeResolver requestedContentTypeResolver) {
Assert.notNull(requestedContentTypeResolver, "'requestedContentTypeResolver' must not be null");
this.requestedContentTypeResolver = requestedContentTypeResolver;
}
/**
* A registry of adapters to adapt a Reactive Streams {@link Publisher} to/from.
* @param adapterRegistry the {@link ReactiveAdapterRegistry} to use.
*/
public void setReactiveAdapterRegistry(ReactiveAdapterRegistry adapterRegistry) {
Assert.notNull(adapterRegistry, "'adapterRegistry' must not be null");
this.adapterRegistry = adapterRegistry;
}
@Override
public String getComponentType() {
return super.getComponentType().replaceFirst("(http:)", "$1reactive-");
}
@Override
protected void onInit() throws Exception {
super.onInit();
}
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
return Mono.defer(() -> {
if (isRunning()) {
return doHandle(exchange);
}
else {
return serviceUnavailableResponse(exchange);
}
});
}
@SuppressWarnings("unchecked")
private Mono<Void> doHandle(ServerWebExchange exchange) {
return extractRequestBody(exchange)
.doOnSubscribe(s -> this.activeCount.incrementAndGet())
.map(body -> new HttpEntity<>(body, exchange.getRequest().getHeaders()))
.map(entity -> buildMessage(entity, exchange))
.flatMap(requestMessage -> {
if (this.expectReply) {
return sendAndReceiveMessageReactive(requestMessage)
.flatMap(replyMessage -> populateResponse(exchange, replyMessage));
}
else {
send(requestMessage);
return setStatusCode(exchange);
}
})
.doOnTerminate((e, t) -> this.activeCount.decrementAndGet());
}
@SuppressWarnings("unchecked")
private <T> Mono<T> extractRequestBody(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
if (isReadable(request)) {
MediaType contentType;
if (request.getHeaders().getContentType() == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
else {
contentType = request.getHeaders().getContentType();
}
if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType)) {
return (Mono<T>) exchange.getFormData();
}
else if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType)) {
return (Mono<T>) exchange.getMultipartData();
}
else {
ResolvableType bodyType = getRequestPayloadType();
if (bodyType == null) {
bodyType =
"text".equals(contentType.getType())
? ResolvableType.forClass(String.class)
: ResolvableType.forClass(byte[].class);
}
Class<?> resolvedType = bodyType.resolve();
ReactiveAdapter adapter = (resolvedType != null ? this.adapterRegistry.getAdapter(resolvedType) : null);
ResolvableType elementType = (adapter != null ? bodyType.getGeneric() : bodyType);
HttpMessageReader<?> httpMessageReader = this.codecConfigurer
.getReaders()
.stream()
.filter(reader -> reader.canRead(elementType, contentType))
.findFirst()
.orElseThrow(() -> new UnsupportedMediaTypeStatusException(
"Could not convert request: no suitable HttpMessageReader found for expected type ["
+ elementType + "] and content type [" + contentType + "]"));
Map<String, Object> readHints = Collections.emptyMap();
if (adapter != null && adapter.isMultiValue()) {
Flux<?> flux = httpMessageReader.read(bodyType, elementType, request, response, readHints);
return (Mono<T>) Mono.just(adapter.fromPublisher(flux));
}
else {
Mono<?> mono = httpMessageReader.readMono(bodyType, elementType, request, response, readHints);
if (adapter != null) {
return (Mono<T>) Mono.just(adapter.fromPublisher(mono));
}
else {
return (Mono<T>) mono;
}
}
}
}
else {
return (Mono<T>) Mono.just(exchange.getRequest().getQueryParams());
}
}
@SuppressWarnings("unchecked")
private Message<?> buildMessage(HttpEntity<?> httpEntity, ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
HttpHeaders requestHeaders = request.getHeaders();
Map<String, Object> exchangeAttributes = exchange.getAttributes();
StandardEvaluationContext evaluationContext = createEvaluationContext();
evaluationContext.setVariable("requestAttributes", exchangeAttributes);
MultiValueMap<String, String> requestParams = request.getQueryParams();
evaluationContext.setVariable("requestParams", requestParams);
evaluationContext.setVariable("requestHeaders", requestHeaders);
if (!CollectionUtils.isEmpty(request.getCookies())) {
evaluationContext.setVariable("cookies", request.getCookies());
}
Map<String, String> pathVariables =
(Map<String, String>) exchangeAttributes.get(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(pathVariables)) {
evaluationContext.setVariable("pathVariables", pathVariables);
}
Map<String, MultiValueMap<String, String>> matrixVariables =
(Map<String, MultiValueMap<String, String>>) exchangeAttributes.get(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(matrixVariables)) {
evaluationContext.setVariable("matrixVariables", matrixVariables);
}
evaluationContext.setRootObject(httpEntity);
Object payload;
if (getPayloadExpression() != null) {
payload = getPayloadExpression().getValue(evaluationContext);
if (payload == null) {
throw new IllegalStateException("The payload expression '" + getPayloadExpression().getExpressionString()
+ "' returned null.");
}
}
else {
payload = httpEntity.getBody();
}
Map<String, Object> headers = getHeaderMapper().toHeaders(request.getHeaders());
if (!CollectionUtils.isEmpty(getHeaderExpressions())) {
for (Map.Entry<String, Expression> entry : getHeaderExpressions().entrySet()) {
String headerName = entry.getKey();
Expression headerExpression = entry.getValue();
Object headerValue = headerExpression.getValue(evaluationContext);
if (headerValue != null) {
headers.put(headerName, headerValue);
}
}
}
AbstractIntegrationMessageBuilder<?> messageBuilder;
if (payload instanceof Message<?>) {
messageBuilder =
getMessageBuilderFactory()
.fromMessage((Message<?>) payload)
.copyHeadersIfAbsent(headers);
}
else {
messageBuilder =
getMessageBuilderFactory()
.withPayload(payload)
.copyHeaders(headers);
}
return messageBuilder
.setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_URL, request.getURI().toString())
.setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_METHOD, request.getMethod().toString())
.setHeader(org.springframework.integration.http.HttpHeaders.USER_PRINCIPAL, exchange.getPrincipal().block())
.build();
}
private Mono<Void> populateResponse(ServerWebExchange exchange, Message<?> replyMessage) {
ServerHttpResponse response = exchange.getResponse();
getHeaderMapper().fromHeaders(replyMessage.getHeaders(), response.getHeaders());
Object responseContent = replyMessage;
if (getExtractReplyPayload()) {
responseContent = replyMessage.getPayload();
}
if (responseContent instanceof HttpStatus) {
response.setStatusCode((HttpStatus) responseContent);
return response.setComplete();
}
else {
HttpStatus httpStatus = resolveHttpStatusFromHeaders(replyMessage.getHeaders());
if (httpStatus != null) {
response.setStatusCode(httpStatus);
}
return writeResponseBody(exchange, responseContent);
}
}
@SuppressWarnings("unchecked")
private Mono<Void> writeResponseBody(ServerWebExchange exchange, Object body) {
ResolvableType bodyType = ResolvableType.forInstance(body);
ReactiveAdapter adapter = this.adapterRegistry.getAdapter(bodyType.resolve(), body);
Publisher<?> publisher;
ResolvableType elementType;
if (adapter != null) {
publisher = adapter.toPublisher(body);
ResolvableType genericType = bodyType.getGeneric(0);
elementType = getElementType(adapter, genericType);
}
else {
publisher = Mono.justOrEmpty(body);
elementType = bodyType;
}
if (void.class == elementType.getRawClass() || Void.class == elementType.getRawClass()) {
return Mono.from((Publisher<Void>) publisher);
}
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(bodyType);
MediaType bestMediaType = selectMediaType(exchange, () -> producibleMediaTypes);
if (bestMediaType != null) {
for (HttpMessageWriter<?> writer : this.codecConfigurer.getWriters()) {
if (writer.canWrite(bodyType, bestMediaType)) {
return ((HttpMessageWriter<Object>) writer).write(publisher, elementType,
bestMediaType, exchange.getResponse(), Collections.emptyMap());
}
}
}
else {
if (producibleMediaTypes.isEmpty()) {
return Mono.error(new IllegalStateException("No HttpMessageWriters for response type: " + bodyType));
}
}
return Mono.error(new NotAcceptableStatusException(producibleMediaTypes));
}
private ResolvableType getElementType(ReactiveAdapter adapter, ResolvableType genericType) {
if (adapter.isNoValue()) {
return ResolvableType.forClass(Void.class);
}
else if (genericType != ResolvableType.NONE) {
return genericType;
}
else {
return ResolvableType.forClass(Object.class);
}
}
private List<MediaType> getProducibleMediaTypes(ResolvableType elementType) {
return this.codecConfigurer.getWriters()
.stream()
.filter(converter -> converter.canWrite(elementType, null))
.flatMap(converter -> converter.getWritableMediaTypes().stream())
.collect(Collectors.toList());
}
private MediaType selectMediaType(ServerWebExchange exchange, Supplier<List<MediaType>> producibleTypesSupplier) {
List<MediaType> acceptableTypes = getAcceptableTypes(exchange);
List<MediaType> producibleTypes = getProducibleTypes(exchange, producibleTypesSupplier);
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<>();
for (MediaType acceptable : acceptableTypes) {
for (MediaType producible : producibleTypes) {
if (acceptable.isCompatibleWith(producible)) {
compatibleMediaTypes.add(selectMoreSpecificMediaType(acceptable, producible));
}
}
}
List<MediaType> result = new ArrayList<>(compatibleMediaTypes);
MediaType.sortBySpecificityAndQuality(result);
for (MediaType mediaType : result) {
if (mediaType.isConcrete()) {
return mediaType;
}
else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION_ALL)) {
return MediaType.APPLICATION_OCTET_STREAM;
}
}
return null;
}
private List<MediaType> getAcceptableTypes(ServerWebExchange exchange) {
List<MediaType> mediaTypes = this.requestedContentTypeResolver.resolveMediaTypes(exchange);
return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes);
}
@SuppressWarnings("unchecked")
private List<MediaType> getProducibleTypes(ServerWebExchange exchange,
Supplier<List<MediaType>> producibleTypesSupplier) {
Set<MediaType> mediaTypes = exchange.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
return (mediaTypes != null ? new ArrayList<>(mediaTypes) : producibleTypesSupplier.get());
}
private MediaType selectMoreSpecificMediaType(MediaType acceptable, MediaType producible) {
producible = producible.copyQualityValue(acceptable);
Comparator<MediaType> comparator = MediaType.SPECIFICITY_COMPARATOR;
return (comparator.compare(acceptable, producible) <= 0 ? acceptable : producible);
}
private Mono<Void> setStatusCode(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
if (getStatusCodeExpression() != null) {
HttpStatus httpStatus = evaluateHttpStatus();
if (httpStatus != null) {
response.setStatusCode(httpStatus);
}
}
return response.setComplete();
}
private Mono<Void> serviceUnavailableResponse(ServerWebExchange exchange) {
if (logger.isDebugEnabled()) {
logger.debug("Endpoint is stopped; returning status " + HttpStatus.SERVICE_UNAVAILABLE);
}
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.SERVICE_UNAVAILABLE);
return response.writeWith(
Mono.just(response.bufferFactory()
.wrap("Endpoint is stopped".getBytes())));
}
}

View File

@@ -0,0 +1,170 @@
/*
* Copyright 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.integration.http.inbound;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.integration.http.support.HttpContextUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.result.condition.NameValueExpression;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebHandler;
/**
* The {@link org.springframework.web.servlet.HandlerMapping} implementation that
* detects and registers {@link org.springframework.web.servlet.mvc.method.RequestMappingInfo}s for
* {@link HttpRequestHandlingEndpointSupport} from a Spring Integration HTTP configuration
* of {@code <inbound-channel-adapter/>} and {@code <inbound-gateway/>} elements.
* <p>
* This class is automatically configured as a bean in the application context during the
* parsing phase of the {@code <reactive-inbound-gateway/>}
* elements, if there is none registered, yet. However it can be configured as a regular
* bean with appropriate configuration for
* {@link org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping}.
* It is recommended to have only one similar bean in the application context using the 'id'
* {@link org.springframework.integration.http.support.HttpContextUtils#REACTIVE_HANDLER_MAPPING_BEAN_NAME}.
* <p>
* In most cases, Spring MVC offers to configure Request Mapping via
* {@code org.springframework.stereotype.Controller} and
* {@link org.springframework.web.bind.annotation.RequestMapping}.
* That's why Spring MVC's Handler Mapping infrastructure relies on
* {@link org.springframework.web.method.HandlerMethod}, as different methods at the same
* {@code org.springframework.stereotype.Controller} user-class may have their own
* {@link org.springframework.web.bind.annotation.RequestMapping}.
* On the other side, all Spring Integration HTTP Inbound Endpoints are configured on
* the basis of the same {@link HttpRequestHandlingEndpointSupport} class and there is no
* single {@link org.springframework.web.reactive.result.method.RequestMappingInfo} configuration without
* {@link org.springframework.web.method.HandlerMethod} in Spring MVC.
* Accordingly {@link ReactiveIntegrationRequestMappingHandlerMapping} is a
* {@link org.springframework.web.reactive.HandlerMapping}
* compromise implementation between method-level annotations and component-level
* (e.g. Spring Integration XML) configurations.
*
* @author Artem Bilan
*
* @since 5.0
*
* @see RequestMapping
* @see RequestMappingHandlerMapping
*/
public class ReactiveIntegrationRequestMappingHandlerMapping extends RequestMappingHandlerMapping
implements ApplicationListener<ContextRefreshedEvent> {
private static final Method HANDLER_METHOD = ReflectionUtils.findMethod(WebHandler.class,
"handle", ServerWebExchange.class);
private final AtomicBoolean initialized = new AtomicBoolean();
@Override
protected boolean isHandler(Class<?> beanType) {
return ReactiveHttpInboundEndpoint.class.isAssignableFrom(beanType);
}
@Override
protected void detectHandlerMethods(Object handler) {
if (handler instanceof String) {
handler = getApplicationContext().getBean((String) handler);
}
RequestMappingInfo mapping = getMappingForEndpoint((ReactiveHttpInboundEndpoint) handler);
if (mapping != null) {
registerMapping(mapping, handler, HANDLER_METHOD);
}
}
/**
* Create a {@link RequestMappingInfo} from
* a Spring Integration Reactive HTTP Inbound Endpoint {@link RequestMapping}.
* @see RequestMappingHandlerMapping#getMappingForMethod
*/
private RequestMappingInfo getMappingForEndpoint(ReactiveHttpInboundEndpoint endpoint) {
org.springframework.web.bind.annotation.RequestMapping requestMappingAnnotation =
HttpContextUtils.convertRequestMappingToAnnotation(endpoint.getRequestMapping());
if (requestMappingAnnotation != null) {
return createRequestMappingInfo(requestMappingAnnotation, getCustomTypeCondition(endpoint.getClass()));
}
else {
return null;
}
}
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {
CrossOrigin crossOrigin = ((BaseHttpInboundEndpoint) handler).getCrossOrigin();
if (crossOrigin != null) {
CorsConfiguration config = new CorsConfiguration();
for (String origin : crossOrigin.getOrigin()) {
config.addAllowedOrigin(origin);
}
for (RequestMethod requestMethod : crossOrigin.getMethod()) {
config.addAllowedMethod(requestMethod.name());
}
for (String header : crossOrigin.getAllowedHeaders()) {
config.addAllowedHeader(header);
}
for (String header : crossOrigin.getExposedHeaders()) {
config.addExposedHeader(header);
}
if (crossOrigin.getAllowCredentials() != null) {
config.setAllowCredentials(crossOrigin.getAllowCredentials());
}
if (crossOrigin.getMaxAge() != -1) {
config.setMaxAge(crossOrigin.getMaxAge());
}
if (CollectionUtils.isEmpty(config.getAllowedMethods())) {
for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) {
config.addAllowedMethod(allowedMethod.name());
}
}
if (CollectionUtils.isEmpty(config.getAllowedHeaders())) {
for (NameValueExpression<String> headerExpression : mappingInfo.getHeadersCondition().getExpressions()) {
if (!headerExpression.isNegated()) {
config.addAllowedHeader(headerExpression.getName());
}
}
}
return config.applyPermitDefaultValues();
}
return null;
}
@Override
public void afterPropertiesSet() {
// No-op in favor of onApplicationEvent
}
/**
* {@link HttpRequestHandlingEndpointSupport}s may depend on auto-created
* {@code requestChannel}s, so MVC Handlers detection should be postponed
* as late as possible.
* @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#afterPropertiesSet()
*/
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (!this.initialized.getAndSet(true)) {
super.afterPropertiesSet();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2016 the original author or authors.
* Copyright 2013-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.
@@ -33,6 +33,8 @@ import org.springframework.web.bind.annotation.RequestMethod;
*/
public class RequestMapping {
private String name = "";
private String[] pathPatterns;
private HttpMethod[] methods = new HttpMethod[]{HttpMethod.GET, HttpMethod.POST};
@@ -45,6 +47,14 @@ public class RequestMapping {
private String[] produces = new String[0];
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public void setPathPatterns(String... pathPatterns) {
Assert.notEmpty(pathPatterns, "at least one path pattern is required");
this.pathPatterns = pathPatterns;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2016 the original author or authors.
* Copyright 2013-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.
@@ -16,7 +16,13 @@
package org.springframework.integration.http.support;
import java.util.HashMap;
import java.util.Map;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* Utility class for accessing HTTP integration components
@@ -33,19 +39,36 @@ public final class HttpContextUtils {
}
/**
* The {@code boolean} flag to indicate if the {@code org.springframework.web.servlet.DispatcherServlet}
* The {@code boolean} flag to indicate if the
* {@code org.springframework.web.servlet.DispatcherServlet}
* is present in the CLASSPATH to allow to register the Integration server components,
* e.g. {@code IntegrationGraphController}.
*/
public static final boolean SERVLET_PRESENT =
public static final boolean WEB_MVC_PRESENT =
ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet",
HttpContextUtils.class.getClassLoader());
HttpContextUtils.class.getClassLoader());
/**
* @see org.springframework.integration.http.config.HttpInboundEndpointParser
* The {@code boolean} flag to indicate if the
* {@code org.springframework.web.reactive.result.method.RequestMappingInfo}
* is present in the CLASSPATH to allow to register the Integration server reactive components.
*/
public static final boolean WEB_FLUX_PRESENT =
ClassUtils.isPresent("org.springframework.web.reactive.result.method.RequestMappingInfo",
HttpContextUtils.class.getClassLoader());
/**
* The name for the infrastructure
* {@link org.springframework.integration.http.inbound.IntegrationRequestMappingHandlerMapping} bean.
*/
public static final String HANDLER_MAPPING_BEAN_NAME = "integrationRequestMappingHandlerMapping";
/**
* The name for the infrastructure
* {@link org.springframework.integration.http.inbound.ReactiveIntegrationRequestMappingHandlerMapping} bean.
*/
public static final String REACTIVE_HANDLER_MAPPING_BEAN_NAME = "reactiveIntegrationRequestMappingHandlerMapping";
/**
* Represents the environment property for the {@code IntegrationGraphController} request mapping path.
*/
@@ -62,4 +85,30 @@ public final class HttpContextUtils {
*/
public static final String GRAPH_CONTROLLER_BEAN_NAME = "integrationGraphController";
/**
* Converts a provided {@link org.springframework.integration.http.inbound.RequestMapping}
* to the Spring Web {@link RequestMapping} annotation.
* @param requestMapping the {@link org.springframework.integration.http.inbound.RequestMapping} to convert.
* @return the {@link RequestMapping} annotation.
* @since 5.0
*/
public static RequestMapping convertRequestMappingToAnnotation(
org.springframework.integration.http.inbound.RequestMapping requestMapping) {
if (ObjectUtils.isEmpty(requestMapping.getPathPatterns())) {
return null;
}
Map<String, Object> requestMappingAttributes = new HashMap<>();
requestMappingAttributes.put("name", requestMapping.getName());
requestMappingAttributes.put("value", requestMapping.getPathPatterns());
requestMappingAttributes.put("path", requestMapping.getPathPatterns());
requestMappingAttributes.put("method", requestMapping.getRequestMethods());
requestMappingAttributes.put("params", requestMapping.getParams());
requestMappingAttributes.put("headers", requestMapping.getHeaders());
requestMappingAttributes.put("consumes", requestMapping.getConsumes());
requestMappingAttributes.put("produces", requestMapping.getProduces());
return AnnotationUtils.synthesizeAnnotation(requestMappingAttributes, RequestMapping.class, null);
}
}

View File

@@ -16,6 +16,9 @@
package org.springframework.integration.http.dsl;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -27,11 +30,13 @@ import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.reactivestreams.Publisher;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -44,7 +49,9 @@ import org.springframework.integration.http.outbound.HttpRequestExecutingMessage
import org.springframework.integration.http.outbound.ReactiveHttpRequestExecutingMessageHandler;
import org.springframework.integration.security.channel.ChannelSecurityInterceptor;
import org.springframework.integration.security.channel.SecuredChannel;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.RoleVoter;
@@ -57,16 +64,19 @@ import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.client.MockMvcClientHttpRequestFactory;
import org.springframework.test.web.reactive.server.HttpHandlerConnector;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
/**
* @author Artem Bilan
@@ -90,12 +100,18 @@ public class HttpDslTests {
private MockMvc mockMvc;
private WebTestClient webTestClient;
@Before
public void setup() {
this.mockMvc =
MockMvcBuilders.webAppContextSetup(this.wac)
.apply(springSecurity())
.build();
this.webTestClient =
WebTestClient.bindToApplicationContext(this.wac)
.build();
}
@@ -140,9 +156,45 @@ public class HttpDslTests {
.string("FOO"));
}
@Autowired
private PollableChannel storeChannel;
@Test
@SuppressWarnings("unchecked")
public void testHttpReactivePost() {
this.webTestClient.post().uri("/reactivePost")
.body(Flux.just("foo", "bar", "baz"), String.class)
.exchange()
.expectStatus().isAccepted();
Message<?> store = this.storeChannel.receive(10_000);
assertNotNull(store);
assertThat(store.getPayload(), instanceOf(Flux.class));
StepVerifier
.create((Publisher<String>) store.getPayload())
.expectNext("foo", "bar", "baz")
.verifyComplete();
}
@Test
public void testSse() {
Flux<String> responseBody =
this.webTestClient.get().uri("/sse")
.exchange()
.returnResult(String.class)
.getResponseBody();
StepVerifier
.create(responseBody)
.expectNext("foo", "bar", "baz")
.verifyComplete();
}
@Configuration
@EnableWebMvc
@EnableWebFlux
@EnableWebSecurity
@EnableIntegration
public static class ContextConfiguration extends WebSecurityConfigurerAdapter {
@@ -211,6 +263,26 @@ public class HttpDslTests {
.get();
}
@Bean
public IntegrationFlow httpReactiveInboundChannelAdapterFlow() {
return IntegrationFlows
.from(Http.inboundReactiveChannelAdapter("/reactivePost")
.requestMapping(m -> m.methods(HttpMethod.POST))
.requestPayloadType(ResolvableType.forClassWithGenerics(Flux.class, String.class))
.statusCodeFunction(m -> HttpStatus.ACCEPTED))
.channel(c -> c.queue("storeChannel"))
.get();
}
@Bean
public IntegrationFlow sseFlow() {
return IntegrationFlows
.from(Http.inboundReactiveGateway("/sse")
.requestMapping(m -> m.produces(MediaType.TEXT_EVENT_STREAM_VALUE)))
.handle((p, h) -> Flux.just("foo", "bar", "baz"))
.get();
}
@Bean
public AccessDecisionManager accessDecisionManager() {
return new AffirmativeBased(Collections.singletonList(new RoleVoter()));

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -64,6 +64,7 @@ import org.springframework.util.SerializationUtils;
* @author Gunnar Hillert
* @author Artem Bilan
* @author Biju Kunjummen
*
* @since 2.0
*/
public class HttpRequestHandlingMessagingGatewayTests extends AbstractHttpInboundTests {
@@ -96,7 +97,7 @@ public class HttpRequestHandlingMessagingGatewayTests extends AbstractHttpInboun
QueueChannel requestChannel = new QueueChannel();
HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(false);
gateway.setBeanFactory(mock(BeanFactory.class));
gateway.setRequestPayloadType(String.class);
gateway.setRequestPayloadTypeClass(String.class);
gateway.setRequestChannel(requestChannel);
gateway.afterPropertiesSet();
gateway.start();
@@ -134,7 +135,7 @@ public class HttpRequestHandlingMessagingGatewayTests extends AbstractHttpInboun
HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(true);
gateway.setStatusCodeExpression(new LiteralExpression("foo"));
gateway.setBeanFactory(mock(BeanFactory.class));
gateway.setRequestPayloadType(String.class);
gateway.setRequestPayloadTypeClass(String.class);
gateway.setRequestChannel(requestChannel);
gateway.afterPropertiesSet();
gateway.start();
@@ -162,7 +163,7 @@ public class HttpRequestHandlingMessagingGatewayTests extends AbstractHttpInboun
});
HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(true);
gateway.setBeanFactory(mock(BeanFactory.class));
gateway.setRequestPayloadType(String.class);
gateway.setRequestPayloadTypeClass(String.class);
gateway.setRequestChannel(requestChannel);
gateway.afterPropertiesSet();
gateway.start();
@@ -238,7 +239,7 @@ public class HttpRequestHandlingMessagingGatewayTests extends AbstractHttpInboun
QueueChannel channel = new QueueChannel();
HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(false);
gateway.setBeanFactory(mock(BeanFactory.class));
gateway.setRequestPayloadType(TestBean.class);
gateway.setRequestPayloadTypeClass(TestBean.class);
gateway.setRequestChannel(channel);
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();

View File

@@ -47,6 +47,7 @@ import org.springframework.web.context.request.RequestContextHolder;
/**
* @author Gary Russell
* @author Artem Bilan
*
* @since 4.2
*/
public class MultipartAsRawByteArrayTests {
@@ -61,7 +62,7 @@ public class MultipartAsRawByteArrayTests {
QueueChannel requestChannel = new QueueChannel();
gw.setRequestChannel(requestChannel);
gw.setBeanFactory(mock(BeanFactory.class));
gw.setRequestPayloadType(byte[].class);
gw.setRequestPayloadTypeClass(byte[].class);
gw.afterPropertiesSet();
gw.start();

View File

@@ -0,0 +1,170 @@
/*
* Copyright 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.integration.http.inbound;
import java.util.Objects;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.FluxMessageChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.messaging.MessageChannel;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.config.EnableWebFlux;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import reactor.core.publisher.Flux;
/**
* @author Artem Bilan
*
* @since 5.0
*/
@RunWith(SpringRunner.class)
@DirtiesContext
public class ReactiveHttpInboundEndpointTests {
@Autowired
private WebTestClient webTestClient;
@Autowired
private ReactiveHttpInboundEndpoint simpleInboundEndpoint;
@Test
public void testSimpleGet() {
this.webTestClient.get().uri("/test")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("It works!");
this.simpleInboundEndpoint.stop();
this.webTestClient.get().uri("/test")
.exchange()
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
.expectBody(String.class).isEqualTo("Endpoint is stopped");
}
@Test
public void testJsonResult() {
this.webTestClient.get().uri("/persons")
.accept(MediaType.APPLICATION_JSON_UTF8)
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason")
.jsonPath("$[2].name").isEqualTo("John");
}
@Configuration
@EnableWebFlux
@EnableIntegration
public static class ContextConfiguration {
@Bean
public WebTestClient webTestClient(ApplicationContext applicationContext) {
return WebTestClient.bindToApplicationContext(applicationContext).build();
}
@Bean
public ReactiveHttpInboundEndpoint simpleInboundEndpoint() {
ReactiveHttpInboundEndpoint endpoint = new ReactiveHttpInboundEndpoint();
RequestMapping requestMapping = new RequestMapping();
requestMapping.setPathPatterns("/test");
endpoint.setRequestMapping(requestMapping);
endpoint.setRequestChannelName("serviceChannel");
return endpoint;
}
@ServiceActivator(inputChannel = "serviceChannel")
String service() {
return "It works!";
}
@Bean
public ReactiveHttpInboundEndpoint jsonInboundEndpoint() {
ReactiveHttpInboundEndpoint endpoint = new ReactiveHttpInboundEndpoint();
RequestMapping requestMapping = new RequestMapping();
requestMapping.setPathPatterns("/persons");
endpoint.setRequestMapping(requestMapping);
endpoint.setRequestChannel(fluxResultChannel());
return endpoint;
}
@Bean
public MessageChannel fluxResultChannel() {
return new FluxMessageChannel();
}
@ServiceActivator(inputChannel = "fluxResultChannel")
Flux<Person> getPersons() {
return Flux.just(new Person("Jane"), new Person("Jason"), new Person("John"));
}
}
static class Person {
private final String name;
@JsonCreator
Person(@JsonProperty("name") String name) {
this.name = name;
}
public String getName() {
return this.name;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Person person = (Person) o;
return Objects.equals(this.name, person.name);
}
@Override
public int hashCode() {
return getName().hashCode();
}
@Override
public String toString() {
return "Person[name='" + this.name + "']";
}
}
}

View File

@@ -45,6 +45,7 @@ import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
/**
* @author Artem Bilan
* @author Gary Russell
*
* @since 4.3.1
*/
public class JdbcLockRegistryLeaderInitiatorTests {
@@ -155,15 +156,16 @@ public class JdbcLockRegistryLeaderInitiatorTests {
assertThat(initiator1.getContext().isLeader(), is(true));
assertThat(initiator2.getContext().isLeader(), is(false));
// Stop second initiator, so the first one will be leader even after yield
initiator2.stop();
CountDownLatch revoked11 = new CountDownLatch(1);
initiator1.setLeaderEventPublisher(new CountingPublisher(new CountDownLatch(1), revoked11));
CountDownLatch granted11 = new CountDownLatch(1);
initiator1.setLeaderEventPublisher(new CountingPublisher(granted11));
initiator1.getContext().yield();
assertThat(revoked11.await(20, TimeUnit.SECONDS), is(true));
assertThat(initiator1.getContext().isLeader(), is(false));
assertThat(granted11.await(20, TimeUnit.SECONDS), is(true));
assertThat(initiator1.getContext().isLeader(), is(true));
initiator1.stop();
}

View File

@@ -122,6 +122,59 @@ This also shows how to customize the HTTP methods accepted by the gateway, which
The reply message will be available in the Model map.
The key that is used for that map entry by default is 'reply', but this can be overridden by setting the 'replyKey' property on the endpoint's configuration.
=== WebFlux Server Side support
Starting with _version 5.0_, the `ReactiveHttpInboundEndpoint`, http://docs.spring.io/spring/docs/5.0.0.RC3/spring-framework-reference/web.html#web-reactive[WebFlux] `WebHandler`, implementation is provided.
This component is similar to the MVC-based `HttpRequestHandlingEndpointSupport` with which it shares some common options via the newly extracted `BaseHttpInboundEndpoint`.
Instead of MVC, it is used in the Spring WebFlux Reactive environment.
A simple sample for explanation:
[source,java]
----
@Configuration
@EnableWebFlux
@EnableIntegration
public class ReactiveHttpConfiguration {
@Bean
public ReactiveHttpInboundEndpoint simpleInboundEndpoint() {
ReactiveHttpInboundEndpoint endpoint = new ReactiveHttpInboundEndpoint();
RequestMapping requestMapping = new RequestMapping();
requestMapping.setPathPatterns("/test");
endpoint.setRequestMapping(requestMapping);
endpoint.setRequestChannelName("serviceChannel");
return endpoint;
}
@ServiceActivator(inputChannel = "serviceChannel")
String service() {
return "It works!";
}
}
----
As can be seen, the configuration is similar to the `HttpRequestHandlingEndpointSupport` mentioned above, except that we use `@EnableWebFlux` to add the WebFlux infrastructure to our integration application.
Also, the `ReactiveHttpInboundEndpoint` performs `sendAndReceive` operation to the downstream flow using back-pressure, on demand based capabilities, provided by the reactive HTTP server implementation.
NOTE: The reply part is non-blocking as well and based on the internal `FutureReplyChannel` which is flat-mapped to a reply `Mono` for on demand resolution.
The `ReactiveHttpInboundEndpoint` can be configured with a custom `ServerCodecConfigurer`, `RequestedContentTypeResolver` and even a `ReactiveAdapterRegistry`.
The latter provides a mechanism where we can return a reply as any reactive type - Reactor `Flux`, RxJava `Observable`, `Flowable` etc.
This way, we can simply implement https://en.wikipedia.org/wiki/Server-sent_events[Server Sent Events] scenarios with Spring Integration components:
[source,java]
----
@Bean
public IntegrationFlow sseFlow() {
return IntegrationFlows
.from(Http.inboundReactiveGateway("/sse")
.requestMapping(m -> m.produces(MediaType.TEXT_EVENT_STREAM_VALUE)))
.handle((p, h) -> Flux.just("foo", "bar", "baz"))
.get();
}
----
[[http-outbound]]
=== Http Outbound Components
==== HttpRequestExecutingMessageHandler
@@ -193,8 +246,8 @@ Of course, this can be an abstract class, or even an interface (such as `java.io
==== ReactiveHttpRequestExecutingMessageHandler
The `ReactiveHttpRequestExecutingMessageHandler` implementation is very similar to `HttpRequestExecutingMessageHandler` instead of delegating to a `WebClient` from Spring Framework WebFlux module.
To configure it, write a bean like this:
The `ReactiveHttpRequestExecutingMessageHandler` (starting with _version 5.0_) implementation is very similar to `HttpRequestExecutingMessageHandler`, using a `WebClient` from the Spring Framework WebFlux module.
To configure it, define a bean like this:
[source,xml]
----
@@ -686,6 +739,26 @@ public RequestMapping mapping() {
requestMapping.setMethods(HttpMethod.POST);
return requestMapping;
}
@Bean
public ReactiveHttpInboundEndpoint jsonInboundEndpoint() {
ReactiveHttpInboundEndpoint endpoint = new ReactiveHttpInboundEndpoint();
RequestMapping requestMapping = new RequestMapping();
requestMapping.setPathPatterns("/persons");
endpoint.setRequestMapping(requestMapping);
endpoint.setRequestChannel(fluxResultChannel());
return endpoint;
}
@Bean
public MessageChannel fluxResultChannel() {
return new FluxMessageChannel();
}
@ServiceActivator(inputChannel = "fluxResultChannel")
Flux<Person> getPersons() {
return Flux.just(new Person("Jane"), new Person("Jason"), new Person("John"));
}
----
.Inbound Gateway Using the Java DSL
@@ -699,6 +772,17 @@ public IntegrationFlow inbound() {
.channel("httpRequest")
.get();
}
@Bean
public IntegrationFlow httpReactiveInboundChannelAdapterFlow() {
return IntegrationFlows
.from(Http.inboundReactiveChannelAdapter("/reactivePost")
.requestMapping(m -> m.methods(HttpMethod.POST))
.requestPayloadType(ResolvableType.forClassWithGenerics(Flux.class, String.class))
.statusCodeFunction(m -> HttpStatus.ACCEPTED))
.channel(c -> c.queue("storeChannel"))
.get();
}
----
.Outbound Gateway Using Java Configuration
@@ -713,6 +797,16 @@ public HttpRequestExecutingMessageHandler outbound() {
handler.setExpectedResponseType(String.class);
return handler;
}
@ServiceActivator(inputChannel = "reactiveHttpOutRequest")
@Bean
public ReactiveHttpRequestExecutingMessageHandler reactiveOutbound(WebClient client) {
ReactiveHttpRequestExecutingMessageHandler handler =
new ReactiveHttpRequestExecutingMessageHandler("http://localhost:8080/foo", client);
handler.setHttpMethod(HttpMethod.POST);
handler.setExpectedResponseType(String.class);
return handler;
}
----
.Outbound Gateway Using the Java DSL
@@ -726,6 +820,18 @@ public IntegrationFlow outbound() {
.expectedResponseType(String.class))
.get();
}
@Bean
public IntegrationFlow outboundReactive() {
return f -> f
.handle(Http.<MultiValueMap<String, String>>outboundReactiveGateway(m ->
UriComponentsBuilder.fromUriString("http://localhost:8080/foo")
.queryParams(m.getPayload())
.build()
.toUri())
.httpMethod(HttpMethod.GET)
.expectedResponseType(String.class));
}
----
[[http-timeout]]

View File

@@ -29,11 +29,11 @@ The new `MongoDbOutboundGateway` allows you to make queries to the database on d
See <<mongodb-outbound-gateway>> for more information.
==== HTTP Reactive Outbound Gateway and Channel Adapter
==== HTTP Reactive Inbound and Outbound Gateways and Channel Adapters
The new `ReactiveHttpRequestExecutingMessageHandler` adds support for WebFlux `WebClient` for outbound channel adapter and gateway.
The new `ReactiveHttpInboundEndpoint` and `ReactiveHttpRequestExecutingMessageHandler` add support for Spring WebFlux Framework gateways and channel adapters.
See <<http-outbound>> for more information.
See <<http>> for more information.
==== Content Type Conversion