From 55fddb35fcde565282e088a554e6c4850222790d Mon Sep 17 00:00:00 2001 From: Matej Vasek Date: Thu, 8 Feb 2024 09:49:19 +0100 Subject: [PATCH] feat: JSON format to assume JSON content-type where possible (#604) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matej VaĊĦek --- .../jackson/CloudEventDeserializer.java | 17 +++-- .../io/cloudevents/jackson/JsonFormat.java | 2 +- .../jackson/JsonFormatOptions.java | 18 ++++- .../jackson/CloudEventDeserializerTest.java | 69 +++++++++++++++++++ 4 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 formats/json-jackson/src/test/java/io/cloudevents/jackson/CloudEventDeserializerTest.java diff --git a/formats/json-jackson/src/main/java/io/cloudevents/jackson/CloudEventDeserializer.java b/formats/json-jackson/src/main/java/io/cloudevents/jackson/CloudEventDeserializer.java index 53fc48215..364ae19ad 100644 --- a/formats/json-jackson/src/main/java/io/cloudevents/jackson/CloudEventDeserializer.java +++ b/formats/json-jackson/src/main/java/io/cloudevents/jackson/CloudEventDeserializer.java @@ -42,18 +42,21 @@ class CloudEventDeserializer extends StdDeserializer { private final boolean forceExtensionNameLowerCaseDeserialization; private final boolean forceIgnoreInvalidExtensionNameDeserialization; + private final boolean disableDataContentTypeDefaulting; protected CloudEventDeserializer() { - this(false, false); + this(false, false, false); } protected CloudEventDeserializer( boolean forceExtensionNameLowerCaseDeserialization, - boolean forceIgnoreInvalidExtensionNameDeserialization + boolean forceIgnoreInvalidExtensionNameDeserialization, + boolean disableDataContentTypeDefaulting ) { super(CloudEvent.class); this.forceExtensionNameLowerCaseDeserialization = forceExtensionNameLowerCaseDeserialization; this.forceIgnoreInvalidExtensionNameDeserialization = forceIgnoreInvalidExtensionNameDeserialization; + this.disableDataContentTypeDefaulting = disableDataContentTypeDefaulting; } private static class JsonMessage implements CloudEventReader { @@ -62,17 +65,20 @@ private static class JsonMessage implements CloudEventReader { private final ObjectNode node; private final boolean forceExtensionNameLowerCaseDeserialization; private final boolean forceIgnoreInvalidExtensionNameDeserialization; + private final boolean disableDataContentTypeDefaulting; public JsonMessage( JsonParser p, ObjectNode node, boolean forceExtensionNameLowerCaseDeserialization, - boolean forceIgnoreInvalidExtensionNameDeserialization + boolean forceIgnoreInvalidExtensionNameDeserialization, + boolean disableDataContentTypeDefaulting ) { this.p = p; this.node = node; this.forceExtensionNameLowerCaseDeserialization = forceExtensionNameLowerCaseDeserialization; this.forceIgnoreInvalidExtensionNameDeserialization = forceIgnoreInvalidExtensionNameDeserialization; + this.disableDataContentTypeDefaulting = disableDataContentTypeDefaulting; } @Override @@ -92,6 +98,9 @@ public , V> V read(CloudEventWriterFactory w // Parse datacontenttype if any String contentType = getOptionalStringNode(this.node, this.p, "datacontenttype"); + if (!this.disableDataContentTypeDefaulting && contentType == null && this.node.has("data")) { + contentType = "application/json"; + } if (contentType != null) { writer.withContextAttribute("datacontenttype", contentType); } @@ -257,7 +266,7 @@ public CloudEvent deserialize(JsonParser p, DeserializationContext ctxt) throws ObjectNode node = ctxt.readValue(p, ObjectNode.class); try { - return new JsonMessage(p, node, this.forceExtensionNameLowerCaseDeserialization, this.forceIgnoreInvalidExtensionNameDeserialization) + return new JsonMessage(p, node, this.forceExtensionNameLowerCaseDeserialization, this.forceIgnoreInvalidExtensionNameDeserialization, this.disableDataContentTypeDefaulting) .read(CloudEventBuilder::fromSpecVersion); } catch (RuntimeException e) { // Yeah this is bad but it's needed to support checked exceptions... diff --git a/formats/json-jackson/src/main/java/io/cloudevents/jackson/JsonFormat.java b/formats/json-jackson/src/main/java/io/cloudevents/jackson/JsonFormat.java index d73edbdf9..cddbaf802 100644 --- a/formats/json-jackson/src/main/java/io/cloudevents/jackson/JsonFormat.java +++ b/formats/json-jackson/src/main/java/io/cloudevents/jackson/JsonFormat.java @@ -213,7 +213,7 @@ public static SimpleModule getCloudEventJacksonModule(JsonFormatOptions options) ceModule.addSerializer(CloudEvent.class, new CloudEventSerializer( options.isForceDataBase64Serialization(), options.isForceStringSerialization())); ceModule.addDeserializer(CloudEvent.class, new CloudEventDeserializer( - options.isForceExtensionNameLowerCaseDeserialization(), options.isForceIgnoreInvalidExtensionNameDeserialization())); + options.isForceExtensionNameLowerCaseDeserialization(), options.isForceIgnoreInvalidExtensionNameDeserialization(), options.isDataContentTypeDefaultingDisabled())); return ceModule; } diff --git a/formats/json-jackson/src/main/java/io/cloudevents/jackson/JsonFormatOptions.java b/formats/json-jackson/src/main/java/io/cloudevents/jackson/JsonFormatOptions.java index bf18ebf31..230d9ebc6 100644 --- a/formats/json-jackson/src/main/java/io/cloudevents/jackson/JsonFormatOptions.java +++ b/formats/json-jackson/src/main/java/io/cloudevents/jackson/JsonFormatOptions.java @@ -21,24 +21,27 @@ public final class JsonFormatOptions { private final boolean forceStringSerialization; private final boolean forceExtensionNameLowerCaseDeserialization; private final boolean forceIgnoreInvalidExtensionNameDeserialization; + private final boolean disableDataContentTypeDefaulting; /** * Create a new instance of this class options the serialization / deserialization. */ public JsonFormatOptions() { - this(false, false, false, false); + this(false, false, false, false, false); } JsonFormatOptions( boolean forceDataBase64Serialization, boolean forceStringSerialization, boolean forceExtensionNameLowerCaseDeserialization, - boolean forceIgnoreInvalidExtensionNameDeserialization + boolean forceIgnoreInvalidExtensionNameDeserialization, + boolean disableDataContentTypeDefaulting ) { this.forceDataBase64Serialization = forceDataBase64Serialization; this.forceStringSerialization = forceStringSerialization; this.forceExtensionNameLowerCaseDeserialization = forceExtensionNameLowerCaseDeserialization; this.forceIgnoreInvalidExtensionNameDeserialization = forceIgnoreInvalidExtensionNameDeserialization; + this.disableDataContentTypeDefaulting = disableDataContentTypeDefaulting; } public static JsonFormatOptionsBuilder builder() { @@ -61,11 +64,14 @@ public boolean isForceIgnoreInvalidExtensionNameDeserialization() { return this.forceIgnoreInvalidExtensionNameDeserialization; } + public boolean isDataContentTypeDefaultingDisabled() { return this.disableDataContentTypeDefaulting; } + public static class JsonFormatOptionsBuilder { private boolean forceDataBase64Serialization = false; private boolean forceStringSerialization = false; private boolean forceExtensionNameLowerCaseDeserialization = false; private boolean forceIgnoreInvalidExtensionNameDeserialization = false; + private boolean disableDataContentTypeDefaulting = false; public JsonFormatOptionsBuilder forceDataBase64Serialization(boolean forceDataBase64Serialization) { this.forceDataBase64Serialization = forceDataBase64Serialization; @@ -87,12 +93,18 @@ public JsonFormatOptionsBuilder forceIgnoreInvalidExtensionNameDeserialization(b return this; } + public JsonFormatOptionsBuilder disableDataContentTypeDefaulting(boolean disableDataContentTypeDefaulting) { + this.disableDataContentTypeDefaulting = disableDataContentTypeDefaulting; + return this; + } + public JsonFormatOptions build() { return new JsonFormatOptions( this.forceDataBase64Serialization, this.forceStringSerialization, this.forceExtensionNameLowerCaseDeserialization, - this.forceIgnoreInvalidExtensionNameDeserialization + this.forceIgnoreInvalidExtensionNameDeserialization, + this.disableDataContentTypeDefaulting ); } } diff --git a/formats/json-jackson/src/test/java/io/cloudevents/jackson/CloudEventDeserializerTest.java b/formats/json-jackson/src/test/java/io/cloudevents/jackson/CloudEventDeserializerTest.java new file mode 100644 index 000000000..be8f5f418 --- /dev/null +++ b/formats/json-jackson/src/test/java/io/cloudevents/jackson/CloudEventDeserializerTest.java @@ -0,0 +1,69 @@ +package io.cloudevents.jackson; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import io.cloudevents.CloudEvent; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.StringReader; + +import static io.cloudevents.jackson.JsonFormat.getCloudEventJacksonModule; +import static org.assertj.core.api.Assertions.assertThat; + +public class CloudEventDeserializerTest { + + private static final String nonBinaryPayload = "{\n" + + " \"specversion\" : \"1.0\",\n" + + " \"type\" : \"com.example.someevent\",\n" + + " \"source\" : \"/mycontext\",\n" + + " \"subject\": null,\n" + + " \"id\" : \"D234-1234-1234\",\n" + + " \"time\" : \"2018-04-05T17:31:00Z\",\n" + + " \"comexampleextension1\" : \"value\",\n" + + " \"comexampleothervalue\" : 5,\n" + + " \"data\" : \"I'm just a string\"\n" + + "}"; + + private static final String binaryPayload = "{\n" + + " \"specversion\" : \"1.0\",\n" + + " \"type\" : \"com.example.someevent\",\n" + + " \"source\" : \"/mycontext\",\n" + + " \"id\" : \"D234-1234-1234\",\n" + + " \"data_base64\" : \"eyAieHl6IjogMTIzIH0=\"\n" + + "}"; + + @Test + void impliedDataContentTypeNonBinaryData() throws IOException { + ObjectMapper mapper = getObjectMapper(false); + StringReader reader = new StringReader(nonBinaryPayload); + CloudEvent ce = mapper.readValue(reader, CloudEvent.class); + assertThat(ce.getDataContentType()).isEqualTo("application/json"); + + mapper = getObjectMapper(true); + reader = new StringReader(nonBinaryPayload); + ce = mapper.readValue(reader, CloudEvent.class); + assertThat(ce.getDataContentType()).isNull(); + } + + @Test + void impliedDataContentTypeBinaryData() throws IOException { + final ObjectMapper mapper = getObjectMapper(false); + StringReader reader = new StringReader(binaryPayload); + CloudEvent ce = mapper.readValue(reader, CloudEvent.class); + assertThat(ce.getDataContentType()).isNull(); + } + + private static ObjectMapper getObjectMapper(boolean disableDataContentTypeDefaulting) { + final ObjectMapper mapper = new ObjectMapper(); + final SimpleModule module = getCloudEventJacksonModule( + JsonFormatOptions + .builder() + .disableDataContentTypeDefaulting(disableDataContentTypeDefaulting) + .build() + ); + mapper.registerModule(module); + return mapper; + } + +}