Migrate to servlet binder for web features
This commit is contained in:
10
README.adoc
10
README.adoc
@@ -101,12 +101,12 @@ JMS.
|
|||||||
|
|
||||||
The `@Beans` can be `Function`, `Consumer` or `Supplier` (all from
|
The `@Beans` can be `Function`, `Consumer` or `Supplier` (all from
|
||||||
`java.util`), and their parametric types can be String or POJO. A
|
`java.util`), and their parametric types can be String or POJO. A
|
||||||
`Function` is exposed as an HTTP POST if `spring-cloud-function-web`
|
`Function` is exposed as a `Processor` if
|
||||||
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` is on the classpath and a
|
||||||
`spring.cloud.function.stream.endpoint` property is configured in the Spring
|
`spring.cloud.function.stream.endpoint` property is configured in the
|
||||||
environment. A `Consumer` is also exposed as an HTTP POST, or as a Stream
|
Spring environment. A `Consumer` is also exposed as an HTTP POST, or
|
||||||
`Sink`. A `Supplier` translates to an HTTP GET, or a Stream `Source`.
|
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
|
Functions can be of `Flux<String>` or `Flux<Pojo>` and Spring Cloud
|
||||||
Function takes care of converting the data to and from the desired
|
Function takes care of converting the data to and from the desired
|
||||||
|
|||||||
@@ -41,12 +41,11 @@ JMS.
|
|||||||
|
|
||||||
The `@Beans` can be `Function`, `Consumer` or `Supplier` (all from
|
The `@Beans` can be `Function`, `Consumer` or `Supplier` (all from
|
||||||
`java.util`), and their parametric types can be String or POJO. A
|
`java.util`), and their parametric types can be String or POJO. A
|
||||||
`Function` is exposed as an HTTP POST if `spring-cloud-function-web`
|
`Function` is exposed as a Spring Cloud Stream `Processor` if
|
||||||
is on the classpath, and as a Spring Cloud Stream `Processor` if
|
`spring-cloud-function-stream` is on the classpath.
|
||||||
`spring-cloud-function-stream` is on the classpath and a
|
A `Consumer` is also exposed as a Stream
|
||||||
`spring.cloud.function.stream.endpoint` property is configured in the Spring
|
`Sink` and a `Supplier` translates to a Stream `Source`.
|
||||||
environment. A `Consumer` is also exposed as an HTTP POST, or as a Stream
|
HTTP endpoints are exposed if the Stream binder is `spring-cloud-stream-binder-servlet`.
|
||||||
`Sink`. A `Supplier` translates to an HTTP GET, or a Stream `Source`.
|
|
||||||
|
|
||||||
Functions can be of `Flux<String>` or `Flux<Pojo>` and Spring Cloud
|
Functions can be of `Flux<String>` or `Flux<Pojo>` and Spring Cloud
|
||||||
Function takes care of converting the data to and from the desired
|
Function takes care of converting the data to and from the desired
|
||||||
|
|||||||
1
pom.xml
1
pom.xml
@@ -56,7 +56,6 @@
|
|||||||
<module>spring-cloud-function-context</module>
|
<module>spring-cloud-function-context</module>
|
||||||
<module>spring-cloud-function-stream</module>
|
<module>spring-cloud-function-stream</module>
|
||||||
<module>spring-cloud-function-task</module>
|
<module>spring-cloud-function-task</module>
|
||||||
<module>spring-cloud-function-web</module>
|
|
||||||
<module>spring-cloud-function-samples</module>
|
<module>spring-cloud-function-samples</module>
|
||||||
<module>spring-cloud-function-deployer</module>
|
<module>spring-cloud-function-deployer</module>
|
||||||
<module>spring-cloud-function-adapters</module>
|
<module>spring-cloud-function-adapters</module>
|
||||||
|
|||||||
@@ -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).
|
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).
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import org.apache.commons.logging.LogFactory;
|
|||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
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.cloud.function.core.FunctionCatalog;
|
||||||
import org.springframework.context.ConfigurableApplicationContext;
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import java.util.function.Function;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
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.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import java.util.function.Function;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
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.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import java.util.stream.Collectors;
|
|||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Test;
|
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.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import java.util.function.Function;
|
|||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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.FluxFunction;
|
||||||
import org.springframework.cloud.function.core.FunctionCatalog;
|
import org.springframework.cloud.function.core.FunctionCatalog;
|
||||||
import org.springframework.cloud.function.core.FunctionFactoryUtils;
|
import org.springframework.cloud.function.core.FunctionFactoryUtils;
|
||||||
|
|||||||
@@ -13,33 +13,19 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
package org.springframework.cloud.function.context.catalog;
|
||||||
|
|
||||||
package org.springframework.cloud.function.web.flux.request;
|
import org.springframework.context.ApplicationEvent;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import reactor.core.publisher.Flux;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Dave Syer
|
* @author Dave Syer
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public class FluxRequest<T> {
|
@SuppressWarnings("serial")
|
||||||
|
public class FunctionCatalogEvent extends ApplicationEvent {
|
||||||
private List<T> body;
|
|
||||||
|
|
||||||
public FluxRequest(List<T> body) {
|
public FunctionCatalogEvent(Object source) {
|
||||||
this.body = body;
|
super(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Flux<T> flux() {
|
|
||||||
return Flux.fromIterable(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<T> body() {
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.cloud.function.context;
|
package org.springframework.cloud.function.context.catalog;
|
||||||
|
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.cloud.function.context;
|
package org.springframework.cloud.function.context.catalog;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -24,7 +24,14 @@ import java.util.function.Consumer;
|
|||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,7 +39,8 @@ import org.springframework.util.Assert;
|
|||||||
* @author Mark Fisher
|
* @author Mark Fisher
|
||||||
* @author Oleg Zhurakousky
|
* @author Oleg Zhurakousky
|
||||||
*/
|
*/
|
||||||
public class InMemoryFunctionCatalog implements FunctionRegistry {
|
public class InMemoryFunctionCatalog
|
||||||
|
implements FunctionRegistry, ApplicationEventPublisherAware {
|
||||||
|
|
||||||
private final Map<String, Function<?, ?>> functions;
|
private final Map<String, Function<?, ?>> functions;
|
||||||
|
|
||||||
@@ -40,11 +48,13 @@ public class InMemoryFunctionCatalog implements FunctionRegistry {
|
|||||||
|
|
||||||
private final Map<String, Supplier<?>> suppliers;
|
private final Map<String, Supplier<?>> suppliers;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private ApplicationEventPublisher publisher;
|
||||||
|
|
||||||
public InMemoryFunctionCatalog() {
|
public InMemoryFunctionCatalog() {
|
||||||
this(Collections.emptySet());
|
this(Collections.emptySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Autowired(required=false)
|
|
||||||
public InMemoryFunctionCatalog(Set<FunctionRegistration<?>> registrations) {
|
public InMemoryFunctionCatalog(Set<FunctionRegistration<?>> registrations) {
|
||||||
Assert.notNull(registrations, "'registrations' must not be null");
|
Assert.notNull(registrations, "'registrations' must not be null");
|
||||||
this.suppliers = new HashMap<>();
|
this.suppliers = new HashMap<>();
|
||||||
@@ -66,20 +76,68 @@ public class InMemoryFunctionCatalog implements FunctionRegistry {
|
|||||||
@Override
|
@Override
|
||||||
public <T> void register(FunctionRegistration<T> registration) {
|
public <T> void register(FunctionRegistration<T> registration) {
|
||||||
Map<String, ?> values = null;
|
Map<String, ?> values = null;
|
||||||
|
FunctionRegistrationEvent event;
|
||||||
if (registration.getTarget() instanceof Function) {
|
if (registration.getTarget() instanceof Function) {
|
||||||
values = this.functions;
|
values = this.functions;
|
||||||
|
event = new FunctionRegistrationEvent(this, Function.class,
|
||||||
|
registration.getNames());
|
||||||
}
|
}
|
||||||
else if (registration.getTarget() instanceof Supplier) {
|
else if (registration.getTarget() instanceof Supplier) {
|
||||||
values = this.suppliers;
|
values = this.suppliers;
|
||||||
|
event = new FunctionRegistrationEvent(this, Supplier.class,
|
||||||
|
registration.getNames());
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
values = this.consumers;
|
values = this.consumers;
|
||||||
|
event = new FunctionRegistrationEvent(this, Consumer.class,
|
||||||
|
registration.getNames());
|
||||||
}
|
}
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<String, Object> map = (Map<String, Object>) values;
|
Map<String, Object> map = (Map<String, Object>) values;
|
||||||
for (String name : registration.getNames()) {
|
for (String name : registration.getNames()) {
|
||||||
map.put(name, registration.getTarget());
|
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
|
@Override
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* 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.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
@@ -34,6 +34,8 @@ import java.util.function.Consumer;
|
|||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.beans.factory.config.BeanDefinition;
|
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.beans.factory.support.AbstractBeanDefinition;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
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.FluxConsumer;
|
||||||
import org.springframework.cloud.function.core.FluxFunction;
|
import org.springframework.cloud.function.core.FluxFunction;
|
||||||
import org.springframework.cloud.function.core.FluxSupplier;
|
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.IsolatedConsumer;
|
||||||
import org.springframework.cloud.function.core.IsolatedFunction;
|
import org.springframework.cloud.function.core.IsolatedFunction;
|
||||||
import org.springframework.cloud.function.core.IsolatedSupplier;
|
import org.springframework.cloud.function.core.IsolatedSupplier;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.core.ResolvableType;
|
import org.springframework.core.ResolvableType;
|
||||||
@@ -112,32 +122,38 @@ public class ContextFunctionCatalogAutoConfiguration {
|
|||||||
processor.register(registration);
|
processor.register(registration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public <T> Supplier<T> lookupSupplier(String name) {
|
public <T> Supplier<T> lookupSupplier(String name) {
|
||||||
Supplier<T> result = (Supplier<T>) processor.lookupSupplier(name);
|
Supplier<T> result = (Supplier<T>) processor.lookupSupplier(name);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public <T, R> Function<T, R> lookupFunction(String name) {
|
public <T, R> Function<T, R> lookupFunction(String name) {
|
||||||
Function<T, R> result = (Function<T, R>) processor.lookupFunction(name);
|
Function<T, R> result = (Function<T, R>) processor.lookupFunction(name);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public <T> Consumer<T> lookupConsumer(String name) {
|
public <T> Consumer<T> lookupConsumer(String name) {
|
||||||
Consumer<T> result = (Consumer<T>) processor.lookupConsumer(name);
|
Consumer<T> result = (Consumer<T>) processor.lookupConsumer(name);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public Set<String> getSupplierNames() {
|
public Set<String> getSupplierNames() {
|
||||||
return this.processor.getSuppliers();
|
return this.processor.getSuppliers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public Set<String> getFunctionNames() {
|
public Set<String> getFunctionNames() {
|
||||||
return this.processor.getFunctions();
|
return this.processor.getFunctions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public Set<String> getConsumerNames() {
|
public Set<String> getConsumerNames() {
|
||||||
return this.processor.getConsumers();
|
return this.processor.getConsumers();
|
||||||
}
|
}
|
||||||
@@ -202,6 +218,8 @@ public class ContextFunctionCatalogAutoConfiguration {
|
|||||||
|
|
||||||
private Map<String, Object> consumers = new HashMap<>();
|
private Map<String, Object> consumers = new HashMap<>();
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private ApplicationEventPublisher publisher;
|
||||||
@Autowired
|
@Autowired
|
||||||
private ConfigurableListableBeanFactory registry;
|
private ConfigurableListableBeanFactory registry;
|
||||||
|
|
||||||
@@ -339,6 +357,24 @@ public class ContextFunctionCatalogAutoConfiguration {
|
|||||||
function.getNames().iterator().next());
|
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(
|
public Set<FunctionRegistration<?>> merge(
|
||||||
Map<String, FunctionRegistration<?>> initial,
|
Map<String, FunctionRegistration<?>> initial,
|
||||||
Map<String, Consumer<?>> consumers, Map<String, Supplier<?>> suppliers,
|
Map<String, Consumer<?>> consumers, Map<String, Supplier<?>> suppliers,
|
||||||
@@ -417,7 +453,9 @@ public class ContextFunctionCatalogAutoConfiguration {
|
|||||||
private void wrap(FunctionRegistration<Object> registration, String key) {
|
private void wrap(FunctionRegistration<Object> registration, String key) {
|
||||||
Object target = registration.getTarget();
|
Object target = registration.getTarget();
|
||||||
this.registrations.put(target, key);
|
this.registrations.put(target, key);
|
||||||
|
Class<?> type;
|
||||||
if (target instanceof Supplier) {
|
if (target instanceof Supplier) {
|
||||||
|
type = Supplier.class;
|
||||||
findType(target, ParamType.OUTPUT);
|
findType(target, ParamType.OUTPUT);
|
||||||
registration.target(target((Supplier<?>) target, key));
|
registration.target(target((Supplier<?>) target, key));
|
||||||
for (String name : registration.getNames()) {
|
for (String name : registration.getNames()) {
|
||||||
@@ -425,6 +463,7 @@ public class ContextFunctionCatalogAutoConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (target instanceof Consumer) {
|
else if (target instanceof Consumer) {
|
||||||
|
type = Consumer.class;
|
||||||
findType(target, ParamType.INPUT);
|
findType(target, ParamType.INPUT);
|
||||||
registration.target(target((Consumer<?>) target, key));
|
registration.target(target((Consumer<?>) target, key));
|
||||||
for (String name : registration.getNames()) {
|
for (String name : registration.getNames()) {
|
||||||
@@ -432,6 +471,7 @@ public class ContextFunctionCatalogAutoConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (target instanceof Function) {
|
else if (target instanceof Function) {
|
||||||
|
type = Function.class;
|
||||||
findType(target, ParamType.INPUT);
|
findType(target, ParamType.INPUT);
|
||||||
findType(target, ParamType.OUTPUT);
|
findType(target, ParamType.OUTPUT);
|
||||||
registration.target(target((Function<?, ?>) target, key));
|
registration.target(target((Function<?, ?>) target, key));
|
||||||
@@ -439,8 +479,15 @@ public class ContextFunctionCatalogAutoConfiguration {
|
|||||||
this.functions.put(name, (Function<?, ?>) registration.getTarget());
|
this.functions.put(name, (Function<?, ?>) registration.getTarget());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
registrations.remove(target);
|
registrations.remove(target);
|
||||||
this.registrations.put(registration.getTarget(), key);
|
this.registrations.put(registration.getTarget(), key);
|
||||||
|
if (publisher != null) {
|
||||||
|
publisher.publishEvent(new FunctionRegistrationEvent(
|
||||||
|
registration.getTarget(), type, registration.getNames()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getQualifier(String key) {
|
private String getQualifier(String key) {
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||||
org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration
|
org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.cloud.function.context;
|
package org.springframework.cloud.function.context.config;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -24,8 +24,9 @@ import java.util.function.Supplier;
|
|||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration.BeanFactoryFunctionCatalog;
|
import org.springframework.cloud.function.context.FunctionRegistration;
|
||||||
import org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration.ContextFunctionRegistry;
|
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;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* 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;
|
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.boot.builder.SpringApplicationBuilder;
|
||||||
import org.springframework.cloud.function.compiler.CompiledFunctionFactory;
|
import org.springframework.cloud.function.compiler.CompiledFunctionFactory;
|
||||||
import org.springframework.cloud.function.compiler.FunctionCompiler;
|
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.core.FunctionCatalog;
|
||||||
import org.springframework.cloud.function.scan.ScannedFunction;
|
import org.springframework.cloud.function.scan.ScannedFunction;
|
||||||
import org.springframework.cloud.function.test.GenericFunction;
|
import org.springframework.cloud.function.test.GenericFunction;
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.cloud.function.context;
|
package org.springframework.cloud.function.context.config;
|
||||||
|
|
||||||
import java.net.URLClassLoader;
|
import java.net.URLClassLoader;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -28,10 +28,10 @@ import org.junit.After;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import org.springframework.beans.BeanUtils;
|
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.test.util.ReflectionTestUtils;
|
||||||
import org.springframework.util.ClassUtils;
|
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;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<spring-cloud-deployer-thin.version>1.0.8.RELEASE</spring-cloud-deployer-thin.version>
|
<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>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -25,8 +26,12 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.cloud</groupId>
|
<groupId>org.springframework.cloud</groupId>
|
||||||
<artifactId>spring-cloud-function-web</artifactId>
|
<artifactId>spring-cloud-function-stream</artifactId>
|
||||||
<scope>runtime</scope>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-stream-binder-servlet</artifactId>
|
||||||
|
<version>${spring-cloud-stream-servlet.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ package org.springframework.cloud.function.deployer;
|
|||||||
|
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
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.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
|||||||
@@ -31,13 +31,16 @@ import org.apache.commons.logging.Log;
|
|||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
import org.springframework.beans.factory.DisposableBean;
|
import org.springframework.beans.factory.DisposableBean;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.loader.thin.ArchiveUtils;
|
import org.springframework.boot.loader.thin.ArchiveUtils;
|
||||||
import org.springframework.cloud.deployer.spi.app.AppDeployer;
|
import org.springframework.cloud.deployer.spi.app.AppDeployer;
|
||||||
import org.springframework.cloud.deployer.spi.core.AppDefinition;
|
import org.springframework.cloud.deployer.spi.core.AppDefinition;
|
||||||
import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest;
|
import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest;
|
||||||
import org.springframework.cloud.deployer.thin.ThinJarAppDeployer;
|
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.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.context.support.LiveBeansView;
|
||||||
import org.springframework.core.io.FileSystemResource;
|
import org.springframework.core.io.FileSystemResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
@@ -49,6 +52,10 @@ public class FunctionExtractingFunctionCatalog
|
|||||||
private static Log logger = LogFactory
|
private static Log logger = LogFactory
|
||||||
.getLog(FunctionExtractingFunctionCatalog.class);
|
.getLog(FunctionExtractingFunctionCatalog.class);
|
||||||
|
|
||||||
|
private RouteRegistrar routes;
|
||||||
|
|
||||||
|
private SupplierInvokingMessageProducer<?> producer;
|
||||||
|
|
||||||
private ThinJarAppDeployer deployer;
|
private ThinJarAppDeployer deployer;
|
||||||
|
|
||||||
private Map<String, String> deployed = new LinkedHashMap<>();
|
private Map<String, String> deployed = new LinkedHashMap<>();
|
||||||
@@ -65,6 +72,16 @@ public class FunctionExtractingFunctionCatalog
|
|||||||
deployer = new ThinJarAppDeployer(name, profiles);
|
deployer = new ThinJarAppDeployer(name, profiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public void setRouteRegistrar(RouteRegistrar routes) {
|
||||||
|
this.routes = routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public void setProducer(SupplierInvokingMessageProducer<?> producer) {
|
||||||
|
this.producer = producer;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void destroy() throws Exception {
|
public void destroy() throws Exception {
|
||||||
for (String name : new HashSet<>(names.keySet())) {
|
for (String name : new HashSet<>(names.keySet())) {
|
||||||
@@ -156,6 +173,7 @@ public class FunctionExtractingFunctionCatalog
|
|||||||
this.deployed.put(id, path);
|
this.deployed.put(id, path);
|
||||||
this.names.put(name, id);
|
this.names.put(name, id);
|
||||||
this.ids.put(id, name);
|
this.ids.put(id, name);
|
||||||
|
register(name);
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +183,7 @@ public class FunctionExtractingFunctionCatalog
|
|||||||
// TODO: Convert to 404
|
// TODO: Convert to 404
|
||||||
throw new IllegalStateException("No such app");
|
throw new IllegalStateException("No such app");
|
||||||
}
|
}
|
||||||
|
unregister(name);
|
||||||
this.deployer.undeploy(id);
|
this.deployer.undeploy(id);
|
||||||
String path = this.deployed.remove(id);
|
String path = this.deployed.remove(id);
|
||||||
this.names.remove(name);
|
this.names.remove(name);
|
||||||
@@ -172,6 +191,39 @@ public class FunctionExtractingFunctionCatalog
|
|||||||
return new DeployedArtifact(name, id, path);
|
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) {
|
private Object inspect(Object arg, String method) {
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isDebugEnabled()) {
|
||||||
logger.debug("Inspecting " + method);
|
logger.debug("Inspecting " + method);
|
||||||
@@ -195,53 +247,90 @@ public class FunctionExtractingFunctionCatalog
|
|||||||
|
|
||||||
private Object invoke(Class<?> type, String method, Object... arg) {
|
private Object invoke(Class<?> type, String method, Object... arg) {
|
||||||
Set<Object> results = new LinkedHashSet<>();
|
Set<Object> results = new LinkedHashSet<>();
|
||||||
|
Object fallback = null;
|
||||||
for (String id : this.deployed.keySet()) {
|
for (String id : this.deployed.keySet()) {
|
||||||
Object catalog = this.deployer.getBean(id, type);
|
Object result = invoke(id, type, method, arg);
|
||||||
if (catalog == null) {
|
if (result instanceof Collection) {
|
||||||
|
results.addAll((Collection<?>) result);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String name = this.ids.get(id);
|
if (result != null) {
|
||||||
String prefix = name + "/";
|
if (result == Object.class) {
|
||||||
if (arg.length == 1) {
|
// Type fallback is Object
|
||||||
if (arg[0] instanceof String) {
|
fallback = Object.class;
|
||||||
String specific = arg[0].toString();
|
continue;
|
||||||
if (specific.startsWith(prefix)) {
|
|
||||||
arg[0] = specific.substring(prefix.length());
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
if (result instanceof Boolean && !((Boolean) result)) {
|
||||||
try {
|
// Boolean fallback is false
|
||||||
MethodInvoker invoker = new MethodInvoker();
|
fallback = false;
|
||||||
invoker.setTargetObject(catalog);
|
continue;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
return result;
|
||||||
catch (Exception e) {
|
|
||||||
throw new IllegalStateException("Cannot extract catalog", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (fallback != null) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Results: " + results);
|
||||||
|
}
|
||||||
return arg.length > 0 ? null : 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() {
|
public Map<String, Object> deployed() {
|
||||||
Map<String, Object> result = new LinkedHashMap<>();
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
for (String name : this.names.keySet()) {
|
for (String name : this.names.keySet()) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
exclusions.spring-web-reactive: org.springframework:spring-web-reactive
|
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.reator-netty: io.projectreactor.ipc:reactor-netty
|
||||||
exclusions.spring-cloud-stream: org.springframework.cloud:spring-cloud-stream
|
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-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-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-cloud-stream-binder-kafka: org.springframework.cloud:spring-cloud-stream-binder-kafka
|
||||||
exclusions.spring-boot-starter-web: org.springframework.boot:spring-boot-starter-web
|
exclusions.spring-boot-starter-web: org.springframework.boot:spring-boot-starter-web
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ import org.junit.runners.Suite.SuiteClasses;
|
|||||||
* @author Dave Syer
|
* @author Dave Syer
|
||||||
*/
|
*/
|
||||||
@RunWith(Suite.class)
|
@RunWith(Suite.class)
|
||||||
@SuiteClasses({ FunctionExtractingFunctionCatalogIntegrationTests.class,
|
@SuiteClasses({ FunctionAppDeployerTests.class,
|
||||||
FunctionExtractingFunctionCatalogTests.class })
|
FunctionExtractingFunctionCatalogTests.class,
|
||||||
|
FunctionExtractingFunctionCatalogIntegrationTests.class })
|
||||||
@Ignore
|
@Ignore
|
||||||
public class AdhocTestSuite {
|
public class AdhocTestSuite {
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ public class FunctionAppDeployerTests {
|
|||||||
@Test
|
@Test
|
||||||
public void stream() throws Exception {
|
public void stream() throws Exception {
|
||||||
String first = deploy("maven://io.spring.sample:function-sample:1.0.0.BUILD-SNAPSHOT",
|
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");
|
"--spring.cloud.function.stream.supplier.enabled=false", "--debug=true");
|
||||||
// Deployment is blocking so it either failed or succeeded.
|
// Deployment is blocking so it either failed or succeeded.
|
||||||
assertThat(deployer.status(first).getState()).isEqualTo(DeploymentState.deployed);
|
assertThat(deployer.status(first).getState()).isEqualTo(DeploymentState.deployed);
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ public class FunctionExtractingFunctionCatalogIntegrationTests {
|
|||||||
public static void open() throws Exception {
|
public static void open() throws Exception {
|
||||||
port = SocketUtils.findAvailableTcpPort();
|
port = SocketUtils.findAvailableTcpPort();
|
||||||
// System.setProperty("debug", "true");
|
// System.setProperty("debug", "true");
|
||||||
context = new ApplicationRunner().start("--server.port=" + port,
|
context = new ApplicationRunner().start("--server.port=" + port, "--debug",
|
||||||
"--spring.cloud.stream.enabled=false");
|
"--logging.level.org.springframework.cloud.function=DEBUG");
|
||||||
deploy("sample", "maven://io.spring.sample:function-sample:1.0.0.BUILD-SNAPSHOT");
|
deploy("sample", "maven://com.example:function-sample-pojo:1.0.0.BUILD-SNAPSHOT");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void deploy(String name, String path) throws Exception {
|
private static void deploy(String name, String path) throws Exception {
|
||||||
@@ -79,23 +79,31 @@ public class FunctionExtractingFunctionCatalogIntegrationTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void words() {
|
public void words() {
|
||||||
assertThat(new TestRestTemplate()
|
assertThat(new TestRestTemplate().getForObject(
|
||||||
.getForObject("http://localhost:" + port + "/sample/words", String.class))
|
"http://localhost:" + port + "/stream/sample/words", String.class))
|
||||||
.isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
|
.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
|
@Test
|
||||||
public void uppercase() {
|
public void uppercase() {
|
||||||
assertThat(new TestRestTemplate().postForObject(
|
assertThat(new TestRestTemplate().postForObject(
|
||||||
"http://localhost:" + port + "/sample/uppercase", "{\"value\":\"foo\"}",
|
"http://localhost:" + port + "/stream/sample/uppercase",
|
||||||
String.class)).isEqualTo("[{\"value\":\"FOO\"}]");
|
"{\"value\":\"foo\"}", String.class)).isEqualTo("{\"value\":\"FOO\"}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void another() throws Exception {
|
public void another() throws Exception {
|
||||||
deploy("strings", "maven://io.spring.sample:function-sample:1.0.0.BUILD-SNAPSHOT");
|
deploy("strings", "maven://io.spring.sample:function-sample:1.0.0.BUILD-SNAPSHOT");
|
||||||
assertThat(new TestRestTemplate().getForObject(
|
assertThat(new TestRestTemplate().getForObject(
|
||||||
"http://localhost:" + port + "/strings/words", String.class))
|
"http://localhost:" + port + "/stream/strings/words", String.class))
|
||||||
.isEqualTo("[\"foo\",\"bar\"]");
|
.isEqualTo("[\"foo\",\"bar\"]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,13 +114,13 @@ public class FunctionExtractingFunctionCatalogIntegrationTests {
|
|||||||
assertThat(undeploy.contains(
|
assertThat(undeploy.contains(
|
||||||
"\"path\":\"maven://io.spring.sample:function-sample-pojo:1.0.0.BUILD-SNAPSHOT\""));
|
"\"path\":\"maven://io.spring.sample:function-sample-pojo:1.0.0.BUILD-SNAPSHOT\""));
|
||||||
ResponseEntity<String> result = new TestRestTemplate().exchange(RequestEntity
|
ResponseEntity<String> result = new TestRestTemplate().exchange(RequestEntity
|
||||||
.get(new URI("http://localhost:" + port + "/sample/words")).build(),
|
.get(new URI("http://localhost:" + port + "/stream/sample/words"))
|
||||||
String.class);
|
.build(), String.class);
|
||||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||||
deploy("sample", "maven://io.spring.sample:function-sample-pojo:1.0.0.BUILD-SNAPSHOT");
|
deploy("sample", "maven://io.spring.sample:function-sample-pojo:1.0.0.BUILD-SNAPSHOT");
|
||||||
assertThat(new TestRestTemplate().postForObject(
|
assertThat(new TestRestTemplate().postForObject(
|
||||||
"http://localhost:" + port + "/sample/uppercase", "{\"value\":\"foo\"}",
|
"http://localhost:" + port + "/stream/sample/uppercase",
|
||||||
String.class)).isEqualTo("[{\"value\":\"FOO\"}]");
|
"{\"value\":\"foo\"}", String.class)).isEqualTo("{\"value\":\"FOO\"}");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
<aws-lambda-events.version>1.2.1</aws-lambda-events.version>
|
<aws-lambda-events.version>1.2.1</aws-lambda-events.version>
|
||||||
<reactor.version>3.0.7.RELEASE</reactor.version>
|
<reactor.version>3.0.7.RELEASE</reactor.version>
|
||||||
<spring-cloud-function.version>1.0.0.BUILD-SNAPSHOT</spring-cloud-function.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>
|
<start-class>example.Config</start-class>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
@@ -36,7 +37,12 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.cloud</groupId>
|
<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>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -72,6 +78,11 @@
|
|||||||
<type>pom</type>
|
<type>pom</type>
|
||||||
<scope>import</scope>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-stream-binder-servlet</artifactId>
|
||||||
|
<version>${spring-cloud-stream-servlet.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -26,17 +26,13 @@
|
|||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.cloud</groupId>
|
|
||||||
<artifactId>spring-cloud-function-web</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.cloud</groupId>
|
<groupId>org.springframework.cloud</groupId>
|
||||||
<artifactId>spring-cloud-function-stream</artifactId>
|
<artifactId>spring-cloud-function-stream</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.cloud</groupId>
|
<groupId>org.springframework.cloud</groupId>
|
||||||
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
|
<artifactId>spring-cloud-stream-binder-servlet</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.cloud</groupId>
|
<groupId>org.springframework.cloud</groupId>
|
||||||
@@ -65,6 +61,11 @@
|
|||||||
<type>pom</type>
|
<type>pom</type>
|
||||||
<scope>import</scope>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-stream-binder-servlet</artifactId>
|
||||||
|
<version>${spring-cloud-stream-servlet.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,6 @@
|
|||||||
|
|
||||||
package com.example;
|
package com.example;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
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 org.springframework.test.context.junit4.SpringRunner;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Mark Fisher
|
* @author Mark Fisher
|
||||||
*/
|
*/
|
||||||
@RunWith(SpringRunner.class)
|
@RunWith(SpringRunner.class)
|
||||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
|
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = {
|
||||||
properties = {
|
"spring.cloud.function.compile.test.lambda=com.example.SampleCompiledConsumerTests.Reference::set",
|
||||||
"spring.cloud.function.compile.test.lambda=com.example.SampleCompiledConsumerTests.Reference::set",
|
"spring.cloud.function.compile.test.inputType=String",
|
||||||
"spring.cloud.function.compile.test.inputType=String",
|
"spring.cloud.function.compile.test.type=consumer" })
|
||||||
"spring.cloud.function.compile.test.type=consumer"
|
|
||||||
})
|
|
||||||
public class SampleCompiledConsumerTests {
|
public class SampleCompiledConsumerTests {
|
||||||
|
|
||||||
@LocalServerPort
|
@LocalServerPort
|
||||||
@@ -47,8 +44,8 @@ public class SampleCompiledConsumerTests {
|
|||||||
@Test
|
@Test
|
||||||
public void print() {
|
public void print() {
|
||||||
assertThat(new TestRestTemplate().postForObject(
|
assertThat(new TestRestTemplate().postForObject(
|
||||||
"http://localhost:" + port + "/test", "it works",
|
"http://localhost:" + port + "/stream/test", "it works", String.class))
|
||||||
String.class)).isEqualTo("it works");
|
.isEqualTo("it works");
|
||||||
assertEquals("it works", Reference.instance);
|
assertEquals("it works", Reference.instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public class SampleCompiledFunctionTests {
|
|||||||
@Test
|
@Test
|
||||||
public void lowercase() {
|
public void lowercase() {
|
||||||
assertThat(new TestRestTemplate().postForObject(
|
assertThat(new TestRestTemplate().postForObject(
|
||||||
"http://localhost:" + port + "/test", "it works", String.class))
|
"http://localhost:" + port + "/stream/test", "it works", String.class))
|
||||||
.isEqualTo("it works!!!");
|
.isEqualTo("it works!!!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,16 +22,18 @@
|
|||||||
<java.version>1.8</java.version>
|
<java.version>1.8</java.version>
|
||||||
<reactor.version>3.1.1.RELEASE</reactor.version>
|
<reactor.version>3.1.1.RELEASE</reactor.version>
|
||||||
<spring-cloud-function.version>1.0.0.BUILD-SNAPSHOT</spring-cloud-function.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>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.cloud</groupId>
|
<groupId>org.springframework.cloud</groupId>
|
||||||
<artifactId>spring-cloud-function-web</artifactId>
|
<artifactId>spring-cloud-function-stream</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.cloud</groupId>
|
<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>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,11 @@
|
|||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.cloud</groupId>
|
<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>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class SampleApplicationTests {
|
|||||||
@Test
|
@Test
|
||||||
public void words() {
|
public void words() {
|
||||||
assertThat(new TestRestTemplate()
|
assertThat(new TestRestTemplate()
|
||||||
.getForObject("http://localhost:" + port + "/words", String.class))
|
.getForObject("http://localhost:" + port + "/stream/words", String.class))
|
||||||
.isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
|
.isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,14 +48,14 @@ public class SampleApplicationTests {
|
|||||||
public void uppercase() {
|
public void uppercase() {
|
||||||
// TODO: make this work with a JSON stream as well (like in WebFlux)
|
// TODO: make this work with a JSON stream as well (like in WebFlux)
|
||||||
assertThat(new TestRestTemplate().postForObject(
|
assertThat(new TestRestTemplate().postForObject(
|
||||||
"http://localhost:" + port + "/uppercase", "[{\"value\":\"foo\"}]",
|
"http://localhost:" + port + "/stream/uppercase", "[{\"value\":\"foo\"}]",
|
||||||
String.class)).isEqualTo("[{\"value\":\"FOO\"}]");
|
String.class)).isEqualTo("[{\"value\":\"FOO\"}]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void lowercase() {
|
public void lowercase() {
|
||||||
assertThat(new TestRestTemplate().postForObject(
|
assertThat(new TestRestTemplate().postForObject(
|
||||||
"http://localhost:" + port + "/lowercase", "[{\"value\":\"Foo\"}]",
|
"http://localhost:" + port + "/stream/lowercase", "[{\"value\":\"Foo\"}]",
|
||||||
String.class)).isEqualTo("[{\"value\":\"foo\"}]");
|
String.class)).isEqualTo("[{\"value\":\"foo\"}]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,11 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.cloud</groupId>
|
<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>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.cloud</groupId>
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
|||||||
@@ -27,11 +27,11 @@
|
|||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.cloud</groupId>
|
<groupId>org.springframework.cloud</groupId>
|
||||||
<artifactId>spring-cloud-function-web</artifactId>
|
<artifactId>spring-cloud-function-stream</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.cloud</groupId>
|
<groupId>org.springframework.cloud</groupId>
|
||||||
<artifactId>spring-cloud-function-context</artifactId>
|
<artifactId>spring-cloud-stream-binder-servlet</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.cloud</groupId>
|
<groupId>org.springframework.cloud</groupId>
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<artifactId>spring-cloud-function-stream</artifactId>
|
<artifactId>spring-cloud-function-stream</artifactId>
|
||||||
@@ -13,6 +14,10 @@
|
|||||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<servlet-binder.version>1.0.0.BUILD-SNAPSHOT</servlet-binder.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.projectreactor</groupId>
|
<groupId>io.projectreactor</groupId>
|
||||||
@@ -41,7 +46,8 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.cloud</groupId>
|
<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>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -14,14 +14,14 @@
|
|||||||
* limitations under the License.
|
* 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.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
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.function.core.FunctionCatalog;
|
||||||
import org.springframework.cloud.stream.annotation.EnableBinding;
|
import org.springframework.cloud.stream.annotation.EnableBinding;
|
||||||
import org.springframework.cloud.stream.binder.Binder;
|
import org.springframework.cloud.stream.binder.Binder;
|
||||||
@@ -39,7 +39,7 @@ import org.springframework.context.annotation.Lazy;
|
|||||||
@ConditionalOnBean(FunctionCatalog.class)
|
@ConditionalOnBean(FunctionCatalog.class)
|
||||||
@ConditionalOnProperty(name = "spring.cloud.stream.enabled", havingValue = "true", matchIfMissing = true)
|
@ConditionalOnProperty(name = "spring.cloud.stream.enabled", havingValue = "true", matchIfMissing = true)
|
||||||
@EnableBinding(Processor.class)
|
@EnableBinding(Processor.class)
|
||||||
public class StreamConfiguration {
|
public class StreamAutoConfiguration {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private StreamConfigurationProperties properties;
|
private StreamConfigurationProperties properties;
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.cloud.function.stream;
|
package org.springframework.cloud.function.stream.config;
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.cloud.function.stream;
|
package org.springframework.cloud.function.stream.config;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -26,7 +26,7 @@ import java.util.function.Consumer;
|
|||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
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.function.core.FunctionCatalog;
|
||||||
import org.springframework.cloud.stream.annotation.Input;
|
import org.springframework.cloud.stream.annotation.Input;
|
||||||
import org.springframework.cloud.stream.annotation.Output;
|
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) {
|
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();
|
: MessageBuilder.withPayload(result).copyHeadersIfAbsent(headers).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
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
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import org.junit.runner.RunWith;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
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.messaging.Processor;
|
||||||
import org.springframework.cloud.stream.test.binder.MessageCollector;
|
import org.springframework.cloud.stream.test.binder.MessageCollector;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import org.junit.runner.RunWith;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
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.messaging.Processor;
|
||||||
import org.springframework.cloud.stream.test.binder.MessageCollector;
|
import org.springframework.cloud.stream.test.binder.MessageCollector;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.cloud.function.scan;
|
package org.springframework.cloud.function.stream.scan;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.function.Function;
|
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;
|
||||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
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.context.annotation.ComponentScan;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@@ -64,14 +65,14 @@ public class ComponentTests {
|
|||||||
public void greeter() throws Exception {
|
public void greeter() throws Exception {
|
||||||
ResponseEntity<String> result = rest
|
ResponseEntity<String> result = rest
|
||||||
.exchange(
|
.exchange(
|
||||||
RequestEntity.post(new URI("/greeter"))
|
RequestEntity.post(new URI("/stream/greeter"))
|
||||||
.contentType(MediaType.TEXT_PLAIN).body("World"),
|
.contentType(MediaType.TEXT_PLAIN).body("World"),
|
||||||
String.class);
|
String.class);
|
||||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
assertThat(result.getBody()).isEqualTo("Hello World");
|
assertThat(result.getBody()).isEqualTo("Hello World");
|
||||||
}
|
}
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication(exclude=TestSupportBinderAutoConfiguration.class)
|
||||||
@ComponentScan
|
@ComponentScan
|
||||||
protected static class TestConfiguration {
|
protected static class TestConfiguration {
|
||||||
}
|
}
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ import org.junit.runner.RunWith;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
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.messaging.Source;
|
||||||
import org.springframework.cloud.stream.test.binder.MessageCollector;
|
import org.springframework.cloud.stream.test.binder.MessageCollector;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.cloud.function.web;
|
package org.springframework.cloud.function.stream.web;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.function.Supplier;
|
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;
|
||||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
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.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.RequestEntity;
|
import org.springframework.http.RequestEntity;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@@ -43,7 +45,7 @@ import reactor.core.publisher.Flux;
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@RunWith(SpringRunner.class)
|
@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 {
|
public class PrefixTests {
|
||||||
|
|
||||||
@LocalServerPort
|
@LocalServerPort
|
||||||
@@ -66,8 +68,8 @@ public class PrefixTests {
|
|||||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
@EnableAutoConfiguration
|
@EnableAutoConfiguration(exclude=TestSupportBinderAutoConfiguration.class)
|
||||||
@org.springframework.boot.test.context.TestConfiguration
|
@Configuration
|
||||||
protected static class TestConfiguration {
|
protected static class TestConfiguration {
|
||||||
@Bean({ "words", "get/more" })
|
@Bean({ "words", "get/more" })
|
||||||
public Supplier<Flux<String>> words() {
|
public Supplier<Flux<String>> words() {
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.springframework.cloud.function.web;
|
package org.springframework.cloud.function.stream.web;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.time.Duration;
|
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.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
|
||||||
import org.springframework.boot.context.embedded.LocalServerPort;
|
import org.springframework.boot.context.embedded.LocalServerPort;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
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.http.ResponseEntity;
|
||||||
import org.springframework.messaging.Message;
|
import org.springframework.messaging.Message;
|
||||||
import org.springframework.messaging.support.MessageBuilder;
|
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.test.context.junit4.SpringRunner;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
@@ -59,7 +60,10 @@ import reactor.core.publisher.Flux;
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@RunWith(SpringRunner.class)
|
@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 {
|
public class RestApplicationTests {
|
||||||
|
|
||||||
private static final MediaType EVENT_STREAM = MediaType.TEXT_EVENT_STREAM;
|
private static final MediaType EVENT_STREAM = MediaType.TEXT_EVENT_STREAM;
|
||||||
@@ -76,21 +80,17 @@ public class RestApplicationTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void staticResource() throws Exception {
|
@Ignore("Needs Spring 5?")
|
||||||
assertThat(rest.getForObject("/test.html", String.class)).contains("<body>Test");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void wordsSSE() throws Exception {
|
public void wordsSSE() throws Exception {
|
||||||
assertThat(rest.exchange(
|
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"));
|
String.class).getBody()).isEqualTo(sse("foo", "bar"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void wordsJson() throws Exception {
|
public void wordsJson() throws Exception {
|
||||||
assertThat(rest
|
assertThat(rest
|
||||||
.exchange(RequestEntity.get(new URI("/words"))
|
.exchange(RequestEntity.get(new URI("/stream/words"))
|
||||||
.accept(MediaType.APPLICATION_JSON).build(), String.class)
|
.accept(MediaType.APPLICATION_JSON).build(), String.class)
|
||||||
.getBody()).isEqualTo("[\"foo\",\"bar\"]");
|
.getBody()).isEqualTo("[\"foo\",\"bar\"]");
|
||||||
}
|
}
|
||||||
@@ -99,31 +99,31 @@ public class RestApplicationTests {
|
|||||||
@Ignore("Fix error handling")
|
@Ignore("Fix error handling")
|
||||||
public void errorJson() throws Exception {
|
public void errorJson() throws Exception {
|
||||||
assertThat(rest
|
assertThat(rest
|
||||||
.exchange(RequestEntity.get(new URI("/bang"))
|
.exchange(RequestEntity.get(new URI("/stream/bang"))
|
||||||
.accept(MediaType.APPLICATION_JSON).build(), String.class)
|
.accept(MediaType.APPLICATION_JSON).build(), String.class)
|
||||||
.getBody()).isEqualTo("[\"foo\"]");
|
.getBody()).isEqualTo("[\"foo\"]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void words() throws Exception {
|
public void words() throws Exception {
|
||||||
ResponseEntity<String> result = rest
|
ResponseEntity<String> result = rest.exchange(
|
||||||
.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.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
|
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void word() throws Exception {
|
public void word() throws Exception {
|
||||||
ResponseEntity<String> result = rest
|
ResponseEntity<String> result = rest.exchange(
|
||||||
.exchange(RequestEntity.get(new URI("/word")).build(), String.class);
|
RequestEntity.get(new URI("/stream/word")).build(), String.class);
|
||||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
assertThat(result.getBody()).isEqualTo("foo");
|
assertThat(result.getBody()).isEqualTo("[\"foo\"]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void foos() throws Exception {
|
public void foos() throws Exception {
|
||||||
ResponseEntity<String> result = rest
|
ResponseEntity<String> result = rest.exchange(
|
||||||
.exchange(RequestEntity.get(new URI("/foos")).build(), String.class);
|
RequestEntity.get(new URI("/stream/foos")).build(), String.class);
|
||||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
assertThat(result.getBody())
|
assertThat(result.getBody())
|
||||||
.isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
|
.isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
|
||||||
@@ -131,9 +131,9 @@ public class RestApplicationTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void qualifierFoos() throws Exception {
|
public void qualifierFoos() throws Exception {
|
||||||
ResponseEntity<String> result = rest.exchange(RequestEntity.post(new URI("/foos"))
|
ResponseEntity<String> result = rest.exchange(RequestEntity
|
||||||
.contentType(MediaType.APPLICATION_JSON).body("[\"foo\",\"bar\"]"),
|
.post(new URI("/stream/foos")).contentType(MediaType.APPLICATION_JSON)
|
||||||
String.class);
|
.body("[\"foo\",\"bar\"]"), String.class);
|
||||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
assertThat(result.getBody())
|
assertThat(result.getBody())
|
||||||
.isEqualTo("[{\"value\":\"[FOO]\"},{\"value\":\"[BAR]\"}]");
|
.isEqualTo("[{\"value\":\"[FOO]\"},{\"value\":\"[BAR]\"}]");
|
||||||
@@ -141,25 +141,26 @@ public class RestApplicationTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getMore() throws Exception {
|
public void getMore() throws Exception {
|
||||||
ResponseEntity<String> result = rest
|
ResponseEntity<String> result = rest.exchange(
|
||||||
.exchange(RequestEntity.get(new URI("/get/more")).build(), String.class);
|
RequestEntity.get(new URI("/stream/get/more")).build(), String.class);
|
||||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
|
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void bareWords() throws Exception {
|
public void bareWords() throws Exception {
|
||||||
ResponseEntity<String> result = rest
|
ResponseEntity<String> result = rest.exchange(
|
||||||
.exchange(RequestEntity.get(new URI("/bareWords")).build(), String.class);
|
RequestEntity.get(new URI("/stream/bareWords")).build(), String.class);
|
||||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
|
assertThat(result.getBody()).isEqualTo("[[\"foo\",\"bar\"]]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore("Should this even work? Or do we need to be explicit about the JSON?")
|
@Ignore("Should this even work? Or do we need to be explicit about the JSON?")
|
||||||
public void updates() throws Exception {
|
public void updates() throws Exception {
|
||||||
ResponseEntity<String> result = rest.exchange(
|
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(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
|
||||||
assertThat(test.list).hasSize(2);
|
assertThat(test.list).hasSize(2);
|
||||||
assertThat(result.getBody()).isEqualTo("onetwo");
|
assertThat(result.getBody()).isEqualTo("onetwo");
|
||||||
@@ -168,7 +169,7 @@ public class RestApplicationTests {
|
|||||||
@Test
|
@Test
|
||||||
public void updatesJson() throws Exception {
|
public void updatesJson() throws Exception {
|
||||||
ResponseEntity<String> result = rest.exchange(RequestEntity
|
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);
|
.body("[\"one\",\"two\"]"), String.class);
|
||||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
|
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
|
||||||
assertThat(test.list).hasSize(2);
|
assertThat(test.list).hasSize(2);
|
||||||
@@ -178,19 +179,20 @@ public class RestApplicationTests {
|
|||||||
@Test
|
@Test
|
||||||
public void addFoos() throws Exception {
|
public void addFoos() throws Exception {
|
||||||
ResponseEntity<String> result = rest.exchange(RequestEntity
|
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);
|
.body("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"), String.class);
|
||||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
|
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
|
||||||
assertThat(test.list).hasSize(2);
|
assertThat(test.list).hasSize(2);
|
||||||
assertThat(result.getBody())
|
assertThat(result.getBody())
|
||||||
.isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
|
.isEqualTo("[{\"value\":\"foo\"}, {\"value\":\"bar\"}]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void bareUpdates() throws Exception {
|
public void bareUpdates() throws Exception {
|
||||||
ResponseEntity<String> result = rest.exchange(RequestEntity
|
ResponseEntity<String> result = rest.exchange(RequestEntity
|
||||||
.post(new URI("/bareUpdates")).contentType(MediaType.APPLICATION_JSON)
|
.post(new URI("/stream/bareUpdates"))
|
||||||
.body("[\"one\",\"two\"]"), String.class);
|
.contentType(MediaType.APPLICATION_JSON).body("[\"one\",\"two\"]"),
|
||||||
|
String.class);
|
||||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
|
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
|
||||||
assertThat(test.list).hasSize(2);
|
assertThat(test.list).hasSize(2);
|
||||||
assertThat(result.getBody()).isEqualTo("[\"one\",\"two\"]");
|
assertThat(result.getBody()).isEqualTo("[\"one\",\"two\"]");
|
||||||
@@ -199,7 +201,7 @@ public class RestApplicationTests {
|
|||||||
@Test
|
@Test
|
||||||
public void timeoutJson() throws Exception {
|
public void timeoutJson() throws Exception {
|
||||||
assertThat(rest
|
assertThat(rest
|
||||||
.exchange(RequestEntity.get(new URI("/timeout"))
|
.exchange(RequestEntity.get(new URI("/stream/timeout"))
|
||||||
.accept(MediaType.APPLICATION_JSON).build(), String.class)
|
.accept(MediaType.APPLICATION_JSON).build(), String.class)
|
||||||
.getBody()).isEqualTo("[\"foo\"]");
|
.getBody()).isEqualTo("[\"foo\"]");
|
||||||
}
|
}
|
||||||
@@ -207,23 +209,22 @@ public class RestApplicationTests {
|
|||||||
@Test
|
@Test
|
||||||
public void emptyJson() throws Exception {
|
public void emptyJson() throws Exception {
|
||||||
assertThat(rest
|
assertThat(rest
|
||||||
.exchange(RequestEntity.get(new URI("/empty"))
|
.exchange(RequestEntity.get(new URI("/stream/empty"))
|
||||||
.accept(MediaType.APPLICATION_JSON).build(), String.class)
|
.accept(MediaType.APPLICATION_JSON).build(), String.class)
|
||||||
.getBody()).isEqualTo("[]");
|
.getBody()).isEqualTo("[]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void sentences() throws Exception {
|
public void sentences() throws Exception {
|
||||||
assertThat(rest
|
assertThat(rest.exchange(RequestEntity.get(new URI("/stream/sentences")).build(),
|
||||||
.exchange(RequestEntity.get(new URI("/sentences")).build(), String.class)
|
String.class).getBody())
|
||||||
.getBody()).isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]");
|
.isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void sentencesAcceptAny() throws Exception {
|
public void sentencesAcceptAny() throws Exception {
|
||||||
assertThat(rest.exchange(
|
assertThat(rest.exchange(RequestEntity.get(new URI("/stream/sentences"))
|
||||||
RequestEntity.get(new URI("/sentences")).accept(MediaType.ALL).build(),
|
.accept(MediaType.ALL).build(), String.class).getBody())
|
||||||
String.class).getBody())
|
|
||||||
.isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]");
|
.isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +232,7 @@ public class RestApplicationTests {
|
|||||||
public void sentencesAcceptJson() throws Exception {
|
public void sentencesAcceptJson() throws Exception {
|
||||||
ResponseEntity<String> result = rest
|
ResponseEntity<String> result = rest
|
||||||
.exchange(
|
.exchange(
|
||||||
RequestEntity.get(new URI("/sentences"))
|
RequestEntity.get(new URI("/stream/sentences"))
|
||||||
.accept(MediaType.APPLICATION_JSON).build(),
|
.accept(MediaType.APPLICATION_JSON).build(),
|
||||||
String.class);
|
String.class);
|
||||||
assertThat(result.getBody()).isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]");
|
assertThat(result.getBody()).isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]");
|
||||||
@@ -240,9 +241,10 @@ public class RestApplicationTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Ignore("Maybe not supported")
|
||||||
public void sentencesAcceptSse() throws Exception {
|
public void sentencesAcceptSse() throws Exception {
|
||||||
ResponseEntity<String> result = rest.exchange(
|
ResponseEntity<String> result = rest.exchange(RequestEntity
|
||||||
RequestEntity.get(new URI("/sentences")).accept(EVENT_STREAM).build(),
|
.get(new URI("/stream/sentences")).accept(EVENT_STREAM).build(),
|
||||||
String.class);
|
String.class);
|
||||||
assertThat(result.getBody())
|
assertThat(result.getBody())
|
||||||
.isEqualTo(sse("[\"go\",\"home\"]", "[\"come\",\"back\"]"));
|
.isEqualTo(sse("[\"go\",\"home\"]", "[\"come\",\"back\"]"));
|
||||||
@@ -253,15 +255,16 @@ public class RestApplicationTests {
|
|||||||
@Test
|
@Test
|
||||||
public void uppercase() throws Exception {
|
public void uppercase() throws Exception {
|
||||||
ResponseEntity<String> result = rest.exchange(RequestEntity
|
ResponseEntity<String> result = rest.exchange(RequestEntity
|
||||||
.post(new URI("/uppercase")).contentType(MediaType.APPLICATION_JSON)
|
.post(new URI("/stream/uppercase"))
|
||||||
.body("[\"foo\",\"bar\"]"), String.class);
|
.contentType(MediaType.APPLICATION_JSON).body("[\"foo\",\"bar\"]"),
|
||||||
|
String.class);
|
||||||
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
|
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void messages() throws Exception {
|
public void messages() throws Exception {
|
||||||
ResponseEntity<String> result = rest.exchange(RequestEntity
|
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);
|
.header("x-foo", "bar").body("[\"foo\",\"bar\"]"), String.class);
|
||||||
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
|
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
|
||||||
assertThat(result.getHeaders().getFirst("x-foo")).isEqualTo("bar");
|
assertThat(result.getHeaders().getFirst("x-foo")).isEqualTo("bar");
|
||||||
@@ -271,7 +274,7 @@ public class RestApplicationTests {
|
|||||||
@Test
|
@Test
|
||||||
public void headers() throws Exception {
|
public void headers() throws Exception {
|
||||||
ResponseEntity<String> result = rest.exchange(RequestEntity
|
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);
|
.body("[\"foo\",\"bar\"]"), String.class);
|
||||||
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
|
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
|
||||||
assertThat(result.getHeaders().getFirst("foo")).isEqualTo("bar");
|
assertThat(result.getHeaders().getFirst("foo")).isEqualTo("bar");
|
||||||
@@ -282,7 +285,7 @@ public class RestApplicationTests {
|
|||||||
public void uppercaseSingleValue() throws Exception {
|
public void uppercaseSingleValue() throws Exception {
|
||||||
ResponseEntity<String> result = rest
|
ResponseEntity<String> result = rest
|
||||||
.exchange(
|
.exchange(
|
||||||
RequestEntity.post(new URI("/uppercase"))
|
RequestEntity.post(new URI("/stream/uppercase"))
|
||||||
.contentType(MediaType.TEXT_PLAIN).body("foo"),
|
.contentType(MediaType.TEXT_PLAIN).body("foo"),
|
||||||
String.class);
|
String.class);
|
||||||
assertThat(result.getBody()).isEqualTo("(FOO)");
|
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")
|
@Ignore("WebFlux would split the request body into lines: TODO make this work the same")
|
||||||
public void uppercasePlainText() throws Exception {
|
public void uppercasePlainText() throws Exception {
|
||||||
ResponseEntity<String> result = rest.exchange(
|
ResponseEntity<String> result = rest.exchange(
|
||||||
RequestEntity.post(new URI("/uppercase"))
|
RequestEntity.post(new URI("/stream/uppercase"))
|
||||||
.contentType(MediaType.TEXT_PLAIN).body("foo\nbar"),
|
.contentType(MediaType.TEXT_PLAIN).body("foo\nbar"),
|
||||||
String.class);
|
String.class);
|
||||||
assertThat(result.getBody()).isEqualTo("(FOO)(BAR)");
|
assertThat(result.getBody()).isEqualTo("(FOO)(BAR)");
|
||||||
@@ -301,7 +304,7 @@ public class RestApplicationTests {
|
|||||||
@Test
|
@Test
|
||||||
public void uppercaseFoos() throws Exception {
|
public void uppercaseFoos() throws Exception {
|
||||||
ResponseEntity<String> result = rest.exchange(RequestEntity
|
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);
|
.body("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"), String.class);
|
||||||
assertThat(result.getBody())
|
assertThat(result.getBody())
|
||||||
.isEqualTo("[{\"value\":\"FOO\"},{\"value\":\"BAR\"}]");
|
.isEqualTo("[{\"value\":\"FOO\"},{\"value\":\"BAR\"}]");
|
||||||
@@ -311,16 +314,19 @@ public class RestApplicationTests {
|
|||||||
public void uppercaseFoo() throws Exception {
|
public void uppercaseFoo() throws Exception {
|
||||||
// Single Foo can be parsed
|
// Single Foo can be parsed
|
||||||
ResponseEntity<String> result = rest.exchange(RequestEntity
|
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);
|
.body("{\"value\":\"foo\"}"), String.class);
|
||||||
assertThat(result.getBody()).isEqualTo("[{\"value\":\"FOO\"}]");
|
assertThat(result.getBody()).isEqualTo("{\"value\":\"FOO\"}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void bareUppercaseFoos() throws Exception {
|
public void bareUppercaseFoos() throws Exception {
|
||||||
ResponseEntity<String> result = rest.exchange(RequestEntity
|
ResponseEntity<String> result = rest
|
||||||
.post(new URI("/bareUpFoos")).contentType(MediaType.APPLICATION_JSON)
|
.exchange(
|
||||||
.body("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"), String.class);
|
RequestEntity.post(new URI("/stream/bareUpFoos"))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"),
|
||||||
|
String.class);
|
||||||
assertThat(result.getBody())
|
assertThat(result.getBody())
|
||||||
.isEqualTo("[{\"value\":\"FOO\"},{\"value\":\"BAR\"}]");
|
.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
|
// Single Foo can be parsed and returns a single value if the function is defined
|
||||||
// that way
|
// that way
|
||||||
ResponseEntity<String> result = rest.exchange(RequestEntity
|
ResponseEntity<String> result = rest.exchange(RequestEntity
|
||||||
.post(new URI("/bareUpFoos")).contentType(MediaType.APPLICATION_JSON)
|
.post(new URI("/stream/bareUpFoos"))
|
||||||
.body("{\"value\":\"foo\"}"), String.class);
|
.contentType(MediaType.APPLICATION_JSON).body("{\"value\":\"foo\"}"),
|
||||||
|
String.class);
|
||||||
assertThat(result.getBody()).isEqualTo("{\"value\":\"FOO\"}");
|
assertThat(result.getBody()).isEqualTo("{\"value\":\"FOO\"}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void bareUppercase() throws Exception {
|
public void bareUppercase() throws Exception {
|
||||||
ResponseEntity<String> result = rest.exchange(RequestEntity
|
ResponseEntity<String> result = rest.exchange(RequestEntity
|
||||||
.post(new URI("/bareUppercase")).contentType(MediaType.APPLICATION_JSON)
|
.post(new URI("/stream/bareUppercase"))
|
||||||
.body("[\"foo\",\"bar\"]"), String.class);
|
.contentType(MediaType.APPLICATION_JSON).body("[\"foo\",\"bar\"]"),
|
||||||
|
String.class);
|
||||||
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
|
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void transform() throws Exception {
|
public void transform() throws Exception {
|
||||||
ResponseEntity<String> result = rest.exchange(RequestEntity
|
ResponseEntity<String> result = rest.exchange(RequestEntity
|
||||||
.post(new URI("/transform")).contentType(MediaType.APPLICATION_JSON)
|
.post(new URI("/stream/transform"))
|
||||||
.body("[\"foo\",\"bar\"]"), String.class);
|
.contentType(MediaType.APPLICATION_JSON).body("[\"foo\",\"bar\"]"),
|
||||||
|
String.class);
|
||||||
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
|
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void postMore() throws Exception {
|
public void postMore() throws Exception {
|
||||||
ResponseEntity<String> result = rest.exchange(RequestEntity
|
ResponseEntity<String> result = rest.exchange(RequestEntity
|
||||||
.post(new URI("/post/more")).contentType(MediaType.APPLICATION_JSON)
|
.post(new URI("/stream/post/more"))
|
||||||
.body("[\"foo\",\"bar\"]"), String.class);
|
.contentType(MediaType.APPLICATION_JSON).body("[\"foo\",\"bar\"]"),
|
||||||
|
String.class);
|
||||||
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
|
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void postMoreFoo() {
|
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
|
@Test
|
||||||
public void uppercaseGet() {
|
public void uppercaseGet() {
|
||||||
assertThat(rest.getForObject("/uppercase/foo", String.class)).isEqualTo("(FOO)");
|
assertThat(rest.getForObject("/stream/uppercase/foo", String.class))
|
||||||
|
.isEqualTo("(FOO)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void convertGet() {
|
public void convertGet() {
|
||||||
assertThat(rest.getForObject("/wrap/123", String.class)).isEqualTo("..123..");
|
assertThat(rest.getForObject("/stream/wrap/123", String.class))
|
||||||
|
.isEqualTo("..123..");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void convertPost() throws Exception {
|
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);
|
.contentType(MediaType.TEXT_PLAIN).body("123"), String.class);
|
||||||
assertThat(result.getBody()).isEqualTo("..123..");
|
assertThat(result.getBody()).isEqualTo("..123..");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void convertPostJson() throws Exception {
|
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
|
ResponseEntity<String> result = rest
|
||||||
.exchange(
|
.exchange(
|
||||||
RequestEntity.post(new URI("/doubler"))
|
RequestEntity.post(new URI("/stream/doubler"))
|
||||||
.contentType(MediaType.TEXT_PLAIN).body("123"),
|
.contentType(MediaType.TEXT_PLAIN).body("123"),
|
||||||
String.class);
|
String.class);
|
||||||
assertThat(result.getBody()).isEqualTo("[246]");
|
assertThat(result.getBody()).isEqualTo("246");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void supplierFirst() {
|
public void supplierFirst() {
|
||||||
assertThat(rest.getForObject("/not/a/function", String.class))
|
assertThat(rest.getForObject("/stream/not/a/function", String.class))
|
||||||
.isEqualTo("[\"hello\"]");
|
.isEqualTo("[\"hello\"]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void convertGetJson() throws Exception {
|
public void convertGetJson() throws Exception {
|
||||||
assertThat(rest
|
assertThat(rest
|
||||||
.exchange(RequestEntity.get(new URI("/entity/321"))
|
.exchange(RequestEntity.get(new URI("/stream/entity/321"))
|
||||||
.accept(MediaType.APPLICATION_JSON).build(), String.class)
|
.accept(MediaType.APPLICATION_JSON).build(), String.class)
|
||||||
.getBody()).isEqualTo("{\"value\":321}");
|
.getBody()).isEqualTo("{\"value\":321}");
|
||||||
}
|
}
|
||||||
@@ -410,7 +421,7 @@ public class RestApplicationTests {
|
|||||||
@Test
|
@Test
|
||||||
public void uppercaseJsonArray() throws Exception {
|
public void uppercaseJsonArray() throws Exception {
|
||||||
assertThat(rest.exchange(
|
assertThat(rest.exchange(
|
||||||
RequestEntity.post(new URI("/maps"))
|
RequestEntity.post(new URI("/stream/maps"))
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
// The new line in the middle is optional
|
// The new line in the middle is optional
|
||||||
.body("[{\"value\":\"foo\"},\n{\"value\":\"bar\"}]"),
|
.body("[{\"value\":\"foo\"},\n{\"value\":\"bar\"}]"),
|
||||||
@@ -419,8 +430,9 @@ public class RestApplicationTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Ignore("Doesn't make sense: if you post in an array you expect to get back an array")
|
||||||
public void uppercaseSSE() throws Exception {
|
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)
|
.accept(EVENT_STREAM).contentType(MediaType.APPLICATION_JSON)
|
||||||
.body("[\"foo\",\"bar\"]"), String.class).getBody())
|
.body("[\"foo\",\"bar\"]"), String.class).getBody())
|
||||||
.isEqualTo(sse("(FOO)", "(BAR)"));
|
.isEqualTo(sse("(FOO)", "(BAR)"));
|
||||||
@@ -430,7 +442,6 @@ public class RestApplicationTests {
|
|||||||
return "data:" + StringUtils.arrayToDelimitedString(values, "\n\ndata:") + "\n\n";
|
return "data:" + StringUtils.arrayToDelimitedString(values, "\n\ndata:") + "\n\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
@EnableAutoConfiguration
|
|
||||||
@TestConfiguration
|
@TestConfiguration
|
||||||
public static class ApplicationConfiguration {
|
public static class ApplicationConfiguration {
|
||||||
|
|
||||||
@@ -541,7 +552,7 @@ public class RestApplicationTests {
|
|||||||
public Supplier<Flux<String>> bang() {
|
public Supplier<Flux<String>> bang() {
|
||||||
return () -> Flux.fromArray(new String[] { "foo", "bar" }).map(value -> {
|
return () -> Flux.fromArray(new String[] { "foo", "bar" }).map(value -> {
|
||||||
if (value.equals("bar")) {
|
if (value.equals("bar")) {
|
||||||
throw new RuntimeException("Bar");
|
// throw new RuntimeException("Bar");
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
@@ -549,7 +560,7 @@ public class RestApplicationTests {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public Supplier<Flux<String>> empty() {
|
public Supplier<Flux<String>> empty() {
|
||||||
return () -> Flux.fromIterable(Collections.emptyList());
|
return () -> Flux.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean("not/a/function")
|
@Bean("not/a/function")
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.cloud.function.web;
|
package org.springframework.cloud.function.stream.web;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.function.Supplier;
|
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.BeanDefinitionRegistry;
|
||||||
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
|
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
|
||||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
|
||||||
import org.springframework.boot.context.embedded.LocalServerPort;
|
import org.springframework.boot.context.embedded.LocalServerPort;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
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.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.HttpStatus;
|
||||||
import org.springframework.http.RequestEntity;
|
import org.springframework.http.RequestEntity;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
import org.springframework.test.context.junit4.SpringRunner;
|
import org.springframework.test.context.junit4.SpringRunner;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
@@ -48,7 +50,9 @@ import reactor.core.publisher.Flux;
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@RunWith(SpringRunner.class)
|
@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 {
|
public class SingletonTests {
|
||||||
|
|
||||||
@LocalServerPort
|
@LocalServerPort
|
||||||
@@ -59,35 +63,33 @@ public class SingletonTests {
|
|||||||
@Test
|
@Test
|
||||||
public void words() throws Exception {
|
public void words() throws Exception {
|
||||||
ResponseEntity<String> result = rest.exchange(
|
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.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
|
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@EnableAutoConfiguration
|
@TestConfiguration
|
||||||
@org.springframework.boot.test.context.TestConfiguration
|
protected static class TestApplicationConfiguration
|
||||||
protected static class TestConfiguration {
|
implements PriorityOrdered, BeanDefinitionRegistryPostProcessor {
|
||||||
@Bean
|
@Override
|
||||||
public static BeanDefinitionRegistryPostProcessor processor() {
|
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
|
||||||
return new BeanDefinitionRegistryPostProcessor() {
|
throws BeansException {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void postProcessBeanFactory(
|
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
|
||||||
ConfigurableListableBeanFactory beanFactory)
|
throws BeansException {
|
||||||
throws BeansException {
|
// Simulates what happens when you add a compiled function
|
||||||
}
|
RootBeanDefinition beanDefinition = new RootBeanDefinition(MySupplier.class);
|
||||||
|
registry.registerBeanDefinition("words", beanDefinition);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void postProcessBeanDefinitionRegistry(
|
public int getOrder() {
|
||||||
BeanDefinitionRegistry registry) throws BeansException {
|
return 0;
|
||||||
// Simulates what happens when you add a compiled function
|
|
||||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(MySupplier.class);
|
|
||||||
registry.registerBeanDefinition("words", beanDefinition);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static class MySupplier implements Supplier<Flux<String>> {
|
static class MySupplier implements Supplier<Flux<String>> {
|
||||||
@Override
|
@Override
|
||||||
public Flux<String> get() {
|
public Flux<String> get() {
|
||||||
@@ -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>
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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";
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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": ""
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
|
||||||
org.springframework.cloud.function.web.flux.ReactorAutoConfiguration
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body>Test</body></html>
|
|
||||||
39
spring-cloud-stream-binder-servlet/README.md
Normal file
39
spring-cloud-stream-binder-servlet/README.md
Normal 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. |
|
||||||
126
spring-cloud-stream-binder-servlet/pom.xml
Normal file
126
spring-cloud-stream-binder-servlet/pom.xml
Normal 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>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* 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
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* 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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
package org.springframework.cloud.stream.binder.servlet;
|
||||||
|
|
||||||
package org.springframework.cloud.function.web;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Mark Fisher
|
* @author Dave Syer
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
@SpringBootApplication
|
public interface EnabledBindings {
|
||||||
public class RestApplication {
|
|
||||||
|
String getInput(String output);
|
||||||
|
|
||||||
|
Set<String> getOutputs();
|
||||||
|
|
||||||
|
Set<String> getInputs();
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
SpringApplication.run(RestApplication.class, args);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -28,9 +28,10 @@ import org.springframework.util.ObjectUtils;
|
|||||||
* @author Dave Syer
|
* @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();
|
HttpHeaders result = new HttpHeaders();
|
||||||
for (String name : headers.keySet()) {
|
for (String name : headers.keySet()) {
|
||||||
Object value = headers.get(name);
|
Object value = headers.get(name);
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
servlet:\
|
||||||
|
org.springframework.cloud.stream.binder.servlet.config.ServletServiceAutoConfiguration
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||||
|
org.springframework.cloud.stream.binder.servlet.config.MessageHandlingAutoConfiguration
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
Reference in New Issue
Block a user