All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.smallrye.reactive.messaging.Messages Maven / Gradle / Ivy

The newest version!
package io.smallrye.reactive.messaging;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.eclipse.microprofile.reactive.messaging.Message;
import org.eclipse.microprofile.reactive.messaging.Metadata;

import io.smallrye.common.annotation.CheckReturnValue;

/**
 * Utilities for handling coordination between messages.
 */
public interface Messages {

    /**
     * Chains the given message with some other messages.
     * It coordinates the acknowledgement. When all the other messages are acknowledged successfully, the passed
     * message is acknowledged. If one of the other messages is acknowledged negatively, the passed message is also
     * nacked (with the same reason). Subsequent ack/nack will be ignored.
     * 

* * @param message the message * @return the chain builder that let you decide how the metadata are passed, and the set of messages. */ @CheckReturnValue static MessageChainBuilder chain(Message message) { return new MessageChainBuilder(message); } /** * Merges multiple messages into a single one. * This is an implementation of a merge pattern: n messages combined into 1. *

* Whe resulting message payload is computed using the combinator function. * When the returned message is acked/nacked, the passes messages are acked/nacked accordingly. *

* Metadata are also merged. The metadata of all the messages are copied into the resulting message. If, for a given * class, the metadata is already present in the result message, it's either ignored, or merged if the class * implements {@link MergeableMetadata}. * * @param list the list of message, must not be empty, must not be null * @param combinator the combinator method, must not be null * @param the payload type of the produced message * @return the resulting message */ static Message merge(List> list, Function, T> combinator) { if (list.isEmpty()) { return Message.of(combinator.apply(Collections.emptyList())); } T payload; try { payload = combinator.apply(list.stream().map(Message::getPayload).collect(Collectors.toList())); } catch (Exception e) { // Nack all the messages; list.forEach(m -> m.nack(e)); throw e; } Function> ack = metadata -> { List> acks = new ArrayList<>(); for (Message message : list) { acks.add(message.ack(metadata).toCompletableFuture()); } return CompletableFuture.allOf(acks.toArray(new CompletableFuture[0])); }; BiFunction> nack = (metadata, throwable) -> { List> nacks = new ArrayList<>(); for (Message message : list) { nacks.add(message.nack(metadata, throwable).toCompletableFuture()); } return CompletableFuture.allOf(nacks.toArray(new CompletableFuture[0])); }; Metadata metadata = list.get(0).getMetadata(); for (int i = 1; i < list.size(); i++) { Metadata other = list.get(i).getMetadata(); metadata = merge(metadata, other); } return Message.of(payload) .withAckWithMetadata(ack) .withNackWithMetadata(nack) .withMetadata(metadata); } /** * Merges multiple messages into a single one. *

* Whe resulting message payload is computed using the combinator function. * When the returned message is acked/nacked, the passes messages are acked/nacked accordingly. *

* Metadata are also merged. The metadata of all the messages are copied into the resulting message. If, for a given * class, the metadata is already present in the result message, it's either ignored, or merged if the class * implements {@link MergeableMetadata}. * * @param list the list of message, must not be empty, must not be null * @param the payload type of the passed messages * @return the resulting message */ static Message> merge(List> list) { if (list.isEmpty()) { return Message.of(Collections.emptyList()); } List payload = list.stream().map(Message::getPayload).collect(Collectors.toList()); Function> ack = metadata -> { List> acks = new ArrayList<>(); for (Message message : list) { acks.add(message.ack(metadata).toCompletableFuture()); } return CompletableFuture.allOf(acks.toArray(new CompletableFuture[0])); }; BiFunction> nack = (metadata, throwable) -> { List> nacks = new ArrayList<>(); for (Message message : list) { nacks.add(message.nack(metadata, throwable).toCompletableFuture()); } return CompletableFuture.allOf(nacks.toArray(new CompletableFuture[0])); }; Metadata metadata = list.get(0).getMetadata(); for (int i = 1; i < list.size(); i++) { Metadata other = list.get(i).getMetadata(); metadata = merge(metadata, other); } return Message.of(payload) .withAckWithMetadata(ack) .withNackWithMetadata(nack) .withMetadata(metadata); } @SuppressWarnings({ "unchecked", "rawtypes" }) static Metadata merge(Metadata first, Metadata second) { Metadata result = first; for (Object meta : second) { Class clazz = meta.getClass(); Optional value = result.get(clazz); // Do we already have a value? if (value.isEmpty()) { // No, just add the value from the second metadata result = result.with(meta); } else { // Yes, is it mergeable. if (MergeableMetadata.class.isAssignableFrom(clazz)) { // Yes - merge MergeableMetadata current = (MergeableMetadata) value.get(); Object merged = current.merge(meta); if (merged != null) { result = result.with(merged); } else { // null is an exception, it means that the value must not be added to the metadata // at all. result = result.without(clazz); } } else { // No, keep current one. } } } return result; } /** * The message chain builder allows chaining message and configure metadata propagation. * By default, all the metadata from the given message are copied into the chained messages. */ class MessageChainBuilder { private final Message input; private Metadata metadata; private MessageChainBuilder(Message message) { this.input = message; this.metadata = message.getMetadata().copy(); } /** * Do not copy any metadata from the initial message to the chained message. * * @return the current {@link MessageChainBuilder} */ @CheckReturnValue public MessageChainBuilder withoutMetadata() { this.metadata = Metadata.empty(); return this; } /** * Copy the given metadata of the given classes from the initial message to the chained message, if the initial * message does not include a metadata object of the given class. * * In general, this method must be used after {@link #withoutMetadata()}. * * @return the current {@link MessageChainBuilder} */ @CheckReturnValue public MessageChainBuilder withMetadata(Class... mc) { for (Class clazz : mc) { Optional o = input.getMetadata().get(clazz); o.ifPresent(value -> this.metadata = metadata.with(value)); } return this; } /** * Do not the given metadata of the given classes from the initial message to the chained message, if the initial * message does not include a metadata object of the given class. * * @return the current {@link MessageChainBuilder} */ @CheckReturnValue public MessageChainBuilder withoutMetadata(Class... mc) { for (Class clazz : mc) { this.metadata = this.metadata.without(clazz); } return this; } /** * Chain the passed array of messages. * The messages are not modified, but should not be used afterward, and should be replaced by the messages contained * in the returned list. * This method preserve the order. So, the first message corresponds to the first message in the returned list. * The message from the returned list have the necessary logic to chain the ack/nack signals and the copied metadata. * * @param messages the chained messages, must not be empty, must not be null, must not contain null * @return the list of modified messages */ public List> with(Message... messages) { AtomicBoolean done = new AtomicBoolean(); // Must be modifiable List> trackers = Arrays.stream(messages).collect(Collectors.toCollection(CopyOnWriteArrayList::new)); List> outcomes = new ArrayList<>(); for (Message message : messages) { Message tmp = message; for (Object metadatum : metadata) { tmp = tmp.addMetadata(metadatum); } outcomes.add(tmp .withAckWithMetadata((metadata) -> { CompletionStage acked = message.ack(metadata); if (trackers.remove(message)) { if (trackers.isEmpty() && done.compareAndSet(false, true)) { return acked.thenCompose(x -> input.ack(metadata)); } } return acked; }) .withNackWithMetadata((reason, metadata) -> { CompletionStage nacked = message.nack(reason, metadata); if (trackers.remove(message)) { if (done.compareAndSet(false, true)) { return nacked.thenCompose(x -> input.nack(reason, metadata)); } } return nacked; })); } return outcomes; } /** * Chain the passed map of messages. * The messages are not modified, but should not be used afterward, and should be replaced by the messages * contained in the returned {@link TargetedMessages}. * Returned {@link TargetedMessages} keeps the same channel keys as the passed map. * Messages from the returned {@link TargetedMessages} have the necessary logic to chain the ack/nack signals * and the copied metadata. * * @param messages the map of channel name to message * @return the {@link TargetedMessages} containing modified messages with chained acknowledgement to the input */ public TargetedMessages with(Map> messages) { AtomicBoolean done = new AtomicBoolean(); // Must be modifiable List> trackers = new CopyOnWriteArrayList<>(messages.values()); Map> outcomes = new HashMap<>(); for (Map.Entry> entry : messages.entrySet()) { String key = entry.getKey(); Message message = entry.getValue(); Message tmp = message; for (Object metadatum : metadata) { tmp = tmp.addMetadata(metadatum); } outcomes.put(key, tmp .withAckWithMetadata(metadata -> { CompletionStage acked = message.ack(metadata); if (trackers.remove(message)) { if (trackers.isEmpty() && done.compareAndSet(false, true)) { return acked.thenCompose(x -> input.ack(metadata)); } } return acked; }) .withNackWithMetadata((reason, metadata) -> { CompletionStage nacked = message.nack(reason, metadata); if (trackers.remove(message)) { if (done.compareAndSet(false, true)) { return nacked.thenCompose(x -> input.nack(reason, metadata)); } } return nacked; })); } return TargetedMessages.from(outcomes); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy