diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/bindings/BindingFactory.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/bindings/BindingFactory.java index ceafa3d7e..5f89b7e8a 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/bindings/BindingFactory.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/bindings/BindingFactory.java @@ -4,11 +4,16 @@ import io.github.springwolf.asyncapi.v3.bindings.ChannelBinding; import io.github.springwolf.asyncapi.v3.bindings.MessageBinding; import io.github.springwolf.asyncapi.v3.bindings.OperationBinding; +import io.github.springwolf.asyncapi.v3.model.ReferenceUtil; import io.github.springwolf.asyncapi.v3.model.schema.SchemaObject; import java.util.Map; public interface BindingFactory { + default String getChannelId(T annotation) { + return ReferenceUtil.toValidId(getChannelName(annotation)); + } + String getChannelName(T annotation); Map buildChannelBinding(T annotation); diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/MessageHelper.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/MessageHelper.java index 70cae2199..e616544c3 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/MessageHelper.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/MessageHelper.java @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.springwolf.core.asyncapi.scanners.common; -import io.github.springwolf.asyncapi.v3.model.ReferenceUtil; import io.github.springwolf.asyncapi.v3.model.channel.message.MessageObject; import io.github.springwolf.asyncapi.v3.model.channel.message.MessageReference; import lombok.extern.slf4j.Slf4j; @@ -28,14 +27,13 @@ public static Map toMessagesMap(Set mes return toMessageReferences(messages, aggregator); } - public static Map toOperationsMessagesMap( - String channelName, Set messages) { - if (channelName == null || channelName.isBlank()) { - throw new IllegalArgumentException("channelName must not be empty"); + public static Map toOperationsMessagesMap(String channelId, Set messages) { + if (channelId == null || channelId.isBlank()) { + throw new IllegalArgumentException("channelId must not be empty"); } - Function aggregator = (message) -> - MessageReference.toChannelMessage(ReferenceUtil.toValidId(channelName), message.getMessageId()); + Function aggregator = + (message) -> MessageReference.toChannelMessage(channelId, message.getMessageId()); return toMessageReferences(messages, aggregator); } diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/message/SpringAnnotationMessagesService.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/message/SpringAnnotationMessagesService.java index bee976309..71b2f5e5b 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/message/SpringAnnotationMessagesService.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/message/SpringAnnotationMessagesService.java @@ -49,8 +49,8 @@ public Map buildMessages( .collect(toSet()); if (messageType == MessageType.OPERATION) { - String channelName = bindingFactory.getChannelName(classAnnotation); - return toOperationsMessagesMap(channelName, messages); + String channelId = bindingFactory.getChannelName(classAnnotation); + return toOperationsMessagesMap(channelId, messages); } return toMessagesMap(messages); } diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/operation/SpringAnnotationOperationService.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/operation/SpringAnnotationOperationService.java index 2a7fb4aac..a40550aac 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/operation/SpringAnnotationOperationService.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/operation/SpringAnnotationOperationService.java @@ -2,7 +2,6 @@ package io.github.springwolf.core.asyncapi.scanners.common.operation; import io.github.springwolf.asyncapi.v3.bindings.OperationBinding; -import io.github.springwolf.asyncapi.v3.model.ReferenceUtil; import io.github.springwolf.asyncapi.v3.model.channel.ChannelReference; import io.github.springwolf.asyncapi.v3.model.channel.message.MessageObject; import io.github.springwolf.asyncapi.v3.model.channel.message.MessageReference; @@ -30,7 +29,7 @@ public Operation buildOperation( MessageObject message = springAnnotationMessageService.buildMessage(annotation, payloadType, headerSchema); Map operationBinding = bindingFactory.buildOperationBinding(annotation); Map opBinding = operationBinding != null ? new HashMap<>(operationBinding) : null; - String channelId = ReferenceUtil.toValidId(bindingFactory.getChannelName(annotation)); + String channelId = bindingFactory.getChannelId(annotation); return Operation.builder() .action(OperationAction.RECEIVE) diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/operations/annotations/SpringAnnotationClassLevelOperationsScanner.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/operations/annotations/SpringAnnotationClassLevelOperationsScanner.java index c532b1cad..0a2ed6701 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/operations/annotations/SpringAnnotationClassLevelOperationsScanner.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/operations/annotations/SpringAnnotationClassLevelOperationsScanner.java @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.springwolf.core.asyncapi.scanners.operations.annotations; -import io.github.springwolf.asyncapi.v3.model.ReferenceUtil; import io.github.springwolf.asyncapi.v3.model.operation.Operation; import io.github.springwolf.asyncapi.v3.model.operation.OperationAction; import io.github.springwolf.core.asyncapi.scanners.bindings.BindingFactory; @@ -44,8 +43,7 @@ private Stream> mapClassToOperation( Class component, Set> annotatedMethods) { ClassAnnotation classAnnotation = AnnotationUtil.findFirstAnnotationOrThrow(classAnnotationClass, component); - String channelName = bindingFactory.getChannelName(classAnnotation); - String channelId = ReferenceUtil.toValidId(channelName); + String channelId = bindingFactory.getChannelId(classAnnotation); String operationId = StringUtils.joinWith("_", channelId, OperationAction.RECEIVE.type, component.getSimpleName()); diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/operations/annotations/SpringAnnotationMethodLevelOperationsScanner.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/operations/annotations/SpringAnnotationMethodLevelOperationsScanner.java index b347733ce..ef3ac9834 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/operations/annotations/SpringAnnotationMethodLevelOperationsScanner.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/operations/annotations/SpringAnnotationMethodLevelOperationsScanner.java @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.springwolf.core.asyncapi.scanners.operations.annotations; -import io.github.springwolf.asyncapi.v3.model.ReferenceUtil; import io.github.springwolf.asyncapi.v3.model.operation.Operation; import io.github.springwolf.asyncapi.v3.model.operation.OperationAction; import io.github.springwolf.asyncapi.v3.model.schema.SchemaObject; @@ -44,8 +43,7 @@ public Stream> scan(Class clazz) { private Map.Entry mapMethodToOperation(MethodAndAnnotation method) { MethodAnnotation annotation = AnnotationUtil.findFirstAnnotationOrThrow(methodAnnotationClass, method.method()); - String channelName = bindingFactory.getChannelName(annotation); - String channelId = ReferenceUtil.toValidId(channelName); + String channelId = bindingFactory.getChannelId(annotation); String operationId = StringUtils.joinWith( "_", channelId, OperationAction.RECEIVE.type, method.method().getName()); diff --git a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/common/operation/SpringAnnotationOperationServiceTest.java b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/common/operation/SpringAnnotationOperationServiceTest.java index 1e8e3b46f..cc0b9fb48 100644 --- a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/common/operation/SpringAnnotationOperationServiceTest.java +++ b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/common/operation/SpringAnnotationOperationServiceTest.java @@ -40,7 +40,7 @@ class SpringAnnotationOperationServiceTest { @BeforeEach void setUp() { // when - when(bindingFactory.getChannelName(any())).thenReturn(CHANNEL_ID); + when(bindingFactory.getChannelId(any())).thenReturn(CHANNEL_ID); doReturn(defaultOperationBinding).when(bindingFactory).buildOperationBinding(any()); } diff --git a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/operations/annotations/SpringAnnotationClassLevelOperationsScannerTest.java b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/operations/annotations/SpringAnnotationClassLevelOperationsScannerTest.java index 47b732253..c0626c184 100644 --- a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/operations/annotations/SpringAnnotationClassLevelOperationsScannerTest.java +++ b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/operations/annotations/SpringAnnotationClassLevelOperationsScannerTest.java @@ -35,14 +35,14 @@ class SpringAnnotationClassLevelOperationsScannerTest { springAnnotationOperationsService, List.of(operationCustomizer)); - private static final String CHANNEL_NAME = "test-channel"; + private static final String CHANNEL_NAME_ID = "test-channel"; private static final Map defaultMessageBinding = Map.of("protocol", new AMQPMessageBinding()); @BeforeEach void setUp() { - when(bindingFactory.getChannelName(any())).thenReturn(CHANNEL_NAME); + when(bindingFactory.getChannelId(any())).thenReturn(CHANNEL_NAME_ID); } @Test @@ -56,7 +56,7 @@ void scan() { scanner.scan(ClassWithTestListenerAnnotation.class).toList(); // then - String operationName = CHANNEL_NAME + "_receive_ClassWithTestListenerAnnotation"; + String operationName = CHANNEL_NAME_ID + "_receive_ClassWithTestListenerAnnotation"; assertThat(operations).containsExactly(Map.entry(operationName, operation)); } diff --git a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/operations/annotations/SpringAnnotationMethodLevelOperationsScannerTest.java b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/operations/annotations/SpringAnnotationMethodLevelOperationsScannerTest.java index 9dad7c4b4..6a1090d08 100644 --- a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/operations/annotations/SpringAnnotationMethodLevelOperationsScannerTest.java +++ b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/scanners/operations/annotations/SpringAnnotationMethodLevelOperationsScannerTest.java @@ -37,11 +37,11 @@ class SpringAnnotationMethodLevelOperationsScannerTest { springAnnotationOperationService, List.of(operationCustomizer)); - private static final String CHANNEL_NAME = "test-channel"; + private static final String CHANNEL_ID = "test-channel"; @BeforeEach void setUp() { - when(bindingFactory.getChannelName(any())).thenReturn(CHANNEL_NAME); + when(bindingFactory.getChannelId(any())).thenReturn(CHANNEL_ID); } @Test @@ -56,7 +56,7 @@ void scan_componentHasTestListenerMethods() { scanner.scan(ClassWithTestListenerAnnotation.class).toList(); // then - String operationName = CHANNEL_NAME + "_receive_methodWithAnnotation"; + String operationName = CHANNEL_ID + "_receive_methodWithAnnotation"; assertThat(operations).containsExactly(Map.entry(operationName, operation)); } diff --git a/springwolf-examples/e2e/tests/publishing.spec.ts b/springwolf-examples/e2e/tests/publishing.spec.ts index c177efa6b..a3b2cc25d 100644 --- a/springwolf-examples/e2e/tests/publishing.spec.ts +++ b/springwolf-examples/e2e/tests/publishing.spec.ts @@ -72,8 +72,8 @@ function testPublishingEveryChannelItem() { messageTitle === "Message" || // Unable to instantiate ExamplePayloadProtobufDto$Message class messageTitle === "VehicleBase" || // Unable to publish abstract class for discriminator demo messageTitle.startsWith("GenericPayload") || // Unable to publish generic payload (amqp) - channelName === "#" || // Publishing through amqp exchange is not supported, see GH-366 - channelName === "example-topic-routing-key" // Publishing through amqp exchange is not supported, see GH-366 + channelName === "CRUD-topic-exchange-2" || // Publishing through amqp exchange is not supported, see GH-366 + channelName === "example-topic-exchange_example-topic-routing-key" // Publishing through amqp exchange is not supported, see GH-366 ) { return; // skip } diff --git a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitConfiguration.java b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitConfiguration.java index adfe68e26..7f3d6cc27 100644 --- a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitConfiguration.java +++ b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/configuration/RabbitConfiguration.java @@ -2,6 +2,7 @@ package io.github.springwolf.examples.amqp.configuration; import io.github.springwolf.examples.amqp.AmqpConstants; +import io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.Exchange; @@ -32,11 +33,6 @@ public Queue anotherQueue() { return new Queue(AmqpConstants.QUEUE_ANOTHER_QUEUE, false); } - @Bean - public Queue exampleBindingsQueue() { - return new Queue(AmqpConstants.QUEUE_EXAMPLE_BINDINGS_QUEUE, false, false, true); - } - @Bean public Queue queueRead() { return new Queue(AmqpConstants.QUEUE_READ, false); @@ -52,6 +48,17 @@ public Queue multiPayloadQueue() { return new Queue(AmqpConstants.QUEUE_MULTI_PAYLOAD_QUEUE); } + /** + * Defined by @RabbitListener annotation in {@link io.github.springwolf.examples.amqp.consumers.ExampleConsumer#bindingsExample(AnotherPayloadDto)} + */ + @Bean + public Queue exampleBindingsQueue() { + return new Queue(AmqpConstants.QUEUE_EXAMPLE_BINDINGS_QUEUE, false, false, true); + } + + /** + * Defined by @RabbitListener annotation in {@link io.github.springwolf.examples.amqp.consumers.ExampleConsumer#bindingsExample(AnotherPayloadDto)} + */ @Bean public Binding exampleTopicBinding(Queue exampleBindingsQueue, Exchange exampleTopicExchange) { return BindingBuilder.bind(exampleBindingsQueue) diff --git a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/consumers/ExampleConsumer.java b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/consumers/ExampleConsumer.java index 79416adbc..2f836b972 100644 --- a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/consumers/ExampleConsumer.java +++ b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/springwolf/examples/amqp/consumers/ExampleConsumer.java @@ -55,7 +55,7 @@ public void receiveAnotherPayload(AnotherPayloadDto payload) { autoDelete = "true"), key = AmqpConstants.ROUTING_KEY_EXAMPLE_TOPIC_ROUTING_KEY) }) - public void bindingsExample(AnotherPayloadDto payload) { + public void bindingsExample(ExamplePayloadDto payload) { log.info( "Received new message in {}" + " through exchange {}" + " using routing key {}: {}", AmqpConstants.QUEUE_EXAMPLE_BINDINGS_QUEUE, @@ -112,7 +112,7 @@ public void bindingsUpdate(Message message, @Payload GenericPayloadDto): {}", message, - AmqpConstants.QUEUE_UPDATE, + AmqpConstants.EXCHANGE_CRUD_TOPIC_EXCHANGE_1, payload.toString()); } @@ -130,7 +130,7 @@ public void bindingsRead(Message message, @Payload ExamplePayloadDto payload) { log.info( "Received new message {} in {} (ExamplePayloadDto): {}", message, - AmqpConstants.QUEUE_UPDATE, + AmqpConstants.EXCHANGE_CRUD_TOPIC_EXCHANGE_2, payload.toString()); } } diff --git a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/SpringContextIntegrationTest.java b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/SpringContextIntegrationTest.java index 23ffa7d7e..3a9873cd7 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/SpringContextIntegrationTest.java +++ b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/springwolf/examples/amqp/SpringContextIntegrationTest.java @@ -46,12 +46,13 @@ void testContextWithApplicationProperties() { void testAllChannelsAreFound() { assertThat(asyncApiService.getAsyncAPI().getChannels().keySet()) .containsExactlyInAnyOrder( - "#", + "CRUD-topic-exchange-1", + "CRUD-topic-exchange-2", "another-queue", "example-bindings-queue", "example-queue", "example-topic-exchange", - "example-topic-routing-key", + "example-topic-exchange_example-topic-routing-key", "multi-payload-queue", "queue-create", "queue-delete", diff --git a/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json b/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json index d574635c5..8e3a1a802 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json +++ b/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json @@ -25,12 +25,9 @@ } }, "channels": { - "#": { - "address": "#", + "CRUD-topic-exchange-1": { + "address": "CRUD-topic-exchange-1", "messages": { - "io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto": { - "$ref": "#/components/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" - }, "io.github.springwolf.examples.amqp.dtos.GenericPayloadDtoIo.github.springwolf.examples.amqp.dtos.ExamplePayloadDto": { "$ref": "#/components/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDtoIo.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" } @@ -49,6 +46,27 @@ } } }, + "CRUD-topic-exchange-2": { + "address": "CRUD-topic-exchange-2", + "messages": { + "io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto": { + "$ref": "#/components/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" + } + }, + "bindings": { + "amqp": { + "is": "routingKey", + "exchange": { + "name": "CRUD-topic-exchange-2", + "type": "topic", + "durable": true, + "autoDelete": false, + "vhost": "/" + }, + "bindingVersion": "0.3.0" + } + } + }, "another-queue": { "address": "another-queue", "messages": { @@ -115,11 +133,11 @@ } } }, - "example-topic-routing-key": { - "address": "example-topic-routing-key", + "example-topic-exchange_example-topic-routing-key": { + "address": "example-topic-exchange_example-topic-routing-key", "messages": { - "io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto": { - "$ref": "#/components/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" + "io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto": { + "$ref": "#/components/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" } }, "bindings": { @@ -487,43 +505,37 @@ } }, "operations": { - "#_receive_bindingsRead": { + "CRUD-topic-exchange-1_receive_bindingsUpdate": { "action": "receive", "channel": { - "$ref": "#/channels/#" + "$ref": "#/channels/CRUD-topic-exchange-1" }, "bindings": { "amqp": { "expiration": 0, - "cc": [ - "#" - ], "bindingVersion": "0.3.0" } }, "messages": [ { - "$ref": "#/channels/#/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" + "$ref": "#/channels/CRUD-topic-exchange-1/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDtoIo.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" } ] }, - "#_receive_bindingsUpdate": { + "CRUD-topic-exchange-2_receive_bindingsRead": { "action": "receive", "channel": { - "$ref": "#/channels/#" + "$ref": "#/channels/CRUD-topic-exchange-2" }, "bindings": { "amqp": { "expiration": 0, - "cc": [ - "#" - ], "bindingVersion": "0.3.0" } }, "messages": [ { - "$ref": "#/channels/#/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDtoIo.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" + "$ref": "#/channels/CRUD-topic-exchange-2/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" } ] }, @@ -535,9 +547,6 @@ "bindings": { "amqp": { "expiration": 0, - "cc": [ - "another-queue" - ], "bindingVersion": "0.3.0" } }, @@ -555,9 +564,6 @@ "bindings": { "amqp": { "expiration": 0, - "cc": [ - "example-queue" - ], "bindingVersion": "0.3.0" } }, @@ -567,49 +573,46 @@ } ] }, - "example-topic-exchange_send_sendMessage": { - "action": "send", + "example-topic-exchange_example-topic-routing-key_receive_bindingsExample": { + "action": "receive", "channel": { - "$ref": "#/channels/example-topic-exchange" + "$ref": "#/channels/example-topic-exchange_example-topic-routing-key" }, - "title": "example-topic-exchange_send", - "description": "Custom, optional description defined in the AsyncPublisher annotation", "bindings": { "amqp": { "expiration": 0, - "cc": [ ], - "priority": 0, - "deliveryMode": 1, - "mandatory": false, - "bcc": [ ], - "timestamp": false, - "ack": false, "bindingVersion": "0.3.0" } }, "messages": [ { - "$ref": "#/channels/example-topic-exchange/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" + "$ref": "#/channels/example-topic-exchange_example-topic-routing-key/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" } ] }, - "example-topic-routing-key_receive_bindingsExample": { - "action": "receive", + "example-topic-exchange_send_sendMessage": { + "action": "send", "channel": { - "$ref": "#/channels/example-topic-routing-key" + "$ref": "#/channels/example-topic-exchange" }, + "title": "example-topic-exchange_send", + "description": "Custom, optional description defined in the AsyncPublisher annotation", "bindings": { "amqp": { "expiration": 0, - "cc": [ - "example-topic-routing-key" - ], + "cc": [ ], + "priority": 0, + "deliveryMode": 1, + "mandatory": false, + "bcc": [ ], + "timestamp": false, + "ack": false, "bindingVersion": "0.3.0" } }, "messages": [ { - "$ref": "#/channels/example-topic-routing-key/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" + "$ref": "#/channels/example-topic-exchange/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" } ] }, @@ -621,9 +624,6 @@ "bindings": { "amqp": { "expiration": 0, - "cc": [ - "multi-payload-queue" - ], "bindingVersion": "0.3.0" } }, @@ -644,9 +644,6 @@ "bindings": { "amqp": { "expiration": 0, - "cc": [ - "queue-create" - ], "bindingVersion": "0.3.0" } }, @@ -664,9 +661,6 @@ "bindings": { "amqp": { "expiration": 0, - "cc": [ - "queue-delete" - ], "bindingVersion": "0.3.0" } }, diff --git a/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.yaml b/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.yaml index f5f17d59a..ef6152692 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.yaml +++ b/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.yaml @@ -19,11 +19,9 @@ servers: host: amqp:5672 protocol: amqp channels: - '#': - address: "#" + CRUD-topic-exchange-1: + address: CRUD-topic-exchange-1 messages: - io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto: - $ref: "#/components/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" io.github.springwolf.examples.amqp.dtos.GenericPayloadDtoIo.github.springwolf.examples.amqp.dtos.ExamplePayloadDto: $ref: "#/components/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDtoIo.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" bindings: @@ -36,6 +34,21 @@ channels: autoDelete: false vhost: / bindingVersion: 0.3.0 + CRUD-topic-exchange-2: + address: CRUD-topic-exchange-2 + messages: + io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto: + $ref: "#/components/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" + bindings: + amqp: + is: routingKey + exchange: + name: CRUD-topic-exchange-2 + type: topic + durable: true + autoDelete: false + vhost: / + bindingVersion: 0.3.0 another-queue: address: another-queue messages: @@ -83,11 +96,11 @@ channels: messages: io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto: $ref: "#/components/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" - example-topic-routing-key: - address: example-topic-routing-key + example-topic-exchange_example-topic-routing-key: + address: example-topic-exchange_example-topic-routing-key messages: - io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto: - $ref: "#/components/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" + io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto: + $ref: "#/components/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" bindings: amqp: is: routingKey @@ -339,30 +352,26 @@ components: amqp: bindingVersion: 0.3.0 operations: - '#_receive_bindingsRead': + CRUD-topic-exchange-1_receive_bindingsUpdate: action: receive channel: - $ref: "#/channels/#" + $ref: "#/channels/CRUD-topic-exchange-1" bindings: amqp: expiration: 0 - cc: - - "#" bindingVersion: 0.3.0 messages: - - $ref: "#/channels/#/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" - '#_receive_bindingsUpdate': + - $ref: "#/channels/CRUD-topic-exchange-1/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDtoIo.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" + CRUD-topic-exchange-2_receive_bindingsRead: action: receive channel: - $ref: "#/channels/#" + $ref: "#/channels/CRUD-topic-exchange-2" bindings: amqp: expiration: 0 - cc: - - "#" bindingVersion: 0.3.0 messages: - - $ref: "#/channels/#/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDtoIo.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" + - $ref: "#/channels/CRUD-topic-exchange-2/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" another-queue_receive_receiveAnotherPayload: action: receive channel: @@ -370,8 +379,6 @@ operations: bindings: amqp: expiration: 0 - cc: - - another-queue bindingVersion: 0.3.0 messages: - $ref: "#/channels/another-queue/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" @@ -382,11 +389,19 @@ operations: bindings: amqp: expiration: 0 - cc: - - example-queue bindingVersion: 0.3.0 messages: - $ref: "#/channels/example-queue/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" + example-topic-exchange_example-topic-routing-key_receive_bindingsExample: + action: receive + channel: + $ref: "#/channels/example-topic-exchange_example-topic-routing-key" + bindings: + amqp: + expiration: 0 + bindingVersion: 0.3.0 + messages: + - $ref: "#/channels/example-topic-exchange_example-topic-routing-key/messages/io.github.springwolf.examples.amqp.dtos.ExamplePayloadDto" example-topic-exchange_send_sendMessage: action: send channel: @@ -406,18 +421,6 @@ operations: bindingVersion: 0.3.0 messages: - $ref: "#/channels/example-topic-exchange/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" - example-topic-routing-key_receive_bindingsExample: - action: receive - channel: - $ref: "#/channels/example-topic-routing-key" - bindings: - amqp: - expiration: 0 - cc: - - example-topic-routing-key - bindingVersion: 0.3.0 - messages: - - $ref: "#/channels/example-topic-routing-key/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" multi-payload-queue_receive_bindingsBeanExample: action: receive channel: @@ -425,8 +428,6 @@ operations: bindings: amqp: expiration: 0 - cc: - - multi-payload-queue bindingVersion: 0.3.0 messages: - $ref: "#/channels/multi-payload-queue/messages/io.github.springwolf.examples.amqp.dtos.AnotherPayloadDto" @@ -438,8 +439,6 @@ operations: bindings: amqp: expiration: 0 - cc: - - queue-create bindingVersion: 0.3.0 messages: - $ref: "#/channels/queue-create/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDtoJava.lang.String" @@ -450,8 +449,6 @@ operations: bindings: amqp: expiration: 0 - cc: - - queue-delete bindingVersion: 0.3.0 messages: - $ref: "#/channels/queue-delete/messages/io.github.springwolf.examples.amqp.dtos.GenericPayloadDtoJava.lang.Long" diff --git a/springwolf-plugins/springwolf-amqp-plugin/build.gradle b/springwolf-plugins/springwolf-amqp-plugin/build.gradle index 0456765da..7db4a3bcb 100644 --- a/springwolf-plugins/springwolf-amqp-plugin/build.gradle +++ b/springwolf-plugins/springwolf-amqp-plugin/build.gradle @@ -45,7 +45,6 @@ dependencies { testAnnotationProcessor "org.projectlombok:lombok:${lombokVersion}" testImplementation 'org.junit.jupiter:junit-jupiter-api' - testImplementation("org.junit.jupiter:junit-jupiter-params") testRuntimeOnly 'org.junit.jupiter:junit-jupiter' } diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/AmqpBindingFactory.java b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/AmqpBindingFactory.java index d06f3d691..80c53ebbd 100644 --- a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/AmqpBindingFactory.java +++ b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/AmqpBindingFactory.java @@ -16,7 +16,7 @@ import java.util.Map; public class AmqpBindingFactory implements BindingFactory { - private final RabbitListenerUtil.RabbitListenerUtilContext context; + private final RabbitListenerUtilContext context; private final StringValueResolver stringValueResolver; public AmqpBindingFactory( @@ -24,7 +24,7 @@ public AmqpBindingFactory( List exchanges, List bindings, StringValueResolver stringValueResolver) { - this.context = RabbitListenerUtil.RabbitListenerUtilContext.create(queues, exchanges, bindings); + this.context = RabbitListenerUtilContext.create(queues, exchanges, bindings); this.stringValueResolver = stringValueResolver; } @@ -33,6 +33,11 @@ public String getChannelName(RabbitListener annotation) { return RabbitListenerUtil.getChannelName(annotation, stringValueResolver); } + @Override + public String getChannelId(RabbitListener annotation) { + return RabbitListenerUtil.getChannelId(annotation, stringValueResolver); + } + @Override public Map buildChannelBinding(RabbitListener annotation) { return RabbitListenerUtil.buildChannelBinding(annotation, stringValueResolver, context); diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/RabbitListenerUtil.java b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/RabbitListenerUtil.java index 0a8ef6bca..581d53f9e 100644 --- a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/RabbitListenerUtil.java +++ b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/RabbitListenerUtil.java @@ -24,15 +24,27 @@ import org.springframework.util.StringValueResolver; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; import java.util.stream.Stream; +/** + * Note: bindings, queues, and queuesToDeclare are mutually exclusive + *
    + *
  • queues (string) point to queue beans (default exchange + routing key) + *
  • queuesToDeclare (object) will create queues on broker and matching beans (default exchange + routing key) + *
  • queueBinding (object) will create queue and exchange on broker and matching beans (exchange must match if present) + *
+ *
+ * How does rabbitmq work? + *
    + *
  1. Producer sends a message to an exchange (default exchange if not specified) + *
  2. Exchange routes the message to a queue based on the routing key (routing key = queue name if not specified) + *
  3. Consumer consumes the message from the queue + *
+ */ @Slf4j public class RabbitListenerUtil { public static final String BINDING_NAME = "amqp"; @@ -40,35 +52,49 @@ public class RabbitListenerUtil { private static final Boolean DEFAULT_DURABLE = true; private static final Boolean DEFAULT_EXCLUSIVE = false; private static final String DEFAULT_EXCHANGE_TYPE = ExchangeTypes.DIRECT; + private static final String DEFAULT_EXCHANGE_ROUTING_KEY = "#"; public static String getChannelName(RabbitListener annotation, StringValueResolver stringValueResolver) { Stream annotationBindingChannelNames = Arrays.stream(annotation.bindings()) - .flatMap(binding -> Stream.concat( - Stream.of(binding.key()), // if routing key is configured, prefer it - Stream.of(binding.value().name()))); + .flatMap(binding -> channelNameFromAnnotationBindings( + binding.exchange().name(), binding.key()) + .map(stringValueResolver::resolveStringValue)); - return Stream.concat(streamQueueNames(annotation), annotationBindingChannelNames) - .map(stringValueResolver::resolveStringValue) - .filter(Objects::nonNull) - .peek(queue -> log.debug("Resolved channel name: {}", queue)) - .findFirst() - .orElseThrow( - () -> new IllegalArgumentException( - "No channel name was found in @RabbitListener annotation (neither in queues nor bindings property)")); + Stream stream = Stream.concat(streamQueueNames(annotation), annotationBindingChannelNames); + return resolveFirstValue(stream, stringValueResolver, "channel name"); } - public static String getQueueName(RabbitListener annotation, StringValueResolver stringValueResolver) { - Stream annotationBindingChannelNames = Arrays.stream(annotation.bindings()) + public static String getChannelId(RabbitListener annotation, StringValueResolver stringValueResolver) { + Stream annotationBindingChannelIds = Arrays.stream(annotation.bindings()) + .flatMap(binding -> channelIdFromAnnotationBindings( + binding.exchange().name(), binding.key()) + .map(stringValueResolver::resolveStringValue)); + + Stream stream = + Stream.concat(streamQueueNames(annotation).map(ReferenceUtil::toValidId), annotationBindingChannelIds); + return resolveFirstValue(stream, stringValueResolver, "channel id"); + } + + private static String getQueueName(RabbitListener annotation, StringValueResolver resolver) { + Stream annotationBindingQueueNames = Arrays.stream(annotation.bindings()) .flatMap(binding -> Stream.of(binding.value().name())); - return Stream.concat(streamQueueNames(annotation), annotationBindingChannelNames) - .map(stringValueResolver::resolveStringValue) - .filter(Objects::nonNull) - .peek(queue -> log.debug("Resolved queue name: {}", queue)) - .findFirst() - .orElseThrow( - () -> new IllegalArgumentException( - "No queue name was found in @RabbitListener annotation (neither in queues nor bindings property)")); + Stream stream = Stream.concat(streamQueueNames(annotation), annotationBindingQueueNames); + return resolveFirstValue(stream, resolver, "queue name"); + } + + private static Stream channelNameFromAnnotationBindings(String exchangeName, String[] routingKeys) { + List keys = Arrays.stream(routingKeys) + .filter(s -> !s.equals(DEFAULT_EXCHANGE_ROUTING_KEY)) + .toList(); + if (keys.isEmpty()) { + return Stream.of(exchangeName); + } + return keys.stream().map(key -> String.join("_", exchangeName, key)); + } + + private static Stream channelIdFromAnnotationBindings(String exchangeName, String[] keys) { + return channelNameFromAnnotationBindings(exchangeName, keys).map(ReferenceUtil::toValidId); } /** @@ -109,7 +135,7 @@ private static AMQPChannelExchangeProperties buildExchangeProperties( // When a bean is found, its values are preferred regardless of the annotations values. // When using the annotation, it is not possible to differentiate between user set and default parameters - Exchange exchange = context.exchangeMap.get(exchangeName); + Exchange exchange = context.exchangeMap().get(exchangeName); if (exchange != null) { return AMQPChannelExchangeProperties.builder() .name(exchangeName) @@ -141,7 +167,7 @@ private static AMQPChannelExchangeProperties buildExchangeProperties( private static AMQPChannelQueueProperties buildQueueProperties( RabbitListener annotation, StringValueResolver stringValueResolver, RabbitListenerUtilContext context) { String queueName = getQueueName(annotation, stringValueResolver); - org.springframework.amqp.core.Queue queue = context.queueMap.get(queueName); + org.springframework.amqp.core.Queue queue = context.queueMap().get(queueName); boolean autoDelete = queue != null ? queue.isAutoDelete() : DEFAULT_AUTO_DELETE; boolean durable = queue != null ? queue.isDurable() : DEFAULT_DURABLE; boolean exclusive = queue != null ? queue.isExclusive() : DEFAULT_EXCLUSIVE; @@ -184,6 +210,40 @@ public static ChannelObject buildChannelObject(org.springframework.amqp.core.Que .build(); } + public static List buildChannelObject(Binding binding) { + String exchangeId = channelIdFromAnnotationBindings( + binding.getExchange(), List.of(binding.getRoutingKey()).toArray(String[]::new)) + .findFirst() + .get(); + return List.of( + // exchange + ChannelObject.builder() + .channelId(exchangeId) + .address(binding.getRoutingKey()) + .bindings(Map.of( + BINDING_NAME, + AMQPChannelBinding.builder() + .is(AMQPChannelType.ROUTING_KEY) + .exchange(AMQPChannelExchangeProperties.builder() + .name(binding.getExchange()) + .build()) + .build())) + .build(), + // queue (where the exchange forwards the message to) + ChannelObject.builder() + .channelId(ReferenceUtil.toValidId(binding.getDestination())) + .address(binding.getDestination()) + .bindings(Map.of( + BINDING_NAME, + AMQPChannelBinding.builder() + .is(AMQPChannelType.QUEUE) + .queue(AMQPChannelQueueProperties.builder() + .name(binding.getDestination()) + .build()) + .build())) + .build()); + } + private static Boolean parse(String value, Boolean defaultIfEmpty) { if ("".equals(value)) { return defaultIfEmpty; @@ -195,11 +255,12 @@ private static String getExchangeName( RabbitListener annotation, StringValueResolver stringValueResolver, RabbitListenerUtilContext context) { String exchangeName = Stream.of(annotation.bindings()) .map(binding -> binding.exchange().name()) + .map(stringValueResolver::resolveStringValue) .filter(StringUtils::hasText) .findFirst() .orElse(null); - Binding binding = context.bindingMap.get(getChannelName(annotation, stringValueResolver)); + Binding binding = context.bindingMap().get(getChannelName(annotation, stringValueResolver)); if (exchangeName == null && binding != null) { exchangeName = binding.getExchange(); } @@ -213,44 +274,8 @@ private static String getExchangeName( } public static Map buildOperationBinding( - RabbitListener annotation, StringValueResolver stringValueResolver, RabbitListenerUtilContext context) { - return Map.of( - BINDING_NAME, - AMQPOperationBinding.builder() - .cc(getRoutingKeys(annotation, stringValueResolver, context)) - .build()); - } - - private static List getRoutingKeys( - RabbitListener annotation, StringValueResolver stringValueResolver, RabbitListenerUtilContext context) { - List routingKeys = Stream.of(annotation.bindings()) - .map(binding -> { - if (binding.key().length == 0) { - // The routing key is taken from the binding. As the key field in the @QueueBinding can be an - // empty array, it is set as an empty String in that case. - return Collections.singletonList(""); - } - - return Arrays.stream(binding.key()) - .map(stringValueResolver::resolveStringValue) - .toList(); - }) - .findFirst() - .orElse(null); - - Binding binding = context.bindingMap.get(getChannelName(annotation, stringValueResolver)); - if (routingKeys == null && binding != null) { - routingKeys = Collections.singletonList(binding.getRoutingKey()); - } - - // when there is no binding for the queue present at all, it uses the fact that - // RabbitMQ automatically binds default exchange to a queue with queue's name as a routing key. - String exchangeName = getExchangeName(annotation, stringValueResolver, context); - if (routingKeys == null && exchangeName.isEmpty()) { - routingKeys = Collections.singletonList(getQueueName(annotation, stringValueResolver)); - } - - return routingKeys; + RabbitListener annotation, StringValueResolver resolver, RabbitListenerUtilContext context) { + return Map.of(BINDING_NAME, AMQPOperationBinding.builder().build()); } public static Map buildMessageBinding() { @@ -258,22 +283,12 @@ public static Map buildMessageBinding() { return Map.of(BINDING_NAME, new AMQPMessageBinding()); } - public record RabbitListenerUtilContext( - Map queueMap, - Map exchangeMap, - Map bindingMap) { - - public static RabbitListenerUtilContext create( - List queues, List exchanges, List bindings) { - Map queueMap = queues.stream() - .collect(Collectors.toMap( - org.springframework.amqp.core.Queue::getName, Function.identity(), (e1, e2) -> e1)); - Map exchangeMap = exchanges.stream() - .collect(Collectors.toMap(Exchange::getName, Function.identity(), (e1, e2) -> e1)); - Map bindingMap = bindings.stream() - .filter(Binding::isDestinationQueue) - .collect(Collectors.toMap(Binding::getDestination, Function.identity(), (e1, e2) -> e1)); - return new RabbitListenerUtil.RabbitListenerUtilContext(queueMap, exchangeMap, bindingMap); - } + private static String resolveFirstValue(Stream values, StringValueResolver resolver, String valueType) { + return values.map(resolver::resolveStringValue) + .filter(Objects::nonNull) + .peek(value -> log.debug("Resolved {}: {}", valueType, value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No " + valueType + + " was found in @RabbitListener annotation (neither in queues nor bindings property)")); } } diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/RabbitListenerUtilContext.java b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/RabbitListenerUtilContext.java new file mode 100644 index 000000000..c430d6aa2 --- /dev/null +++ b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/RabbitListenerUtilContext.java @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.springwolf.plugins.amqp.asyncapi.scanners.bindings; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.Exchange; +import org.springframework.amqp.core.Queue; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public record RabbitListenerUtilContext( + Map queueMap, Map exchangeMap, Map bindingMap) { + + public static RabbitListenerUtilContext create( + List queues, List exchanges, List bindings) { + Map queueMap = + queues.stream().collect(Collectors.toMap(Queue::getName, Function.identity(), (e1, e2) -> e1)); + Map exchangeMap = + exchanges.stream().collect(Collectors.toMap(Exchange::getName, Function.identity(), (e1, e2) -> e1)); + Map bindingMap = bindings.stream() + .filter(Binding::isDestinationQueue) + .collect(Collectors.toMap(Binding::getDestination, Function.identity(), (e1, e2) -> e1)); + return new RabbitListenerUtilContext(queueMap, exchangeMap, bindingMap); + } +} diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/channels/RabbitQueueBeanScanner.java b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/channels/RabbitQueueBeanScanner.java index 79ef8726a..5406174eb 100644 --- a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/channels/RabbitQueueBeanScanner.java +++ b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/channels/RabbitQueueBeanScanner.java @@ -1,30 +1,30 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.springwolf.plugins.amqp.asyncapi.scanners.channels; -import io.github.springwolf.asyncapi.v3.bindings.amqp.AMQPChannelBinding; import io.github.springwolf.asyncapi.v3.model.channel.ChannelObject; import io.github.springwolf.core.asyncapi.scanners.ChannelsScanner; import io.github.springwolf.plugins.amqp.asyncapi.scanners.bindings.RabbitListenerUtil; import lombok.RequiredArgsConstructor; +import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.Queue; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.Stream; @RequiredArgsConstructor public class RabbitQueueBeanScanner implements ChannelsScanner { private final List queues; + private final List bindings; @Override public Map scan() { - return queues.stream() - .map(RabbitListenerUtil::buildChannelObject) - .collect(Collectors.toMap( - o -> ((AMQPChannelBinding) o.getBindings().get(RabbitListenerUtil.BINDING_NAME)) - .getQueue() - .getName(), - c -> c, - (a, b) -> a)); + return Stream.concat( + queues.stream().map(RabbitListenerUtil::buildChannelObject), + bindings.stream() + .map(RabbitListenerUtil::buildChannelObject) + .flatMap(List::stream)) + .collect(Collectors.toMap(ChannelObject::getChannelId, c -> c, (a, b) -> a)); } } diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/configuration/SpringwolfAmqpScannerConfiguration.java b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/configuration/SpringwolfAmqpScannerConfiguration.java index d4e3a8311..30195f6a6 100644 --- a/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/configuration/SpringwolfAmqpScannerConfiguration.java +++ b/springwolf-plugins/springwolf-amqp-plugin/src/main/java/io/github/springwolf/plugins/amqp/configuration/SpringwolfAmqpScannerConfiguration.java @@ -193,7 +193,7 @@ public OperationsScanner simpleRabbitMethodLevelListenerAnnotationOperationsScan havingValue = "true", matchIfMissing = true) @Order(value = ChannelPriority.AUTO_DISCOVERED) - public RabbitQueueBeanScanner rabbitQueueBeanScanner(List queues) { - return new RabbitQueueBeanScanner(queues); + public RabbitQueueBeanScanner rabbitQueueBeanScanner(List queues, List bindings) { + return new RabbitQueueBeanScanner(queues, bindings); } } diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/RabbitListenerUtilContextTest.java b/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/RabbitListenerUtilContextTest.java new file mode 100644 index 000000000..deae8b049 --- /dev/null +++ b/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/RabbitListenerUtilContextTest.java @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.springwolf.plugins.amqp.asyncapi.scanners.bindings; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.TopicExchange; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Nested +class RabbitListenerUtilContextTest { + @Test + void testEmptyContext() { + // when + RabbitListenerUtilContext context = RabbitListenerUtilContext.create(List.of(), List.of(), List.of()); + + // then + assertEquals(Map.of(), context.queueMap()); + assertEquals(Map.of(), context.exchangeMap()); + assertEquals(Map.of(), context.bindingMap()); + } + + @Test + void testWithSingleTopic() { + org.springframework.amqp.core.Queue queue = + new org.springframework.amqp.core.Queue("queue-1", false, true, true); + TopicExchange exchange = new TopicExchange("exchange-name", false, true); + Binding binding = BindingBuilder.bind(queue).to(exchange).with("routing-key"); + + // when + RabbitListenerUtilContext context = + RabbitListenerUtilContext.create(List.of(queue), List.of(exchange), List.of(binding)); + + // then + assertEquals(Map.of("queue-1", queue), context.queueMap()); + assertEquals(Map.of("exchange-name", exchange), context.exchangeMap()); + assertEquals(Map.of("queue-1", binding), context.bindingMap()); + } + + @Test + void testWithMultipleBeansForOneTopic() { + org.springframework.amqp.core.Queue queueBean = + new org.springframework.amqp.core.Queue("queue-1", false, true, true); + TopicExchange exchangeBean = new TopicExchange("exchange-name", false, true); + Binding bindingBean = BindingBuilder.bind(queueBean).to(exchangeBean).with("routing-key"); + + // In this test, annotation values are different compared to the beans. + // This might happen due to ill user configuration, but like Spring AMQP Springwolf tries to handle it + org.springframework.amqp.core.Queue queueAnnotation = + new org.springframework.amqp.core.Queue("queue-1", false, false, false); + TopicExchange exchangeAnnotation = new TopicExchange("exchange-name", true, false); + Binding bindingAnnotation = + BindingBuilder.bind(queueAnnotation).to(exchangeAnnotation).with("routing-key"); + + // when + RabbitListenerUtilContext context = RabbitListenerUtilContext.create( + List.of(queueBean, queueAnnotation), + List.of(exchangeBean, exchangeAnnotation), + List.of(bindingBean, bindingAnnotation)); + + // then + assertThat(context.queueMap()).hasSize(1); + assertThat(context.queueMap().get("queue-1")).isIn(queueBean, queueAnnotation); + assertThat(context.exchangeMap()).hasSize(1); + assertThat(context.exchangeMap().get("exchange-name")).isIn(exchangeBean, exchangeAnnotation); + assertThat(context.bindingMap()).hasSize(1); + assertThat(context.bindingMap().get("queue-1")).isIn(bindingBean, bindingAnnotation); + } +} diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/RabbitListenerUtilTest.java b/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/RabbitListenerUtilTest.java index e21b7150d..d7d08954c 100644 --- a/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/RabbitListenerUtilTest.java +++ b/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/bindings/RabbitListenerUtilTest.java @@ -12,12 +12,9 @@ import io.github.springwolf.asyncapi.v3.bindings.amqp.AMQPMessageBinding; import io.github.springwolf.asyncapi.v3.bindings.amqp.AMQPOperationBinding; import org.assertj.core.util.Sets; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.amqp.core.Binding; -import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.TopicExchange; import org.springframework.amqp.rabbit.annotation.Exchange; import org.springframework.amqp.rabbit.annotation.Queue; @@ -29,26 +26,51 @@ import java.util.Map; import static java.util.Collections.emptyMap; -import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; class RabbitListenerUtilTest { - private final RabbitListenerUtil.RabbitListenerUtilContext emptyContext = - new RabbitListenerUtil.RabbitListenerUtilContext(emptyMap(), emptyMap(), emptyMap()); + private final RabbitListenerUtilContext emptyContext = + new RabbitListenerUtilContext(emptyMap(), emptyMap(), emptyMap()); + + private final org.springframework.amqp.core.Queue queue = + new org.springframework.amqp.core.Queue("queue-1", false, true, true); + private final RabbitListenerUtilContext queueContext = + RabbitListenerUtilContext.create(List.of(queue), List.of(), List.of()); + + private final TopicExchange exchange = new TopicExchange("exchange-name", false, true); + private final RabbitListenerUtilContext exchangeContext = + RabbitListenerUtilContext.create(List.of(queue), List.of(exchange), List.of()); + + StringValueResolver stringValueResolver = mock(StringValueResolver.class); + + @BeforeEach + void setUp() { + doAnswer(invocation -> { + String arg = (String) invocation.getArguments()[0]; + return switch (arg) { + case "${queue-1}" -> "queue-1"; + case "${queue-1}-id" -> "queue-1-id"; + case "${routing-key}" -> "routing-key"; + case "${exchange-name}" -> "exchange-name"; + case "${exchange-name}_${routing-key}" -> "exchange-name_routing-key"; + default -> arg; + }; + }) + .when(stringValueResolver) + .resolveStringValue(any()); + } @Nested class QueuesConfiguration { - @ParameterizedTest - @ValueSource(classes = {ClassWithQueuesConfiguration.class, ClassWithQueuesToDeclare.class}) - void getChannelName(Class clazz) { + @Test + void getChannelName() { // given - RabbitListener annotation = getAnnotation(clazz); - StringValueResolver stringValueResolver = mock(StringValueResolver.class); - when(stringValueResolver.resolveStringValue("${queue-1}")).thenReturn("queue-1"); + RabbitListener annotation = getAnnotation(ClassWithQueuesConfiguration.class); // when String channelName = RabbitListenerUtil.getChannelName(annotation, stringValueResolver); @@ -57,13 +79,39 @@ void getChannelName(Class clazz) { assertEquals("queue-1", channelName); } - @ParameterizedTest - @ValueSource(classes = {ClassWithQueuesConfiguration.class, ClassWithQueuesToDeclare.class}) - void buildChannelBinding(Class clazz) { + @Test + void buildChannelBinding() { + // given + RabbitListener annotation = getAnnotation(ClassWithQueuesConfiguration.class); + + // when + Map channelBinding = RabbitListenerUtil.buildChannelBinding( + annotation, RabbitListenerUtilTest.this.stringValueResolver, queueContext); + + // then + assertEquals(1, channelBinding.size()); + assertEquals(Sets.newTreeSet("amqp"), channelBinding.keySet()); + assertEquals( + AMQPChannelBinding.builder() + .is(AMQPChannelType.QUEUE) + .queue(AMQPChannelQueueProperties.builder() + .name("queue-1") + .durable(false) + .autoDelete(true) + .exclusive(true) + .vhost("/") + .build()) + .build(), + channelBinding.get("amqp")); + } + + /** + * Technically an invalid configuration as queue will be part of the spring context + */ + @Test + void buildChannelBindingWithEmptyContext() { // given - RabbitListener annotation = getAnnotation(clazz); - StringValueResolver stringValueResolver = mock(StringValueResolver.class); - when(stringValueResolver.resolveStringValue("${queue-1}")).thenReturn("queue-1"); + RabbitListener annotation = getAnnotation(ClassWithQueuesConfiguration.class); // when Map channelBinding = @@ -86,13 +134,28 @@ void buildChannelBinding(Class clazz) { channelBinding.get("amqp")); } - @ParameterizedTest - @ValueSource(classes = {ClassWithQueuesConfiguration.class, ClassWithQueuesToDeclare.class}) - void buildOperationBinding(Class clazz) { + @Test + void buildOperationBinding() { + // given + RabbitListener annotation = getAnnotation(ClassWithQueuesConfiguration.class); + + // when + Map operationBinding = RabbitListenerUtil.buildOperationBinding( + annotation, RabbitListenerUtilTest.this.stringValueResolver, queueContext); + + // then + assertEquals(1, operationBinding.size()); + assertEquals(Sets.newTreeSet("amqp"), operationBinding.keySet()); + assertEquals(AMQPOperationBinding.builder().build(), operationBinding.get("amqp")); + } + + /** + * Technically an invalid configuration as queue will be part of the spring context + */ + @Test + void buildOperationBindingWithEmptyContext() { // given - RabbitListener annotation = getAnnotation(clazz); - StringValueResolver stringValueResolver = mock(StringValueResolver.class); - when(stringValueResolver.resolveStringValue("${queue-1}")).thenReturn("queue-1"); + RabbitListener annotation = getAnnotation(ClassWithQueuesConfiguration.class); // when Map operationBinding = @@ -101,7 +164,7 @@ void buildOperationBinding(Class clazz) { // then assertEquals(1, operationBinding.size()); assertEquals(Sets.newTreeSet("amqp"), operationBinding.keySet()); - assertEquals(AMQPOperationBinding.builder().cc(List.of("queue-1")).build(), operationBinding.get("amqp")); + assertEquals(AMQPOperationBinding.builder().build(), operationBinding.get("amqp")); } @Test @@ -120,25 +183,15 @@ private static class ClassWithQueuesConfiguration { @RabbitListener(queues = "${queue-1}") private void methodWithAnnotation(String payload) {} } - - /** - * Note: bindings, queues, and queuesToDeclare are mutually exclusive - * @see RabbitListener.queuesToDeclare - */ - private static class ClassWithQueuesToDeclare { - @RabbitListener(queuesToDeclare = @Queue(name = "${queue-1}")) - private void methodWithAnnotation(String payload) {} - } } @Nested - class QueueBindingsConfiguration { + class QueuesToDeclareConfiguration { + @Test void getChannelName() { // given - RabbitListener annotation = getAnnotation(ClassWithBindingsConfiguration.class); - StringValueResolver stringValueResolver = mock(StringValueResolver.class); - when(stringValueResolver.resolveStringValue("${queue-1}")).thenReturn("queue-1"); + RabbitListener annotation = getAnnotation(ClassWithQueuesToDeclare.class); // when String channelName = RabbitListenerUtil.getChannelName(annotation, stringValueResolver); @@ -147,12 +200,39 @@ void getChannelName() { assertEquals("queue-1", channelName); } + /** + * Technically an invalid configuration as context should be empty + */ @Test void buildChannelBinding() { // given - RabbitListener annotation = getAnnotation(ClassWithBindingsConfiguration.class); - StringValueResolver stringValueResolver = mock(StringValueResolver.class); - when(stringValueResolver.resolveStringValue("${queue-1}")).thenReturn("queue-1"); + RabbitListener annotation = getAnnotation(ClassWithQueuesToDeclare.class); + + // when + Map channelBinding = RabbitListenerUtil.buildChannelBinding( + annotation, RabbitListenerUtilTest.this.stringValueResolver, queueContext); + + // then + assertEquals(1, channelBinding.size()); + assertEquals(Sets.newTreeSet("amqp"), channelBinding.keySet()); + assertEquals( + AMQPChannelBinding.builder() + .is(AMQPChannelType.QUEUE) + .queue(AMQPChannelQueueProperties.builder() + .name("queue-1") + .durable(false) + .autoDelete(true) + .exclusive(true) + .vhost("/") + .build()) + .build(), + channelBinding.get("amqp")); + } + + @Test + void buildChannelBindingWithEmptyContext() { + // given + RabbitListener annotation = getAnnotation(ClassWithQueuesToDeclare.class); // when Map channelBinding = @@ -163,38 +243,55 @@ void buildChannelBinding() { assertEquals(Sets.newTreeSet("amqp"), channelBinding.keySet()); assertEquals( AMQPChannelBinding.builder() - .is(AMQPChannelType.ROUTING_KEY) - .exchange(AMQPChannelExchangeProperties.builder() - .name("exchange-name") - .type(AMQPChannelExchangeType.TOPIC) + .is(AMQPChannelType.QUEUE) + .queue(AMQPChannelQueueProperties.builder() + .name("queue-1") .durable(true) .autoDelete(false) + .exclusive(false) + .vhost("/") .build()) .build(), channelBinding.get("amqp")); } + /** + * Technically an invalid configuration as context should be empty + */ @Test void buildOperationBinding() { // given - RabbitListener annotation = getAnnotation(ClassWithBindingsConfiguration.class); - StringValueResolver stringValueResolver = mock(StringValueResolver.class); - when(stringValueResolver.resolveStringValue("${queue-1}")).thenReturn("queue-1"); + RabbitListener annotation = getAnnotation(ClassWithQueuesToDeclare.class); // when - Map operationBinding = + Map operationBinding = RabbitListenerUtil.buildOperationBinding( + annotation, RabbitListenerUtilTest.this.stringValueResolver, queueContext); + + // then + assertEquals(1, operationBinding.size()); + assertEquals(Sets.newTreeSet("amqp"), operationBinding.keySet()); + assertEquals(AMQPOperationBinding.builder().build(), operationBinding.get("amqp")); + } + + @Test + void buildOperationBindingWithEmptyContext() { + // given + RabbitListener annotation = getAnnotation(ClassWithQueuesToDeclare.class); + + // when + Map operationBinding = RabbitListenerUtil.buildOperationBinding(annotation, stringValueResolver, emptyContext); // then assertEquals(1, operationBinding.size()); assertEquals(Sets.newTreeSet("amqp"), operationBinding.keySet()); - assertEquals(AMQPOperationBinding.builder().cc(List.of("")).build(), operationBinding.get("amqp")); + assertEquals(AMQPOperationBinding.builder().build(), operationBinding.get("amqp")); } @Test void buildMessageBinding() { // when - Map messageBinding = RabbitListenerUtil.buildMessageBinding(); + Map messageBinding = RabbitListenerUtil.buildMessageBinding(); // then assertEquals(1, messageBinding.size()); @@ -202,40 +299,97 @@ void buildMessageBinding() { assertEquals(new AMQPMessageBinding(), messageBinding.get("amqp")); } - private static class ClassWithBindingsConfiguration { - - @RabbitListener( - bindings = { - @QueueBinding( - exchange = @Exchange(name = "exchange-name", type = "topic"), - value = @Queue(name = "${queue-1}")) - }) + private static class ClassWithQueuesToDeclare { + // Note: Bean should not be in the context as it is created (declared) by the annotation + @RabbitListener(queuesToDeclare = @Queue(name = "${queue-1}")) private void methodWithAnnotation(String payload) {} } } @Nested - class QueueBindingWithRoutingKeyConfiguration { + class QueueBindingsConfiguration { + + @Test + void getChannelId() { + // given + RabbitListener annotation = getAnnotation(ClassWithBindingConfiguration.class); + + // when + String channelName = RabbitListenerUtil.getChannelId(annotation, stringValueResolver); + + // then + assertEquals("exchange-name", channelName); + } + @Test void getChannelName() { // given - RabbitListener annotation = getAnnotation(ClassWithBindingsAndRoutingKeyConfiguration.class); - StringValueResolver stringValueResolver = mock(StringValueResolver.class); - when(stringValueResolver.resolveStringValue("${routing-key}")).thenReturn("routing-key"); + RabbitListener annotation = getAnnotation(ClassWithBindingConfiguration.class); // when String channelName = RabbitListenerUtil.getChannelName(annotation, stringValueResolver); // then - assertEquals("routing-key", channelName); + assertEquals("exchange-name", channelName); } @Test void buildChannelBinding() { // given - RabbitListener annotation = getAnnotation(ClassWithBindingsAndRoutingKeyConfiguration.class); - StringValueResolver stringValueResolver = mock(StringValueResolver.class); - when(stringValueResolver.resolveStringValue("${queue-1}")).thenReturn("queue-1"); + RabbitListener annotation = getAnnotation(ClassWithBindingConfiguration.class); + + // when + Map channelBinding = RabbitListenerUtil.buildChannelBinding( + annotation, RabbitListenerUtilTest.this.stringValueResolver, queueContext); + + // then + assertEquals(1, channelBinding.size()); + assertEquals(Sets.newTreeSet("amqp"), channelBinding.keySet()); + assertEquals( + AMQPChannelBinding.builder() + .is(AMQPChannelType.ROUTING_KEY) + .exchange(AMQPChannelExchangeProperties.builder() + .name("exchange-name") + .type(AMQPChannelExchangeType.DIRECT) + .durable(true) + .autoDelete(false) + .build()) + .build(), + channelBinding.get("amqp")); + } + + @Test + void buildChannelBindingWithExchangeContext() { + // given + RabbitListener annotation = getAnnotation(ClassWithBindingConfiguration.class); + + // when + Map channelBinding = + RabbitListenerUtil.buildChannelBinding(annotation, stringValueResolver, exchangeContext); + + // then + assertEquals(1, channelBinding.size()); + assertEquals(Sets.newTreeSet("amqp"), channelBinding.keySet()); + assertEquals( + AMQPChannelBinding.builder() + .is(AMQPChannelType.ROUTING_KEY) + .exchange(AMQPChannelExchangeProperties.builder() + .name("exchange-name") + .type(AMQPChannelExchangeType.TOPIC) + .durable(false) + .autoDelete(true) + .build()) + .build(), + channelBinding.get("amqp")); + } + + /** + * Technically an invalid configuration as queue and exchange will be part of the spring context + */ + @Test + void buildChannelBindingWithEmptyContext() { + // given + RabbitListener annotation = getAnnotation(ClassWithBindingConfiguration.class); // when Map channelBinding = @@ -260,9 +414,40 @@ void buildChannelBinding() { @Test void buildOperationBinding() { // given - RabbitListener annotation = getAnnotation(ClassWithBindingsAndRoutingKeyConfiguration.class); - StringValueResolver stringValueResolver = mock(StringValueResolver.class); - when(stringValueResolver.resolveStringValue("${routing-key}")).thenReturn("routing-key"); + RabbitListener annotation = getAnnotation(ClassWithBindingConfiguration.class); + + // when + Map operationBinding = RabbitListenerUtil.buildOperationBinding( + annotation, RabbitListenerUtilTest.this.stringValueResolver, queueContext); + + // then + assertEquals(1, operationBinding.size()); + assertEquals(Sets.newTreeSet("amqp"), operationBinding.keySet()); + assertEquals(AMQPOperationBinding.builder().build(), operationBinding.get("amqp")); + } + + @Test + void buildOperationBindingWithExchangeContext() { + // given + RabbitListener annotation = getAnnotation(ClassWithBindingConfiguration.class); + + // when + Map operationBinding = RabbitListenerUtil.buildOperationBinding( + annotation, RabbitListenerUtilTest.this.stringValueResolver, exchangeContext); + + // then + assertEquals(1, operationBinding.size()); + assertEquals(Sets.newTreeSet("amqp"), operationBinding.keySet()); + assertEquals(AMQPOperationBinding.builder().build(), operationBinding.get("amqp")); + } + + /** + * Technically an invalid configuration as queue and exchange will be part of the spring context + */ + @Test + void buildOperationBindingWithEmptyContext() { + // given + RabbitListener annotation = getAnnotation(ClassWithBindingConfiguration.class); // when Map operationBinding = @@ -271,8 +456,7 @@ void buildOperationBinding() { // then assertEquals(1, operationBinding.size()); assertEquals(Sets.newTreeSet("amqp"), operationBinding.keySet()); - assertEquals( - AMQPOperationBinding.builder().cc(List.of("routing-key")).build(), operationBinding.get("amqp")); + assertEquals(AMQPOperationBinding.builder().build(), operationBinding.get("amqp")); } @Test @@ -286,13 +470,11 @@ void buildMessageBinding() { assertEquals(new AMQPMessageBinding(), messageBinding.get("amqp")); } - private static class ClassWithBindingsAndRoutingKeyConfiguration { - + private static class ClassWithBindingConfiguration { @RabbitListener( bindings = { @QueueBinding( - exchange = @Exchange(name = "exchange-name"), - key = "${routing-key}", + exchange = @Exchange(name = "${exchange-name}"), value = @Queue(name = "${queue-1}")) }) private void methodWithAnnotation(String payload) {} @@ -300,46 +482,39 @@ private void methodWithAnnotation(String payload) {} } @Nested - class QueueBindingsWithBeansConfiguration { - private final RabbitListenerUtil.RabbitListenerUtilContext context; + class QueueBindingWithRoutingKeyConfiguration { + @Test + void getChannelId() { + // given + RabbitListener annotation = getAnnotation(ClassWithBindingsAndRoutingKeyConfiguration.class); - // Simulate a RabbitListenerUtilContext that has already been populated by exising spring beans - { - org.springframework.amqp.core.Queue queue = - new org.springframework.amqp.core.Queue("queue-1", false, true, true); - TopicExchange exchange = new TopicExchange("exchange-name", false, true); - context = RabbitListenerUtil.RabbitListenerUtilContext.create( - List.of(queue), - List.of(exchange), - List.of(BindingBuilder.bind(queue).to(exchange).with("routing-key"))); + // when + String channelName = RabbitListenerUtil.getChannelId(annotation, stringValueResolver); + + // then + assertEquals("exchange-name_routing-key", channelName); } @Test void getChannelName() { // given - RabbitListener annotation = getAnnotation(ClassWithBindingsConfiguration.class); - StringValueResolver stringValueResolver = mock(StringValueResolver.class); - when(stringValueResolver.resolveStringValue("${queue-1}")).thenReturn("queue-1"); - when(stringValueResolver.resolveStringValue("${routing-key}")).thenReturn("routing-key"); + RabbitListener annotation = getAnnotation(ClassWithBindingsAndRoutingKeyConfiguration.class); // when String channelName = RabbitListenerUtil.getChannelName(annotation, stringValueResolver); // then - assertEquals("routing-key", channelName); + assertEquals("exchange-name_routing-key", channelName); } @Test void buildChannelBinding() { // given - RabbitListener annotation = getAnnotation(ClassWithBindingsConfiguration.class); - StringValueResolver stringValueResolver = mock(StringValueResolver.class); - when(stringValueResolver.resolveStringValue("${queue-1}")).thenReturn("queue-1"); - when(stringValueResolver.resolveStringValue("${routing-key}")).thenReturn("routing-key"); + RabbitListener annotation = getAnnotation(ClassWithBindingsAndRoutingKeyConfiguration.class); // when Map channelBinding = - RabbitListenerUtil.buildChannelBinding(annotation, stringValueResolver, context); + RabbitListenerUtil.buildChannelBinding(annotation, stringValueResolver, queueContext); // then assertEquals(1, channelBinding.size()); @@ -349,9 +524,34 @@ void buildChannelBinding() { .is(AMQPChannelType.ROUTING_KEY) .exchange(AMQPChannelExchangeProperties.builder() .name("exchange-name") - .type(AMQPChannelExchangeType.TOPIC) - .durable(false) - .autoDelete(true) + .type(AMQPChannelExchangeType.DIRECT) + .durable(true) + .autoDelete(false) + .build()) + .build(), + channelBinding.get("amqp")); + } + + @Test + void buildChannelBindingWithEmptyContext() { + // given + RabbitListener annotation = getAnnotation(ClassWithBindingsAndRoutingKeyConfiguration.class); + + // when + Map channelBinding = + RabbitListenerUtil.buildChannelBinding(annotation, stringValueResolver, emptyContext); + + // then + assertEquals(1, channelBinding.size()); + assertEquals(Sets.newTreeSet("amqp"), channelBinding.keySet()); + assertEquals( + AMQPChannelBinding.builder() + .is(AMQPChannelType.ROUTING_KEY) + .exchange(AMQPChannelExchangeProperties.builder() + .name("exchange-name") + .type(AMQPChannelExchangeType.DIRECT) + .durable(true) + .autoDelete(false) .build()) .build(), channelBinding.get("amqp")); @@ -360,20 +560,31 @@ void buildChannelBinding() { @Test void buildOperationBinding() { // given - RabbitListener annotation = getAnnotation(ClassWithBindingsConfiguration.class); - StringValueResolver stringValueResolver = mock(StringValueResolver.class); - when(stringValueResolver.resolveStringValue("${queue-1}")).thenReturn("queue-1"); - when(stringValueResolver.resolveStringValue("${routing-key}")).thenReturn("routing-key"); + RabbitListener annotation = getAnnotation(ClassWithBindingsAndRoutingKeyConfiguration.class); // when Map operationBinding = - RabbitListenerUtil.buildOperationBinding(annotation, stringValueResolver, context); + RabbitListenerUtil.buildOperationBinding(annotation, stringValueResolver, queueContext); // then assertEquals(1, operationBinding.size()); assertEquals(Sets.newTreeSet("amqp"), operationBinding.keySet()); - assertEquals( - AMQPOperationBinding.builder().cc(List.of("routing-key")).build(), operationBinding.get("amqp")); + assertEquals(AMQPOperationBinding.builder().build(), operationBinding.get("amqp")); + } + + @Test + void buildOperationBindingWithEmptyContext() { + // given + RabbitListener annotation = getAnnotation(ClassWithBindingsAndRoutingKeyConfiguration.class); + + // when + Map operationBinding = + RabbitListenerUtil.buildOperationBinding(annotation, stringValueResolver, emptyContext); + + // then + assertEquals(1, operationBinding.size()); + assertEquals(Sets.newTreeSet("amqp"), operationBinding.keySet()); + assertEquals(AMQPOperationBinding.builder().build(), operationBinding.get("amqp")); } @Test @@ -387,82 +598,19 @@ void buildMessageBinding() { assertEquals(new AMQPMessageBinding(), messageBinding.get("amqp")); } - private static class ClassWithBindingsConfiguration { + private static class ClassWithBindingsAndRoutingKeyConfiguration { @RabbitListener( bindings = { @QueueBinding( - exchange = @Exchange(name = "exchange-name"), - value = @Queue(name = "${queue-1}"), - key = "${routing-key}"), + exchange = @Exchange(name = "${exchange-name}"), + key = "${routing-key}", + value = @Queue(name = "${queue-1}")) }) private void methodWithAnnotation(String payload) {} } } - @Nested - class RabbitListenerUtilContextTest { - @Test - void testEmptyContext() { - // when - RabbitListenerUtil.RabbitListenerUtilContext context = - RabbitListenerUtil.RabbitListenerUtilContext.create(List.of(), List.of(), List.of()); - - // then - assertEquals(Map.of(), context.queueMap()); - assertEquals(Map.of(), context.exchangeMap()); - assertEquals(Map.of(), context.bindingMap()); - } - - @Test - void testWithSingleTopic() { - org.springframework.amqp.core.Queue queue = - new org.springframework.amqp.core.Queue("queue-1", false, true, true); - TopicExchange exchange = new TopicExchange("exchange-name", false, true); - Binding binding = BindingBuilder.bind(queue).to(exchange).with("routing-key"); - - // when - RabbitListenerUtil.RabbitListenerUtilContext context = RabbitListenerUtil.RabbitListenerUtilContext.create( - List.of(queue), List.of(exchange), List.of(binding)); - - // then - assertEquals(Map.of("queue-1", queue), context.queueMap()); - assertEquals(Map.of("exchange-name", exchange), context.exchangeMap()); - assertEquals(Map.of("queue-1", binding), context.bindingMap()); - } - - @Test - void testWithMultipleBeansForOneTopic() { - org.springframework.amqp.core.Queue queueBean = - new org.springframework.amqp.core.Queue("queue-1", false, true, true); - TopicExchange exchangeBean = new TopicExchange("exchange-name", false, true); - Binding bindingBean = - BindingBuilder.bind(queueBean).to(exchangeBean).with("routing-key"); - - // In this test, annotation values are different compared to the beans. - // This might happen due to ill user configuration, but like Spring AMQP Springwolf tries to handle it - org.springframework.amqp.core.Queue queueAnnotation = - new org.springframework.amqp.core.Queue("queue-1", false, false, false); - TopicExchange exchangeAnnotation = new TopicExchange("exchange-name", true, false); - Binding bindingAnnotation = - BindingBuilder.bind(queueAnnotation).to(exchangeAnnotation).with("routing-key"); - - // when - RabbitListenerUtil.RabbitListenerUtilContext context = RabbitListenerUtil.RabbitListenerUtilContext.create( - List.of(queueBean, queueAnnotation), - List.of(exchangeBean, exchangeAnnotation), - List.of(bindingBean, bindingAnnotation)); - - // then - assertThat(context.queueMap()).hasSize(1); - assertThat(context.queueMap().get("queue-1")).isIn(queueBean, queueAnnotation); - assertThat(context.exchangeMap()).hasSize(1); - assertThat(context.exchangeMap().get("exchange-name")).isIn(exchangeBean, exchangeAnnotation); - assertThat(context.bindingMap()).hasSize(1); - assertThat(context.bindingMap().get("queue-1")).isIn(bindingBean, bindingAnnotation); - } - } - private static RabbitListener getAnnotation(Class clazz) { return clazz.getDeclaredMethods()[0].getAnnotation(RabbitListener.class); } diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/channels/RabbitQueueBeanScannerTest.java b/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/channels/RabbitQueueBeanScannerTest.java index d12d8244b..e7e94e0c2 100644 --- a/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/channels/RabbitQueueBeanScannerTest.java +++ b/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/springwolf/plugins/amqp/asyncapi/scanners/channels/RabbitQueueBeanScannerTest.java @@ -2,10 +2,12 @@ package io.github.springwolf.plugins.amqp.asyncapi.scanners.channels; import io.github.springwolf.asyncapi.v3.bindings.amqp.AMQPChannelBinding; +import io.github.springwolf.asyncapi.v3.bindings.amqp.AMQPChannelExchangeProperties; import io.github.springwolf.asyncapi.v3.bindings.amqp.AMQPChannelQueueProperties; import io.github.springwolf.asyncapi.v3.bindings.amqp.AMQPChannelType; import io.github.springwolf.asyncapi.v3.model.channel.ChannelObject; import org.junit.jupiter.api.Test; +import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.Queue; import java.util.List; @@ -19,7 +21,8 @@ class RabbitQueueBeanScannerTest { void scan() { // given var queue = new Queue("name"); - var scanner = new RabbitQueueBeanScanner(List.of(queue)); + var binding = new Binding("destination", Binding.DestinationType.QUEUE, "exchange", "routingKey", null); + var scanner = new RabbitQueueBeanScanner(List.of(queue), List.of(binding)); // when var result = scanner.scan(); @@ -42,6 +45,32 @@ void scan() { .exclusive(false) .build()) .build())) + .build(), + "exchange_routingKey", + ChannelObject.builder() + .channelId("exchange_routingKey") + .address("routingKey") + .bindings(Map.of( + "amqp", + AMQPChannelBinding.builder() + .is(AMQPChannelType.ROUTING_KEY) + .exchange(AMQPChannelExchangeProperties.builder() + .name("exchange") + .build()) + .build())) + .build(), + "destination", + ChannelObject.builder() + .channelId("destination") + .address("destination") + .bindings(Map.of( + "amqp", + AMQPChannelBinding.builder() + .is(AMQPChannelType.QUEUE) + .queue(AMQPChannelQueueProperties.builder() + .name("destination") + .build()) + .build())) .build())); } } diff --git a/springwolf-plugins/springwolf-stomp-plugin/src/main/java/io/github/springwolf/plugins/stomp/asyncapi/scanners/operation/annotations/SendToCustomizer.java b/springwolf-plugins/springwolf-stomp-plugin/src/main/java/io/github/springwolf/plugins/stomp/asyncapi/scanners/operation/annotations/SendToCustomizer.java index 40b22d22e..4e4836f24 100644 --- a/springwolf-plugins/springwolf-stomp-plugin/src/main/java/io/github/springwolf/plugins/stomp/asyncapi/scanners/operation/annotations/SendToCustomizer.java +++ b/springwolf-plugins/springwolf-stomp-plugin/src/main/java/io/github/springwolf/plugins/stomp/asyncapi/scanners/operation/annotations/SendToCustomizer.java @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.springwolf.plugins.stomp.asyncapi.scanners.operation.annotations; -import io.github.springwolf.asyncapi.v3.model.ReferenceUtil; import io.github.springwolf.asyncapi.v3.model.channel.ChannelReference; import io.github.springwolf.asyncapi.v3.model.channel.message.MessageReference; import io.github.springwolf.asyncapi.v3.model.operation.Operation; @@ -25,7 +24,7 @@ public class SendToCustomizer implements OperationCustomizer { public void customize(Operation operation, Method method) { SendTo annotation = AnnotationUtil.findFirstAnnotation(SendTo.class, method); if (annotation != null) { - String channelId = ReferenceUtil.toValidId(bindingFactory.getChannelName(annotation)); + String channelId = bindingFactory.getChannelId(annotation); String payloadName = payloadService.extractSchema(method).name(); operation.setReply(OperationReply.builder() diff --git a/springwolf-plugins/springwolf-stomp-plugin/src/main/java/io/github/springwolf/plugins/stomp/asyncapi/scanners/operation/annotations/SendToUserCustomizer.java b/springwolf-plugins/springwolf-stomp-plugin/src/main/java/io/github/springwolf/plugins/stomp/asyncapi/scanners/operation/annotations/SendToUserCustomizer.java index af3e36526..086899c39 100644 --- a/springwolf-plugins/springwolf-stomp-plugin/src/main/java/io/github/springwolf/plugins/stomp/asyncapi/scanners/operation/annotations/SendToUserCustomizer.java +++ b/springwolf-plugins/springwolf-stomp-plugin/src/main/java/io/github/springwolf/plugins/stomp/asyncapi/scanners/operation/annotations/SendToUserCustomizer.java @@ -1,7 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.springwolf.plugins.stomp.asyncapi.scanners.operation.annotations; -import io.github.springwolf.asyncapi.v3.model.ReferenceUtil; import io.github.springwolf.asyncapi.v3.model.channel.ChannelReference; import io.github.springwolf.asyncapi.v3.model.channel.message.MessageReference; import io.github.springwolf.asyncapi.v3.model.operation.Operation; @@ -25,7 +24,7 @@ public class SendToUserCustomizer implements OperationCustomizer { public void customize(Operation operation, Method method) { SendToUser annotation = AnnotationUtil.findFirstAnnotation(SendToUser.class, method); if (annotation != null) { - String channelId = ReferenceUtil.toValidId(bindingFactory.getChannelName(annotation)); + String channelId = bindingFactory.getChannelId(annotation); String payloadName = payloadService.extractSchema(method).name(); operation.setReply(OperationReply.builder() diff --git a/springwolf-plugins/springwolf-stomp-plugin/src/test/java/io/github/springwolf/plugins/stomp/asyncapi/scanners/operation/annotations/SendToCustomizerTest.java b/springwolf-plugins/springwolf-stomp-plugin/src/test/java/io/github/springwolf/plugins/stomp/asyncapi/scanners/operation/annotations/SendToCustomizerTest.java index a8a52f4a6..3056ca92b 100644 --- a/springwolf-plugins/springwolf-stomp-plugin/src/test/java/io/github/springwolf/plugins/stomp/asyncapi/scanners/operation/annotations/SendToCustomizerTest.java +++ b/springwolf-plugins/springwolf-stomp-plugin/src/test/java/io/github/springwolf/plugins/stomp/asyncapi/scanners/operation/annotations/SendToCustomizerTest.java @@ -31,6 +31,7 @@ class SendToCustomizerTest { void customize() throws NoSuchMethodException { // given Operation operation = new Operation(); + when(bindingFactory.getChannelId(any())).thenReturn(CHANNEL_ID); when(bindingFactory.getChannelName(any())).thenReturn(CHANNEL_ID); when(payloadService.extractSchema(any())).thenReturn(new PayloadSchemaObject(MESSAGE_ID, MESSAGE_ID, null)); diff --git a/springwolf-plugins/springwolf-stomp-plugin/src/test/java/io/github/springwolf/plugins/stomp/asyncapi/scanners/operation/annotations/SendToUserCustomizerTest.java b/springwolf-plugins/springwolf-stomp-plugin/src/test/java/io/github/springwolf/plugins/stomp/asyncapi/scanners/operation/annotations/SendToUserCustomizerTest.java index 486e47466..47b23afcb 100644 --- a/springwolf-plugins/springwolf-stomp-plugin/src/test/java/io/github/springwolf/plugins/stomp/asyncapi/scanners/operation/annotations/SendToUserCustomizerTest.java +++ b/springwolf-plugins/springwolf-stomp-plugin/src/test/java/io/github/springwolf/plugins/stomp/asyncapi/scanners/operation/annotations/SendToUserCustomizerTest.java @@ -31,6 +31,7 @@ class SendToUserCustomizerTest { void customize() throws NoSuchMethodException { // given Operation operation = new Operation(); + when(bindingFactory.getChannelId(any())).thenReturn(CHANNEL_ID); when(bindingFactory.getChannelName(any())).thenReturn(CHANNEL_ID); when(payloadService.extractSchema(any())).thenReturn(new PayloadSchemaObject(MESSAGE_ID, MESSAGE_ID, null));