Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignore not valid extension name in jackson CloudEventDeserializer #429

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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