From 491e1aa2243cc67bfaad6b56ba602bf4a32a066a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Kwiecie=C5=84?= <37042650+krzysztofxkwiecien@users.noreply.github.com> Date: Fri, 24 Nov 2023 09:56:12 +0100 Subject: [PATCH] feat(jms): JMS support (#432) * jms * docs: add @krzysztofxkwiecien as a contributor * readme & workflows update * added jms mock to website * test(jms): fix docker-compose setup * test: Add MessageBindingProcessor and OperationBindingProcessor coverage * test(jms): add controller and binding tests * chore: prepare jms artifact --------- Co-authored-by: Timon Back Co-authored-by: sam0r040 <93372330+sam0r040@users.noreply.github.com> --- .all-contributorsrc | 9 + .github/workflows/springwolf-plugins.yml | 2 +- README.md | 2 + RELEASING.md | 7 +- build.gradle | 1 + settings.gradle | 3 + .../springwolf-jms-example/.env | 1 + .../springwolf-jms-example/README.md | 6 + .../springwolf-jms-example/build.gradle | 63 +++++ .../springwolf-jms-example/docker-compose.yml | 20 ++ .../jms/SpringwolfJmsExampleApplication.java | 13 + .../example/jms/config/ConverterConfig.java | 20 ++ .../jms/consumers/ExampleConsumer.java | 33 +++ .../example/jms/dtos/AnotherPayloadDto.java | 23 ++ .../example/jms/dtos/ExamplePayloadDto.java | 30 +++ .../jms/producers/AnotherProducer.java | 32 +++ .../src/main/resources/application.properties | 33 +++ .../example/jms/ApiIntegrationTest.java | 44 ++++ .../springwolf/example/jms/ApiSystemTest.java | 66 +++++ .../jms/JmsTestContainerExtension.java | 42 ++++ .../example/jms/ProducerSystemTest.java | 66 +++++ ...fJmsExampleApplicationIntegrationTest.java | 23 ++ .../src/test/resources/asyncapi.json | 172 +++++++++++++ .../AmqpMessageBindingProcessorTest.java | 40 +++ .../AmqpOperationBindingProcessorTest.java | 32 +++ .../springwolf-jms-plugin/README.md | 42 ++++ .../springwolf-jms-plugin/build.gradle | 71 ++++++ .../controller/SpringwolfJmsController.java | 32 +++ .../jms/SpringwolfJmsAutoConfiguration.java | 23 ++ .../SpringwolfJmsProducerConfiguration.java | 37 +++ .../SpringwolfJmsScannerConfiguration.java | 46 ++++ .../processor/JmsMessageBindingProcessor.java | 37 +++ .../JmsOperationBindingProcessor.java | 14 ++ .../channels/annotation/JmsListenerUtil.java | 36 +++ .../MethodLevelJmsListenerScanner.java | 61 +++++ .../annotation/JmsAsyncOperationBinding.java | 21 ++ .../SpringwolfJmsConfigConstants.java | 16 ++ .../SpringwolfJmsConfigProperties.java | 50 ++++ .../producer/SpringwolfJmsProducer.java | 44 ++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + ...pringwolfJmsControllerIntegrationTest.java | 195 +++++++++++++++ .../JmsMessageBindingProcessorTest.java | 40 +++ .../JmsOperationBindingProcessorTest.java | 28 +++ .../annotation/JmsListenerUtilTest.java | 94 ++++++++ ...evelJmsListenerScannerIntegrationTest.java | 227 ++++++++++++++++++ .../ObjectMapperTestConfiguration.java | 17 ++ ...sProducerConfigurationIntegrationTest.java | 107 +++++++++ .../producer/SpringwolfJmsProducerTest.java | 38 +++ ...ingwolfKafkaControllerIntegrationTest.java | 10 +- .../KafkaMessageBindingProcessorTest.java | 40 +++ .../KafkaOperationBindingProcessorTest.java | 28 +++ .../SnsOperationBindingProcessor.java | 4 +- .../SnsMessageBindingProcessorTest.java | 40 +++ .../SnsOperationBindingProcessorTest.java | 28 +++ .../SqsMessageBindingProcessorTest.java | 40 +++ .../SqsOperationBindingProcessorTest.java | 28 +++ .../src/app/shared/mock/mock-server.ts | 3 + 57 files changed, 2269 insertions(+), 12 deletions(-) create mode 120000 springwolf-examples/springwolf-jms-example/.env create mode 100644 springwolf-examples/springwolf-jms-example/README.md create mode 100644 springwolf-examples/springwolf-jms-example/build.gradle create mode 100644 springwolf-examples/springwolf-jms-example/docker-compose.yml create mode 100644 springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/SpringwolfJmsExampleApplication.java create mode 100644 springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/config/ConverterConfig.java create mode 100644 springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/consumers/ExampleConsumer.java create mode 100644 springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/dtos/AnotherPayloadDto.java create mode 100644 springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/dtos/ExamplePayloadDto.java create mode 100644 springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/producers/AnotherProducer.java create mode 100644 springwolf-examples/springwolf-jms-example/src/main/resources/application.properties create mode 100644 springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/ApiIntegrationTest.java create mode 100644 springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/ApiSystemTest.java create mode 100644 springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/JmsTestContainerExtension.java create mode 100644 springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/ProducerSystemTest.java create mode 100644 springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/SpringwolfJmsExampleApplicationIntegrationTest.java create mode 100644 springwolf-examples/springwolf-jms-example/src/test/resources/asyncapi.json create mode 100644 springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/AmqpMessageBindingProcessorTest.java create mode 100644 springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/AmqpOperationBindingProcessorTest.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/README.md create mode 100644 springwolf-plugins/springwolf-jms-plugin/build.gradle create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/controller/SpringwolfJmsController.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/jms/SpringwolfJmsAutoConfiguration.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/jms/SpringwolfJmsProducerConfiguration.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/jms/SpringwolfJmsScannerConfiguration.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/JmsMessageBindingProcessor.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/JmsOperationBindingProcessor.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/JmsListenerUtil.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelJmsListenerScanner.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/JmsAsyncOperationBinding.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/configuration/properties/SpringwolfJmsConfigConstants.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/configuration/properties/SpringwolfJmsConfigProperties.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/producer/SpringwolfJmsProducer.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/controller/SpringwolfJmsControllerIntegrationTest.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/JmsMessageBindingProcessorTest.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/JmsOperationBindingProcessorTest.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/JmsListenerUtilTest.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelJmsListenerScannerIntegrationTest.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/ObjectMapperTestConfiguration.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfJmsProducerConfigurationIntegrationTest.java create mode 100644 springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/producer/SpringwolfJmsProducerTest.java rename springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/{ => controller}/SpringwolfKafkaControllerIntegrationTest.java (95%) create mode 100644 springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/KafkaMessageBindingProcessorTest.java create mode 100644 springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/KafkaOperationBindingProcessorTest.java create mode 100644 springwolf-plugins/springwolf-sns-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/SnsMessageBindingProcessorTest.java create mode 100644 springwolf-plugins/springwolf-sns-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/SnsOperationBindingProcessorTest.java create mode 100644 springwolf-plugins/springwolf-sqs-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/SqsMessageBindingProcessorTest.java create mode 100644 springwolf-plugins/springwolf-sqs-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/SqsOperationBindingProcessorTest.java diff --git a/.all-contributorsrc b/.all-contributorsrc index e49f22373..5eeba88d0 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -243,6 +243,15 @@ "contributions": [ "code" ] + }, + { + "login": "krzysztofxkwiecien", + "name": "Krzysztof Kwiecień", + "avatar_url": "https://avatars.githubusercontent.com/u/37042650?v=4", + "profile": "https://github.com/krzysztofxkwiecien", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/.github/workflows/springwolf-plugins.yml b/.github/workflows/springwolf-plugins.yml index ea0ab02e0..3a433ce27 100644 --- a/.github/workflows/springwolf-plugins.yml +++ b/.github/workflows/springwolf-plugins.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - plugin: [ "amqp", "cloud-stream", "kafka", "sns", "sqs" ] + plugin: [ "amqp", "cloud-stream", "jms", "kafka", "sns", "sqs" ] env: plugin: springwolf-plugins/springwolf-${{ matrix.plugin }}-plugin diff --git a/README.md b/README.md index 9f00b1c1b..f114bed47 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ More details in the documentation. | [AWS SNS](https://github.com/springwolf/springwolf-core/tree/master/springwolf-plugins/springwolf-sns-plugin) | [AWS SNS Example](https://github.com/springwolf/springwolf-core/tree/master/springwolf-examples/springwolf-sns-example) | ![Maven Central](https://img.shields.io/maven-central/v/io.github.springwolf/springwolf-sns?color=green&label=springwolf-sqs&style=plastic) | ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/io.github.springwolf/springwolf-sns?label=springwolf-sns&server=https%3A%2F%2Fs01.oss.sonatype.org&style=plastic) | | [AWS SQS](https://github.com/springwolf/springwolf-core/tree/master/springwolf-plugins/springwolf-sqs-plugin) | [AWS SQS Example](https://github.com/springwolf/springwolf-core/tree/master/springwolf-examples/springwolf-sqs-example) | ![Maven Central](https://img.shields.io/maven-central/v/io.github.springwolf/springwolf-sqs?color=green&label=springwolf-sqs&style=plastic) | ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/io.github.springwolf/springwolf-sqs?label=springwolf-sqs&server=https%3A%2F%2Fs01.oss.sonatype.org&style=plastic) | | [Cloud Stream](https://github.com/springwolf/springwolf-core/tree/master/springwolf-plugins/springwolf-cloud-stream-plugin) | [Cloud Stream Example](https://github.com/springwolf/springwolf-core/tree/master/springwolf-examples/springwolf-cloud-stream-example) | ![Maven Central](https://img.shields.io/maven-central/v/io.github.springwolf/springwolf-cloud-stream?color=green&label=springwolf-cloud-stream&style=plastic) | ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/io.github.springwolf/springwolf-cloud-stream?label=springwolf-cloud-stream&server=https%3A%2F%2Fs01.oss.sonatype.org&style=plastic) | +| [JMS](https://github.com/springwolf/springwolf-core/tree/master/springwolf-plugins/springwolf-jms-plugin) | [JMS Example](https://github.com/springwolf/springwolf-core/tree/master/springwolf-examples/springwolf-jms-example) | ![Maven Central](https://img.shields.io/maven-central/v/io.github.springwolf/springwolf-jms?color=green&label=springwolf-jms&style=plastic) | ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/io.github.springwolf/springwolf-jms?label=springwolf-jms&server=https%3A%2F%2Fs01.oss.sonatype.org&style=plastic) | | [Kafka](https://github.com/springwolf/springwolf-core/tree/master/springwolf-plugins/springwolf-kafka-plugin) | [Kafka Example](https://github.com/springwolf/springwolf-core/tree/master/springwolf-examples/springwolf-kafka-example) | ![Maven Central](https://img.shields.io/maven-central/v/io.github.springwolf/springwolf-kafka?color=green&label=springwolf-kafka&style=plastic) | ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/io.github.springwolf/springwolf-kafka?label=springwolf-kafka&server=https%3A%2F%2Fs01.oss.sonatype.org&style=plastic) | | [Common Model Converter](https://github.com/springwolf/springwolf-core/tree/master/springwolf-add-ons/springwolf-common-model-converters) | | ![Maven Central](https://img.shields.io/maven-central/v/io.github.springwolf/springwolf-common-model-converters?color=green&label=springwolf-common-model-converters&style=plastic) | ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/io.github.springwolf/springwolf-common-model-converters?label=springwolf-common-model-converters&server=https%3A%2F%2Fs01.oss.sonatype.org&style=plastic) | | [Generic Binding](https://github.com/springwolf/springwolf-core/tree/master/springwolf-add-ons/springwolf-generic-binding) | | ![Maven Central](https://img.shields.io/maven-central/v/io.github.springwolf/springwolf-generic-binding?color=green&label=springwolf-generic-binding&style=plastic) | ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/io.github.springwolf/springwolf-generic-binding?label=springwolf-generic-binding&server=https%3A%2F%2Fs01.oss.sonatype.org&style=plastic) | @@ -142,6 +143,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Sheheryar Aamir
Sheheryar Aamir

💻 jmwestbe
jmwestbe

💻 pdalfarr
pdalfarr

💻 + Krzysztof Kwiecień
Krzysztof Kwiecień

💻 diff --git a/RELEASING.md b/RELEASING.md index e59c53928..b596aa125 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -6,9 +6,10 @@ The following list describe the steps necessary to release a new version. 2. Run docker compose and manually test ui (or verify on website): 1. AMQP: https://amqp.demo.springwolf.dev/ 2. CloudStream https://cloud-stream.demo.springwolf.dev/ - 3. Kafka: https://kafka.demo.springwolf.dev/ - 4. SNS: https://sns.demo.springwolf.dev/ - 5. SQS: https://sqs.demo.springwolf.dev/ + 3. JMS: https://jms.demo.springwolf.dev/ + 4. Kafka: https://kafka.demo.springwolf.dev/ + 5. SNS: https://sns.demo.springwolf.dev/ + 6. SQS: https://sqs.demo.springwolf.dev/ 3. Update `all-contributors` in [README.md](README.md) 4. Remove the `-SNAPHSOT` postfix in `.env`, create a new branch `release/0.X.X` (version number), commit & push 5. Run GitHub `Publish releases` pipeline from the newly created release branch diff --git a/build.gradle b/build.gradle index c8df14b3b..e80d66b13 100644 --- a/build.gradle +++ b/build.gradle @@ -117,6 +117,7 @@ allprojects { project.name == 'springwolf-core' || project.name == 'springwolf-amqp' || project.name == 'springwolf-cloud-stream' || + project.name == 'springwolf-jms' || project.name == 'springwolf-kafka' || project.name == 'springwolf-sns' || project.name == 'springwolf-sqs' || diff --git a/settings.gradle b/settings.gradle index e1104c57e..ae0946379 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,11 +4,13 @@ include( 'springwolf-core', 'springwolf-plugins:springwolf-amqp-plugin', 'springwolf-plugins:springwolf-cloud-stream-plugin', + 'springwolf-plugins:springwolf-jms-plugin', 'springwolf-plugins:springwolf-kafka-plugin', 'springwolf-plugins:springwolf-sns-plugin', 'springwolf-plugins:springwolf-sqs-plugin', 'springwolf-examples:springwolf-amqp-example', 'springwolf-examples:springwolf-cloud-stream-example', + 'springwolf-examples:springwolf-jms-example', 'springwolf-examples:springwolf-kafka-example', 'springwolf-examples:springwolf-sns-example', 'springwolf-examples:springwolf-sqs-example', @@ -20,6 +22,7 @@ include( project(':springwolf-plugins:springwolf-amqp-plugin').name = 'springwolf-amqp' project(':springwolf-plugins:springwolf-cloud-stream-plugin').name = 'springwolf-cloud-stream' +project(':springwolf-plugins:springwolf-jms-plugin').name = 'springwolf-jms' project(':springwolf-plugins:springwolf-kafka-plugin').name = 'springwolf-kafka' project(':springwolf-plugins:springwolf-sns-plugin').name = 'springwolf-sns' project(':springwolf-plugins:springwolf-sqs-plugin').name = 'springwolf-sqs' diff --git a/springwolf-examples/springwolf-jms-example/.env b/springwolf-examples/springwolf-jms-example/.env new file mode 120000 index 000000000..c7360fb82 --- /dev/null +++ b/springwolf-examples/springwolf-jms-example/.env @@ -0,0 +1 @@ +../../.env \ No newline at end of file diff --git a/springwolf-examples/springwolf-jms-example/README.md b/springwolf-examples/springwolf-jms-example/README.md new file mode 100644 index 000000000..fc5e472fa --- /dev/null +++ b/springwolf-examples/springwolf-jms-example/README.md @@ -0,0 +1,6 @@ +## Usage + +### Run with docker compose (recommended) +1. Copy the `docker-compose.yml` file to your machine. +2. Run `$ docker-compose up`. +3. Visit `localhost:8080/springwolf/asyncapi-ui.html` or try the API: `$ curl localhost:8080/springwolf/docs`. diff --git a/springwolf-examples/springwolf-jms-example/build.gradle b/springwolf-examples/springwolf-jms-example/build.gradle new file mode 100644 index 000000000..3118a410b --- /dev/null +++ b/springwolf-examples/springwolf-jms-example/build.gradle @@ -0,0 +1,63 @@ +plugins { + id 'java' + + id 'org.springframework.boot' + id 'io.spring.dependency-management' + id 'ca.cutterslade.analyze' + + id 'com.bmuschko.docker-spring-boot-application' +} + +dependencies { + implementation project(":springwolf-core") + implementation project(":springwolf-add-ons:springwolf-generic-binding") + implementation project(":springwolf-plugins:springwolf-jms") + + annotationProcessor project(":springwolf-plugins:springwolf-jms") + runtimeOnly project(":springwolf-ui") + + compileOnly "jakarta.jms:jakarta.jms-api" + + implementation "org.slf4j:slf4j-api:${slf4jApiVersion}" + + implementation "io.swagger.core.v3:swagger-annotations:${swaggerVersion}" + + implementation "org.springframework:spring-context" + implementation "org.springframework:spring-jms" + implementation "org.springframework.boot:spring-boot" + implementation "org.springframework.boot:spring-boot-autoconfigure" + runtimeOnly "org.springframework.boot:spring-boot-starter-web" + runtimeOnly "org.springframework.boot:spring-boot-starter-artemis" + + testRuntimeOnly "org.junit.jupiter:junit-jupiter:${junitJupiterVersion}" + + testImplementation "org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}" + + testImplementation "org.mockito:mockito-core:${mockitoCoreVersion}" + + testImplementation "org.springframework.boot:spring-boot-test" + testImplementation "org.springframework:spring-beans" + testImplementation "org.springframework:spring-web" + testImplementation "org.springframework:spring-test" + + testImplementation "org.testcontainers:testcontainers:${testcontainersVersion}" + testImplementation "org.testcontainers:junit-jupiter:${testcontainersVersion}" +} + +docker { + springBootApplication { + maintainer = 'shamir.stav@gmail.com' + baseImage = 'eclipse-temurin:17-jre-focal' + ports = [8080] + images = ["stavshamir/springwolf-jms-example:${project.version}"] + } + + registryCredentials { + username = project.findProperty('DOCKERHUB_USERNAME') ?: '' + password = project.findProperty('DOCKERHUB_TOKEN') ?: '' + } +} + +test { + dependsOn dockerBuildImage +} diff --git a/springwolf-examples/springwolf-jms-example/docker-compose.yml b/springwolf-examples/springwolf-jms-example/docker-compose.yml new file mode 100644 index 000000000..ecab0a081 --- /dev/null +++ b/springwolf-examples/springwolf-jms-example/docker-compose.yml @@ -0,0 +1,20 @@ +version: '3' +services: + app: + image: stavshamir/springwolf-jms-example:${SPRINGWOLF_VERSION} + links: + - activemq + environment: + BOOTSTRAP_SERVER: tcp://activemq:61616 + ports: + - "8080:8080" + depends_on: + - activemq + + activemq: + image: apache/activemq-artemis:2.31.2 + environment: + EXTRA_ARGS: --http-host 0.0.0.0 --relax-jolokia --nio + ports: + - "61616:61616" + - "8161:8161" diff --git a/springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/SpringwolfJmsExampleApplication.java b/springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/SpringwolfJmsExampleApplication.java new file mode 100644 index 000000000..68ba0328a --- /dev/null +++ b/springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/SpringwolfJmsExampleApplication.java @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.example.jms; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringwolfJmsExampleApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringwolfJmsExampleApplication.class, args); + } +} diff --git a/springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/config/ConverterConfig.java b/springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/config/ConverterConfig.java new file mode 100644 index 000000000..6b830d929 --- /dev/null +++ b/springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/config/ConverterConfig.java @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.example.jms.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.support.converter.MappingJackson2MessageConverter; +import org.springframework.jms.support.converter.MessageConverter; +import org.springframework.jms.support.converter.MessageType; + +@Configuration +class ConverterConfig { + + @Bean + public MessageConverter jacksonJmsMessageConverter() { + MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(); + converter.setTargetType(MessageType.TEXT); + converter.setTypeIdPropertyName("_type"); + return converter; + } +} diff --git a/springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/consumers/ExampleConsumer.java b/springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/consumers/ExampleConsumer.java new file mode 100644 index 000000000..175455a6c --- /dev/null +++ b/springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/consumers/ExampleConsumer.java @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.example.jms.consumers; + +import io.github.stavshamir.springwolf.example.jms.dtos.AnotherPayloadDto; +import io.github.stavshamir.springwolf.example.jms.dtos.ExamplePayloadDto; +import io.github.stavshamir.springwolf.example.jms.producers.AnotherProducer; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class ExampleConsumer { + private final AnotherProducer anotherProducer; + + @JmsListener(destination = "example-queue") + public void receiveExamplePayload(ExamplePayloadDto payload) { + log.info("Received new message in example-queue: {}", payload.toString()); + + AnotherPayloadDto example = new AnotherPayloadDto(); + example.setExample(payload); + example.setFoo("foo"); + + anotherProducer.sendMessage(example); + } + + @JmsListener(destination = "another-queue") + public void receiveAnotherPayload(AnotherPayloadDto payload) { + log.info("Received new message in another-queue: {}", payload.toString()); + } +} diff --git a/springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/dtos/AnotherPayloadDto.java b/springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/dtos/AnotherPayloadDto.java new file mode 100644 index 000000000..102eed06f --- /dev/null +++ b/springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/dtos/AnotherPayloadDto.java @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.example.jms.dtos; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Schema(description = "Another payload model") +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AnotherPayloadDto { + + @Schema(description = "Foo field", example = "bar", requiredMode = NOT_REQUIRED) + private String foo; + + @Schema(description = "Example field", requiredMode = REQUIRED) + private ExamplePayloadDto example; +} diff --git a/springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/dtos/ExamplePayloadDto.java b/springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/dtos/ExamplePayloadDto.java new file mode 100644 index 000000000..70966ca38 --- /dev/null +++ b/springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/dtos/ExamplePayloadDto.java @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.example.jms.dtos; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Schema(description = "Example payload model") +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ExamplePayloadDto { + @Schema(description = "Some string field", example = "some string value", requiredMode = REQUIRED) + private String someString; + + @Schema(description = "Some long field", example = "5") + private long someLong; + + @Schema(description = "Some enum field", example = "FOO2", requiredMode = REQUIRED) + private ExampleEnum someEnum; + + public enum ExampleEnum { + FOO1, + FOO2, + FOO3 + } +} diff --git a/springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/producers/AnotherProducer.java b/springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/producers/AnotherProducer.java new file mode 100644 index 000000000..8c5a12aa5 --- /dev/null +++ b/springwolf-examples/springwolf-jms-example/src/main/java/io/github/stavshamir/springwolf/example/jms/producers/AnotherProducer.java @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.example.jms.producers; + +import io.github.stavshamir.springwolf.addons.generic_binding.annotation.AsyncGenericOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncOperation; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AsyncPublisher; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.JmsAsyncOperationBinding; +import io.github.stavshamir.springwolf.example.jms.dtos.AnotherPayloadDto; +import lombok.RequiredArgsConstructor; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class AnotherProducer { + private final JmsTemplate template; + + public static final String QUEUE = "another-queue"; + + @AsyncPublisher( + operation = + @AsyncOperation( + channelName = QUEUE, + description = "Custom, optional description defined in the AsyncPublisher annotation")) + @JmsAsyncOperationBinding + @AsyncGenericOperationBinding( + type = "jms", + fields = {"internal-field=customValue", "nested.key=nestedValue"}) + public void sendMessage(AnotherPayloadDto msg) { + template.convertAndSend(QUEUE, msg); + } +} diff --git a/springwolf-examples/springwolf-jms-example/src/main/resources/application.properties b/springwolf-examples/springwolf-jms-example/src/main/resources/application.properties new file mode 100644 index 000000000..e4675a685 --- /dev/null +++ b/springwolf-examples/springwolf-jms-example/src/main/resources/application.properties @@ -0,0 +1,33 @@ +######### +# Spring configuration +spring.application.name=Springwolf example project - JMS + + +######### +# JMS configuration +spring.artemis.broker-url=${BOOTSTRAP_SERVER:tcp://localhost:61616} +spring.artemis.user=artemis +spring.artemis.password=artemis + + +######### +# Springwolf configuration +springwolf.enabled=true +springwolf.docket.base-package=io.github.stavshamir.springwolf.example.jms +springwolf.docket.info.title=${spring.application.name} +springwolf.docket.info.version=1.0.0 +springwolf.docket.info.description=Springwolf example project to demonstrate springwolfs abilities +springwolf.docket.info.terms-of-service=http://asyncapi.org/terms +springwolf.docket.info.contact.name=springwolf +springwolf.docket.info.contact.email=example@example.com +springwolf.docket.info.contact.url=https://github.com/springwolf/springwolf-core +springwolf.docket.info.license.name=Apache License 2.0 +springwolf.docket.servers.jms.protocol=jms +springwolf.docket.servers.jms.url=${spring.artemis.broker-url} +springwolf.use-fqn=true + +springwolf.plugin.jms.publishing.enabled=true + + +# For debugging purposes +logging.level.io.github.stavshamir.springwolf=DEBUG diff --git a/springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/ApiIntegrationTest.java b/springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/ApiIntegrationTest.java new file mode 100644 index 000000000..1b9202b03 --- /dev/null +++ b/springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/ApiIntegrationTest.java @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.example.jms; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest( + classes = {SpringwolfJmsExampleApplication.class}, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ExtendWith({JmsTestContainerExtension.class}) +public class ApiIntegrationTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Value("${server.port}") + public Integer serverPort; + + @Test + void asyncApiResourceArtifactTest() throws IOException { + String url = "/springwolf/docs"; + String actual = restTemplate.getForObject(url, String.class); + Files.writeString(Path.of("src", "test", "resources", "asyncapi.actual.json"), actual); + + InputStream s = this.getClass().getResourceAsStream("/asyncapi.json"); + String expectedWithoutServersJmsUrlPatch = + new String(s.readAllBytes(), StandardCharsets.UTF_8).replace("\r\n", "\n"); + String expected = expectedWithoutServersJmsUrlPatch.replace("activemq:61616", "localhost:61616"); + + assertEquals(expected, actual); + } +} diff --git a/springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/ApiSystemTest.java b/springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/ApiSystemTest.java new file mode 100644 index 000000000..c31b6e37d --- /dev/null +++ b/springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/ApiSystemTest.java @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.example.jms; + +import org.junit.jupiter.api.Test; +import org.springframework.web.client.RestTemplate; +import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * While the assertion of this test is identical to ApiIntegrationTests, + * the setup uses a full docker-compose context with a real jms instance. + */ +@Testcontainers +// @Ignore("Uncomment this line if you have issues running this test on your local machine.") +public class ApiSystemTest { + + private static final RestTemplate restTemplate = new RestTemplate(); + private static final String APP_NAME = "app_1"; + private static final int APP_PORT = 8080; + + private static final Map ENV = new HashMap<>(); + + static { + try (InputStream input = new FileInputStream(".env")) { + var properties = new Properties(); + properties.load(input); + properties.forEach((key, value) -> ENV.put(String.valueOf(key), String.valueOf(value))); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Container + public DockerComposeContainer environment = new DockerComposeContainer<>(new File("docker-compose.yml")) + .withExposedService(APP_NAME, APP_PORT) + .withEnv(ENV); + + private String baseUrl() { + String host = environment.getServiceHost(APP_NAME, APP_PORT); + int port = environment.getServicePort(APP_NAME, APP_PORT); + return String.format("http://%s:%d", host, port); + } + + @Test + void asyncapiDocsShouldReturnTheCorrectJsonResponse() throws IOException { + String url = baseUrl() + "/springwolf/docs"; + String actual = restTemplate.getForObject(url, String.class); + + InputStream s = this.getClass().getResourceAsStream("/asyncapi.json"); + String expected = new String(s.readAllBytes(), StandardCharsets.UTF_8).replace("\r\n", "\n"); + + assertEquals(expected, actual); + } +} diff --git a/springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/JmsTestContainerExtension.java b/springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/JmsTestContainerExtension.java new file mode 100644 index 000000000..fffba6c81 --- /dev/null +++ b/springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/JmsTestContainerExtension.java @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.example.jms; + +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; + +import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; + +/** + * JUnit5 extension to start the localstack testcontainers once + * and keep it running until all test classes have been completed. + */ +public class JmsTestContainerExtension implements BeforeAllCallback, ExtensionContext.Store.CloseableResource { + + private static volatile boolean started = false; + + static GenericContainer activeMq = + new GenericContainer<>(DockerImageName.parse("apache/activemq-artemis:2.31.2")).withExposedPorts(61616); + + @Override + public void beforeAll(ExtensionContext extensionContext) { + if (!started) { + started = true; + + beforeAllOnce(); + + // Ensure closeableResource {@see #close()} method is called + extensionContext.getRoot().getStore(GLOBAL).put("any unique name", this); + } + } + + private static void beforeAllOnce() { + activeMq.start(); + } + + @Override + public void close() { + activeMq.stop(); + } +} diff --git a/springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/ProducerSystemTest.java b/springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/ProducerSystemTest.java new file mode 100644 index 000000000..42f9dfe92 --- /dev/null +++ b/springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/ProducerSystemTest.java @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.example.jms; + +import io.github.stavshamir.springwolf.example.jms.consumers.ExampleConsumer; +import io.github.stavshamir.springwolf.example.jms.dtos.ExamplePayloadDto; +import io.github.stavshamir.springwolf.producer.SpringwolfJmsProducer; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.annotation.DirtiesContext; +import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.io.File; +import java.util.Map; + +import static io.github.stavshamir.springwolf.example.jms.dtos.ExamplePayloadDto.ExampleEnum.FOO1; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +/** + * While the assertion of this test is identical to ApiIntegrationTests, + * the setup uses a full docker-compose context with a real jms instance. + */ +@SpringBootTest( + classes = {SpringwolfJmsExampleApplication.class}, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Testcontainers +@DirtiesContext +@TestMethodOrder(OrderAnnotation.class) +// @Ignore("Uncomment this line if you have issues running this test on your local machine.") +public class ProducerSystemTest { + + @Autowired + SpringwolfJmsProducer springwolfJmsProducer; + + @SpyBean + ExampleConsumer exampleConsumer; + + @Container + public static DockerComposeContainer environment = new DockerComposeContainer<>(new File("docker-compose.yml")) + .withServices("activemq") + .waitingFor("activemq", Wait.forLogMessage(".*Artemis Console available.*", 1)); + + @Test + @Order(2) + void producerCanUseSpringwolfConfigurationToSendMessage() { + // given + ExamplePayloadDto payload = new ExamplePayloadDto(); + payload.setSomeString("foo"); + payload.setSomeLong(5); + payload.setSomeEnum(FOO1); + + // when + springwolfJmsProducer.send("example-queue", Map.of(), payload); + + // then + verify(exampleConsumer, timeout(10000)).receiveExamplePayload(payload); + } +} diff --git a/springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/SpringwolfJmsExampleApplicationIntegrationTest.java b/springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/SpringwolfJmsExampleApplicationIntegrationTest.java new file mode 100644 index 000000000..5590987fd --- /dev/null +++ b/springwolf-examples/springwolf-jms-example/src/test/java/io/github/stavshamir/springwolf/example/jms/SpringwolfJmsExampleApplicationIntegrationTest.java @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.example.jms; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@SpringBootTest +@ExtendWith({JmsTestContainerExtension.class}) +class SpringwolfJmsExampleApplicationIntegrationTest { + + @Autowired + private ApplicationContext context; + + @Test + void testContext() { + assertNotNull(context); + } +} diff --git a/springwolf-examples/springwolf-jms-example/src/test/resources/asyncapi.json b/springwolf-examples/springwolf-jms-example/src/test/resources/asyncapi.json new file mode 100644 index 000000000..e8469db42 --- /dev/null +++ b/springwolf-examples/springwolf-jms-example/src/test/resources/asyncapi.json @@ -0,0 +1,172 @@ +{ + "asyncapi": "2.6.0", + "info": { + "title": "Springwolf example project - JMS", + "version": "1.0.0", + "description": "Springwolf example project to demonstrate springwolfs abilities", + "contact": { + "name": "springwolf", + "url": "https://github.com/springwolf/springwolf-core", + "email": "example@example.com" + }, + "license": { + "name": "Apache License 2.0" + } + }, + "defaultContentType": "application/json", + "servers": { + "jms": { + "url": "tcp://activemq:61616", + "protocol": "jms" + } + }, + "channels": { + "another-queue": { + "subscribe": { + "operationId": "another-queue_subscribe", + "description": "Custom, optional description defined in the AsyncPublisher annotation", + "bindings": { + "jms": { + "internal-field": "customValue", + "nested": { + "key": "nestedValue" + } + } + }, + "message": { + "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", + "name": "io.github.stavshamir.springwolf.example.jms.dtos.AnotherPayloadDto", + "title": "AnotherPayloadDto", + "description": "Another payload model", + "payload": { + "$ref": "#/components/schemas/io.github.stavshamir.springwolf.example.jms.dtos.AnotherPayloadDto" + }, + "headers": { + "$ref": "#/components/schemas/HeadersNotDocumented" + }, + "bindings": { + "jms": { } + } + } + }, + "publish": { + "operationId": "another-queue_publish_receiveAnotherPayload", + "description": "Auto-generated description", + "bindings": { + "jms": { } + }, + "message": { + "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", + "name": "io.github.stavshamir.springwolf.example.jms.dtos.AnotherPayloadDto", + "title": "AnotherPayloadDto", + "payload": { + "$ref": "#/components/schemas/io.github.stavshamir.springwolf.example.jms.dtos.AnotherPayloadDto" + }, + "headers": { + "$ref": "#/components/schemas/HeadersNotDocumented" + }, + "bindings": { + "jms": { } + } + } + } + }, + "example-queue": { + "publish": { + "operationId": "example-queue_publish_receiveExamplePayload", + "description": "Auto-generated description", + "bindings": { + "jms": { } + }, + "message": { + "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", + "name": "io.github.stavshamir.springwolf.example.jms.dtos.ExamplePayloadDto", + "title": "ExamplePayloadDto", + "payload": { + "$ref": "#/components/schemas/io.github.stavshamir.springwolf.example.jms.dtos.ExamplePayloadDto" + }, + "headers": { + "$ref": "#/components/schemas/HeadersNotDocumented" + }, + "bindings": { + "jms": { } + } + } + }, + "bindings": { + "jms": { } + } + } + }, + "components": { + "schemas": { + "HeadersNotDocumented": { + "type": "object", + "properties": { }, + "example": { } + }, + "io.github.stavshamir.springwolf.example.jms.dtos.AnotherPayloadDto": { + "required": [ + "example" + ], + "type": "object", + "properties": { + "example": { + "$ref": "#/components/schemas/io.github.stavshamir.springwolf.example.jms.dtos.ExamplePayloadDto" + }, + "foo": { + "type": "string", + "description": "Foo field", + "example": "bar" + } + }, + "description": "Another payload model", + "example": { + "example": { + "someEnum": "FOO2", + "someLong": 5, + "someString": "some string value" + }, + "foo": "bar" + } + }, + "io.github.stavshamir.springwolf.example.jms.dtos.ExamplePayloadDto": { + "required": [ + "someEnum", + "someString" + ], + "type": "object", + "properties": { + "someEnum": { + "type": "string", + "description": "Some enum field", + "example": "FOO2", + "enum": [ + "FOO1", + "FOO2", + "FOO3" + ] + }, + "someLong": { + "type": "integer", + "description": "Some long field", + "format": "int64", + "example": 5 + }, + "someString": { + "type": "string", + "description": "Some string field", + "example": "some string value" + } + }, + "description": "Example payload model", + "example": { + "someEnum": "FOO2", + "someLong": 5, + "someString": "some string value" + } + } + } + }, + "tags": [ ] +} \ No newline at end of file diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/AmqpMessageBindingProcessorTest.java b/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/AmqpMessageBindingProcessorTest.java new file mode 100644 index 000000000..d030234a8 --- /dev/null +++ b/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/AmqpMessageBindingProcessorTest.java @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor; + +import com.asyncapi.v2.binding.message.amqp.AMQPMessageBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.ProcessedMessageBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AmqpAsyncOperationBinding; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class AmqpMessageBindingProcessorTest { + private final AmqpMessageBindingProcessor processor = new AmqpMessageBindingProcessor(); + + @Test + void processTest() throws NoSuchMethodException { + Method method = AmqpMessageBindingProcessorTest.class.getMethod("methodWithAnnotation"); + + ProcessedMessageBinding binding = processor.process(method).get(); + + assertThat(binding.getType()).isEqualTo("amqp"); + assertThat(binding.getBinding()).isEqualTo(new AMQPMessageBinding()); + } + + @Test + void processWithoutAnnotationTest() throws NoSuchMethodException { + Method method = AmqpMessageBindingProcessorTest.class.getMethod("methodWithoutAnnotation"); + + Optional binding = processor.process(method); + + assertThat(binding).isNotPresent(); + } + + @AmqpAsyncOperationBinding + public void methodWithAnnotation() {} + + public void methodWithoutAnnotation() {} +} diff --git a/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/AmqpOperationBindingProcessorTest.java b/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/AmqpOperationBindingProcessorTest.java new file mode 100644 index 000000000..2b87e6160 --- /dev/null +++ b/springwolf-plugins/springwolf-amqp-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/AmqpOperationBindingProcessorTest.java @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor; + +import com.asyncapi.v2.binding.operation.amqp.AMQPOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.ProcessedOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.AmqpAsyncOperationBinding; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class AmqpOperationBindingProcessorTest { + private final AmqpOperationBindingProcessor processor = new AmqpOperationBindingProcessor(); + + @Test + void mapToOperationBindingTest() throws NoSuchMethodException { + AmqpAsyncOperationBinding annotation = AmqpOperationBindingProcessorTest.class + .getMethod("methodWithAnnotation") + .getAnnotation(AmqpAsyncOperationBinding.class); + + ProcessedOperationBinding binding = processor.mapToOperationBinding(annotation); + + assertThat(binding.getType()).isEqualTo("amqp"); + assertThat(binding.getBinding()) + .isEqualTo( + new AMQPOperationBinding(0, null, List.of(), 0, 0, false, null, null, false, false, "0.2.0")); + } + + @AmqpAsyncOperationBinding + public void methodWithAnnotation() {} +} diff --git a/springwolf-plugins/springwolf-jms-plugin/README.md b/springwolf-plugins/springwolf-jms-plugin/README.md new file mode 100644 index 000000000..1ba369656 --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/README.md @@ -0,0 +1,42 @@ +# Springwolf JMS Plugin + +##### Automated documentation for Spring Boot application with JMS consumers + +### Table Of Contents + +- [About](#about) +- [Usage](#usage) + - [Dependencies](#dependencies) + - [Configuration class](#configuration-class) +- [Verify](#verify) +- [Example Project](#example-project) + +### About + +This plugin generates an [AsyncAPI document](https://www.asyncapi.com/) from `@JmsListener` methods. + +### Usage + +Add the following dependencies and configuration class to enable this plugin. + +#### Dependencies + +```groovy +dependencies { + // Provides the documentation API + implementation 'io.github.springwolf:springwolf-jms:' + + // Provides the UI - optional (recommended) + runtimeOnly 'io.github.springwolf:springwolf-ui:' +} +``` + +#### Verify + +If you have included the UI dependency, access it with the following url: `localhost:8080/springwolf/asyncapi-ui.html`. +If not, try the following endpoint: `localhost:8080/springwolf/docs`. + +### Example Project + +See [springwolf-jms-example](https://github.com/springwolf/springwolf-core/tree/master/springwolf-examples/springwolf-jms-example) +. diff --git a/springwolf-plugins/springwolf-jms-plugin/build.gradle b/springwolf-plugins/springwolf-jms-plugin/build.gradle new file mode 100644 index 000000000..06343fd23 --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/build.gradle @@ -0,0 +1,71 @@ +plugins { + id 'java-library' + + id 'org.springframework.boot' + id 'io.spring.dependency-management' + id 'ca.cutterslade.analyze' +} + +dependencies { + api project(":springwolf-core") + + implementation "jakarta.jms:jakarta.jms-api" + + implementation "com.asyncapi:asyncapi-core:${asyncapiCoreVersion}" + implementation "org.slf4j:slf4j-api:${slf4jApiVersion}" + + runtimeOnly "org.apache.activemq:activemq-broker" + + implementation "org.springframework:spring-context" + implementation "org.springframework:spring-core" + implementation "org.springframework:spring-jms" + implementation "org.springframework:spring-web" + + implementation "org.springframework.boot:spring-boot" + implementation "org.springframework.boot:spring-boot-autoconfigure" + + compileOnly "com.google.code.findbugs:jsr305:${jsr305Version}" + permitUnusedDeclared "com.google.code.findbugs:jsr305:${jsr305Version}" + + annotationProcessor "org.projectlombok:lombok:${lombokVersion}" + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" + + testRuntimeOnly "org.junit.jupiter:junit-jupiter:${junitJupiterVersion}" + testRuntimeOnly "org.springframework.boot:spring-boot-starter-web" + + testImplementation "org.assertj:assertj-core:${assertjCoreVersion}" + + testImplementation "org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}" + + testImplementation "org.mockito:mockito-core:${mockitoCoreVersion}" + + testImplementation "org.springframework:spring-beans" + testImplementation "org.springframework:spring-test" + + testImplementation "org.springframework.boot:spring-boot-test" + testImplementation "org.springframework.boot:spring-boot-test-autoconfigure" + + testAnnotationProcessor "org.projectlombok:lombok:${lombokVersion}" +} + +jar { + enabled = true + archiveClassifier = '' +} +bootJar.enabled = false + +java { + withJavadocJar() + withSourcesJar() +} + +publishing { + publications { + mavenJava(MavenPublication) { + pom { + name = 'springwolf-jms' + description = 'Automated JSON API documentation for JMS Listeners built with Spring' + } + } + } +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/controller/SpringwolfJmsController.java b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/controller/SpringwolfJmsController.java new file mode 100644 index 000000000..be56a8d2d --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/controller/SpringwolfJmsController.java @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.controller; + +import io.github.stavshamir.springwolf.asyncapi.controller.dtos.MessageDto; +import io.github.stavshamir.springwolf.producer.SpringwolfJmsProducer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/springwolf/jms") +@Slf4j +public class SpringwolfJmsController extends PublishingBaseController { + + private final SpringwolfJmsProducer producer; + + public SpringwolfJmsController(PublishingPayloadCreator publishingPayloadCreator, SpringwolfJmsProducer producer) { + super(publishingPayloadCreator); + this.producer = producer; + } + + @Override + protected boolean isEnabled() { + return producer.isEnabled(); + } + + @Override + protected void publishMessage(String topic, MessageDto message, Object payload) { + log.debug("Publishing to JMS queue {}: {}", topic, message); + producer.send(topic, message.getHeaders(), payload); + } +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/jms/SpringwolfJmsAutoConfiguration.java b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/jms/SpringwolfJmsAutoConfiguration.java new file mode 100644 index 000000000..2c1733b6c --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/jms/SpringwolfJmsAutoConfiguration.java @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.jms; + +import io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigConstants; +import io.github.stavshamir.springwolf.configuration.properties.SpringwolfJmsConfigProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +/** + * Autoconfiguration for the springwolf jms plugin. + */ +@AutoConfiguration +@Import({SpringwolfJmsScannerConfiguration.class, SpringwolfJmsProducerConfiguration.class}) +@ConditionalOnProperty(name = SpringwolfConfigConstants.SPRINGWOLF_ENABLED, havingValue = "true", matchIfMissing = true) +public class SpringwolfJmsAutoConfiguration { + + @Bean + public SpringwolfJmsConfigProperties springwolfJmsConfigProperties() { + return new SpringwolfJmsConfigProperties(); + } +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/jms/SpringwolfJmsProducerConfiguration.java b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/jms/SpringwolfJmsProducerConfiguration.java new file mode 100644 index 000000000..87646bc79 --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/jms/SpringwolfJmsProducerConfiguration.java @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.jms; + +import io.github.stavshamir.springwolf.asyncapi.controller.PublishingPayloadCreator; +import io.github.stavshamir.springwolf.asyncapi.controller.SpringwolfJmsController; +import io.github.stavshamir.springwolf.producer.SpringwolfJmsProducer; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.core.JmsTemplate; + +import java.util.List; + +import static io.github.stavshamir.springwolf.configuration.properties.SpringwolfJmsConfigConstants.SPRINGWOLF_JMS_CONFIG_PREFIX; +import static io.github.stavshamir.springwolf.configuration.properties.SpringwolfJmsConfigConstants.SPRINGWOLF_JMS_PLUGIN_PUBLISHING_ENABLED; + +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty( + prefix = SPRINGWOLF_JMS_CONFIG_PREFIX, + name = SPRINGWOLF_JMS_PLUGIN_PUBLISHING_ENABLED, + havingValue = "true") +public class SpringwolfJmsProducerConfiguration { + + @Bean + @ConditionalOnMissingBean + public SpringwolfJmsProducer springwolfJmsProducer(List jmsTemplates) { + return new SpringwolfJmsProducer(jmsTemplates); + } + + @Bean + @ConditionalOnMissingBean + public SpringwolfJmsController springwolfJmsController( + PublishingPayloadCreator publishingPayloadCreator, SpringwolfJmsProducer springwolfJmsProducer) { + return new SpringwolfJmsController(publishingPayloadCreator, springwolfJmsProducer); + } +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/jms/SpringwolfJmsScannerConfiguration.java b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/jms/SpringwolfJmsScannerConfiguration.java new file mode 100644 index 000000000..1767b15c8 --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/jms/SpringwolfJmsScannerConfiguration.java @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.jms; + +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.BindingProcessorPriority; +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor.JmsMessageBindingProcessor; +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor.JmsOperationBindingProcessor; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelPriority; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.MethodLevelJmsListenerScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; +import io.github.stavshamir.springwolf.schemas.SchemasService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +import static io.github.stavshamir.springwolf.configuration.properties.SpringwolfJmsConfigConstants.SPRINGWOLF_SCANNER_JMS_LISTENER_ENABLED; + +/** + * spring configuration defining the scanner beans for the jms plugin + */ +@Configuration(proxyBeanMethods = false) +public class SpringwolfJmsScannerConfiguration { + + @Bean + @Order(value = ChannelPriority.AUTO_DISCOVERED) + @ConditionalOnProperty(name = SPRINGWOLF_SCANNER_JMS_LISTENER_ENABLED, havingValue = "true", matchIfMissing = true) + public MethodLevelJmsListenerScanner methodLevelJmsListenerScanner( + ComponentClassScanner componentClassScanner, SchemasService schemasService) { + return new MethodLevelJmsListenerScanner(componentClassScanner, schemasService); + } + + @Bean + @Order(value = BindingProcessorPriority.PROTOCOL_BINDING) + @ConditionalOnMissingBean + public JmsMessageBindingProcessor jmsMessageBindingProcessor() { + return new JmsMessageBindingProcessor(); + } + + @Bean + @Order(value = BindingProcessorPriority.PROTOCOL_BINDING) + @ConditionalOnMissingBean + public JmsOperationBindingProcessor jmsOperationBindingProcessor() { + return new JmsOperationBindingProcessor(); + } +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/JmsMessageBindingProcessor.java b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/JmsMessageBindingProcessor.java new file mode 100644 index 000000000..7d8d4db85 --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/JmsMessageBindingProcessor.java @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor; + +import com.asyncapi.v2.binding.message.jms.JMSMessageBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.MessageBindingProcessor; +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.ProcessedMessageBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.JmsAsyncOperationBinding; +import org.springframework.context.EmbeddedValueResolverAware; +import org.springframework.util.StringValueResolver; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Optional; + +public class JmsMessageBindingProcessor implements MessageBindingProcessor, EmbeddedValueResolverAware { + private StringValueResolver resolver; + + @Override + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.resolver = resolver; + } + + @Override + public Optional process(Method method) { + return Arrays.stream(method.getAnnotations()) + .filter(annotation -> annotation instanceof JmsAsyncOperationBinding) + .map(annotation -> (JmsAsyncOperationBinding) annotation) + .findAny() + .map(this::mapToMessageBinding); + } + + private ProcessedMessageBinding mapToMessageBinding(JmsAsyncOperationBinding bindingAnnotation) { + JMSMessageBinding jmsMessageBinding = new JMSMessageBinding(); + + return new ProcessedMessageBinding(bindingAnnotation.type(), jmsMessageBinding); + } +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/JmsOperationBindingProcessor.java b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/JmsOperationBindingProcessor.java new file mode 100644 index 000000000..9430829a5 --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/JmsOperationBindingProcessor.java @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor; + +import com.asyncapi.v2.binding.operation.jms.JMSOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.ProcessedOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.JmsAsyncOperationBinding; + +public class JmsOperationBindingProcessor extends AbstractOperationBindingProcessor { + + @Override + protected ProcessedOperationBinding mapToOperationBinding(JmsAsyncOperationBinding bindingAnnotation) { + return new ProcessedOperationBinding(bindingAnnotation.type(), new JMSOperationBinding()); + } +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/JmsListenerUtil.java b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/JmsListenerUtil.java new file mode 100644 index 000000000..057296e97 --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/JmsListenerUtil.java @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation; + +import com.asyncapi.v2.binding.channel.ChannelBinding; +import com.asyncapi.v2.binding.channel.jms.JMSChannelBinding; +import com.asyncapi.v2.binding.message.MessageBinding; +import com.asyncapi.v2.binding.message.jms.JMSMessageBinding; +import com.asyncapi.v2.binding.operation.OperationBinding; +import com.asyncapi.v2.binding.operation.jms.JMSOperationBinding; +import lombok.extern.slf4j.Slf4j; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.util.StringValueResolver; + +import java.util.Map; + +@Slf4j +public class JmsListenerUtil { + + public static String getChannelName(JmsListener annotation, StringValueResolver resolver) { + return resolver.resolveStringValue(annotation.destination()); + } + + public static Map buildChannelBinding( + JmsListener annotation, StringValueResolver resolver) { + return Map.of("jms", new JMSChannelBinding()); + } + + public static Map buildOperationBinding( + JmsListener annotation, StringValueResolver resolver) { + return Map.of("jms", new JMSOperationBinding()); + } + + public static Map buildMessageBinding() { + return Map.of("jms", new JMSMessageBinding()); + } +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelJmsListenerScanner.java b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelJmsListenerScanner.java new file mode 100644 index 000000000..1df17d1bb --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelJmsListenerScanner.java @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation; + +import com.asyncapi.v2.binding.channel.ChannelBinding; +import com.asyncapi.v2.binding.message.MessageBinding; +import com.asyncapi.v2.binding.operation.OperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner; +import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; +import io.github.stavshamir.springwolf.schemas.SchemasService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.EmbeddedValueResolverAware; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.util.StringValueResolver; + +import java.lang.reflect.Method; +import java.util.Map; + +@Slf4j +public class MethodLevelJmsListenerScanner extends AbstractMethodLevelListenerScanner + implements ChannelsScanner, EmbeddedValueResolverAware { + + private StringValueResolver resolver; + + public MethodLevelJmsListenerScanner(ComponentClassScanner componentClassScanner, SchemasService schemasService) { + super(componentClassScanner, schemasService); + } + + @Override + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.resolver = resolver; + } + + @Override + protected Class getListenerAnnotationClass() { + return JmsListener.class; + } + + @Override + protected String getChannelName(JmsListener annotation) { + return JmsListenerUtil.getChannelName(annotation, resolver); + } + + @Override + protected Map buildChannelBinding(JmsListener annotation) { + return JmsListenerUtil.buildChannelBinding(annotation, resolver); + } + + @Override + protected Map buildOperationBinding(JmsListener annotation) { + return JmsListenerUtil.buildOperationBinding(annotation, resolver); + } + + @Override + protected Map buildMessageBinding(JmsListener annotation) { + return JmsListenerUtil.buildMessageBinding(); + } + + protected Class getPayloadType(Method method) { + return SpringPayloadAnnotationTypeExtractor.getPayloadType(method); + } +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/JmsAsyncOperationBinding.java b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/JmsAsyncOperationBinding.java new file mode 100644 index 000000000..27190c4f4 --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/annotation/JmsAsyncOperationBinding.java @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation; + +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.AsyncOperationBinding; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * {@code @JmsAsyncOperationBinding} is a method-level annotation used in combination with {@link AsyncPublisher} or {@link AsyncListener}. + * It configures the operation binding for the JMS protocol. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = {ElementType.METHOD}) +@AsyncOperationBinding +public @interface JmsAsyncOperationBinding { + + String type() default "jms"; +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/configuration/properties/SpringwolfJmsConfigConstants.java b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/configuration/properties/SpringwolfJmsConfigConstants.java new file mode 100644 index 000000000..1dae7aa5c --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/configuration/properties/SpringwolfJmsConfigConstants.java @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.configuration.properties; + +import static io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigConstants.ENABLED; +import static io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigConstants.SCANNER; +import static io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigConstants.SPRINGWOLF_PLUGIN_CONFIG_PREFIX; + +public class SpringwolfJmsConfigConstants { + + public static final String SPRINGWOLF_JMS_CONFIG_PREFIX = SPRINGWOLF_PLUGIN_CONFIG_PREFIX + ".jms"; + + public static final String SPRINGWOLF_JMS_PLUGIN_PUBLISHING_ENABLED = "publishing.enabled"; + + public static final String SPRINGWOLF_SCANNER_JMS_LISTENER_ENABLED = + SPRINGWOLF_JMS_CONFIG_PREFIX + SCANNER + ".jms-listener" + ENABLED; +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/configuration/properties/SpringwolfJmsConfigProperties.java b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/configuration/properties/SpringwolfJmsConfigProperties.java new file mode 100644 index 000000000..861d4bd2a --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/configuration/properties/SpringwolfJmsConfigProperties.java @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.configuration.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.lang.Nullable; + +/** + * This class is used to create metadata for auto-completion in spring configuration properties/yaml by using + * the spring-boot-configuration-processor. + */ +@ConfigurationProperties(prefix = SpringwolfJmsConfigConstants.SPRINGWOLF_JMS_CONFIG_PREFIX) +@Getter +@Setter +public class SpringwolfJmsConfigProperties { + + @Nullable + private Publishing publishing; + + @Nullable + private Scanner scanner; + + @Getter + @Setter + public static class Publishing { + + /** + * Enables/Disables the possibility to publish messages through springwolf on the configured jms instance. + */ + private boolean enabled = false; + } + + @Getter + @Setter + public static class Scanner { + + private static JmsListener jmsListener; + + @Getter + @Setter + public static class JmsListener { + + /** + * This mirrors the ConfigConstant {@see SpringwolfJmsConfigConstants#SPRINGWOLF_SCANNER_JMS_LISTENER_ENABLED} + */ + private boolean enabled = true; + } + } +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/producer/SpringwolfJmsProducer.java b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/producer/SpringwolfJmsProducer.java new file mode 100644 index 000000000..41acc5c92 --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/main/java/io/github/stavshamir/springwolf/producer/SpringwolfJmsProducer.java @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.producer; + +import jakarta.jms.JMSException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.jms.core.JmsTemplate; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@Slf4j +public class SpringwolfJmsProducer { + + private final Optional template; + + public SpringwolfJmsProducer(List templates) { + this.template = templates.isEmpty() ? Optional.empty() : Optional.of(templates.get(0)); + } + + public boolean isEnabled() { + return template.isPresent(); + } + + public void send(String channelName, Map headers, Object payload) { + if (template.isPresent()) { + template.get().convertAndSend(channelName, payload, message -> { + if (headers != null) { + headers.forEach((name, value) -> { + try { + message.setStringProperty(name, value); + } catch (JMSException ex) { + log.warn("Unable to set JMS Header key=%s value=%s".formatted(name, value), ex); + } + }); + } + return message; + }); + + } else { + log.warn("JMS producer is not configured"); + } + } +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/springwolf-plugins/springwolf-jms-plugin/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..c380b3330 --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +io.github.stavshamir.springwolf.asyncapi.jms.SpringwolfJmsAutoConfiguration diff --git a/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/controller/SpringwolfJmsControllerIntegrationTest.java b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/controller/SpringwolfJmsControllerIntegrationTest.java new file mode 100644 index 000000000..f72a1208a --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/controller/SpringwolfJmsControllerIntegrationTest.java @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.controller; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigProperties; +import io.github.stavshamir.springwolf.producer.SpringwolfJmsProducer; +import io.github.stavshamir.springwolf.schemas.DefaultSchemasService; +import io.github.stavshamir.springwolf.schemas.SchemasService; +import io.github.stavshamir.springwolf.schemas.example.ExampleJsonGenerator; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.extern.jackson.Jacksonized; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Map; + +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(SpringwolfJmsController.class) +@ContextConfiguration( + classes = { + SpringwolfJmsController.class, + PublishingPayloadCreator.class, + SpringwolfJmsProducer.class, + DefaultSchemasService.class, + ExampleJsonGenerator.class, + SpringwolfConfigProperties.class, + }) +@TestPropertySource( + properties = { + "springwolf.docket.base-package=io.github.stavshamir.springwolf.asyncapi", + "springwolf.docket.info.title=Title", + "springwolf.docket.info.version=1.0", + "springwolf.docket.servers.jms.protocol=jms", + "springwolf.docket.servers.jms.url=127.0.0.1", + "springwolf.plugin.jms.publishing.enabled=true", + "springwolf.use-fqn=true" + }) +class SpringwolfJmsControllerIntegrationTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private SchemasService schemasService; + + @MockBean + private SpringwolfJmsProducer springwolfJmsProducer; + + @Captor + private ArgumentCaptor payloadCaptor; + + @Captor + private ArgumentCaptor> headerCaptor; + + @BeforeEach + void setup() { + when(springwolfJmsProducer.isEnabled()).thenReturn(true); + + schemasService.register(PayloadDto.class); + } + + @Test + void testControllerShouldReturnBadRequestIfPayloadIsEmpty() { + try { + String content = + """ + { + "bindings": null, + "headers": null, + "payload": "" + }"""; + mvc.perform(post("/springwolf/jms/publish?topic=test-topic") + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isBadRequest()); + } catch (Exception e) { + verifyNoInteractions(springwolfJmsProducer); + } + } + + @Test + void testControllerShouldReturnBadRequestIfPayloadIsNotSet() { + try { + String content = + """ + { + "bindings": null, + "headers": null + }"""; + mvc.perform(post("/springwolf/jms/publish?topic=test-topic") + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isBadRequest()); + } catch (Exception e) { + verifyNoInteractions(springwolfJmsProducer); + } + } + + @Test + void testControllerShouldReturnNotFoundIfNoJmsProducerIsEnabled() throws Exception { + when(springwolfJmsProducer.isEnabled()).thenReturn(false); + + String content = + """ + { + "bindings": null, + "headers": null, + "payload": "{ \\"some-payload-key\\" : \\"some-payload-value\\" }" + }"""; + mvc.perform(post("/springwolf/jms/publish?topic=test-topic") + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isNotFound()); + } + + @Test + void testControllerShouldCallJmsProducerIfOnlyPayloadIsSend() throws Exception { + when(springwolfJmsProducer.isEnabled()).thenReturn(true); + + String content = + """ + { + "bindings": null, + "headers": null, + "payload": "{ \\"some-payload-key\\" : \\"some-payload-value\\" }", + "payloadType": "io.github.stavshamir.springwolf.asyncapi.controller.SpringwolfJmsControllerIntegrationTest$PayloadDto" + }"""; + + mvc.perform(post("/springwolf/jms/publish") + .param("topic", "test-topic") + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isOk()); + + verify(springwolfJmsProducer).send(eq("test-topic"), isNull(), payloadCaptor.capture()); + + assertThat(payloadCaptor.getValue()).isEqualTo(new PayloadDto("some-payload-value")); + } + + @Test + void testControllerShouldCallJmsProducerIfPayloadAndHeadersAreSend() throws Exception { + when(springwolfJmsProducer.isEnabled()).thenReturn(true); + + String content = + """ + { + "bindings": null, + "headers": { + "some-header-key": "some-header-value" + }, + "payload": "{ \\"some-payload-key\\" : \\"some-payload-value\\" }", + "payloadType": "io.github.stavshamir.springwolf.asyncapi.controller.SpringwolfJmsControllerIntegrationTest$PayloadDto" + } + """; + + mvc.perform(post("/springwolf/jms/publish?topic=test-topic") + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isOk()); + + verify(springwolfJmsProducer).send(eq("test-topic"), headerCaptor.capture(), payloadCaptor.capture()); + + assertThat(headerCaptor.getValue()).isEqualTo(singletonMap("some-header-key", "some-header-value")); + assertThat(payloadCaptor.getValue()).isEqualTo(new PayloadDto("some-payload-value")); + } + + @Data + @AllArgsConstructor + @Jacksonized + @Builder + public static class PayloadDto { + @JsonProperty("some-payload-key") + private String field; + } +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/JmsMessageBindingProcessorTest.java b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/JmsMessageBindingProcessorTest.java new file mode 100644 index 000000000..53765e63a --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/JmsMessageBindingProcessorTest.java @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor; + +import com.asyncapi.v2.binding.message.jms.JMSMessageBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.ProcessedMessageBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.JmsAsyncOperationBinding; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class JmsMessageBindingProcessorTest { + private final JmsMessageBindingProcessor processor = new JmsMessageBindingProcessor(); + + @Test + void processTest() throws NoSuchMethodException { + Method method = JmsMessageBindingProcessorTest.class.getMethod("methodWithAnnotation"); + + ProcessedMessageBinding binding = processor.process(method).get(); + + assertThat(binding.getType()).isEqualTo("jms"); + assertThat(binding.getBinding()).isEqualTo(new JMSMessageBinding()); + } + + @Test + void processWithoutAnnotationTest() throws NoSuchMethodException { + Method method = JmsMessageBindingProcessorTest.class.getMethod("methodWithoutAnnotation"); + + Optional binding = processor.process(method); + + assertThat(binding).isNotPresent(); + } + + @JmsAsyncOperationBinding + public void methodWithAnnotation() {} + + public void methodWithoutAnnotation() {} +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/JmsOperationBindingProcessorTest.java b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/JmsOperationBindingProcessorTest.java new file mode 100644 index 000000000..99879973b --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/JmsOperationBindingProcessorTest.java @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor; + +import com.asyncapi.v2.binding.operation.jms.JMSOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.ProcessedOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.JmsAsyncOperationBinding; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class JmsOperationBindingProcessorTest { + private final JmsOperationBindingProcessor processor = new JmsOperationBindingProcessor(); + + @Test + void mapToOperationBindingTest() throws NoSuchMethodException { + JmsAsyncOperationBinding annotation = JmsOperationBindingProcessorTest.class + .getMethod("methodWithAnnotation") + .getAnnotation(JmsAsyncOperationBinding.class); + + ProcessedOperationBinding binding = processor.mapToOperationBinding(annotation); + + assertThat(binding.getType()).isEqualTo("jms"); + assertThat(binding.getBinding()).isEqualTo(new JMSOperationBinding()); + } + + @JmsAsyncOperationBinding + public void methodWithAnnotation() {} +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/JmsListenerUtilTest.java b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/JmsListenerUtilTest.java new file mode 100644 index 000000000..783f87ce8 --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/JmsListenerUtilTest.java @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation; + +import com.asyncapi.v2.binding.channel.ChannelBinding; +import com.asyncapi.v2.binding.channel.jms.JMSChannelBinding; +import com.asyncapi.v2.binding.message.MessageBinding; +import com.asyncapi.v2.binding.message.jms.JMSMessageBinding; +import com.asyncapi.v2.binding.operation.OperationBinding; +import com.asyncapi.v2.binding.operation.jms.JMSOperationBinding; +import org.assertj.core.util.Sets; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.util.StringValueResolver; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class JmsListenerUtilTest { + private static final String QUEUE = "test-queue"; + + @Nested + class FullConfiguration { + @Test + void getChannelName() { + // given + JmsListener annotation = getAnnotation(ClassWithFullJmsListenerConfiguration.class); + StringValueResolver resolver = mock(StringValueResolver.class); + when(resolver.resolveStringValue("${queue-1}")).thenReturn("queue-1"); + + // when + String channelName = JmsListenerUtil.getChannelName(annotation, resolver); + + // then + assertEquals("queue-1", channelName); + } + + @Test + void buildChannelBinding() { + // given + JmsListener annotation = getAnnotation(ClassWithFullJmsListenerConfiguration.class); + StringValueResolver resolver = mock(StringValueResolver.class); + + // when + Map channelBinding = + JmsListenerUtil.buildChannelBinding(annotation, resolver); + + // then + assertEquals(1, channelBinding.size()); + assertEquals(Sets.newTreeSet("jms"), channelBinding.keySet()); + assertEquals(new JMSChannelBinding(), channelBinding.get("jms")); + } + + @Test + void buildOperationBinding() { + // given + JmsListener annotation = getAnnotation(ClassWithFullJmsListenerConfiguration.class); + StringValueResolver resolver = mock(StringValueResolver.class); + + // when + Map operationBinding = + JmsListenerUtil.buildOperationBinding(annotation, resolver); + + // then + assertEquals(1, operationBinding.size()); + assertEquals(Sets.newTreeSet("jms"), operationBinding.keySet()); + assertEquals(new JMSOperationBinding(), operationBinding.get("jms")); + } + + @Test + void buildMessageBinding() { + // when + Map messageBinding = JmsListenerUtil.buildMessageBinding(); + + // then + assertEquals(1, messageBinding.size()); + assertEquals(Sets.newTreeSet("jms"), messageBinding.keySet()); + assertEquals(new JMSMessageBinding(), messageBinding.get("jms")); + } + + private static class ClassWithFullJmsListenerConfiguration { + + @JmsListener(destination = "${queue-1}") + private void methodWithAnnotation(String payload) {} + } + } + + private static JmsListener getAnnotation(Class clazz) { + return clazz.getDeclaredMethods()[0].getAnnotation(JmsListener.class); + } +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelJmsListenerScannerIntegrationTest.java b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelJmsListenerScannerIntegrationTest.java new file mode 100644 index 000000000..e86dcdefe --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/annotation/MethodLevelJmsListenerScannerIntegrationTest.java @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation; + +import com.asyncapi.v2._6_0.model.channel.ChannelItem; +import com.asyncapi.v2._6_0.model.channel.operation.Operation; +import com.asyncapi.v2.binding.channel.jms.JMSChannelBinding; +import com.asyncapi.v2.binding.message.MessageBinding; +import com.asyncapi.v2.binding.message.jms.JMSMessageBinding; +import com.asyncapi.v2.binding.operation.jms.JMSOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; +import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message; +import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.PayloadReference; +import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.AsyncHeaders; +import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.header.HeaderReference; +import io.github.stavshamir.springwolf.configuration.AsyncApiDocket; +import io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigProperties; +import io.github.stavshamir.springwolf.schemas.DefaultSchemasService; +import io.github.stavshamir.springwolf.schemas.example.ExampleJsonGenerator; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.Map; +import java.util.Set; + +import static java.util.Collections.singleton; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration( + classes = { + MethodLevelJmsListenerScanner.class, + DefaultSchemasService.class, + ExampleJsonGenerator.class, + SpringwolfConfigProperties.class, + }) +@TestPropertySource(properties = "jms.value.test=test-queue") +class MethodLevelJmsListenerScannerIntegrationTest { + + @Autowired + private MethodLevelJmsListenerScanner scanner; + + @MockBean + private ComponentClassScanner componentsScanner; + + @MockBean + private AsyncApiDocket asyncApiDocket; + + private static final String QUEUE = "test-queue"; + private static final Map defaultOperationBinding = Map.of("jms", new JMSOperationBinding()); + private static final Map defaultMessageBinding = + Map.of("jms", new JMSMessageBinding()); + private static final Map defaultChannelBinding = Map.of("jms", new JMSChannelBinding()); + + private void setClassToScan(Class classToScan) { + Set> classesToScan = singleton(classToScan); + when(componentsScanner.scan()).thenReturn(classesToScan); + } + + @Test + void scan_componentHasNoJmsListenerMethods() { + setClassToScan(ClassWithoutJmsListenerAnnotations.class); + + Map channels = scanner.scan(); + + assertThat(channels).isEmpty(); + } + + @Test + void scan_componentHasJmsListenerMethods_hardCodedTopic() { + // Given a class with methods annotated with JmsListener, whose value attribute is hard coded + setClassToScan(ClassWithJmsListenerAnnotationHardCodedTopic.class); + + // When scan is called + Map actualChannelItems = scanner.scan(); + + // Then the returned collection contains the channel + Message message = Message.builder() + .name(SimpleFoo.class.getName()) + .title(SimpleFoo.class.getSimpleName()) + .payload(PayloadReference.fromModelName(SimpleFoo.class.getSimpleName())) + .headers(HeaderReference.fromModelName(AsyncHeaders.NOT_DOCUMENTED.getSchemaName())) + .bindings(defaultMessageBinding) + .build(); + + Operation operation = Operation.builder() + .description("Auto-generated description") + .operationId("test-queue_publish_methodWithAnnotation") + .bindings(defaultOperationBinding) + .message(message) + .build(); + + ChannelItem expectedChannelItem = ChannelItem.builder() + .bindings(defaultChannelBinding) + .publish(operation) + .build(); + + assertThat(actualChannelItems).containsExactly(Map.entry(QUEUE, expectedChannelItem)); + } + + @Test + void scan_componentHasJmsListenerMethods_embeddedValueTopic() { + // Given a class with methods annotated with JmsListener, whose value attribute is an embedded value + setClassToScan(ClassWithJmsListenerAnnotationsEmbeddedValueTopic.class); + + // When scan is called + Map actualChannelItems = scanner.scan(); + + // Then the returned collection contains the channel + Message message = Message.builder() + .name(SimpleFoo.class.getName()) + .title(SimpleFoo.class.getSimpleName()) + .payload(PayloadReference.fromModelName(SimpleFoo.class.getSimpleName())) + .headers(HeaderReference.fromModelName(AsyncHeaders.NOT_DOCUMENTED.getSchemaName())) + .bindings(defaultMessageBinding) + .build(); + + Operation operation = Operation.builder() + .description("Auto-generated description") + .operationId("test-queue_publish_methodWithAnnotation1") + .bindings(defaultOperationBinding) + .message(message) + .build(); + + ChannelItem expectedChannelItem = ChannelItem.builder() + .bindings(defaultChannelBinding) + .publish(operation) + .build(); + + assertThat(actualChannelItems).containsExactly(Map.entry(QUEUE, expectedChannelItem)); + } + + @Test + void scan_componentHasJmsListenerMethods_multipleParamsWithoutPayloadAnnotation() { + // Given a class with a method annotated with JmsListener: + // - The method has more than one parameter + // - No parameter is annotated with @Payload + setClassToScan(ClassWithJmsListenerAnnotationMultipleParamsWithoutPayloadAnnotation.class); + + // Then an exception is thrown when scan is called + assertThatThrownBy(() -> scanner.scan()).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void scan_componentHasJmsListenerMethods_multipleParamsWithPayloadAnnotation() { + // Given a class with a method annotated with JmsListener: + // - The method has more than one parameter + // - There is a parameter is annotated with @Payload + setClassToScan(ClassWithJmsListenerAnnotationMultipleParamsWithPayloadAnnotation.class); + + // When scan is called + Map actualChannelItems = scanner.scan(); + + // Then the returned collection contains the channel, and the payload is of the parameter annotated with + // @Payload + + Message message = Message.builder() + .name(SimpleFoo.class.getName()) + .title(SimpleFoo.class.getSimpleName()) + .payload(PayloadReference.fromModelName(SimpleFoo.class.getSimpleName())) + .headers(HeaderReference.fromModelName(AsyncHeaders.NOT_DOCUMENTED.getSchemaName())) + .bindings(defaultMessageBinding) + .build(); + + Operation operation = Operation.builder() + .description("Auto-generated description") + .operationId("test-queue_publish_methodWithAnnotation") + .bindings(defaultOperationBinding) + .message(message) + .build(); + + ChannelItem expectedChannelItem = ChannelItem.builder() + .bindings(defaultChannelBinding) + .publish(operation) + .build(); + + assertThat(actualChannelItems).containsExactly(Map.entry(QUEUE, expectedChannelItem)); + } + + private static class ClassWithoutJmsListenerAnnotations { + + private void methodWithoutAnnotation() {} + } + + private static class ClassWithJmsListenerAnnotationHardCodedTopic { + + @JmsListener(destination = QUEUE) + private void methodWithAnnotation(SimpleFoo payload) {} + + private void methodWithoutAnnotation() {} + } + + private static class ClassWithJmsListenerAnnotationsEmbeddedValueTopic { + + @JmsListener(destination = "${jms.value.test}") + private void methodWithAnnotation1(SimpleFoo payload) {} + } + + private static class ClassWithJmsListenerAnnotationMultipleParamsWithoutPayloadAnnotation { + + @JmsListener(destination = QUEUE) + private void methodWithAnnotation(SimpleFoo payload, String anotherParam) {} + } + + private static class ClassWithJmsListenerAnnotationMultipleParamsWithPayloadAnnotation { + + @JmsListener(destination = QUEUE) + private void methodWithAnnotation(String anotherParam, @Payload SimpleFoo payload) {} + } + + @Data + @NoArgsConstructor + private static class SimpleFoo { + private String s; + private boolean b; + } +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/ObjectMapperTestConfiguration.java b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/ObjectMapperTestConfiguration.java new file mode 100644 index 000000000..6e87c75f9 --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/ObjectMapperTestConfiguration.java @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class ObjectMapperTestConfiguration { + + @ConditionalOnMissingBean + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfJmsProducerConfigurationIntegrationTest.java b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfJmsProducerConfigurationIntegrationTest.java new file mode 100644 index 000000000..13ea12c4e --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/configuration/SpringwolfJmsProducerConfigurationIntegrationTest.java @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.configuration; + +import io.github.stavshamir.springwolf.asyncapi.AsyncApiService; +import io.github.stavshamir.springwolf.asyncapi.ChannelsService; +import io.github.stavshamir.springwolf.asyncapi.controller.PublishingPayloadCreator; +import io.github.stavshamir.springwolf.asyncapi.controller.SpringwolfJmsController; +import io.github.stavshamir.springwolf.asyncapi.jms.SpringwolfJmsAutoConfiguration; +import io.github.stavshamir.springwolf.asyncapi.scanners.classes.ComponentClassScanner; +import io.github.stavshamir.springwolf.producer.SpringwolfJmsProducer; +import io.github.stavshamir.springwolf.schemas.SchemasService; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.MockBeans; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SpringwolfJmsProducerConfigurationIntegrationTest { + + @ExtendWith(SpringExtension.class) + @ContextConfiguration( + classes = { + SpringwolfJmsAutoConfiguration.class, + PublishingPayloadCreator.class, + ObjectMapperTestConfiguration.class + }) + @TestPropertySource( + properties = { + "springwolf.enabled=true", + "springwolf.docket.info.title=Info title was loaded from spring properties", + "springwolf.docket.info.version=1.0.0", + "springwolf.docket.base-package=io.github.stavshamir.springwolf.example", + "springwolf.docket.servers.test-protocol.protocol=test", + "springwolf.docket.servers.test-protocol.url=some-server:1234", + "springwolf.plugin.jms.publishing.enabled=true" + }) + @MockBeans( + value = { + @MockBean(ComponentClassScanner.class), + @MockBean(SchemasService.class), + @MockBean(AsyncApiDocketService.class), + @MockBean(AsyncApiService.class), + @MockBean(JmsTemplate.class) + }) + @Nested + class JmsProducerWillBeCreatedIfEnabledTest { + @Autowired + private Optional springwolfJmsProducer; + + @Autowired + private Optional springwolfJmsController; + + @Test + void springwolfJmsProducerShouldBePresentInSpringContext() { + assertThat(springwolfJmsProducer).isPresent(); + assertThat(springwolfJmsController).isPresent(); + } + } + + @ExtendWith(SpringExtension.class) + @ContextConfiguration( + classes = { + SpringwolfJmsAutoConfiguration.class, + PublishingPayloadCreator.class, + ObjectMapperTestConfiguration.class + }) + @TestPropertySource( + properties = { + "springwolf.enabled=true", + "springwolf.docket.info.title=Info title was loaded from spring properties", + "springwolf.docket.info.version=1.0.0", + "springwolf.docket.base-package=io.github.stavshamir.springwolf.example", + "springwolf.docket.servers.test-protocol.protocol=test", + "springwolf.docket.servers.test-protocol.url=some-server:1234", + "springwolf.plugin.jms.publishing.enabled=false" + }) + @MockBeans( + value = { + @MockBean(ComponentClassScanner.class), + @MockBean(SchemasService.class), + @MockBean(ChannelsService.class), + @MockBean(JmsTemplate.class) + }) + @Nested + class JmsProducerWillNotBeCreatedIfDisabledTest { + @Autowired + private Optional springwolfJmsProducer; + + @Autowired + private Optional springwolfJmsController; + + @Test + void springwolfJmsProducerShouldNotBePresentInSpringContext() { + assertThat(springwolfJmsProducer).isNotPresent(); + assertThat(springwolfJmsController).isNotPresent(); + } + } +} diff --git a/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/producer/SpringwolfJmsProducerTest.java b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/producer/SpringwolfJmsProducerTest.java new file mode 100644 index 000000000..493323d65 --- /dev/null +++ b/springwolf-plugins/springwolf-jms-plugin/src/test/java/io/github/stavshamir/springwolf/producer/SpringwolfJmsProducerTest.java @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.producer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.jms.core.MessagePostProcessor; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +class SpringwolfJmsProducerTest { + private SpringwolfJmsProducer springwolfJmsProducer; + + private JmsTemplate template; + + @BeforeEach + void setUp() { + template = mock(JmsTemplate.class); + + springwolfJmsProducer = new SpringwolfJmsProducer(Collections.singletonList(template)); + } + + @Test + void send_defaultExchangeAndChannelNameAsRoutingKey() { + Map payload = new HashMap<>(); + springwolfJmsProducer.send("channel-name", Map.of(), payload); + + verify(template).convertAndSend(eq("channel-name"), same(payload), any(MessagePostProcessor.class)); + } +} diff --git a/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/SpringwolfKafkaControllerIntegrationTest.java b/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/controller/SpringwolfKafkaControllerIntegrationTest.java similarity index 95% rename from springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/SpringwolfKafkaControllerIntegrationTest.java rename to springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/controller/SpringwolfKafkaControllerIntegrationTest.java index c21398d0b..707bc049a 100644 --- a/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/SpringwolfKafkaControllerIntegrationTest.java +++ b/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/controller/SpringwolfKafkaControllerIntegrationTest.java @@ -1,9 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 -package io.github.stavshamir.springwolf.asyncapi; +package io.github.stavshamir.springwolf.asyncapi.controller; import com.fasterxml.jackson.annotation.JsonProperty; -import io.github.stavshamir.springwolf.asyncapi.controller.PublishingPayloadCreator; -import io.github.stavshamir.springwolf.asyncapi.controller.SpringwolfKafkaController; import io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigProperties; import io.github.stavshamir.springwolf.producer.SpringwolfKafkaProducer; import io.github.stavshamir.springwolf.schemas.DefaultSchemasService; @@ -145,7 +143,7 @@ void testControllerShouldCallKafkaProducerIfOnlyPayloadIsSend() throws Exception "bindings": null, "headers": null, "payload": "{ \\"some-payload-key\\" : \\"some-payload-value\\" }", - "payloadType": "io.github.stavshamir.springwolf.asyncapi.SpringwolfKafkaControllerIntegrationTest$PayloadDto" + "payloadType": "io.github.stavshamir.springwolf.asyncapi.controller.SpringwolfKafkaControllerIntegrationTest$PayloadDto" }"""; mvc.perform(post("/springwolf/kafka/publish") @@ -171,7 +169,7 @@ void testControllerShouldCallKafkaProducerIfPayloadAndHeadersAreSend() throws Ex "some-header-key": "some-header-value" }, "payload": "{ \\"some-payload-key\\" : \\"some-payload-value\\" }", - "payloadType": "io.github.stavshamir.springwolf.asyncapi.SpringwolfKafkaControllerIntegrationTest$PayloadDto" + "payloadType": "io.github.stavshamir.springwolf.asyncapi.controller.SpringwolfKafkaControllerIntegrationTest$PayloadDto" } """; @@ -201,7 +199,7 @@ void testControllerShouldCallKafkaProducerIfPayloadAndHeadersAndBindingsAreSend( "some-header-key": "some-header-value" }, "payload": "{ \\"some-payload-key\\" : \\"some-payload-value\\" }", - "payloadType": "io.github.stavshamir.springwolf.asyncapi.SpringwolfKafkaControllerIntegrationTest$PayloadDto" + "payloadType": "io.github.stavshamir.springwolf.asyncapi.controller.SpringwolfKafkaControllerIntegrationTest$PayloadDto" }"""; mvc.perform(post("/springwolf/kafka/publish?topic=test-topic") diff --git a/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/KafkaMessageBindingProcessorTest.java b/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/KafkaMessageBindingProcessorTest.java new file mode 100644 index 000000000..10de169ef --- /dev/null +++ b/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/KafkaMessageBindingProcessorTest.java @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor; + +import com.asyncapi.v2.binding.message.kafka.KafkaMessageBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.ProcessedMessageBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.KafkaAsyncOperationBinding; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class KafkaMessageBindingProcessorTest { + private final KafkaMessageBindingProcessor processor = new KafkaMessageBindingProcessor(); + + @Test + void processTest() throws NoSuchMethodException { + Method method = KafkaMessageBindingProcessorTest.class.getMethod("methodWithAnnotation"); + + ProcessedMessageBinding binding = processor.process(method).get(); + + assertThat(binding.getType()).isEqualTo("kafka"); + assertThat(binding.getBinding()).isEqualTo(new KafkaMessageBinding()); + } + + @Test + void processWithoutAnnotationTest() throws NoSuchMethodException { + Method method = KafkaMessageBindingProcessorTest.class.getMethod("methodWithoutAnnotation"); + + Optional binding = processor.process(method); + + assertThat(binding).isNotPresent(); + } + + @KafkaAsyncOperationBinding + public void methodWithAnnotation() {} + + public void methodWithoutAnnotation() {} +} diff --git a/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/KafkaOperationBindingProcessorTest.java b/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/KafkaOperationBindingProcessorTest.java new file mode 100644 index 000000000..9328c4d71 --- /dev/null +++ b/springwolf-plugins/springwolf-kafka-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/KafkaOperationBindingProcessorTest.java @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor; + +import com.asyncapi.v2.binding.operation.kafka.KafkaOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.ProcessedOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.KafkaAsyncOperationBinding; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class KafkaOperationBindingProcessorTest { + private final KafkaOperationBindingProcessor processor = new KafkaOperationBindingProcessor(); + + @Test + void mapToOperationBindingTest() throws NoSuchMethodException { + KafkaAsyncOperationBinding annotation = KafkaOperationBindingProcessorTest.class + .getMethod("methodWithAnnotation") + .getAnnotation(KafkaAsyncOperationBinding.class); + + ProcessedOperationBinding binding = processor.mapToOperationBinding(annotation); + + assertThat(binding.getType()).isEqualTo("kafka"); + assertThat(binding.getBinding()).isEqualTo(new KafkaOperationBinding()); + } + + @KafkaAsyncOperationBinding + public void methodWithAnnotation() {} +} diff --git a/springwolf-plugins/springwolf-sns-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/SnsOperationBindingProcessor.java b/springwolf-plugins/springwolf-sns-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/SnsOperationBindingProcessor.java index d794787bf..1d4f39d7f 100644 --- a/springwolf-plugins/springwolf-sns-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/SnsOperationBindingProcessor.java +++ b/springwolf-plugins/springwolf-sns-plugin/src/main/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/SnsOperationBindingProcessor.java @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata; -import com.asyncapi.v2.binding.operation.sqs.SQSOperationBinding; +import com.asyncapi.v2.binding.operation.sns.SNSOperationBinding; import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.ProcessedOperationBinding; import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor.AbstractOperationBindingProcessor; import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.SnsAsyncOperationBinding; @@ -10,6 +10,6 @@ public class SnsOperationBindingProcessor extends AbstractOperationBindingProces @Override protected ProcessedOperationBinding mapToOperationBinding(SnsAsyncOperationBinding bindingAnnotation) { - return new ProcessedOperationBinding(bindingAnnotation.type(), new SQSOperationBinding()); + return new ProcessedOperationBinding(bindingAnnotation.type(), new SNSOperationBinding()); } } diff --git a/springwolf-plugins/springwolf-sns-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/SnsMessageBindingProcessorTest.java b/springwolf-plugins/springwolf-sns-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/SnsMessageBindingProcessorTest.java new file mode 100644 index 000000000..8bb7cd1d0 --- /dev/null +++ b/springwolf-plugins/springwolf-sns-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/SnsMessageBindingProcessorTest.java @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata; + +import com.asyncapi.v2.binding.message.sns.SNSMessageBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.ProcessedMessageBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.SnsAsyncOperationBinding; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class SnsMessageBindingProcessorTest { + private final SnsMessageBindingProcessor processor = new SnsMessageBindingProcessor(); + + @Test + void processTest() throws NoSuchMethodException { + Method method = SnsMessageBindingProcessorTest.class.getMethod("methodWithAnnotation"); + + ProcessedMessageBinding binding = processor.process(method).get(); + + assertThat(binding.getType()).isEqualTo("sns"); + assertThat(binding.getBinding()).isEqualTo(new SNSMessageBinding()); + } + + @Test + void processWithoutAnnotationTest() throws NoSuchMethodException { + Method method = SnsMessageBindingProcessorTest.class.getMethod("methodWithoutAnnotation"); + + Optional binding = processor.process(method); + + assertThat(binding).isNotPresent(); + } + + @SnsAsyncOperationBinding + public void methodWithAnnotation() {} + + public void methodWithoutAnnotation() {} +} diff --git a/springwolf-plugins/springwolf-sns-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/SnsOperationBindingProcessorTest.java b/springwolf-plugins/springwolf-sns-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/SnsOperationBindingProcessorTest.java new file mode 100644 index 000000000..5da3b8643 --- /dev/null +++ b/springwolf-plugins/springwolf-sns-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/channels/operationdata/SnsOperationBindingProcessorTest.java @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata; + +import com.asyncapi.v2.binding.operation.sns.SNSOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.ProcessedOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.SnsAsyncOperationBinding; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class SnsOperationBindingProcessorTest { + private final SnsOperationBindingProcessor processor = new SnsOperationBindingProcessor(); + + @Test + void mapToOperationBindingTest() throws NoSuchMethodException { + SnsAsyncOperationBinding annotation = SnsOperationBindingProcessorTest.class + .getMethod("methodWithAnnotation") + .getAnnotation(SnsAsyncOperationBinding.class); + + ProcessedOperationBinding binding = processor.mapToOperationBinding(annotation); + + assertThat(binding.getType()).isEqualTo("sns"); + assertThat(binding.getBinding()).isEqualTo(new SNSOperationBinding()); + } + + @SnsAsyncOperationBinding + public void methodWithAnnotation() {} +} diff --git a/springwolf-plugins/springwolf-sqs-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/SqsMessageBindingProcessorTest.java b/springwolf-plugins/springwolf-sqs-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/SqsMessageBindingProcessorTest.java new file mode 100644 index 000000000..77ab05262 --- /dev/null +++ b/springwolf-plugins/springwolf-sqs-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/SqsMessageBindingProcessorTest.java @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor; + +import com.asyncapi.v2.binding.message.sqs.SQSMessageBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.ProcessedMessageBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.SqsAsyncOperationBinding; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class SqsMessageBindingProcessorTest { + private final SqsMessageBindingProcessor processor = new SqsMessageBindingProcessor(); + + @Test + void processTest() throws NoSuchMethodException { + Method method = SqsMessageBindingProcessorTest.class.getMethod("methodWithAnnotation"); + + ProcessedMessageBinding binding = processor.process(method).get(); + + assertThat(binding.getType()).isEqualTo("sqs"); + assertThat(binding.getBinding()).isEqualTo(new SQSMessageBinding()); + } + + @Test + void processWithoutAnnotationTest() throws NoSuchMethodException { + Method method = SqsMessageBindingProcessorTest.class.getMethod("methodWithoutAnnotation"); + + Optional binding = processor.process(method); + + assertThat(binding).isNotPresent(); + } + + @SqsAsyncOperationBinding + public void methodWithAnnotation() {} + + public void methodWithoutAnnotation() {} +} diff --git a/springwolf-plugins/springwolf-sqs-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/SqsOperationBindingProcessorTest.java b/springwolf-plugins/springwolf-sqs-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/SqsOperationBindingProcessorTest.java new file mode 100644 index 000000000..3289547ff --- /dev/null +++ b/springwolf-plugins/springwolf-sqs-plugin/src/test/java/io/github/stavshamir/springwolf/asyncapi/scanners/bindings/processor/SqsOperationBindingProcessorTest.java @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.asyncapi.scanners.bindings.processor; + +import com.asyncapi.v2.binding.operation.sqs.SQSOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.ProcessedOperationBinding; +import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.SqsAsyncOperationBinding; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class SqsOperationBindingProcessorTest { + private final SqsOperationBindingProcessor processor = new SqsOperationBindingProcessor(); + + @Test + void mapToOperationBindingTest() throws NoSuchMethodException { + SqsAsyncOperationBinding annotation = SqsOperationBindingProcessorTest.class + .getMethod("methodWithAnnotation") + .getAnnotation(SqsAsyncOperationBinding.class); + + ProcessedOperationBinding binding = processor.mapToOperationBinding(annotation); + + assertThat(binding.getType()).isEqualTo("sqs"); + assertThat(binding.getBinding()).isEqualTo(new SQSOperationBinding()); + } + + @SqsAsyncOperationBinding + public void methodWithAnnotation() {} +} diff --git a/springwolf-ui/src/app/shared/mock/mock-server.ts b/springwolf-ui/src/app/shared/mock/mock-server.ts index 180faf6c9..13ba25552 100644 --- a/springwolf-ui/src/app/shared/mock/mock-server.ts +++ b/springwolf-ui/src/app/shared/mock/mock-server.ts @@ -9,6 +9,7 @@ import mockSpringwolfCloudStream from "../../../../../springwolf-examples/spring import mockSpringwolfKafka from "../../../../../springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json"; import mockSpringwolfSns from "../../../../../springwolf-examples/springwolf-sns-example/src/test/resources/asyncapi.json"; import mockSpringwolfSqs from "../../../../../springwolf-examples/springwolf-sqs-example/src/test/resources/asyncapi.json"; +import mockSpringwolfJms from "../../../../../springwolf-examples/springwolf-jms-example/src/test/resources/asyncapi.json"; export class MockServer implements InMemoryDbService { createDb() { @@ -53,6 +54,8 @@ export class MockServer implements InMemoryDbService { return mockSpringwolfSns; } else if (hostname.includes("sqs")) { return mockSpringwolfSqs; + } else if (hostname.includes("jms")) { + return mockSpringwolfJms; } // Kafka is default return mockSpringwolfKafka;