From f0b2ce769133ed878a2892edccd8d2d94cf233f8 Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Fri, 13 Nov 2020 10:24:27 +0100 Subject: [PATCH] GH-422 GH-606 Add support for simplifying message headers to attribute mapping Added CloudEventAttributesProvider and default implementation Added CloudEventMessageUtils --- .../cloudevent/CloudEventAttributes.java | 84 +++++++++++++++++++ .../CloudEventAtttributesProvider.java | 60 +++++++++++++ ...entDataContentTypeMessagePreProcessor.java | 22 ++--- .../CloudEventJsonMessageConverter.java | 3 +- ...Utils.java => CloudEventMessageUtils.java} | 35 +++++--- .../DefaultCloudEventAttributesProvider.java | 64 ++++++++++++++ .../cloudevent/RequiredAttributeAccessor.java | 56 +++++++++++++ ...ntextFunctionCatalogAutoConfiguration.java | 7 ++ ...ava => CloudEventTypeConversionTests.java} | 60 ++++++++----- .../cloudevent/CloudeventDemoApplication.java | 38 +++++++++ .../spring/cloudevent/SpringReleaseEvent.java | 10 +++ ...loudeventDemoApplicationFunctionTests.java | 68 ++++++++++----- 12 files changed, 438 insertions(+), 69 deletions(-) create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventAttributes.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventAtttributesProvider.java rename spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/{CloudEventUtils.java => CloudEventMessageUtils.java} (77%) create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/DefaultCloudEventAttributesProvider.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/RequiredAttributeAccessor.java rename spring-cloud-function-context/src/test/java/org/springframework/cloud/function/cloudevent/{CloudEventJsonMessageConverterTests.java => CloudEventTypeConversionTests.java} (72%) diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventAttributes.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventAttributes.java new file mode 100644 index 000000000..0b534db81 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventAttributes.java @@ -0,0 +1,84 @@ +/* + * 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 org.springframework.cloud.function.cloudevent; + +import java.util.HashMap; +import java.util.Map; + + +/** + * + * @author Oleg Zhurakousky + * @since 3.1 + */ +public class CloudEventAttributes extends HashMap { + + /** + * + */ + private static final long serialVersionUID = 5393610770855366497L; + + + + CloudEventAttributes(Map headers) { + super(headers); + } + + @SuppressWarnings("unchecked") + public A getId() { + return this.containsKey(CloudEventMessageUtils.CE_ID) + ? (A) this.get(CloudEventMessageUtils.CE_ID) + : (A) this.get(CloudEventMessageUtils.ID); + } + + @SuppressWarnings("unchecked") + public A getSource() { + return this.containsKey(CloudEventMessageUtils.CE_SOURCE) + ? (A) this.get(CloudEventMessageUtils.CE_SOURCE) + : (A) this.get(CloudEventMessageUtils.SOURCE); + } + + @SuppressWarnings("unchecked") + public A getSpecversion() { + return this.containsKey(CloudEventMessageUtils.CE_SPECVERSION) + ? (A) this.get(CloudEventMessageUtils.CE_SPECVERSION) + : (A) this.get(CloudEventMessageUtils.SPECVERSION); + } + + @SuppressWarnings("unchecked") + public A getType() { + return this.containsKey(CloudEventMessageUtils.CE_TYPE) + ? (A) this.get(CloudEventMessageUtils.CE_TYPE) + : (A) this.get(CloudEventMessageUtils.TYPE); + } + + @SuppressWarnings("unchecked") + public A getDataContentType() { + return this.containsKey(CloudEventMessageUtils.CE_DATACONTENTTYPE) + ? (A) this.get(CloudEventMessageUtils.CE_DATACONTENTTYPE) + : (A) this.get(CloudEventMessageUtils.DATACONTENTTYPE); + } + + public void setDataContentType(String datacontenttype) { + this.put(CloudEventMessageUtils.CE_DATACONTENTTYPE, datacontenttype); + } + + @SuppressWarnings("unchecked") + public A getAtttribute(String name) { + return (A) this.get(name); + } +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventAtttributesProvider.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventAtttributesProvider.java new file mode 100644 index 000000000..de8986ef4 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventAtttributesProvider.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020-2020 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 org.springframework.cloud.function.cloudevent; + +import org.springframework.messaging.MessageHeaders; + +/** + * + * @author Oleg Zhurakousky + * @since 3.1 + */ +public interface CloudEventAtttributesProvider { + + /** + * Will construct instance of {@link CloudEventAttributes} setting its required attributes. + * + * @param ce_id value for Cloud Event 'id' attribute + * @param ce_specversion value for Cloud Event 'specversion' attribute + * @param ce_source value for Cloud Event 'source' attribute + * @param ce_type value for Cloud Event 'type' attribute + * @return instance of {@link CloudEventAttributes} + */ + CloudEventAttributes get(String ce_id, String ce_specversion, String ce_source, String ce_type); + + /** + * Will construct instance of {@link CloudEventAttributes} + * Should default/generate cloud event ID and SPECVERSION. + * + * @param ce_source value for Cloud Event 'source' attribute + * @param ce_type value for Cloud Event 'type' attribute + * @return instance of {@link CloudEventAttributes} + */ + CloudEventAttributes get(String ce_source, String ce_type); + + + /** + * Will construct instance of {@link CloudEventAttributes} from {@link MessageHeaders}. + * + * Should copy Cloud Event related headers into an instance of {@link CloudEventAttributes} + * NOTE: Certain headers must not be copied. + * + * @param headers instance of {@link MessageHeaders} + * @return modifiable instance of {@link CloudEventAttributes} + */ + RequiredAttributeAccessor get(MessageHeaders headers); +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventDataContentTypeMessagePreProcessor.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventDataContentTypeMessagePreProcessor.java index e0998d6da..5ff1d60ea 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventDataContentTypeMessagePreProcessor.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventDataContentTypeMessagePreProcessor.java @@ -50,7 +50,7 @@ public class CloudEventDataContentTypeMessagePreProcessor implements Function apply(Message inputMessage) { - if (CloudEventUtils.isBinary(inputMessage)) { + if (CloudEventMessageUtils.isBinary(inputMessage.getHeaders())) { String dataContentType = this.getDataContentType(inputMessage.getHeaders()); Message message = MessageBuilder.fromMessage(inputMessage) .setHeader(MessageHeaders.CONTENT_TYPE, dataContentType) @@ -78,7 +78,7 @@ public class CloudEventDataContentTypeMessagePreProcessor implements Function cloudEventMessage = MessageBuilder.fromMessage(inputMessage) .setHeader(MessageHeaders.CONTENT_TYPE, cloudEventDeserializationContentType) - .setHeader(CloudEventUtils.CE_DATACONTENTTYPE, dataContentType).build(); + .setHeader(CloudEventMessageUtils.CE_DATACONTENTTYPE, dataContentType).build(); Map structuredCloudEvent = (Map) this.messageConverter .fromMessage(cloudEventMessage, Map.class); Message binaryCeMessage = this.buildCeMessageFromStructured(structuredCloudEvent); @@ -90,27 +90,27 @@ public class CloudEventDataContentTypeMessagePreProcessor implements Function buildCeMessageFromStructured(Map structuredCloudEvent) { - MessageBuilder builder = MessageBuilder.withPayload(structuredCloudEvent.get(CloudEventUtils.DATA)); - structuredCloudEvent.remove(CloudEventUtils.DATA); + MessageBuilder builder = MessageBuilder.withPayload(structuredCloudEvent.get(CloudEventMessageUtils.DATA)); + structuredCloudEvent.remove(CloudEventMessageUtils.DATA); builder.copyHeaders(structuredCloudEvent); return builder.build(); } private String getDataContentType(MessageHeaders headers) { - if (headers.containsKey(CloudEventUtils.DATACONTENTTYPE)) { - return (String) headers.get(CloudEventUtils.DATACONTENTTYPE); + if (headers.containsKey(CloudEventMessageUtils.DATACONTENTTYPE)) { + return (String) headers.get(CloudEventMessageUtils.DATACONTENTTYPE); } - else if (headers.containsKey(CloudEventUtils.CE_DATACONTENTTYPE)) { - return (String) headers.get(CloudEventUtils.CE_DATACONTENTTYPE); + else if (headers.containsKey(CloudEventMessageUtils.CE_DATACONTENTTYPE)) { + return (String) headers.get(CloudEventMessageUtils.CE_DATACONTENTTYPE); } else if (headers.containsKey(MessageHeaders.CONTENT_TYPE)) { return headers.get(MessageHeaders.CONTENT_TYPE).toString(); } - return "application/json"; + return MimeTypeUtils.APPLICATION_JSON_VALUE; } private boolean isStructured(Message message) { - if (!CloudEventUtils.isBinary(message)) { + if (!CloudEventMessageUtils.isBinary(message.getHeaders())) { Map headers = message.getHeaders(); if (headers.containsKey(MessageHeaders.CONTENT_TYPE)) { diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventJsonMessageConverter.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventJsonMessageConverter.java index 1e8ed88ce..1ab79e861 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventJsonMessageConverter.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventJsonMessageConverter.java @@ -32,7 +32,8 @@ import org.springframework.util.MimeType; public class CloudEventJsonMessageConverter extends JsonMessageConverter { public CloudEventJsonMessageConverter(JsonMapper jsonMapper) { - super(jsonMapper, new MimeType("application", "cloudevents+json")); + super(jsonMapper, new MimeType(CloudEventMessageUtils.APPLICATION_CLOUDEVENTS.getType(), + CloudEventMessageUtils.APPLICATION_CLOUDEVENTS.getSubtype() + "+json")); this.setStrictContentTypeMatch(true); } } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventUtils.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventMessageUtils.java similarity index 77% rename from spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventUtils.java rename to spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventMessageUtils.java index d972b1731..b92de95d3 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventUtils.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventMessageUtils.java @@ -19,6 +19,8 @@ package org.springframework.cloud.function.cloudevent; import java.util.Map; import org.springframework.messaging.Message; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; /** * Miscellaneous utility methods to deal with Cloud Events - https://cloudevents.io/. @@ -28,12 +30,22 @@ import org.springframework.messaging.Message; * @author Oleg Zhurakousky * @since 3.1 */ -public final class CloudEventUtils { +public final class CloudEventMessageUtils { - private CloudEventUtils() { + private CloudEventMessageUtils() { } + /** + * String value of 'application/cloudevents' mime type. + */ + public static String APPLICATION_CLOUDEVENTS_VALUE = "application/cloudevents"; + + /** + * {@link MimeType} instance representing 'application/cloudevents' mime type. + */ + public static MimeType APPLICATION_CLOUDEVENTS = MimeTypeUtils.parseMimeType(APPLICATION_CLOUDEVENTS_VALUE); + /** * Prefix for attributes. */ @@ -132,16 +144,15 @@ public final class CloudEventUtils { /** * Checks if {@link Message} represents cloud event in binary-mode. */ - public static boolean isBinary(Message message) { - Map headers = message.getHeaders(); - return (headers.containsKey("id") - && headers.containsKey("source") - && headers.containsKey("specversion") - && headers.containsKey("type")) + public static boolean isBinary(Map headers) { + return (headers.containsKey(ID) + && headers.containsKey(SOURCE) + && headers.containsKey(SPECVERSION) + && headers.containsKey(TYPE)) || - (headers.containsKey("ce_id") - && headers.containsKey("ce_source") - && headers.containsKey("ce_specversion") - && headers.containsKey("ce_type")); + (headers.containsKey(CE_ID) + && headers.containsKey(CE_SOURCE) + && headers.containsKey(CE_SPECVERSION) + && headers.containsKey(CE_TYPE)); } } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/DefaultCloudEventAttributesProvider.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/DefaultCloudEventAttributesProvider.java new file mode 100644 index 000000000..d01451065 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/DefaultCloudEventAttributesProvider.java @@ -0,0 +1,64 @@ +/* + * 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 org.springframework.cloud.function.cloudevent; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.springframework.messaging.MessageHeaders; +import org.springframework.util.Assert; + +/** + * + * @author Oleg Zhurakousky + * @since 3.1 + * + */ +public class DefaultCloudEventAttributesProvider implements CloudEventAtttributesProvider { + /* + * should i provide instance() method for convinience or should it be always injected into function + */ + + @Override + public CloudEventAttributes get(String ce_id, String ce_specversion, String ce_source, String ce_type) { + Assert.hasText(ce_id, "'ce_id' must not be null or empty"); + Assert.hasText(ce_specversion, "'ce_specversion' must not be null or empty"); + Assert.hasText(ce_source, "'ce_source' must not be null or empty"); + Assert.hasText(ce_type, "'ce_type' must not be null or empty"); + Map requiredAttributes = new HashMap<>(); + requiredAttributes.put(CloudEventMessageUtils.CE_ID, ce_id); + requiredAttributes.put(CloudEventMessageUtils.CE_SPECVERSION, ce_specversion); + requiredAttributes.put(CloudEventMessageUtils.CE_SOURCE, ce_source); + requiredAttributes.put(CloudEventMessageUtils.CE_TYPE, ce_type); + return new CloudEventAttributes(requiredAttributes); + } + + @Override + public CloudEventAttributes get(String ce_source, String ce_type) { + return this.get(UUID.randomUUID().toString(), "1.0", ce_source, ce_type); + } + + /** + * By default it will copy all the headers while exposing accessor to allow user to modify any of them. + */ + @Override + public RequiredAttributeAccessor get(MessageHeaders headers) { + return new RequiredAttributeAccessor(headers); + } + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/RequiredAttributeAccessor.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/RequiredAttributeAccessor.java new file mode 100644 index 000000000..c9b440188 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/RequiredAttributeAccessor.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020-2020 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 org.springframework.cloud.function.cloudevent; + +import java.util.Map; + +/** + * + * @author Oleg Zhurakousky + * @since 3.1 + */ +public class RequiredAttributeAccessor extends CloudEventAttributes { + + /** + * + */ + private static final long serialVersionUID = 859410409447601477L; + + RequiredAttributeAccessor(Map headers) { + super(headers); + } + + public RequiredAttributeAccessor setId(String id) { + this.put(CloudEventMessageUtils.CE_ID, id); + return this; + } + + public RequiredAttributeAccessor setSource(String source) { + this.put(CloudEventMessageUtils.CE_SOURCE, source); + return this; + } + + public RequiredAttributeAccessor setSpecversion(String specversion) { + this.put(CloudEventMessageUtils.CE_SPECVERSION, specversion); + return this; + } + + public RequiredAttributeAccessor setType(String type) { + this.put(CloudEventMessageUtils.CE_TYPE, type); + return this; + } +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java index 02fd37def..5418c896a 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java @@ -30,8 +30,10 @@ import com.google.gson.Gson; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.function.cloudevent.CloudEventAtttributesProvider; import org.springframework.cloud.function.cloudevent.CloudEventDataContentTypeMessagePreProcessor; import org.springframework.cloud.function.cloudevent.CloudEventJsonMessageConverter; +import org.springframework.cloud.function.cloudevent.DefaultCloudEventAttributesProvider; import org.springframework.cloud.function.context.FunctionCatalog; import org.springframework.cloud.function.context.FunctionProperties; import org.springframework.cloud.function.context.FunctionRegistry; @@ -72,6 +74,11 @@ public class ContextFunctionCatalogAutoConfiguration { static final String PREFERRED_MAPPER_PROPERTY = "spring.http.converters.preferred-json-mapper"; + @Bean + public CloudEventAtttributesProvider cloudEventAttributesProvider() { + return new DefaultCloudEventAttributesProvider(); + } + @Bean public FunctionRegistry functionCatalog(List messageConverters, JsonMapper jsonMapper, ConfigurableApplicationContext context) { ConfigurableConversionService conversionService = (ConfigurableConversionService) context.getBeanFactory().getConversionService(); diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/cloudevent/CloudEventJsonMessageConverterTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/cloudevent/CloudEventTypeConversionTests.java similarity index 72% rename from spring-cloud-function-context/src/test/java/org/springframework/cloud/function/cloudevent/CloudEventJsonMessageConverterTests.java rename to spring-cloud-function-context/src/test/java/org/springframework/cloud/function/cloudevent/CloudEventTypeConversionTests.java index 558e21192..88df74cd2 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/cloudevent/CloudEventJsonMessageConverterTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/cloudevent/CloudEventTypeConversionTests.java @@ -41,13 +41,15 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Oleg Zhurakousky * */ -public class CloudEventJsonMessageConverterTests { +public class CloudEventTypeConversionTests { @Test public void testFromMessageBinaryPayloadMatchesType() { SmartCompositeMessageConverter messageConverter = this.configure(DummyConfiguration.class); - Message message = MessageBuilder.withPayload("Hello Ricky").setHeader("ce_source", "https://spring.io/") - .setHeader("ce_id", UUID.randomUUID().toString()).setHeader("ce_type", "org.springframework") - .setHeader("ce_specversion", "1.0").setHeader("ce_datacontenttype", "text/plain").build(); + CloudEventAtttributesProvider ceAttrProvider = new DefaultCloudEventAttributesProvider(); + CloudEventAttributes ceAttributes = ceAttrProvider + .get(UUID.randomUUID().toString(), "1.0", "https://spring.io/", "org.springframework"); + ceAttributes.setDataContentType("text/plain"); + Message message = MessageBuilder.withPayload("Hello Ricky").copyHeaders(ceAttributes).build(); String converted = (String) messageConverter.fromMessage(message, String.class); assertThat(converted).isEqualTo("Hello Ricky"); @@ -56,12 +58,14 @@ public class CloudEventJsonMessageConverterTests { @Test public void testFromMessageBinaryPayloadDoesNotMatchType() { SmartCompositeMessageConverter messageConverter = this.configure(DummyConfiguration.class); + CloudEventAtttributesProvider ceAttrProvider = new DefaultCloudEventAttributesProvider(); + CloudEventAttributes ceAttributes = ceAttrProvider + .get(UUID.randomUUID().toString(), "1.0", "https://spring.io/", "org.springframework"); Message message = MessageBuilder.withPayload("Hello Ricky".getBytes()) + .copyHeaders(ceAttributes) .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.parseMimeType("application/cloudevents+json;charset=utf-8")) - .setHeader("ce_source", "https://spring.io/").setHeader("ce_id", UUID.randomUUID().toString()) - .setHeader("ce_type", "org.springframework").setHeader("ce_specversion", "1.0") - .setHeader("ce_datacontenttype", "text/plain").build(); + .build(); String converted = (String) messageConverter.fromMessage(message, String.class); assertThat(converted).isEqualTo("Hello Ricky"); } @@ -70,11 +74,14 @@ public class CloudEventJsonMessageConverterTests { // this works public void testFromMessageBinaryPayloadNoDataContentTypeToString() { SmartCompositeMessageConverter messageConverter = this.configure(DummyConfiguration.class); + CloudEventAtttributesProvider ceAttrProvider = new DefaultCloudEventAttributesProvider(); + CloudEventAttributes ceAttributes = ceAttrProvider + .get(UUID.randomUUID().toString(), "1.0", "https://spring.io/", "org.springframework"); Message message = MessageBuilder.withPayload("Hello Ricky".getBytes()) + .copyHeaders(ceAttributes) .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.parseMimeType("application/cloudevents+json;charset=utf-8")) - .setHeader("ce_source", "https://spring.io/").setHeader("ce_id", UUID.randomUUID().toString()) - .setHeader("ce_type", "org.springframework").setHeader("ce_specversion", "1.0").build(); + .build(); String converted = (String) messageConverter.fromMessage(message, String.class); assertThat(converted).isEqualTo("Hello Ricky"); } @@ -82,11 +89,13 @@ public class CloudEventJsonMessageConverterTests { @Test // Unlike the previous test the type here is POJO so no special treatement public void testFromMessageBinaryPayloadNoDataContentTypeToPOJO() { SmartCompositeMessageConverter messageConverter = this.configure(DummyConfiguration.class); + CloudEventAtttributesProvider ceAttrProvider = new DefaultCloudEventAttributesProvider(); + CloudEventAttributes ceAttributes = ceAttrProvider.get("https://spring.io/", "org.springframework"); Message message = MessageBuilder.withPayload("Hello Ricky".getBytes()) + .copyHeaders(ceAttributes) .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.parseMimeType("application/cloudevents+json;charset=utf-8")) - .setHeader("ce_source", "https://spring.io/").setHeader("ce_id", UUID.randomUUID().toString()) - .setHeader("ce_type", "org.springframework").setHeader("ce_specversion", "1.0").build(); + .build(); String converted = (String) messageConverter.fromMessage(message, Person.class); assertThat(converted).isNull(); } @@ -94,28 +103,35 @@ public class CloudEventJsonMessageConverterTests { @Test // will fall on default CT which is json public void testFromMessageBinaryPayloadNoDataContentTypeToPOJOThatWorks() { SmartCompositeMessageConverter messageConverter = this.configure(DummyConfiguration.class); + CloudEventAtttributesProvider ceAttrProvider = new DefaultCloudEventAttributesProvider(); + CloudEventAttributes ceAttributes = ceAttrProvider.get("https://spring.io/", "org.springframework"); Message message = MessageBuilder.withPayload("{\"name\":\"Ricky\"}".getBytes()) + .copyHeaders(ceAttributes) .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.parseMimeType("application/cloudevents+json;charset=utf-8")) - .setHeader("ce_source", "https://spring.io/").setHeader("ce_id", UUID.randomUUID().toString()) - .setHeader("ce_type", "org.springframework").setHeader("ce_specversion", "1.0").build(); + .build(); Person converted = (Person) messageConverter.fromMessage(message, Person.class); assertThat(converted.getName()).isEqualTo("Ricky"); } @Test // will fall on default CT which is json public void testFromMessageStructured() { - String cloudEventStructured = "{\n" + " \"specversion\" : \"1.0\",\n" - + " \"type\" : \"org.springframework\",\n" + " \"source\" : \"https://spring.io/\",\n" - + " \"id\" : \"A234-1234-1234\",\n" + " \"datacontenttype\" : \"application/json\",\n" - + " \"data\" : {\n" + " \"version\" : \"1.0\",\n" - + " \"releaseName\" : \"Spring Framework\",\n" + " \"releaseDate\" : \"24-03-2004\"\n" - + " }\n" + " }"; + String cloudEventStructured = "{\n" + + " \"specversion\" : \"1.0\",\n" + + " \"type\" : \"org.springframework\",\n" + + " \"source\" : \"https://spring.io/\",\n" + + " \"id\" : \"A234-1234-1234\",\n" + + " \"datacontenttype\" : \"application/json\",\n" + + " \"data\" : {\n" + + " \"version\" : \"1.0\",\n" + + " \"releaseName\" : \"Spring Framework\",\n" + + " \"releaseDate\" : \"24-03-2004\"\n" + + " }\n" + + " }"; SmartCompositeMessageConverter messageConverter = this.configure(DummyConfiguration.class); Message message = MessageBuilder.withPayload(cloudEventStructured) - .setHeader(MessageHeaders.CONTENT_TYPE, - MimeTypeUtils.parseMimeType("application/cloudevents+json;charset=utf-8")) - .setHeader("ce_datacontenttype", "application/json").build(); + .setHeader(MessageHeaders.CONTENT_TYPE, CloudEventMessageUtils.APPLICATION_CLOUDEVENTS_VALUE + "+json") + .setHeader(CloudEventMessageUtils.CE_DATACONTENTTYPE, MimeTypeUtils.APPLICATION_JSON_VALUE).build(); SpringReleaseEvent springReleaseEvent = (SpringReleaseEvent) messageConverter.fromMessage(message, SpringReleaseEvent.class); assertThat(springReleaseEvent.getReleaseName()).isEqualTo("Spring Framework"); diff --git a/spring-cloud-function-samples/function-sample-cloudevent/src/main/java/io/spring/cloudevent/CloudeventDemoApplication.java b/spring-cloud-function-samples/function-sample-cloudevent/src/main/java/io/spring/cloudevent/CloudeventDemoApplication.java index c50848174..02b0964f1 100644 --- a/spring-cloud-function-samples/function-sample-cloudevent/src/main/java/io/spring/cloudevent/CloudeventDemoApplication.java +++ b/spring-cloud-function-samples/function-sample-cloudevent/src/main/java/io/spring/cloudevent/CloudeventDemoApplication.java @@ -20,8 +20,12 @@ import java.util.function.Function; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.function.cloudevent.CloudEventAttributes; +import org.springframework.cloud.function.cloudevent.CloudEventAtttributesProvider; +import org.springframework.cloud.function.cloudevent.DefaultCloudEventAttributesProvider; import org.springframework.context.annotation.Bean; import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; /** * Sample application that demonstrates how user functions can be triggered by cloud event. @@ -77,4 +81,38 @@ public class CloudeventDemoApplication { return v.toString(); }; } + + @Bean + public Function, Message> consumeAndProduceCloudEvent(CloudEventAtttributesProvider ceAttrProvider) { + return ceMessage -> { + SpringReleaseEvent data = ceMessage.getPayload(); + data.setVersion("2.0"); + data.setReleaseDateAsString("01-10-2006"); + + CloudEventAttributes ceAttributes = ceAttrProvider.get(ceMessage.getHeaders()) + .setSource("https://interface21.icom/") + .setType("com.interface21"); + + return MessageBuilder.withPayload(data).copyHeaders(ceAttributes).build(); + }; + } + +// // spring.io/applicationName +// +// @Bean +// public Function, SpringReleaseEvent> consumeAndProduceCloudEvent() { +// return ceMessage -> { +// SpringReleaseEvent data = ceMessage.getPayload(); +// data.setVersion("2.0"); +// data.setReleaseDateAsString("01-10-2006"); +// +// CloudEventAtttributesProvider ceAttrProvider = new DefaultCloudEventAttributesProvider(); +// +// CloudEventAttributes ceAttributes = ceAttrProvider.get(ceMessage.getHeaders()) +// .setSource("https://interface21.icom/") +// .setType("com.interface21"); +// +// return MessageBuilder.withPayload(data).copyHeaders(ceAttributes).build(); +// }; +// } } diff --git a/spring-cloud-function-samples/function-sample-cloudevent/src/main/java/io/spring/cloudevent/SpringReleaseEvent.java b/spring-cloud-function-samples/function-sample-cloudevent/src/main/java/io/spring/cloudevent/SpringReleaseEvent.java index 4d3c71fdb..2bf869554 100644 --- a/spring-cloud-function-samples/function-sample-cloudevent/src/main/java/io/spring/cloudevent/SpringReleaseEvent.java +++ b/spring-cloud-function-samples/function-sample-cloudevent/src/main/java/io/spring/cloudevent/SpringReleaseEvent.java @@ -16,6 +16,7 @@ package io.spring.cloudevent; +import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; @@ -44,6 +45,15 @@ public class SpringReleaseEvent { this.releaseDate = releaseDate; } + public void setReleaseDateAsString(String releaseDate) { + try { + this.releaseDate = new SimpleDateFormat("dd-MM-yyyy").parse(releaseDate); + } + catch (ParseException e) { + throw new IllegalArgumentException(e); + } + } + public String getReleaseName() { return releaseName; } diff --git a/spring-cloud-function-samples/function-sample-cloudevent/src/test/java/io/spring/cloudevent/CloudeventDemoApplicationFunctionTests.java b/spring-cloud-function-samples/function-sample-cloudevent/src/test/java/io/spring/cloudevent/CloudeventDemoApplicationFunctionTests.java index 21053b6e4..c20062e19 100644 --- a/spring-cloud-function-samples/function-sample-cloudevent/src/test/java/io/spring/cloudevent/CloudeventDemoApplicationFunctionTests.java +++ b/spring-cloud-function-samples/function-sample-cloudevent/src/test/java/io/spring/cloudevent/CloudeventDemoApplicationFunctionTests.java @@ -20,8 +20,10 @@ import java.util.function.Function; import org.junit.jupiter.api.Test; import org.springframework.boot.SpringApplication; +import org.springframework.cloud.function.cloudevent.CloudEventAtttributesProvider; +import org.springframework.cloud.function.cloudevent.DefaultCloudEventAttributesProvider; import org.springframework.cloud.function.context.FunctionCatalog; -import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; @@ -34,31 +36,51 @@ public class CloudeventDemoApplicationFunctionTests { @Test public void demoPureFunctionInvocation() { - ApplicationContext context = SpringApplication.run(CloudeventDemoApplication.class); - FunctionCatalog catalog = context.getBean(FunctionCatalog.class); - Message binaryCloudEventMessage = MessageBuilder - .withPayload("{\"releaseDate\":\"24-03-2004\", \"releaseName\":\"Spring Framework\", \"version\":\"1.0\"}") - .setHeader("ce-specversion", "1.0") - .setHeader("ce-type", "com.example.springevent") - .setHeader("ce-source", "spring.io/spring-event") - .setHeader("ce-id", "123-456-9876-09") - .build(); - /* - * NOTE how it makes no difference what the actual function signature - * is (see `asPOJOMessage` and `asPOJO` specifically). Type conversion will happen - * inside spring-cloud-function. - */ - Function, String> asPojoMessage = catalog.lookup("asPOJOMessage"); - System.out.println(asPojoMessage.apply(binaryCloudEventMessage)); + try(ConfigurableApplicationContext context = SpringApplication.run(CloudeventDemoApplication.class)) { + FunctionCatalog catalog = context.getBean(FunctionCatalog.class); + CloudEventAtttributesProvider ceAttrProvider = new DefaultCloudEventAttributesProvider(); + Message binaryCloudEventMessage = MessageBuilder + .withPayload("{\"releaseDate\":\"24-03-2004\", \"releaseName\":\"Spring Framework\", \"version\":\"1.0\"}") + .copyHeaders(ceAttrProvider.get("spring.io/spring-event", "com.example.springevent")) + .build(); - Function, String> asPojo = catalog.lookup("asPOJO"); - System.out.println(asPojo.apply(binaryCloudEventMessage)); + /* + * NOTE how it makes no difference what the actual function signature + * is (see `asPOJOMessage` and `asPOJO` specifically). Type conversion will happen + * inside spring-cloud-function. + */ + Function, String> asPojoMessage = catalog.lookup("asPOJOMessage"); + System.out.println(asPojoMessage.apply(binaryCloudEventMessage)); - Function, String> asString = catalog.lookup("asString"); - System.out.println(asString.apply(binaryCloudEventMessage)); + Function, String> asPojo = catalog.lookup("asPOJO"); + System.out.println(asPojo.apply(binaryCloudEventMessage)); - Function, String> asStringMessage = catalog.lookup("asStringMessage"); - System.out.println(asStringMessage.apply(binaryCloudEventMessage)); + Function, String> asString = catalog.lookup("asString"); + System.out.println(asString.apply(binaryCloudEventMessage)); + + Function, String> asStringMessage = catalog.lookup("asStringMessage"); + System.out.println(asStringMessage.apply(binaryCloudEventMessage)); + } + } + + @Test + public void demoPureFunctionProduceConsumeCloudEvent() { + try(ConfigurableApplicationContext context = SpringApplication.run(CloudeventDemoApplication.class)) { + FunctionCatalog catalog = context.getBean(FunctionCatalog.class); + CloudEventAtttributesProvider ceAttrProvider = new DefaultCloudEventAttributesProvider(); + Message binaryCloudEventMessage = MessageBuilder + .withPayload("{\"releaseDate\":\"24-03-2004\", \"releaseName\":\"Spring Framework\", \"version\":\"1.0\"}") + .copyHeaders(ceAttrProvider.get("spring.io/spring-event", "com.example.springevent")) + .build(); + + /* + * NOTE how it makes no difference what the actual function signature + * is (see `asPOJOMessage` and `asPOJO` specifically). Type conversion will happen + * inside spring-cloud-function. + */ + Function, Message> asPojoMessage = catalog.lookup("consumeAndProduceCloudEvent"); + System.out.println(asPojoMessage.apply(binaryCloudEventMessage)); + } } }