|
1 | 1 | package io.smallrye.reactive.messaging; |
2 | 2 |
|
3 | 3 | import java.util.ArrayList; |
| 4 | +import java.util.Arrays; |
4 | 5 | import java.util.Collections; |
5 | 6 | import java.util.List; |
6 | 7 | import java.util.Optional; |
7 | 8 | import java.util.concurrent.CompletableFuture; |
8 | 9 | import java.util.concurrent.CompletionStage; |
| 10 | +import java.util.concurrent.CopyOnWriteArrayList; |
| 11 | +import java.util.concurrent.atomic.AtomicBoolean; |
9 | 12 | import java.util.function.Function; |
10 | 13 | import java.util.function.Supplier; |
11 | 14 | import java.util.stream.Collectors; |
12 | 15 |
|
13 | 16 | import org.eclipse.microprofile.reactive.messaging.Message; |
14 | 17 | import org.eclipse.microprofile.reactive.messaging.Metadata; |
15 | 18 |
|
| 19 | +import io.smallrye.common.annotation.CheckReturnValue; |
| 20 | + |
| 21 | +/** |
| 22 | + * A class handling coordination between messages. |
| 23 | + */ |
16 | 24 | public class Messages { |
17 | 25 |
|
18 | 26 | private Messages() { |
19 | 27 | // Avoid direct instantiation. |
20 | 28 | } |
21 | 29 |
|
| 30 | + /** |
| 31 | + * Chains the given message with some other messages. |
| 32 | + * It coordinates the acknowledgement. When all the other messages are acknowledged successfully, the passed |
| 33 | + * message is acknowledged. If one of the other messages is acknowledged negatively, the passed message is also |
| 34 | + * nacked (with the same reason). Subsequent ack/nack will be ignored. |
| 35 | + * <p> |
| 36 | + * |
| 37 | + * @param message the message |
| 38 | + * @return the chain builder that let you decide how the metadata are passed, and the set of messages. |
| 39 | + */ |
| 40 | + @CheckReturnValue |
| 41 | + public static MessageChainBuilder chain(Message<?> message) { |
| 42 | + return new MessageChainBuilder(message); |
| 43 | + } |
| 44 | + |
| 45 | + /** |
| 46 | + * Merges multiple messages into a single one. |
| 47 | + * This is an implementation of a <em>merge pattern</em>: n messages combined into 1. |
| 48 | + * <p> |
| 49 | + * Whe resulting message payload is computed using the combinator function. |
| 50 | + * When the returned message is acked/nacked, the passes messages are acked/nacked accordingly. |
| 51 | + * <p> |
| 52 | + * Metadata are also merged. The metadata of all the messages are copied into the resulting message. If, for a given |
| 53 | + * class, the metadata is already present in the result message, it's either ignored, or merged if the class |
| 54 | + * implements {@link MergeableMetadata}. |
| 55 | + * |
| 56 | + * @param list the list of message, must not be empty, must not be null |
| 57 | + * @param combinator the combinator method, must not be null |
| 58 | + * @param <T> the payload type of the produced message |
| 59 | + * @return the resulting message |
| 60 | + */ |
22 | 61 | public static <T> Message<T> merge(List<Message<?>> list, Function<List<?>, T> combinator) { |
23 | 62 | if (list.isEmpty()) { |
24 | 63 | return Message.of(combinator.apply(Collections.emptyList())); |
@@ -59,6 +98,20 @@ public static <T> Message<T> merge(List<Message<?>> list, Function<List<?>, T> c |
59 | 98 | .withMetadata(metadata); |
60 | 99 | } |
61 | 100 |
|
| 101 | + /** |
| 102 | + * Merges multiple messages into a single one. |
| 103 | + * <p> |
| 104 | + * Whe resulting message payload is computed using the combinator function. |
| 105 | + * When the returned message is acked/nacked, the passes messages are acked/nacked accordingly. |
| 106 | + * <p> |
| 107 | + * Metadata are also merged. The metadata of all the messages are copied into the resulting message. If, for a given |
| 108 | + * class, the metadata is already present in the result message, it's either ignored, or merged if the class |
| 109 | + * implements {@link MergeableMetadata}. |
| 110 | + * |
| 111 | + * @param list the list of message, must not be empty, must not be null |
| 112 | + * @param <T> the payload type of the passed messages |
| 113 | + * @return the resulting message |
| 114 | + */ |
62 | 115 | public static <T> Message<List<T>> merge(List<Message<T>> list) { |
63 | 116 | if (list.isEmpty()) { |
64 | 117 | return Message.of(Collections.emptyList()); |
@@ -90,7 +143,7 @@ public static <T> Message<List<T>> merge(List<Message<T>> list) { |
90 | 143 | .withMetadata(metadata); |
91 | 144 | } |
92 | 145 |
|
93 | | - @SuppressWarnings("unchecked") |
| 146 | + @SuppressWarnings({ "unchecked", "rawtypes" }) |
94 | 147 | private static Metadata merge(Metadata first, Metadata second) { |
95 | 148 | Metadata result = first; |
96 | 149 | for (Object meta : second) { |
@@ -121,4 +174,103 @@ private static Metadata merge(Metadata first, Metadata second) { |
121 | 174 | return result; |
122 | 175 | } |
123 | 176 |
|
| 177 | + /** |
| 178 | + * The message chain builder allows chaining message and configure metadata propagation. |
| 179 | + * By default, all the metadata from the given message are copied into the chained messages. |
| 180 | + */ |
| 181 | + public static class MessageChainBuilder { |
| 182 | + private final Message<?> input; |
| 183 | + private Metadata metadata; |
| 184 | + |
| 185 | + private MessageChainBuilder(Message<?> message) { |
| 186 | + this.input = message; |
| 187 | + this.metadata = message.getMetadata().copy(); |
| 188 | + } |
| 189 | + |
| 190 | + /** |
| 191 | + * Do not copy any metadata from the initial message to the chained message. |
| 192 | + * |
| 193 | + * @return the current {@link MessageChainBuilder} |
| 194 | + */ |
| 195 | + @CheckReturnValue |
| 196 | + public MessageChainBuilder withoutMetadata() { |
| 197 | + this.metadata = Metadata.empty(); |
| 198 | + return this; |
| 199 | + } |
| 200 | + |
| 201 | + /** |
| 202 | + * Copy the given metadata of the given classes from the initial message to the chained message, if the initial |
| 203 | + * message does not include a metadata object of the given class. |
| 204 | + * |
| 205 | + * In general, this method must be used after {@link #withoutMetadata()}. |
| 206 | + * |
| 207 | + * @return the current {@link MessageChainBuilder} |
| 208 | + */ |
| 209 | + @CheckReturnValue |
| 210 | + public MessageChainBuilder withMetadata(Class<?>... mc) { |
| 211 | + for (Class<?> clazz : mc) { |
| 212 | + Optional<?> o = input.getMetadata().get(clazz); |
| 213 | + o.ifPresent(value -> this.metadata = metadata.with(value)); |
| 214 | + } |
| 215 | + return this; |
| 216 | + } |
| 217 | + |
| 218 | + /** |
| 219 | + * Do not the given metadata of the given classes from the initial message to the chained message, if the initial |
| 220 | + * message does not include a metadata object of the given class. |
| 221 | + * |
| 222 | + * @return the current {@link MessageChainBuilder} |
| 223 | + */ |
| 224 | + @CheckReturnValue |
| 225 | + public MessageChainBuilder withoutMetadata(Class<?>... mc) { |
| 226 | + for (Class<?> clazz : mc) { |
| 227 | + this.metadata = this.metadata.without(clazz); |
| 228 | + } |
| 229 | + return this; |
| 230 | + } |
| 231 | + |
| 232 | + /** |
| 233 | + * Passed the chained messages. |
| 234 | + * The messages are not modified, but should not be used afterward, and should be replaced by the messages contained |
| 235 | + * in the returned list. |
| 236 | + * This method preserve the order. So, the first message corresponds to the first message in the returned list. |
| 237 | + * The message from the returned list have the necessary logic to chain the ack/nack signals and the copied metadata. |
| 238 | + * |
| 239 | + * @param messages the chained messages, must not be empty, must not be null, must not contain null |
| 240 | + * @return the list of modified messages |
| 241 | + */ |
| 242 | + public List<Message<?>> with(Message<?>... messages) { |
| 243 | + AtomicBoolean done = new AtomicBoolean(); |
| 244 | + |
| 245 | + // Must be modifiable |
| 246 | + List<Message<?>> trackers = Arrays.stream(messages).collect(Collectors.toCollection(CopyOnWriteArrayList::new)); |
| 247 | + List<Message<?>> outcomes = new ArrayList<>(); |
| 248 | + for (Message<?> message : messages) { |
| 249 | + Message<?> tmp = message; |
| 250 | + for (Object metadatum : metadata) { |
| 251 | + tmp = tmp.addMetadata(metadatum); |
| 252 | + } |
| 253 | + outcomes.add(tmp |
| 254 | + .withAck(() -> { |
| 255 | + CompletionStage<Void> acked = message.ack(); |
| 256 | + if (trackers.remove(message)) { |
| 257 | + if (trackers.isEmpty() && done.compareAndSet(false, true)) { |
| 258 | + return acked.thenCompose(x -> input.ack()); |
| 259 | + } |
| 260 | + } |
| 261 | + return acked; |
| 262 | + }) |
| 263 | + .withNack((reason) -> { |
| 264 | + CompletionStage<Void> nacked = message.nack(reason); |
| 265 | + if (trackers.remove(message)) { |
| 266 | + if (done.compareAndSet(false, true)) { |
| 267 | + return nacked.thenCompose(x -> input.nack(reason)); |
| 268 | + } |
| 269 | + } |
| 270 | + return nacked; |
| 271 | + })); |
| 272 | + } |
| 273 | + return outcomes; |
| 274 | + } |
| 275 | + } |
124 | 276 | } |
0 commit comments