com.azure.core.models.CloudEvent Maven / Gradle / Ivy
Show all versions of azure-core Show documentation
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.core.models;
import com.azure.core.annotation.Fluent;
import com.azure.core.util.BinaryData;
import com.azure.core.util.CoreUtils;
import com.azure.core.util.logging.ClientLogger;
import com.azure.json.JsonProviders;
import com.azure.json.JsonReader;
import com.azure.json.JsonSerializable;
import com.azure.json.JsonToken;
import com.azure.json.JsonWriter;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRawValue;
import java.io.IOException;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* Represents the CloudEvent conforming to the 1.0 schema defined by the
* Cloud Native Computing Foundation.
*
*
* CloudEvents is a specification for describing event data in common formats to provide interoperability across
* services, platforms and systems.
*
*
*
* Some Azure services, for instance, EventGrid, are compatible with this specification. You can use this class to
* communicate with these Azure services.
*
*
* Depending on your scenario, you can either use the constructor
* {@link #CloudEvent(String, String, BinaryData, CloudEventDataFormat, String)} to create a CloudEvent, or use the
* factory method {@link #fromString(String)} to deserialize CloudEvent instances from a Json String representation of
* CloudEvents.
*
*
*
* If you have the data payload of a CloudEvent and want to send it out, use the constructor
* {@link #CloudEvent(String, String, BinaryData, CloudEventDataFormat, String)} to create it. Then you can serialize
* the CloudEvent into its Json String representation and send it.
*
*
*
* Create CloudEvent Samples
*
*
*
* // Use BinaryData.fromBytes() to create data in format CloudEventDataFormat.BYTES
* byte[] exampleBytes = "Hello World".getBytes(StandardCharsets.UTF_8);
* CloudEvent cloudEvent = new CloudEvent("/cloudevents/example/source", "Example.EventType",
* BinaryData.fromBytes(exampleBytes), CloudEventDataFormat.BYTES, "application/octet-stream");
*
* // Use BinaryData.fromObject() to create CloudEvent data in format CloudEventDataFormat.JSON
* // From a model class
* User user = new User("Stephen", "James");
* CloudEvent cloudEventDataObject = new CloudEvent("/cloudevents/example/source", "Example.EventType",
* BinaryData.fromObject(user), CloudEventDataFormat.JSON, "application/json");
*
* // From a String
* CloudEvent cloudEventDataStr = new CloudEvent("/cloudevents/example/source", "Example.EventType",
* BinaryData.fromObject("Hello World"), CloudEventDataFormat.JSON, "text/plain");
*
* // From an Integer
* CloudEvent cloudEventDataInt = new CloudEvent("/cloudevents/example/source", "Example.EventType",
* BinaryData.fromObject(1), CloudEventDataFormat.JSON, "int");
*
* // From a Boolean
* CloudEvent cloudEventDataBool = new CloudEvent("/cloudevents/example/source", "Example.EventType",
* BinaryData.fromObject(true), CloudEventDataFormat.JSON, "bool");
*
* // From null
* CloudEvent cloudEventDataNull = new CloudEvent("/cloudevents/example/source", "Example.EventType",
* BinaryData.fromObject(null), CloudEventDataFormat.JSON, "null");
*
* // Use BinaryData.fromString() if you have a Json String for the CloudEvent data.
* String jsonStringForData = "\"Hello World\""; // A json String.
* CloudEvent cloudEventDataJsonStr = new CloudEvent("/cloudevents/example/source", "Example.EventType",
* BinaryData.fromString(jsonStringForData), CloudEventDataFormat.JSON, "text/plain");
*
*
*
*
* On the contrary, if you receive CloudEvents and have the Json string representation of one or more of
* CloudEvents, use {@link #fromString(String)} to deserialize them from the Json string.
*
*
*
* Deserialize CloudEvent Samples
*
*
*
* List<CloudEvent> cloudEventList = CloudEvent.fromString(cloudEventJsonString);
* CloudEvent cloudEvent = cloudEventList.get(0);
* BinaryData cloudEventData = cloudEvent.getData();
*
* byte[] bytesValue = cloudEventData.toBytes(); // If data payload is in bytes (data_base64 is not null).
* User objectValue = cloudEventData.toObject(User.class); // If data payload is a User object.
* int intValue = cloudEventData.toObject(Integer.class); // If data payload is an int.
* boolean boolValue = cloudEventData.toObject(Boolean.class); // If data payload is boolean.
* String stringValue = cloudEventData.toObject(String.class); // If data payload is String.
* String jsonStringValue = cloudEventData.toString(); // The data payload represented in Json String.
*
*
*/
@Fluent
public final class CloudEvent implements JsonSerializable {
private static final String SPEC_VERSION = "1.0";
private static final ClientLogger LOGGER = new ClientLogger(CloudEvent.class);
private static final Set RESERVED_ATTRIBUTE_NAMES = new HashSet<>(Arrays.asList("specversion", "id",
"source", "type", "datacontenttype", "dataschema", "subject", "time", "data", "data_base64"));
private static final String ILLEGAL_ATTRIBUTE_NAME_MESSAGE = "Extension attribute 'name' must have only lower-case "
+ "alphanumeric characters and not be one of the CloudEvent reserved attribute names: "
+ String.join(", ", RESERVED_ATTRIBUTE_NAMES);
/*
* An identifier for the event. The combination of id and source must be
* unique for each distinct event.
*/
@JsonProperty(value = "id", required = true)
private String id;
/*
* Identifies the context in which an event happened. The combination of id
* and source must be unique for each distinct event.
*/
@JsonProperty(value = "source", required = true)
private String source;
/*
* Event data specific to the event type. This is internal only for data serialization.
*/
@JsonProperty(value = "data")
@JsonRawValue
private String data;
/*
* Event data specific to the event type, encoded as a base64 string. This is internal only for
* data_base64 serialization.
*/
@JsonProperty(value = "data_base64")
private String dataBase64;
/*
* Type of event related to the originating occurrence.
*/
@JsonProperty(value = "type", required = true)
private String type;
/*
* The time (in UTC) the event was generated, in RFC3339 format.
*/
@JsonProperty(value = "time")
private OffsetDateTime time;
/*
* The version of the CloudEvents specification which the event uses.
*/
@JsonProperty(value = "specversion", required = true)
private String specVersion;
/*
* Identifies the schema that data adheres to.
*/
@JsonProperty(value = "dataschema")
private String dataSchema;
/*
* Content type of data value.
*/
@JsonProperty(value = "datacontenttype")
private String dataContentType;
/*
* This describes the subject of the event in the context of the event
* producer (identified by source).
*/
@JsonProperty(value = "subject")
private String subject;
@JsonIgnore
private Map extensionAttributes;
/*
* Cache serialized data for getData()
*/
@JsonIgnore
private BinaryData binaryData;
/**
* Create an instance of {@link CloudEvent}.
*
* {@code source}, {@code type}, {@code id}, and {@code specversion} are required attributes according to the
* CNCF CloudEvent spec.
* You must set the {@code source} and {@code type} when using this constructor. For convenience, {@code id} and
* {@code specversion} are automatically assigned. You can change the {@code id} by using {@link #setId(String)}
* after you create a CloudEvent. But you can not change {@code specversion} because this class is specifically for
* CloudEvent 1.0 schema.
*
* For the CloudEvent data payload, this constructor accepts {@code data} of {@link BinaryData} as the
* CloudEvent payload. The {@code data} can be created from objects of type String, bytes, boolean, null, array or
* other types. A CloudEvent will be serialized to its Json String representation to be sent out. Use param
* {@code format} to indicate whether the {@code data} will be serialized as bytes, or Json. When
* {@link CloudEventDataFormat#BYTES} is used, the data payload will be serialized to base64 bytes and stored in
* attribute data_base64 of the CloudEvent's Json representation. When {@link CloudEventDataFormat#JSON} is
* used, the data payload will be serialized as Json data and stored in attribute data of the CloudEvent's
* Json representation.
*
* Create CloudEvent Samples
*
*
* // Use BinaryData.fromBytes() to create data in format CloudEventDataFormat.BYTES
* byte[] exampleBytes = "Hello World".getBytes(StandardCharsets.UTF_8);
* CloudEvent cloudEvent = new CloudEvent("/cloudevents/example/source", "Example.EventType",
* BinaryData.fromBytes(exampleBytes), CloudEventDataFormat.BYTES, "application/octet-stream");
*
* // Use BinaryData.fromObject() to create CloudEvent data in format CloudEventDataFormat.JSON
* // From a model class
* User user = new User("Stephen", "James");
* CloudEvent cloudEventDataObject = new CloudEvent("/cloudevents/example/source", "Example.EventType",
* BinaryData.fromObject(user), CloudEventDataFormat.JSON, "application/json");
*
* // From a String
* CloudEvent cloudEventDataStr = new CloudEvent("/cloudevents/example/source", "Example.EventType",
* BinaryData.fromObject("Hello World"), CloudEventDataFormat.JSON, "text/plain");
*
* // From an Integer
* CloudEvent cloudEventDataInt = new CloudEvent("/cloudevents/example/source", "Example.EventType",
* BinaryData.fromObject(1), CloudEventDataFormat.JSON, "int");
*
* // From a Boolean
* CloudEvent cloudEventDataBool = new CloudEvent("/cloudevents/example/source", "Example.EventType",
* BinaryData.fromObject(true), CloudEventDataFormat.JSON, "bool");
*
* // From null
* CloudEvent cloudEventDataNull = new CloudEvent("/cloudevents/example/source", "Example.EventType",
* BinaryData.fromObject(null), CloudEventDataFormat.JSON, "null");
*
* // Use BinaryData.fromString() if you have a Json String for the CloudEvent data.
* String jsonStringForData = "\"Hello World\""; // A json String.
* CloudEvent cloudEventDataJsonStr = new CloudEvent("/cloudevents/example/source", "Example.EventType",
* BinaryData.fromString(jsonStringForData), CloudEventDataFormat.JSON, "text/plain");
*
*
*
* @param source Identifies the context in which an event happened. The combination of id and source must be unique
* for each distinct event.
* @param type Type of event related to the originating occurrence.
* @param data A {@link BinaryData} that wraps the original data, which can be a String, byte[], or model class.
* @param format Set to {@link CloudEventDataFormat#BYTES} to serialize the data to base64 format, or
* {@link CloudEventDataFormat#JSON} to serialize the data to JSON value.
* @param dataContentType The content type of the data. It has no impact on how the data is serialized but tells the
* event subscriber how to use the data. Typically, the value is of MIME types such as "application/json",
* "text/plain", "text/xml", "avro/binary", etc. It can be null.
* @throws NullPointerException If source or type is null or format is null while data isn't null.
* @throws IllegalArgumentException if format is {@link CloudEventDataFormat#JSON} but the data isn't in a correct
* JSON format.
*/
public CloudEvent(String source, String type, BinaryData data, CloudEventDataFormat format,
String dataContentType) {
this.source = Objects.requireNonNull(source, "'source' cannot be null.");
this.type = Objects.requireNonNull(type, "'type' cannot be null.");
if (data != null) {
Objects.requireNonNull(format, "'format' cannot be null when 'data' isn't null.");
if (CloudEventDataFormat.BYTES == format) {
this.dataBase64 = Base64.getEncoder().encodeToString(data.toBytes());
} else {
try (JsonReader jsonReader = JsonProviders.createReader(data.toBytes())) {
JsonToken token = jsonReader.nextToken();
if (token == JsonToken.START_OBJECT || token == JsonToken.START_ARRAY) {
this.data = jsonReader.readChildren();
} else if (token == JsonToken.STRING) {
this.data = "\"" + jsonReader.getString() + "\"";
} else {
this.data = jsonReader.getString();
}
} catch (IOException e) {
throw LOGGER
.logExceptionAsError(new IllegalArgumentException("'data' isn't in valid Json format", e));
}
}
}
this.dataContentType = dataContentType;
this.id = CoreUtils.randomUuid().toString();
this.specVersion = CloudEvent.SPEC_VERSION;
this.binaryData = data;
this.time = OffsetDateTime.now(ZoneOffset.UTC);
}
private CloudEvent() {
// for deserialization
}
/**
* Deserialize {@link CloudEvent} JSON string representation that has one CloudEvent object or an array of
* CloudEvent objects into a list of CloudEvents, and validate whether any CloudEvents have null {@code id},
* {@code source}, or {@code type}. If you want to skip this validation, use {@link #fromString(String, boolean)}.
*
* Deserialize CloudEvent Samples
*
*
* List<CloudEvent> cloudEventList = CloudEvent.fromString(cloudEventJsonString);
* CloudEvent cloudEvent = cloudEventList.get(0);
* BinaryData cloudEventData = cloudEvent.getData();
*
* byte[] bytesValue = cloudEventData.toBytes(); // If data payload is in bytes (data_base64 is not null).
* User objectValue = cloudEventData.toObject(User.class); // If data payload is a User object.
* int intValue = cloudEventData.toObject(Integer.class); // If data payload is an int.
* boolean boolValue = cloudEventData.toObject(Boolean.class); // If data payload is boolean.
* String stringValue = cloudEventData.toObject(String.class); // If data payload is String.
* String jsonStringValue = cloudEventData.toString(); // The data payload represented in Json String.
*
*
*
* @param cloudEventsJson the JSON payload containing one or more events.
* @return all the events in the payload deserialized as {@link CloudEvent CloudEvents}.
* @throws NullPointerException if cloudEventsJson is null.
* @throws IllegalArgumentException if the input parameter isn't a correct JSON string for a CloudEvent or an array
* of CloudEvents, or any deserialized CloudEvents have null {@code id}, {@code source}, or {@code type}.
*/
public static List fromString(String cloudEventsJson) {
return fromString(cloudEventsJson, false);
}
/**
* Deserialize {@link CloudEvent CloudEvents} JSON string representation that has one CloudEvent object or an array
* of CloudEvent objects into a list of CloudEvents.
*
* @param cloudEventsJson the JSON payload containing one or more events.
* @param skipValidation set to true if you'd like to skip the validation for the deserialized CloudEvents. A valid
* CloudEvent should have 'id', 'source' and 'type' not null.
* @return all the events in the payload deserialized as {@link CloudEvent CloudEvents}.
* @throws NullPointerException if cloudEventsJson is null.
* @throws IllegalArgumentException if the input parameter isn't a JSON string for a CloudEvent or an array of
* CloudEvents, or skipValidation is false and any CloudEvents have null id', 'source', or 'type'.
*/
public static List fromString(String cloudEventsJson, boolean skipValidation) {
Objects.requireNonNull(cloudEventsJson, "'cloudEventsJson' cannot be null");
List cloudEvents;
try (JsonReader jsonReader = JsonProviders.createReader(cloudEventsJson)) {
JsonToken arrayOrObjectCheckToken = jsonReader.nextToken();
if (arrayOrObjectCheckToken == JsonToken.START_OBJECT) {
cloudEvents = new ArrayList<>(1);
cloudEvents.add(jsonReader.readObject(CloudEvent::fromJson));
} else if (arrayOrObjectCheckToken == JsonToken.START_ARRAY) {
cloudEvents = jsonReader.readArray(CloudEvent::fromJson);
} else if (arrayOrObjectCheckToken == JsonToken.NULL) {
return null;
} else {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("JSON string started at an invalid state "
+ "for reading a single instance or an array of CloudEvents. Starting token was: "
+ arrayOrObjectCheckToken));
}
} catch (IOException ex) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException(ex));
}
if (skipValidation || cloudEvents == null) {
return cloudEvents;
}
for (int i = 0; i < cloudEvents.size(); i++) {
CloudEvent event = cloudEvents.get(i);
if (event.getId() == null || event.getSource() == null || event.getType() == null) {
List nullAttributes = new ArrayList<>();
if (event.getId() == null) {
nullAttributes.add("'id'");
}
if (event.getSource() == null) {
nullAttributes.add("'source'");
}
if (event.getType() == null) {
nullAttributes.add("'type'");
}
throw LOGGER.logExceptionAsError(new IllegalArgumentException("'id', 'source' and 'type' are mandatory "
+ "attributes for a CloudEvent according to the spec. This JSON string doesn't have "
+ CoreUtils.stringJoin(", ", nullAttributes) + " for the object at index " + i + ". Please make "
+ "sure the input Json string has the required attributes or use "
+ "CloudEvent.fromString(cloudEventsJson, true) to skip the null check."));
}
}
return cloudEvents;
}
/**
* Get the id of the cloud event.
*
* @return the id.
*/
public String getId() {
return this.id;
}
/**
* Set a custom id. Note that a random id is already set by default.
*
* @param id the id to set.
* @return the cloud event itself.
* @throws NullPointerException if id is null.
* @throws IllegalArgumentException if id is empty.
*/
public CloudEvent setId(String id) {
Objects.requireNonNull(id, "'id' cannot be null");
if (id.isEmpty()) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException("'id' cannot be empty"));
}
this.id = id;
return this;
}
/**
* Get the source of the event.
*
* @return the source.
*/
public String getSource() {
return this.source;
}
/**
* Get the data associated with this event as a {@link BinaryData}, which has API to deserialize the data into a
* String, an Object, or a byte[].
*
* @return A {@link BinaryData} that wraps the event's data payload.
*/
public BinaryData getData() {
if (this.binaryData == null) {
if (this.data != null) {
this.binaryData = BinaryData.fromString(this.data);
} else if (this.dataBase64 != null) {
this.binaryData = BinaryData.fromBytes(Base64.getDecoder().decode(this.dataBase64));
}
}
return this.binaryData;
}
/**
* Get the type of event, e.g. "Contoso.Items.ItemReceived".
*
* @return the type of the event.
*/
public String getType() {
return this.type;
}
/**
* Get the time associated with the occurrence of the event.
*
* @return the event time, or null if the time is not set.
*/
public OffsetDateTime getTime() {
return this.time;
}
/**
* Set the time associated with the occurrence of the event.
*
* At creation, the time is set to the current UTC time. It can be unset by setting it to null.
*
* @param time the time to set.
* @return the cloud event itself.
*/
public CloudEvent setTime(OffsetDateTime time) {
this.time = time;
return this;
}
/**
* Get the content MIME type that the data is in.
*
* @return the content type the data is in, or null it is not set.
*/
public String getDataContentType() {
return this.dataContentType;
}
/**
* Get the schema that the data adheres to.
*
* @return a URI of the data schema, or null if it is not set.
*/
public String getDataSchema() {
return this.dataSchema;
}
/**
* Set the schema that the data adheres to.
*
* @param dataSchema a String identifying the schema of the data. The CNCF CloudEvent spec dataschema is
* defined as a URI. For compatibility with legacy system, this class accepts any String. But for interoperability,
* you should use a URI format string.
* @return the cloud event itself.
*/
public CloudEvent setDataSchema(String dataSchema) {
this.dataSchema = dataSchema;
return this;
}
/**
* Get the subject associated with this event.
*
* @return the subject, or null if it is not set.
*/
public String getSubject() {
return this.subject;
}
/**
* Set the subject of the event.
*
* @param subject the subject to set.
* @return the cloud event itself.
*/
public CloudEvent setSubject(String subject) {
this.subject = subject;
return this;
}
/**
* Get a map of the additional user-defined attributes associated with this event.
*
* @return an unmodifiable map of the extension attributes.
*/
@JsonAnyGetter
public Map getExtensionAttributes() {
return this.extensionAttributes == null
? Collections.emptyMap()
: Collections.unmodifiableMap(this.extensionAttributes);
}
/**
* Add/Overwrite a single extension attribute to the cloud event.
*
* @param name the name of the attribute. It must contain only lower-case alphanumeric characters and not be any
* CloudEvent reserved attribute names.
* @param value the value to associate with the name.
* @return the cloud event itself.
* @throws NullPointerException if name or value is null.
* @throws IllegalArgumentException if name format isn't correct.
*/
@JsonAnySetter
public CloudEvent addExtensionAttribute(String name, Object value) {
Objects.requireNonNull(name, "'name' cannot be null.");
Objects.requireNonNull(value, "'value' cannot be null.");
if (!validateAttributeName(name)) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException(ILLEGAL_ATTRIBUTE_NAME_MESSAGE));
}
if (this.extensionAttributes == null) {
this.extensionAttributes = new HashMap<>();
}
this.extensionAttributes.put(name, value);
return this;
}
/**
* Get the spec version. Users don't need to access it because it's always 1.0. Make it package level to test
* deserialization.
*
* @return The spec version.
*/
String getSpecVersion() {
return this.specVersion;
}
/**
* Set the spec version. Users don't need to access it because it's always 1.0. Make it package level to test
* serialization.
*
* @return the cloud event itself.
*/
CloudEvent setSpecVersion(String specVersion) {
this.specVersion = specVersion;
return this;
}
private static boolean validateAttributeName(String name) {
if (RESERVED_ATTRIBUTE_NAMES.contains(name)) {
return false;
}
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))) {
return false;
}
}
return true;
}
@Override
public JsonWriter toJson(JsonWriter jsonWriter) throws IOException {
jsonWriter.writeStartObject().writeStringField("id", id).writeStringField("source", source);
if (dataBase64 != null) {
jsonWriter.writeStringField("data_base64", dataBase64);
} else if (data != null) {
jsonWriter.writeRawField("data", data);
} else {
jsonWriter.writeNullField("data");
}
jsonWriter.writeStringField("type", type);
if (time != null) {
jsonWriter.writeStringField("time", time.toString());
}
jsonWriter.writeStringField("specversion", specVersion)
.writeStringField("dataschema", dataSchema)
.writeStringField("datacontenttype", dataContentType)
.writeStringField("subject", subject);
if (!CoreUtils.isNullOrEmpty(extensionAttributes)) {
for (Map.Entry extensionAttribute : extensionAttributes.entrySet()) {
jsonWriter.writeUntypedField(String.valueOf(extensionAttribute.getKey()),
extensionAttribute.getValue());
}
}
return jsonWriter.writeEndObject();
}
/**
* Reads a JSON stream into a {@link CloudEvent}.
*
* @param jsonReader The {@link JsonReader} being read.
* @return The {@link CloudEvent} that the JSON stream represented, or null if it pointed to JSON null.
* @throws IOException If a {@link CloudEvent} fails to be read from the {@code jsonReader}.
*/
public static CloudEvent fromJson(JsonReader jsonReader) throws IOException {
return jsonReader.readObject(reader -> {
CloudEvent cloudEvent = new CloudEvent();
while (reader.nextToken() != JsonToken.END_OBJECT) {
String fieldName = reader.getFieldName();
JsonToken token = reader.nextToken();
if ("id".equals(fieldName)) {
cloudEvent.id = reader.getString();
} else if ("source".equals(fieldName)) {
cloudEvent.source = reader.getString();
} else if ("data".equals(fieldName)) {
if (token == JsonToken.START_OBJECT || token == JsonToken.START_ARRAY) {
cloudEvent.data = jsonReader.readChildren();
} else if (token == JsonToken.STRING) {
cloudEvent.data = "\"" + jsonReader.getRawText() + "\"";
} else {
cloudEvent.data = jsonReader.getString();
}
} else if ("data_base64".equals(fieldName)) {
cloudEvent.dataBase64 = reader.getString();
} else if ("type".equals(fieldName)) {
cloudEvent.type = reader.getString();
} else if ("time".equals(fieldName)) {
cloudEvent.time = reader.getNullable(r -> OffsetDateTime.parse(r.getString()));
} else if ("specversion".equals(fieldName)) {
cloudEvent.specVersion = reader.getString();
} else if ("dataschema".equals(fieldName)) {
cloudEvent.dataSchema = reader.getString();
} else if ("datacontenttype".equals(fieldName)) {
cloudEvent.dataContentType = reader.getString();
} else if ("subject".equals(fieldName)) {
cloudEvent.subject = reader.getString();
} else {
if (cloudEvent.extensionAttributes == null) {
cloudEvent.extensionAttributes = new LinkedHashMap<>();
}
cloudEvent.extensionAttributes.put(fieldName, reader.readUntyped());
}
}
return cloudEvent;
});
}
}