Skip to content

Commit

Permalink
Ignore invalid extension names in jackson CloudEventDeserializer (#429)
Browse files Browse the repository at this point in the history
Signed-off-by: mhyeon-lee <[email protected]>
  • Loading branch information
Myeonghyeon-Lee authored Dec 10, 2021
1 parent 8d91cda commit ceb0675
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,39 @@
* 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
Expand Down Expand Up @@ -127,6 +147,14 @@ public <T extends CloudEventWriter<V>, V> V read(CloudEventWriterFactory<T, V> 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()) {
Expand Down Expand Up @@ -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 <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
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -204,6 +218,20 @@ public static Stream<Arguments> deserializeTestArguments() {
);
}

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",
Expand Down
Loading

0 comments on commit ceb0675

Please sign in to comment.