Skip to content

Conversation

@cppwfs
Copy link
Contributor

@cppwfs cppwfs commented Sep 26, 2025

Introduces Cloud Events v1.0 specification support including message converters, transformers, and utilities.

Key components added:

  • CloudEventMessageConverter for message format conversion
  • ToCloudEventTransformer for transforming messages to Cloud Events
  • MessageBinaryMessageReader/Writer for binary format handling
  • CloudEventProperties for configuration management
  • Header pattern matching utilities for flexible event mapping
  • Add reference docs and what's-new paragraph

@cppwfs cppwfs requested a review from artembilan September 26, 2025 20:28
@cppwfs cppwfs force-pushed the SI-cloud-events branch 2 times, most recently from 03d1f51 to 02a1329 Compare September 26, 2025 20:47
Copy link
Member

@artembilan artembilan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some at a glance review.

Thank you!

Copy link
Member

@artembilan artembilan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for a lengthy review and some doubts I've expressed.

I addition, what are your thoughts about content of this package in Spring Cloud Function: https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent ?

I mean transformer does the trick indeed, but only from an integration flow context.
How about the way to be able to construct CloudEvent programmatically?
Or just existing SDK API is enough to deal with?

@cppwfs cppwfs force-pushed the SI-cloud-events branch 2 times, most recently from cabc5e5 to d14c5f8 Compare October 6, 2025 15:32
@cppwfs
Copy link
Contributor Author

cppwfs commented Oct 6, 2025

For now hold off on a review. I'm writing up some questions for some guidance on some of the issues you raised in the last 2 reviews.

Copy link
Member

@artembilan artembilan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for lengthy review: too many concerns.

Please, consider to rebase your branch to the latest main.
And take into account that it is already 7.1.

Thanks

Copy link
Member

@artembilan artembilan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you have addressed everything from the previous review, but probably just locally.
Please, push it into PR with rebase to the latest main.
This way I might review it again before Monday.

Thanks

@cppwfs cppwfs added this to the 7.1.0-M1 milestone Dec 29, 2025
Copy link
Member

@artembilan artembilan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for such a lengthy review before New Year 😄 .

* Create a ToCloudEventTransformer.
* @param extensionPatterns patterns to evaluate whether message headers should be added as extensions
* to the CloudEvent
* @since 7.1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This @since on methods is redundant since all of them have been introduced together with the class.

protected void onInit() {
super.onInit();
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
if (this.sourceExpression == null) { // in the case the user sets the value prior to onInit.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, remove the comment.
It does not give any value.
Plus, it sounds like does not reflect reality: the condition is about null, but sentence in comment says something opposite.

String appName = applicationContext.getEnvironment().getProperty("spring.application.name");
logger.warn("'spring.application.name' is not set. " +
"CloudEvent source URIs will use 'null' as the application name. " +
"Consider setting 'spring.application.name'");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this warning has to be conditional on appName == null

String type = this.typeExpression.getValue(this.evaluationContext, message, String.class);
MessageHeaders headers = message.getHeaders();
MessageHeaderAccessor accessor = new MessageHeaderAccessor(message);
MimeType mimeType = accessor.getContentType();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, consider to use StaticMessageHeaderAccessor.getContentType() instead.
That way we won't have an extra MessageHeaderAccessor volatile object.


==== Extension Patterns

The extensionPatterns constructor parameter is a vararg of ``String``s.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to wrap into code snippet.
Just say of strings and that's enough.
Really this sentence does not emphasize about exactly String object logic and API.

.withHeader(MessageHeaders.CONTENT_TYPE, "application/octet-stream")
.build();
// Transformer with extension patterns
ToCloudEventTransformer transformer = new ToCloudEventTransformer();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment about does not reflect this constructor behavior.

// Input message with headers
Message<byte[]> inputMessage = MessageBuilder
.withPayload("Hello CloudEvents".getBytes(StandardCharsets.UTF_8))
.withHeader(MessageHeaders.CONTENT_TYPE, "application/octet-stream")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I would say this is text/plain.
The octet-stream is too general and could be used somewhere else.
But having just text and ignoring its respective content type, might lead to a wrong impression.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to provide the output of the transformation to add clarification?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we want an output format explained somewhere, but that is not a concern of this code example.
What I mean here that Hello CloudEvents is a text/plain, not an application/octet-stream.
That's only a change I want to see in this example.
Otherwise it is misleading.

[[eventformats]]
==== EventFormats

The `ToCloudEventTransformer` uses ``EventFormat``s to serialize the CloudEvent into the message's payload.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to say more about contentType header and how it is used for EventFormat search.
I also don't see anywhere that we mention the output contentType and those ce- attributes produced by our transformer.
That is really most important part of the transformer: give a message with some data and get back a message with that data but in CloudEvents format.
And that could be done by the EventFormat delegation or flattening into message headers.
Looks like that is missing in this doc.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was my request about the transformer output.
The configuration explanation is good, but what is the result of that transformation.
That's is exactly what users are going to deal with.


assertThat(result).isNotNull();
assertThat(result).isInstanceOf(Message.class);
return (Message<byte[]>) result;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole message is redundant.
We simply can use Transformer.transform() API in all those tests.

Introduces Cloud Events v1.0 specification support
including message converters, transformers, and utilities.

Key components added:
- CloudEventMessageConverter for message format conversion
- ToCloudEventTransformer for transforming messages to Cloud Events
- MessageBinaryMessageReader/Writer for binary format handling
- CloudEventProperties for configuration management
- Header pattern matching utilities for flexible event mapping
- Add reference docs and what's-new paragraph
Remove v1 subpackage and flatten the CloudEvents package hierarchy.
Introduce strategy pattern for format conversion to replace enum-based approach,
improving extensibility and reduce dependencies.

Key changes:
- Move all classes from cloudevents.v1 to cloudevents base package
- Remove optional format dependencies (JSON, XML, Avro) from build
- Replace `ConversionType` enum with `FormatStrategy` interface
- Add `CloudEventMessageFormatStrategy` as default implementation
- Inline `HeaderPatternMatcher` logic into `ToCloudEventTransformerExtensions`
- Add `@NullMarked` package annotations and `@Nullable` throughout
- Document `targetClass` parameter behavior in `CloudEventMessageConverter`
- Split transformer tests for better organization and coverage
- Update component type identifier to "ce:to-cloudevents-transformer"
- Remove unnecessary docs from package-info
- Simplify the CloudEvent transformer by consolidating configuration
  directly into ToCloudEventTransformer class rather than using
  separate configuration objects
- Remove CloudEventProperties and ToCloudEventTransformerExtensions
  classes to reduce abstraction layers and improve maintainability
- Make MessageBinaryMessageReader package-private and convert
  CloudEventMessageConverter methods to static where possible
- Move extension filtering logic into a private inner class within
  the transformer
- Remove CloudEventsHeaders class and CE_PREFIX constant as the
  prefix is no longer used as a configurable value
Replace custom CloudEvent converter infrastructure with direct
CloudEvents SDK format implementations.

Key changes:
- Replace `FormatStrategy` pattern-based approach with direct
  `EventFormatProvider` integration from CloudEvents SDK
- Remove custom converter classes (`CloudEventMessageConverter`,
  `MessageBinaryMessageReader`, `MessageBuilderMessageWriter`)
- Simplify transformer to use Expression-based configuration for
  all CloudEvent attributes (id, source, type, dataSchema, subject)
- Add validation for required CloudEvent attributes with clear
  error messages when expressions evaluate to null or empty values
- Update documentation to reflect Expression-based API and
  byte[] payload requirement
- Consolidate tests by removing coverage for deleted converter
  infrastructure
The previous extension extraction mechanism using `Expression` arrays
and a separate `ExtensionsExtractor` interface was overly complex for
the simple use case of pattern matching header names.

This change simplifies the API by:
- Removing the `ExtensionsExtractor` interface and implementations
- Replacing `Expression`-based extension configuration with simple
  String pattern matching in the transformer constructor
- Updating all javadocs to use imperative voice per Spring
  conventions (e.g., "Converts messages" instead of "A transformer
  that converts messages")
- Making default value descriptions more concise (e.g., "Defaults
  to null" instead of "Default Expression evaluates to a null")
- Add extensionPattern match logic
Enable CloudEvents to be sent with headers
instead of requiring structured format serialization. This provides
flexibility when integrating with systems that don't support
CloudEvents structured formats.

Introduce `CloudEventMessageConverter` to handle CloudEvent to
Message conversion, utilizing the CloudEvents SDK's
`MessageWriter` abstraction.

Add `noFormat` configuration option to `ToCloudEventsTransformer`.
When enabled and no `EventFormat` is available for the content type,
CloudEvent attributes are written to message headers with
configurable prefix (defaults to "ce-").

Add `cloudEventPrefix` property to customize the header prefix
when `noFormat` is set to true, supporting different integration scenarios.

Add test coverage for binary content mode including
extension handling, custom prefixes, and validation that original
headers are preserved alongside CloudEvent headers.
Update javadoc comments in `CloudEventMessageConverter` and
`ToCloudEventsTransformer` to improve clarity and readability.
Enhance the CloudEvents reference documentation to better explain
how extensions are populated using pattern matching instead of
SpEL expressions.

Changes include:
- Clarify `CloudEventMessageConverter` class-level javadoc
- Improve `isNoFormat()` and `setNoFormat()` method documentation
- Update reference docs to reflect extension patterns approach
- Fix minor formatting issues (double spaces)
This commit updates the CloudEvents module from version 7.0 to 7.1 by
consolidating classes and improving the architecture. The previous
design separated the CloudEvent conversion logic across multiple
classes (`CloudEventMessageConverter` and `MessageBuilderMessageWriter`),
which added unnecessary complexity for what is fundamentally a single
transformation operation.

Key changes:
- Removed `CloudEventMessageConverter` and `MessageBuilderMessageWriter`
  as standalone classes
- Consolidated conversion logic into `ToCloudEventTransformer` by
  making `MessageBuilderMessageWriter` a private static inner class
- Renamed `ToCloudEventsTransformer` to `ToCloudEventTransformer` to
  match naming conventions (singular form)
- Removed Avro dependencies and associated tests that were no longer
  needed
- Changed `setNoFormat()` to `setFailOnNoFormat()` for clearer semantics
- Enhanced null safety by removing `@Nullable` from setters that should
  always have values
- Re-add isFailOnNoFormat logic and tests that was removed
- Improved documentation and added defensive copying for the
  `extensionPatterns` array parameter
- Updated tests to reflect the new class names and structure
- Rebased

The refactoring reduces the public API surface while maintaining all
functionality, making the code easier to understand and maintain.
Address multiple improvements to the CloudEvents transformer module:

- Reorganize constructor ordering in `ToCloudEventTransformer` to put
  the no-arg constructor first, following Java conventions
- Remove unused Avro test dependency from build.gradle
- Improve content type handling using `MessageHeaderAccessor`
  and `MimeType` instead of direct String extraction
- Refactor extension processing to use `Map.copyOf()` for immutability
- Rename `ToCloudEventTransformerExtensions` to singular
  `ToCloudEventTransformerExtension` for consistency
- Inline constant strings for specversion and datacontenttype keys
- Fix content type header to use `eventFormat.serializedContentType()`
  instead of the input message's content type
- Simplify binary mode message creation by constructing
  `MessageHeaders` directly
- Update test assertions to match null appName behavior
- Enhance documentation clarity: improve wording, fix grammar,
  capitalize table entries, and add examples for content types
- Restructure documentation sections for better readability
- Update `MessageBuilderMessageWriter` to implement
  `CloudEventWriterFactory` instead of `MessageWriter`
  The message writer had default functions unused by the
  read method.
- Remove unused `setEvent()` method and `dataContentTypeKey` field
  from `MessageBuilderMessageWriter`
- Apply Spring code convention

private static final String DEFAULT_PREFIX = "ce-";

private static final EventFormatProvider eventFormatProvider = EventFormatProvider.getInstance();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is static final then it has to be in upper case.
It is a constant then, therefore respective naming convention.

.setHeader(MessageHeaders.CONTENT_TYPE, eventFormat.serializedContentType())
.build();
}
HashMap<String, Object> messageMap = new HashMap<>(headers);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think putting the contentType header could be hidden in the MessageBuilderMessageWriter.
Perhaps in its end() implementation.
That way we would not need to wrap headers into a new map.

.withExtension(extensions)
.build();

EventFormat eventFormat = eventFormatProvider.resolveFormat(contentType);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have realized that provider is based on exactly cloudevents+ content types, then we cannot rely on our common types.
I think we should come back to the idea about an EventFormat injection into this our transformer.
(My apologies that I misled to the EventFormatProvider).

We probably still can use an EventFormatProvider, however not against standard contentType header, but with some custom SpEL expression.
This way the logic would be:

  1. Use injected EventFormat (if any)
  2. Resolve eventFormatContentTypeExpression (if any)
  3. Call EventFormatProvider for the previous expression result.
  4. If no such a format, fail if failOnNoFormat
  5. Still use an input contentType header as a dataContentType in the CloudEvent
  6. Still populate a proper output contentType from the EventFormat or standard one in the MessageBuilderMessageWriter

WDYT?

assertThat(cloudEvent.getData().toBytes()).isEqualTo(PAYLOAD);
assertThat(cloudEvent.getSource().toString()).isEqualTo("/spring/null.transformerWithNoExtensions");
assertThat(cloudEvent.getDataSchema()).isNull();
assertThat(cloudEvent.getDataContentType()).isEqualTo(JsonFormat.CONTENT_TYPE);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the current behavior reflects whatever is possible with the current implementation.
However that is not what we want with our transformer.
See my suggestion above about an EventFormat injection and so on.


@Test
void emptyStringPayloadHandling() {
Message<byte[]> message = createBaseMessage("".getBytes(), "application/cloudevents+json").build();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to close the loop: in some other comment about I've suggested an EventFormat injection or respective eventFormatContentTypeExpression instead of this not fully flexible logic against a input contentType header.

| "spring.message"

| `dataContentType`
| The contentType of the message, defaults to `application/octet-stream`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, examples here you provide are exactly what I would expect sending a text/json in the contentType to our transformer. So, this value would be copied to the dataContentType attribute of the cloud event.
Meanwhile the output contentType header would be applicaiton/cloudevents (or with + if format is involved).

// Input message with headers
Message<byte[]> inputMessage = MessageBuilder
.withPayload("Hello CloudEvents".getBytes(StandardCharsets.UTF_8))
.withHeader(MessageHeaders.CONTENT_TYPE, "application/octet-stream")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we want an output format explained somewhere, but that is not a concern of this code example.
What I mean here that Hello CloudEvents is a text/plain, not an application/octet-stream.
That's only a change I want to see in this example.
Otherwise it is misleading.

[[eventformats]]
==== EventFormats

The `ToCloudEventTransformer` uses ``EventFormat``s to serialize the CloudEvent into the message's payload.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was my request about the transformer output.
The configuration explanation is good, but what is the result of that transformation.
That's is exactly what users are going to deal with.

Code quality improvements:
- Remove unnecessary `@since 7.1` tags from internal javadocs
- Fix NullAway suppressions by adding proper null checks
- Replace `MessageHeaderAccessor` with `StaticMessageHeaderAccessor`
- Eliminate redundant field initialization (failOnNoFormat defaults to false)
- Simplify extension comparison using `Boolean.TRUE.equals()`
- Remove unnecessary Map.copyOf() in extension implementation
- Fix app name null check to only warn when actually null

Test refactoring:
- Make test helper methods static for better encapsulation
- Rename test methods to clearly indicate content type being tested
- Separate JSON format vs application/json content type test cases
- Add explicit verification for application/* content types
- Improve test assertions using containsEntry() instead of get()
- Remove unnecessary null checks in tests
- Consolidate duplicate test setup code

Documentation:
- Add package-info.java descriptions for cloudevents packages
- Update reference documentation for extension patterns clarity
private Map<String, Object> getCloudEventExtensions(MessageHeaders headers) {
Map<String, Object> cloudEventExtensions = new HashMap<>();
Boolean patternResult;
String headerKey;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These variables really belong to the loop scope below.
Why would you declare them outside of the loop?

* Perform attribute and extension mapping based on {@link Expression}s.
*
* @author Glenn Renfro
* @since 7.1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed blank line after @author


/**
* Convert messages to CloudEvent format.
* Perform attribute and extension mapping based on {@link Expression}s.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK with attribute.
But extension is not does by expression.
Please, consider to revise this Javadoc to reflect reality about the transformer logic.
I guess we can leave EventFormat out of this for now, since we don't have a final solution yet.
But extensionPatterns could be already explained in this Javadocs properly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants