Migrate to servlet binder for web features

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

View File

View File

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

View File

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

View File

@@ -0,0 +1,32 @@
/*
* 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 EnabledBindings {
String getInput(String output);
Set<String> getOutputs();
Set<String> getInputs();
}

View File

@@ -0,0 +1,85 @@
/*
* 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 java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.http.HttpHeaders;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.ObjectUtils;
/**
* @author Dave Syer
*
*/
class HeaderUtils {
public static HttpHeaders fromMessage(Map<String, Object> headers,
HttpHeaders request) {
HttpHeaders result = new HttpHeaders();
for (String name : headers.keySet()) {
Object value = headers.get(name);
name = name.toLowerCase();
if (MessageHeaders.ID.equals(name)) {
continue;
}
if (request.containsKey(name)) {
if (name.startsWith("x-")) {
if (!name.startsWith("x-forwarded")) {
Collection<?> values = multi(value);
for (Object object : values) {
result.set(name, object.toString());
}
}
}
}
else {
Collection<?> values = multi(value);
for (Object object : values) {
result.set(name, object.toString());
}
}
}
return result;
}
private static Collection<?> multi(Object value) {
if (value instanceof Collection) {
Collection<?> collection = (Collection<?>) value;
return collection;
}
else if (ObjectUtils.isArray(value)) {
Object[] values = ObjectUtils.toObjectArray(value);
return Arrays.asList(values);
}
return Arrays.asList(value);
}
public static MessageHeaders fromHttp(HttpHeaders headers) {
Map<String, Object> map = new LinkedHashMap<>();
for (String name : headers.keySet()) {
Collection<?> values = multi(headers.get(name));
name = name.toLowerCase();
Object value = values == null ? null
: (values.size() == 1 ? values.iterator().next() : values);
map.put(name, value);
}
return new MessageHeaders(map);
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet;
import java.util.ArrayList;
import java.util.List;
/**
* Internal convenience class to help with JSON message bodies. In particular translating
* between JSON arrays and lists of payloads.
*
* @author Dave Syer
*
*/
class JsonUtils {
/**
* Split a JSON array into a list of individual objects, without parsing the objects
* themselves..
*/
public static List<String> split(String body) {
body = body.trim();
// it's an array
List<String> strings = new ArrayList<>();
int index = 0;
int open = 0;
boolean inString = false;
StringBuilder builder = new StringBuilder();
while (index++ < body.length() - 1) {
char current = body.charAt(index);
builder.append(current);
if (body.charAt(index - 1) != '\\') {
if (current == '"') {
if (!inString) {
open++;
inString = true;
}
else {
open--;
inString = false;
}
}
else if (current == '[') {
open++;
}
else if (current == ']') {
open--;
}
else if (current == '{') {
open++;
}
else if (current == '}') {
open--;
}
}
if (open == 0) {
if (builder.charAt(0) == '"') {
builder.delete(0, 1);
builder.delete(builder.length() - 1, builder.length());
}
strings.add(builder.toString());
builder.setLength(0);
while (index++ < body.length() - 1 && body.charAt(index) != ',') {
}
while (index++ < body.length() - 1
&& Character.isWhitespace(body.charAt(index))) {
}
index--;
}
}
return strings;
}
}

View File

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

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet;
import java.util.Set;
/**
* @author Dave Syer
*
*/
public interface RouteRegistrar {
void registerRoutes(Set<String> routes);
void unregisterRoutes(Set<String> routes);
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet;
import java.util.Set;
/**
* @author Dave Syer
*
*/
public interface RouteRegistry {
Set<String> routes();
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet;
import org.springframework.cloud.stream.binder.AbstractBinder;
import org.springframework.cloud.stream.binder.Binding;
import org.springframework.cloud.stream.binder.ConsumerProperties;
import org.springframework.cloud.stream.binder.DefaultBinding;
import org.springframework.cloud.stream.binder.ProducerProperties;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
/**
* A {@link org.springframework.cloud.stream.binder.Binder} implementation backed by HTTP.
*
* @author Dave Syer
*/
public class ServletMessageChannelBinder
extends AbstractBinder<MessageChannel, ConsumerProperties, ProducerProperties> {
private MessageController controller;
public ServletMessageChannelBinder(MessageController controller) {
this.controller = controller;
}
@Override
protected Binding<MessageChannel> doBindConsumer(String name, String group,
MessageChannel inputTarget, ConsumerProperties properties) {
controller.bind(name, group, inputTarget);
return new DefaultBinding<MessageChannel>(name, group, inputTarget, null);
}
@Override
protected Binding<MessageChannel> doBindProducer(String name,
MessageChannel outboundBindTarget, ProducerProperties properties) {
controller.subscribe(name, (SubscribableChannel) outboundBindTarget);
return new DefaultBinding<MessageChannel>(name, null, outboundBindTarget, null);
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet.config;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.binder.servlet.EnabledBindings;
import org.springframework.cloud.stream.binding.BindingBeanDefinitionRegistryUtils;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils;
/**
* @author Dave Syer
*
*/
public class BeanFactoryEnabledBindings implements EnabledBindings {
private final ConfigurableListableBeanFactory beanFactory;
private final AtomicBoolean initialized = new AtomicBoolean(false);
private final Map<String, String> outputsToInputs = new HashMap<>();
private final Set<String> outputs = new HashSet<>();
private final Set<String> inputs = new HashSet<>();
private final BindingServiceProperties binding;
public BeanFactoryEnabledBindings(ConfigurableListableBeanFactory beanFactory,
BindingServiceProperties binding) {
this.beanFactory = beanFactory;
this.binding = binding;
}
@Override
public Set<String> getInputs() {
init();
return this.inputs;
}
@Override
public Set<String> getOutputs() {
init();
return this.outputs;
}
@Override
public String getInput(String output) {
init();
return outputsToInputs.get(output);
}
private void init() {
if (initialized.compareAndSet(false, true)) {
String[] names = beanFactory.getBeanNamesForAnnotation(EnableBinding.class);
for (String bean : names) {
Class<?> type = beanFactory.getType(bean);
MultiValueMap<String, Object> attrs = AnnotatedElementUtils
.getAllAnnotationAttributes(type, EnableBinding.class.getName());
List<Object> list = attrs.get("value");
if (list != null) {
for (Object object : list) {
Class<?>[] bindings = (Class<?>[]) object;
for (Class<?> binding : bindings) {
List<String> inputs = new ArrayList<>();
List<String> outputs = new ArrayList<>();
ReflectionUtils.doWithMethods(binding, method -> {
Input input = AnnotationUtils.findAnnotation(method,
Input.class);
Output output = AnnotationUtils.findAnnotation(method,
Output.class);
if (input != null) {
String name = BindingBeanDefinitionRegistryUtils
.getBindingTargetName(input, method);
inputs.add(BeanFactoryEnabledBindings.this.binding
.getBindingDestination(name));
}
if (output != null) {
String name = BindingBeanDefinitionRegistryUtils
.getBindingTargetName(output, method);
outputs.add(BeanFactoryEnabledBindings.this.binding
.getBindingDestination(name));
}
});
BeanFactoryEnabledBindings.this.outputs.addAll(outputs);
BeanFactoryEnabledBindings.this.inputs.addAll(inputs);
if (inputs.size() == 1 && outputs.size() == 1) {
BeanFactoryEnabledBindings.this.outputsToInputs
.put(outputs.get(0), inputs.get(0));
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2015-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet.config;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.stream.binder.servlet.EnabledBindings;
import org.springframework.cloud.stream.binder.servlet.MessageController;
import org.springframework.cloud.stream.binder.servlet.RouteRegistry;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Dave Syer
*/
@Configuration
@AutoConfigureBefore({ WebMvcAutoConfiguration.class })
@ConfigurationProperties("spring.cloud.stream.binder.servlet")
@ConditionalOnProperty(name = "spring.cloud.stream.enabled", havingValue = "true", matchIfMissing = true)
public class MessageHandlingAutoConfiguration {
/**
* The prefix for the HTTP endpoint.
*/
private String prefix = "stream";
/**
* The buffer timeout for messages sent to output channels, and accessed via GET.
*/
private long bufferTimeoutSeconds = 10;
/**
* The receive timeout for messages in a send-and-receive from a linked output via
* POST.
*/
private long receiveTimeoutMillis = 100;
@Autowired(required = false)
private List<RouteRegistry> registries;
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public long getBufferTimeoutSeconds() {
return bufferTimeoutSeconds;
}
public void setBufferTimeoutSeconds(long bufferTimeoutSeconds) {
this.bufferTimeoutSeconds = bufferTimeoutSeconds;
}
public long getReceiveTimeoutMillis() {
return receiveTimeoutMillis;
}
public void setReceiveTimeoutMillis(long receiveTimeoutMillis) {
this.receiveTimeoutMillis = receiveTimeoutMillis;
}
@Bean
public MessageController messageController(EnabledBindings bindings) {
MessageController controller = new MessageController(prefix, bindings);
controller.setBufferTimeoutSeconds(bufferTimeoutSeconds);
controller.setReceiveTimeoutSeconds(receiveTimeoutMillis);
if (registries != null) {
for (RouteRegistry registry : registries) {
controller.registerRoutes(registry.routes());
}
}
return controller;
}
@Bean
public BeanFactoryEnabledBindings enabledBindings(
ConfigurableListableBeanFactory beanFactory,
BindingServiceProperties binding) {
return new BeanFactoryEnabledBindings(beanFactory, binding);
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2015-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.cloud.stream.binder.Binder;
import org.springframework.cloud.stream.binder.servlet.MessageController;
import org.springframework.cloud.stream.binder.servlet.ServletMessageChannelBinder;
import org.springframework.cloud.stream.config.codec.kryo.KryoCodecAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.integration.codec.Codec;
/**
* @author Dave Syer
*/
@Configuration
@ConditionalOnMissingBean(Binder.class)
@AutoConfigureBefore({ WebMvcAutoConfiguration.class })
@Import({ PropertyPlaceholderAutoConfiguration.class, KryoCodecAutoConfiguration.class })
public class ServletServiceAutoConfiguration {
@Autowired
private Codec codec;
@Bean
public ServletMessageChannelBinder servletMessageChannelBinder(
MessageController controller) {
ServletMessageChannelBinder messageChannelBinder = new ServletMessageChannelBinder(
controller);
messageChannelBinder.setCodec(this.codec);
return messageChannelBinder;
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet;
import java.util.Arrays;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Dave Syer
*
*/
public class JsonUtilsTests {
private ObjectMapper mapper = new ObjectMapper();
@Test
public void empty() {
assertThat(JsonUtils.split("[]")).isEmpty();
}
@Test
public void strings() {
assertThat(JsonUtils.split("[\"foo\", \"bar\"]")).hasSize(2).contains("foo",
"bar");
}
@Test
public void objects() throws Exception {
assertThat(JsonUtils.split(
mapper.writeValueAsString(Arrays.asList(new Foo("foo"), new Foo("bar")))))
.hasSize(2)
.contains("{\"value\":\"foo\"}", "{\"value\":\"bar\"}");
}
@Test
public void arrays() throws Exception {
assertThat(JsonUtils.split(mapper.writeValueAsString(
Arrays.asList(Arrays.asList(new Foo("foo"), new Foo("bar")))))).hasSize(1)
.contains("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
}
protected static class Foo {
private String value;
public Foo() {
}
public Foo(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.http.MediaType;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class DoubleSinkMessageChannelBinderTests implements MessageHandler {
@Autowired
private Sink sink;
@Autowired
private Custom custom;
@Autowired
private MockMvc mockMvc;
private Message<?> message;
@Test
public void string() throws Exception {
sink.input().subscribe(this);
mockMvc.perform(
post("/stream/input").contentType(MediaType.TEXT_PLAIN).content("hello"))
.andExpect(status().isAccepted())
.andExpect(content().string(containsString("hello")));
assertThat(this.message).isNotNull();
sink.input().unsubscribe(this);
}
@Test
public void custom() throws Exception {
custom.input().subscribe(this);
mockMvc.perform(
post("/stream/custom").contentType(MediaType.TEXT_PLAIN).content("hello"))
.andExpect(status().isAccepted())
.andExpect(content().string(containsString("hello")));
assertThat(this.message).isNotNull();
custom.input().unsubscribe(this);
}
@SpringBootApplication
@EnableBinding({ Sink.class, Custom.class })
protected static class TestConfiguration {
}
@Override
public void handleMessage(Message<?> message) throws MessagingException {
this.message = message;
}
interface Custom {
@Input("custom")
SubscribableChannel input();
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.http.MediaType;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class HeaderProcessorMessageChannelBinderTests {
@Autowired
private MockMvc mockMvc;
@Test
public void function() throws Exception {
mockMvc.perform(post("/stream/input").contentType(MediaType.APPLICATION_JSON)
.content("\"hello\"")).andExpect(status().isOk())
.andExpect(header().string("x-foo", "bar"))
.andExpect(content().string(containsString("HELLO")));
}
@SpringBootApplication
@EnableBinding(Processor.class)
protected static class TestConfiguration {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public Message<String> uppercase(Message<String> input) {
return MessageBuilder.withPayload(input.getPayload().toUpperCase())
.copyHeadersIfAbsent(input.getHeaders()).setHeader("x-foo", "bar")
.build();
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.binder.servlet.MessageController;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.http.MediaType;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class HeadersDroppedRoutedProcessorMessageChannelBinderTests {
@Autowired
private MockMvc mockMvc;
@Test
public void function() throws Exception {
mockMvc.perform(post("/stream/input/words")
.contentType(MediaType.APPLICATION_JSON).content("\"hello\""))
.andExpect(status().isOk())
.andExpect(header().string(MessageController.ROUTE_KEY, "words"))
.andExpect(content().string(containsString("HELLO")));
}
@SpringBootApplication
@EnableBinding(Processor.class)
protected static class TestConfiguration {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public String uppercase(String input) {
return input.toUpperCase();
}
}
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet.test;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.binder.servlet.MessageController;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.http.MediaType;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest("spring.cloud.stream.bindings.input.destination:words")
@AutoConfigureMockMvc
@DirtiesContext
public class NamedProcessorMessageChannelBinderTests {
@Autowired
private MockMvc mockMvc;
@Autowired
private MessageController controller;
@Autowired
private Processor processor;
@Before
public void init() {
controller.reset();
}
@Test
public void function() throws Exception {
mockMvc.perform(post("/stream/words").contentType(MediaType.APPLICATION_JSON)
.content("\"hello\"")).andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void implicit() throws Exception {
mockMvc.perform(post("/stream").contentType(MediaType.APPLICATION_JSON)
.content("\"hello\"")).andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void empty() throws Exception {
mockMvc.perform(get("/stream/output").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string(containsString("[]")));
}
@Test
public void output() throws Exception {
processor.input().send(MessageBuilder.withPayload("hello").build());
mockMvc.perform(get("/stream/output").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void outputWithRoute() throws Exception {
processor.input().send(MessageBuilder.withPayload("hello")
.setHeader(MessageController.ROUTE_KEY, "uppercase").build());
mockMvc.perform(
get("/stream/output/uppercase").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@SpringBootApplication
@EnableBinding(Processor.class)
protected static class TestConfiguration {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public String uppercase(String input) {
return input.toUpperCase();
}
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.http.MediaType;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class NamedSinkMessageChannelBinderTests implements MessageHandler {
@Autowired
private Sink sink;
@Autowired
private MockMvc mockMvc;
private Message<?> message;
@Test
public void consumer() throws Exception {
sink.input().subscribe(this);
mockMvc.perform(post("/stream/input").contentType(MediaType.APPLICATION_JSON)
.content("\"hello\"")).andExpect(status().isAccepted())
.andExpect(content().string(containsString("hello")));
assertThat(this.message).isNotNull();
sink.input().unsubscribe(this);
}
@SpringBootApplication
@EnableBinding(Sink.class)
protected static class TestConfiguration {
}
@Override
public void handleMessage(Message<?> message) throws MessagingException {
this.message = message;
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest("spring.cloud.stream.bindings.output.destination:words")
@AutoConfigureMockMvc
@DirtiesContext
public class NamedSourceMessageChannelBinderTests {
@Autowired
private Source source;
@Autowired
private MockMvc mockMvc;
@Test
public void supplier() throws Exception {
source.output().send(MessageBuilder.withPayload("hello").build());
mockMvc.perform(get("/stream/words")).andExpect(status().isOk())
.andExpect(content().string(containsString("hello")));
}
@SpringBootApplication
@EnableBinding(Source.class)
protected static class TestConfiguration {
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.http.MediaType;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest(properties = "logging.level.org.springframework.web=DEBUG")
@AutoConfigureMockMvc
@DirtiesContext
public class PojoProcessorMessageChannelBinderTests {
@Autowired
private MockMvc mockMvc;
@Test
public void json() throws Exception {
mockMvc.perform(post("/stream").contentType(MediaType.APPLICATION_JSON)
.content("{\"value\":\"hello\"}")).andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void text() throws Exception {
mockMvc.perform(post("/stream").contentType(MediaType.TEXT_PLAIN)
.content("[{\"value\":\"hello\"}]")).andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void single() throws Exception {
mockMvc.perform(post("/stream").contentType(MediaType.TEXT_PLAIN)
.content("{\"value\":\"hello\"}")).andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@SpringBootApplication
@EnableBinding(Processor.class)
protected static class TestConfiguration {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public Foo uppercase(Foo input) {
return input.toUpperCase();
}
}
protected static class Foo {
private String value;
public Foo() {
}
public Foo(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public Foo toUpperCase() {
return new Foo(value.toUpperCase());
}
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.http.MediaType;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class PojoSinkMessageChannelBinderTests implements MessageHandler {
@Autowired
private Sink sink;
@Autowired
private MockMvc mockMvc;
private Message<?> message;
@Test
public void consumer() throws Exception {
sink.input().subscribe(this);
mockMvc.perform(post("/stream/input").contentType(MediaType.APPLICATION_JSON)
.content("{\"value\":\"hello\"}")).andExpect(status().isAccepted())
.andExpect(content().string("{\"value\":\"hello\"}"));
assertThat(this.message).isNotNull();
sink.input().unsubscribe(this);
}
@Test
public void multi() throws Exception {
sink.input().subscribe(this);
mockMvc.perform(post("/stream/input").contentType(MediaType.APPLICATION_JSON)
.content("[{\"value\":\"hello\"},{\"value\":\"world\"}]"))
.andExpect(status().isAccepted()).andExpect(content()
.string("[{\"value\":\"hello\"}, {\"value\":\"world\"}]"));
assertThat(this.message).isNotNull();
sink.input().unsubscribe(this);
}
@SpringBootApplication
@EnableBinding(Sink.class)
protected static class TestConfiguration {
}
@Override
public void handleMessage(Message<?> message) throws MessagingException {
this.message = message;
}
protected static class Foo {
private String value;
public Foo() {
}
public Foo(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public Foo toUpperCase() {
return new Foo(value.toUpperCase());
}
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.http.MediaType;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest("spring.cloud.stream.binder.servlet.prefix:awesome")
@AutoConfigureMockMvc
@DirtiesContext
public class PrefixMessageChannelBinderTests implements MessageHandler {
@Autowired
private Sink sink;
@Autowired
private MockMvc mockMvc;
private Message<?> message;
@Test
public void consumer() throws Exception {
sink.input().subscribe(this);
mockMvc.perform(post("/awesome/input").contentType(MediaType.APPLICATION_JSON)
.content("\"hello\"")).andExpect(status().isAccepted())
.andExpect(content().string(containsString("hello")));
assertThat(this.message).isNotNull();
sink.input().unsubscribe(this);
}
@SpringBootApplication
@EnableBinding(Sink.class)
protected static class TestConfiguration {
}
@Override
public void handleMessage(Message<?> message) throws MessagingException {
this.message = message;
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet.test;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.http.MediaType;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class ProcessorMessageChannelBinderTests {
@Autowired
private Processor processor;
@Autowired
private MockMvc mockMvc;
@Before
public void init() throws Exception {
mockMvc.perform(get("/stream/output?purge=true")).andReturn();
}
@Test
public void supplier() throws Exception {
processor.output().send(MessageBuilder.withPayload("hello").build());
mockMvc.perform(get("/stream/output")).andExpect(status().isOk())
.andExpect(content().string(containsString("hello")));
}
@Test
public void missing() throws Exception {
// Missing route is not found if channel is explicit
mockMvc.perform(get("/stream/output/missing")).andExpect(status().isNotFound())
.andExpect(content().string(equalTo("")));
}
@Test
public void empty() throws Exception {
// Missing route where channel can be inferred (there is an input channel) is
// going to be passed on as a body
mockMvc.perform(get("/stream/missing")).andExpect(status().isOk())
.andExpect(content().string(equalTo("MISSING")));
}
@Test
public void function() throws Exception {
mockMvc.perform(post("/stream/input").contentType(MediaType.APPLICATION_JSON)
.content("\"hello\"")).andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void keyed() throws Exception {
mockMvc.perform(get("/stream/hello")).andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void implicit() throws Exception {
mockMvc.perform(post("/stream").contentType(MediaType.APPLICATION_JSON)
.content("\"hello\"")).andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void string() throws Exception {
mockMvc.perform(
post("/stream/input").contentType(MediaType.TEXT_PLAIN).content("hello"))
.andExpect(status().isOk()).andExpect(content().string(equalTo("HELLO")));
}
@Test
public void multi() throws Exception {
mockMvc.perform(post("/stream/input").contentType(MediaType.APPLICATION_JSON)
.content("[\"hello\",\"world\"]")).andExpect(status().isOk())
.andExpect(content().string("[\"HELLO\",\"WORLD\"]"));
}
@SpringBootApplication
@EnableBinding(Processor.class)
protected static class TestConfiguration {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public String uppercase(String input) {
return input.toUpperCase();
}
public static void main(String[] args) throws Exception {
SpringApplication.run(
ProcessorMessageChannelBinderTests.TestConfiguration.class,
"--logging.level.root=INFO");
}
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.binder.servlet.MessageController;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.http.MediaType;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class RoutedProcessorMessageChannelBinderTests {
@Autowired
private MockMvc mockMvc;
@Test
public void function() throws Exception {
mockMvc.perform(post("/stream/input/words")
.contentType(MediaType.APPLICATION_JSON).content("\"hello\""))
.andExpect(status().isOk())
.andExpect(header().string(MessageController.ROUTE_KEY, "words"))
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void keyed() throws Exception {
mockMvc.perform(get("/stream/words/hello")).andExpect(status().isOk())
.andExpect(header().string(MessageController.ROUTE_KEY, "words"))
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void channelAndKeyed() throws Exception {
mockMvc.perform(get("/stream/input/words/hello")).andExpect(status().isOk())
.andExpect(header().string(MessageController.ROUTE_KEY, "words"))
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void implicit() throws Exception {
mockMvc.perform(post("/stream/words").contentType(MediaType.APPLICATION_JSON)
.content("\"hello\"")).andExpect(status().isOk())
.andExpect(header().string(MessageController.ROUTE_KEY, "words"))
.andExpect(content().string(containsString("HELLO")));
}
@SpringBootApplication
@EnableBinding(Processor.class)
protected static class TestConfiguration {
@StreamListener(Processor.INPUT)
@SendTo(Processor.OUTPUT)
public Message<String> uppercase(Message<String> input) {
return MessageBuilder.withPayload(input.getPayload().toUpperCase())
.copyHeadersIfAbsent(input.getHeaders()).build();
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.binder.servlet.MessageController;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.http.MediaType;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class RoutedSinkMessageChannelBinderTests implements MessageHandler {
@Autowired
private Sink sink;
@Autowired
private MockMvc mockMvc;
private Message<?> message;
@Test
public void consumer() throws Exception {
sink.input().subscribe(this);
mockMvc.perform(post("/stream/input/words")
.contentType(MediaType.APPLICATION_JSON).content("\"hello\""))
.andExpect(status().isAccepted())
.andExpect(content().string(containsString("hello")));
assertThat(this.message).isNotNull();
assertThat(this.message.getHeaders().get(MessageController.ROUTE_KEY))
.isEqualTo("words");
sink.input().unsubscribe(this);
}
@SpringBootApplication
@EnableBinding(Sink.class)
protected static class TestConfiguration {
}
@Override
public void handleMessage(Message<?> message) throws MessagingException {
this.message = message;
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.stream.binder.servlet.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.binder.servlet.MessageController;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class RoutedSourceMessageChannelBinderTests {
@Autowired
private Source source;
@Autowired
private MockMvc mockMvc;
@Test
public void supplier() throws Exception {
source.output().send(MessageBuilder.withPayload("hello")
.setHeader(MessageController.ROUTE_KEY, "words").build());
mockMvc.perform(get("/stream/output/words")).andExpect(status().isOk())
.andExpect(content().string(containsString("hello")));
}
@Test
public void implicit() throws Exception {
source.output().send(MessageBuilder.withPayload("hello")
.setHeader(MessageController.ROUTE_KEY, "words").build());
mockMvc.perform(get("/stream/words")).andExpect(status().isOk())
.andExpect(content().string(containsString("hello")));
}
@SpringBootApplication
@EnableBinding(Source.class)
protected static class TestConfiguration {
}
}

View File

@@ -0,0 +1,96 @@
/*
* 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.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.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
@AutoConfigureMockMvc
@DirtiesContext
public class SinkAndProcessorMessageChannelBinderTests {
protected static Log log = LogFactory
.getLog(SinkAndProcessorMessageChannelBinderTests.class);
@Autowired
private MockMvc mockMvc;
@Test
public void function() throws Exception {
mockMvc.perform(get("/stream/output?purge=true")).andReturn();
mockMvc.perform(post("/stream/input/words")
.contentType(MediaType.APPLICATION_JSON).content("\"hello\""))
.andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void consumer() throws Exception {
mockMvc.perform(get("/stream/output?purge=true")).andReturn();
mockMvc.perform(post("/stream/input/accept")
.contentType(MediaType.APPLICATION_JSON).content("\"hello\""))
.andExpect(status().isAccepted())
.andExpect(content().string(containsString("hello")));
}
@SpringBootApplication
@EnableBinding(Processor.class)
protected static class TestConfiguration {
@Autowired
private Processor processor;
@StreamListener(value = Processor.INPUT, condition = "headers['stream_routekey']=='words'")
public void uppercase(Message<String> input) {
processor.output()
.send(MessageBuilder.withPayload(input.getPayload().toUpperCase())
.copyHeaders(input.getHeaders()).build());
}
@StreamListener(value = Processor.INPUT, condition = "headers['stream_routekey']=='accept'")
public void accept(String input) {
log.warn("Processed: " + input);
}
}
}

View File

@@ -0,0 +1,111 @@
/*
* 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 SinkMessageChannelBinderTests 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);
}
@Test
public void multi() throws Exception {
sink.input().subscribe(this);
mockMvc.perform(post("/stream/input").contentType(MediaType.APPLICATION_JSON)
.content("[\"hello\",\"world\"]")).andExpect(status().isAccepted())
.andExpect(content().string(containsString("[\"hello\",\"world\"]")));
assertThat(this.message).isNotNull();
sink.input().unsubscribe(this);
}
@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 missing() throws Exception {
sink.input().subscribe(this);
// It gets routed to "input" channel with key "missing"
mockMvc.perform(post("/stream/missing").contentType(MediaType.TEXT_PLAIN)
.content("hello")).andExpect(status().isAccepted())
.andExpect(content().string(containsString("hello")));
assertThat(this.message).isNotNull();
sink.input().unsubscribe(this);
}
@SpringBootApplication
@EnableBinding(Sink.class)
protected static class TestConfiguration {
}
@Override
public void handleMessage(Message<?> message) throws MessagingException {
this.message = message;
}
}

View File

@@ -0,0 +1,77 @@
/*
* 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.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.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class SinkWithResponseMessageChannelBinderTests {
@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(content().string(containsString("HELLO")));
}
@SpringBootApplication
@EnableBinding(Processor.class)
protected static class TestConfiguration {
@Autowired
private Processor processor;
@StreamListener(value = Processor.INPUT, condition = "headers['stream_routekey']=='words'")
public void uppercase(Message<String> input) {
// TODO: this won't work without the Message<?> wrapper for the input because
// the headers don't get copied.
processor.output()
.send(MessageBuilder.withPayload(input.getPayload().toUpperCase())
.copyHeaders(input.getHeaders()).build());
}
}
}

View File

@@ -0,0 +1,107 @@
/*
* 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 java.util.Collections;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.RouteRegistry;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.messaging.Message;
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
@AutoConfigureMockMvc
@DirtiesContext
public class SourceAndProcessorMessageChannelBinderTests {
protected static Log log = LogFactory
.getLog(SourceAndProcessorMessageChannelBinderTests.class);
@Autowired
private MockMvc mockMvc;
@Test
public void function() throws Exception {
mockMvc.perform(get("/stream/output?purge=true")).andReturn();
mockMvc.perform(post("/stream/input/words")
.contentType(MediaType.APPLICATION_JSON).content("\"hello\""))
.andExpect(status().isOk())
.andExpect(content().string(containsString("HELLO")));
}
@Test
public void missing() throws Exception {
mockMvc.perform(get("/stream/output?purge=true")).andReturn();
mockMvc.perform(get("/stream/output/missing")).andExpect(status().isNotFound());
}
@Test
public void empty() throws Exception {
// An explicit route registration prevents the implicit conversion of route into
// body
mockMvc.perform(get("/stream/output?purge=true")).andReturn();
mockMvc.perform(get("/stream/empty")).andExpect(status().isOk())
.andExpect(content().string(containsString("[]")));
}
@SpringBootApplication
@EnableBinding(Processor.class)
protected static class TestConfiguration {
@Autowired
private Processor processor;
@StreamListener(Processor.INPUT)
public void uppercase(Message<String> input) {
processor.output()
.send(MessageBuilder.withPayload(input.getPayload().toUpperCase())
.copyHeaders(input.getHeaders()).build());
}
@Bean
public RouteRegistry routeRegistry() {
return () -> Collections.singleton("empty");
}
}
}

View File

@@ -0,0 +1,94 @@
/*
* 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.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.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
public class SourceMessageChannelBinderTests {
@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/output")).andExpect(status().isOk())
.andExpect(content().string(containsString("hello")));
}
@Test
public void implicit() throws Exception {
source.output().send(MessageBuilder.withPayload("hello").build());
mockMvc.perform(get("/stream")).andExpect(status().isOk())
.andExpect(content().string(containsString("hello")));
}
@Test
public void trailing() throws Exception {
source.output().send(MessageBuilder.withPayload("hello").build());
mockMvc.perform(get("/stream/")).andExpect(status().isOk())
.andExpect(content().string(containsString("hello")));
}
@Test
public void empty() throws Exception {
mockMvc.perform(get("/stream/output?purge=true")).andReturn();
mockMvc.perform(get("/stream/output")).andExpect(status().isOk())
.andExpect(content().string(containsString("[]")));
}
@Test
public void missing() throws Exception {
// Missing route is just empty
mockMvc.perform(get("/stream/missing")).andExpect(status().isNotFound())
.andExpect(content().string(equalTo("")));
}
@SpringBootApplication
@EnableBinding(Source.class)
protected static class TestConfiguration {
}
}

View File

@@ -0,0 +1,195 @@
/*
* 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 java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@DirtiesContext
public class SseSourceMessageChannelBinderTests {
private static Log log = LogFactory.getLog(SseSourceMessageChannelBinderTests.class);
@Autowired
private Source source;
private CountDownLatch latch = new CountDownLatch(1);
private RestTemplate rest = new RestTemplate();
@LocalServerPort
private int port;
private String message = null;
@Before
public void init() throws Exception {
rest.getForEntity(
new URI("http://localhost:" + port + "/stream/output?purge=true"),
String.class);
}
@Test
public void supplier() throws Exception {
source.output().send(MessageBuilder.withPayload("hello").build());
rest.getInterceptors().add(new NonClosingInterceptor());
ResponseEntity<String> response = rest.execute(
new URI("http://localhost:" + port + "/stream/output"), HttpMethod.GET,
request -> request.getHeaders()
.setAccept(Arrays.asList(MediaType.TEXT_EVENT_STREAM)),
this::extract);
assertThat(response.getHeaders().getContentType())
.isGreaterThan(MediaType.TEXT_EVENT_STREAM);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isEqualTo("data:hello\n\n");
}
@Test
public void lateSending() throws Exception {
message = "world";
rest.getInterceptors().add(new NonClosingInterceptor());
ResponseEntity<String> response = rest.execute(
new URI("http://localhost:" + port + "/stream/output"), HttpMethod.GET,
request -> request.getHeaders()
.setAccept(Arrays.asList(MediaType.TEXT_EVENT_STREAM)),
this::extract);
assertThat(response.getHeaders().getContentType())
.isGreaterThan(MediaType.TEXT_EVENT_STREAM);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isEqualTo("data:world\n\n");
}
@SpringBootApplication
@EnableBinding(Source.class)
protected static class TestConfiguration {
}
private ResponseEntity<String> extract(ClientHttpResponse response)
throws IOException {
if (message != null) {
// Once there is an incoming request we can send a message to it
source.output().send(MessageBuilder.withPayload(message).build());
}
byte[] bytes = new byte[1024];
StringBuilder builder = new StringBuilder();
int read = 0;
while (read >= 0
&& StringUtils.countOccurrencesOf(builder.toString(), "\n") < 2) {
read = response.getBody().read(bytes, 0, bytes.length);
if (read > 0) {
latch.countDown();
builder.append(new String(bytes, 0, read));
}
log.debug("Building: " + builder);
}
log.debug("Done: " + builder);
return ResponseEntity.status(response.getStatusCode())
.headers(response.getHeaders()).body(builder.toString());
}
/**
* Special interceptor that prevents the response from being closed and allows us to
* assert on the contents of an event stream.
*/
private class NonClosingInterceptor implements ClientHttpRequestInterceptor {
private class NonClosingResponse implements ClientHttpResponse {
private ClientHttpResponse delegate;
public NonClosingResponse(ClientHttpResponse delegate) {
this.delegate = delegate;
}
@Override
public InputStream getBody() throws IOException {
return delegate.getBody();
}
@Override
public HttpHeaders getHeaders() {
return delegate.getHeaders();
}
@Override
public HttpStatus getStatusCode() throws IOException {
return delegate.getStatusCode();
}
@Override
public int getRawStatusCode() throws IOException {
return delegate.getRawStatusCode();
}
@Override
public String getStatusText() throws IOException {
return delegate.getStatusText();
}
@Override
public void close() {
}
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
return new NonClosingResponse(execution.execute(request, body));
}
}
}

View File

@@ -0,0 +1,2 @@
spring.main.banner-mode=off
#logging.level.org.springframework.cloud.stream.binder.servlet=DEBUG