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

@@ -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 + "']";
}
}
}