Skip to content

Remove deprecated APIs from CAP Java Messaging docs #1843

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
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
143 changes: 32 additions & 111 deletions java/messaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ With the availability of a messaging service, you can now use it to send message
MessagingService messagingService;

// Sending via the technical API of the messaging service
messagingService.emit("My/Destination/Messaging/Topic", "raw message payload");
messagingService.emit("My/Topic", Map.of("message", "hello world"));

// Sending by emitting a context via CAP service API
TopicMessageEventContext context = TopicMessageEventContext.create("My/Destination/Messaging/Topic");
context.setData("raw message payload");
TopicMessageEventContext context = TopicMessageEventContext.create("My/Topic");
context.setDataMap(Map.of("message", "hello world"));
messagingService.emit(context);
```

Expand All @@ -99,11 +99,11 @@ To receive messages matching a desired topic from a message broker, you just nee
Example:

```java
@On(service = "messaging-name", event = "My/Destination/Messaging/Topic")
@On(service = "messaging-name", event = "My/Topic")
public void receiveMyTopic(TopicMessageEventContext context) {
// get ID and payload of message
String msgId = context.getMessageId();
String payload = context.getData();
Map<String, Object> payload = context.getDataMap();
// ...
}
```
Expand Down Expand Up @@ -483,11 +483,11 @@ cds:
routes:
- service: "em-instance-01"
events:
- "My/Destination/Messaging/A"
- "My/Topic/A"
- service: "em-instance-02"
events:
- "My/Destination/Messaging/B"
- "My/Destination/Messaging/C*"
- "My/Topic/B"
- "My/Topic/C*"
```
:::

Expand All @@ -498,11 +498,11 @@ To use such a configuration, you need to use the composite messaging service for
@Qualifier(MessagingService.COMPOSITE_NAME)
MessagingService messagingService;

messagingService.emit("My/Destination/Messaging/A", "raw message payload to em-instance-01");
messagingService.emit("My/Destination/Messaging/B", "raw message payload to em-instance-02");
messagingService.emit("My/Topic/A", Map.of("hello", "instance 01"));
messagingService.emit("My/Topic/B", Map.of("hello", "instance 02"));
```

As you can see in the configuration, the usage and routing of two messaging services is defined (`em-instance-01`, and `em-instance-02`), each with different topics that should be routed via the service (for example, topic `My/Destination/Messaging/A` will be sent/received via `em-instance-01`, and topic `My/Destination/Messaging/B` will be sent/received via `em-instance-02`). The composite service uses the routing configuration in order to dispatch messages as well as subscriptions to the appropriate messaging service. As shown in the sample code, you can simply use the composite message service and submit messages to topics as desired. The messages will be routed to according messaging services as defined in the configuration automatically. To change the routing of messages you can simply change the configuration, without the need of changing your code.
As you can see in the configuration, the usage and routing of two messaging services is defined (`em-instance-01`, and `em-instance-02`), each with different topics that should be routed via the service (for example, topic `My/Topic/A` will be sent/received via `em-instance-01`, and topic `My/Topic/B` will be sent/received via `em-instance-02`). The composite service uses the routing configuration in order to dispatch messages as well as subscriptions to the appropriate messaging service. As shown in the sample code, you can simply use the composite message service and submit messages to topics as desired. The messages will be routed to according messaging services as defined in the configuration automatically. To change the routing of messages you can simply change the configuration, without the need of changing your code.

::: tip
If you emit messages with a topic to the composite messaging service that isn't defined in its routing configuration, then the delivery will fail. Consider careful review of your configuration, when you start sending/receiving messages from/to new topics.
Expand All @@ -511,12 +511,12 @@ If you emit messages with a topic to the composite messaging service that isn't
Example for receiving messages with a given topic via the composite messaging service:

```java
@On(service = MessagingService.COMPOSITE_NAME, event = "My/Destination/Messaging/A")
@On(service = MessagingService.COMPOSITE_NAME, event = "My/Topic/A")
public void receiveA(TopicMessageEventContext context) {
...
}

@On(service = MessagingService.COMPOSITE_NAME, event = "My/Destination/Messaging/B")
@On(service = MessagingService.COMPOSITE_NAME, event = "My/Topic/B")
public void receiveB(TopicMessageEventContext context) {
...
}
Expand Down Expand Up @@ -616,7 +616,7 @@ cds:
@On(service = "messaging-name", event = "my-custom-queue")
public void receiveMyCustomQueueMessage(TopicMessageEventContext context) {
// access the message as usual
String payload = context.getData();
Map<String, Object> payload = context.getDataMap();
}
```

Expand All @@ -638,7 +638,7 @@ cds:
@On(service = "messaging-name")
public void receiveMyCustomQueueAllMessages(TopicMessageEventContext context) {
// access the message as usual
String payload = context.getData();
Map<String, Object> payload = context.getDataMap();
// ...
}
```
Expand Down Expand Up @@ -691,16 +691,17 @@ private void handleError(MessagingErrorEventContext ctx) {

// how to access the event context of the raised exception:
// ctx.getException().getEventContexts().stream().findFirst().ifPresent(e -> {
// TopicMessageEventContext errorContext = e.as(TopicMessageEventContext.class);
// String event = e.getEvent());
// String payload = e.get("data"));
// Map<String, Object> payload = errorContext.getDataMap();
// });

ctx.setResult(true); // acknowledge
}
}
```

In a multi-tenant setup with several microservices, messages of a tenant not yet subscribed to the own microservice would be already received from the message queue. In this case, the message cannot be processed for the tenant because the tenant context is not yet available. By default, the standard error handler still acknowledges the message to prevent it from getting stuck in the message sequence. To change this behavior, the custom error handler from the example above can be extended by checking the exception type of the unknown tenant.
In a multi-tenant setup with several microservices, messages of a tenant not yet subscribed to the own microservice would be already received from the message queue. In this case, the message cannot be processed for the tenant because the tenant context is not yet available. By default, the standard error handler still acknowledges the message to prevent it from getting stuck in the message sequence. To change this behavior, the custom error handler from the example above can be extended by checking the exception type of the unknown tenant.


```java
Expand All @@ -712,15 +713,15 @@ private void handleError(MessagingErrorEventContext ctx) {
errorCode.equals(CdsErrorStatuses.INVALID_DATA_FORMAT.getCodeString())) {
// error handling for infrastructure error
ctx.setResult(false); // no acknowledgement

} else if (errorCode.equals(CdsErrorStatuses.TENANT_NOT_EXISTS.getCodeString())) {
// error handling for unknown tenant context

// tenant of the received message
String tenant = ctx.getTenant();

// received message
Map<String, Object> headers = ctx.getMessageHeaders();
Map<String, Object> headers = ctx.getMessageHeaders();
Map<String, Object> message = ctx.getMessageData();

ctx.setResult(true); // acknowledge
Expand All @@ -729,8 +730,9 @@ private void handleError(MessagingErrorEventContext ctx) {

// how to access the event context of the raised exception:
// ctx.getException().getEventContexts().stream().findFirst().ifPresent(e -> {
// TopicMessageEventContext errorContext = e.as(TopicMessageEventContext.class);
// String event = e.getEvent());
// String payload = e.get("data"));
// Map<String, Object> payload = errorContext.getDataMap();
// });

ctx.setResult(true); // acknowledge
Expand Down Expand Up @@ -768,7 +770,7 @@ If you want to consume the message purely locally, and prevent the message from
You can register your handler with a higher priority than the default handler like this:

```java
@On(service = "messaging-name", event = "My/Destination/Messaging/Topic")
@On(service = "messaging-name", event = "My/Topic")
@HandlerOrder(HandlerOrder.EARLY)
public void receiveMyTopic(TopicMessageEventContext context) {
// Check if message is outgoing to message broker, or incoming from message broker
Expand Down Expand Up @@ -808,99 +810,20 @@ When using SAP Event Mesh, the placeholder `$namespace` can be used to dynamical
Besides these kinds of topic manipulations, additional topic manipulations might occur, depending on the used message broker or the chosen format of the event message.


### Enhanced Messages Representation

The configuration property `structured` determines if messages are represented as a plain String (`false`) or always structured as two separate maps, representing data and headers (`true`). Setting this property enables handling of message headers, like `cloudevents` headers, separately from the message itself. This works for all messaging brokers supported by CAP. If using a message broker that supports native headers, for example Kafka, the headers are separated from the business data. On incoming messages the flag determines the internal representation of the message either as a plain string or two maps of message data and message headers. Having header data separated, avoids adding extra information or metadata as part of the business data when sending them to the message broker. Additionally the header data is clearly separated on the consumer side, because they provided by different data and headers maps.

The default value for the configuration property `structured` is `true`.

Configuration example:
::: code-group
```yaml [srv/src/main/resources/application.yaml]
cds:
messaging.services:
- name: "messaging-name"
kind: "enterprise-messaging"
structured: true
```
:::
#### Emitting Events

The interface `MessagingService` provides a new method for emitting events with a data map and a headers map:

```java
void emit(String topic, Map<String, Object> data, Map<String, Object> headers);
```

This method takes a (`cloudevents`) message, separated into data and headers and sends it to the specified topic of this message broker. It produces the same final message, regardless of the structured flag. Usually data and headers are combined into a final JSON string message following the rule: `{...headers, data: data}`. Brokers that natively support headers, for example Kafka, are able to separate headers from data, when using this method.

```java
String topic;
MessagingService messagingService;

messagingService.emit(topic, Map.of("firstname", "John", "lastname", "Doe"), Map.of("timestamp", Instant.now()));
```

The semantics of the method `MessagingService.emit(String topic, String message)` has been changed depending on the structured flag: If the service is not configured with the structured flag (default), the message is sent to the specified topic of this message broker as is. If the service is configured with the structured flag, the message string is converted into a map following the rule: `{message: message}`. The map is then interpreted as data map and passed to `MessagingService.emit(String topic, Map<String, Object> dataMap)`. Usually this results in a final message like: `{data: {message: message}}`.

Example:

```java
String topic;
MessagingService messagingService;

messagingService.emit(topic, "hello world");
```

If the service is not configured with the structured flag, the message is sent as is and on the consumer side `TopicMessageEventContext.getData()` returns:

```txt
hello world
```

If the service is configured with the structured flag, the message is converted to a map and on the consumer side `TopicMessageEventContext.getData()` returns:

```json
{"data": {"message": "hello world"}}
```
### Messages Representation
{#enhanced-messages-representation}

#### Handling events
Messages are provided as two separate `Map` objects, representing data and headers. This enables handling of message headers, like `cloudevents` headers, separately from the message data itself. If using a message broker that supports native headers, for example Kafka, the headers are separated from the business data in the message broker. For message brokers that don't natively support headers, headers and data are merged into a combined structure by following the rule `{...headers, data: data}`.

The structured flag of the consumer determines how the event payload is provided by the event context.

If set to `false`, the event payload can be accessed using `getData()` as a string; `getDataMap()` and `getHeadersMap()` return `null`:

```java
@On(event = "myEvent")
public void handleMyEvent(EventContext context) {
TopicMessageEventContext ctx = context.as(TopicMessageEventContext.class);
String data = ctx.getData();

// ...
}
```

If set to `true`, the event payload can be accessed using `getDataMap()` and `getHeadersMap()`; `getData()` returns null:

```java
@On(event = "myEvent")
public void handleMyEvent(EventContext context) {
TopicMessageEventContext ctx = context.as(TopicMessageEventContext.class);
Map<String, Object> data = ctx.getDataMap();
Map<String, Object> headers = ctx.getHeadersMap();

// ...
}
```
Message data is always structured and when sent to message brokers serialized to JSON. In case a message data is received, that is not a JSON string and can't be parsed as a `Map`, the plain message data is wrapped into a `Map` structure with key `message`.

::: tip
Handling of CDS-defined events is independent of value of the `structured` property.
[Events declared in the CDS model](#cds-declared-events) always result in a structured message data.
:::


### CloudEvents

CAP is able to produce event messages compatible with the [CloudEvents](https://cloudevents.io/) standard in JSON format. To enable this feature, set the configuration parameter `format` of the messaging service to `cloudevents`, for example, like:
CAP is able to produce event messages compatible with the [CloudEvents](https://cloudevents.io/) standard. To enable this feature, set the configuration parameter `format` of the messaging service to `cloudevents`, for example, like:

Excerpt from _application.yaml_:
::: code-group
Expand All @@ -913,11 +836,11 @@ cds:
format: "cloudevents"
```
:::
With this setting, basic header fields (like `type`, `source`, `id`, `datacontenttype`, `specversion`, `time`) of the JSON-based message format will be populated with sensible data (if they have not been set manually before). The event name will be used as-is (without prefixing or any other modifications) as `type` and set in the according CloudEvents header field.
With this setting, basic header fields (like `type`, `source`, `id`, `datacontenttype`, `specversion`, `time`) will be populated with sensible data (if they have not been set manually before). The event name will be used as-is (without prefixing or any other modifications) as `type` and set in the according CloudEvents header field.

When using CloudEvents format with SAP Event Mesh, the following default prefixing of topics will be applied (if not manually declared differently): Default for `publishPrefix` is set to `$namespace/ce/` and default for `subscribePrefix` is set to `+/+/+/ce/`. Make sure that these prefixes are allowed topic prefixes in your SAP Event Mesh service configuration (especially its topic rules section).

When using a CAP service that has [events declared in its CDS model](#cds-declared-events), then the event's payload structure will automatically be embedded in the `data` attribute of a valid JSON-based CloudEvents message.
The message data will automatically be embedded in the `data` attribute of the CloudEvents message.

::: tip
Headers of the CloudEvents message can be accessed using the EventContext generated for this event by using its generic `get(String)` API.
Expand All @@ -935,8 +858,6 @@ private void ratingChanged(ReviewedContext context) {
}
```

When using a CAP messaging service directly to emit the raw message payload as a String, please make sure to emit a valid JSON object representation in this String that has the message payload embedded in the `data` attribute (for example `messagingService.emit("sap.cap.reviews.v1.ReviewService.changed.v1", "{"data":{"subject":"4711","rating":3.6}}");`). Then all missing header fields that are required for a valid CloudEvents message will be extended. If you emit a String that is not a valid JSON, then the message cannot be extended and the String will be emitted as-is as the event message content.

[Learn more about **CloudEvents**.](../guides/messaging/#cloudevents){.learn-more}

<span id="aftercloudevents" />
2 changes: 1 addition & 1 deletion java/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ Some property defaults have been adjusted:
| `cds.sql.hana.optimizationMode` | `legacy` | `hex` | SQL for SAP HANA is optimized for the HEX engine. |
| `cds.odataV4.lazyI18n.enabled` | `null` | `true` | Lazy localization is now enabled by default in multitenant scenarios. |
| `cds.auditLog.personalData.`<br>`throwOnMissingDataSubject` | `false` | `true` | Raise errors for incomplete personal data annotations by default. |
| `cds.messaging.services.<key>.structured` | `false` | `true` | [Enhanced message representation](./messaging.md#enhanced-messages-representation) is now enabled by default. |
| `cds.messaging.services.<key>.structured` | `false` | `true` | [Structured message representation](./messaging.md#messages-representation) is now enabled by default. |

### Adjusted Property Behavior

Expand Down