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 b5ad5a644..020cd1c90 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 @@ -38,19 +38,35 @@ * Jackson {@link com.fasterxml.jackson.databind.JsonDeserializer} for {@link CloudEvent} */ class CloudEventDeserializer extends StdDeserializer { + private final boolean forceExtensionNameLowerCaseDeserialization; + private final boolean forceIgnoreInvalidExtensionNameDeserialization; - protected CloudEventDeserializer() { + protected CloudEventDeserializer( + boolean forceExtensionNameLowerCaseDeserialization, + boolean forceIgnoreInvalidExtensionNameDeserialization + ) { super(CloudEvent.class); + this.forceExtensionNameLowerCaseDeserialization = forceExtensionNameLowerCaseDeserialization; + this.forceIgnoreInvalidExtensionNameDeserialization = forceIgnoreInvalidExtensionNameDeserialization; } private static class JsonMessage implements CloudEventReader { private final JsonParser p; private final ObjectNode node; - - public JsonMessage(JsonParser p, ObjectNode node) { + private final boolean forceExtensionNameLowerCaseDeserialization; + private final boolean forceIgnoreInvalidExtensionNameDeserialization; + + public JsonMessage( + JsonParser p, + ObjectNode node, + boolean forceExtensionNameLowerCaseDeserialization, + boolean forceIgnoreInvalidExtensionNameDeserialization + ) { this.p = p; this.node = node; + this.forceExtensionNameLowerCaseDeserialization = forceExtensionNameLowerCaseDeserialization; + this.forceIgnoreInvalidExtensionNameDeserialization = forceIgnoreInvalidExtensionNameDeserialization; } @Override @@ -126,10 +142,12 @@ public , V> V read(CloudEventWriterFactory w // Now let's process the extensions node.fields().forEachRemaining(entry -> { - String extensionName = entry.getKey().toLowerCase(); + String extensionName = entry.getKey(); + if (this.forceExtensionNameLowerCaseDeserialization) { + extensionName = extensionName.toLowerCase(); + } - // ignore not valid extension name - if (!this.isValidExtensionName(extensionName)) { + if (this.shouldSkipExtensionName(extensionName)) { return; } @@ -199,6 +217,11 @@ private void assertNodeType(JsonNode node, JsonNodeType type, String attributeNa } } + // ignore not valid extension name + private boolean shouldSkipExtensionName(String extensionName) { + return this.forceIgnoreInvalidExtensionNameDeserialization && !this.isValidExtensionName(extensionName); + } + /** * Validates the extension name as defined in CloudEvents spec. * @@ -228,7 +251,8 @@ public CloudEvent deserialize(JsonParser p, DeserializationContext ctxt) throws ObjectNode node = ctxt.readValue(p, ObjectNode.class); try { - return new JsonMessage(p, node).read(CloudEventBuilder::fromSpecVersion); + return new JsonMessage(p, node, this.forceExtensionNameLowerCaseDeserialization, this.forceIgnoreInvalidExtensionNameDeserialization) + .read(CloudEventBuilder::fromSpecVersion); } catch (RuntimeException e) { // Yeah this is bad but it's needed to support checked exceptions... if (e.getCause() instanceof IOException) { 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 a374b6c1e..220b94123 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 @@ -35,7 +35,7 @@ * using Jackson. This format is resolvable with {@link io.cloudevents.core.provider.EventFormatProvider} using the content type {@link #CONTENT_TYPE}. *

* If you want to use the {@link CloudEvent} serializers/deserializers directly in your mapper, you can use {@link #getCloudEventJacksonModule()} or - * {@link #getCloudEventJacksonModule(boolean, boolean)} to get a {@link SimpleModule} to register in your {@link ObjectMapper} instance. + * {@link #getCloudEventJacksonModule(boolean, boolean, boolean, boolean)} to get a {@link SimpleModule} to register in your {@link ObjectMapper} instance. */ public final class JsonFormat implements EventFormat { @@ -47,41 +47,95 @@ public final class JsonFormat implements EventFormat { private final ObjectMapper mapper; private final boolean forceDataBase64Serialization; private final boolean forceStringSerialization; + private final boolean forceExtensionNameLowerCaseDeserialization; + private final boolean forceIgnoreInvalidExtensionNameDeserialization; /** * Create a new instance of this class customizing the serialization configuration. * * @param forceDataBase64Serialization force json base64 encoding for data * @param forceStringSerialization force string serialization for non json data field + * @param forceExtensionNameLowerCaseDeserialization force extension name deserialization for lower case + * @param forceIgnoreInvalidExtensionNameDeserialization force extension name deserialization for ignoring invalid name * @see #withForceJsonDataToBase64() * @see #withForceNonJsonDataToString() + * @see #withForceExtensionNameLowerCaseDeserialization() + * @see #withForceIgnoreInvalidExtensionNameDeserialization() */ - public JsonFormat(boolean forceDataBase64Serialization, boolean forceStringSerialization) { + public JsonFormat( + boolean forceDataBase64Serialization, + boolean forceStringSerialization, + boolean forceExtensionNameLowerCaseDeserialization, + boolean forceIgnoreInvalidExtensionNameDeserialization + ) { this.mapper = new ObjectMapper(); - this.mapper.registerModule(getCloudEventJacksonModule(forceDataBase64Serialization, forceStringSerialization)); + this.mapper.registerModule( + getCloudEventJacksonModule( + forceDataBase64Serialization, + forceStringSerialization, + forceExtensionNameLowerCaseDeserialization, + forceIgnoreInvalidExtensionNameDeserialization + ) + ); this.forceDataBase64Serialization = forceDataBase64Serialization; this.forceStringSerialization = forceStringSerialization; + this.forceExtensionNameLowerCaseDeserialization = forceExtensionNameLowerCaseDeserialization; + this.forceIgnoreInvalidExtensionNameDeserialization = forceIgnoreInvalidExtensionNameDeserialization; } /** * Create a new instance of this class with default serialization configuration */ public JsonFormat() { - this(false, false); + this(false, false, false, false); } /** * @return a copy of this JsonFormat that serialize events with json data with Base64 encoding */ public JsonFormat withForceJsonDataToBase64() { - return new JsonFormat(true, this.forceStringSerialization); + return new JsonFormat( + true, + this.forceStringSerialization, + this.forceExtensionNameLowerCaseDeserialization, + this.forceIgnoreInvalidExtensionNameDeserialization + ); } /** * @return a copy of this JsonFormat that serialize events with non-json data as string */ public JsonFormat withForceNonJsonDataToString() { - return new JsonFormat(this.forceDataBase64Serialization, true); + return new JsonFormat( + this.forceDataBase64Serialization, + true, + this.forceExtensionNameLowerCaseDeserialization, + this.forceIgnoreInvalidExtensionNameDeserialization + ); + } + + /** + * @return a copy of this JsonFormat that deserialize events with converting extension name lower case. + */ + public JsonFormat withForceExtensionNameLowerCaseDeserialization() { + return new JsonFormat( + this.forceDataBase64Serialization, + this.forceStringSerialization, + true, + this.forceIgnoreInvalidExtensionNameDeserialization + ); + } + + /** + * @return a copy of this JsonFormat that deserialize events with ignoring invalid extension name + */ + public JsonFormat withForceIgnoreInvalidExtensionNameDeserialization() { + return new JsonFormat( + this.forceDataBase64Serialization, + this.forceStringSerialization, + this.forceExtensionNameLowerCaseDeserialization, + true + ); } @Override @@ -126,20 +180,30 @@ public String serializedContentType() { * @return a {@link SimpleModule} with {@link CloudEvent} serializer/deserializer configured using default values. */ public static SimpleModule getCloudEventJacksonModule() { - return getCloudEventJacksonModule(false, false); + return getCloudEventJacksonModule(false, false, false, false); } /** * @param forceDataBase64Serialization force json base64 encoding for data * @param forceStringSerialization force string serialization for non json data field + * @param forceExtensionNameLowerCaseDeserialization force extension name deserialization for lower case + * @param forceIgnoreInvalidExtensionNameDeserialization force extension name deserialization for ignoring invalid name * @return a JacksonModule with CloudEvent serializer/deserializer customizing the data serialization. * @see #withForceJsonDataToBase64() * @see #withForceNonJsonDataToString() + * @see #withForceExtensionNameLowerCaseDeserialization() + * @see #withForceIgnoreInvalidExtensionNameDeserialization() */ - public static SimpleModule getCloudEventJacksonModule(boolean forceDataBase64Serialization, boolean forceStringSerialization) { + public static SimpleModule getCloudEventJacksonModule( + boolean forceDataBase64Serialization, + boolean forceStringSerialization, + boolean forceExtensionNameLowerCaseDeserialization, + boolean forceIgnoreInvalidExtensionNameDeserialization + ) { final SimpleModule ceModule = new SimpleModule("CloudEvent"); ceModule.addSerializer(CloudEvent.class, new CloudEventSerializer(forceDataBase64Serialization, forceStringSerialization)); - ceModule.addDeserializer(CloudEvent.class, new CloudEventDeserializer()); + ceModule.addDeserializer(CloudEvent.class, new CloudEventDeserializer( + forceExtensionNameLowerCaseDeserialization, forceIgnoreInvalidExtensionNameDeserialization)); return ceModule; } diff --git a/formats/json-jackson/src/test/java/io/cloudevents/jackson/JsonFormatTest.java b/formats/json-jackson/src/test/java/io/cloudevents/jackson/JsonFormatTest.java index de99403de..318cffcc5 100644 --- a/formats/json-jackson/src/test/java/io/cloudevents/jackson/JsonFormatTest.java +++ b/formats/json-jackson/src/test/java/io/cloudevents/jackson/JsonFormatTest.java @@ -27,14 +27,12 @@ import io.cloudevents.core.format.EventDeserializationException; import io.cloudevents.core.provider.EventFormatProvider; import io.cloudevents.rw.CloudEventRWException; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import java.io.IOException; -import java.math.BigInteger; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -90,6 +88,22 @@ void deserialize(String inputFile, CloudEvent output) { .isEqualTo(output); } + @ParameterizedTest + @MethodSource("deserializeTestArgumentsUpperCaseExtensionName") + void deserializeWithUpperCaseExtensionName(String inputFile, CloudEvent output) { + CloudEvent deserialized = getFormat().withForceExtensionNameLowerCaseDeserialization().deserialize(loadFile(inputFile)); + assertThat(deserialized) + .isEqualTo(output); + } + + @ParameterizedTest + @MethodSource("deserializeTestArgumentsInvalidExtensionName") + void deserializeWithInvalidExtensionName(String inputFile, CloudEvent output) { + CloudEvent deserialized = getFormat().withForceIgnoreInvalidExtensionNameDeserialization().deserialize(loadFile(inputFile)); + assertThat(deserialized) + .isEqualTo(output); + } + @ParameterizedTest @MethodSource("roundTripTestArguments") void jsonRoundTrip(String inputFile) throws IOException { @@ -204,6 +218,20 @@ public static Stream deserializeTestArguments() { ); } + public static Stream deserializeTestArgumentsUpperCaseExtensionName() { + return Stream.of( + Arguments.of("v03/json_data_with_ext_upper_case.json", normalizeToJsonValueIfNeeded(V03_WITH_JSON_DATA_WITH_EXT)), + Arguments.of("v1/json_data_with_ext_upper_case.json", normalizeToJsonValueIfNeeded(V1_WITH_JSON_DATA_WITH_EXT)) + ); + } + + public static Stream deserializeTestArgumentsInvalidExtensionName() { + return Stream.of( + Arguments.of("v03/json_data_with_ext_invalid.json", normalizeToJsonValueIfNeeded(V03_WITH_JSON_DATA_WITH_EXT)), + Arguments.of("v1/json_data_with_ext_invalid.json", normalizeToJsonValueIfNeeded(V1_WITH_JSON_DATA_WITH_EXT)) + ); + } + public static Stream roundTripTestArguments() { return Stream.of( "v03/min.json", diff --git a/formats/json-jackson/src/test/resources/v03/json_data_with_ext_invalid.json b/formats/json-jackson/src/test/resources/v03/json_data_with_ext_invalid.json new file mode 100644 index 000000000..a55bfae32 --- /dev/null +++ b/formats/json-jackson/src/test/resources/v03/json_data_with_ext_invalid.json @@ -0,0 +1,15 @@ +{ + "specversion": "0.3", + "id": "1", + "type": "mock.test", + "source": "http://localhost/source", + "schemaurl": "http://localhost/schema", + "datacontenttype": "application/json", + "data": {}, + "subject": "sub", + "time": "2018-04-26T14:48:09+02:00", + "astring": "aaa", + "aboolean": true, + "anumber": 10, + "a_invalid_name": "invalidName" +} diff --git a/formats/json-jackson/src/test/resources/v03/json_data_with_ext_upper_case.json b/formats/json-jackson/src/test/resources/v03/json_data_with_ext_upper_case.json new file mode 100644 index 000000000..1cf194184 --- /dev/null +++ b/formats/json-jackson/src/test/resources/v03/json_data_with_ext_upper_case.json @@ -0,0 +1,14 @@ +{ + "specversion": "0.3", + "id": "1", + "type": "mock.test", + "source": "http://localhost/source", + "schemaurl": "http://localhost/schema", + "datacontenttype": "application/json", + "data": {}, + "subject": "sub", + "time": "2018-04-26T14:48:09+02:00", + "aString": "aaa", + "aBoolean": true, + "aNumber": 10 +} diff --git a/formats/json-jackson/src/test/resources/v1/json_data_with_ext_invalid.json b/formats/json-jackson/src/test/resources/v1/json_data_with_ext_invalid.json new file mode 100644 index 000000000..4eda33f05 --- /dev/null +++ b/formats/json-jackson/src/test/resources/v1/json_data_with_ext_invalid.json @@ -0,0 +1,15 @@ +{ + "specversion": "1.0", + "id": "1", + "type": "mock.test", + "source": "http://localhost/source", + "dataschema": "http://localhost/schema", + "datacontenttype": "application/json", + "data": {}, + "subject": "sub", + "time": "2018-04-26T14:48:09+02:00", + "astring": "aaa", + "aboolean": true, + "anumber": 10, + "a_invalid_name": "invalidName" +} diff --git a/formats/json-jackson/src/test/resources/v1/json_data_with_ext_upper_case.json b/formats/json-jackson/src/test/resources/v1/json_data_with_ext_upper_case.json new file mode 100644 index 000000000..3bcc44817 --- /dev/null +++ b/formats/json-jackson/src/test/resources/v1/json_data_with_ext_upper_case.json @@ -0,0 +1,14 @@ +{ + "specversion": "1.0", + "id": "1", + "type": "mock.test", + "source": "http://localhost/source", + "dataschema": "http://localhost/schema", + "datacontenttype": "application/json", + "data": {}, + "subject": "sub", + "time": "2018-04-26T14:48:09+02:00", + "aString": "aaa", + "aBoolean": true, + "aNumber": 10 +}