Ignore invalid extension names in jackson CloudEventDeserializer (#429)

Signed-off-by: mhyeon-lee <mhyeon.lee@navercorp.com>
This commit is contained in:
Myeonghyeon-Lee 2021-12-10 18:26:47 +09:00 committed by GitHub
parent 8d91cdaee6
commit ceb06757a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 323 additions and 14 deletions

View File

@ -38,19 +38,39 @@ import java.io.IOException;
* Jackson {@link com.fasterxml.jackson.databind.JsonDeserializer} for {@link CloudEvent}
*/
class CloudEventDeserializer extends StdDeserializer<CloudEvent> {
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 @@ class CloudEventDeserializer extends StdDeserializer<CloudEvent> {
// 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 @@ class CloudEventDeserializer extends StdDeserializer<CloudEvent> {
);
}
}
// 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 <a href="https://github.com/cloudevents/spec/blob/master/spec.md#attribute-naming-convention">attribute-naming-convention</a>
*/
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 @@ class CloudEventDeserializer extends StdDeserializer<CloudEvent> {
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) {

View File

@ -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 final class JsonFormat implements EventFormat {
* @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;
}

View File

@ -0,0 +1,99 @@
/*
* 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.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
);
}
}
}

View File

@ -27,14 +27,12 @@ import io.cloudevents.core.builder.CloudEventBuilder;
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 @@ class JsonFormatTest {
.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 @@ class JsonFormatTest {
);
}
public static Stream<Arguments> 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<Arguments> 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<String> roundTripTestArguments() {
return Stream.of(
"v03/min.json",

View File

@ -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"
}

View File

@ -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
}

View File

@ -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"
}

View File

@ -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
}