Skip to content

Commit ceb0675

Browse files
author
Myeonghyeon-Lee
authored
Ignore invalid extension names in jackson CloudEventDeserializer (#429)
Signed-off-by: mhyeon-lee <[email protected]>
1 parent 8d91cda commit ceb0675

File tree

8 files changed

+323
-14
lines changed

8 files changed

+323
-14
lines changed

formats/json-jackson/src/main/java/io/cloudevents/jackson/CloudEventDeserializer.java

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,39 @@
3838
* Jackson {@link com.fasterxml.jackson.databind.JsonDeserializer} for {@link CloudEvent}
3939
*/
4040
class CloudEventDeserializer extends StdDeserializer<CloudEvent> {
41+
private final boolean forceExtensionNameLowerCaseDeserialization;
42+
private final boolean forceIgnoreInvalidExtensionNameDeserialization;
4143

4244
protected CloudEventDeserializer() {
45+
this(false, false);
46+
}
47+
48+
protected CloudEventDeserializer(
49+
boolean forceExtensionNameLowerCaseDeserialization,
50+
boolean forceIgnoreInvalidExtensionNameDeserialization
51+
) {
4352
super(CloudEvent.class);
53+
this.forceExtensionNameLowerCaseDeserialization = forceExtensionNameLowerCaseDeserialization;
54+
this.forceIgnoreInvalidExtensionNameDeserialization = forceIgnoreInvalidExtensionNameDeserialization;
4455
}
4556

4657
private static class JsonMessage implements CloudEventReader {
4758

4859
private final JsonParser p;
4960
private final ObjectNode node;
61+
private final boolean forceExtensionNameLowerCaseDeserialization;
62+
private final boolean forceIgnoreInvalidExtensionNameDeserialization;
5063

51-
public JsonMessage(JsonParser p, ObjectNode node) {
64+
public JsonMessage(
65+
JsonParser p,
66+
ObjectNode node,
67+
boolean forceExtensionNameLowerCaseDeserialization,
68+
boolean forceIgnoreInvalidExtensionNameDeserialization
69+
) {
5270
this.p = p;
5371
this.node = node;
72+
this.forceExtensionNameLowerCaseDeserialization = forceExtensionNameLowerCaseDeserialization;
73+
this.forceIgnoreInvalidExtensionNameDeserialization = forceIgnoreInvalidExtensionNameDeserialization;
5474
}
5575

5676
@Override
@@ -127,6 +147,14 @@ public <T extends CloudEventWriter<V>, V> V read(CloudEventWriterFactory<T, V> w
127147
// Now let's process the extensions
128148
node.fields().forEachRemaining(entry -> {
129149
String extensionName = entry.getKey();
150+
if (this.forceExtensionNameLowerCaseDeserialization) {
151+
extensionName = extensionName.toLowerCase();
152+
}
153+
154+
if (this.shouldSkipExtensionName(extensionName)) {
155+
return;
156+
}
157+
130158
JsonNode extensionValue = entry.getValue();
131159

132160
switch (extensionValue.getNodeType()) {
@@ -192,6 +220,32 @@ private void assertNodeType(JsonNode node, JsonNodeType type, String attributeNa
192220
);
193221
}
194222
}
223+
224+
// ignore not valid extension name
225+
private boolean shouldSkipExtensionName(String extensionName) {
226+
return this.forceIgnoreInvalidExtensionNameDeserialization && !this.isValidExtensionName(extensionName);
227+
}
228+
229+
/**
230+
* Validates the extension name as defined in CloudEvents spec.
231+
*
232+
* @param name the extension name
233+
* @return true if extension name is valid, false otherwise
234+
* @see <a href="https://github.com/cloudevents/spec/blob/master/spec.md#attribute-naming-convention">attribute-naming-convention</a>
235+
*/
236+
private boolean isValidExtensionName(String name) {
237+
for (int i = 0; i < name.length(); i++) {
238+
if (!isValidChar(name.charAt(i))) {
239+
return false;
240+
}
241+
}
242+
return true;
243+
}
244+
245+
private boolean isValidChar(char c) {
246+
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
247+
}
248+
195249
}
196250

197251
@Override
@@ -201,7 +255,8 @@ public CloudEvent deserialize(JsonParser p, DeserializationContext ctxt) throws
201255
ObjectNode node = ctxt.readValue(p, ObjectNode.class);
202256

203257
try {
204-
return new JsonMessage(p, node).read(CloudEventBuilder::fromSpecVersion);
258+
return new JsonMessage(p, node, this.forceExtensionNameLowerCaseDeserialization, this.forceIgnoreInvalidExtensionNameDeserialization)
259+
.read(CloudEventBuilder::fromSpecVersion);
205260
} catch (RuntimeException e) {
206261
// Yeah this is bad but it's needed to support checked exceptions...
207262
if (e.getCause() instanceof IOException) {

formats/json-jackson/src/main/java/io/cloudevents/jackson/JsonFormat.java

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@ public final class JsonFormat implements EventFormat {
4545
public static final String CONTENT_TYPE = "application/cloudevents+json";
4646

4747
private final ObjectMapper mapper;
48-
private final boolean forceDataBase64Serialization;
49-
private final boolean forceStringSerialization;
48+
private final JsonFormatOptions options;
5049

5150
/**
5251
* Create a new instance of this class customizing the serialization configuration.
@@ -57,31 +56,86 @@ public final class JsonFormat implements EventFormat {
5756
* @see #withForceNonJsonDataToString()
5857
*/
5958
public JsonFormat(boolean forceDataBase64Serialization, boolean forceStringSerialization) {
59+
this(
60+
JsonFormatOptions.builder()
61+
.forceDataBase64Serialization(forceDataBase64Serialization)
62+
.forceStringSerialization(forceStringSerialization)
63+
.build()
64+
);
65+
}
66+
67+
/**
68+
* Create a new instance of this class customizing the serialization configuration.
69+
*
70+
* @param options json serialization / deserialization options
71+
*/
72+
public JsonFormat(JsonFormatOptions options) {
6073
this.mapper = new ObjectMapper();
61-
this.mapper.registerModule(getCloudEventJacksonModule(forceDataBase64Serialization, forceStringSerialization));
62-
this.forceDataBase64Serialization = forceDataBase64Serialization;
63-
this.forceStringSerialization = forceStringSerialization;
74+
this.mapper.registerModule(getCloudEventJacksonModule(options));
75+
this.options = options;
6476
}
6577

6678
/**
6779
* Create a new instance of this class with default serialization configuration
6880
*/
6981
public JsonFormat() {
70-
this(false, false);
82+
this(new JsonFormatOptions());
7183
}
7284

7385
/**
7486
* @return a copy of this JsonFormat that serialize events with json data with Base64 encoding
7587
*/
7688
public JsonFormat withForceJsonDataToBase64() {
77-
return new JsonFormat(true, this.forceStringSerialization);
89+
return new JsonFormat(
90+
JsonFormatOptions.builder()
91+
.forceDataBase64Serialization(true)
92+
.forceStringSerialization(this.options.isForceStringSerialization())
93+
.forceExtensionNameLowerCaseDeserialization(this.options.isForceExtensionNameLowerCaseDeserialization())
94+
.forceIgnoreInvalidExtensionNameDeserialization(this.options.isForceIgnoreInvalidExtensionNameDeserialization())
95+
.build()
96+
);
7897
}
7998

8099
/**
81100
* @return a copy of this JsonFormat that serialize events with non-json data as string
82101
*/
83102
public JsonFormat withForceNonJsonDataToString() {
84-
return new JsonFormat(this.forceDataBase64Serialization, true);
103+
return new JsonFormat(
104+
JsonFormatOptions.builder()
105+
.forceDataBase64Serialization(this.options.isForceDataBase64Serialization())
106+
.forceStringSerialization(true)
107+
.forceExtensionNameLowerCaseDeserialization(this.options.isForceExtensionNameLowerCaseDeserialization())
108+
.forceIgnoreInvalidExtensionNameDeserialization(this.options.isForceIgnoreInvalidExtensionNameDeserialization())
109+
.build()
110+
);
111+
}
112+
113+
/**
114+
* @return a copy of this JsonFormat that deserialize events with converting extension name lower case.
115+
*/
116+
public JsonFormat withForceExtensionNameLowerCaseDeserialization() {
117+
return new JsonFormat(
118+
JsonFormatOptions.builder()
119+
.forceDataBase64Serialization(this.options.isForceDataBase64Serialization())
120+
.forceStringSerialization(this.options.isForceStringSerialization())
121+
.forceExtensionNameLowerCaseDeserialization(true)
122+
.forceIgnoreInvalidExtensionNameDeserialization(this.options.isForceIgnoreInvalidExtensionNameDeserialization())
123+
.build()
124+
);
125+
}
126+
127+
/**
128+
* @return a copy of this JsonFormat that deserialize events with ignoring invalid extension name
129+
*/
130+
public JsonFormat withForceIgnoreInvalidExtensionNameDeserialization() {
131+
return new JsonFormat(
132+
JsonFormatOptions.builder()
133+
.forceDataBase64Serialization(this.options.isForceDataBase64Serialization())
134+
.forceStringSerialization(this.options.isForceStringSerialization())
135+
.forceExtensionNameLowerCaseDeserialization(this.options.isForceExtensionNameLowerCaseDeserialization())
136+
.forceIgnoreInvalidExtensionNameDeserialization(true)
137+
.build()
138+
);
85139
}
86140

87141
@Override
@@ -137,9 +191,24 @@ public static SimpleModule getCloudEventJacksonModule() {
137191
* @see #withForceNonJsonDataToString()
138192
*/
139193
public static SimpleModule getCloudEventJacksonModule(boolean forceDataBase64Serialization, boolean forceStringSerialization) {
194+
return getCloudEventJacksonModule(
195+
JsonFormatOptions.builder()
196+
.forceDataBase64Serialization(forceDataBase64Serialization)
197+
.forceStringSerialization(forceStringSerialization)
198+
.build()
199+
);
200+
}
201+
202+
/**
203+
* @param options json serialization / deserialization options
204+
* @return a JacksonModule with CloudEvent serializer/deserializer customizing the data serialization.
205+
*/
206+
public static SimpleModule getCloudEventJacksonModule(JsonFormatOptions options) {
140207
final SimpleModule ceModule = new SimpleModule("CloudEvent");
141-
ceModule.addSerializer(CloudEvent.class, new CloudEventSerializer(forceDataBase64Serialization, forceStringSerialization));
142-
ceModule.addDeserializer(CloudEvent.class, new CloudEventDeserializer());
208+
ceModule.addSerializer(CloudEvent.class, new CloudEventSerializer(
209+
options.isForceDataBase64Serialization(), options.isForceStringSerialization()));
210+
ceModule.addDeserializer(CloudEvent.class, new CloudEventDeserializer(
211+
options.isForceExtensionNameLowerCaseDeserialization(), options.isForceIgnoreInvalidExtensionNameDeserialization()));
143212
return ceModule;
144213
}
145214

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2018-Present The CloudEvents Authors
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
package io.cloudevents.jackson;
18+
19+
public final class JsonFormatOptions {
20+
private final boolean forceDataBase64Serialization;
21+
private final boolean forceStringSerialization;
22+
private final boolean forceExtensionNameLowerCaseDeserialization;
23+
private final boolean forceIgnoreInvalidExtensionNameDeserialization;
24+
25+
/**
26+
* Create a new instance of this class options the serialization / deserialization.
27+
*/
28+
public JsonFormatOptions() {
29+
this(false, false, false, false);
30+
}
31+
32+
JsonFormatOptions(
33+
boolean forceDataBase64Serialization,
34+
boolean forceStringSerialization,
35+
boolean forceExtensionNameLowerCaseDeserialization,
36+
boolean forceIgnoreInvalidExtensionNameDeserialization
37+
) {
38+
this.forceDataBase64Serialization = forceDataBase64Serialization;
39+
this.forceStringSerialization = forceStringSerialization;
40+
this.forceExtensionNameLowerCaseDeserialization = forceExtensionNameLowerCaseDeserialization;
41+
this.forceIgnoreInvalidExtensionNameDeserialization = forceIgnoreInvalidExtensionNameDeserialization;
42+
}
43+
44+
public static JsonFormatOptionsBuilder builder() {
45+
return new JsonFormatOptionsBuilder();
46+
}
47+
48+
public boolean isForceDataBase64Serialization() {
49+
return this.forceDataBase64Serialization;
50+
}
51+
52+
public boolean isForceStringSerialization() {
53+
return this.forceStringSerialization;
54+
}
55+
56+
public boolean isForceExtensionNameLowerCaseDeserialization() {
57+
return this.forceExtensionNameLowerCaseDeserialization;
58+
}
59+
60+
public boolean isForceIgnoreInvalidExtensionNameDeserialization() {
61+
return this.forceIgnoreInvalidExtensionNameDeserialization;
62+
}
63+
64+
public static class JsonFormatOptionsBuilder {
65+
private boolean forceDataBase64Serialization = false;
66+
private boolean forceStringSerialization = false;
67+
private boolean forceExtensionNameLowerCaseDeserialization = false;
68+
private boolean forceIgnoreInvalidExtensionNameDeserialization = false;
69+
70+
public JsonFormatOptionsBuilder forceDataBase64Serialization(boolean forceDataBase64Serialization) {
71+
this.forceDataBase64Serialization = forceDataBase64Serialization;
72+
return this;
73+
}
74+
75+
public JsonFormatOptionsBuilder forceStringSerialization(boolean forceStringSerialization) {
76+
this.forceStringSerialization = forceStringSerialization;
77+
return this;
78+
}
79+
80+
public JsonFormatOptionsBuilder forceExtensionNameLowerCaseDeserialization(boolean forceExtensionNameLowerCaseDeserialization) {
81+
this.forceExtensionNameLowerCaseDeserialization = forceExtensionNameLowerCaseDeserialization;
82+
return this;
83+
}
84+
85+
public JsonFormatOptionsBuilder forceIgnoreInvalidExtensionNameDeserialization(boolean forceIgnoreInvalidExtensionNameDeserialization) {
86+
this.forceIgnoreInvalidExtensionNameDeserialization = forceIgnoreInvalidExtensionNameDeserialization;
87+
return this;
88+
}
89+
90+
public JsonFormatOptions build() {
91+
return new JsonFormatOptions(
92+
this.forceDataBase64Serialization,
93+
this.forceStringSerialization,
94+
this.forceExtensionNameLowerCaseDeserialization,
95+
this.forceIgnoreInvalidExtensionNameDeserialization
96+
);
97+
}
98+
}
99+
}

formats/json-jackson/src/test/java/io/cloudevents/jackson/JsonFormatTest.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,12 @@
2727
import io.cloudevents.core.format.EventDeserializationException;
2828
import io.cloudevents.core.provider.EventFormatProvider;
2929
import io.cloudevents.rw.CloudEventRWException;
30-
import org.junit.jupiter.api.Assertions;
3130
import org.junit.jupiter.api.Test;
3231
import org.junit.jupiter.params.ParameterizedTest;
3332
import org.junit.jupiter.params.provider.Arguments;
3433
import org.junit.jupiter.params.provider.MethodSource;
3534

3635
import java.io.IOException;
37-
import java.math.BigInteger;
3836
import java.net.URISyntaxException;
3937
import java.nio.charset.StandardCharsets;
4038
import java.nio.file.Files;
@@ -90,6 +88,22 @@ void deserialize(String inputFile, CloudEvent output) {
9088
.isEqualTo(output);
9189
}
9290

91+
@ParameterizedTest
92+
@MethodSource("deserializeTestArgumentsUpperCaseExtensionName")
93+
void deserializeWithUpperCaseExtensionName(String inputFile, CloudEvent output) {
94+
CloudEvent deserialized = getFormat().withForceExtensionNameLowerCaseDeserialization().deserialize(loadFile(inputFile));
95+
assertThat(deserialized)
96+
.isEqualTo(output);
97+
}
98+
99+
@ParameterizedTest
100+
@MethodSource("deserializeTestArgumentsInvalidExtensionName")
101+
void deserializeWithInvalidExtensionName(String inputFile, CloudEvent output) {
102+
CloudEvent deserialized = getFormat().withForceIgnoreInvalidExtensionNameDeserialization().deserialize(loadFile(inputFile));
103+
assertThat(deserialized)
104+
.isEqualTo(output);
105+
}
106+
93107
@ParameterizedTest
94108
@MethodSource("roundTripTestArguments")
95109
void jsonRoundTrip(String inputFile) throws IOException {
@@ -204,6 +218,20 @@ public static Stream<Arguments> deserializeTestArguments() {
204218
);
205219
}
206220

221+
public static Stream<Arguments> deserializeTestArgumentsUpperCaseExtensionName() {
222+
return Stream.of(
223+
Arguments.of("v03/json_data_with_ext_upper_case.json", normalizeToJsonValueIfNeeded(V03_WITH_JSON_DATA_WITH_EXT)),
224+
Arguments.of("v1/json_data_with_ext_upper_case.json", normalizeToJsonValueIfNeeded(V1_WITH_JSON_DATA_WITH_EXT))
225+
);
226+
}
227+
228+
public static Stream<Arguments> deserializeTestArgumentsInvalidExtensionName() {
229+
return Stream.of(
230+
Arguments.of("v03/json_data_with_ext_invalid.json", normalizeToJsonValueIfNeeded(V03_WITH_JSON_DATA_WITH_EXT)),
231+
Arguments.of("v1/json_data_with_ext_invalid.json", normalizeToJsonValueIfNeeded(V1_WITH_JSON_DATA_WITH_EXT))
232+
);
233+
}
234+
207235
public static Stream<String> roundTripTestArguments() {
208236
return Stream.of(
209237
"v03/min.json",

0 commit comments

Comments
 (0)