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 7a22a068..0a427719 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 @@ -26,6 +26,7 @@ import com.fasterxml.jackson.databind.exc.MismatchedInputException; import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.ObjectNode; import io.cloudevents.CloudEvent; +import io.cloudevents.CloudEventData; import io.cloudevents.SpecVersion; import io.cloudevents.core.builder.CloudEventBuilder; import io.cloudevents.core.data.BytesCloudEventData; @@ -81,7 +82,7 @@ public class CloudEventDeserializer extends StdDeserializer { } } - byte[] data = null; + CloudEventData data = null; // Now let's handle the data switch (specVersion) { @@ -89,16 +90,16 @@ public class CloudEventDeserializer extends StdDeserializer { boolean isBase64 = "base64".equals(getOptionalStringNode(this.node, this.p, "datacontentencoding")); if (node.has("data")) { if (isBase64) { - data = node.remove("data").binaryValue(); + data = new BytesCloudEventData(node.remove("data").binaryValue()); } else { if (JsonFormat.dataIsJsonContentType(contentType)) { // This solution is quite bad, but i see no alternatives now. // Hopefully in future we can improve it - data = node.remove("data").toString().getBytes(); + data = new JsonCloudEventData(node.remove("data")); } else { JsonNode dataNode = node.remove("data"); assertNodeType(dataNode, JsonNodeType.STRING, "data", "Because content type is not a json, only a string is accepted as data"); - data = dataNode.asText().getBytes(); + data = new BytesCloudEventData(dataNode.asText().getBytes()); } } } @@ -107,16 +108,16 @@ public class CloudEventDeserializer extends StdDeserializer { throw MismatchedInputException.from(p, CloudEvent.class, "CloudEvent cannot have both 'data' and 'data_base64' fields"); } if (node.has("data_base64")) { - data = node.remove("data_base64").binaryValue(); + data = new BytesCloudEventData(node.remove("data_base64").binaryValue()); } else if (node.has("data")) { if (JsonFormat.dataIsJsonContentType(contentType)) { // This solution is quite bad, but i see no alternatives now. // Hopefully in future we can improve it - data = node.remove("data").toString().getBytes(); + data = new JsonCloudEventData(node.remove("data")); } else { JsonNode dataNode = node.remove("data"); assertNodeType(dataNode, JsonNodeType.STRING, "data", "Because content type is not a json, only a string is accepted as data"); - data = dataNode.asText().getBytes(); + data = new BytesCloudEventData(dataNode.asText().getBytes()); } } } @@ -143,7 +144,7 @@ public class CloudEventDeserializer extends StdDeserializer { }); if (data != null) { - return visitor.end(new BytesCloudEventData(data)); + return visitor.end(data); } return visitor.end(); } catch (IOException e) { diff --git a/formats/json-jackson/src/main/java/io/cloudevents/jackson/CloudEventSerializer.java b/formats/json-jackson/src/main/java/io/cloudevents/jackson/CloudEventSerializer.java index a0aa41e4..3d0f9150 100644 --- a/formats/json-jackson/src/main/java/io/cloudevents/jackson/CloudEventSerializer.java +++ b/formats/json-jackson/src/main/java/io/cloudevents/jackson/CloudEventSerializer.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import io.cloudevents.CloudEvent; +import io.cloudevents.CloudEventData; import io.cloudevents.core.impl.CloudEventUtils; import io.cloudevents.rw.CloudEventAttributesWriter; import io.cloudevents.rw.CloudEventExtensionsWriter; @@ -113,28 +114,33 @@ public class CloudEventSerializer extends StdSerializer { // Serialize data if (value.getData() != null) { - byte[] data = value.getData().toBytes(); - String contentType = value.getDataContentType(); - if (shouldSerializeBase64(contentType)) { - switch (value.getSpecVersion()) { - case V03: - gen.writeStringField("datacontentencoding", "base64"); - gen.writeFieldName("data"); - gen.writeBinary(data); - break; - case V1: - gen.writeFieldName("data_base64"); - gen.writeBinary(data); - break; - } - } else if (JsonFormat.dataIsJsonContentType(contentType)) { - // TODO really bad b/c it allocates stuff, is there another solution out there? - char[] dataAsString = new String(data, StandardCharsets.UTF_8).toCharArray(); - gen.writeFieldName("data"); - gen.writeRawValue(dataAsString, 0, dataAsString.length); + CloudEventData data = value.getData(); + if (data instanceof JsonCloudEventData) { + gen.writeObjectField("data", ((JsonCloudEventData) data).getNode()); } else { - gen.writeFieldName("data"); - gen.writeUTF8String(data, 0, data.length); + byte[] dataBytes = data.toBytes(); + String contentType = value.getDataContentType(); + if (shouldSerializeBase64(contentType)) { + switch (value.getSpecVersion()) { + case V03: + gen.writeStringField("datacontentencoding", "base64"); + gen.writeFieldName("data"); + gen.writeBinary(dataBytes); + break; + case V1: + gen.writeFieldName("data_base64"); + gen.writeBinary(dataBytes); + break; + } + } else if (JsonFormat.dataIsJsonContentType(contentType)) { + // TODO really bad b/c it allocates stuff, is there another solution out there? + char[] dataAsString = new String(dataBytes, StandardCharsets.UTF_8).toCharArray(); + gen.writeFieldName("data"); + gen.writeRawValue(dataAsString, 0, dataAsString.length); + } else { + gen.writeFieldName("data"); + gen.writeUTF8String(dataBytes, 0, dataBytes.length); + } } } gen.writeEndObject(); diff --git a/formats/json-jackson/src/main/java/io/cloudevents/jackson/JsonCloudEventData.java b/formats/json-jackson/src/main/java/io/cloudevents/jackson/JsonCloudEventData.java new file mode 100644 index 00000000..26f11766 --- /dev/null +++ b/formats/json-jackson/src/main/java/io/cloudevents/jackson/JsonCloudEventData.java @@ -0,0 +1,48 @@ +package io.cloudevents.jackson; + +import com.fasterxml.jackson.databind.JsonNode; +import io.cloudevents.CloudEventData; + +import java.util.Objects; + +/** + * This class is a wrapper for Jackson {@link JsonNode} implementing the {@link CloudEventData} + */ +public class JsonCloudEventData implements CloudEventData { + + private final JsonNode node; + + public JsonCloudEventData(JsonNode node) { + Objects.requireNonNull(node); + this.node = node; + } + + @Override + public byte[] toBytes() { + return node.toString().getBytes(); + } + + public JsonNode getNode() { + return node; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + JsonCloudEventData that = (JsonCloudEventData) o; + return Objects.equals(getNode(), that.getNode()); + } + + @Override + public int hashCode() { + return Objects.hash(getNode()); + } + + @Override + public String toString() { + return "JsonCloudEventData{" + + "node=" + node + + '}'; + } +} 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 c2257edb..2041e332 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 @@ -19,7 +19,10 @@ package io.cloudevents.jackson; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; import io.cloudevents.CloudEvent; +import io.cloudevents.SpecVersion; +import io.cloudevents.core.builder.CloudEventBuilder; import io.cloudevents.core.provider.EventFormatProvider; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -103,7 +106,7 @@ class JsonFormatTest { assertThat(serialized).isNotEmpty(); CloudEvent output = getFormat().deserialize(serialized); - assertThat(output).isEqualTo(input); + assertThat(output).isEqualTo(normalizeToJsonValueIfNeeded(input)); } public static Stream serializeTestArgumentsDefault() { @@ -151,8 +154,8 @@ class JsonFormatTest { public static Stream deserializeTestArguments() { return Stream.of( Arguments.of("v03/min.json", V03_MIN), - Arguments.of("v03/json_data.json", V03_WITH_JSON_DATA), - Arguments.of("v03/json_data_with_ext.json", V03_WITH_JSON_DATA_WITH_EXT), + Arguments.of("v03/json_data.json", normalizeToJsonValueIfNeeded(V03_WITH_JSON_DATA)), + Arguments.of("v03/json_data_with_ext.json", normalizeToJsonValueIfNeeded(V03_WITH_JSON_DATA_WITH_EXT)), Arguments.of("v03/base64_json_data.json", V03_WITH_JSON_DATA), Arguments.of("v03/base64_json_data_with_ext.json", V03_WITH_JSON_DATA_WITH_EXT), Arguments.of("v03/xml_data.json", V03_WITH_XML_DATA), @@ -160,8 +163,8 @@ class JsonFormatTest { Arguments.of("v03/text_data.json", V03_WITH_TEXT_DATA), Arguments.of("v03/base64_text_data.json", V03_WITH_TEXT_DATA), Arguments.of("v1/min.json", V1_MIN), - Arguments.of("v1/json_data.json", V1_WITH_JSON_DATA), - Arguments.of("v1/json_data_with_ext.json", V1_WITH_JSON_DATA_WITH_EXT), + Arguments.of("v1/json_data.json", normalizeToJsonValueIfNeeded(V1_WITH_JSON_DATA)), + Arguments.of("v1/json_data_with_ext.json", normalizeToJsonValueIfNeeded(V1_WITH_JSON_DATA_WITH_EXT)), Arguments.of("v1/base64_json_data.json", V1_WITH_JSON_DATA), Arguments.of("v1/base64_json_data_with_ext.json", V1_WITH_JSON_DATA_WITH_EXT), Arguments.of("v1/xml_data.json", V1_WITH_XML_DATA), @@ -185,7 +188,11 @@ class JsonFormatTest { ); } - public static byte[] loadFile(String input) { + private JsonFormat getFormat() { + return (JsonFormat) EventFormatProvider.getInstance().resolveFormat(JsonFormat.CONTENT_TYPE); + } + + private static byte[] loadFile(String input) { try { return String.join( "", @@ -196,8 +203,20 @@ class JsonFormatTest { } } - private JsonFormat getFormat() { - return (JsonFormat) EventFormatProvider.getInstance().resolveFormat(JsonFormat.CONTENT_TYPE); + private static CloudEvent normalizeToJsonValueIfNeeded(CloudEvent event) { + if (event.getData() != null && JsonFormat.dataIsJsonContentType(event.getDataContentType())) { + CloudEventBuilder builder = null; + if (event.getSpecVersion() == SpecVersion.V1) { + builder = CloudEventBuilder.v1(event); + } else { + builder = CloudEventBuilder.v03(event); + } + return builder + .withData(new JsonCloudEventData(JsonNodeFactory.instance.objectNode())) + .build(); + } else { + return event; + } } }