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 87d9a9196..1383d7cb1 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,39 @@ * Jackson {@link com.fasterxml.jackson.databind.JsonDeserializer} for {@link CloudEvent} */ class CloudEventDeserializer extends StdDeserializer { + private final boolean forceExtensionNameLowerCaseDeserialization; + private final boolean forceIgnoreInvalidExtensionNameDeserialization; protected CloudEventDeserializer() { + this(false, false); + } + + 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; + private final boolean forceExtensionNameLowerCaseDeserialization; + private final boolean forceIgnoreInvalidExtensionNameDeserialization; - public JsonMessage(JsonParser p, ObjectNode node) { + public JsonMessage( + JsonParser p, + ObjectNode node, + boolean forceExtensionNameLowerCaseDeserialization, + boolean forceIgnoreInvalidExtensionNameDeserialization + ) { this.p = p; this.node = node; + this.forceExtensionNameLowerCaseDeserialization = forceExtensionNameLowerCaseDeserialization; + this.forceIgnoreInvalidExtensionNameDeserialization = forceIgnoreInvalidExtensionNameDeserialization; } @Override @@ -127,6 +147,14 @@ public , V> V read(CloudEventWriterFactory w // Now let's process the extensions node.fields().forEachRemaining(entry -> { String extensionName = entry.getKey(); + if (this.forceExtensionNameLowerCaseDeserialization) { + extensionName = extensionName.toLowerCase(); + } + + if (this.shouldSkipExtensionName(extensionName)) { + return; + } + JsonNode extensionValue = entry.getValue(); switch (extensionValue.getNodeType()) { @@ -192,6 +220,32 @@ 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. + * + * @param name the extension name + * @return true if extension name is valid, false otherwise + * @see attribute-naming-convention + */ + private boolean isValidExtensionName(String name) { + for (int i = 0; i < name.length(); i++) { + if (!isValidChar(name.charAt(i))) { + return false; + } + } + return true; + } + + private boolean isValidChar(char c) { + return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'); + } + } @Override @@ -201,7 +255,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..49467980b 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 @@ -45,8 +45,7 @@ public final class JsonFormat implements EventFormat { public static final String CONTENT_TYPE = "application/cloudevents+json"; private final ObjectMapper mapper; - private final boolean forceDataBase64Serialization; - private final boolean forceStringSerialization; + private final JsonFormatOptions options; /** * Create a new instance of this class customizing the serialization configuration. @@ -57,31 +56,86 @@ public final class JsonFormat implements EventFormat { * @see #withForceNonJsonDataToString() */ public JsonFormat(boolean forceDataBase64Serialization, boolean forceStringSerialization) { + this( + JsonFormatOptions.builder() + .forceDataBase64Serialization(forceDataBase64Serialization) + .forceStringSerialization(forceStringSerialization) + .build() + ); + } + + /** + * Create a new instance of this class customizing the serialization configuration. + * + * @param options json serialization / deserialization options + */ + public JsonFormat(JsonFormatOptions options) { this.mapper = new ObjectMapper(); - this.mapper.registerModule(getCloudEventJacksonModule(forceDataBase64Serialization, forceStringSerialization)); - this.forceDataBase64Serialization = forceDataBase64Serialization; - this.forceStringSerialization = forceStringSerialization; + this.mapper.registerModule(getCloudEventJacksonModule(options)); + this.options = options; } /** * Create a new instance of this class with default serialization configuration */ public JsonFormat() { - this(false, false); + this(new JsonFormatOptions()); } /** * @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( + JsonFormatOptions.builder() + .forceDataBase64Serialization(true) + .forceStringSerialization(this.options.isForceStringSerialization()) + .forceExtensionNameLowerCaseDeserialization(this.options.isForceExtensionNameLowerCaseDeserialization()) + .forceIgnoreInvalidExtensionNameDeserialization(this.options.isForceIgnoreInvalidExtensionNameDeserialization()) + .build() + ); } /** * @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( + JsonFormatOptions.builder() + .forceDataBase64Serialization(this.options.isForceDataBase64Serialization()) + .forceStringSerialization(true) + .forceExtensionNameLowerCaseDeserialization(this.options.isForceExtensionNameLowerCaseDeserialization()) + .forceIgnoreInvalidExtensionNameDeserialization(this.options.isForceIgnoreInvalidExtensionNameDeserialization()) + .build() + ); + } + + /** + * @return a copy of this JsonFormat that deserialize events with converting extension name lower case. + */ + public JsonFormat withForceExtensionNameLowerCaseDeserialization() { + return new JsonFormat( + JsonFormatOptions.builder() + .forceDataBase64Serialization(this.options.isForceDataBase64Serialization()) + .forceStringSerialization(this.options.isForceStringSerialization()) + .forceExtensionNameLowerCaseDeserialization(true) + .forceIgnoreInvalidExtensionNameDeserialization(this.options.isForceIgnoreInvalidExtensionNameDeserialization()) + .build() + ); + } + + /** + * @return a copy of this JsonFormat that deserialize events with ignoring invalid extension name + */ + public JsonFormat withForceIgnoreInvalidExtensionNameDeserialization() { + return new JsonFormat( + JsonFormatOptions.builder() + .forceDataBase64Serialization(this.options.isForceDataBase64Serialization()) + .forceStringSerialization(this.options.isForceStringSerialization()) + .forceExtensionNameLowerCaseDeserialization(this.options.isForceExtensionNameLowerCaseDeserialization()) + .forceIgnoreInvalidExtensionNameDeserialization(true) + .build() + ); } @Override @@ -137,9 +191,24 @@ public static SimpleModule getCloudEventJacksonModule() { * @see #withForceNonJsonDataToString() */ public static SimpleModule getCloudEventJacksonModule(boolean forceDataBase64Serialization, boolean forceStringSerialization) { + return getCloudEventJacksonModule( + JsonFormatOptions.builder() + .forceDataBase64Serialization(forceDataBase64Serialization) + .forceStringSerialization(forceStringSerialization) + .build() + ); + } + + /** + * @param options json serialization / deserialization options + * @return a JacksonModule with CloudEvent serializer/deserializer customizing the data serialization. + */ + public static SimpleModule getCloudEventJacksonModule(JsonFormatOptions options) { final SimpleModule ceModule = new SimpleModule("CloudEvent"); - ceModule.addSerializer(CloudEvent.class, new CloudEventSerializer(forceDataBase64Serialization, forceStringSerialization)); - ceModule.addDeserializer(CloudEvent.class, new CloudEventDeserializer()); + ceModule.addSerializer(CloudEvent.class, new CloudEventSerializer( + options.isForceDataBase64Serialization(), options.isForceStringSerialization())); + ceModule.addDeserializer(CloudEvent.class, new CloudEventDeserializer( + options.isForceExtensionNameLowerCaseDeserialization(), options.isForceIgnoreInvalidExtensionNameDeserialization())); 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 new file mode 100644 index 000000000..bf18ebf31 --- /dev/null +++ b/formats/json-jackson/src/main/java/io/cloudevents/jackson/JsonFormatOptions.java @@ -0,0 +1,99 @@ +/* + * Copyright 2018-Present The CloudEvents Authors + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package io.cloudevents.jackson; + +public final class JsonFormatOptions { + private final boolean forceDataBase64Serialization; + private final boolean forceStringSerialization; + private final boolean forceExtensionNameLowerCaseDeserialization; + private final boolean forceIgnoreInvalidExtensionNameDeserialization; + + /** + * Create a new instance of this class options the serialization / deserialization. + */ + public JsonFormatOptions() { + this(false, false, false, false); + } + + JsonFormatOptions( + boolean forceDataBase64Serialization, + boolean forceStringSerialization, + boolean forceExtensionNameLowerCaseDeserialization, + boolean forceIgnoreInvalidExtensionNameDeserialization + ) { + this.forceDataBase64Serialization = forceDataBase64Serialization; + this.forceStringSerialization = forceStringSerialization; + this.forceExtensionNameLowerCaseDeserialization = forceExtensionNameLowerCaseDeserialization; + this.forceIgnoreInvalidExtensionNameDeserialization = forceIgnoreInvalidExtensionNameDeserialization; + } + + public static JsonFormatOptionsBuilder builder() { + return new JsonFormatOptionsBuilder(); + } + + public boolean isForceDataBase64Serialization() { + return this.forceDataBase64Serialization; + } + + public boolean isForceStringSerialization() { + return this.forceStringSerialization; + } + + public boolean isForceExtensionNameLowerCaseDeserialization() { + return this.forceExtensionNameLowerCaseDeserialization; + } + + public boolean isForceIgnoreInvalidExtensionNameDeserialization() { + return this.forceIgnoreInvalidExtensionNameDeserialization; + } + + public static class JsonFormatOptionsBuilder { + private boolean forceDataBase64Serialization = false; + private boolean forceStringSerialization = false; + private boolean forceExtensionNameLowerCaseDeserialization = false; + private boolean forceIgnoreInvalidExtensionNameDeserialization = false; + + public JsonFormatOptionsBuilder forceDataBase64Serialization(boolean forceDataBase64Serialization) { + this.forceDataBase64Serialization = forceDataBase64Serialization; + return this; + } + + public JsonFormatOptionsBuilder forceStringSerialization(boolean forceStringSerialization) { + this.forceStringSerialization = forceStringSerialization; + return this; + } + + public JsonFormatOptionsBuilder forceExtensionNameLowerCaseDeserialization(boolean forceExtensionNameLowerCaseDeserialization) { + this.forceExtensionNameLowerCaseDeserialization = forceExtensionNameLowerCaseDeserialization; + return this; + } + + public JsonFormatOptionsBuilder forceIgnoreInvalidExtensionNameDeserialization(boolean forceIgnoreInvalidExtensionNameDeserialization) { + this.forceIgnoreInvalidExtensionNameDeserialization = forceIgnoreInvalidExtensionNameDeserialization; + return this; + } + + public JsonFormatOptions build() { + return new JsonFormatOptions( + this.forceDataBase64Serialization, + this.forceStringSerialization, + this.forceExtensionNameLowerCaseDeserialization, + this.forceIgnoreInvalidExtensionNameDeserialization + ); + } + } +} 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 +}