Skip to content
Open
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
16 changes: 16 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ ext {
avroVersion = '1.12.1'
awaitilityVersion = '4.3.0'
camelVersion = '4.15.0'
cloudEventsVersion = '4.0.1'
commonsDbcp2Version = '2.13.0'
commonsIoVersion = '2.20.0'
commonsNetVersion = '3.12.0'
Expand Down Expand Up @@ -486,6 +487,21 @@ project('spring-integration-cassandra') {
}
}

project('spring-integration-cloudevents') {
description = 'Spring Integration CloudEvents Support'

dependencies {
api "io.cloudevents:cloudevents-core:$cloudEventsVersion"
testImplementation "io.cloudevents:cloudevents-json-jackson:$cloudEventsVersion"

testImplementation("io.cloudevents:cloudevents-avro-compact:$cloudEventsVersion") {
exclude group: 'org.apache.avro', module: 'avro'
}
testImplementation "org.apache.avro:avro:$avroVersion"
testImplementation "io.cloudevents:cloudevents-xml:$cloudEventsVersion"
}
}

project('spring-integration-core') {
description = 'Spring Integration Core'

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

@org.jspecify.annotations.NullMarked
package org.springframework.integration.cloudevents;
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2025-present the original author or authors.
*
* 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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* 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 org.springframework.integration.cloudevents.transformer;

import java.util.Objects;

import io.cloudevents.CloudEvent;
import io.cloudevents.core.CloudEventUtils;
import org.jspecify.annotations.Nullable;

import org.springframework.integration.transformer.MessageTransformationException;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.converter.MessageConverter;

/**
* Convert Spring Integration {@link Message}s to CloudEvents.
*
* @author Glenn Renfro
*
* @since 7.0
*/
class CloudEventMessageConverter implements MessageConverter {

private final String cloudEventPrefix;

private final String specVersionKey;

private final String dataContentTypeKey;

/**
* Construct a CloudEventMessageConverter with the specified configuration.
* @param cloudEventPrefix the prefix for CloudEvent headers in binary content mode
* @param specVersionKey the header name for the specification version
* @param dataContentTypeKey the header name for the data content type
*/
CloudEventMessageConverter(String cloudEventPrefix, String specVersionKey, String dataContentTypeKey) {
this.cloudEventPrefix = cloudEventPrefix;
this.specVersionKey = specVersionKey;
this.dataContentTypeKey = dataContentTypeKey;
}

/**
* This converter only supports CloudEvent to Message conversion.
* @throws UnsupportedOperationException always, as this operation is not supported
*/
@Override
public @Nullable Object fromMessage(Message<?> message, Class<?> targetClass) {
throw new UnsupportedOperationException("CloudEventMessageConverter does not support fromMessage method");
}

@Override
public Message<?> toMessage(Object payload, @Nullable MessageHeaders headers) {
if (payload instanceof CloudEvent event) {
return CloudEventUtils.toReader(event).read(new MessageBuilderMessageWriter(this.cloudEventPrefix,
this.specVersionKey, this.dataContentTypeKey, Objects.requireNonNull(headers)));
}
throw new MessageTransformationException("Unsupported payload type. Should be CloudEvent but was: " +
payload.getClass());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Copyright 2025-present the original author or authors.
*
* 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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* 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 org.springframework.integration.cloudevents.transformer;

import java.util.HashMap;
import java.util.Map;

import io.cloudevents.CloudEventData;
import io.cloudevents.SpecVersion;
import io.cloudevents.core.format.EventFormat;
import io.cloudevents.core.message.MessageWriter;
import io.cloudevents.rw.CloudEventContextWriter;
import io.cloudevents.rw.CloudEventRWException;
import io.cloudevents.rw.CloudEventWriter;

import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;

/**
* Adapt CloudEvents to Spring Integration {@link Message}s using the CloudEvents SDK
* {@link MessageWriter} abstraction.
* Write CloudEvent attributes as message headers with a configurable prefix for
* binary content mode serialization. Used internally by {@link CloudEventMessageConverter}
* to convert CloudEvent objects into Spring Integration messages.
*
* @author Glenn Renfro
*
* @since 7.0
*
* @see CloudEventMessageConverter
*/
class MessageBuilderMessageWriter
implements CloudEventWriter<Message<byte[]>>, MessageWriter<MessageBuilderMessageWriter, Message<byte[]>> {

private final String cloudEventPrefix;

private final String specVersionKey;

private final String dataContentTypeKey;

private final Map<String, Object> headers = new HashMap<>();

/**
* Construct a MessageBuilderMessageWriter with the specified configuration.
* @param cloudEventPrefix the prefix to prepend to CloudEvent attribute names in message headers
* @param specVersionKey the header name for the CloudEvent specification version
* @param dataContentTypeKey the header name for the data content type
* @param headers the base message headers to include in the output message
*/
MessageBuilderMessageWriter(String cloudEventPrefix, String specVersionKey, String dataContentTypeKey, Map<String, Object> headers) {
this.headers.putAll(headers);
this.cloudEventPrefix = cloudEventPrefix;
this.specVersionKey = specVersionKey;
this.dataContentTypeKey = dataContentTypeKey;
}

/**
* Set the event in structured content mode.
* Create a message with the serialized CloudEvent as the payload and set the
* data content type header to the format's serialized content type.
* @param format the event format used to serialize the CloudEvent
* @param value the serialized CloudEvent bytes
* @return the Spring Integration message containing the serialized CloudEvent
* @throws CloudEventRWException if an error occurs during message creation
*/
@Override
public Message<byte[]> setEvent(EventFormat format, byte[] value) throws CloudEventRWException {
this.headers.put(this.dataContentTypeKey, format.serializedContentType());
return MessageBuilder.withPayload(value).copyHeaders(this.headers).build();
}

/**
* Complete the message creation with CloudEvent data.
* Create a message with the CloudEvent data as the payload. CloudEvent attributes
* are already set as headers via {@link #withContextAttribute(String, String)}.
* @param value the CloudEvent data to use as the message payload
* @return the Spring Integration message with CloudEvent data and attributes
* @throws CloudEventRWException if an error occurs during message creation
*/
@Override
public Message<byte[]> end(CloudEventData value) throws CloudEventRWException {
return MessageBuilder.withPayload(value.toBytes()).copyHeaders(this.headers).build();
}

/**
* Complete the message creation without CloudEvent data.
* Create a message with an empty payload when the CloudEvent contains no data.
* CloudEvent attributes are set as headers via {@link #withContextAttribute(String, String)}.
* @return the Spring Integration message with an empty payload and CloudEvent attributes as headers
*/
@Override
public Message<byte[]> end() {
return MessageBuilder.withPayload(new byte[0]).copyHeaders(this.headers).build();
}

/**
* Add a CloudEvent context attribute to the message headers.
* Map the CloudEvent attribute to a message header by prepending the configured prefix
* to the attribute name (e.g., "id" becomes "ce-id" with default prefix).
* @param name the CloudEvent attribute name
* @param value the CloudEvent attribute value
* @return this writer for method chaining
* @throws CloudEventRWException if an error occurs while setting the attribute
*/
@Override
public CloudEventContextWriter withContextAttribute(String name, String value) throws CloudEventRWException {
this.headers.put(this.cloudEventPrefix + name, value);
return this;
}

/**
* Initialize the writer with the CloudEvent specification version.
* Set the specification version as a message header using the configured version key.
* @param version the CloudEvent specification version
* @return this writer for method chaining
*/
@Override
public MessageBuilderMessageWriter create(SpecVersion version) {
this.headers.put(this.specVersionKey, version.toString());
return this;
}

}
Loading