HTTP converters for CloudEvent in Spring (#312)
Supports MVC and WebFlux (blocking and non-blocking) HTTP. User can work with `CloudEvent` as a `POJO` type and inject it into `@ReqestMapping` methods. Signed-off-by: Dave Syer <dsyer@vmware.com> Co-authored-by: Oleg Zhurakousky <ozhurakousky@pivotal.io>
This commit is contained in:
parent
bcc1434a39
commit
5099b31f6c
1
pom.xml
1
pom.xml
|
|
@ -74,6 +74,7 @@
|
|||
<module>http/vertx</module>
|
||||
<module>http/restful-ws</module>
|
||||
<module>kafka</module>
|
||||
<module>spring</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
## Spring Support
|
||||
|
||||
### Introduction
|
||||
|
||||
This module provides classes and interfaces that can be used by [Spring frameworks](https://spring.io/) and integrations to assist with Cloud Event processing.
|
||||
|
||||
Given that Spring defines [Message](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/messaging/Message.html) abstraction,
|
||||
which perfectly maps to the structure defined by Cloud Events specification, one may say Cloud Events are already supported by any Spring framework that
|
||||
relies on `Message`. So this modules provides several utilities and strategies to simplify working with Cloud Events in the context of Spring
|
||||
frameworks and integrations (see individual component's javadocs for more details).
|
||||
|
||||
Please see individual samples in `examples/spring` directory of this SDK for more details.
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2018-Present The CloudEvents Authors
|
||||
~ <p>
|
||||
~ 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
|
||||
~ <p>
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~ <p>
|
||||
~ 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.
|
||||
~
|
||||
-->
|
||||
<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/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>io.cloudevents</groupId>
|
||||
<artifactId>cloudevents-parent</artifactId>
|
||||
<version>2.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>cloudevents-spring</artifactId>
|
||||
<name>CloudEvents - support for Spring</name>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<module-name>io.cloudevents.spring</module-name>
|
||||
<spring-boot.version>2.4.0</spring-boot.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webmvc</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webflux</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.cloudevents</groupId>
|
||||
<artifactId>cloudevents-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.cloudevents</groupId>
|
||||
<artifactId>cloudevents-http-basic</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<!-- This will be optional too if we want to support messaging -->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat.embed</groupId>
|
||||
<artifactId>tomcat-embed-core</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Test deps -->
|
||||
<dependency>
|
||||
<groupId>io.cloudevents</groupId>
|
||||
<artifactId>cloudevents-json-jackson</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.cloudevents</groupId>
|
||||
<artifactId>cloudevents-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright 2019-2019 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
|
||||
*
|
||||
* https://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 io.cloudevents.spring.http;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.CloudEventContext;
|
||||
import io.cloudevents.core.CloudEventUtils;
|
||||
import io.cloudevents.core.builder.CloudEventBuilder;
|
||||
import io.cloudevents.core.message.MessageReader;
|
||||
import io.cloudevents.http.HttpMessageFactory;
|
||||
import io.cloudevents.http.impl.HttpMessageWriter;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
/**
|
||||
* Miscellaneous utility methods to assist with Cloud Events in the context of Spring Web
|
||||
* frameworks. Primarily intended for the internal use within Spring-based frameworks or
|
||||
* integrations.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @since 2.0
|
||||
*/
|
||||
public class CloudEventHttpUtils {
|
||||
|
||||
private CloudEventHttpUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link MessageReader} to assist in conversion of an HTTP request to a
|
||||
* {@link CloudEvent}.
|
||||
* @param headers the HTTP request headers
|
||||
* @param body the HTTP request body as a byte array
|
||||
* @return a {@link MessageReader} representing the {@link CloudEvent}
|
||||
*/
|
||||
public static MessageReader toReader(HttpHeaders headers, Supplier<byte[]> body) {
|
||||
return HttpMessageFactory.createReaderFromMultimap(headers, body.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@link HttpMessageWriter} that can hand off a {@link CloudEvent} to an
|
||||
* HTTP response. Mainly useful in a blocking (not async) setting because the response
|
||||
* body has to be consumed directly.
|
||||
* @param headers the response headers (will be mutated)
|
||||
* @param sendBody a consumer for the response body that puts the bytes on the wire
|
||||
*/
|
||||
public static HttpMessageWriter toWriter(HttpHeaders headers, Consumer<byte[]> sendBody) {
|
||||
return HttpMessageFactory.createWriter(headers::set, sendBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for extracting {@link HttpHeaders} from a {@link CloudEvent}. Can,
|
||||
* for instance, be used in a <code>@RequestMapping</code> to return a
|
||||
* {@link ResponseEntity} that has headers copied from a {@link CloudEvent}.
|
||||
* @param event the input {@link CloudEvent}
|
||||
* @return the response headers represented by the event
|
||||
*/
|
||||
public static HttpHeaders toHttp(CloudEventContext event) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
CloudEventUtils.toReader(CloudEventBuilder.fromContext(event).build()).read(toWriter(headers, bytes -> {
|
||||
}));
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for converting {@link HttpHeaders} to a {@link CloudEvent}. The input
|
||||
* headers must represent a valid event in "binary" form, i.e. it must have headers
|
||||
* "ce-id", "ce-specversion" etc.
|
||||
* @param headers the input request headers
|
||||
* @return a {@link CloudEventBuilder} that can be used to create a new
|
||||
* {@link CloudEvent}
|
||||
*
|
||||
*/
|
||||
public static CloudEventBuilder fromHttp(HttpHeaders headers) {
|
||||
return CloudEventBuilder
|
||||
.fromContext(CloudEventUtils.toEvent(CloudEventHttpUtils.toReader(headers, () -> null)));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright 2018-Present The CloudEvents Authors
|
||||
* <p>
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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 io.cloudevents.spring.http;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.cloudevents.core.message.impl.MessageUtils;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
public class CloudEventsHeaders {
|
||||
|
||||
public static final String CE_PREFIX = "ce-";
|
||||
|
||||
public static final Map<String, String> ATTRIBUTES_TO_HEADERS = MessageUtils
|
||||
.generateAttributesToHeadersMapping(v -> {
|
||||
if (v.equals("datacontenttype")) {
|
||||
return HttpHeaders.CONTENT_TYPE;
|
||||
}
|
||||
return CE_PREFIX + v;
|
||||
});
|
||||
|
||||
public static final String SPEC_VERSION = ATTRIBUTES_TO_HEADERS.get("specversion");
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
/**
|
||||
* Provides classes related to working with Cloud Events within the context of Spring and
|
||||
* HTTP.
|
||||
*/
|
||||
package io.cloudevents.spring.http;
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright 2019-2019 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
|
||||
*
|
||||
* https://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 io.cloudevents.spring.mvc;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.core.CloudEventUtils;
|
||||
import io.cloudevents.spring.http.CloudEventHttpUtils;
|
||||
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.AbstractHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
/**
|
||||
* An {@link HttpMessageConverter} for {@link CloudEvent CloudEvents}. Supports the use of
|
||||
* {@link CloudEvent} in a <code>@RequestMapping</code> as either a method parameter
|
||||
* or a return value.
|
||||
*
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class CloudEventHttpMessageConverter extends AbstractHttpMessageConverter<CloudEvent> {
|
||||
|
||||
public CloudEventHttpMessageConverter() {
|
||||
super(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean supports(Class<?> clazz) {
|
||||
return CloudEvent.class.isAssignableFrom(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CloudEvent readInternal(Class<? extends CloudEvent> clazz, HttpInputMessage inputMessage)
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
byte[] body = StreamUtils.copyToByteArray(inputMessage.getBody());
|
||||
return CloudEventHttpUtils.toReader(inputMessage.getHeaders(), () -> body).toEvent();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeInternal(CloudEvent event, HttpOutputMessage outputMessage)
|
||||
throws IOException, HttpMessageNotWritableException {
|
||||
CloudEventUtils.toReader(event)
|
||||
.read(CloudEventHttpUtils.toWriter(outputMessage.getHeaders(), body -> copy(body, outputMessage)));
|
||||
}
|
||||
|
||||
private void copy(byte[] body, HttpOutputMessage outputMessage) {
|
||||
try {
|
||||
StreamUtils.copy(body, outputMessage.getBody());
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* Provides classes related to working with Cloud Events within the context of Spring MVC.
|
||||
*/
|
||||
package io.cloudevents.spring.mvc;
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright 2019-2019 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
|
||||
*
|
||||
* https://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 io.cloudevents.spring.webflux;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.core.message.MessageReader;
|
||||
import io.cloudevents.spring.http.CloudEventHttpUtils;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ReactiveHttpInputMessage;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
/**
|
||||
* A reactive {@link HttpMessageReader} for {@link CloudEvent CloudEvents}, converting
|
||||
* from an HTTP request to a cloud event. Supports the use of {@link CloudEvent} as an
|
||||
* input to a reactive endpoint.
|
||||
*
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class CloudEventHttpMessageReader implements HttpMessageReader<CloudEvent> {
|
||||
|
||||
@Override
|
||||
public List<MediaType> getReadableMediaTypes() {
|
||||
return Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead(ResolvableType elementType, MediaType mediaType) {
|
||||
return CloudEvent.class.isAssignableFrom(elementType.toClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<CloudEvent> read(ResolvableType elementType, ReactiveHttpInputMessage message,
|
||||
Map<String, Object> hints) {
|
||||
return Flux.from(readMono(elementType, message, hints));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<CloudEvent> readMono(ResolvableType elementType, ReactiveHttpInputMessage message,
|
||||
Map<String, Object> hints) {
|
||||
HttpHeaders headers = message.getHeaders();
|
||||
Mono<byte[]> body = DataBufferUtils.join(message.getBody()).map(buffer -> {
|
||||
try {
|
||||
return StreamUtils.copyToByteArray(buffer.asInputStream(true));
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
});
|
||||
return body.map(bytes -> CloudEventHttpUtils.toReader(headers, () -> bytes)).map(MessageReader::toEvent);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright 2019-2019 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
|
||||
*
|
||||
* https://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 io.cloudevents.spring.webflux;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.CloudEventData;
|
||||
import io.cloudevents.SpecVersion;
|
||||
import io.cloudevents.core.CloudEventUtils;
|
||||
import io.cloudevents.core.format.EventFormat;
|
||||
import io.cloudevents.core.message.MessageWriter;
|
||||
import io.cloudevents.rw.CloudEventRWException;
|
||||
import io.cloudevents.rw.CloudEventWriter;
|
||||
import io.cloudevents.spring.http.CloudEventsHeaders;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ReactiveHttpOutputMessage;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
|
||||
/**
|
||||
* A reactive {@link HttpMessageWriter} for {@link CloudEvent CloudEvents}, converting
|
||||
* from a cloud event to an HTTP response. Supports the use of {@link CloudEvent} as an
|
||||
* output from a reactive endpoint.
|
||||
*
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class CloudEventHttpMessageWriter implements HttpMessageWriter<CloudEvent> {
|
||||
|
||||
@Override
|
||||
public List<MediaType> getWritableMediaTypes() {
|
||||
return Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(ResolvableType elementType, MediaType mediaType) {
|
||||
return CloudEvent.class.isAssignableFrom(elementType.toClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> write(Publisher<? extends CloudEvent> inputStream, ResolvableType elementType,
|
||||
MediaType mediaType, ReactiveHttpOutputMessage message, Map<String, Object> hints) {
|
||||
return Mono.from(inputStream).map(CloudEventUtils::toReader)
|
||||
.flatMap(reader -> reader.read(new ReactiveHttpMessageWriter(message)));
|
||||
}
|
||||
|
||||
private static class ReactiveHttpMessageWriter
|
||||
implements MessageWriter<CloudEventWriter<Mono<Void>>, Mono<Void>>, CloudEventWriter<Mono<Void>> {
|
||||
|
||||
private final ReactiveHttpOutputMessage response;
|
||||
|
||||
public ReactiveHttpMessageWriter(ReactiveHttpOutputMessage response) {
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
// Binary visitor factory
|
||||
|
||||
@Override
|
||||
public CloudEventWriter<Mono<Void>> create(SpecVersion version) {
|
||||
this.response.getHeaders().set(CloudEventsHeaders.SPEC_VERSION, version.toString());
|
||||
return this;
|
||||
}
|
||||
|
||||
// Binary visitor
|
||||
|
||||
@Override
|
||||
public ReactiveHttpMessageWriter withAttribute(String name, String value) throws CloudEventRWException {
|
||||
this.response.getHeaders().set(CloudEventsHeaders.ATTRIBUTES_TO_HEADERS.get(name), value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveHttpMessageWriter withExtension(String name, String value) throws CloudEventRWException {
|
||||
this.response.getHeaders().set("ce-" + name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> end(CloudEventData value) throws CloudEventRWException {
|
||||
return copy(value.toBytes(), this.response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> end() {
|
||||
return copy(new byte[0], this.response);
|
||||
}
|
||||
|
||||
// Structured visitor
|
||||
|
||||
@Override
|
||||
public Mono<Void> setEvent(EventFormat format, byte[] value) throws CloudEventRWException {
|
||||
this.response.getHeaders().set(HttpHeaders.CONTENT_TYPE, format.serializedContentType());
|
||||
return copy(value, this.response);
|
||||
}
|
||||
|
||||
private Mono<Void> copy(byte[] bytes, ReactiveHttpOutputMessage message) {
|
||||
DataBuffer data = message.bufferFactory().wrap(bytes);
|
||||
message.getHeaders().setContentLength(bytes.length);
|
||||
return message.writeWith(Mono.just(data));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
/**
|
||||
* Provides classes related to working with Cloud Events within the context of Spring
|
||||
* WebFlux.
|
||||
*/
|
||||
package io.cloudevents.spring.webflux;
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* Copyright 2019-2019 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
|
||||
*
|
||||
* https://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 io.cloudevents.spring.mvc;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.core.builder.CloudEventBuilder;
|
||||
import io.cloudevents.spring.http.CloudEventHttpUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||
class MvcRestControllerTests {
|
||||
|
||||
@Autowired
|
||||
private TestRestTemplate rest;
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@Test
|
||||
void echoWithCorrectHeaders() {
|
||||
|
||||
ResponseEntity<String> response = rest.exchange(RequestEntity.post(URI.create("http://localhost:" + port + "/")) //
|
||||
.header("ce-id", "12345") //
|
||||
.header("ce-specversion", "1.0") //
|
||||
.header("ce-type", "io.spring.event") //
|
||||
.header("ce-source", "https://spring.io/events") //
|
||||
.contentType(MediaType.APPLICATION_JSON) //
|
||||
.body("{\"value\":\"Dave\"}"), String.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).isEqualTo("{\"value\":\"Dave\"}");
|
||||
|
||||
HttpHeaders headers = response.getHeaders();
|
||||
|
||||
assertThat(headers).containsKey("ce-id");
|
||||
assertThat(headers).containsKey("ce-source");
|
||||
assertThat(headers).containsKey("ce-type");
|
||||
|
||||
// assertThat(headers.getFirst("ce-id")).isNotEqualTo("12345");
|
||||
assertThat(headers.getFirst("ce-type")).isEqualTo("io.spring.event.Foo");
|
||||
assertThat(headers.getFirst("ce-source")).isEqualTo("https://spring.io/foos");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void structuredRequestResponseEvents() {
|
||||
|
||||
ResponseEntity<String> response = rest
|
||||
.exchange(RequestEntity.post(URI.create("http://localhost:" + port + "/event")) //
|
||||
.contentType(new MediaType("application", "cloudevents+json")) //
|
||||
.body("{" //
|
||||
+ "\"id\":\"12345\"," //
|
||||
+ "\"specversion\":\"1.0\"," //
|
||||
+ "\"type\":\"io.spring.event\"," //
|
||||
+ "\"source\":\"https://spring.io/events\"," //
|
||||
+ "\"data\":{\"value\":\"Dave\"}}"),
|
||||
String.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).isEqualTo("{\"value\":\"Dave\"}");
|
||||
|
||||
HttpHeaders headers = response.getHeaders();
|
||||
|
||||
assertThat(headers).containsKey("ce-id");
|
||||
assertThat(headers).containsKey("ce-source");
|
||||
assertThat(headers).containsKey("ce-type");
|
||||
|
||||
// assertThat(headers.getFirst("ce-id")).isNotEqualTo("12345");
|
||||
assertThat(headers.getFirst("ce-type")).isEqualTo("io.spring.event.Foo");
|
||||
assertThat(headers.getFirst("ce-source")).isEqualTo("https://spring.io/foos");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestResponseEvents() {
|
||||
|
||||
ResponseEntity<String> response = rest
|
||||
.exchange(RequestEntity.post(URI.create("http://localhost:" + port + "/event")) //
|
||||
.header("ce-id", "12345") //
|
||||
.header("ce-specversion", "1.0") //
|
||||
.header("ce-type", "io.spring.event") //
|
||||
.header("ce-source", "https://spring.io/events") //
|
||||
.contentType(MediaType.APPLICATION_JSON) //
|
||||
.body("{\"value\":\"Dave\"}"), String.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).isEqualTo("{\"value\":\"Dave\"}");
|
||||
|
||||
HttpHeaders headers = response.getHeaders();
|
||||
|
||||
assertThat(headers).containsKey("ce-id");
|
||||
assertThat(headers).containsKey("ce-source");
|
||||
assertThat(headers).containsKey("ce-type");
|
||||
|
||||
assertThat(headers.getFirst("ce-id")).isNotEqualTo("12345");
|
||||
assertThat(headers.getFirst("ce-type")).isEqualTo("io.spring.event.Foo");
|
||||
assertThat(headers.getFirst("ce-source")).isEqualTo("https://spring.io/foos");
|
||||
|
||||
}
|
||||
|
||||
@SpringBootApplication
|
||||
@RestController
|
||||
static class TestApplication {
|
||||
|
||||
@PostMapping("/")
|
||||
public ResponseEntity<Foo> echo(@RequestBody Foo foo, @RequestHeader HttpHeaders headers) {
|
||||
CloudEvent attributes = CloudEventHttpUtils.fromHttp(headers) //
|
||||
.withId(UUID.randomUUID().toString()) //
|
||||
.withSource(URI.create("https://spring.io/foos")) //
|
||||
.withType("io.spring.event.Foo") //
|
||||
.build();
|
||||
HttpHeaders outgoing = CloudEventHttpUtils.toHttp(attributes);
|
||||
return ResponseEntity.ok().headers(outgoing).body(foo);
|
||||
}
|
||||
|
||||
@PostMapping("/event")
|
||||
public CloudEvent ce(@RequestBody CloudEvent event) {
|
||||
CloudEvent attributes = CloudEventBuilder.from(event) //
|
||||
.withId(UUID.randomUUID().toString()) //
|
||||
.withSource(URI.create("https://spring.io/foos")) //
|
||||
.withType("io.spring.event.Foo") //
|
||||
.withData(event.getData().toBytes()) //
|
||||
.build();
|
||||
return attributes;
|
||||
}
|
||||
|
||||
@Configuration
|
||||
public static class CloudEventHandlerConfiguration implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||
converters.add(0, new CloudEventHttpMessageConverter());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Foo {
|
||||
|
||||
private String value;
|
||||
|
||||
public Foo() {
|
||||
}
|
||||
|
||||
public Foo(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Foo [value=" + this.value + "]";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
* Copyright 2019-2019 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
|
||||
*
|
||||
* https://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 io.cloudevents.spring.webflux;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.UUID;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.core.builder.CloudEventBuilder;
|
||||
import io.cloudevents.spring.http.CloudEventHttpUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.boot.web.codec.CodecCustomizer;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.codec.CodecConfigurer;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.main.web-application-type=REACTIVE")
|
||||
class WebFluxRestControllerTests {
|
||||
|
||||
@Autowired
|
||||
private TestRestTemplate rest;
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@Test
|
||||
void echoWithCorrectHeaders() {
|
||||
|
||||
ResponseEntity<String> response = rest.exchange(RequestEntity.post(URI.create("http://localhost:" + port + "/")) //
|
||||
.header("ce-id", "12345") //
|
||||
.header("ce-specversion", "1.0") //
|
||||
.header("ce-type", "io.spring.event") //
|
||||
.header("ce-source", "https://spring.io/events") //
|
||||
.contentType(MediaType.APPLICATION_JSON) //
|
||||
.body("{\"value\":\"Dave\"}"), String.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).isEqualTo("{\"value\":\"Dave\"}");
|
||||
|
||||
HttpHeaders headers = response.getHeaders();
|
||||
|
||||
assertThat(headers).containsKey("ce-id");
|
||||
assertThat(headers).containsKey("ce-source");
|
||||
assertThat(headers).containsKey("ce-type");
|
||||
|
||||
// assertThat(headers.getFirst("ce-id")).isNotEqualTo("12345");
|
||||
assertThat(headers.getFirst("ce-type")).isEqualTo("io.spring.event.Foo");
|
||||
assertThat(headers.getFirst("ce-source")).isEqualTo("https://spring.io/foos");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void structuredRequestResponseEvents() {
|
||||
|
||||
ResponseEntity<String> response = rest
|
||||
.exchange(RequestEntity.post(URI.create("http://localhost:" + port + "/event")) //
|
||||
.contentType(new MediaType("application", "cloudevents+json")) //
|
||||
.body("{" //
|
||||
+ "\"id\":\"12345\"," //
|
||||
+ "\"specversion\":\"1.0\"," //
|
||||
+ "\"type\":\"io.spring.event\"," //
|
||||
+ "\"source\":\"https://spring.io/events\"," //
|
||||
+ "\"data\":{\"value\":\"Dave\"}}"),
|
||||
String.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).isEqualTo("{\"value\":\"Dave\"}");
|
||||
|
||||
HttpHeaders headers = response.getHeaders();
|
||||
|
||||
assertThat(headers).containsKey("ce-id");
|
||||
assertThat(headers).containsKey("ce-source");
|
||||
assertThat(headers).containsKey("ce-type");
|
||||
|
||||
// assertThat(headers.getFirst("ce-id")).isNotEqualTo("12345");
|
||||
assertThat(headers.getFirst("ce-type")).isEqualTo("io.spring.event.Foo");
|
||||
assertThat(headers.getFirst("ce-source")).isEqualTo("https://spring.io/foos");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestResponseEvents() {
|
||||
|
||||
ResponseEntity<String> response = rest
|
||||
.exchange(RequestEntity.post(URI.create("http://localhost:" + port + "/event")) //
|
||||
.header("ce-id", "12345") //
|
||||
.header("ce-specversion", "1.0") //
|
||||
.header("ce-type", "io.spring.event") //
|
||||
.header("ce-source", "https://spring.io/events") //
|
||||
.contentType(MediaType.APPLICATION_JSON) //
|
||||
.body("{\"value\":\"Dave\"}"), String.class);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).isEqualTo("{\"value\":\"Dave\"}");
|
||||
|
||||
HttpHeaders headers = response.getHeaders();
|
||||
|
||||
assertThat(headers).containsKey("ce-id");
|
||||
assertThat(headers).containsKey("ce-source");
|
||||
assertThat(headers).containsKey("ce-type");
|
||||
|
||||
// assertThat(headers.getFirst("ce-id")).isNotEqualTo("12345");
|
||||
assertThat(headers.getFirst("ce-type")).isEqualTo("io.spring.event.Foo");
|
||||
assertThat(headers.getFirst("ce-source")).isEqualTo("https://spring.io/foos");
|
||||
|
||||
}
|
||||
|
||||
@SpringBootApplication
|
||||
@RestController
|
||||
static class TestApplication {
|
||||
|
||||
@PostMapping("/")
|
||||
public ResponseEntity<Foo> echo(@RequestBody Foo foo, @RequestHeader HttpHeaders headers) {
|
||||
CloudEvent attributes = CloudEventHttpUtils.fromHttp(headers).withId(UUID.randomUUID().toString())
|
||||
.withSource(URI.create("https://spring.io/foos")).withType("io.spring.event.Foo").build();
|
||||
HttpHeaders outgoing = CloudEventHttpUtils.toHttp(attributes);
|
||||
return ResponseEntity.ok().headers(outgoing).body(foo);
|
||||
}
|
||||
|
||||
@PostMapping("/event")
|
||||
public Mono<CloudEvent> event(@RequestBody Mono<CloudEvent> body) {
|
||||
return body.map(event -> CloudEventBuilder.from(event).withId(UUID.randomUUID().toString())
|
||||
.withSource(URI.create("https://spring.io/foos")).withType("io.spring.event.Foo")
|
||||
.withData(event.getData().toBytes()).build());
|
||||
}
|
||||
|
||||
@Configuration
|
||||
public static class CloudEventHandlerConfiguration implements CodecCustomizer {
|
||||
|
||||
@Override
|
||||
public void customize(CodecConfigurer configurer) {
|
||||
configurer.customCodecs().register(new CloudEventHttpMessageReader());
|
||||
configurer.customCodecs().register(new CloudEventHttpMessageWriter());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Foo {
|
||||
|
||||
private String value;
|
||||
|
||||
public Foo() {
|
||||
}
|
||||
|
||||
public Foo(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Foo [value=" + this.value + "]";
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue