Migrate to servlet binder for web features

This commit is contained in:
Dave Syer
2017-08-08 08:27:04 +01:00
parent 540b4d378e
commit 1af0d451cf
107 changed files with 4055 additions and 2010 deletions

View File

@@ -101,12 +101,12 @@ JMS.
The `@Beans` can be `Function`, `Consumer` or `Supplier` (all from
`java.util`), and their parametric types can be String or POJO. A
`Function` is exposed as an HTTP POST if `spring-cloud-function-web`
is on the classpath, and as a Spring Cloud Stream `Processor` if
`Function` is exposed as a `Processor` if
`spring-cloud-function-stream` is on the classpath and a
`spring.cloud.function.stream.endpoint` property is configured in the Spring
environment. A `Consumer` is also exposed as an HTTP POST, or as a Stream
`Sink`. A `Supplier` translates to an HTTP GET, or a Stream `Source`.
`spring.cloud.function.stream.endpoint` property is configured in the
Spring environment. A `Consumer` is also exposed as an HTTP POST, or
as a Stream `Sink`. A `Supplier` translates to an HTTP GET, or a
Stream `Source`.
Functions can be of `Flux<String>` or `Flux<Pojo>` and Spring Cloud
Function takes care of converting the data to and from the desired

View File

@@ -41,12 +41,11 @@ JMS.
The `@Beans` can be `Function`, `Consumer` or `Supplier` (all from
`java.util`), and their parametric types can be String or POJO. A
`Function` is exposed as an HTTP POST if `spring-cloud-function-web`
is on the classpath, and as a Spring Cloud Stream `Processor` if
`spring-cloud-function-stream` is on the classpath and a
`spring.cloud.function.stream.endpoint` property is configured in the Spring
environment. A `Consumer` is also exposed as an HTTP POST, or as a Stream
`Sink`. A `Supplier` translates to an HTTP GET, or a Stream `Source`.
`Function` is exposed as a Spring Cloud Stream `Processor` if
`spring-cloud-function-stream` is on the classpath.
A `Consumer` is also exposed as a Stream
`Sink` and a `Supplier` translates to a Stream `Source`.
HTTP endpoints are exposed if the Stream binder is `spring-cloud-stream-binder-servlet`.
Functions can be of `Flux<String>` or `Flux<Pojo>` and Spring Cloud
Function takes care of converting the data to and from the desired

View File

@@ -56,7 +56,6 @@
<module>spring-cloud-function-context</module>
<module>spring-cloud-function-stream</module>
<module>spring-cloud-function-task</module>
<module>spring-cloud-function-web</module>
<module>spring-cloud-function-samples</module>
<module>spring-cloud-function-deployer</module>
<module>spring-cloud-function-adapters</module>

View File

@@ -1,4 +1,4 @@
This project provides an adapter layer for a Spring Cloud Function application onto AWS Lambda. You can write an app with a single `@Bean` of type `Function`, `Consumer` or `Supplier` and it will be deployable in AWS if you get the JAR file laid out right. The best way to make it work is to include `spring-cloud-function-context` as a dependency, but not the higher level adapters (e.g. `spring-cloud-function-web` or `spring-cloud-function-stream`).
This project provides an adapter layer for a Spring Cloud Function application onto AWS Lambda. You can write an app with a single `@Bean` of type `Function`, `Consumer` or `Supplier` and it will be deployable in AWS if you get the JAR file laid out right. The best way to make it work is to include `spring-cloud-function-context` as a dependency, but not the higher level adapters (e.g. `spring-cloud-function-stream`).
The adapter has a couple of generic request handlers that you can use. The most generic is `SpringBootStreamHandler`, which uses a Jackson `ObjectMapper` provided by Spring Boot to serialize and deserialize the objects in the function. There is also a `SpringBootRequestHandler` which you can extend, and provide the input and output types as type parameters (enabling AWS to inspect the class and do the JSON conversions itself).

View File

@@ -32,7 +32,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.function.context.FunctionInspector;
import org.springframework.cloud.function.context.catalog.FunctionInspector;
import org.springframework.cloud.function.core.FunctionCatalog;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.ClassUtils;

View File

@@ -21,7 +21,7 @@ import java.util.function.Function;
import org.junit.Test;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration;
import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

View File

@@ -23,7 +23,7 @@ import java.util.function.Function;
import org.junit.Test;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration;
import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

View File

@@ -24,7 +24,7 @@ import java.util.stream.Collectors;
import org.junit.After;
import org.junit.Test;
import org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration;
import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

View File

@@ -22,7 +22,7 @@ import java.util.function.Function;
import java.util.function.Supplier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.function.context.FunctionInspector;
import org.springframework.cloud.function.context.catalog.FunctionInspector;
import org.springframework.cloud.function.core.FluxFunction;
import org.springframework.cloud.function.core.FunctionCatalog;
import org.springframework.cloud.function.core.FunctionFactoryUtils;

View File

@@ -13,33 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.context.catalog;
package org.springframework.cloud.function.web.flux.request;
import java.util.List;
import reactor.core.publisher.Flux;
import org.springframework.context.ApplicationEvent;
/**
* @author Dave Syer
*
*/
public class FluxRequest<T> {
private List<T> body;
@SuppressWarnings("serial")
public class FunctionCatalogEvent extends ApplicationEvent {
public FluxRequest(List<T> body) {
this.body = body;
public FunctionCatalogEvent(Object source) {
super(source);
}
public Flux<T> flux() {
return Flux.fromIterable(body);
}
public List<T> body() {
return body;
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.function.context;
package org.springframework.cloud.function.context.catalog;
import java.lang.reflect.Type;
import java.util.Optional;

View File

@@ -0,0 +1,45 @@
/*
* 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.
* 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.cloud.function.context.catalog;
import java.util.HashSet;
import java.util.Set;
/**
* @author Dave Syer
*
*/
@SuppressWarnings("serial")
public class FunctionRegistrationEvent extends FunctionCatalogEvent {
private final Class<?> type;
private final Set<String> names;
public FunctionRegistrationEvent(Object source, Class<?> type, Set<String> names) {
super(source);
this.type = type;
this.names = new HashSet<>(names);
}
public Class<?> getType() {
return type;
}
public Set<String> getNames() {
return names;
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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.
* 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.cloud.function.context.catalog;
import java.util.HashSet;
import java.util.Set;
/**
* @author Dave Syer
*
*/
@SuppressWarnings("serial")
public class FunctionUnregistrationEvent extends FunctionCatalogEvent {
private final Class<?> type;
private final Set<String> names;
public FunctionUnregistrationEvent(Object source, Class<?> type, Set<String> names) {
super(source);
this.type = type;
this.names = new HashSet<>(names);
}
public Class<?> getType() {
return type;
}
public Set<String> getNames() {
return names;
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.function.context;
package org.springframework.cloud.function.context.catalog;
import java.util.Collections;
import java.util.HashMap;
@@ -24,7 +24,14 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.function.context.FunctionRegistration;
import org.springframework.cloud.function.context.FunctionRegistry;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.util.Assert;
/**
@@ -32,7 +39,8 @@ import org.springframework.util.Assert;
* @author Mark Fisher
* @author Oleg Zhurakousky
*/
public class InMemoryFunctionCatalog implements FunctionRegistry {
public class InMemoryFunctionCatalog
implements FunctionRegistry, ApplicationEventPublisherAware {
private final Map<String, Function<?, ?>> functions;
@@ -40,11 +48,13 @@ public class InMemoryFunctionCatalog implements FunctionRegistry {
private final Map<String, Supplier<?>> suppliers;
@Autowired(required = false)
private ApplicationEventPublisher publisher;
public InMemoryFunctionCatalog() {
this(Collections.emptySet());
}
@Autowired(required=false)
public InMemoryFunctionCatalog(Set<FunctionRegistration<?>> registrations) {
Assert.notNull(registrations, "'registrations' must not be null");
this.suppliers = new HashMap<>();
@@ -66,20 +76,68 @@ public class InMemoryFunctionCatalog implements FunctionRegistry {
@Override
public <T> void register(FunctionRegistration<T> registration) {
Map<String, ?> values = null;
FunctionRegistrationEvent event;
if (registration.getTarget() instanceof Function) {
values = this.functions;
event = new FunctionRegistrationEvent(this, Function.class,
registration.getNames());
}
else if (registration.getTarget() instanceof Supplier) {
values = this.suppliers;
event = new FunctionRegistrationEvent(this, Supplier.class,
registration.getNames());
}
else {
values = this.consumers;
event = new FunctionRegistrationEvent(this, Consumer.class,
registration.getNames());
}
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) values;
for (String name : registration.getNames()) {
map.put(name, registration.getTarget());
}
publisher.publishEvent(event);
}
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
@PostConstruct
public void init() {
if (publisher != null) {
if (!functions.isEmpty()) {
publisher.publishEvent(new FunctionRegistrationEvent(this, Function.class,
functions.keySet()));
}
if (!consumers.isEmpty()) {
publisher.publishEvent(new FunctionRegistrationEvent(this, Consumer.class,
consumers.keySet()));
}
if (!suppliers.isEmpty()) {
publisher.publishEvent(new FunctionRegistrationEvent(this, Supplier.class,
suppliers.keySet()));
}
}
}
@PreDestroy
public void close() {
if (publisher != null) {
if (!functions.isEmpty()) {
publisher.publishEvent(new FunctionUnregistrationEvent(this,
Function.class, functions.keySet()));
}
if (!consumers.isEmpty()) {
publisher.publishEvent(new FunctionUnregistrationEvent(this,
Consumer.class, consumers.keySet()));
}
if (!suppliers.isEmpty()) {
publisher.publishEvent(new FunctionUnregistrationEvent(this,
Supplier.class, suppliers.keySet()));
}
}
}
@Override

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.function.context;
package org.springframework.cloud.function.context.config;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
@@ -34,6 +34,8 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanDefinition;
@@ -42,6 +44,13 @@ import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueH
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.function.context.FunctionRegistration;
import org.springframework.cloud.function.context.FunctionRegistry;
import org.springframework.cloud.function.context.FunctionScan;
import org.springframework.cloud.function.context.catalog.FunctionInspector;
import org.springframework.cloud.function.context.catalog.FunctionRegistrationEvent;
import org.springframework.cloud.function.context.catalog.FunctionUnregistrationEvent;
import org.springframework.cloud.function.context.catalog.InMemoryFunctionCatalog;
import org.springframework.cloud.function.core.FluxConsumer;
import org.springframework.cloud.function.core.FluxFunction;
import org.springframework.cloud.function.core.FluxSupplier;
@@ -51,6 +60,7 @@ import org.springframework.cloud.function.core.FunctionFactoryUtils;
import org.springframework.cloud.function.core.IsolatedConsumer;
import org.springframework.cloud.function.core.IsolatedFunction;
import org.springframework.cloud.function.core.IsolatedSupplier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ResolvableType;
@@ -112,32 +122,38 @@ public class ContextFunctionCatalogAutoConfiguration {
processor.register(registration);
}
@Override
@SuppressWarnings("unchecked")
public <T> Supplier<T> lookupSupplier(String name) {
Supplier<T> result = (Supplier<T>) processor.lookupSupplier(name);
return result;
}
@Override
@SuppressWarnings("unchecked")
public <T, R> Function<T, R> lookupFunction(String name) {
Function<T, R> result = (Function<T, R>) processor.lookupFunction(name);
return result;
}
@Override
@SuppressWarnings("unchecked")
public <T> Consumer<T> lookupConsumer(String name) {
Consumer<T> result = (Consumer<T>) processor.lookupConsumer(name);
return result;
}
@Override
public Set<String> getSupplierNames() {
return this.processor.getSuppliers();
}
@Override
public Set<String> getFunctionNames() {
return this.processor.getFunctions();
}
@Override
public Set<String> getConsumerNames() {
return this.processor.getConsumers();
}
@@ -202,6 +218,8 @@ public class ContextFunctionCatalogAutoConfiguration {
private Map<String, Object> consumers = new HashMap<>();
@Autowired(required = false)
private ApplicationEventPublisher publisher;
@Autowired
private ConfigurableListableBeanFactory registry;
@@ -339,6 +357,24 @@ public class ContextFunctionCatalogAutoConfiguration {
function.getNames().iterator().next());
}
@PreDestroy
public void close() {
if (publisher != null) {
if (!functions.isEmpty()) {
publisher.publishEvent(new FunctionUnregistrationEvent(this,
Function.class, functions.keySet()));
}
if (!consumers.isEmpty()) {
publisher.publishEvent(new FunctionUnregistrationEvent(this,
Consumer.class, consumers.keySet()));
}
if (!suppliers.isEmpty()) {
publisher.publishEvent(new FunctionUnregistrationEvent(this,
Supplier.class, suppliers.keySet()));
}
}
}
public Set<FunctionRegistration<?>> merge(
Map<String, FunctionRegistration<?>> initial,
Map<String, Consumer<?>> consumers, Map<String, Supplier<?>> suppliers,
@@ -417,7 +453,9 @@ public class ContextFunctionCatalogAutoConfiguration {
private void wrap(FunctionRegistration<Object> registration, String key) {
Object target = registration.getTarget();
this.registrations.put(target, key);
Class<?> type;
if (target instanceof Supplier) {
type = Supplier.class;
findType(target, ParamType.OUTPUT);
registration.target(target((Supplier<?>) target, key));
for (String name : registration.getNames()) {
@@ -425,6 +463,7 @@ public class ContextFunctionCatalogAutoConfiguration {
}
}
else if (target instanceof Consumer) {
type = Consumer.class;
findType(target, ParamType.INPUT);
registration.target(target((Consumer<?>) target, key));
for (String name : registration.getNames()) {
@@ -432,6 +471,7 @@ public class ContextFunctionCatalogAutoConfiguration {
}
}
else if (target instanceof Function) {
type = Function.class;
findType(target, ParamType.INPUT);
findType(target, ParamType.OUTPUT);
registration.target(target((Function<?, ?>) target, key));
@@ -439,8 +479,15 @@ public class ContextFunctionCatalogAutoConfiguration {
this.functions.put(name, (Function<?, ?>) registration.getTarget());
}
}
else {
return;
}
registrations.remove(target);
this.registrations.put(registration.getTarget(), key);
if (publisher != null) {
publisher.publishEvent(new FunctionRegistrationEvent(
registration.getTarget(), type, registration.getNames()));
}
}
private String getQualifier(String key) {

View File

@@ -1,2 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration
org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.function.context;
package org.springframework.cloud.function.context.config;
import java.util.ArrayList;
import java.util.List;
@@ -24,8 +24,9 @@ import java.util.function.Supplier;
import org.junit.Test;
import org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration.BeanFactoryFunctionCatalog;
import org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration.ContextFunctionRegistry;
import org.springframework.cloud.function.context.FunctionRegistration;
import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration.BeanFactoryFunctionCatalog;
import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration.ContextFunctionRegistry;
import static org.assertj.core.api.Assertions.assertThat;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.function.context;
package org.springframework.cloud.function.context.config;
import static org.assertj.core.api.Assertions.assertThat;
@@ -43,6 +43,9 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.function.compiler.CompiledFunctionFactory;
import org.springframework.cloud.function.compiler.FunctionCompiler;
import org.springframework.cloud.function.context.FunctionRegistration;
import org.springframework.cloud.function.context.FunctionScan;
import org.springframework.cloud.function.context.catalog.FunctionInspector;
import org.springframework.cloud.function.core.FunctionCatalog;
import org.springframework.cloud.function.scan.ScannedFunction;
import org.springframework.cloud.function.test.GenericFunction;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.function.context;
package org.springframework.cloud.function.context.config;
import java.net.URLClassLoader;
import java.util.ArrayList;
@@ -28,10 +28,10 @@ import org.junit.After;
import org.junit.Test;
import org.springframework.beans.BeanUtils;
import org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration.ContextFunctionRegistry;
import org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfigurationTests.Bar;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.ClassUtils;
import org.springframework.cloud.function.context.FunctionRegistration;
import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration.ContextFunctionRegistry;
import static org.assertj.core.api.Assertions.assertThat;
@@ -189,4 +189,43 @@ public class ContextFunctionPostProcessorTests {
}
public static class Foo {
private String value;
public Foo(String value) {
this.value = value;
}
Foo() {
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
public static class Bar {
private String message;
public Bar(String value) {
this.message = value;
}
Bar() {
}
public String getMessage() {
return this.message;
}
public void setMessage(String message) {
this.message = message;
}
}
}

View File

@@ -16,6 +16,7 @@
<properties>
<spring-cloud-deployer-thin.version>1.0.8.RELEASE</spring-cloud-deployer-thin.version>
<spring-cloud-stream-servlet.version>1.0.0.BUILD-SNAPSHOT</spring-cloud-stream-servlet.version>
</properties>
<dependencies>
@@ -25,8 +26,12 @@
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-web</artifactId>
<scope>runtime</scope>
<artifactId>spring-cloud-function-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-servlet</artifactId>
<version>${spring-cloud-stream-servlet.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -0,0 +1,84 @@
/*
* 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.
* 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.cloud.function.deployer;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* @author Dave Syer
*
*/
@Component
public class DeployedApplicationFilter extends WebMvcConfigurerAdapter
implements HandlerInterceptor {
private final FunctionExtractingFunctionCatalog deployer;
@Autowired
public DeployedApplicationFilter(FunctionExtractingFunctionCatalog deployer) {
this.deployer = deployer;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(this);
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
String path = (String) request
.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
if (path != null) {
// TODO: extract /stream to config property
if (path.startsWith("/stream")) {
String name = path.substring("/stream/".length());
if (name.contains("/")) {
name = name.substring(0, name.indexOf("/"));
}
if (deployer.deployed().containsKey(name)) {
return true;
}
else {
response.setStatus(HttpStatus.NOT_FOUND.value());
return false;
}
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
}
}

View File

@@ -17,7 +17,7 @@ package org.springframework.cloud.function.deployer;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration;
import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

View File

@@ -31,13 +31,16 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.loader.thin.ArchiveUtils;
import org.springframework.cloud.deployer.spi.app.AppDeployer;
import org.springframework.cloud.deployer.spi.core.AppDefinition;
import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest;
import org.springframework.cloud.deployer.thin.ThinJarAppDeployer;
import org.springframework.cloud.function.context.FunctionInspector;
import org.springframework.cloud.function.context.catalog.FunctionInspector;
import org.springframework.cloud.function.core.FunctionCatalog;
import org.springframework.cloud.function.stream.config.SupplierInvokingMessageProducer;
import org.springframework.cloud.stream.binder.servlet.RouteRegistrar;
import org.springframework.context.support.LiveBeansView;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
@@ -49,6 +52,10 @@ public class FunctionExtractingFunctionCatalog
private static Log logger = LogFactory
.getLog(FunctionExtractingFunctionCatalog.class);
private RouteRegistrar routes;
private SupplierInvokingMessageProducer<?> producer;
private ThinJarAppDeployer deployer;
private Map<String, String> deployed = new LinkedHashMap<>();
@@ -65,6 +72,16 @@ public class FunctionExtractingFunctionCatalog
deployer = new ThinJarAppDeployer(name, profiles);
}
@Autowired
public void setRouteRegistrar(RouteRegistrar routes) {
this.routes = routes;
}
@Autowired
public void setProducer(SupplierInvokingMessageProducer<?> producer) {
this.producer = producer;
}
@Override
public void destroy() throws Exception {
for (String name : new HashSet<>(names.keySet())) {
@@ -156,6 +173,7 @@ public class FunctionExtractingFunctionCatalog
this.deployed.put(id, path);
this.names.put(name, id);
this.ids.put(id, name);
register(name);
return id;
}
@@ -165,6 +183,7 @@ public class FunctionExtractingFunctionCatalog
// TODO: Convert to 404
throw new IllegalStateException("No such app");
}
unregister(name);
this.deployer.undeploy(id);
String path = this.deployed.remove(id);
this.names.remove(name);
@@ -172,6 +191,39 @@ public class FunctionExtractingFunctionCatalog
return new DeployedArtifact(name, id, path);
}
private void register(String name) {
Set<String> names = getSupplierNames(name);
if (routes != null) {
logger.info("Registering routes: " + names);
routes.registerRoutes(getSupplierNames(name));
}
if (producer != null) {
// Need an ApplicationEvent that we can react to in the producer?
for (String supplier : names) {
producer.start(supplier);
}
}
}
@SuppressWarnings("unchecked")
private Set<String> getSupplierNames(String name) {
String id = this.names.get(name);
return (Set<String>) invoke(id, FunctionCatalog.class, "getSupplierNames");
}
private void unregister(String name) {
Set<String> names = getSupplierNames(name);
if (routes != null) {
logger.info("Unregistering routes: " + names);
routes.unregisterRoutes(names);
}
if (producer != null) {
for (String supplier : names) {
producer.stop(supplier);
}
}
}
private Object inspect(Object arg, String method) {
if (logger.isDebugEnabled()) {
logger.debug("Inspecting " + method);
@@ -195,53 +247,90 @@ public class FunctionExtractingFunctionCatalog
private Object invoke(Class<?> type, String method, Object... arg) {
Set<Object> results = new LinkedHashSet<>();
Object fallback = null;
for (String id : this.deployed.keySet()) {
Object catalog = this.deployer.getBean(id, type);
if (catalog == null) {
Object result = invoke(id, type, method, arg);
if (result instanceof Collection) {
results.addAll((Collection<?>) result);
continue;
}
String name = this.ids.get(id);
String prefix = name + "/";
if (arg.length == 1) {
if (arg[0] instanceof String) {
String specific = arg[0].toString();
if (specific.startsWith(prefix)) {
arg[0] = specific.substring(prefix.length());
}
else {
continue;
}
if (result != null) {
if (result == Object.class) {
// Type fallback is Object
fallback = Object.class;
continue;
}
}
try {
MethodInvoker invoker = new MethodInvoker();
invoker.setTargetObject(catalog);
invoker.setTargetMethod(method);
invoker.setArguments(arg);
invoker.prepare();
Object result = invoker.invoke();
if (result != null) {
if (result instanceof Collection) {
for (Object value : (Collection<?>) result) {
results.add(prefix + value);
}
}
else if (result instanceof String) {
return prefix + result;
}
else {
return result;
}
if (result instanceof Boolean && !((Boolean) result)) {
// Boolean fallback is false
fallback = false;
continue;
}
}
catch (Exception e) {
throw new IllegalStateException("Cannot extract catalog", e);
return result;
}
}
if (fallback != null) {
return fallback;
}
if (logger.isDebugEnabled()) {
logger.debug("Results: " + results);
}
return arg.length > 0 ? null : results;
}
private Object invoke(String id, Class<?> type, String method, Object... arg) {
Object catalog = this.deployer.getBean(id, type);
if (catalog == null) {
return null;
}
String name = this.ids.get(id);
String prefix = name + "/";
if (arg.length == 1) {
if (arg[0] instanceof String) {
String specific = arg[0].toString();
if (specific.startsWith(prefix)) {
arg[0] = specific.substring(prefix.length());
}
else {
return null;
}
}
}
try {
MethodInvoker invoker = new MethodInvoker();
invoker.setTargetObject(catalog);
invoker.setTargetMethod(method);
invoker.setArguments(arg);
invoker.prepare();
Object result = invoker.invoke();
if (result != null) {
if (result instanceof Collection) {
Set<String> results = new LinkedHashSet<>();
for (Object value : (Collection<?>) result) {
results.add(prefix + value);
}
return results;
}
else if (result instanceof String) {
if (logger.isDebugEnabled()) {
logger.debug("Prefixed (from \" + name + \"): " + result);
}
return prefix + result;
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Result (from " + name + "): " + result);
}
return result;
}
}
}
catch (Exception e) {
throw new IllegalStateException("Cannot extract catalog", e);
}
return null;
}
public Map<String, Object> deployed() {
Map<String, Object> result = new LinkedHashMap<>();
for (String name : this.names.keySet()) {

View File

@@ -1,8 +1,8 @@
exclusions.spring-web-reactive: org.springframework:spring-web-reactive
exclusions.spring-cloud-function-web: org.springframework.cloud:spring-cloud-function-web
exclusions.reator-netty: io.projectreactor.ipc:reactor-netty
exclusions.spring-cloud-stream: org.springframework.cloud:spring-cloud-stream
exclusions.spring-cloud-stream-reactive: org.springframework.cloud:spring-cloud-stream-reactive
exclusions.spring-cloud-stream-binder-servlet: org.springframework.cloud:spring-cloud-stream-binder-servlet
exclusions.spring-cloud-stream-binder-rabbit: org.springframework.cloud:spring-cloud-stream-binder-rabbit
exclusions.spring-cloud-stream-binder-kafka: org.springframework.cloud:spring-cloud-stream-binder-kafka
exclusions.spring-boot-starter-web: org.springframework.boot:spring-boot-starter-web

View File

@@ -27,8 +27,9 @@ import org.junit.runners.Suite.SuiteClasses;
* @author Dave Syer
*/
@RunWith(Suite.class)
@SuiteClasses({ FunctionExtractingFunctionCatalogIntegrationTests.class,
FunctionExtractingFunctionCatalogTests.class })
@SuiteClasses({ FunctionAppDeployerTests.class,
FunctionExtractingFunctionCatalogTests.class,
FunctionExtractingFunctionCatalogIntegrationTests.class })
@Ignore
public class AdhocTestSuite {

View File

@@ -94,7 +94,7 @@ public class FunctionAppDeployerTests {
@Test
public void stream() throws Exception {
String first = deploy("maven://io.spring.sample:function-sample:1.0.0.BUILD-SNAPSHOT",
"spring.cloud.deployer.thin.profile=stream",
"spring.cloud.deployer.thin.profile=rabbit",
"--spring.cloud.function.stream.supplier.enabled=false", "--debug=true");
// Deployment is blocking so it either failed or succeeded.
assertThat(deployer.status(first).getState()).isEqualTo(DeploymentState.deployed);

View File

@@ -43,9 +43,9 @@ public class FunctionExtractingFunctionCatalogIntegrationTests {
public static void open() throws Exception {
port = SocketUtils.findAvailableTcpPort();
// System.setProperty("debug", "true");
context = new ApplicationRunner().start("--server.port=" + port,
"--spring.cloud.stream.enabled=false");
deploy("sample", "maven://io.spring.sample:function-sample:1.0.0.BUILD-SNAPSHOT");
context = new ApplicationRunner().start("--server.port=" + port, "--debug",
"--logging.level.org.springframework.cloud.function=DEBUG");
deploy("sample", "maven://com.example:function-sample-pojo:1.0.0.BUILD-SNAPSHOT");
}
private static void deploy(String name, String path) throws Exception {
@@ -79,23 +79,31 @@ public class FunctionExtractingFunctionCatalogIntegrationTests {
@Test
public void words() {
assertThat(new TestRestTemplate()
.getForObject("http://localhost:" + port + "/sample/words", String.class))
assertThat(new TestRestTemplate().getForObject(
"http://localhost:" + port + "/stream/sample/words", String.class))
.isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
}
@Test
public void missing() throws Exception {
ResponseEntity<String> result = new TestRestTemplate().exchange(RequestEntity
.get(new URI("http://localhost:" + port + "/stream/missing/words"))
.build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
public void uppercase() {
assertThat(new TestRestTemplate().postForObject(
"http://localhost:" + port + "/sample/uppercase", "{\"value\":\"foo\"}",
String.class)).isEqualTo("[{\"value\":\"FOO\"}]");
"http://localhost:" + port + "/stream/sample/uppercase",
"{\"value\":\"foo\"}", String.class)).isEqualTo("{\"value\":\"FOO\"}");
}
@Test
public void another() throws Exception {
deploy("strings", "maven://io.spring.sample:function-sample:1.0.0.BUILD-SNAPSHOT");
assertThat(new TestRestTemplate().getForObject(
"http://localhost:" + port + "/strings/words", String.class))
"http://localhost:" + port + "/stream/strings/words", String.class))
.isEqualTo("[\"foo\",\"bar\"]");
}
@@ -106,13 +114,13 @@ public class FunctionExtractingFunctionCatalogIntegrationTests {
assertThat(undeploy.contains(
"\"path\":\"maven://io.spring.sample:function-sample-pojo:1.0.0.BUILD-SNAPSHOT\""));
ResponseEntity<String> result = new TestRestTemplate().exchange(RequestEntity
.get(new URI("http://localhost:" + port + "/sample/words")).build(),
String.class);
.get(new URI("http://localhost:" + port + "/stream/sample/words"))
.build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
deploy("sample", "maven://io.spring.sample:function-sample-pojo:1.0.0.BUILD-SNAPSHOT");
assertThat(new TestRestTemplate().postForObject(
"http://localhost:" + port + "/sample/uppercase", "{\"value\":\"foo\"}",
String.class)).isEqualTo("[{\"value\":\"FOO\"}]");
"http://localhost:" + port + "/stream/sample/uppercase",
"{\"value\":\"foo\"}", String.class)).isEqualTo("{\"value\":\"FOO\"}");
}
}

View File

@@ -26,6 +26,7 @@
<aws-lambda-events.version>1.2.1</aws-lambda-events.version>
<reactor.version>3.0.7.RELEASE</reactor.version>
<spring-cloud-function.version>1.0.0.BUILD-SNAPSHOT</spring-cloud-function.version>
<spring-cloud-stream-servlet.version>1.0.0.BUILD-SNAPSHOT</spring-cloud-stream-servlet.version>
<start-class>example.Config</start-class>
</properties>
@@ -36,7 +37,12 @@
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-web</artifactId>
<artifactId>spring-cloud-function-stream</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-servlet</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
@@ -72,6 +78,11 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-servlet</artifactId>
<version>${spring-cloud-stream-servlet.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -1 +1,2 @@
dependencies.spring-cloud-function-web: org.springframework.cloud:spring-cloud-function-web
dependencies.spring-cloud-function-stream: org.springframework.cloud:spring-cloud-function-stream
dependencies.spring-cloud-stream-binder-servlet: org.springframework.cloud:spring-cloud-stream-binder-servlet

View File

@@ -26,17 +26,13 @@
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
<artifactId>spring-cloud-stream-binder-servlet</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
@@ -65,6 +61,11 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-servlet</artifactId>
<version>${spring-cloud-stream-servlet.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -16,8 +16,6 @@
package com.example;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -28,17 +26,16 @@ import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
/**
* @author Mark Fisher
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
properties = {
"spring.cloud.function.compile.test.lambda=com.example.SampleCompiledConsumerTests.Reference::set",
"spring.cloud.function.compile.test.inputType=String",
"spring.cloud.function.compile.test.type=consumer"
})
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = {
"spring.cloud.function.compile.test.lambda=com.example.SampleCompiledConsumerTests.Reference::set",
"spring.cloud.function.compile.test.inputType=String",
"spring.cloud.function.compile.test.type=consumer" })
public class SampleCompiledConsumerTests {
@LocalServerPort
@@ -47,8 +44,8 @@ public class SampleCompiledConsumerTests {
@Test
public void print() {
assertThat(new TestRestTemplate().postForObject(
"http://localhost:" + port + "/test", "it works",
String.class)).isEqualTo("it works");
"http://localhost:" + port + "/stream/test", "it works", String.class))
.isEqualTo("it works");
assertEquals("it works", Reference.instance);
}

View File

@@ -43,7 +43,7 @@ public class SampleCompiledFunctionTests {
@Test
public void lowercase() {
assertThat(new TestRestTemplate().postForObject(
"http://localhost:" + port + "/test", "it works", String.class))
"http://localhost:" + port + "/stream/test", "it works", String.class))
.isEqualTo("it works!!!");
}

View File

@@ -22,16 +22,18 @@
<java.version>1.8</java.version>
<reactor.version>3.1.1.RELEASE</reactor.version>
<spring-cloud-function.version>1.0.0.BUILD-SNAPSHOT</spring-cloud-function.version>
<spring-cloud-stream-servlet.version>1.0.0.BUILD-SNAPSHOT</spring-cloud-stream-servlet.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-web</artifactId>
<artifactId>spring-cloud-function-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-context</artifactId>
<artifactId>spring-cloud-stream-binder-servlet</artifactId>
<version>${spring-cloud-stream-servlet.version}</version>
</dependency>
</dependencies>

View File

@@ -27,7 +27,11 @@
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-web</artifactId>
<artifactId>spring-cloud-function-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-servlet</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -40,7 +40,7 @@ public class SampleApplicationTests {
@Test
public void words() {
assertThat(new TestRestTemplate()
.getForObject("http://localhost:" + port + "/words", String.class))
.getForObject("http://localhost:" + port + "/stream/words", String.class))
.isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
}
@@ -48,14 +48,14 @@ public class SampleApplicationTests {
public void uppercase() {
// TODO: make this work with a JSON stream as well (like in WebFlux)
assertThat(new TestRestTemplate().postForObject(
"http://localhost:" + port + "/uppercase", "[{\"value\":\"foo\"}]",
"http://localhost:" + port + "/stream/uppercase", "[{\"value\":\"foo\"}]",
String.class)).isEqualTo("[{\"value\":\"FOO\"}]");
}
@Test
public void lowercase() {
assertThat(new TestRestTemplate().postForObject(
"http://localhost:" + port + "/lowercase", "[{\"value\":\"Foo\"}]",
"http://localhost:" + port + "/stream/lowercase", "[{\"value\":\"Foo\"}]",
String.class)).isEqualTo("[{\"value\":\"foo\"}]");
}

View File

@@ -31,7 +31,11 @@
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-context</artifactId>
<artifactId>spring-cloud-stream-binder-servlet</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>

View File

@@ -27,11 +27,11 @@
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-web</artifactId>
<artifactId>spring-cloud-function-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-context</artifactId>
<artifactId>spring-cloud-stream-binder-servlet</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>

View File

@@ -0,0 +1,9 @@
<<<<<<< HEAD:spring-cloud-function-samples/function-sample/src/main/resources/META-INF/thin-stream.properties
boms.spring-cloud-dependencies: org.springframework.cloud:spring-cloud-dependencies:Edgware.RELEASE
dependencies.spring-cloud-function-stream: org.springframework.cloud:spring-cloud-function-stream:1.0.0.BUILD-SNAPSHOT
dependencies.spring-cloud-stream-rabbit: org.springframework.cloud:spring-cloud-starter-stream-rabbit
=======
boms.spring-cloud-dependencies: org.springframework.cloud:spring-cloud-dependencies:Dalston.RELEASE
exclusions.spring-cloud-function-stream-binder-servlet: org.springframework.cloud:spring-cloud-stream-binder-servlet
dependencies.spring-cloud-stream-rabbit: org.springframework.cloud:spring-cloud-stream-binder-rabbit
>>>>>>> Migrate to servlet binder for web features:spring-cloud-function-samples/function-sample/src/main/resources/META-INF/thin-rabbit.properties

View File

@@ -1,3 +0,0 @@
boms.spring-cloud-dependencies: org.springframework.cloud:spring-cloud-dependencies:Edgware.RELEASE
dependencies.spring-cloud-function-stream: org.springframework.cloud:spring-cloud-function-stream:1.0.0.BUILD-SNAPSHOT
dependencies.spring-cloud-stream-rabbit: org.springframework.cloud:spring-cloud-starter-stream-rabbit

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-function-stream</artifactId>
@@ -13,6 +14,10 @@
<version>1.0.0.BUILD-SNAPSHOT</version>
</parent>
<properties>
<servlet-binder.version>1.0.0.BUILD-SNAPSHOT</servlet-binder.version>
</properties>
<dependencies>
<dependency>
<groupId>io.projectreactor</groupId>
@@ -41,7 +46,8 @@
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
<artifactId>spring-cloud-stream-binder-servlet</artifactId>
<version>${servlet-binder.version}</version>
<optional>true</optional>
</dependency>
<dependency>

View File

@@ -1,61 +0,0 @@
/*
* 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.cloud.function.stream;
import java.util.function.Supplier;
import org.springframework.cloud.function.core.FunctionCatalog;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.endpoint.MessageProducerSupport;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.Assert;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
/**
* @author Mark Fisher
*/
public class SupplierInvokingMessageProducer<T> extends MessageProducerSupport {
private final FunctionCatalog functionCatalog;
public SupplierInvokingMessageProducer(FunctionCatalog registry) {
this.functionCatalog = registry;
this.setOutputChannelName(Source.OUTPUT);
}
@Override
protected void doStart() {
supplier().subscribeOn(Schedulers.elastic()).subscribe(m -> this.sendMessage(m));
}
private Flux<Message<?>> supplier() {
Supplier<Flux<?>> supplier = null;
Flux<Message<?>> result = Flux.empty();
for (String name : functionCatalog.getSupplierNames()) {
supplier = functionCatalog.lookupSupplier(name);
Assert.notNull(supplier, "Supplier must not be null");
result = Flux.merge(result,
supplier.get().map(payload -> MessageBuilder.withPayload(payload)
.setHeader(StreamConfigurationProperties.ROUTE_KEY, name)
.build()));
}
return result;
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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.
* 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.cloud.function.stream.config;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration;
import org.springframework.cloud.function.core.FunctionCatalog;
import org.springframework.cloud.stream.binder.servlet.RouteRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Dave Syer
*
*/
@Configuration
@ConditionalOnBean(FunctionCatalog.class)
@ConditionalOnClass(RouteRegistry.class)
@AutoConfigureAfter(ContextFunctionCatalogAutoConfiguration.class)
public class RouteRegistryAutoConfiguration {
@Bean
public RouteRegistry supplierRoutes(FunctionCatalog registry) {
return () -> registry.getSupplierNames();
}
}

View File

@@ -14,14 +14,14 @@
* limitations under the License.
*/
package org.springframework.cloud.function.stream;
package org.springframework.cloud.function.stream.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.function.context.FunctionInspector;
import org.springframework.cloud.function.context.catalog.FunctionInspector;
import org.springframework.cloud.function.core.FunctionCatalog;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.binder.Binder;
@@ -39,7 +39,7 @@ import org.springframework.context.annotation.Lazy;
@ConditionalOnBean(FunctionCatalog.class)
@ConditionalOnProperty(name = "spring.cloud.stream.enabled", havingValue = "true", matchIfMissing = true)
@EnableBinding(Processor.class)
public class StreamConfiguration {
public class StreamAutoConfiguration {
@Autowired
private StreamConfigurationProperties properties;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.function.stream;
package org.springframework.cloud.function.stream.config;
import org.springframework.boot.context.properties.ConfigurationProperties;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.function.stream;
package org.springframework.cloud.function.stream.config;
import java.util.ArrayList;
import java.util.HashMap;
@@ -26,7 +26,7 @@ import java.util.function.Consumer;
import java.util.function.Function;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.cloud.function.context.FunctionInspector;
import org.springframework.cloud.function.context.catalog.FunctionInspector;
import org.springframework.cloud.function.core.FunctionCatalog;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
@@ -101,7 +101,11 @@ public class StreamListeningFunctionInvoker implements SmartInitializingSingleto
}
private Message<?> message(Object result, Map<String, Object> headers) {
return result instanceof Message ? (Message<?>) result
return result instanceof Message
// TODO: why do we have to do this? The headers should have come with the
// result.
? MessageBuilder.fromMessage((Message<?>) result)
.copyHeadersIfAbsent(headers).build()
: MessageBuilder.withPayload(result).copyHeadersIfAbsent(headers).build();
}

View File

@@ -0,0 +1,110 @@
/*
* 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.cloud.function.stream.config;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import org.springframework.cloud.function.core.FunctionCatalog;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.endpoint.MessageProducerSupport;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
/**
* @author Mark Fisher
*/
public class SupplierInvokingMessageProducer<T> extends MessageProducerSupport {
private final FunctionCatalog functionCatalog;
private final Set<String> suppliers = new HashSet<>();
private final Map<String, Disposable> disposables = new HashMap<>();
public SupplierInvokingMessageProducer(FunctionCatalog registry) {
this.functionCatalog = registry;
this.setOutputChannelName(Source.OUTPUT);
}
@Override
protected void doStart() {
for (String name : functionCatalog.getSupplierNames()) {
start(name);
}
}
@Override
protected void doStop() {
for (String name : new HashSet<>(suppliers)) {
stop(name);
}
}
public void stop(String name) {
if (disposables.containsKey(name)) {
synchronized (disposables) {
if (disposables.containsKey(name)) {
try {
disposables.get(name).dispose();
}
finally {
disposables.remove(name);
suppliers.remove(name);
}
}
}
}
}
public void start(String name) {
if (!disposables.containsKey(name)) {
synchronized (disposables) {
if (!disposables.containsKey(name)) {
Supplier<Flux<?>> supplier = functionCatalog.lookupSupplier(name);
if (supplier != null) {
suppliers.add(name);
disposables.put(name,
supplier.get().subscribeOn(Schedulers.elastic()).subscribe(m -> send(name, m)));
}
}
}
}
}
private void send(String name, Object payload) {
Message<?> message;
if (payload instanceof Message) {
message = MessageBuilder.fromMessage((Message<?>) payload)
.setHeaderIfAbsent(StreamConfigurationProperties.ROUTE_KEY, name)
.build();
}
else {
message = MessageBuilder.withPayload(payload)
.setHeader(StreamConfigurationProperties.ROUTE_KEY, name).build();
}
getOutputChannel().send(message);
}
}

View File

@@ -1,2 +1,3 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.function.stream.StreamConfiguration
org.springframework.cloud.function.stream.config.StreamAutoConfiguration,\
org.springframework.cloud.function.stream.config.RouteRegistryAutoConfiguration

View File

@@ -29,7 +29,7 @@ import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.function.stream.StreamConfigurationProperties;
import org.springframework.cloud.function.stream.config.StreamConfigurationProperties;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.context.annotation.Bean;

View File

@@ -29,7 +29,7 @@ import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.function.stream.StreamConfigurationProperties;
import org.springframework.cloud.function.stream.config.StreamConfigurationProperties;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.context.annotation.Bean;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.function.scan;
package org.springframework.cloud.function.stream.scan;
import java.net.URI;
import java.util.function.Function;
@@ -28,6 +28,7 @@ import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.cloud.stream.test.binder.TestSupportBinderAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -64,14 +65,14 @@ public class ComponentTests {
public void greeter() throws Exception {
ResponseEntity<String> result = rest
.exchange(
RequestEntity.post(new URI("/greeter"))
RequestEntity.post(new URI("/stream/greeter"))
.contentType(MediaType.TEXT_PLAIN).body("World"),
String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).isEqualTo("Hello World");
}
@SpringBootApplication
@SpringBootApplication(exclude=TestSupportBinderAutoConfiguration.class)
@ComponentScan
protected static class TestConfiguration {
}

View File

@@ -0,0 +1,94 @@
/*
* 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.cloud.function.stream.supplier;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.function.stream.config.StreamConfigurationProperties;
import org.springframework.cloud.function.stream.config.SupplierInvokingMessageProducer;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.Message;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Marius Bogoevici
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RestartStreamSupplierTests.StreamingFunctionApplication.class)
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
public class RestartStreamSupplierTests {
@Autowired
Source source;
@Autowired
MessageCollector messageCollector;
@Autowired
SupplierInvokingMessageProducer<?> producer;
@Rule
public ExpectedException expected = ExpectedException.none();
@Test
public void exhausted() throws Exception {
test();
expected.expect(NullPointerException.class);
test();
}
@Test
public void restart() throws Exception {
test();
assertThat(messageCollector.forChannel(source.output())).isEmpty();
producer.stop();
producer.start();
test();
}
private void test() throws Exception {
Message<?> result = messageCollector.forChannel(source.output()).poll(1000,
TimeUnit.MILLISECONDS);
assertThat(result.getPayload()).isEqualTo("foo");
assertThat(result.getHeaders().get(StreamConfigurationProperties.ROUTE_KEY))
.isEqualTo("simpleSupplier");
}
@SpringBootApplication
public static class StreamingFunctionApplication {
@Bean
public Supplier<String> simpleSupplier() {
return () -> "foo";
}
}
}

View File

@@ -25,7 +25,7 @@ import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.function.stream.StreamConfigurationProperties;
import org.springframework.cloud.function.stream.config.StreamConfigurationProperties;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.context.annotation.Bean;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.function.web;
package org.springframework.cloud.function.stream.web;
import java.net.URI;
import java.util.function.Supplier;
@@ -28,7 +28,9 @@ import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.cloud.stream.test.binder.TestSupportBinderAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
@@ -43,7 +45,7 @@ import reactor.core.publisher.Flux;
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.cloud.function.web.path=/functions")
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.cloud.stream.binder.servlet.prefix=/functions")
public class PrefixTests {
@LocalServerPort
@@ -66,8 +68,8 @@ public class PrefixTests {
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@EnableAutoConfiguration
@org.springframework.boot.test.context.TestConfiguration
@EnableAutoConfiguration(exclude=TestSupportBinderAutoConfiguration.class)
@Configuration
protected static class TestConfiguration {
@Bean({ "words", "get/more" })
public Supplier<Flux<String>> words() {

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.web;
package org.springframework.cloud.function.stream.web;
import java.net.URI;
import java.time.Duration;
@@ -34,7 +34,6 @@ import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
@@ -47,6 +46,8 @@ import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.StringUtils;
@@ -59,7 +60,10 @@ import reactor.core.publisher.Flux;
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = {
"logging.level.org.springframework.integration=DEBUG",
"spring.autoconfigure.exclude=org.springframework.cloud.stream.test.binder.TestSupportBinderAutoConfiguration" })
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
public class RestApplicationTests {
private static final MediaType EVENT_STREAM = MediaType.TEXT_EVENT_STREAM;
@@ -76,21 +80,17 @@ public class RestApplicationTests {
}
@Test
public void staticResource() throws Exception {
assertThat(rest.getForObject("/test.html", String.class)).contains("<body>Test");
}
@Test
@Ignore("Needs Spring 5?")
public void wordsSSE() throws Exception {
assertThat(rest.exchange(
RequestEntity.get(new URI("/words")).accept(EVENT_STREAM).build(),
RequestEntity.get(new URI("/stream/words")).accept(EVENT_STREAM).build(),
String.class).getBody()).isEqualTo(sse("foo", "bar"));
}
@Test
public void wordsJson() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/words"))
.exchange(RequestEntity.get(new URI("/stream/words"))
.accept(MediaType.APPLICATION_JSON).build(), String.class)
.getBody()).isEqualTo("[\"foo\",\"bar\"]");
}
@@ -99,31 +99,31 @@ public class RestApplicationTests {
@Ignore("Fix error handling")
public void errorJson() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/bang"))
.exchange(RequestEntity.get(new URI("/stream/bang"))
.accept(MediaType.APPLICATION_JSON).build(), String.class)
.getBody()).isEqualTo("[\"foo\"]");
}
@Test
public void words() throws Exception {
ResponseEntity<String> result = rest
.exchange(RequestEntity.get(new URI("/words")).build(), String.class);
ResponseEntity<String> result = rest.exchange(
RequestEntity.get(new URI("/stream/words")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
}
@Test
public void word() throws Exception {
ResponseEntity<String> result = rest
.exchange(RequestEntity.get(new URI("/word")).build(), String.class);
ResponseEntity<String> result = rest.exchange(
RequestEntity.get(new URI("/stream/word")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).isEqualTo("foo");
assertThat(result.getBody()).isEqualTo("[\"foo\"]");
}
@Test
public void foos() throws Exception {
ResponseEntity<String> result = rest
.exchange(RequestEntity.get(new URI("/foos")).build(), String.class);
ResponseEntity<String> result = rest.exchange(
RequestEntity.get(new URI("/stream/foos")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody())
.isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
@@ -131,9 +131,9 @@ public class RestApplicationTests {
@Test
public void qualifierFoos() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity.post(new URI("/foos"))
.contentType(MediaType.APPLICATION_JSON).body("[\"foo\",\"bar\"]"),
String.class);
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/stream/foos")).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody())
.isEqualTo("[{\"value\":\"[FOO]\"},{\"value\":\"[BAR]\"}]");
@@ -141,25 +141,26 @@ public class RestApplicationTests {
@Test
public void getMore() throws Exception {
ResponseEntity<String> result = rest
.exchange(RequestEntity.get(new URI("/get/more")).build(), String.class);
ResponseEntity<String> result = rest.exchange(
RequestEntity.get(new URI("/stream/get/more")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
}
@Test
public void bareWords() throws Exception {
ResponseEntity<String> result = rest
.exchange(RequestEntity.get(new URI("/bareWords")).build(), String.class);
ResponseEntity<String> result = rest.exchange(
RequestEntity.get(new URI("/stream/bareWords")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
assertThat(result.getBody()).isEqualTo("[[\"foo\",\"bar\"]]");
}
@Test
@Ignore("Should this even work? Or do we need to be explicit about the JSON?")
public void updates() throws Exception {
ResponseEntity<String> result = rest.exchange(
RequestEntity.post(new URI("/updates")).body("one\ntwo"), String.class);
RequestEntity.post(new URI("/stream/updates")).body("one\ntwo"),
String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
assertThat(test.list).hasSize(2);
assertThat(result.getBody()).isEqualTo("onetwo");
@@ -168,7 +169,7 @@ public class RestApplicationTests {
@Test
public void updatesJson() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/updates")).contentType(MediaType.APPLICATION_JSON)
.post(new URI("/stream/updates")).contentType(MediaType.APPLICATION_JSON)
.body("[\"one\",\"two\"]"), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
assertThat(test.list).hasSize(2);
@@ -178,19 +179,20 @@ public class RestApplicationTests {
@Test
public void addFoos() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/addFoos")).contentType(MediaType.APPLICATION_JSON)
.post(new URI("/stream/addFoos")).contentType(MediaType.APPLICATION_JSON)
.body("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
assertThat(test.list).hasSize(2);
assertThat(result.getBody())
.isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
.isEqualTo("[{\"value\":\"foo\"}, {\"value\":\"bar\"}]");
}
@Test
public void bareUpdates() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/bareUpdates")).contentType(MediaType.APPLICATION_JSON)
.body("[\"one\",\"two\"]"), String.class);
.post(new URI("/stream/bareUpdates"))
.contentType(MediaType.APPLICATION_JSON).body("[\"one\",\"two\"]"),
String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
assertThat(test.list).hasSize(2);
assertThat(result.getBody()).isEqualTo("[\"one\",\"two\"]");
@@ -199,7 +201,7 @@ public class RestApplicationTests {
@Test
public void timeoutJson() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/timeout"))
.exchange(RequestEntity.get(new URI("/stream/timeout"))
.accept(MediaType.APPLICATION_JSON).build(), String.class)
.getBody()).isEqualTo("[\"foo\"]");
}
@@ -207,23 +209,22 @@ public class RestApplicationTests {
@Test
public void emptyJson() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/empty"))
.exchange(RequestEntity.get(new URI("/stream/empty"))
.accept(MediaType.APPLICATION_JSON).build(), String.class)
.getBody()).isEqualTo("[]");
}
@Test
public void sentences() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/sentences")).build(), String.class)
.getBody()).isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]");
assertThat(rest.exchange(RequestEntity.get(new URI("/stream/sentences")).build(),
String.class).getBody())
.isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]");
}
@Test
public void sentencesAcceptAny() throws Exception {
assertThat(rest.exchange(
RequestEntity.get(new URI("/sentences")).accept(MediaType.ALL).build(),
String.class).getBody())
assertThat(rest.exchange(RequestEntity.get(new URI("/stream/sentences"))
.accept(MediaType.ALL).build(), String.class).getBody())
.isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]");
}
@@ -231,7 +232,7 @@ public class RestApplicationTests {
public void sentencesAcceptJson() throws Exception {
ResponseEntity<String> result = rest
.exchange(
RequestEntity.get(new URI("/sentences"))
RequestEntity.get(new URI("/stream/sentences"))
.accept(MediaType.APPLICATION_JSON).build(),
String.class);
assertThat(result.getBody()).isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]");
@@ -240,9 +241,10 @@ public class RestApplicationTests {
}
@Test
@Ignore("Maybe not supported")
public void sentencesAcceptSse() throws Exception {
ResponseEntity<String> result = rest.exchange(
RequestEntity.get(new URI("/sentences")).accept(EVENT_STREAM).build(),
ResponseEntity<String> result = rest.exchange(RequestEntity
.get(new URI("/stream/sentences")).accept(EVENT_STREAM).build(),
String.class);
assertThat(result.getBody())
.isEqualTo(sse("[\"go\",\"home\"]", "[\"come\",\"back\"]"));
@@ -253,15 +255,16 @@ public class RestApplicationTests {
@Test
public void uppercase() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/uppercase")).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class);
.post(new URI("/stream/uppercase"))
.contentType(MediaType.APPLICATION_JSON).body("[\"foo\",\"bar\"]"),
String.class);
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
}
@Test
public void messages() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/messages")).contentType(MediaType.APPLICATION_JSON)
.post(new URI("/stream/messages")).contentType(MediaType.APPLICATION_JSON)
.header("x-foo", "bar").body("[\"foo\",\"bar\"]"), String.class);
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
assertThat(result.getHeaders().getFirst("x-foo")).isEqualTo("bar");
@@ -271,7 +274,7 @@ public class RestApplicationTests {
@Test
public void headers() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/headers")).contentType(MediaType.APPLICATION_JSON)
.post(new URI("/stream/headers")).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class);
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
assertThat(result.getHeaders().getFirst("foo")).isEqualTo("bar");
@@ -282,7 +285,7 @@ public class RestApplicationTests {
public void uppercaseSingleValue() throws Exception {
ResponseEntity<String> result = rest
.exchange(
RequestEntity.post(new URI("/uppercase"))
RequestEntity.post(new URI("/stream/uppercase"))
.contentType(MediaType.TEXT_PLAIN).body("foo"),
String.class);
assertThat(result.getBody()).isEqualTo("(FOO)");
@@ -292,7 +295,7 @@ public class RestApplicationTests {
@Ignore("WebFlux would split the request body into lines: TODO make this work the same")
public void uppercasePlainText() throws Exception {
ResponseEntity<String> result = rest.exchange(
RequestEntity.post(new URI("/uppercase"))
RequestEntity.post(new URI("/stream/uppercase"))
.contentType(MediaType.TEXT_PLAIN).body("foo\nbar"),
String.class);
assertThat(result.getBody()).isEqualTo("(FOO)(BAR)");
@@ -301,7 +304,7 @@ public class RestApplicationTests {
@Test
public void uppercaseFoos() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/upFoos")).contentType(MediaType.APPLICATION_JSON)
.post(new URI("/stream/upFoos")).contentType(MediaType.APPLICATION_JSON)
.body("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"), String.class);
assertThat(result.getBody())
.isEqualTo("[{\"value\":\"FOO\"},{\"value\":\"BAR\"}]");
@@ -311,16 +314,19 @@ public class RestApplicationTests {
public void uppercaseFoo() throws Exception {
// Single Foo can be parsed
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/upFoos")).contentType(MediaType.APPLICATION_JSON)
.post(new URI("/stream/upFoos")).contentType(MediaType.APPLICATION_JSON)
.body("{\"value\":\"foo\"}"), String.class);
assertThat(result.getBody()).isEqualTo("[{\"value\":\"FOO\"}]");
assertThat(result.getBody()).isEqualTo("{\"value\":\"FOO\"}");
}
@Test
public void bareUppercaseFoos() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/bareUpFoos")).contentType(MediaType.APPLICATION_JSON)
.body("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"), String.class);
ResponseEntity<String> result = rest
.exchange(
RequestEntity.post(new URI("/stream/bareUpFoos"))
.contentType(MediaType.APPLICATION_JSON)
.body("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"),
String.class);
assertThat(result.getBody())
.isEqualTo("[{\"value\":\"FOO\"},{\"value\":\"BAR\"}]");
}
@@ -330,79 +336,84 @@ public class RestApplicationTests {
// Single Foo can be parsed and returns a single value if the function is defined
// that way
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/bareUpFoos")).contentType(MediaType.APPLICATION_JSON)
.body("{\"value\":\"foo\"}"), String.class);
.post(new URI("/stream/bareUpFoos"))
.contentType(MediaType.APPLICATION_JSON).body("{\"value\":\"foo\"}"),
String.class);
assertThat(result.getBody()).isEqualTo("{\"value\":\"FOO\"}");
}
@Test
public void bareUppercase() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/bareUppercase")).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class);
.post(new URI("/stream/bareUppercase"))
.contentType(MediaType.APPLICATION_JSON).body("[\"foo\",\"bar\"]"),
String.class);
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
}
@Test
public void transform() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/transform")).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class);
.post(new URI("/stream/transform"))
.contentType(MediaType.APPLICATION_JSON).body("[\"foo\",\"bar\"]"),
String.class);
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
}
@Test
public void postMore() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/post/more")).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class);
.post(new URI("/stream/post/more"))
.contentType(MediaType.APPLICATION_JSON).body("[\"foo\",\"bar\"]"),
String.class);
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
}
@Test
public void postMoreFoo() {
assertThat(rest.getForObject("/post/more/foo", String.class)).isEqualTo("(FOO)");
assertThat(rest.getForObject("/stream/post/more/foo", String.class))
.isEqualTo("(FOO)");
}
@Test
public void uppercaseGet() {
assertThat(rest.getForObject("/uppercase/foo", String.class)).isEqualTo("(FOO)");
assertThat(rest.getForObject("/stream/uppercase/foo", String.class))
.isEqualTo("(FOO)");
}
@Test
public void convertGet() {
assertThat(rest.getForObject("/wrap/123", String.class)).isEqualTo("..123..");
assertThat(rest.getForObject("/stream/wrap/123", String.class))
.isEqualTo("..123..");
}
@Test
public void convertPost() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity.post(new URI("/wrap"))
ResponseEntity<String> result = rest.exchange(RequestEntity.post(new URI("/stream/wrap"))
.contentType(MediaType.TEXT_PLAIN).body("123"), String.class);
assertThat(result.getBody()).isEqualTo("..123..");
}
@Test
public void convertPostJson() throws Exception {
// If you POST a single value to a Function<Flux<Integer>,Flux<Integer>> it can't
// determine if the output is single valued, so it has to send an array back
ResponseEntity<String> result = rest
.exchange(
RequestEntity.post(new URI("/doubler"))
RequestEntity.post(new URI("/stream/doubler"))
.contentType(MediaType.TEXT_PLAIN).body("123"),
String.class);
assertThat(result.getBody()).isEqualTo("[246]");
assertThat(result.getBody()).isEqualTo("246");
}
@Test
public void supplierFirst() {
assertThat(rest.getForObject("/not/a/function", String.class))
assertThat(rest.getForObject("/stream/not/a/function", String.class))
.isEqualTo("[\"hello\"]");
}
@Test
public void convertGetJson() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/entity/321"))
.exchange(RequestEntity.get(new URI("/stream/entity/321"))
.accept(MediaType.APPLICATION_JSON).build(), String.class)
.getBody()).isEqualTo("{\"value\":321}");
}
@@ -410,7 +421,7 @@ public class RestApplicationTests {
@Test
public void uppercaseJsonArray() throws Exception {
assertThat(rest.exchange(
RequestEntity.post(new URI("/maps"))
RequestEntity.post(new URI("/stream/maps"))
.contentType(MediaType.APPLICATION_JSON)
// The new line in the middle is optional
.body("[{\"value\":\"foo\"},\n{\"value\":\"bar\"}]"),
@@ -419,8 +430,9 @@ public class RestApplicationTests {
}
@Test
@Ignore("Doesn't make sense: if you post in an array you expect to get back an array")
public void uppercaseSSE() throws Exception {
assertThat(rest.exchange(RequestEntity.post(new URI("/uppercase"))
assertThat(rest.exchange(RequestEntity.post(new URI("/stream/uppercase"))
.accept(EVENT_STREAM).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class).getBody())
.isEqualTo(sse("(FOO)", "(BAR)"));
@@ -430,7 +442,6 @@ public class RestApplicationTests {
return "data:" + StringUtils.arrayToDelimitedString(values, "\n\ndata:") + "\n\n";
}
@EnableAutoConfiguration
@TestConfiguration
public static class ApplicationConfiguration {
@@ -541,7 +552,7 @@ public class RestApplicationTests {
public Supplier<Flux<String>> bang() {
return () -> Flux.fromArray(new String[] { "foo", "bar" }).map(value -> {
if (value.equals("bar")) {
throw new RuntimeException("Bar");
// throw new RuntimeException("Bar");
}
return value;
});
@@ -549,7 +560,7 @@ public class RestApplicationTests {
@Bean
public Supplier<Flux<String>> empty() {
return () -> Flux.fromIterable(Collections.emptyList());
return () -> Flux.empty();
}
@Bean("not/a/function")

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.function.web;
package org.springframework.cloud.function.stream.web;
import java.net.URI;
import java.util.function.Supplier;
@@ -28,15 +28,17 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.cloud.function.stream.web.SingletonTests.TestApplicationConfiguration;
import org.springframework.core.PriorityOrdered;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
@@ -48,7 +50,9 @@ import reactor.core.publisher.Flux;
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "debug=true",
"spring.autoconfigure.exclude=org.springframework.cloud.stream.test.binder.TestSupportBinderAutoConfiguration"})
@ContextConfiguration(classes = TestApplicationConfiguration.class)
public class SingletonTests {
@LocalServerPort
@@ -59,35 +63,33 @@ public class SingletonTests {
@Test
public void words() throws Exception {
ResponseEntity<String> result = rest.exchange(
RequestEntity.get(new URI("/words")).build(), String.class);
RequestEntity.get(new URI("/stream/words")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
}
@EnableAutoConfiguration
@org.springframework.boot.test.context.TestConfiguration
protected static class TestConfiguration {
@Bean
public static BeanDefinitionRegistryPostProcessor processor() {
return new BeanDefinitionRegistryPostProcessor() {
@TestConfiguration
protected static class TestApplicationConfiguration
implements PriorityOrdered, BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
}
@Override
public void postProcessBeanFactory(
ConfigurableListableBeanFactory beanFactory)
throws BeansException {
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
throws BeansException {
// Simulates what happens when you add a compiled function
RootBeanDefinition beanDefinition = new RootBeanDefinition(MySupplier.class);
registry.registerBeanDefinition("words", beanDefinition);
}
@Override
public void postProcessBeanDefinitionRegistry(
BeanDefinitionRegistry registry) throws BeansException {
// Simulates what happens when you add a compiled function
RootBeanDefinition beanDefinition = new RootBeanDefinition(MySupplier.class);
registry.registerBeanDefinition("words", beanDefinition);
}
};
@Override
public int getOrder() {
return 0;
}
}
static class MySupplier implements Supplier<Flux<String>> {
@Override
public Flux<String> get() {

View File

@@ -1,50 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-function-web</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Function Web Support</name>
<description>Spring Cloud Function Web Support</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-context</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot.experimental</groupId>
<artifactId>spring-boot-thin-layout</artifactId>
<version>${wrapper.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,123 +0,0 @@
/*
* 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.
* 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.cloud.function.web.flux;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.function.context.FunctionInspector;
import org.springframework.cloud.function.web.flux.request.FluxRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @author Dave Syer
* @author Mark Fisher
*/
@Component
public class FunctionController {
private static Log logger = LogFactory.getLog(FunctionController.class);
private FunctionInspector inspector;
private boolean debug = false;
public FunctionController(FunctionInspector inspector) {
this.inspector = inspector;
}
public void setDebug(boolean debug) {
this.debug = debug;
}
@PostMapping(path = "/**")
@ResponseBody
public ResponseEntity<Flux<?>> post(
@RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.constants.WebRequestConstants.function") Function<Flux<?>, Flux<?>> function,
@RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.constants.WebRequestConstants.consumer") Consumer<Flux<?>> consumer,
@RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.constants.WebRequestConstants.input_single") Boolean single,
@RequestBody FluxRequest<?> body) {
if (function != null) {
Flux<?> flux = body.flux();
if (debug) {
flux = flux.log();
}
Flux<?> result = function.apply(flux);
if (logger.isDebugEnabled()) {
logger.debug("Handled POST with function");
}
return ResponseEntity.ok().body(debug ? result.log() : result);
}
if (consumer != null) {
Flux<?> flux = body.flux().cache(); // send a copy back to the caller
if (debug) {
flux = flux.log();
}
consumer.accept(flux);
if (logger.isDebugEnabled()) {
logger.debug("Handled POST with consumer");
}
return ResponseEntity.status(HttpStatus.ACCEPTED).body(flux);
}
throw new IllegalArgumentException("no such function");
}
@GetMapping(path = "/**")
@ResponseBody
public Object get(
@RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.constants.WebRequestConstants.function") Function<Flux<?>, Flux<?>> function,
@RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.constants.WebRequestConstants.supplier") Supplier<Flux<?>> supplier,
@RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.constants.WebRequestConstants.argument") String argument) {
if (function != null) {
return value(function, argument);
}
return supplier(supplier);
}
private Flux<?> supplier(Supplier<Flux<?>> supplier) {
Flux<?> result = supplier.get();
if (logger.isDebugEnabled()) {
logger.debug("Handled GET with supplier");
}
return debug ? result.log() : result;
}
private Mono<?> value(Function<Flux<?>, Flux<?>> function,
@PathVariable String value) {
Object input = inspector.convert(function, value);
Mono<?> result = Mono.from(function.apply(Flux.just(input)));
if (logger.isDebugEnabled()) {
logger.debug("Handled GET with function");
}
return debug ? result.log() : result;
}
}

View File

@@ -1,161 +0,0 @@
/*
* Copyright 2012-2015 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.cloud.function.web.flux;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.function.context.FunctionInspector;
import org.springframework.cloud.function.core.FunctionCatalog;
import org.springframework.cloud.function.web.flux.constants.WebRequestConstants;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
* @author Dave Syer
*
*/
@Configuration
@ConditionalOnClass(RequestMappingHandlerMapping.class)
public class FunctionHandlerMapping extends RequestMappingHandlerMapping
implements InitializingBean {
private final FunctionCatalog functions;
private final FunctionController controller;
@Value("${spring.cloud.function.web.path:}")
private String prefix = "";
@Value("${debug:${DEBUG:false}}")
private String debug = "false";
@Autowired
public FunctionHandlerMapping(FunctionCatalog catalog, FunctionInspector inspector) {
this.functions = catalog;
logger.info("FunctionCatalog: " + catalog + ", FunctionInspector: " + inspector);
setOrder(super.getOrder() - 5);
this.controller = new FunctionController(inspector);
}
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
this.controller.setDebug(!"false".equals(debug));
detectHandlerMethods(controller);
while (prefix.endsWith("/")) {
prefix = prefix.substring(0, prefix.length() - 1);
}
}
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request)
throws Exception {
HandlerMethod handler = super.getHandlerInternal(request);
if (handler == null) {
return null;
}
String path = (String) request
.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
if (StringUtils.hasText(prefix) && !path.startsWith(prefix)) {
return null;
}
if (path.startsWith(prefix)) {
path = path.substring(prefix.length());
}
if (path == null) {
return handler;
}
Object function = findFunctionForGet(request, path);
if (function != null) {
if (logger.isDebugEnabled()) {
logger.debug("Found function for GET: " + path);
}
request.setAttribute(WebRequestConstants.HANDLER, function);
return handler;
}
function = findFunctionForPost(request, path);
if (function != null) {
if (logger.isDebugEnabled()) {
logger.debug("Found function for POST: " + path);
}
request.setAttribute(WebRequestConstants.HANDLER, function);
return handler;
}
return null;
}
private Object findFunctionForPost(HttpServletRequest request, String path) {
if (!request.getMethod().equals("POST")) {
return null;
}
path = path.startsWith("/") ? path.substring(1) : path;
Consumer<Object> consumer = functions.lookupConsumer(path);
if (consumer != null) {
request.setAttribute(WebRequestConstants.CONSUMER, consumer);
return consumer;
}
Function<Object, Object> function = functions.lookupFunction(path);
if (function != null) {
request.setAttribute(WebRequestConstants.FUNCTION, function);
return function;
}
return null;
}
private Object findFunctionForGet(HttpServletRequest request, String path) {
if (!request.getMethod().equals("GET")) {
return null;
}
path = path.startsWith("/") ? path.substring(1) : path;
Supplier<Object> supplier = functions.lookupSupplier(path);
if (supplier != null) {
request.setAttribute(WebRequestConstants.SUPPLIER, supplier);
return supplier;
}
StringBuilder builder = new StringBuilder();
String name = path;
String value = null;
for (String element : path.split("/")) {
if (builder.length() > 0) {
builder.append("/");
}
builder.append(element);
name = builder.toString();
value = path.length() > name.length() ? path.substring(name.length() + 1)
: null;
Function<Object, Object> function = functions.lookupFunction(name);
if (function != null) {
request.setAttribute(WebRequestConstants.FUNCTION, function);
request.setAttribute(WebRequestConstants.ARGUMENT, value);
return function;
}
}
return null;
}
}

View File

@@ -1,114 +0,0 @@
/*
* 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.
* 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.cloud.function.web.flux;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.cloud.function.context.FunctionInspector;
import org.springframework.cloud.function.core.FunctionCatalog;
import org.springframework.cloud.function.web.flux.request.FluxHandlerMethodArgumentResolver;
import org.springframework.cloud.function.web.flux.response.FluxReturnValueHandler;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ClassUtils;
import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import reactor.core.publisher.Flux;
/**
* @author Dave Syer
* @author Mark Fisher
*/
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Flux.class, AsyncHandlerMethodReturnValueHandler.class })
public class ReactorAutoConfiguration {
@Autowired
private ApplicationContext context;
@Bean
public FunctionHandlerMapping functionHandlerMapping(FunctionCatalog catalog,
FunctionInspector inspector) {
return new FunctionHandlerMapping(catalog, inspector);
}
@Configuration
@ConditionalOnMissingClass("org.springframework.core.ReactiveAdapter")
protected static class FluxReturnValueConfiguration {
@Bean
public FluxReturnValueHandler fluxReturnValueHandler(FunctionInspector inspector,
HttpMessageConverters converters) {
return new FluxReturnValueHandler(inspector, converters.getConverters());
}
}
@Configuration
protected static class FluxArgumentResolverConfiguration {
@Bean
public FluxHandlerMethodArgumentResolver fluxHandlerMethodArgumentResolver(
FunctionInspector inspector, ObjectMapper mapper) {
return new FluxHandlerMethodArgumentResolver(inspector, mapper);
}
}
@Bean
public BeanPostProcessor fluxRequestMappingHandlerAdapterProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof RequestMappingHandlerAdapter) {
RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(
adapter.getArgumentResolvers());
resolvers.add(0,
context.getBean(FluxHandlerMethodArgumentResolver.class));
adapter.setArgumentResolvers(resolvers);
if (!ClassUtils.isPresent("org.springframework.core.ReactiveAdapter",
null)) {
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(
adapter.getReturnValueHandlers());
handlers.add(0, context.getBean(FluxReturnValueHandler.class));
adapter.setReturnValueHandlers(handlers);
}
}
return bean;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
};
}
}

View File

@@ -1,39 +0,0 @@
/*
* 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.
* 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.cloud.function.web.flux.constants;
/**
* Common storage for web request attribute names (in a separate package to avoid cycles).
*
* @author Dave Syer
*
*/
public abstract class WebRequestConstants {
public static final String FUNCTION = WebRequestConstants.class.getName()
+ ".function";
public static final String CONSUMER = WebRequestConstants.class.getName()
+ ".consumer";
public static final String SUPPLIER = WebRequestConstants.class.getName()
+ ".supplier";
public static final String ARGUMENT = WebRequestConstants.class.getName()
+ ".argument";
public static final String HANDLER = WebRequestConstants.class.getName() + ".handler";
public static final String INPUT_SINGLE = WebRequestConstants.class.getName()
+ ".input_single";
}

View File

@@ -1,53 +0,0 @@
/*
* 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.
* 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.cloud.function.web.flux.request;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.cloud.function.context.FunctionInspector;
public abstract class DelegateHandler<T> {
private final ListableBeanFactory factory;
private FunctionInspector processor;
private Object handler;
private final Object source;
public DelegateHandler(ListableBeanFactory factory, Object source) {
this.factory = factory;
this.source = source;
}
public Class<?> type() {
return processor().getInputType(handler());
}
private Object handler() {
if (handler == null) {
handler = source instanceof String ? factory.getBean((String) source)
: source;
}
return handler;
}
private FunctionInspector processor() {
if (processor == null) {
processor = factory.getBean(FunctionInspector.class);
}
return processor;
}
}

View File

@@ -1,134 +0,0 @@
/*
* 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.
* 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.cloud.function.web.flux.request;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.function.context.FunctionInspector;
import org.springframework.cloud.function.web.flux.constants.WebRequestConstants;
import org.springframework.cloud.function.web.util.HeaderUtils;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.util.ContentCachingRequestWrapper;
/**
* Converter for request bodies of type <code>Flux<String></code>.
*
* @author Dave Syer
*
*/
public class FluxHandlerMethodArgumentResolver
implements HandlerMethodArgumentResolver, Ordered {
private static Log logger = LogFactory
.getLog(FluxHandlerMethodArgumentResolver.class);
private final ObjectMapper mapper;
private FunctionInspector inspector;
public FluxHandlerMethodArgumentResolver(FunctionInspector inspector,
ObjectMapper mapper) {
this.inspector = inspector;
this.mapper = mapper;
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
Object handler = webRequest.getAttribute(WebRequestConstants.HANDLER,
NativeWebRequest.SCOPE_REQUEST);
Class<?> type = inspector.getInputType(handler);
if (type == null) {
type = Object.class;
}
boolean message = inspector.isMessage(handler);
List<Object> body;
ContentCachingRequestWrapper nativeRequest = new ContentCachingRequestWrapper(
webRequest.getNativeRequest(HttpServletRequest.class));
if (logger.isDebugEnabled()) {
logger.debug("Resolving request body into type: " + type);
}
if (isPlainText(webRequest) && CharSequence.class.isAssignableFrom(type)) {
body = Arrays.asList(StreamUtils.copyToString(nativeRequest.getInputStream(),
Charset.forName("UTF-8")));
}
else {
try {
body = mapper.readValue(nativeRequest.getInputStream(),
mapper.getTypeFactory()
.constructCollectionLikeType(ArrayList.class, type));
}
catch (JsonMappingException e) {
nativeRequest.setAttribute(WebRequestConstants.INPUT_SINGLE, true);
body = Arrays.asList(
mapper.readValue(nativeRequest.getContentAsByteArray(), type));
}
}
if (message) {
List<Object> messages = new ArrayList<>();
for (Object payload : body) {
messages.add(MessageBuilder.withPayload(payload)
.copyHeaders(HeaderUtils.fromHttp(new ServletServerHttpRequest(
webRequest.getNativeRequest(HttpServletRequest.class))
.getHeaders()))
.build());
}
body = messages;
}
return new FluxRequest<Object>(body);
}
private boolean isPlainText(NativeWebRequest webRequest) {
String value = webRequest.getHeader("Content-Type");
if (value != null) {
return MediaType.valueOf(value).isCompatibleWith(MediaType.TEXT_PLAIN);
}
return false;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return FluxRequest.class.isAssignableFrom(parameter.getParameterType());
}
}

View File

@@ -1,60 +0,0 @@
/*
* 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.
* 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.cloud.function.web.flux.response;
import org.reactivestreams.Publisher;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import reactor.core.publisher.Flux;
/**
* A specialized {@link ResponseBodyEmitter} that handles {@link Flux} return types.
*
* @author Dave Syer
*/
class FluxResponseBodyEmitter extends ResponseBodyEmitter {
private final MediaType mediaType;
private ResponseBodyEmitterSubscriber subscriber;
public FluxResponseBodyEmitter(Publisher<?> observable) {
this(new HttpHeaders(), null, observable);
}
public FluxResponseBodyEmitter(HttpHeaders request, MediaType mediaType,
Publisher<?> observable) {
super();
this.mediaType = mediaType;
this.subscriber = new ResponseBodyEmitterSubscriber(request, mediaType,
observable, this, MediaType.APPLICATION_JSON.isCompatibleWith(mediaType));
}
@Override
protected void extendResponse(ServerHttpResponse outputMessage) {
super.extendResponse(outputMessage);
this.subscriber.extendResponse(outputMessage);
HttpHeaders headers = outputMessage.getHeaders();
if (headers.getContentType() == null && this.mediaType != null
&& !MediaType.ALL.equals(this.mediaType)) {
headers.setContentType(this.mediaType);
}
}
}

View File

@@ -1,55 +0,0 @@
/*
* Copyright 2013-2016 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.cloud.function.web.flux.response;
import org.reactivestreams.Publisher;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import reactor.core.publisher.Flux;
/**
* A specialized {@link ResponseBodyEmitter} that handles {@link Flux} return types with
* SSE streams.
*
* @author Dave Syer
*/
class FluxResponseSseEmitter extends SseEmitter {
private ResponseBodyEmitterSubscriber subscriber;
public FluxResponseSseEmitter(Publisher<?> observable) {
this(new HttpHeaders(), MediaType.valueOf("text/plain"), observable);
}
public FluxResponseSseEmitter(HttpHeaders request, MediaType mediaType,
Publisher<?> observable) {
super();
this.subscriber = new ResponseBodyEmitterSubscriber(request, mediaType,
observable, this, false);
}
@Override
protected void extendResponse(ServerHttpResponse outputMessage) {
super.extendResponse(outputMessage);
this.subscriber.extendResponse(outputMessage);
}
}

View File

@@ -1,267 +0,0 @@
/*
* Copyright 2013-2016 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.cloud.function.web.flux.response;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Stream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.el.stream.Optional;
import org.reactivestreams.Publisher;
import org.springframework.cloud.function.context.FunctionInspector;
import org.springframework.cloud.function.web.flux.constants.WebRequestConstants;
import org.springframework.cloud.function.web.util.HeaderUtils;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.messaging.Message;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* A specialized {@link AsyncHandlerMethodReturnValueHandler} that handles {@link Flux}
* return types.
*
* @author Dave Syer
*/
public class FluxReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
private static Log logger = LogFactory.getLog(FluxReturnValueHandler.class);
private ResponseBodyEmitterReturnValueHandler delegate;
private RequestResponseBodyMethodProcessor single;
private long timeout = 1000L;
private static final MediaType EVENT_STREAM = MediaType.valueOf("text/event-stream");
private FunctionInspector inspector;
private MethodParameter singleReturnType;
public FluxReturnValueHandler(FunctionInspector inspector,
List<HttpMessageConverter<?>> messageConverters) {
this.inspector = inspector;
this.delegate = new ResponseBodyEmitterReturnValueHandler(messageConverters);
this.single = new RequestResponseBodyMethodProcessor(messageConverters);
Method method = ReflectionUtils.findMethod(getClass(), "singleValue");
singleReturnType = new MethodParameter(method, -1);
}
ResponseEntity<Object> singleValue() {
return null;
}
/**
* Timeout for clients. If no items are seen on an HTTP response in this period then
* the response is closed.
*
* @param timeout the timeout to set
*/
public void setTimeout(long timeout) {
this.timeout = timeout;
}
@Override
public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
if (returnValue != null) {
return supportsReturnType(returnType);
}
return false;
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (returnType.getParameterType() != null
&& (Publisher.class.isAssignableFrom(returnType.getParameterType())
|| isResponseEntity(returnType)))
|| Publisher.class
.isAssignableFrom(returnType.getMethod().getReturnType());
}
private boolean isResponseEntity(MethodParameter returnType) {
if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) {
Class<?> bodyType = ResolvableType.forMethodParameter(returnType)
.getGeneric(0).resolve();
return bodyType != null && Flux.class.isAssignableFrom(bodyType);
}
return false;
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws Exception {
if (returnValue == null) {
mavContainer.setRequestHandled(true);
return;
}
Object adaptFrom = returnValue;
if (returnValue instanceof ResponseEntity) {
ResponseEntity<?> value = (ResponseEntity<?>) returnValue;
adaptFrom = value.getBody();
HttpServletResponse response = webRequest
.getNativeResponse(HttpServletResponse.class);
response.setStatus(value.getStatusCodeValue());
HttpHeaders headers = value.getHeaders();
for (String name : headers.keySet()) {
List<String> list = headers.get(name);
for (String header : list) {
response.addHeader(name, header);
}
}
}
Publisher<?> flux = (Publisher<?>) adaptFrom;
Object handler = webRequest.getAttribute(WebRequestConstants.HANDLER,
NativeWebRequest.SCOPE_REQUEST);
Class<?> type = inspector.getOutputType(handler);
boolean inputSingle = isInputSingle(webRequest, handler);
if (inputSingle && isOutputSingle(handler)) {
Object result = Flux.from(flux).blockFirst();
if (result instanceof Message) {
Message<?> message = (Message<?>) result;
result = message.getPayload();
addHeaders(webRequest, message);
}
single.handleReturnValue(result, singleReturnType, mavContainer, webRequest);
return;
}
MediaType mediaType = null;
if (isPlainText(webRequest) && CharSequence.class.isAssignableFrom(type)) {
mediaType = MediaType.TEXT_PLAIN;
}
else {
mediaType = findMediaType(webRequest);
}
if (logger.isDebugEnabled()) {
logger.debug(
"Handling return value " + type + " with media type: " + mediaType);
}
ServletServerHttpRequest request = new ServletServerHttpRequest(
webRequest.getNativeRequest(HttpServletRequest.class));
delegate.handleReturnValue(
getEmitter(timeout, flux, mediaType, request.getHeaders()), returnType,
mavContainer, webRequest);
}
private void addHeaders(NativeWebRequest webRequest, Message<?> message) {
HttpServletResponse response = webRequest
.getNativeResponse(HttpServletResponse.class);
ServletServerHttpRequest request = new ServletServerHttpRequest(
webRequest.getNativeRequest(HttpServletRequest.class));
HttpHeaders headers = HeaderUtils.fromMessage(message.getHeaders(),
request.getHeaders());
for (String name : headers.keySet()) {
for (Object object : headers.get(name)) {
response.addHeader(name, object.toString());
}
}
}
private boolean isInputSingle(NativeWebRequest webRequest, Object handler) {
Boolean single = (Boolean) webRequest.getAttribute(
WebRequestConstants.INPUT_SINGLE, NativeWebRequest.SCOPE_REQUEST);
if (single == null) {
return handler instanceof Supplier;
}
return single;
}
private boolean isOutputSingle(Object handler) {
Class<?> type = inspector.getOutputType(handler);
Class<?> wrapper = inspector.getOutputWrapper(handler);
if (Stream.class.isAssignableFrom(type)) {
return false;
}
if (wrapper == type) {
return true;
}
if (Mono.class.equals(wrapper) || Optional.class.equals(wrapper)) {
return true;
}
return false;
}
private MediaType findMediaType(NativeWebRequest webRequest) {
List<MediaType> accepts = Arrays.asList(MediaType.ALL);
MediaType mediaType = null;
if (webRequest.getHeader("Accept") != null) {
accepts = MediaType.parseMediaTypes(webRequest.getHeader("Accept"));
for (MediaType accept : accepts) {
if (!MediaType.ALL.equals(accept)
&& MediaType.APPLICATION_JSON.isCompatibleWith(accept)) {
mediaType = MediaType.APPLICATION_JSON;
// Prefer JSON if that is acceptable
break;
}
else if (mediaType == null) {
mediaType = accept;
}
}
}
if (mediaType == null) {
mediaType = MediaType.APPLICATION_JSON;
}
return mediaType;
}
private boolean isPlainText(NativeWebRequest webRequest) {
String value = webRequest.getHeader("Content-Type");
if (value != null) {
return MediaType.valueOf(value).isCompatibleWith(MediaType.TEXT_PLAIN);
}
return false;
}
private ResponseBodyEmitter getEmitter(Long timeout, Publisher<?> flux,
MediaType mediaType, HttpHeaders request) {
Publisher<?> exported = flux instanceof Mono ? Mono.from(flux)
: Flux.from(flux).timeout(Duration.ofMillis(timeout), Flux.empty());
if (!MediaType.ALL.equals(mediaType)
&& EVENT_STREAM.isCompatibleWith(mediaType)) {
// TODO: more subtle content negotiation
return new FluxResponseSseEmitter(request, MediaType.APPLICATION_JSON,
exported);
}
return new FluxResponseBodyEmitter(request, mediaType, exported);
}
}

View File

@@ -1,212 +0,0 @@
/*
* Copyright 2013-2016 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.cloud.function.web.flux.response;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.springframework.cloud.function.web.util.HeaderUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.messaging.Message;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* Subscriber that emits any value produced by the {@link Flux} into the delegated
* {@link ResponseBodyEmitter}.
*
* @author Dave Syer
*/
class ResponseBodyEmitterSubscriber implements Subscriber<Object> {
private final MediaType mediaType;
private Subscription subscription;
private final ResponseBodyEmitter responseBodyEmitter;
private boolean completed;
private boolean firstElementWritten;
private boolean single;
private final boolean json;
private Message<?> first;
private final HttpHeaders request;
public ResponseBodyEmitterSubscriber(HttpHeaders request, MediaType mediaType,
Publisher<?> observable, ResponseBodyEmitter responseBodyEmitter,
boolean json) {
this.request = request;
this.mediaType = mediaType;
this.responseBodyEmitter = responseBodyEmitter;
this.json = json;
this.responseBodyEmitter.onTimeout(new Timeout());
this.responseBodyEmitter.onCompletion(new Complete());
this.single = observable instanceof Mono;
observable.subscribe(this);
}
public void extendResponse(ServerHttpResponse response) {
headers(response);
}
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(Object value) {
Object object = value;
if (object instanceof Message) {
Message<?> message = (Message<?>) object;
object = message.getPayload();
this.first = message;
}
try {
if (isJson()) {
if (!this.firstElementWritten) {
if (!single) {
responseBodyEmitter.send("[");
this.firstElementWritten = true;
}
}
else {
responseBodyEmitter.send(",");
}
if (!single && object.getClass() == String.class
&& !((String) object).contains("\"")) {
object = "\"" + object + "\"";
}
}
if (!completed) {
responseBodyEmitter.send(object, mediaType);
}
}
catch (
IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
private void headers(ServerHttpResponse response) {
if (this.first != null) {
Message<?> message = first;
try {
HttpHeaders headers = HeaderUtils.fromMessage(message.getHeaders(),
request);
for (String name : headers.keySet()) {
for (String value : headers.get(name)) {
response.getHeaders().add(name, value);
}
}
}
catch (Exception e) {
// Headers could not be set
}
}
}
@Override
public void onError(Throwable e) {
if (!completed) {
completed = true;
try {
if (isJson()) {
if (!single) {
if (!this.firstElementWritten) {
responseBodyEmitter.send("[]");
}
else {
responseBodyEmitter.send("]");
}
}
}
if (e instanceof TimeoutException) {
responseBodyEmitter.complete();
}
else {
responseBodyEmitter.completeWithError(e);
}
}
catch (IOException ex) {
throw new RuntimeException(ex.getMessage(), ex);
}
}
}
@Override
public void onComplete() {
if (!completed) {
completed = true;
try {
if (isJson()) {
if (!single) {
if (!this.firstElementWritten) {
responseBodyEmitter.send("[");
}
responseBodyEmitter.send("]");
}
}
}
catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
responseBodyEmitter.complete();
}
}
private boolean isJson() {
return json;
}
class Complete implements Runnable {
@Override
public void run() {
ResponseBodyEmitterSubscriber.this.subscription.cancel();
}
}
class Timeout implements Runnable {
@Override
public void run() {
onComplete();
ResponseBodyEmitterSubscriber.this.subscription.cancel();
}
}
}

View File

@@ -1,9 +0,0 @@
{"properties": [
{
"name": "spring.cloud.function.web.path",
"type": "java.lang.String",
"description": "Path to web resources for functions (should start with / if not empty).",
"defaultValue": ""
}]
}

View File

@@ -1,2 +0,0 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.function.web.flux.ReactorAutoConfiguration

View File

@@ -1,392 +0,0 @@
/*
* 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.
* 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.cloud.function.mvc;
import java.net.URI;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.cloud.function.mvc.MvcRestApplicationTests.TestConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* Tests for vanilla MVC handling (no function layer). Validates the MVC customizations
* that are added in this project independently of the specific concerns of function.
*
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestConfiguration.class, webEnvironment = WebEnvironment.RANDOM_PORT)
public class MvcRestApplicationTests {
private static final MediaType EVENT_STREAM = MediaType.valueOf("text/event-stream");
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate rest;
@Autowired
private TestConfiguration test;
@Before
public void init() {
test.list.clear();
}
@Test
public void wordsSSE() throws Exception {
assertThat(rest.exchange(
RequestEntity.get(new URI("/words")).accept(EVENT_STREAM).build(),
String.class).getBody()).isEqualTo(sse("foo", "bar"));
}
@Test
public void wordsJson() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/words"))
.accept(MediaType.APPLICATION_JSON).build(), String.class)
.getBody()).isEqualTo("[\"foo\",\"bar\"]");
}
@Test
@Ignore("Fix error handling")
public void errorJson() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/bang"))
.accept(MediaType.APPLICATION_JSON).build(), String.class)
.getBody()).isEqualTo("[\"foo\"]");
}
@Test
public void words() throws Exception {
ResponseEntity<String> result = rest
.exchange(RequestEntity.get(new URI("/words")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
}
@Test
public void foos() throws Exception {
ResponseEntity<String> result = rest
.exchange(RequestEntity.get(new URI("/foos")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody())
.isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
}
@Test
public void getMore() throws Exception {
ResponseEntity<String> result = rest
.exchange(RequestEntity.get(new URI("/get/more")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
}
@Test
@Ignore("Should this even work? Or do we need to be explicit about the JSON?")
public void updates() throws Exception {
ResponseEntity<String> result = rest.exchange(
RequestEntity.post(new URI("/updates")).body("one\ntwo"), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
assertThat(test.list).hasSize(2);
assertThat(result.getBody()).isEqualTo("onetwo");
}
@Test
public void updatesJson() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/updates")).contentType(MediaType.APPLICATION_JSON)
.body("[\"one\",\"two\"]"), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
assertThat(test.list).hasSize(2);
assertThat(result.getBody()).isEqualTo("[\"one\",\"two\"]");
}
@Test
public void addFoos() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/addFoos")).contentType(MediaType.APPLICATION_JSON)
.body("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
assertThat(test.list).hasSize(2);
assertThat(result.getBody())
.isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
}
@Test
public void timeout() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/timeout")).build(), String.class)
.getBody()).isEqualTo("[\"foo\"]");
}
@Test
public void emptyJson() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/empty"))
.accept(MediaType.APPLICATION_JSON).build(), String.class)
.getBody()).isEqualTo("[]");
}
@Test
public void sentences() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/sentences")).build(), String.class)
.getBody()).isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]");
}
@Test
public void sentencesAcceptAny() throws Exception {
assertThat(rest.exchange(
RequestEntity.get(new URI("/sentences")).accept(MediaType.ALL).build(),
String.class).getBody())
.isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]");
}
@Test
public void sentencesAcceptJson() throws Exception {
ResponseEntity<String> result = rest
.exchange(
RequestEntity.get(new URI("/sentences"))
.accept(MediaType.APPLICATION_JSON).build(),
String.class);
assertThat(result.getBody()).isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]");
assertThat(result.getHeaders().getContentType())
.isGreaterThanOrEqualTo(MediaType.APPLICATION_JSON);
}
@Test
public void uppercase() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/uppercase")).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class);
assertThat(result.getBody()).isEqualTo("[\"[FOO]\",\"[BAR]\"]");
}
@Test
public void uppercaseFoos() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/upFoos")).contentType(MediaType.APPLICATION_JSON)
.body("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"), String.class);
assertThat(result.getBody())
.isEqualTo("[{\"value\":\"FOO\"},{\"value\":\"BAR\"}]");
}
@Test
public void transform() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/transform")).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class);
assertThat(result.getBody()).isEqualTo("[\"[FOO]\",\"[BAR]\"]");
}
@Test
public void postMore() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/post/more")).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class);
assertThat(result.getBody()).isEqualTo("[\"[FOO]\",\"[BAR]\"]");
}
@Test
public void uppercaseGet() {
assertThat(rest.getForObject("/uppercase/foo", String.class)).isEqualTo("[FOO]");
}
@Test
public void convertGet() {
assertThat(rest.getForObject("/wrap/123", String.class)).isEqualTo("..123..");
}
@Test
public void convertGetJson() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/entity/321"))
.accept(MediaType.APPLICATION_JSON).build(), String.class)
.getBody()).isEqualTo("{\"value\":321}");
}
@Test
public void uppercaseJsonStream() throws Exception {
assertThat(rest
.exchange(RequestEntity.post(new URI("/maps"))
.contentType(MediaType.APPLICATION_JSON)
.body("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"), String.class)
.getBody()).isEqualTo("[{\"value\":\"FOO\"},{\"value\":\"BAR\"}]");
}
@Test
public void uppercaseSSE() throws Exception {
assertThat(rest.exchange(RequestEntity.post(new URI("/uppercase"))
.accept(EVENT_STREAM).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class).getBody())
.isEqualTo(sse("[FOO]", "[BAR]"));
}
private String sse(String... values) {
return "data:" + StringUtils.arrayToDelimitedString(values, "\n\ndata:") + "\n\n";
}
@EnableAutoConfiguration
@RestController
@Configuration
public static class TestConfiguration {
private List<String> list = new ArrayList<>();
@PostMapping({ "/uppercase", "/transform", "/post/more" })
public Flux<?> uppercase(@RequestBody List<String> flux) {
return Flux.fromIterable(flux).log()
.map(value -> "[" + value.trim().toUpperCase() + "]");
}
@PostMapping("/upFoos")
public Flux<Foo> upFoos(@RequestBody List<Foo> list) {
return Flux.fromIterable(list).log()
.map(value -> new Foo(value.getValue().trim().toUpperCase()));
}
@GetMapping("/uppercase/{id}")
public Mono<?> uppercaseGet(@PathVariable String id) {
return Mono.just(id).map(value -> "[" + value.trim().toUpperCase() + "]");
}
@GetMapping("/wrap/{id}")
public Mono<?> wrapGet(@PathVariable int id) {
return Mono.just(id).log().map(value -> ".." + value + "..");
}
@GetMapping("/entity/{id}")
public Mono<Map<String, Object>> entity(@PathVariable Integer id) {
return Mono.just(id).log()
.map(value -> Collections.singletonMap("value", value));
}
@PostMapping("/maps")
public Flux<Map<String, String>> maps(
@RequestBody List<Map<String, String>> flux) {
return Flux.fromIterable(flux).map(value -> {
value.put("value", value.get("value").trim().toUpperCase());
return value;
});
}
@GetMapping({ "/words", "/get/more" })
public Flux<Object> words() {
return Flux.fromArray(new String[] { "foo", "bar" });
}
@GetMapping("/foos")
public Flux<Foo> foos() {
return Flux.just(new Foo("foo"), new Foo("bar"));
}
@PostMapping("/updates")
@ResponseStatus(HttpStatus.ACCEPTED)
public Flux<?> updates(@RequestBody List<String> list) {
Flux<String> flux = Flux.fromIterable(list).cache();
flux.subscribe(value -> this.list.add(value));
return flux;
}
@PostMapping("/addFoos")
@ResponseStatus(HttpStatus.ACCEPTED)
public Flux<Foo> addFoos(@RequestBody List<Foo> list) {
Flux<Foo> flux = Flux.fromIterable(list).cache();
flux.subscribe(value -> this.list.add(value.getValue()));
return flux;
}
@GetMapping("/bang")
public Flux<?> bang() {
return Flux.fromArray(new String[] { "foo", "bar" }).map(value -> {
if (value.equals("bar")) {
throw new RuntimeException("Bar");
}
return value;
});
}
@GetMapping("/empty")
public Flux<?> empty() {
return Flux.fromIterable(Collections.emptyList());
}
@GetMapping("/timeout")
public Flux<?> timeout() {
return Flux.defer(() -> Flux.<String>create(emitter -> {
emitter.next("foo");
}).timeout(Duration.ofMillis(100L), Flux.empty()));
}
@GetMapping("/sentences")
public Flux<List<String>> sentences() {
return Flux.just(Arrays.asList("go", "home"), Arrays.asList("come", "back"));
}
}
public static class Foo {
private String value;
public Foo(String value) {
this.value = value;
}
Foo() {
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}

View File

@@ -1 +0,0 @@
<html><body>Test</body></html>

View File

@@ -0,0 +1,39 @@
A Spring Cloud Stream binder for a Servlet container using Spring MVC.
## Usage
Add this jar to the classpath of a Spring Cloud Stream application as the only binder implementation. Endpoints are exposed at
| Method | Path | Description |
|-----------|------------------------------------|----------------------------|
| GET | `/stream/{channel}/{route}` | If the channel is an `@Output` returns a list of the payloads of all messages sent to the channel with a routing key equal to `{route}`. |
| GET | `/stream/{channel}/{route}/{body}` | If the channel is an `@Input` sends the last segment of the path (the `{body}`) as a payload to the `{route}`. If the channel is not an input, it's an error (404). |
| POST | `/stream/{channel}/{route}` | Accepts a single value or a list of payloads and sends them to the `@Input` called `{channel}` with routing key header equal to `{route}`.|
The result of the POST depends on whether an `@Output` is linked to the `@Input`. By default a link is made if the user has `@EnableBinding` with an interface having precisely one `@Output` and one `@Input` (e.g. using `Processor` from Spring Cloud Stream). In the case that there is no linked `@Output`, the return value from the POST is a 202 (Accepted) and a mirror of the input. If an `@Output` is linked, then the contents of the output channel are returned with a 200 status (OK).
The routing key is sent via a message header named `stream_routekey`. It is a plain string, and can be multiple URI segments (i.e. contain "/"). It will show up in HTTP response headers if present. Message headers can be sent in a POST request using HTTP headers (as well as the special case of the route key being part of the path).
Both the channel and route are optional, but can be used to disambiguate if necessary. So for example:
| Method | Path | Description |
|-----------|--------------------------|----------------------------|
| GET | `/stream` | Defaults the channel name to `output` (or to the name of the `@Output` if there is only one). Messages sent with no routing key are delivered. |
| GET | `/stream/{channel}` | Uses and explicit channel name but an empty routing key. If the channel is not a registered output, then it will be interpreted as a route (see below). |
| GET | `/stream/{route}/{body}` | Equivalent to `GET /stream/input/{route}/{body}` if there is a unique input channel. |
| GET | `/stream/{route}` | If the channel is missing (i.e. the first path segment is not a registered output or input), then it will be interpreted as a route. Equivalent to `GET /stream/output/{route}` if there is a unique output channel. |
| POST | `/stream/{route}` | As long as the first segment of the route is not an input channel name, this defaults the channel to `input` (or to the name of the `@Input` if it is unique).|
| POST | `/stream` | Defaults the channel to `input` (or to the name of the `@Input` if it is unique). The routing key is empty.|
Note that with a GET, if the channel is not a registered output, then it will be interpreted as a route. So if there is a default input channel, then the path will be transformed into `{route}/{body}` (agin with route optional, if there is only one path segment) and sent to the input channel.
The result of a GET is a moving time window by default (the last 10 seconds of data are buffered). Clients can request an infinite stream of data using `GET` with `Accept: text/event-stream` (or a compatible media type).
Configuration properties (in addition to the ones provided by Spring Cloud Stream for bindings and channel names, etc.):
| Key | Default | Description |
|--------------------------------|---------|----------------------------|
| `spring.cloud.stream.binder.servlet.prefix` | `stream` | The prefix for the URL paths |
| `spring.cloud.stream.binder.servlet.buffer-timeout-seconds` | 10 | The buffer size in seconds to store messages from the output channels. |
| `spring.cloud.stream.binder.servlet.receive-timeout-millis` | 100 | The timeout for send and receive if POST has a linked output channel. Only relevant if the message processing is asynchronous. |

View File

@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-stream-binder-servlet</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-cloud-stream-binder-servlet</name>
<description>Servlet binder implementation</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-build</artifactId>
<version>1.3.5.RELEASE</version>
</parent>
<properties>
<reactor.version>3.1.1.RELEASE</reactor.version>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-codec</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<profile>
<id>spring</id>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>http://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>http://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>http://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>${reactor.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Edgware.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>

View File

@@ -5,7 +5,7 @@
* 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
* 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,
@@ -13,20 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet;
package org.springframework.cloud.function.web;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.Set;
/**
* @author Mark Fisher
* @author Dave Syer
*
*/
@SpringBootApplication
public class RestApplication {
public interface EnabledBindings {
String getInput(String output);
Set<String> getOutputs();
Set<String> getInputs();
public static void main(String[] args) {
SpringApplication.run(RestApplication.class, args);
}
}

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.web.util;
package org.springframework.cloud.stream.binder.servlet;
import java.util.Arrays;
import java.util.Collection;
@@ -28,9 +28,10 @@ import org.springframework.util.ObjectUtils;
* @author Dave Syer
*
*/
public class HeaderUtils {
class HeaderUtils {
public static HttpHeaders fromMessage(MessageHeaders headers, HttpHeaders request) {
public static HttpHeaders fromMessage(Map<String, Object> headers,
HttpHeaders request) {
HttpHeaders result = new HttpHeaders();
for (String name : headers.keySet()) {
Object value = headers.get(name);

View File

@@ -0,0 +1,86 @@
/*
* 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.
* 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.cloud.stream.binder.servlet;
import java.util.ArrayList;
import java.util.List;
/**
* Internal convenience class to help with JSON message bodies. In particular translating
* between JSON arrays and lists of payloads.
*
* @author Dave Syer
*
*/
class JsonUtils {
/**
* Split a JSON array into a list of individual objects, without parsing the objects
* themselves..
*/
public static List<String> split(String body) {
body = body.trim();
// it's an array
List<String> strings = new ArrayList<>();
int index = 0;
int open = 0;
boolean inString = false;
StringBuilder builder = new StringBuilder();
while (index++ < body.length() - 1) {
char current = body.charAt(index);
builder.append(current);
if (body.charAt(index - 1) != '\\') {
if (current == '"') {
if (!inString) {
open++;
inString = true;
}
else {
open--;
inString = false;
}
}
else if (current == '[') {
open++;
}
else if (current == ']') {
open--;
}
else if (current == '{') {
open++;
}
else if (current == '}') {
open--;
}
}
if (open == 0) {
if (builder.charAt(0) == '"') {
builder.delete(0, 1);
builder.delete(builder.length() - 1, builder.length());
}
strings.add(builder.toString());
builder.setLength(0);
while (index++ < body.length() - 1 && body.charAt(index) != ',') {
}
while (index++ < body.length() - 1
&& Character.isWhitespace(body.charAt(index))) {
}
index--;
}
}
return strings;
}
}

View File

@@ -0,0 +1,457 @@
/*
* 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.
* 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.cloud.stream.binder.servlet;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.reactivestreams.Processor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import reactor.core.publisher.Flux;
import reactor.core.publisher.UnicastProcessor;
/**
* @author Dave Syer
*
*/
@RestController
@RequestMapping("/${spring.cloud.stream.binder.servlet.prefix:stream}")
public class MessageController implements RouteRegistrar {
public static final String ROUTE_KEY = "stream_routekey";
private final ConcurrentMap<String, Bridge<Message<?>>> queues = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Set<SseEmitter>> emitters = new ConcurrentHashMap<>();
private final Map<String, MessageChannel> inputs = new HashMap<>();
private final Map<String, String> outputs = new HashMap<>();
private final EnabledBindings bindings;
private final MessagingTemplate template = new MessagingTemplate();
private String prefix;
public long timeoutSeconds = 10;
private long receiveTimeoutMillis;
private Set<String> routes = new LinkedHashSet<>();
public MessageController(String prefix, EnabledBindings bindings) {
if (!prefix.startsWith("/")) {
prefix = "/" + prefix;
}
if (!prefix.endsWith("/")) {
prefix = prefix + "/";
}
this.prefix = prefix;
this.bindings = bindings;
this.template.setReceiveTimeout(this.receiveTimeoutMillis);
}
public void setReceiveTimeoutSeconds(long receiveTimeoutMillis) {
this.receiveTimeoutMillis = receiveTimeoutMillis;
this.template.setReceiveTimeout(receiveTimeoutMillis);
}
public void setBufferTimeoutSeconds(long timeoutSeconds) {
this.timeoutSeconds = timeoutSeconds;
}
@GetMapping(path = "/**", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public ResponseEntity<SseEmitter> sse(
@RequestAttribute("org.springframework.web.servlet.HandlerMapping.pathWithinHandlerMapping") String path,
@RequestHeader HttpHeaders headers) throws IOException {
Route route = output(path);
String channel = route.getChannel();
if (!bindings.getOutputs().contains(channel)) {
return org.springframework.http.ResponseEntity.notFound().build();
}
Message<Collection<Object>> message = poll(route.getChannel(), route.getKey(),
true);
SseEmitter body = emit(route, message);
return ResponseEntity.ok()
.headers(HeaderUtils.fromMessage(message.getHeaders(), headers))
.body(body);
}
@GetMapping("/**")
public ResponseEntity<Object> supplier(
@RequestAttribute("org.springframework.web.servlet.HandlerMapping.pathWithinHandlerMapping") String path,
@RequestHeader HttpHeaders headers,
@RequestParam(required = false) boolean purge) {
Route route = output(path);
String channel = route.getChannel();
if (bindings.getOutputs().contains(channel)) {
Message<Collection<Object>> polled = poll(channel, route.getKey(), !purge);
if (routes.contains(route.getKey()) || !polled.getPayload().isEmpty()
|| route.getKey() == null) {
return convert(polled, headers);
}
}
route = input(path);
channel = route.getChannel();
if (!bindings.getInputs().contains(channel)) {
return ResponseEntity.notFound().build();
}
String body = route.getKey();
body = body.contains("/") ? body.substring(body.lastIndexOf("/") + 1) : body;
path = path.replaceAll("/" + body, "");
return string(path, body, headers);
}
@PostMapping(path = "/**", consumes = MediaType.TEXT_PLAIN_VALUE)
public ResponseEntity<Object> string(
@RequestAttribute("org.springframework.web.servlet.HandlerMapping.pathWithinHandlerMapping") String path,
@RequestBody String body, @RequestHeader HttpHeaders headers) {
return function(path, body, headers);
}
@PostMapping(path = "/**", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Object> json(
@RequestAttribute("org.springframework.web.servlet.HandlerMapping.pathWithinHandlerMapping") String path,
@RequestBody String body, @RequestHeader HttpHeaders headers) {
return function(path, extract(body), headers);
}
private Object extract(String body) {
body = body.trim();
Object result = body;
if (body.startsWith("[")) {
result = JsonUtils.split(body);
}
return result;
}
@PostMapping("/**")
public ResponseEntity<Object> function(
@RequestAttribute("org.springframework.web.servlet.HandlerMapping.pathWithinHandlerMapping") String path,
@RequestBody Object body, @RequestHeader HttpHeaders headers) {
Route route = input(path);
String channel = route.getChannel();
if (!inputs.containsKey(channel)) {
return ResponseEntity.notFound().build();
}
Collection<Object> collection;
boolean single = false;
if (body instanceof String) {
body = extract((String) body);
}
if (body instanceof Collection) {
@SuppressWarnings("unchecked")
Collection<Object> list = (Collection<Object>) body;
collection = list;
}
else {
if (ObjectUtils.isArray(body)) {
collection = Arrays.asList(ObjectUtils.toObjectArray(body));
}
else {
single = true;
collection = Arrays.asList(body);
}
}
Map<String, Object> messageHeaders = new HashMap<>(HeaderUtils.fromHttp(headers));
if (route.getKey() != null) {
messageHeaders.put(ROUTE_KEY, route.getKey());
}
MessageChannel input = inputs.get(channel);
Map<String, Object> outputHeaders = null;
List<Object> results = new ArrayList<>();
HttpStatus status = HttpStatus.ACCEPTED;
// This is a total guess. We have no way to guarantee that the user will
// implement a Processor so that inputs always get an output, so either
// nothing might come back or there might be multiple outputs and we only get
// one of them.
if (this.outputs.containsKey(channel)) {
for (Object payload : collection) {
Message<?> result = template.sendAndReceive(input, MessageBuilder
.withPayload(payload).copyHeadersIfAbsent(messageHeaders)
.setHeader(MessageHeaders.REPLY_CHANNEL, outputs.get(channel))
.build());
if (result != null) {
if (outputHeaders == null) {
outputHeaders = new LinkedHashMap<>(result.getHeaders());
}
results.add(result.getPayload());
}
}
status = HttpStatus.OK;
if (results.isEmpty()) {
// If nothing came back, just assume it was intentional, and say that
// we accepted the inputs.
status = HttpStatus.ACCEPTED;
results.addAll(collection);
}
}
else {
for (Object payload : collection) {
template.send(input, MessageBuilder.withPayload(payload)
.copyHeadersIfAbsent(messageHeaders).build());
}
outputHeaders = messageHeaders;
results.addAll(collection);
}
if (outputHeaders == null) {
outputHeaders = new LinkedHashMap<>();
}
outputHeaders.put(ROUTE_KEY, route.getKey());
if (single && results.size() == 1) {
body = results.get(0);
}
else {
body = results;
}
if (headers.getContentType() != null
&& headers.getContentType().includes(MediaType.APPLICATION_JSON)
&& body.toString().contains("\"")) {
body = body.toString();
}
return convert(status, MessageBuilder.withPayload(body)
.copyHeadersIfAbsent(outputHeaders).build(), headers);
}
private ResponseEntity<Object> convert(Message<?> message, HttpHeaders request) {
return convert(HttpStatus.OK, message, request);
}
private ResponseEntity<Object> convert(HttpStatus status, Message<?> message,
HttpHeaders request) {
return ResponseEntity.status(status)
.headers(HeaderUtils.fromMessage(message.getHeaders(), request))
.body(message.getPayload());
}
private SseEmitter emit(Route route, Message<Collection<Object>> message)
throws IOException {
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
String path = route.getPath();
if (!emitters.containsKey(path)) {
emitters.putIfAbsent(path, new HashSet<>());
}
emitters.get(path).add(emitter);
emitter.onCompletion(() -> emitters.get(path).remove(emitter));
emitter.onTimeout(() -> emitters.get(path).remove(emitter));
for (Object body : message.getPayload()) {
emitter.send(body);
}
return emitter;
}
public void reset() {
queues.clear();
}
private Message<Collection<Object>> poll(String channel, String key,
boolean requeue) {
List<Object> list = new ArrayList<>();
List<Message<?>> messages = new ArrayList<>();
Bridge<Message<?>> queue = queues.get(new Route(key, channel).getPath());
if (queue != null) {
queue.receive().subscribe(message -> {
messages.add(message);
list.add(message.getPayload());
});
if (!requeue) {
queue.reset();
}
}
MessageBuilder<Collection<Object>> builder = MessageBuilder.withPayload(list);
if (!messages.isEmpty()) {
builder.copyHeadersIfAbsent(messages.get(0).getHeaders());
}
return builder.build();
}
public void subscribe(String name, SubscribableChannel outboundBindTarget) {
this.outputs.put(bindings.getInput(name), name);
outboundBindTarget.subscribe(message -> this.append(name, message));
}
private void append(String name, Message<?> message) {
String key = (String) message.getHeaders().get(ROUTE_KEY);
if (message.getHeaders().getReplyChannel() instanceof MessageChannel) {
MessageChannel replyChannel = (MessageChannel) message.getHeaders()
.getReplyChannel();
replyChannel.send(message);
return;
}
Route route = new Route(key, name);
String path = route.getPath();
if (!queues.containsKey(path)) {
Bridge<Message<?>> flux = new Bridge<>();
queues.putIfAbsent(path, flux);
}
queues.get(path).send(message);
if (emitters.containsKey(path)) {
Set<SseEmitter> list = new HashSet<>(emitters.get(path));
for (SseEmitter emitter : list) {
try {
emitter.send(message.getPayload());
}
catch (IOException e) {
emitters.get(path).remove(emitter);
}
}
}
}
public void bind(String name, String group, MessageChannel inputTarget) {
this.inputs.put(name, inputTarget);
}
public Route output(String path) {
return new Route(prefix, path,
bindings.getOutputs().size() == 1
? bindings.getOutputs().iterator().next()
: "output");
}
public Route input(String path) {
return new Route(prefix, path,
bindings.getInputs().size() == 1 ? bindings.getInputs().iterator().next()
: "input");
}
private class Route {
private String key;
private String channel;
private String path;
private Route(String prefix, String path, String defaultChannel) {
String channel;
String route = null;
// Strip the prefix first
if (path.length() > prefix.length()) {
path = path.substring(prefix.length());
}
else {
path = "";
}
// Then extract the first segment of the path, and call it a "channel"
String[] paths = path.split("/");
if (paths.length > 1) {
channel = paths[0];
route = path.substring(channel.length() + 1, path.length());
}
else {
channel = path;
}
// If it's not actually a channel we know about, use the default, and call the
// whole path a "route"
if (!bindings.getInputs().contains(channel)
& !bindings.getOutputs().contains(channel)) {
channel = defaultChannel;
route = path.length() > 0 ? path : null;
}
this.channel = channel;
this.key = route;
this.path = key != null ? key + "/" + channel : channel;
}
public Route(String key, String channel) {
this.key = key;
this.channel = channel;
this.path = key != null ? key + "/" + channel : channel;
}
public String getPath() {
return path;
}
public String getKey() {
return key;
}
public String getChannel() {
return channel;
}
}
private class Bridge<T> {
private Processor<T, T> emitter;
private Flux<T> sink;
public Bridge() {
reset();
}
public void reset() {
this.emitter = UnicastProcessor.<T>create().serialize();
this.sink = Flux.from(emitter).replay().autoConnect()
.take(Duration.ofSeconds(timeoutSeconds));
}
public void send(T item) {
emitter.onNext(item);
}
public Flux<T> receive() {
return sink;
}
}
@Override
public void registerRoutes(Set<String> routes) {
this.routes.addAll(routes);
}
@Override
public void unregisterRoutes(Set<String> routes) {
this.routes.removeAll(routes);
for (String path : routes) {
queues.remove(output(prefix + path).getPath());
}
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.
* 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.cloud.stream.binder.servlet;
import java.util.Set;
/**
* @author Dave Syer
*
*/
public interface RouteRegistrar {
void registerRoutes(Set<String> routes);
void unregisterRoutes(Set<String> routes);
}

View File

@@ -0,0 +1,27 @@
/*
* 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.
* 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.cloud.stream.binder.servlet;
import java.util.Set;
/**
* @author Dave Syer
*
*/
public interface RouteRegistry {
Set<String> routes();
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2014-2016 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.cloud.stream.binder.servlet;
import org.springframework.cloud.stream.binder.AbstractBinder;
import org.springframework.cloud.stream.binder.Binding;
import org.springframework.cloud.stream.binder.ConsumerProperties;
import org.springframework.cloud.stream.binder.DefaultBinding;
import org.springframework.cloud.stream.binder.ProducerProperties;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
/**
* A {@link org.springframework.cloud.stream.binder.Binder} implementation backed by HTTP.
*
* @author Dave Syer
*/
public class ServletMessageChannelBinder
extends AbstractBinder<MessageChannel, ConsumerProperties, ProducerProperties> {
private MessageController controller;
public ServletMessageChannelBinder(MessageController controller) {
this.controller = controller;
}
@Override
protected Binding<MessageChannel> doBindConsumer(String name, String group,
MessageChannel inputTarget, ConsumerProperties properties) {
controller.bind(name, group, inputTarget);
return new DefaultBinding<MessageChannel>(name, group, inputTarget, null);
}
@Override
protected Binding<MessageChannel> doBindProducer(String name,
MessageChannel outboundBindTarget, ProducerProperties properties) {
controller.subscribe(name, (SubscribableChannel) outboundBindTarget);
return new DefaultBinding<MessageChannel>(name, null, outboundBindTarget, null);
}
}

View File

@@ -0,0 +1,120 @@
/*
* 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.
* 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.cloud.stream.binder.servlet.config;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.binder.servlet.EnabledBindings;
import org.springframework.cloud.stream.binding.BindingBeanDefinitionRegistryUtils;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils;
/**
* @author Dave Syer
*
*/
public class BeanFactoryEnabledBindings implements EnabledBindings {
private final ConfigurableListableBeanFactory beanFactory;
private final AtomicBoolean initialized = new AtomicBoolean(false);
private final Map<String, String> outputsToInputs = new HashMap<>();
private final Set<String> outputs = new HashSet<>();
private final Set<String> inputs = new HashSet<>();
private final BindingServiceProperties binding;
public BeanFactoryEnabledBindings(ConfigurableListableBeanFactory beanFactory,
BindingServiceProperties binding) {
this.beanFactory = beanFactory;
this.binding = binding;
}
@Override
public Set<String> getInputs() {
init();
return this.inputs;
}
@Override
public Set<String> getOutputs() {
init();
return this.outputs;
}
@Override
public String getInput(String output) {
init();
return outputsToInputs.get(output);
}
private void init() {
if (initialized.compareAndSet(false, true)) {
String[] names = beanFactory.getBeanNamesForAnnotation(EnableBinding.class);
for (String bean : names) {
Class<?> type = beanFactory.getType(bean);
MultiValueMap<String, Object> attrs = AnnotatedElementUtils
.getAllAnnotationAttributes(type, EnableBinding.class.getName());
List<Object> list = attrs.get("value");
if (list != null) {
for (Object object : list) {
Class<?>[] bindings = (Class<?>[]) object;
for (Class<?> binding : bindings) {
List<String> inputs = new ArrayList<>();
List<String> outputs = new ArrayList<>();
ReflectionUtils.doWithMethods(binding, method -> {
Input input = AnnotationUtils.findAnnotation(method,
Input.class);
Output output = AnnotationUtils.findAnnotation(method,
Output.class);
if (input != null) {
String name = BindingBeanDefinitionRegistryUtils
.getBindingTargetName(input, method);
inputs.add(BeanFactoryEnabledBindings.this.binding
.getBindingDestination(name));
}
if (output != null) {
String name = BindingBeanDefinitionRegistryUtils
.getBindingTargetName(output, method);
outputs.add(BeanFactoryEnabledBindings.this.binding
.getBindingDestination(name));
}
});
BeanFactoryEnabledBindings.this.outputs.addAll(outputs);
BeanFactoryEnabledBindings.this.inputs.addAll(inputs);
if (inputs.size() == 1 && outputs.size() == 1) {
BeanFactoryEnabledBindings.this.outputsToInputs
.put(outputs.get(0), inputs.get(0));
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2015-2016 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.cloud.stream.binder.servlet.config;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.stream.binder.servlet.EnabledBindings;
import org.springframework.cloud.stream.binder.servlet.MessageController;
import org.springframework.cloud.stream.binder.servlet.RouteRegistry;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Dave Syer
*/
@Configuration
@AutoConfigureBefore({ WebMvcAutoConfiguration.class })
@ConfigurationProperties("spring.cloud.stream.binder.servlet")
@ConditionalOnProperty(name = "spring.cloud.stream.enabled", havingValue = "true", matchIfMissing = true)
public class MessageHandlingAutoConfiguration {
/**
* The prefix for the HTTP endpoint.
*/
private String prefix = "stream";
/**
* The buffer timeout for messages sent to output channels, and accessed via GET.
*/
private long bufferTimeoutSeconds = 10;
/**
* The receive timeout for messages in a send-and-receive from a linked output via
* POST.
*/
private long receiveTimeoutMillis = 100;
@Autowired(required = false)
private List<RouteRegistry> registries;
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public long getBufferTimeoutSeconds() {
return bufferTimeoutSeconds;
}
public void setBufferTimeoutSeconds(long bufferTimeoutSeconds) {
this.bufferTimeoutSeconds = bufferTimeoutSeconds;
}
public long getReceiveTimeoutMillis() {
return receiveTimeoutMillis;
}
public void setReceiveTimeoutMillis(long receiveTimeoutMillis) {
this.receiveTimeoutMillis = receiveTimeoutMillis;
}
@Bean
public MessageController messageController(EnabledBindings bindings) {
MessageController controller = new MessageController(prefix, bindings);
controller.setBufferTimeoutSeconds(bufferTimeoutSeconds);
controller.setReceiveTimeoutSeconds(receiveTimeoutMillis);
if (registries != null) {
for (RouteRegistry registry : registries) {
controller.registerRoutes(registry.routes());
}
}
return controller;
}
@Bean
public BeanFactoryEnabledBindings enabledBindings(
ConfigurableListableBeanFactory beanFactory,
BindingServiceProperties binding) {
return new BeanFactoryEnabledBindings(beanFactory, binding);
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2015-2016 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.cloud.stream.binder.servlet.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.cloud.stream.binder.Binder;
import org.springframework.cloud.stream.binder.servlet.MessageController;
import org.springframework.cloud.stream.binder.servlet.ServletMessageChannelBinder;
import org.springframework.cloud.stream.config.codec.kryo.KryoCodecAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.integration.codec.Codec;
/**
* @author Dave Syer
*/
@Configuration
@ConditionalOnMissingBean(Binder.class)
@AutoConfigureBefore({ WebMvcAutoConfiguration.class })
@Import({ PropertyPlaceholderAutoConfiguration.class, KryoCodecAutoConfiguration.class })
public class ServletServiceAutoConfiguration {
@Autowired
private Codec codec;
@Bean
public ServletMessageChannelBinder servletMessageChannelBinder(
MessageController controller) {
ServletMessageChannelBinder messageChannelBinder = new ServletMessageChannelBinder(
controller);
messageChannelBinder.setCodec(this.codec);
return messageChannelBinder;
}
}

View File

@@ -0,0 +1,5 @@
/**
* This package contains an implementation of the {@link org.springframework.cloud.stream.binder.Binder} for Redis.
*/
package org.springframework.cloud.stream.binder.servlet;

View File

@@ -0,0 +1,2 @@
servlet:\
org.springframework.cloud.stream.binder.servlet.config.ServletServiceAutoConfiguration

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.stream.binder.servlet.config.MessageHandlingAutoConfiguration

View File

@@ -0,0 +1,79 @@
/*
* 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.
* 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.cloud.stream.binder.servlet;
import java.util.Arrays;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Dave Syer
*
*/
public class JsonUtilsTests {
private ObjectMapper mapper = new ObjectMapper();
@Test
public void empty() {
assertThat(JsonUtils.split("[]")).isEmpty();
}
@Test
public void strings() {
assertThat(JsonUtils.split("[\"foo\", \"bar\"]")).hasSize(2).contains("foo",
"bar");
}
@Test
public void objects() throws Exception {
assertThat(JsonUtils.split(
mapper.writeValueAsString(Arrays.asList(new Foo("foo"), new Foo("bar")))))
.hasSize(2)
.contains("{\"value\":\"foo\"}", "{\"value\":\"bar\"}");
}
@Test
public void arrays() throws Exception {
assertThat(JsonUtils.split(mapper.writeValueAsString(
Arrays.asList(Arrays.asList(new Foo("foo"), new Foo("bar")))))).hasSize(1)
.contains("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
}
protected static class Foo {
private String value;
public Foo() {
}
public Foo(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}

View File

@@ -0,0 +1,101 @@
/*
* 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.
* 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.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.http.MediaType;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class DoubleSinkMessageChannelBinderTests implements MessageHandler {
@Autowired
private Sink sink;
@Autowired
private Custom custom;
@Autowired
private MockMvc mockMvc;
private Message<?> message;
@Test
public void string() throws Exception {
sink.input().subscribe(this);
mockMvc.perform(
post("/stream/input").contentType(MediaType.TEXT_PLAIN).content("hello"))
.andExpect(status().isAccepted())
.andExpect(content().string(containsString("hello")));
assertThat(this.message).isNotNull();
sink.input().unsubscribe(this);
}
@Test
public void custom() throws Exception {
custom.input().subscribe(this);
mockMvc.perform(
post("/stream/custom").contentType(MediaType.TEXT_PLAIN).content("hello"))
.andExpect(status().isAccepted())
.andExpect(content().string(containsString("hello")));
assertThat(this.message).isNotNull();
custom.input().unsubscribe(this);
}
@SpringBootApplication
@EnableBinding({ Sink.class, Custom.class })
protected static class TestConfiguration {
}
@Override
public void handleMessage(Message<?> message) throws MessagingException {
this.message = message;
}
interface Custom {
@Input("custom")
SubscribableChannel input();
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.
* 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.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.http.MediaType;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class HeaderProcessorMessageChannelBinderTests {
@Autowired
private MockMvc mockMvc;
@Test
public void function() throws Exception {
mockMvc.perform(post("/stream/input").contentType(MediaType.APPLICATION_JSON)
.content("\"hello\"")).andExpect(status().isOk())
.andExpect(header().string("x-foo", "bar"))
.andExpect(content().string(containsString("HELLO")));
}
@SpringBootApplication
@EnableBinding(Processor.class)
protected static class TestConfiguration {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public Message<String> uppercase(Message<String> input) {
return MessageBuilder.withPayload(input.getPayload().toUpperCase())
.copyHeadersIfAbsent(input.getHeaders()).setHeader("x-foo", "bar")
.build();
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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.
* 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.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.binder.servlet.MessageController;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.http.MediaType;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class HeadersDroppedRoutedProcessorMessageChannelBinderTests {
@Autowired
private MockMvc mockMvc;
@Test
public void function() throws Exception {
mockMvc.perform(post("/stream/input/words")
.contentType(MediaType.APPLICATION_JSON).content("\"hello\""))
.andExpect(status().isOk())
.andExpect(header().string(MessageController.ROUTE_KEY, "words"))
.andExpect(content().string(containsString("HELLO")));
}
@SpringBootApplication
@EnableBinding(Processor.class)
protected static class TestConfiguration {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public String uppercase(String input) {
return input.toUpperCase();
}
}
}

View File

@@ -0,0 +1,116 @@
/*
* 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.
* 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.cloud.stream.binder.servlet.test;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.binder.servlet.MessageController;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.http.MediaType;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest("spring.cloud.stream.bindings.input.destination:words")
@AutoConfigureMockMvc
@DirtiesContext
public class NamedProcessorMessageChannelBinderTests {
@Autowired
private MockMvc mockMvc;
@Autowired
private MessageController controller;
@Autowired
private Processor processor;
@Before
public void init() {
controller.reset();
}
@Test
public void function() throws Exception {
mockMvc.perform(post("/stream/words").contentType(MediaType.APPLICATION_JSON)
.content("\"hello\"")).andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void implicit() throws Exception {
mockMvc.perform(post("/stream").contentType(MediaType.APPLICATION_JSON)
.content("\"hello\"")).andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void empty() throws Exception {
mockMvc.perform(get("/stream/output").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string(containsString("[]")));
}
@Test
public void output() throws Exception {
processor.input().send(MessageBuilder.withPayload("hello").build());
mockMvc.perform(get("/stream/output").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void outputWithRoute() throws Exception {
processor.input().send(MessageBuilder.withPayload("hello")
.setHeader(MessageController.ROUTE_KEY, "uppercase").build());
mockMvc.perform(
get("/stream/output/uppercase").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@SpringBootApplication
@EnableBinding(Processor.class)
protected static class TestConfiguration {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public String uppercase(String input) {
return input.toUpperCase();
}
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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.
* 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.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.http.MediaType;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class NamedSinkMessageChannelBinderTests implements MessageHandler {
@Autowired
private Sink sink;
@Autowired
private MockMvc mockMvc;
private Message<?> message;
@Test
public void consumer() throws Exception {
sink.input().subscribe(this);
mockMvc.perform(post("/stream/input").contentType(MediaType.APPLICATION_JSON)
.content("\"hello\"")).andExpect(status().isAccepted())
.andExpect(content().string(containsString("hello")));
assertThat(this.message).isNotNull();
sink.input().unsubscribe(this);
}
@SpringBootApplication
@EnableBinding(Sink.class)
protected static class TestConfiguration {
}
@Override
public void handleMessage(Message<?> message) throws MessagingException {
this.message = message;
}
}

View File

@@ -0,0 +1,65 @@
/*
* 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.
* 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.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest("spring.cloud.stream.bindings.output.destination:words")
@AutoConfigureMockMvc
@DirtiesContext
public class NamedSourceMessageChannelBinderTests {
@Autowired
private Source source;
@Autowired
private MockMvc mockMvc;
@Test
public void supplier() throws Exception {
source.output().send(MessageBuilder.withPayload("hello").build());
mockMvc.perform(get("/stream/words")).andExpect(status().isOk())
.andExpect(content().string(containsString("hello")));
}
@SpringBootApplication
@EnableBinding(Source.class)
protected static class TestConfiguration {
}
}

View File

@@ -0,0 +1,106 @@
/*
* 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.
* 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.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.http.MediaType;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest(properties = "logging.level.org.springframework.web=DEBUG")
@AutoConfigureMockMvc
@DirtiesContext
public class PojoProcessorMessageChannelBinderTests {
@Autowired
private MockMvc mockMvc;
@Test
public void json() throws Exception {
mockMvc.perform(post("/stream").contentType(MediaType.APPLICATION_JSON)
.content("{\"value\":\"hello\"}")).andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void text() throws Exception {
mockMvc.perform(post("/stream").contentType(MediaType.TEXT_PLAIN)
.content("[{\"value\":\"hello\"}]")).andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void single() throws Exception {
mockMvc.perform(post("/stream").contentType(MediaType.TEXT_PLAIN)
.content("{\"value\":\"hello\"}")).andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@SpringBootApplication
@EnableBinding(Processor.class)
protected static class TestConfiguration {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public Foo uppercase(Foo input) {
return input.toUpperCase();
}
}
protected static class Foo {
private String value;
public Foo() {
}
public Foo(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public Foo toUpperCase() {
return new Foo(value.toUpperCase());
}
}
}

View File

@@ -0,0 +1,113 @@
/*
* 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.
* 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.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.http.MediaType;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class PojoSinkMessageChannelBinderTests implements MessageHandler {
@Autowired
private Sink sink;
@Autowired
private MockMvc mockMvc;
private Message<?> message;
@Test
public void consumer() throws Exception {
sink.input().subscribe(this);
mockMvc.perform(post("/stream/input").contentType(MediaType.APPLICATION_JSON)
.content("{\"value\":\"hello\"}")).andExpect(status().isAccepted())
.andExpect(content().string("{\"value\":\"hello\"}"));
assertThat(this.message).isNotNull();
sink.input().unsubscribe(this);
}
@Test
public void multi() throws Exception {
sink.input().subscribe(this);
mockMvc.perform(post("/stream/input").contentType(MediaType.APPLICATION_JSON)
.content("[{\"value\":\"hello\"},{\"value\":\"world\"}]"))
.andExpect(status().isAccepted()).andExpect(content()
.string("[{\"value\":\"hello\"}, {\"value\":\"world\"}]"));
assertThat(this.message).isNotNull();
sink.input().unsubscribe(this);
}
@SpringBootApplication
@EnableBinding(Sink.class)
protected static class TestConfiguration {
}
@Override
public void handleMessage(Message<?> message) throws MessagingException {
this.message = message;
}
protected static class Foo {
private String value;
public Foo() {
}
public Foo(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public Foo toUpperCase() {
return new Foo(value.toUpperCase());
}
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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.
* 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.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.http.MediaType;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest("spring.cloud.stream.binder.servlet.prefix:awesome")
@AutoConfigureMockMvc
@DirtiesContext
public class PrefixMessageChannelBinderTests implements MessageHandler {
@Autowired
private Sink sink;
@Autowired
private MockMvc mockMvc;
private Message<?> message;
@Test
public void consumer() throws Exception {
sink.input().subscribe(this);
mockMvc.perform(post("/awesome/input").contentType(MediaType.APPLICATION_JSON)
.content("\"hello\"")).andExpect(status().isAccepted())
.andExpect(content().string(containsString("hello")));
assertThat(this.message).isNotNull();
sink.input().unsubscribe(this);
}
@SpringBootApplication
@EnableBinding(Sink.class)
protected static class TestConfiguration {
}
@Override
public void handleMessage(Message<?> message) throws MessagingException {
this.message = message;
}
}

View File

@@ -0,0 +1,138 @@
/*
* 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.
* 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.cloud.stream.binder.servlet.test;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.http.MediaType;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class ProcessorMessageChannelBinderTests {
@Autowired
private Processor processor;
@Autowired
private MockMvc mockMvc;
@Before
public void init() throws Exception {
mockMvc.perform(get("/stream/output?purge=true")).andReturn();
}
@Test
public void supplier() throws Exception {
processor.output().send(MessageBuilder.withPayload("hello").build());
mockMvc.perform(get("/stream/output")).andExpect(status().isOk())
.andExpect(content().string(containsString("hello")));
}
@Test
public void missing() throws Exception {
// Missing route is not found if channel is explicit
mockMvc.perform(get("/stream/output/missing")).andExpect(status().isNotFound())
.andExpect(content().string(equalTo("")));
}
@Test
public void empty() throws Exception {
// Missing route where channel can be inferred (there is an input channel) is
// going to be passed on as a body
mockMvc.perform(get("/stream/missing")).andExpect(status().isOk())
.andExpect(content().string(equalTo("MISSING")));
}
@Test
public void function() throws Exception {
mockMvc.perform(post("/stream/input").contentType(MediaType.APPLICATION_JSON)
.content("\"hello\"")).andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void keyed() throws Exception {
mockMvc.perform(get("/stream/hello")).andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void implicit() throws Exception {
mockMvc.perform(post("/stream").contentType(MediaType.APPLICATION_JSON)
.content("\"hello\"")).andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void string() throws Exception {
mockMvc.perform(
post("/stream/input").contentType(MediaType.TEXT_PLAIN).content("hello"))
.andExpect(status().isOk()).andExpect(content().string(equalTo("HELLO")));
}
@Test
public void multi() throws Exception {
mockMvc.perform(post("/stream/input").contentType(MediaType.APPLICATION_JSON)
.content("[\"hello\",\"world\"]")).andExpect(status().isOk())
.andExpect(content().string("[\"HELLO\",\"WORLD\"]"));
}
@SpringBootApplication
@EnableBinding(Processor.class)
protected static class TestConfiguration {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public String uppercase(String input) {
return input.toUpperCase();
}
public static void main(String[] args) throws Exception {
SpringApplication.run(
ProcessorMessageChannelBinderTests.TestConfiguration.class,
"--logging.level.root=INFO");
}
}
}

View File

@@ -0,0 +1,99 @@
/*
* 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.
* 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.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.binder.servlet.MessageController;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.http.MediaType;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class RoutedProcessorMessageChannelBinderTests {
@Autowired
private MockMvc mockMvc;
@Test
public void function() throws Exception {
mockMvc.perform(post("/stream/input/words")
.contentType(MediaType.APPLICATION_JSON).content("\"hello\""))
.andExpect(status().isOk())
.andExpect(header().string(MessageController.ROUTE_KEY, "words"))
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void keyed() throws Exception {
mockMvc.perform(get("/stream/words/hello")).andExpect(status().isOk())
.andExpect(header().string(MessageController.ROUTE_KEY, "words"))
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void channelAndKeyed() throws Exception {
mockMvc.perform(get("/stream/input/words/hello")).andExpect(status().isOk())
.andExpect(header().string(MessageController.ROUTE_KEY, "words"))
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void implicit() throws Exception {
mockMvc.perform(post("/stream/words").contentType(MediaType.APPLICATION_JSON)
.content("\"hello\"")).andExpect(status().isOk())
.andExpect(header().string(MessageController.ROUTE_KEY, "words"))
.andExpect(content().string(containsString("HELLO")));
}
@SpringBootApplication
@EnableBinding(Processor.class)
protected static class TestConfiguration {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public Message<String> uppercase(Message<String> input) {
return MessageBuilder.withPayload(input.getPayload().toUpperCase())
.copyHeadersIfAbsent(input.getHeaders()).build();
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* 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.
* 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.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.binder.servlet.MessageController;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.http.MediaType;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class RoutedSinkMessageChannelBinderTests implements MessageHandler {
@Autowired
private Sink sink;
@Autowired
private MockMvc mockMvc;
private Message<?> message;
@Test
public void consumer() throws Exception {
sink.input().subscribe(this);
mockMvc.perform(post("/stream/input/words")
.contentType(MediaType.APPLICATION_JSON).content("\"hello\""))
.andExpect(status().isAccepted())
.andExpect(content().string(containsString("hello")));
assertThat(this.message).isNotNull();
assertThat(this.message.getHeaders().get(MessageController.ROUTE_KEY))
.isEqualTo("words");
sink.input().unsubscribe(this);
}
@SpringBootApplication
@EnableBinding(Sink.class)
protected static class TestConfiguration {
}
@Override
public void handleMessage(Message<?> message) throws MessagingException {
this.message = message;
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.
* 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.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.binder.servlet.MessageController;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class RoutedSourceMessageChannelBinderTests {
@Autowired
private Source source;
@Autowired
private MockMvc mockMvc;
@Test
public void supplier() throws Exception {
source.output().send(MessageBuilder.withPayload("hello")
.setHeader(MessageController.ROUTE_KEY, "words").build());
mockMvc.perform(get("/stream/output/words")).andExpect(status().isOk())
.andExpect(content().string(containsString("hello")));
}
@Test
public void implicit() throws Exception {
source.output().send(MessageBuilder.withPayload("hello")
.setHeader(MessageController.ROUTE_KEY, "words").build());
mockMvc.perform(get("/stream/words")).andExpect(status().isOk())
.andExpect(content().string(containsString("hello")));
}
@SpringBootApplication
@EnableBinding(Source.class)
protected static class TestConfiguration {
}
}

Some files were not shown because too many files have changed in this diff Show More