From 590ee2fced23c646ec55d6cb396e89cce4eba8ce Mon Sep 17 00:00:00 2001 From: Juan C Galvis <8420868+juancgalvis@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:16:02 -0500 Subject: [PATCH 01/12] next(shared-starter): Create shared starter classes for any broker type to enable many brokers usage for 5 version --- .../async/commons/config/BrokerConfig.java | 23 +- .../async/kafka/KafkaDirectAsyncGateway.java | 75 ++++++ .../async/kafka/KafkaDomainEventBus.java | 10 + .../async/rabbit/RabbitDomainEventBus.java | 18 +- .../ApplicationNotificationListener.java | 9 +- .../listeners/GenericMessageListener.java | 3 +- build.gradle | 9 - .../reactive-commons/1-getting-started.md | 2 +- .../9-configuration-properties.md | 8 +- .../api/domain/DomainEventBus.java | 2 + gradle.properties | 4 +- gradle/wrapper/gradle-wrapper.jar | Bin 61574 -> 43462 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 29 +- main.gradle | 38 +++ .../main/java/sample/EDASampleSenderApp.java | 2 +- .../src/main/java/sample/KafkaConfig.java | 4 +- .../src/main/java/sample/ListenerConfig.java | 2 +- settings.gradle | 2 +- .../async-commons-starter.gradle | 17 ++ .../annotations/EnableCommandListeners.java | 10 +- .../annotations/EnableDirectAsyncGateway.java | 9 +- .../annotations/EnableDomainEventBus.java | 6 +- .../annotations/EnableEventListeners.java | 8 +- .../annotations/EnableMessageListeners.java | 30 +++ .../EnableNotificationListener.java | 8 +- .../annotations/EnableQueryListeners.java | 10 +- .../async/starter/broker/BrokerProvider.java | 29 ++ .../starter/broker/BrokerProviderFactory.java | 13 + .../async/starter/broker/DiscardProvider.java | 8 + .../async/starter/broker/Status.java | 12 + .../starter/config/ConnectionManager.java | 34 +++ .../async/starter}/config/DomainHandlers.java | 2 +- .../starter/config/ReactiveCommonsConfig.java | 138 ++++++++++ .../health/ReactiveCommonsHealthConfig.java | 20 ++ .../ReactiveCommonsHealthIndicator.java | 36 +++ .../InvalidConfigurationException.java | 0 .../listeners/AbstractListenerConfig.java | 16 ++ .../listeners/CommandsListenerConfig.java | 25 ++ .../listeners/EventsListenerConfig.java | 25 ++ .../NotificationEventsListenerConfig.java | 25 ++ .../listeners/QueriesListenerConfig.java | 25 ++ .../starter/props}/GenericAsyncProps.java | 8 +- .../props}/GenericAsyncPropsDomain.java | 2 +- .../GenericAsyncPropsDomainProperties.java | 2 +- .../senders/DirectAsyncGatewayConfig.java | 29 ++ .../async/starter/senders/EventBusConfig.java | 23 ++ .../senders/GenericDirectAsyncGateway.java | 83 ++++++ .../senders/GenericDomainEventBus.java | 36 +++ .../async-kafka-starter.gradle | 3 +- .../async/kafka/KafkaBrokerProvider.java | 108 ++++++++ .../kafka/KafkaBrokerProviderFactory.java | 59 ++++ .../async/kafka/KafkaDiscardProvider.java | 34 +++ .../async/kafka/KafkaSetupUtils.java | 86 ++++++ .../async/kafka/config/ConnectionManager.java | 78 ------ .../async/kafka/config/DomainHandlers.java | 23 -- .../async/kafka/config/RCKafkaConfig.java | 179 ------------- .../config/RCKafkaEventListenerConfig.java | 49 ---- .../config/RCKafkaHandlersConfiguration.java | 32 --- ...CKafkaNotificationEventListenerConfig.java | 42 --- .../kafka/config/props/AsyncKafkaProps.java | 10 +- .../config/props/AsyncKafkaPropsDomain.java | 2 +- .../AsyncKafkaPropsDomainProperties.java | 2 +- .../config/spring/KafkaPropertiesBase.java | 4 +- .../health/KafkaReactiveHealthIndicator.java | 32 +++ .../starter/impl/kafka/RCKafkaConfig.java | 54 ++++ .../kafka/KafkaBrokerProviderFactoryTest.java | 75 ++++++ .../async/kafka/KafkaBrokerProviderTest.java | 147 ++++++++++ .../async/kafka/KafkaDiscardProviderTest.java | 38 +++ .../KafkaReactiveHealthIndicatorTest.java | 71 +++++ .../starter/impl/rabbit/KafkaConfigTest.java | 44 +++ .../src/test/resources/application.yaml | 8 + .../config/DirectAsyncGatewayConfig.java | 2 +- .../config/EventBusConfig.java | 2 +- .../config/RabbitMqConfig.java | 5 +- .../config/RabbitProperties.java | 2 +- .../async-commons-rabbit-starter.gradle | 2 +- .../async/RabbitEDADirectAsyncGateway.java | 25 -- .../annotations/EnableDomainEventBus.java | 19 -- .../annotations/EnableEventListeners.java | 18 -- .../annotations/EnableMessageListeners.java | 26 -- .../EnableNotificationListener.java | 19 -- .../async/rabbit/RabbitMQBrokerProvider.java | 160 +++++++++++ .../rabbit/RabbitMQBrokerProviderFactory.java | 57 ++++ .../async/rabbit/RabbitMQDiscardProvider.java | 34 +++ .../async/rabbit/RabbitMQSetupUtils.java | 122 +++++++++ .../rabbit/config/CommandListenersConfig.java | 38 --- .../rabbit/config/ConnectionManager.java | 73 ----- .../config/DirectAsyncGatewayConfig.java | 82 ------ .../async/rabbit/config/EventBusConfig.java | 31 --- .../rabbit/config/EventListenersConfig.java | 54 ---- .../config/NotificationListenersConfig.java | 41 --- .../rabbit/config/QueryListenerConfig.java | 41 --- .../rabbit/config/RabbitHealthConfig.java | 19 -- .../async/rabbit/config/RabbitMqConfig.java | 253 ------------------ .../async/rabbit/config/RabbitProperties.java | 2 + .../config/RabbitPropertiesAutoConfig.java | 2 + .../async/rabbit/config/props/AsyncProps.java | 11 +- .../rabbit/config/props/AsyncPropsDomain.java | 2 +- .../AsyncRabbitPropsDomainProperties.java | 2 +- .../{ => spring}/RabbitPropertiesBase.java | 2 +- .../DomainRabbitReactiveHealthIndicator.java | 70 ----- .../health/RabbitReactiveHealthIndicator.java | 51 ++++ .../async/rabbit/health/Status.java | 11 - .../starter/impl/rabbit/RabbitMQConfig.java | 63 +++++ .../RabbitMQBrokerProviderFactoryTest.java | 74 +++++ .../rabbit/RabbitMQBrokerProviderTest.java | 192 +++++++++++++ .../rabbit/RabbitMQDiscardProviderTest.java | 37 +++ .../config/CommandListenersConfigTest.java | 67 ----- .../config/EventListenersConfigTest.java | 80 ------ .../NotificationListenersConfigTest.java | 73 ----- .../config/QueryListenerConfigTest.java | 72 ----- .../rabbit/config/RabbitMqConfigTest.java | 67 ----- ...=> RabbitReactiveHealthIndicatorTest.java} | 55 ++-- .../impl/rabbit/RabbitMQConfigTest.java | 42 +++ starters/shared/shared-starter.gradle | 10 - 116 files changed, 2513 insertions(+), 1709 deletions(-) create mode 100644 async/async-kafka/src/main/java/org/reactivecommons/async/kafka/KafkaDirectAsyncGateway.java create mode 100644 starters/async-commons-starter/async-commons-starter.gradle rename starters/{async-rabbit-starter => async-commons-starter}/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableCommandListeners.java (50%) rename starters/{async-rabbit-starter => async-commons-starter}/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableDirectAsyncGateway.java (50%) rename starters/{async-kafka-starter/src/main/java/org/reactivecommons/async/kafka => async-commons-starter/src/main/java/org/reactivecommons/async/impl/config}/annotations/EnableDomainEventBus.java (74%) rename starters/{async-kafka-starter/src/main/java/org/reactivecommons/async/kafka => async-commons-starter/src/main/java/org/reactivecommons/async/impl/config}/annotations/EnableEventListeners.java (55%) create mode 100644 starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableMessageListeners.java rename starters/{async-kafka-starter/src/main/java/org/reactivecommons/async/kafka => async-commons-starter/src/main/java/org/reactivecommons/async/impl/config}/annotations/EnableNotificationListener.java (54%) rename starters/{async-rabbit-starter => async-commons-starter}/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableQueryListeners.java (50%) create mode 100644 starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/BrokerProvider.java create mode 100644 starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/BrokerProviderFactory.java create mode 100644 starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/DiscardProvider.java create mode 100644 starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/Status.java create mode 100644 starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/ConnectionManager.java rename starters/{async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit => async-commons-starter/src/main/java/org/reactivecommons/async/starter}/config/DomainHandlers.java (93%) create mode 100644 starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/ReactiveCommonsConfig.java create mode 100644 starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/health/ReactiveCommonsHealthConfig.java create mode 100644 starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/health/ReactiveCommonsHealthIndicator.java rename starters/{shared => async-commons-starter}/src/main/java/org/reactivecommons/async/starter/exceptions/InvalidConfigurationException.java (100%) create mode 100644 starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/AbstractListenerConfig.java create mode 100644 starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/CommandsListenerConfig.java create mode 100644 starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/EventsListenerConfig.java create mode 100644 starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/NotificationEventsListenerConfig.java create mode 100644 starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/QueriesListenerConfig.java rename starters/{shared/src/main/java/org/reactivecommons/async/starter => async-commons-starter/src/main/java/org/reactivecommons/async/starter/props}/GenericAsyncProps.java (67%) rename starters/{shared/src/main/java/org/reactivecommons/async/starter => async-commons-starter/src/main/java/org/reactivecommons/async/starter/props}/GenericAsyncPropsDomain.java (99%) rename starters/{shared/src/main/java/org/reactivecommons/async/starter => async-commons-starter/src/main/java/org/reactivecommons/async/starter/props}/GenericAsyncPropsDomainProperties.java (96%) create mode 100644 starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/DirectAsyncGatewayConfig.java create mode 100644 starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/EventBusConfig.java create mode 100644 starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/GenericDirectAsyncGateway.java create mode 100644 starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/GenericDomainEventBus.java create mode 100644 starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaBrokerProvider.java create mode 100644 starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaBrokerProviderFactory.java create mode 100644 starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaDiscardProvider.java create mode 100644 starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaSetupUtils.java delete mode 100644 starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/ConnectionManager.java delete mode 100644 starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/DomainHandlers.java delete mode 100644 starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/RCKafkaConfig.java delete mode 100644 starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/RCKafkaEventListenerConfig.java delete mode 100644 starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/RCKafkaHandlersConfiguration.java delete mode 100644 starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/RCKafkaNotificationEventListenerConfig.java create mode 100644 starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/health/KafkaReactiveHealthIndicator.java create mode 100644 starters/async-kafka-starter/src/main/java/org/reactivecommons/async/starter/impl/kafka/RCKafkaConfig.java create mode 100644 starters/async-kafka-starter/src/test/java/org/reactivecommons/async/kafka/KafkaBrokerProviderFactoryTest.java create mode 100644 starters/async-kafka-starter/src/test/java/org/reactivecommons/async/kafka/KafkaBrokerProviderTest.java create mode 100644 starters/async-kafka-starter/src/test/java/org/reactivecommons/async/kafka/KafkaDiscardProviderTest.java create mode 100644 starters/async-kafka-starter/src/test/java/org/reactivecommons/async/kafka/health/KafkaReactiveHealthIndicatorTest.java create mode 100644 starters/async-kafka-starter/src/test/java/org/reactivecommons/async/starter/impl/rabbit/KafkaConfigTest.java create mode 100644 starters/async-kafka-starter/src/test/resources/application.yaml rename starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/{ => standalone}/config/DirectAsyncGatewayConfig.java (97%) rename starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/{ => standalone}/config/EventBusConfig.java (92%) rename starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/{ => standalone}/config/RabbitMqConfig.java (95%) rename starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/{ => standalone}/config/RabbitProperties.java (82%) delete mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/RabbitEDADirectAsyncGateway.java delete mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableDomainEventBus.java delete mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableEventListeners.java delete mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableMessageListeners.java delete mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableNotificationListener.java create mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProvider.java create mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProviderFactory.java create mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQDiscardProvider.java create mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQSetupUtils.java delete mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/CommandListenersConfig.java delete mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/ConnectionManager.java delete mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/DirectAsyncGatewayConfig.java delete mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/EventBusConfig.java delete mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/EventListenersConfig.java delete mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/NotificationListenersConfig.java delete mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/QueryListenerConfig.java delete mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/RabbitHealthConfig.java delete mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/RabbitMqConfig.java rename starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/{ => spring}/RabbitPropertiesBase.java (99%) delete mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/DomainRabbitReactiveHealthIndicator.java create mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/RabbitReactiveHealthIndicator.java delete mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/Status.java create mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/starter/impl/rabbit/RabbitMQConfig.java create mode 100644 starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProviderFactoryTest.java create mode 100644 starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProviderTest.java create mode 100644 starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/RabbitMQDiscardProviderTest.java delete mode 100644 starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/CommandListenersConfigTest.java delete mode 100644 starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/EventListenersConfigTest.java delete mode 100644 starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/NotificationListenersConfigTest.java delete mode 100644 starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/QueryListenerConfigTest.java delete mode 100644 starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/RabbitMqConfigTest.java rename starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/health/{DomainRabbitReactiveHealthIndicatorTest.java => RabbitReactiveHealthIndicatorTest.java} (58%) create mode 100644 starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/starter/impl/rabbit/RabbitMQConfigTest.java delete mode 100644 starters/shared/shared-starter.gradle diff --git a/async/async-commons/src/main/java/org/reactivecommons/async/commons/config/BrokerConfig.java b/async/async-commons/src/main/java/org/reactivecommons/async/commons/config/BrokerConfig.java index fa588ad0..e3bd99f9 100644 --- a/async/async-commons/src/main/java/org/reactivecommons/async/commons/config/BrokerConfig.java +++ b/async/async-commons/src/main/java/org/reactivecommons/async/commons/config/BrokerConfig.java @@ -1,8 +1,11 @@ package org.reactivecommons.async.commons.config; +import lombok.Getter; + import java.time.Duration; import java.util.UUID; +@Getter public class BrokerConfig { private final String routingKey = UUID.randomUUID().toString().replaceAll("-", ""); private final boolean persistentQueries; @@ -24,24 +27,4 @@ public BrokerConfig(boolean persistentQueries, boolean persistentCommands, boole this.replyTimeout = replyTimeout; } - public boolean isPersistentQueries() { - return persistentQueries; - } - - public boolean isPersistentCommands() { - return persistentCommands; - } - - public boolean isPersistentEvents() { - return persistentEvents; - } - - public Duration getReplyTimeout() { - return replyTimeout; - } - - public String getRoutingKey() { - return routingKey; - } - } diff --git a/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/KafkaDirectAsyncGateway.java b/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/KafkaDirectAsyncGateway.java new file mode 100644 index 00000000..910a189d --- /dev/null +++ b/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/KafkaDirectAsyncGateway.java @@ -0,0 +1,75 @@ +package org.reactivecommons.async.kafka; + +import io.cloudevents.CloudEvent; +import org.reactivecommons.api.domain.Command; +import org.reactivecommons.async.api.AsyncQuery; +import org.reactivecommons.async.api.DirectAsyncGateway; +import org.reactivecommons.async.api.From; +import reactor.core.publisher.Mono; + +public class KafkaDirectAsyncGateway implements DirectAsyncGateway { + @Override + public Mono sendCommand(Command command, String targetName) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public Mono sendCommand(Command command, String targetName, long delayMillis) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public Mono sendCommand(Command command, String targetName, String domain) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public Mono sendCommand(Command command, String targetName, long delayMillis, String domain) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public Mono sendCommand(CloudEvent command, String targetName) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public Mono sendCommand(CloudEvent command, String targetName, long delayMillis) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public Mono sendCommand(CloudEvent command, String targetName, String domain) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public Mono sendCommand(CloudEvent command, String targetName, long delayMillis, String domain) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public Mono requestReply(AsyncQuery query, String targetName, Class type) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public Mono requestReply(AsyncQuery query, String targetName, Class type, String domain) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public Mono requestReply(CloudEvent query, String targetName, Class type) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public Mono requestReply(CloudEvent query, String targetName, Class type, String domain) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public Mono reply(T response, From from) { + throw new UnsupportedOperationException("Not implemented yet"); + } +} diff --git a/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/KafkaDomainEventBus.java b/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/KafkaDomainEventBus.java index d24f0a76..e986c84e 100644 --- a/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/KafkaDomainEventBus.java +++ b/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/KafkaDomainEventBus.java @@ -16,8 +16,18 @@ public Publisher emit(DomainEvent event) { return sender.send(event); } + @Override + public Publisher emit(String domain, DomainEvent event) { + throw new UnsupportedOperationException("Not implemented yet"); + } + @Override public Publisher emit(CloudEvent event) { return sender.send(event); } + + @Override + public Publisher emit(String domain, CloudEvent event) { + throw new UnsupportedOperationException("Not implemented yet"); + } } diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/RabbitDomainEventBus.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/RabbitDomainEventBus.java index bb249453..d930f22c 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/RabbitDomainEventBus.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/RabbitDomainEventBus.java @@ -1,12 +1,12 @@ package org.reactivecommons.async.rabbit; import io.cloudevents.CloudEvent; -import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; +import org.reactivecommons.api.domain.DomainEvent; +import org.reactivecommons.api.domain.DomainEventBus; import org.reactivecommons.async.commons.config.BrokerConfig; +import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; -import org.reactivecommons.api.domain.DomainEvent; -import org.reactivecommons.api.domain.DomainEventBus; import java.util.Collections; @@ -29,7 +29,12 @@ public RabbitDomainEventBus(ReactiveMessageSender sender, String exchange, Broke @Override public Mono emit(DomainEvent event) { return sender.sendWithConfirm(event, exchange, event.getName(), Collections.emptyMap(), persistentEvents) - .onErrorMap(err -> new RuntimeException("Event send failure: " + event.getName(), err)); + .onErrorMap(err -> new RuntimeException("Event send failure: " + event.getName(), err)); + } + + @Override + public Publisher emit(String domain, DomainEvent event) { + throw new UnsupportedOperationException("Not implemented yet"); } @Override @@ -39,4 +44,9 @@ public Publisher emit(CloudEvent cloudEvent) { .onErrorMap(err -> new RuntimeException("Event send failure: " + cloudEvent.getType(), err)); } + @Override + public Publisher emit(String domain, CloudEvent event) { + throw new UnsupportedOperationException("Not implemented yet"); + } + } diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationNotificationListener.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationNotificationListener.java index c5e204b5..6a6e4336 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationNotificationListener.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationNotificationListener.java @@ -7,10 +7,10 @@ import org.reactivecommons.async.api.handlers.registered.RegisteredEventListener; import org.reactivecommons.async.commons.DiscardNotifier; import org.reactivecommons.async.commons.EventExecutor; +import org.reactivecommons.async.commons.HandlerResolver; import org.reactivecommons.async.commons.communications.Message; import org.reactivecommons.async.commons.converters.MessageConverter; import org.reactivecommons.async.commons.ext.CustomReporter; -import org.reactivecommons.async.commons.HandlerResolver; import org.reactivecommons.async.rabbit.communications.ReactiveMessageListener; import org.reactivecommons.async.rabbit.communications.TopologyCreator; import reactor.core.publisher.Flux; @@ -51,9 +51,6 @@ public ApplicationNotificationListener(ReactiveMessageListener receiver, } protected Mono setUpBindings(TopologyCreator creator) { - final Mono declareExchange = creator.declare(exchange(exchangeName) - .type("topic") - .durable(true)); final Mono declareQueue = creator.declare( queue(queueName) @@ -65,6 +62,10 @@ protected Mono setUpBindings(TopologyCreator creator) { .flatMap(listener -> creator.bind(binding(exchangeName, listener.getPath(), queueName))); if (createTopology) { + final Mono declareExchange = creator.declare(exchange(exchangeName) + .type("topic") + .durable(true)); + return declareExchange .then(declareQueue) .thenMany(bindings) diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/GenericMessageListener.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/GenericMessageListener.java index a6555431..27811aa5 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/GenericMessageListener.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/GenericMessageListener.java @@ -139,7 +139,8 @@ protected Mono handle(AcknowledgableDelivery msj, Instan } private void onTerminate() { - messageFlux.doOnTerminate(this::onTerminate) + messageFlux + .doOnTerminate(this::onTerminate) .subscribe(new LoggerSubscriber<>(getClass().getName())); } diff --git a/build.gradle b/build.gradle index 251626ad..47a10da8 100644 --- a/build.gradle +++ b/build.gradle @@ -18,15 +18,6 @@ plugins { id 'co.com.bancolombia.cleanArchitecture' version '3.17.13' } -sonar { - properties { - property 'sonar.projectKey', 'reactive-commons_reactive-commons-java' - property 'sonar.coverage.exclusions', 'samples/**/*' - property 'sonar.organization', 'reactive-commons' - property 'sonar.host.url', 'https://sonarcloud.io' - } -} - repositories { mavenCentral() } diff --git a/docs/docs/reactive-commons/1-getting-started.md b/docs/docs/reactive-commons/1-getting-started.md index 3deb07a1..36fa4c10 100644 --- a/docs/docs/reactive-commons/1-getting-started.md +++ b/docs/docs/reactive-commons/1-getting-started.md @@ -83,7 +83,7 @@ spring: You can also set it in runtime for example from a secret, so you can create the `RabbitProperties` bean like: -```java title="org.reactivecommons.async.rabbit.config.RabbitProperties" +```java title="org.reactivecommons.async.rabbit.standalone.config.RabbitProperties" @Configuration public class MyRabbitMQConfig { diff --git a/docs/docs/reactive-commons/9-configuration-properties.md b/docs/docs/reactive-commons/9-configuration-properties.md index 2155dacb..c64b9ad4 100644 --- a/docs/docs/reactive-commons/9-configuration-properties.md +++ b/docs/docs/reactive-commons/9-configuration-properties.md @@ -27,6 +27,9 @@ app: createTopology: true # if your organization have restrictions with automatic topology creation you can set it to false and create it manually or by your organization process. delayedCommands: false # Enable to send a delayed command to an external target prefetchCount: 250 # is the maximum number of in flight messages you can reduce it to process less concurrent messages, this settings acts per instance of your service + useDiscardNotifierPerDomain: false # if true it uses a discard notifier for each domain,when false it uses a single discard notifier for all domains with default 'app' domain + enabled: true # if you want to disable this domain you can set it to false + brokerType: "rabbitmq" # please don't change this value flux: maxConcurrency: 250 # max concurrency of listener flow domain: @@ -64,7 +67,7 @@ You can override this settings programmatically through a `AsyncPropsDomainPrope ```java package sample; -import org.reactivecommons.async.rabbit.config.RabbitProperties; +import org.reactivecommons.async.rabbit.standalone.config.RabbitProperties; import org.reactivecommons.async.rabbit.config.props.AsyncProps; import org.reactivecommons.async.rabbit.config.props.AsyncRabbitPropsDomainProperties; import org.springframework.context.annotation.Bean; @@ -133,6 +136,9 @@ reactive: retryDelay: 1000 # interval for message retries, with and without DLQRetry checkExistingTopics: true # if you don't want to verify topic existence before send a record you can set it to false createTopology: true # if your organization have restrictions with automatic topology creation you can set it to false and create it manually or by your organization process. + useDiscardNotifierPerDomain: false # if true it uses a discard notifier for each domain,when false it uses a single discard notifier for all domains with default 'app' domain + enabled: true # if you want to disable this domain you can set it to false + brokerType: "kafka" # please don't change this value domain: ignoreThisListener: false # Allows you to disable event listener for this specific domain connectionProperties: # you can override the connection properties of each domain diff --git a/domain/domain-events/src/main/java/org/reactivecommons/api/domain/DomainEventBus.java b/domain/domain-events/src/main/java/org/reactivecommons/api/domain/DomainEventBus.java index df697403..458a0c0c 100644 --- a/domain/domain-events/src/main/java/org/reactivecommons/api/domain/DomainEventBus.java +++ b/domain/domain-events/src/main/java/org/reactivecommons/api/domain/DomainEventBus.java @@ -5,6 +5,8 @@ public interface DomainEventBus { Publisher emit(DomainEvent event); + Publisher emit(String domain, DomainEvent event); Publisher emit(CloudEvent event); + Publisher emit(String domain, CloudEvent event); } diff --git a/gradle.properties b/gradle.properties index 80e3741c..d6a490b4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=5.0.1-beta -toPublish=domain-events-api,async-commons-api,async-commons,shared-starter,async-rabbit,async-commons-rabbit-standalone,async-commons-rabbit-starter,async-kafka,async-kafka-starter +version=5.0.1-betaLOCAL +toPublish=domain-events-api,async-commons-api,async-commons,shared-starter,async-rabbit,async-commons-rabbit-standalone,async-commons-rabbit-starter,async-kafka,async-kafka-starter,async-commons-starter onlyUpdater=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 943f0cbfa754578e88a3dae77fce6e3dea56edbf..d64cd4917707c1f8861d8cb53dd15194d4248596 100644 GIT binary patch literal 43462 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 literal 61574 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 ScNgau(U?ep-K_E5zy1%ZQTdPn diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3499ded5..a4413138 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 65dcd68d..1aa94a42 100755 --- a/gradlew +++ b/gradlew @@ -83,10 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/main.gradle b/main.gradle index f06cfc60..9249d5ca 100644 --- a/main.gradle +++ b/main.gradle @@ -12,6 +12,23 @@ allprojects { } group 'org.reactivecommons' + + sonar { + properties { + property "sonar.sourceEncoding", "UTF-8" + property 'sonar.projectKey', 'reactive-commons_reactive-commons-java' + property 'sonar.organization', 'reactive-commons' + property 'sonar.host.url', 'https://sonarcloud.io' + property "sonar.sources", "src/main" + property "sonar.test", "src/test" + property "sonar.java.binaries", "build/classes" + property "sonar.junit.reportPaths", "build/test-results/test" + property "sonar.java-coveragePlugin", "jacoco" + property "sonar.coverage.jacoco.xmlReportPaths", "${rootDir}/build/reports/jacoco/generateMergedReport/generateMergedReport.xml" + property "sonar.exclusions", ".github/**,samples/**/*" + property 'sonar.coverage.exclusions', 'samples/**/*' + } + } } nexusPublishing { @@ -39,9 +56,14 @@ subprojects { testCompileOnly 'org.projectlombok:lombok' } + test.finalizedBy(project.tasks.jacocoTestReport) + jacocoTestReport { + dependsOn test reports { xml.setRequired true + html.setRequired true + csv.setRequired false } } @@ -134,6 +156,22 @@ subprojects { } } +tasks.register('generateMergedReport', JacocoReport) { + dependsOn test + dependsOn subprojects.test + dependsOn subprojects.javadoc + dependsOn subprojects.jacocoTestReport + additionalSourceDirs.setFrom files(subprojects.sourceSets.main.allSource.srcDirs) + sourceDirectories.setFrom files(subprojects.sourceSets.main.allSource.srcDirs) + classDirectories.setFrom files(subprojects.sourceSets.main.output).filter({ !it.toString().contains("sample") }) + executionData.setFrom project.fileTree(dir: '.', include: '**/build/jacoco/test.exec') + reports { + xml.setRequired true + csv.setRequired false + html.setRequired true + } +} + tasks.named('wrapper') { gradleVersion = '8.8' } \ No newline at end of file diff --git a/samples/async/async-kafka-sender-client/src/main/java/sample/EDASampleSenderApp.java b/samples/async/async-kafka-sender-client/src/main/java/sample/EDASampleSenderApp.java index f2c87a97..eaf287da 100644 --- a/samples/async/async-kafka-sender-client/src/main/java/sample/EDASampleSenderApp.java +++ b/samples/async/async-kafka-sender-client/src/main/java/sample/EDASampleSenderApp.java @@ -1,7 +1,7 @@ package sample; import lombok.extern.java.Log; -import org.reactivecommons.async.kafka.annotations.EnableDomainEventBus; +import org.reactivecommons.async.impl.config.annotations.EnableDomainEventBus; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/samples/async/async-kafka-sender-client/src/main/java/sample/KafkaConfig.java b/samples/async/async-kafka-sender-client/src/main/java/sample/KafkaConfig.java index 85e0ad3a..f1eabbba 100644 --- a/samples/async/async-kafka-sender-client/src/main/java/sample/KafkaConfig.java +++ b/samples/async/async-kafka-sender-client/src/main/java/sample/KafkaConfig.java @@ -1,6 +1,6 @@ package sample; -import org.reactivecommons.async.kafka.config.RCKafkaConfig; +import org.reactivecommons.async.kafka.KafkaSetupUtils; import org.reactivecommons.async.kafka.config.props.AsyncKafkaProps; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -20,7 +20,7 @@ public AsyncKafkaProps kafkaProps() throws IOException { kafkaProps.setMaxRetries(5); kafkaProps.setRetryDelay(1000); kafkaProps.setWithDLQRetry(true); - kafkaProps.setConnectionProperties(RCKafkaConfig.readPropsFromDotEnv(Path.of(".kafka-env"))); + kafkaProps.setConnectionProperties(KafkaSetupUtils.readPropsFromDotEnv(Path.of(".kafka-env"))); return kafkaProps; } } diff --git a/samples/async/async-kafka-sender-client/src/main/java/sample/ListenerConfig.java b/samples/async/async-kafka-sender-client/src/main/java/sample/ListenerConfig.java index ababe387..a9b4c9f1 100644 --- a/samples/async/async-kafka-sender-client/src/main/java/sample/ListenerConfig.java +++ b/samples/async/async-kafka-sender-client/src/main/java/sample/ListenerConfig.java @@ -4,7 +4,7 @@ import lombok.extern.log4j.Log4j2; import org.reactivecommons.api.domain.DomainEvent; import org.reactivecommons.async.api.HandlerRegistry; -import org.reactivecommons.async.kafka.annotations.EnableEventListeners; +import org.reactivecommons.async.impl.config.annotations.EnableEventListeners; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; diff --git a/settings.gradle b/settings.gradle index b64577d1..410651d8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,7 +13,7 @@ FileTree buildFiles = fileTree(rootDir) { String rootDirPath = rootDir.absolutePath + File.separator buildFiles.each { File buildFile -> - boolean isDefaultName = 'build.gradle'.equals(buildFile.name) + boolean isDefaultName = 'build.gradle' == buildFile.name if (isDefaultName) { String buildFilePath = buildFile.parentFile.absolutePath String projectPath = buildFilePath.replace(rootDirPath, '').replace(File.separator, ':') diff --git a/starters/async-commons-starter/async-commons-starter.gradle b/starters/async-commons-starter/async-commons-starter.gradle new file mode 100644 index 00000000..43493bd0 --- /dev/null +++ b/starters/async-commons-starter/async-commons-starter.gradle @@ -0,0 +1,17 @@ +ext { + artifactId = 'async-commons-starter' + artifactDescription = 'Async Commons Starter for Spring Boot' +} +dependencies { + api 'io.projectreactor:reactor-core' + api project(':async-commons') + compileOnly 'org.springframework.boot:spring-boot-starter' + compileOnly 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-aop' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + + testImplementation 'io.projectreactor:reactor-test' + testImplementation 'org.springframework.boot:spring-boot-starter-actuator' +} \ No newline at end of file diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableCommandListeners.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableCommandListeners.java similarity index 50% rename from starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableCommandListeners.java rename to starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableCommandListeners.java index fcbe9833..e3181e2e 100644 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableCommandListeners.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableCommandListeners.java @@ -1,16 +1,20 @@ package org.reactivecommons.async.impl.config.annotations; -import org.reactivecommons.async.rabbit.config.CommandListenersConfig; +import org.reactivecommons.async.starter.listeners.CommandsListenerConfig; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import java.lang.annotation.*; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented -@Import(CommandListenersConfig.class) +@Import(CommandsListenerConfig.class) @Configuration public @interface EnableCommandListeners { } diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableDirectAsyncGateway.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableDirectAsyncGateway.java similarity index 50% rename from starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableDirectAsyncGateway.java rename to starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableDirectAsyncGateway.java index 359913fd..72ce234f 100644 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableDirectAsyncGateway.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableDirectAsyncGateway.java @@ -1,10 +1,15 @@ package org.reactivecommons.async.impl.config.annotations; -import org.reactivecommons.async.rabbit.config.DirectAsyncGatewayConfig; +import org.reactivecommons.async.starter.config.ReactiveCommonsConfig; +import org.reactivecommons.async.starter.senders.DirectAsyncGatewayConfig; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import java.lang.annotation.*; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/annotations/EnableDomainEventBus.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableDomainEventBus.java similarity index 74% rename from starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/annotations/EnableDomainEventBus.java rename to starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableDomainEventBus.java index f95879f0..b3e33c09 100644 --- a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/annotations/EnableDomainEventBus.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableDomainEventBus.java @@ -1,6 +1,6 @@ -package org.reactivecommons.async.kafka.annotations; +package org.reactivecommons.async.impl.config.annotations; -import org.reactivecommons.async.kafka.config.RCKafkaConfig; +import org.reactivecommons.async.starter.senders.EventBusConfig; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -14,7 +14,7 @@ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented -@Import(RCKafkaConfig.class) +@Import(EventBusConfig.class) @Configuration public @interface EnableDomainEventBus { } diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/annotations/EnableEventListeners.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableEventListeners.java similarity index 55% rename from starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/annotations/EnableEventListeners.java rename to starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableEventListeners.java index 366c97e0..c5f9839d 100644 --- a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/annotations/EnableEventListeners.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableEventListeners.java @@ -1,8 +1,6 @@ -package org.reactivecommons.async.kafka.annotations; +package org.reactivecommons.async.impl.config.annotations; -import org.reactivecommons.async.kafka.config.RCKafkaConfig; -import org.reactivecommons.async.kafka.config.RCKafkaEventListenerConfig; -import org.reactivecommons.async.kafka.config.RCKafkaHandlersConfiguration; +import org.reactivecommons.async.starter.listeners.EventsListenerConfig; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -15,7 +13,7 @@ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented -@Import({RCKafkaEventListenerConfig.class, RCKafkaHandlersConfiguration.class, RCKafkaConfig.class}) +@Import(EventsListenerConfig.class) @Configuration public @interface EnableEventListeners { } diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableMessageListeners.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableMessageListeners.java new file mode 100644 index 00000000..3107c83e --- /dev/null +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableMessageListeners.java @@ -0,0 +1,30 @@ +package org.reactivecommons.async.impl.config.annotations; + +import org.reactivecommons.async.starter.listeners.CommandsListenerConfig; +import org.reactivecommons.async.starter.listeners.EventsListenerConfig; +import org.reactivecommons.async.starter.listeners.NotificationEventsListenerConfig; +import org.reactivecommons.async.starter.listeners.QueriesListenerConfig; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation enables all messages listeners (Query, Commands, Events). If you want to enable separately, please use + * EnableCommandListeners, EnableQueryListeners or EnableEventListeners. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@Documented +@Import({CommandsListenerConfig.class, QueriesListenerConfig.class, EventsListenerConfig.class, + NotificationEventsListenerConfig.class}) +@Configuration +public @interface EnableMessageListeners { +} + + + diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/annotations/EnableNotificationListener.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableNotificationListener.java similarity index 54% rename from starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/annotations/EnableNotificationListener.java rename to starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableNotificationListener.java index 1ae0af73..e4ce2e36 100644 --- a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/annotations/EnableNotificationListener.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableNotificationListener.java @@ -1,8 +1,6 @@ -package org.reactivecommons.async.kafka.annotations; +package org.reactivecommons.async.impl.config.annotations; -import org.reactivecommons.async.kafka.config.RCKafkaConfig; -import org.reactivecommons.async.kafka.config.RCKafkaHandlersConfiguration; -import org.reactivecommons.async.kafka.config.RCKafkaNotificationEventListenerConfig; +import org.reactivecommons.async.starter.listeners.NotificationEventsListenerConfig; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -16,7 +14,7 @@ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented -@Import({RCKafkaNotificationEventListenerConfig.class, RCKafkaHandlersConfiguration.class, RCKafkaConfig.class}) +@Import(NotificationEventsListenerConfig.class) @Configuration public @interface EnableNotificationListener { } diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableQueryListeners.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableQueryListeners.java similarity index 50% rename from starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableQueryListeners.java rename to starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableQueryListeners.java index 6eb878b0..001433e2 100644 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableQueryListeners.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableQueryListeners.java @@ -1,16 +1,20 @@ package org.reactivecommons.async.impl.config.annotations; -import org.reactivecommons.async.rabbit.config.QueryListenerConfig; +import org.reactivecommons.async.starter.listeners.QueriesListenerConfig; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import java.lang.annotation.*; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented -@Import(QueryListenerConfig.class) +@Import(QueriesListenerConfig.class) @Configuration public @interface EnableQueryListeners { } diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/BrokerProvider.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/BrokerProvider.java new file mode 100644 index 00000000..88d438d4 --- /dev/null +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/BrokerProvider.java @@ -0,0 +1,29 @@ +package org.reactivecommons.async.starter.broker; + +import org.reactivecommons.api.domain.DomainEventBus; +import org.reactivecommons.async.api.DirectAsyncGateway; +import org.reactivecommons.async.commons.HandlerResolver; +import org.reactivecommons.async.starter.props.GenericAsyncProps; +import org.springframework.boot.actuate.health.Health; +import reactor.core.publisher.Mono; + +@SuppressWarnings("rawtypes") +public interface BrokerProvider { + T getProps(); + + DomainEventBus getDomainBus(); + + DirectAsyncGateway getDirectAsyncGateway(HandlerResolver resolver); + + void listenDomainEvents(HandlerResolver resolver); + + void listenNotificationEvents(HandlerResolver resolver); + + void listenCommands(HandlerResolver resolver); + + void listenQueries(HandlerResolver resolver); + + void listenReplies(HandlerResolver resolver); + + Mono healthCheck(); +} diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/BrokerProviderFactory.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/BrokerProviderFactory.java new file mode 100644 index 00000000..8a599069 --- /dev/null +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/BrokerProviderFactory.java @@ -0,0 +1,13 @@ +package org.reactivecommons.async.starter.broker; + +import org.reactivecommons.async.starter.props.GenericAsyncProps; + +@SuppressWarnings("rawtypes") +public interface BrokerProviderFactory { + String getBrokerType(); + + DiscardProvider getDiscardProvider(T props); + + BrokerProvider getProvider(String domain, T props, DiscardProvider discardProvider); + +} diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/DiscardProvider.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/DiscardProvider.java new file mode 100644 index 00000000..aa735233 --- /dev/null +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/DiscardProvider.java @@ -0,0 +1,8 @@ +package org.reactivecommons.async.starter.broker; + +import org.reactivecommons.async.commons.DiscardNotifier; + +import java.util.function.Supplier; + +public interface DiscardProvider extends Supplier { +} diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/Status.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/Status.java new file mode 100644 index 00000000..9c6cff48 --- /dev/null +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/Status.java @@ -0,0 +1,12 @@ +package org.reactivecommons.async.starter.broker; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class Status { + private final boolean up; + private final String domain; + private final String details; // version or error +} diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/ConnectionManager.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/ConnectionManager.java new file mode 100644 index 00000000..020e8d0a --- /dev/null +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/ConnectionManager.java @@ -0,0 +1,34 @@ +package org.reactivecommons.async.starter.config; + +import org.reactivecommons.async.starter.broker.BrokerProvider; + +import java.util.Map; +import java.util.TreeMap; +import java.util.function.BiConsumer; + +@SuppressWarnings("rawtypes") +public class ConnectionManager { + private final Map connections = new TreeMap<>(); + + public void forDomain(BiConsumer consumer) { + connections.forEach(consumer); + } + + public ConnectionManager addDomain(String domain, BrokerProvider domainConn) { + connections.put(domain, domainConn); + return this; + } + + private BrokerProvider getChecked(String domain) { + BrokerProvider domainProvider = connections.get(domain); + if (domainProvider == null) { + throw new RuntimeException("You are trying to use the domain " + domain + + " but this connection is not defined"); + } + return domainProvider; + } + + public Map getProviders() { + return connections; + } +} diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/DomainHandlers.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/DomainHandlers.java similarity index 93% rename from starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/DomainHandlers.java rename to starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/DomainHandlers.java index aed074ce..4e7e7662 100644 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/DomainHandlers.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/DomainHandlers.java @@ -1,4 +1,4 @@ -package org.reactivecommons.async.rabbit.config; +package org.reactivecommons.async.starter.config; import org.reactivecommons.async.commons.HandlerResolver; diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/ReactiveCommonsConfig.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/ReactiveCommonsConfig.java new file mode 100644 index 00000000..40653de7 --- /dev/null +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/ReactiveCommonsConfig.java @@ -0,0 +1,138 @@ +package org.reactivecommons.async.starter.config; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import lombok.RequiredArgsConstructor; +import lombok.extern.java.Log; +import org.reactivecommons.async.api.DefaultCommandHandler; +import org.reactivecommons.async.api.DefaultQueryHandler; +import org.reactivecommons.async.api.HandlerRegistry; +import org.reactivecommons.async.commons.HandlerResolver; +import org.reactivecommons.async.commons.HandlerResolverBuilder; +import org.reactivecommons.async.commons.config.BrokerConfig; +import org.reactivecommons.async.commons.converters.json.DefaultObjectMapperSupplier; +import org.reactivecommons.async.commons.converters.json.ObjectMapperSupplier; +import org.reactivecommons.async.commons.ext.CustomReporter; +import org.reactivecommons.async.commons.ext.DefaultCustomReporter; +import org.reactivecommons.async.commons.reply.ReactiveReplyRouter; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.broker.BrokerProviderFactory; +import org.reactivecommons.async.starter.broker.DiscardProvider; +import org.reactivecommons.async.starter.config.health.ReactiveCommonsHealthConfig; +import org.reactivecommons.async.starter.props.GenericAsyncProps; +import org.reactivecommons.async.starter.props.GenericAsyncPropsDomain; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import reactor.core.publisher.Mono; + +import java.util.Map; + +import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; + +@Log +@Configuration +@RequiredArgsConstructor +@Import(ReactiveCommonsHealthConfig.class) +@ComponentScan("org.reactivecommons.async.starter.impl") +public class ReactiveCommonsConfig { + + @Bean + @SuppressWarnings({"rawtypes", "unchecked"}) + public ConnectionManager buildConnectionManager(ApplicationContext context) { + final Map props = context.getBeansOfType(GenericAsyncPropsDomain.class); + final Map providers = context.getBeansOfType(BrokerProviderFactory.class); + + ConnectionManager connectionManager = new ConnectionManager(); + props.forEach((beanName, domainProps) -> { + final GenericAsyncProps defaultDomainProps = domainProps.getProps(DEFAULT_DOMAIN); + domainProps.forEach((domain, asyncPropsObject) -> { + String domainName = (String) domain; + final GenericAsyncProps asyncProps = (GenericAsyncProps) asyncPropsObject; + if (asyncProps.isEnabled()) { + BrokerProviderFactory factory = providers.get(asyncProps.getBrokerType()); + if (!defaultDomainProps.isEnabled()) { + asyncProps.setUseDiscardNotifierPerDomain(true); + } + DiscardProvider discardProvider = factory.getDiscardProvider(defaultDomainProps); + BrokerProvider provider = factory.getProvider(domainName, asyncProps, discardProvider); + connectionManager.addDomain(domainName, provider); + } + }); + }); + return connectionManager; + } + + @Bean + @SuppressWarnings({"rawtypes", "unchecked"}) + public DomainHandlers buildHandlers(ApplicationContext context, + HandlerRegistry primaryRegistry, DefaultCommandHandler commandHandler) { + DomainHandlers handlers = new DomainHandlers(); + final Map registries = context.getBeansOfType(HandlerRegistry.class); + if (!registries.containsValue(primaryRegistry)) { + registries.put("primaryHandlerRegistry", primaryRegistry); + } + final Map props = context.getBeansOfType(GenericAsyncPropsDomain.class); + props.forEach((beanName, properties) -> properties.forEach((domain, asyncProps) -> { + String domainName = (String) domain; + HandlerResolver resolver = HandlerResolverBuilder.buildResolver(domainName, registries, commandHandler); + handlers.add(domainName, resolver); + })); + return handlers; + } + + @Bean + @ConditionalOnMissingBean + public BrokerConfig brokerConfig() { + return new BrokerConfig(); + } + + @Bean + @ConditionalOnMissingBean + public ObjectMapperSupplier objectMapperSupplier() { + return new DefaultObjectMapperSupplier(); + } + + @Bean + @ConditionalOnMissingBean + public CustomReporter reactiveCommonsCustomErrorReporter() { + return new DefaultCustomReporter(); + } + + @Bean + @ConditionalOnMissingBean + @SuppressWarnings("rawtypes") + public DefaultQueryHandler defaultHandler() { + return (DefaultQueryHandler) command -> + Mono.error(new RuntimeException("No Handler Registered")); + } + + @Bean + @ConditionalOnMissingBean + @SuppressWarnings("rawtypes") + public DefaultCommandHandler defaultCommandHandler() { + return message -> Mono.error(new RuntimeException("No Handler Registered")); + } + + @Bean + @ConditionalOnMissingBean(HandlerRegistry.class) + public HandlerRegistry defaultHandlerRegistry() { + return HandlerRegistry.register(); + } + + @Bean + @ConditionalOnMissingBean(ReactiveReplyRouter.class) + public ReactiveReplyRouter defaultReactiveReplyRouter() { + return new ReactiveReplyRouter(); + } + + @Bean + @ConditionalOnMissingBean(MeterRegistry.class) + public MeterRegistry defaultRabbitMeterRegistry() { + return new SimpleMeterRegistry(); + } + +} diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/health/ReactiveCommonsHealthConfig.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/health/ReactiveCommonsHealthConfig.java new file mode 100644 index 00000000..c649942d --- /dev/null +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/health/ReactiveCommonsHealthConfig.java @@ -0,0 +1,20 @@ +package org.reactivecommons.async.starter.config.health; + +import org.reactivecommons.async.starter.config.ConnectionManager; +import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnClass(AbstractReactiveHealthIndicator.class) +public class ReactiveCommonsHealthConfig { + + @Bean + @ConditionalOnProperty(prefix = "management.health.reactive-commons", name = "enabled", havingValue = "true", + matchIfMissing = true) + public ReactiveCommonsHealthIndicator reactiveCommonsHealthIndicator(ConnectionManager manager) { + return new ReactiveCommonsHealthIndicator(manager); + } +} diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/health/ReactiveCommonsHealthIndicator.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/health/ReactiveCommonsHealthIndicator.java new file mode 100644 index 00000000..2140b20d --- /dev/null +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/health/ReactiveCommonsHealthIndicator.java @@ -0,0 +1,36 @@ +package org.reactivecommons.async.starter.config.health; + +import lombok.AllArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.reactivecommons.async.starter.config.ConnectionManager; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Log4j2 +@AllArgsConstructor +public class ReactiveCommonsHealthIndicator extends AbstractReactiveHealthIndicator { + private final ConnectionManager manager; + + @Override + @SuppressWarnings("unchecked") + protected Mono doHealthCheck(Health.Builder builder) { + return Flux.fromIterable(manager.getProviders().values()) + .flatMap(BrokerProvider::healthCheck) + .reduceWith(Health::up, (health, status) -> reduceHealth((Health.Builder) health, (Health) status)) + .map(b -> ((Health.Builder) b).build()); + + } + + private Health.Builder reduceHealth(Health.Builder builder, Health status) { + String domain = status.getDetails().get("domain").toString(); + if (!status.getStatus().equals(Status.DOWN)) { + log.error("Broker of domain {} is down", domain); + return builder.down().withDetail(domain, status.getDetails()); + } + return builder.withDetail(domain, status.getDetails()); + } +} diff --git a/starters/shared/src/main/java/org/reactivecommons/async/starter/exceptions/InvalidConfigurationException.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/exceptions/InvalidConfigurationException.java similarity index 100% rename from starters/shared/src/main/java/org/reactivecommons/async/starter/exceptions/InvalidConfigurationException.java rename to starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/exceptions/InvalidConfigurationException.java diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/AbstractListenerConfig.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/AbstractListenerConfig.java new file mode 100644 index 00000000..3d6e5664 --- /dev/null +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/AbstractListenerConfig.java @@ -0,0 +1,16 @@ +package org.reactivecommons.async.starter.listeners; + +import org.reactivecommons.async.commons.HandlerResolver; +import org.reactivecommons.async.starter.config.ConnectionManager; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.config.DomainHandlers; + +public abstract class AbstractListenerConfig { + + public AbstractListenerConfig(ConnectionManager manager, DomainHandlers handlers) { + manager.forDomain((domain, provider) -> listen(domain, provider, handlers.get(domain))); + } + + @SuppressWarnings("rawtypes") + abstract void listen(String domain, BrokerProvider provider, HandlerResolver resolver); +} diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/CommandsListenerConfig.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/CommandsListenerConfig.java new file mode 100644 index 00000000..7690d131 --- /dev/null +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/CommandsListenerConfig.java @@ -0,0 +1,25 @@ +package org.reactivecommons.async.starter.listeners; + + +import org.reactivecommons.async.commons.HandlerResolver; +import org.reactivecommons.async.starter.config.ConnectionManager; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.config.DomainHandlers; +import org.reactivecommons.async.starter.config.ReactiveCommonsConfig; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import(ReactiveCommonsConfig.class) +public class CommandsListenerConfig extends AbstractListenerConfig { + + public CommandsListenerConfig(ConnectionManager manager, DomainHandlers handlers) { + super(manager, handlers); + } + + @SuppressWarnings("rawtypes") + @Override + void listen(String domain, BrokerProvider provider, HandlerResolver resolver) { + provider.listenCommands(resolver); + } +} diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/EventsListenerConfig.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/EventsListenerConfig.java new file mode 100644 index 00000000..be7d075e --- /dev/null +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/EventsListenerConfig.java @@ -0,0 +1,25 @@ +package org.reactivecommons.async.starter.listeners; + + +import org.reactivecommons.async.commons.HandlerResolver; +import org.reactivecommons.async.starter.config.ConnectionManager; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.config.DomainHandlers; +import org.reactivecommons.async.starter.config.ReactiveCommonsConfig; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import(ReactiveCommonsConfig.class) +public class EventsListenerConfig extends AbstractListenerConfig { + + public EventsListenerConfig(ConnectionManager manager, DomainHandlers handlers) { + super(manager, handlers); + } + + @SuppressWarnings("rawtypes") + @Override + void listen(String domain, BrokerProvider provider, HandlerResolver resolver) { + provider.listenDomainEvents(resolver); + } +} diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/NotificationEventsListenerConfig.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/NotificationEventsListenerConfig.java new file mode 100644 index 00000000..647f7994 --- /dev/null +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/NotificationEventsListenerConfig.java @@ -0,0 +1,25 @@ +package org.reactivecommons.async.starter.listeners; + + +import org.reactivecommons.async.commons.HandlerResolver; +import org.reactivecommons.async.starter.config.ConnectionManager; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.config.DomainHandlers; +import org.reactivecommons.async.starter.config.ReactiveCommonsConfig; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import(ReactiveCommonsConfig.class) +public class NotificationEventsListenerConfig extends AbstractListenerConfig { + + public NotificationEventsListenerConfig(ConnectionManager manager, DomainHandlers handlers) { + super(manager, handlers); + } + + @SuppressWarnings("rawtypes") + @Override + void listen(String domain, BrokerProvider provider, HandlerResolver resolver) { + provider.listenNotificationEvents(resolver); + } +} diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/QueriesListenerConfig.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/QueriesListenerConfig.java new file mode 100644 index 00000000..9710c770 --- /dev/null +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/QueriesListenerConfig.java @@ -0,0 +1,25 @@ +package org.reactivecommons.async.starter.listeners; + + +import org.reactivecommons.async.commons.HandlerResolver; +import org.reactivecommons.async.starter.config.ConnectionManager; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.config.DomainHandlers; +import org.reactivecommons.async.starter.config.ReactiveCommonsConfig; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import(ReactiveCommonsConfig.class) +public class QueriesListenerConfig extends AbstractListenerConfig { + + public QueriesListenerConfig(ConnectionManager manager, DomainHandlers handlers) { + super(manager, handlers); + } + + @SuppressWarnings("rawtypes") + @Override + void listen(String domain, BrokerProvider provider, HandlerResolver resolver) { + provider.listenQueries(resolver); + } +} diff --git a/starters/shared/src/main/java/org/reactivecommons/async/starter/GenericAsyncProps.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncProps.java similarity index 67% rename from starters/shared/src/main/java/org/reactivecommons/async/starter/GenericAsyncProps.java rename to starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncProps.java index f2b56b92..1e3432d4 100644 --- a/starters/shared/src/main/java/org/reactivecommons/async/starter/GenericAsyncProps.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncProps.java @@ -1,4 +1,4 @@ -package org.reactivecommons.async.starter; +package org.reactivecommons.async.starter.props; import lombok.AllArgsConstructor; import lombok.Getter; @@ -18,4 +18,10 @@ public abstract class GenericAsyncProps

{ abstract public void setConnectionProperties(P properties); abstract public P getConnectionProperties(); + + abstract public String getBrokerType(); + + abstract public boolean isEnabled(); + + abstract public void setUseDiscardNotifierPerDomain(boolean enabled); } \ No newline at end of file diff --git a/starters/shared/src/main/java/org/reactivecommons/async/starter/GenericAsyncPropsDomain.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomain.java similarity index 99% rename from starters/shared/src/main/java/org/reactivecommons/async/starter/GenericAsyncPropsDomain.java rename to starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomain.java index 411abad8..bd43a327 100644 --- a/starters/shared/src/main/java/org/reactivecommons/async/starter/GenericAsyncPropsDomain.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomain.java @@ -1,4 +1,4 @@ -package org.reactivecommons.async.starter; +package org.reactivecommons.async.starter.props; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; diff --git a/starters/shared/src/main/java/org/reactivecommons/async/starter/GenericAsyncPropsDomainProperties.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomainProperties.java similarity index 96% rename from starters/shared/src/main/java/org/reactivecommons/async/starter/GenericAsyncPropsDomainProperties.java rename to starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomainProperties.java index f04336c4..5e5d8498 100644 --- a/starters/shared/src/main/java/org/reactivecommons/async/starter/GenericAsyncPropsDomainProperties.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomainProperties.java @@ -1,4 +1,4 @@ -package org.reactivecommons.async.starter; +package org.reactivecommons.async.starter.props; import lombok.Getter; import lombok.Setter; diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/DirectAsyncGatewayConfig.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/DirectAsyncGatewayConfig.java new file mode 100644 index 00000000..df6efea3 --- /dev/null +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/DirectAsyncGatewayConfig.java @@ -0,0 +1,29 @@ +package org.reactivecommons.async.starter.senders; + +import lombok.RequiredArgsConstructor; +import lombok.extern.java.Log; +import org.reactivecommons.async.api.DirectAsyncGateway; +import org.reactivecommons.async.starter.config.ConnectionManager; +import org.reactivecommons.async.starter.config.DomainHandlers; +import org.reactivecommons.async.starter.config.ReactiveCommonsConfig; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@Log +@Configuration +@RequiredArgsConstructor +@Import(ReactiveCommonsConfig.class) +public class DirectAsyncGatewayConfig { + + @Bean + public DirectAsyncGateway genericDirectAsyncGateway(ConnectionManager manager, DomainHandlers handlers) { + ConcurrentMap directAsyncGateways = new ConcurrentHashMap<>(); + manager.forDomain((domain, provider) -> directAsyncGateways.put(domain, + provider.getDirectAsyncGateway(handlers.get(domain)))); + return new GenericDirectAsyncGateway(directAsyncGateways); + } +} diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/EventBusConfig.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/EventBusConfig.java new file mode 100644 index 00000000..b2b69e0b --- /dev/null +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/EventBusConfig.java @@ -0,0 +1,23 @@ +package org.reactivecommons.async.starter.senders; + +import org.reactivecommons.api.domain.DomainEventBus; +import org.reactivecommons.async.starter.config.ConnectionManager; +import org.reactivecommons.async.starter.config.ReactiveCommonsConfig; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@Configuration +@Import(ReactiveCommonsConfig.class) +public class EventBusConfig { + + @Bean + public DomainEventBus genericDomainEventBus(ConnectionManager manager) { + ConcurrentMap domainEventBuses = new ConcurrentHashMap<>(); + manager.forDomain((domain, provider) -> domainEventBuses.put(domain, provider.getDomainBus())); + return new GenericDomainEventBus(domainEventBuses); + } +} diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/GenericDirectAsyncGateway.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/GenericDirectAsyncGateway.java new file mode 100644 index 00000000..67acbf81 --- /dev/null +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/GenericDirectAsyncGateway.java @@ -0,0 +1,83 @@ +package org.reactivecommons.async.starter.senders; + +import io.cloudevents.CloudEvent; +import lombok.AllArgsConstructor; +import org.reactivecommons.api.domain.Command; +import org.reactivecommons.async.api.AsyncQuery; +import org.reactivecommons.async.api.DirectAsyncGateway; +import org.reactivecommons.async.api.From; +import reactor.core.publisher.Mono; + +import java.util.concurrent.ConcurrentMap; + +import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; + +@AllArgsConstructor +public class GenericDirectAsyncGateway implements DirectAsyncGateway { + private final ConcurrentMap directAsyncGateways; + + @Override + public Mono sendCommand(Command command, String targetName) { + return sendCommand(command, targetName, DEFAULT_DOMAIN); + } + + @Override + public Mono sendCommand(Command command, String targetName, long delayMillis) { + return sendCommand(command, targetName, delayMillis, DEFAULT_DOMAIN); + } + + @Override + public Mono sendCommand(Command command, String targetName, String domain) { + return directAsyncGateways.get(domain).sendCommand(command, targetName); + } + + @Override + public Mono sendCommand(Command command, String targetName, long delayMillis, String domain) { + return directAsyncGateways.get(domain).sendCommand(command, targetName, delayMillis); + } + + @Override + public Mono sendCommand(CloudEvent command, String targetName) { + return sendCommand(command, targetName, DEFAULT_DOMAIN); + } + + @Override + public Mono sendCommand(CloudEvent command, String targetName, long delayMillis) { + return sendCommand(command, targetName, delayMillis, DEFAULT_DOMAIN); + } + + @Override + public Mono sendCommand(CloudEvent command, String targetName, String domain) { + return directAsyncGateways.get(domain).sendCommand(command, targetName); + } + + @Override + public Mono sendCommand(CloudEvent command, String targetName, long delayMillis, String domain) { + return directAsyncGateways.get(domain).sendCommand(command, targetName, delayMillis); + } + + @Override + public Mono requestReply(AsyncQuery query, String targetName, Class type) { + return requestReply(query, targetName, type, DEFAULT_DOMAIN); + } + + @Override + public Mono requestReply(AsyncQuery query, String targetName, Class type, String domain) { + return directAsyncGateways.get(domain).requestReply(query, targetName, type); + } + + @Override + public Mono requestReply(CloudEvent query, String targetName, Class type) { + return requestReply(query, targetName, type, DEFAULT_DOMAIN); + } + + @Override + public Mono requestReply(CloudEvent query, String targetName, Class type, String domain) { + return directAsyncGateways.get(domain).requestReply(query, targetName, type); + } + + @Override + public Mono reply(T response, From from) { + return directAsyncGateways.get(DEFAULT_DOMAIN).reply(response, from); + } +} diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/GenericDomainEventBus.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/GenericDomainEventBus.java new file mode 100644 index 00000000..7b659885 --- /dev/null +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/GenericDomainEventBus.java @@ -0,0 +1,36 @@ +package org.reactivecommons.async.starter.senders; + +import io.cloudevents.CloudEvent; +import lombok.AllArgsConstructor; +import org.reactivecommons.api.domain.DomainEvent; +import org.reactivecommons.api.domain.DomainEventBus; +import org.reactivestreams.Publisher; + +import java.util.concurrent.ConcurrentMap; + +import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; + +@AllArgsConstructor +public class GenericDomainEventBus implements DomainEventBus { + private final ConcurrentMap domainEventBuses; + + @Override + public Publisher emit(DomainEvent event) { + return emit(DEFAULT_DOMAIN, event); + } + + @Override + public Publisher emit(String domain, DomainEvent event) { + return domainEventBuses.get(domain).emit(event); + } + + @Override + public Publisher emit(CloudEvent event) { + return emit(DEFAULT_DOMAIN, event); + } + + @Override + public Publisher emit(String domain, CloudEvent event) { + return domainEventBuses.get(domain).emit(event); + } +} diff --git a/starters/async-kafka-starter/async-kafka-starter.gradle b/starters/async-kafka-starter/async-kafka-starter.gradle index 41ba255e..49439d3c 100644 --- a/starters/async-kafka-starter/async-kafka-starter.gradle +++ b/starters/async-kafka-starter/async-kafka-starter.gradle @@ -5,7 +5,8 @@ ext { dependencies { api project(':async-kafka') - api project(':shared-starter') + api project(':async-commons-starter') + implementation 'org.apache.kafka:kafka-clients' compileOnly 'org.springframework.boot:spring-boot-starter' compileOnly 'org.springframework.boot:spring-boot-starter-actuator' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaBrokerProvider.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaBrokerProvider.java new file mode 100644 index 00000000..434b079c --- /dev/null +++ b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaBrokerProvider.java @@ -0,0 +1,108 @@ +package org.reactivecommons.async.kafka; + +import io.micrometer.core.instrument.MeterRegistry; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.reactivecommons.api.domain.DomainEventBus; +import org.reactivecommons.async.api.DirectAsyncGateway; +import org.reactivecommons.async.commons.DiscardNotifier; +import org.reactivecommons.async.commons.HandlerResolver; +import org.reactivecommons.async.commons.ext.CustomReporter; +import org.reactivecommons.async.commons.reply.ReactiveReplyRouter; +import org.reactivecommons.async.kafka.communications.ReactiveMessageListener; +import org.reactivecommons.async.kafka.communications.ReactiveMessageSender; +import org.reactivecommons.async.kafka.communications.topology.KafkaCustomizations; +import org.reactivecommons.async.kafka.communications.topology.TopologyCreator; +import org.reactivecommons.async.kafka.config.props.AsyncKafkaProps; +import org.reactivecommons.async.kafka.converters.json.KafkaJacksonMessageConverter; +import org.reactivecommons.async.kafka.health.KafkaReactiveHealthIndicator; +import org.reactivecommons.async.kafka.listeners.ApplicationEventListener; +import org.reactivecommons.async.kafka.listeners.ApplicationNotificationsListener; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.ssl.SslBundles; +import reactor.core.publisher.Mono; + +@Getter +@AllArgsConstructor +public class KafkaBrokerProvider implements BrokerProvider { + private final String domain; + private final AsyncKafkaProps props; + private final ReactiveReplyRouter router; + private final KafkaJacksonMessageConverter converter; + private final MeterRegistry meterRegistry; + private final CustomReporter errorReporter; + private final KafkaReactiveHealthIndicator healthIndicator; + private final ReactiveMessageListener receiver; + private final ReactiveMessageSender sender; + private final DiscardNotifier discardNotifier; + private final TopologyCreator topologyCreator; + private final KafkaCustomizations customizations; + private final SslBundles sslBundles; + + @Override + public DomainEventBus getDomainBus() { + return new KafkaDomainEventBus(sender); + } + + @Override + public DirectAsyncGateway getDirectAsyncGateway(HandlerResolver resolver) { + return new KafkaDirectAsyncGateway(); + } + + @Override + public void listenDomainEvents(HandlerResolver resolver) { + if (!props.getDomain().isIgnoreThisListener()) { + if (!resolver.getEventListeners().isEmpty()) { + ApplicationEventListener eventListener = new ApplicationEventListener(receiver, + resolver, + converter, + props.getWithDLQRetry(), + props.getCreateTopology(), + props.getMaxRetries(), + props.getRetryDelay(), + discardNotifier, + errorReporter, + props.getAppName()); + eventListener.startListener(topologyCreator); + } + } + } + + @Override + public void listenNotificationEvents(HandlerResolver resolver) { + if (!resolver.getNotificationListeners().isEmpty()) { + ApplicationNotificationsListener notificationEventListener = new ApplicationNotificationsListener(receiver, + resolver, + converter, + props.getWithDLQRetry(), + props.getCreateTopology(), + props.getMaxRetries(), + props.getRetryDelay(), + discardNotifier, + errorReporter, + props.getAppName()); + notificationEventListener.startListener(topologyCreator); + } + } + + @Override + public void listenCommands(HandlerResolver resolver) { + + } + + @Override + public void listenQueries(HandlerResolver resolver) { + + } + + @Override + public void listenReplies(HandlerResolver resolver) { + + } + + @Override + public Mono healthCheck() { + return healthIndicator.health(); + } +} diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaBrokerProviderFactory.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaBrokerProviderFactory.java new file mode 100644 index 00000000..0c06949a --- /dev/null +++ b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaBrokerProviderFactory.java @@ -0,0 +1,59 @@ +package org.reactivecommons.async.kafka; + +import io.micrometer.core.instrument.MeterRegistry; +import lombok.AllArgsConstructor; +import org.apache.kafka.clients.admin.AdminClient; +import org.reactivecommons.async.commons.DiscardNotifier; +import org.reactivecommons.async.commons.ext.CustomReporter; +import org.reactivecommons.async.commons.reply.ReactiveReplyRouter; +import org.reactivecommons.async.kafka.communications.ReactiveMessageListener; +import org.reactivecommons.async.kafka.communications.ReactiveMessageSender; +import org.reactivecommons.async.kafka.communications.topology.KafkaCustomizations; +import org.reactivecommons.async.kafka.communications.topology.TopologyCreator; +import org.reactivecommons.async.kafka.config.props.AsyncKafkaProps; +import org.reactivecommons.async.kafka.converters.json.KafkaJacksonMessageConverter; +import org.reactivecommons.async.kafka.health.KafkaReactiveHealthIndicator; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.broker.BrokerProviderFactory; +import org.reactivecommons.async.starter.broker.DiscardProvider; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.stereotype.Service; + +@Service("kafka") +@AllArgsConstructor +public class KafkaBrokerProviderFactory implements BrokerProviderFactory { + private final ReactiveReplyRouter router; + private final KafkaJacksonMessageConverter converter; + private final MeterRegistry meterRegistry; + private final CustomReporter errorReporter; + private final KafkaCustomizations customizations; + private final SslBundles sslBundles; + + @Override + public String getBrokerType() { + return "kafka"; + } + + @Override + public DiscardProvider getDiscardProvider(AsyncKafkaProps props) { + return new KafkaDiscardProvider(props, converter, customizations, sslBundles); + } + + @Override + public BrokerProvider getProvider(String domain, AsyncKafkaProps props, DiscardProvider discardProvider) { + TopologyCreator creator = KafkaSetupUtils.createTopologyCreator(props, customizations, sslBundles); + ReactiveMessageSender sender = KafkaSetupUtils.createMessageSender(props, converter, creator, sslBundles); + ReactiveMessageListener listener = KafkaSetupUtils.createMessageListener(props, sslBundles); + AdminClient adminClient = AdminClient.create(props.getConnectionProperties().buildAdminProperties(sslBundles)); + KafkaReactiveHealthIndicator healthIndicator = new KafkaReactiveHealthIndicator(domain, adminClient); + DiscardNotifier discardNotifier; + if (props.isUseDiscardNotifierPerDomain()) { + discardNotifier = KafkaSetupUtils.createDiscardNotifier(sender, converter); + } else { + discardNotifier = discardProvider.get(); + } + return new KafkaBrokerProvider(domain, props, router, converter, meterRegistry, errorReporter, + healthIndicator, listener, sender, discardNotifier, creator, customizations, sslBundles); + } + +} diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaDiscardProvider.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaDiscardProvider.java new file mode 100644 index 00000000..11307a8b --- /dev/null +++ b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaDiscardProvider.java @@ -0,0 +1,34 @@ +package org.reactivecommons.async.kafka; + +import lombok.AllArgsConstructor; +import org.reactivecommons.async.commons.DiscardNotifier; +import org.reactivecommons.async.commons.converters.MessageConverter; +import org.reactivecommons.async.kafka.communications.ReactiveMessageSender; +import org.reactivecommons.async.kafka.communications.topology.KafkaCustomizations; +import org.reactivecommons.async.kafka.communications.topology.TopologyCreator; +import org.reactivecommons.async.kafka.config.props.AsyncKafkaProps; +import org.reactivecommons.async.starter.broker.DiscardProvider; +import org.springframework.boot.ssl.SslBundles; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@AllArgsConstructor +public class KafkaDiscardProvider implements DiscardProvider { + private final AsyncKafkaProps props; + private final MessageConverter converter; + private final KafkaCustomizations customizations; + private final SslBundles sslBundles; + private final Map discardNotifier = new ConcurrentHashMap<>(); + + @Override + public DiscardNotifier get() { + return discardNotifier.computeIfAbsent(true, this::buildDiscardNotifier); + } + + private DiscardNotifier buildDiscardNotifier(boolean ignored) { + TopologyCreator creator = KafkaSetupUtils.createTopologyCreator(props, customizations, sslBundles); + ReactiveMessageSender sender = KafkaSetupUtils.createMessageSender(props, converter, creator, sslBundles); + return KafkaSetupUtils.createDiscardNotifier(sender, converter); + } +} diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaSetupUtils.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaSetupUtils.java new file mode 100644 index 00000000..42fe7fd8 --- /dev/null +++ b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaSetupUtils.java @@ -0,0 +1,86 @@ +package org.reactivecommons.async.kafka; + +import lombok.experimental.UtilityClass; +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.common.serialization.ByteArrayDeserializer; +import org.apache.kafka.common.serialization.ByteArraySerializer; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; +import org.reactivecommons.async.commons.DLQDiscardNotifier; +import org.reactivecommons.async.commons.DiscardNotifier; +import org.reactivecommons.async.commons.converters.MessageConverter; +import org.reactivecommons.async.kafka.communications.ReactiveMessageListener; +import org.reactivecommons.async.kafka.communications.ReactiveMessageSender; +import org.reactivecommons.async.kafka.communications.topology.KafkaCustomizations; +import org.reactivecommons.async.kafka.communications.topology.TopologyCreator; +import org.reactivecommons.async.kafka.config.KafkaProperties; +import org.reactivecommons.async.kafka.config.props.AsyncKafkaProps; +import org.springframework.boot.ssl.SslBundles; +import reactor.kafka.receiver.ReceiverOptions; +import reactor.kafka.sender.KafkaSender; +import reactor.kafka.sender.SenderOptions; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +@UtilityClass +public class KafkaSetupUtils { + + public static DiscardNotifier createDiscardNotifier(ReactiveMessageSender sender, MessageConverter converter) { + return new DLQDiscardNotifier(new KafkaDomainEventBus(sender), converter); + } + + + public static ReactiveMessageSender createMessageSender(AsyncKafkaProps config, + MessageConverter converter, + TopologyCreator topologyCreator, + SslBundles sslBundles) { + KafkaProperties props = config.getConnectionProperties(); + props.setClientId(config.getAppName()); // CLIENT_ID_CONFIG + props.getProducer().setKeySerializer(StringSerializer.class); // KEY_SERIALIZER_CLASS_CONFIG; + props.getProducer().setValueSerializer(ByteArraySerializer.class); // VALUE_SERIALIZER_CLASS_CONFIG + SenderOptions senderOptions = SenderOptions.create(props.buildProducerProperties(sslBundles)); + KafkaSender kafkaSender = KafkaSender.create(senderOptions); + return new ReactiveMessageSender(kafkaSender, converter, topologyCreator); + } + + // Receiver + + public static ReactiveMessageListener createMessageListener(AsyncKafkaProps config, SslBundles sslBundles) { + KafkaProperties props = config.getConnectionProperties(); + props.getConsumer().setKeyDeserializer(StringDeserializer.class); // KEY_DESERIALIZER_CLASS_CONFIG + props.getConsumer().setValueDeserializer(ByteArrayDeserializer.class); // VALUE_DESERIALIZER_CLASS_CONFIG + ReceiverOptions receiverOptions = ReceiverOptions.create(props.buildConsumerProperties(sslBundles)); + return new ReactiveMessageListener(receiverOptions); + } + + // Shared + public static TopologyCreator createTopologyCreator(AsyncKafkaProps config, KafkaCustomizations customizations, + SslBundles sslBundles) { + AdminClient adminClient = AdminClient.create(config.getConnectionProperties().buildAdminProperties(sslBundles)); + return new TopologyCreator(adminClient, customizations, config.getCheckExistingTopics()); + } + + // Utilities + + public static KafkaProperties readPropsFromDotEnv(Path path) throws IOException { + String env = Files.readString(path); + String[] split = env.split("\n"); + KafkaProperties props = new KafkaProperties(); + Map properties = props.getProperties(); + for (String s : split) { + if (s.startsWith("#")) { + continue; + } + String[] split1 = s.split("=", 2); + properties.put(split1[0], split1[1]); + } + return props; + } + + public static String jassConfig(String username, String password) { + return String.format("org.apache.kafka.common.security.plain.PlainLoginModule required username=\"%s\" password=\"%s\";", username, password); + } +} diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/ConnectionManager.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/ConnectionManager.java deleted file mode 100644 index 83d0425b..00000000 --- a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/ConnectionManager.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.reactivecommons.async.kafka.config; - -import lombok.Builder; -import lombok.Getter; -import lombok.Setter; -import org.reactivecommons.async.commons.DiscardNotifier; -import org.reactivecommons.async.kafka.communications.ReactiveMessageListener; -import org.reactivecommons.async.kafka.communications.ReactiveMessageSender; -import org.reactivecommons.async.kafka.communications.topology.TopologyCreator; - -import java.util.Map; -import java.util.TreeMap; -import java.util.function.BiConsumer; - -public class ConnectionManager { - private final Map connections = new TreeMap<>(); - - @Builder - @Getter - public static class DomainConnections { - private final ReactiveMessageListener listener; - private final ReactiveMessageSender sender; - private final TopologyCreator topologyCreator; - @Setter - private DiscardNotifier discardNotifier; - } - - public void forSender(BiConsumer consumer) { - connections.forEach((key, conn) -> consumer.accept(key, conn.getSender())); - } - - public void forListener(BiConsumer consumer) { - connections.forEach((key, conn) -> consumer.accept(key, conn.getListener())); - } - - public void setDiscardNotifier(String domain, DiscardNotifier discardNotifier) { - getChecked(domain).setDiscardNotifier(discardNotifier); - } - - public ConnectionManager addDomain(String domain, ReactiveMessageListener listener, ReactiveMessageSender sender, - TopologyCreator topologyCreator) { - connections.put(domain, DomainConnections.builder() - .listener(listener) - .sender(sender) - .topologyCreator(topologyCreator) - .build()); - return this; - } - - public ReactiveMessageSender getSender(String domain) { - return getChecked(domain).getSender(); - } - - public ReactiveMessageListener getListener(String domain) { - return getChecked(domain).getListener(); - } - - private DomainConnections getChecked(String domain) { - DomainConnections domainConnections = connections.get(domain); - if (domainConnections == null) { - throw new RuntimeException("You are trying to use the domain " + domain - + " but this connection is not defined"); - } - return domainConnections; - } - - public DiscardNotifier getDiscardNotifier(String domain) { - return getChecked(domain).getDiscardNotifier(); - } - - public TopologyCreator getTopologyCreator(String domain) { - return getChecked(domain).getTopologyCreator(); - } - - public Map getProviders() { - return connections; - } -} diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/DomainHandlers.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/DomainHandlers.java deleted file mode 100644 index 002a7605..00000000 --- a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/DomainHandlers.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.reactivecommons.async.kafka.config; - -import org.reactivecommons.async.commons.HandlerResolver; - -import java.util.Map; -import java.util.TreeMap; - -public class DomainHandlers { - private final Map handlers = new TreeMap<>(); - - public void add(String domain, HandlerResolver resolver) { - this.handlers.put(domain, resolver); - } - - public HandlerResolver get(String domain) { - HandlerResolver handlerResolver = handlers.get(domain); - if (handlerResolver == null) { - throw new RuntimeException("You are trying to use the domain " + domain - + " but this connection is not defined"); - } - return handlerResolver; - } -} diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/RCKafkaConfig.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/RCKafkaConfig.java deleted file mode 100644 index 950cb497..00000000 --- a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/RCKafkaConfig.java +++ /dev/null @@ -1,179 +0,0 @@ -package org.reactivecommons.async.kafka.config; - -import org.apache.kafka.clients.admin.AdminClient; -import org.apache.kafka.common.serialization.ByteArrayDeserializer; -import org.apache.kafka.common.serialization.ByteArraySerializer; -import org.apache.kafka.common.serialization.StringDeserializer; -import org.apache.kafka.common.serialization.StringSerializer; -import org.reactivecommons.api.domain.DomainEventBus; -import org.reactivecommons.async.api.DefaultCommandHandler; -import org.reactivecommons.async.api.HandlerRegistry; -import org.reactivecommons.async.commons.DLQDiscardNotifier; -import org.reactivecommons.async.commons.DiscardNotifier; -import org.reactivecommons.async.commons.HandlerResolver; -import org.reactivecommons.async.commons.HandlerResolverBuilder; -import org.reactivecommons.async.commons.converters.MessageConverter; -import org.reactivecommons.async.commons.converters.json.DefaultObjectMapperSupplier; -import org.reactivecommons.async.commons.converters.json.ObjectMapperSupplier; -import org.reactivecommons.async.commons.ext.CustomReporter; -import org.reactivecommons.async.commons.ext.DefaultCustomReporter; -import org.reactivecommons.async.kafka.KafkaDomainEventBus; -import org.reactivecommons.async.kafka.communications.ReactiveMessageListener; -import org.reactivecommons.async.kafka.communications.ReactiveMessageSender; -import org.reactivecommons.async.kafka.communications.topology.KafkaCustomizations; -import org.reactivecommons.async.kafka.communications.topology.TopologyCreator; -import org.reactivecommons.async.kafka.config.props.AsyncKafkaProps; -import org.reactivecommons.async.kafka.config.props.AsyncKafkaPropsDomain; -import org.reactivecommons.async.kafka.config.props.AsyncKafkaPropsDomainProperties; -import org.reactivecommons.async.kafka.converters.json.KafkaJacksonMessageConverter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.ssl.SslBundles; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import reactor.core.publisher.Mono; -import reactor.kafka.receiver.ReceiverOptions; -import reactor.kafka.sender.KafkaSender; -import reactor.kafka.sender.SenderOptions; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Map; - -import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; - -@Configuration -@EnableConfigurationProperties({KafkaPropertiesAutoConfig.class, AsyncKafkaPropsDomainProperties.class}) -@Import(AsyncKafkaPropsDomain.class) // RabbitHealthConfig.class -public class RCKafkaConfig { - - @Bean - public ConnectionManager kafkaConnectionManager(AsyncKafkaPropsDomain props, - MessageConverter converter, - KafkaCustomizations customizations, - SslBundles sslBundles) { - ConnectionManager connectionManager = new ConnectionManager(); - props.forEach((domain, properties) -> { - TopologyCreator creator = createTopologyCreator(properties, customizations, sslBundles); - ReactiveMessageSender sender = createMessageSender(properties, converter, creator, sslBundles); - ReactiveMessageListener listener = createMessageListener(properties, sslBundles); - connectionManager.addDomain(domain, listener, sender, creator); - - ReactiveMessageSender appDomainSender = connectionManager.getSender(domain); - DomainEventBus appDomainEventBus = new KafkaDomainEventBus(appDomainSender); - DiscardNotifier notifier = new DLQDiscardNotifier(appDomainEventBus, converter); - connectionManager.setDiscardNotifier(domain, notifier); - }); - return connectionManager; - } - - // Sender - @Bean - @ConditionalOnMissingBean(DomainEventBus.class) - public DomainEventBus kafkaDomainEventBus(ConnectionManager manager) { - return new KafkaDomainEventBus(manager.getSender(DEFAULT_DOMAIN)); - } - - private static ReactiveMessageSender createMessageSender(AsyncKafkaProps config, - MessageConverter converter, - TopologyCreator topologyCreator, - SslBundles sslBundles) { - KafkaProperties props = config.getConnectionProperties(); - props.setClientId(config.getAppName()); // CLIENT_ID_CONFIG - props.getProducer().setKeySerializer(StringSerializer.class); // KEY_SERIALIZER_CLASS_CONFIG; - props.getProducer().setValueSerializer(ByteArraySerializer.class); // VALUE_SERIALIZER_CLASS_CONFIG - SenderOptions senderOptions = SenderOptions.create(props.buildProducerProperties(sslBundles)); - KafkaSender kafkaSender = KafkaSender.create(senderOptions); - return new ReactiveMessageSender(kafkaSender, converter, topologyCreator); - } - - // Receiver - - private static ReactiveMessageListener createMessageListener(AsyncKafkaProps config, SslBundles sslBundles) { - KafkaProperties props = config.getConnectionProperties(); - props.getConsumer().setKeyDeserializer(StringDeserializer.class); // KEY_DESERIALIZER_CLASS_CONFIG - props.getConsumer().setValueDeserializer(ByteArrayDeserializer.class); // VALUE_DESERIALIZER_CLASS_CONFIG - ReceiverOptions receiverOptions = ReceiverOptions.create(props.buildConsumerProperties(sslBundles)); - return new ReactiveMessageListener(receiverOptions); - } - - // Shared - private static TopologyCreator createTopologyCreator(AsyncKafkaProps config, KafkaCustomizations customizations, - SslBundles sslBundles) { - AdminClient adminClient = AdminClient.create(config.getConnectionProperties().buildAdminProperties(sslBundles)); - return new TopologyCreator(adminClient, customizations, config.getCheckExistingTopics()); - } - - @Bean - @ConditionalOnMissingBean(KafkaCustomizations.class) - public KafkaCustomizations defaultKafkaCustomizations() { - return new KafkaCustomizations(); - } - - @Bean - @ConditionalOnMissingBean(MessageConverter.class) - public MessageConverter kafkaJacksonMessageConverter(ObjectMapperSupplier objectMapperSupplier) { - return new KafkaJacksonMessageConverter(objectMapperSupplier.get()); - } - - @Bean - @ConditionalOnMissingBean(DiscardNotifier.class) - public DiscardNotifier kafkaDiscardNotifier(DomainEventBus domainEventBus, MessageConverter messageConverter) { - return new DLQDiscardNotifier(domainEventBus, messageConverter); - } - - @Bean - @ConditionalOnMissingBean(ObjectMapperSupplier.class) - public ObjectMapperSupplier defaultObjectMapperSupplier() { - return new DefaultObjectMapperSupplier(); - } - - @Bean - @ConditionalOnMissingBean(CustomReporter.class) - public CustomReporter defaultKafkaCustomReporter() { - return new DefaultCustomReporter(); - } - - @Bean - @ConditionalOnMissingBean(AsyncKafkaPropsDomain.KafkaSecretFiller.class) - public AsyncKafkaPropsDomain.KafkaSecretFiller defaultKafkaSecretFiller() { - return (ignoredDomain, ignoredProps) -> { - }; - } - - @Bean - @ConditionalOnMissingBean(KafkaProperties.class) - public KafkaProperties defaultKafkaProperties(KafkaPropertiesAutoConfig properties, ObjectMapperSupplier supplier) { - return supplier.get().convertValue(properties, KafkaProperties.class); - } - - @Bean - @ConditionalOnMissingBean(DefaultCommandHandler.class) - public DefaultCommandHandler defaultCommandHandler() { - return command -> Mono.empty(); - } - - // Utilities - - public static KafkaProperties readPropsFromDotEnv(Path path) throws IOException { - String env = Files.readString(path); - String[] split = env.split("\n"); - KafkaProperties props = new KafkaProperties(); - Map properties = props.getProperties(); - for (String s : split) { - if (s.startsWith("#")) { - continue; - } - String[] split1 = s.split("=", 2); - properties.put(split1[0], split1[1]); - } - return props; - } - - public static String jassConfig(String username, String password) { - return String.format("org.apache.kafka.common.security.plain.PlainLoginModule required username=\"%s\" password=\"%s\";", username, password); - } -} diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/RCKafkaEventListenerConfig.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/RCKafkaEventListenerConfig.java deleted file mode 100644 index 47ee16c2..00000000 --- a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/RCKafkaEventListenerConfig.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.reactivecommons.async.kafka.config; - -import org.reactivecommons.async.commons.converters.MessageConverter; -import org.reactivecommons.async.commons.ext.CustomReporter; -import org.reactivecommons.async.kafka.config.props.AsyncKafkaProps; -import org.reactivecommons.async.kafka.config.props.AsyncKafkaPropsDomain; -import org.reactivecommons.async.kafka.listeners.ApplicationEventListener; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import java.util.concurrent.atomic.AtomicReference; - -import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; - -@Configuration -public class RCKafkaEventListenerConfig { - - @Bean - public ApplicationEventListener kafkaEventListener(ConnectionManager manager, - DomainHandlers handlers, - AsyncKafkaPropsDomain asyncPropsDomain, - MessageConverter messageConverter, - CustomReporter customReporter) { - AtomicReference external = new AtomicReference<>(); - manager.forListener((domain, receiver) -> { - AsyncKafkaProps asyncProps = asyncPropsDomain.getProps(domain); - if (!asyncProps.getDomain().isIgnoreThisListener()) { - ApplicationEventListener eventListener = new ApplicationEventListener(receiver, - handlers.get(domain), - messageConverter, - asyncProps.getWithDLQRetry(), - asyncProps.getCreateTopology(), - asyncProps.getMaxRetries(), - asyncProps.getRetryDelay(), - manager.getDiscardNotifier(domain), - customReporter, - asyncProps.getAppName()); - if (DEFAULT_DOMAIN.equals(domain)) { - external.set(eventListener); - } - - eventListener.startListener(manager.getTopologyCreator(domain)); - } - }); - - return external.get(); - } - -} diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/RCKafkaHandlersConfiguration.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/RCKafkaHandlersConfiguration.java deleted file mode 100644 index 7eecd274..00000000 --- a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/RCKafkaHandlersConfiguration.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.reactivecommons.async.kafka.config; - - -import org.reactivecommons.async.api.DefaultCommandHandler; -import org.reactivecommons.async.api.HandlerRegistry; -import org.reactivecommons.async.commons.HandlerResolver; -import org.reactivecommons.async.commons.HandlerResolverBuilder; -import org.reactivecommons.async.kafka.config.props.AsyncKafkaPropsDomain; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import java.util.Map; - -@Configuration -public class RCKafkaHandlersConfiguration { - - @Bean - public DomainHandlers buildHandlers(AsyncKafkaPropsDomain props, ApplicationContext context, - HandlerRegistry primaryRegistry, DefaultCommandHandler commandHandler) { - DomainHandlers handlers = new DomainHandlers(); - final Map registries = context.getBeansOfType(HandlerRegistry.class); - if (!registries.containsValue(primaryRegistry)) { - registries.put("primaryHandlerRegistry", primaryRegistry); - } - props.forEach((domain, properties) -> { - HandlerResolver resolver = HandlerResolverBuilder.buildResolver(domain, registries, commandHandler); - handlers.add(domain, resolver); - }); - return handlers; - } -} diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/RCKafkaNotificationEventListenerConfig.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/RCKafkaNotificationEventListenerConfig.java deleted file mode 100644 index eb84c021..00000000 --- a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/RCKafkaNotificationEventListenerConfig.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.reactivecommons.async.kafka.config; - -import org.reactivecommons.async.api.DefaultCommandHandler; -import org.reactivecommons.async.commons.converters.MessageConverter; -import org.reactivecommons.async.commons.ext.CustomReporter; -import org.reactivecommons.async.kafka.config.props.AsyncKafkaProps; -import org.reactivecommons.async.kafka.config.props.AsyncKafkaPropsDomain; -import org.reactivecommons.async.kafka.listeners.ApplicationNotificationsListener; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import reactor.core.publisher.Mono; - -import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; - -@Configuration -public class RCKafkaNotificationEventListenerConfig { - - @Bean - public ApplicationNotificationsListener kafkaNotificationEventListener(ConnectionManager manager, - DomainHandlers handlers, - AsyncKafkaPropsDomain asyncPropsDomain, - MessageConverter messageConverter, - CustomReporter customReporter) { - AsyncKafkaProps props = asyncPropsDomain.getProps(DEFAULT_DOMAIN); - ApplicationNotificationsListener eventListener = new ApplicationNotificationsListener( - manager.getListener(DEFAULT_DOMAIN), - handlers.get(DEFAULT_DOMAIN), - messageConverter, - props.getWithDLQRetry(), - props.getCreateTopology(), - props.getMaxRetries(), - props.getRetryDelay(), - manager.getDiscardNotifier(DEFAULT_DOMAIN), - customReporter, - props.getAppName()); - - eventListener.startListener(manager.getTopologyCreator(DEFAULT_DOMAIN)); - - return eventListener; - } -} diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaProps.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaProps.java index 1636c90e..55919f1e 100644 --- a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaProps.java +++ b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaProps.java @@ -7,7 +7,7 @@ import lombok.Setter; import lombok.experimental.SuperBuilder; import org.reactivecommons.async.kafka.config.KafkaProperties; -import org.reactivecommons.async.starter.GenericAsyncProps; +import org.reactivecommons.async.starter.props.GenericAsyncProps; import org.springframework.boot.context.properties.NestedConfigurationProperty; @@ -43,4 +43,12 @@ public class AsyncKafkaProps extends GenericAsyncProps { @Builder.Default private Boolean checkExistingTopics = true; + @Builder.Default + private boolean useDiscardNotifierPerDomain = false; + + @Builder.Default + private boolean enabled = true; + + @Builder.Default + private String brokerType = "kafka"; } diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaPropsDomain.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaPropsDomain.java index fb50627b..babe8d05 100644 --- a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaPropsDomain.java +++ b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaPropsDomain.java @@ -3,7 +3,7 @@ import lombok.Getter; import lombok.Setter; import org.reactivecommons.async.kafka.config.KafkaProperties; -import org.reactivecommons.async.starter.GenericAsyncPropsDomain; +import org.reactivecommons.async.starter.props.GenericAsyncPropsDomain; import org.springframework.beans.factory.annotation.Value; import java.lang.reflect.Constructor; diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaPropsDomainProperties.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaPropsDomainProperties.java index 015c7cf8..67df456c 100644 --- a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaPropsDomainProperties.java +++ b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaPropsDomainProperties.java @@ -1,7 +1,7 @@ package org.reactivecommons.async.kafka.config.props; import org.reactivecommons.async.kafka.config.KafkaProperties; -import org.reactivecommons.async.starter.GenericAsyncPropsDomainProperties; +import org.reactivecommons.async.starter.props.GenericAsyncPropsDomainProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import java.util.Map; diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/spring/KafkaPropertiesBase.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/spring/KafkaPropertiesBase.java index ca677543..036432b4 100644 --- a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/spring/KafkaPropertiesBase.java +++ b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/spring/KafkaPropertiesBase.java @@ -1640,12 +1640,12 @@ public void setRandom(boolean random) { public static class Cleanup { /** - * Cleanup the application’s local state directory on startup. + * Cleanup the application?s local state directory on startup. */ private boolean onStartup = false; /** - * Cleanup the application’s local state directory on shutdown. + * Cleanup the application?s local state directory on shutdown. */ private boolean onShutdown = false; diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/health/KafkaReactiveHealthIndicator.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/health/KafkaReactiveHealthIndicator.java new file mode 100644 index 00000000..a2d4d3d4 --- /dev/null +++ b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/health/KafkaReactiveHealthIndicator.java @@ -0,0 +1,32 @@ +package org.reactivecommons.async.kafka.health; + +import lombok.AllArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.apache.kafka.clients.admin.AdminClient; +import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import reactor.core.publisher.Mono; + +@Log4j2 +@AllArgsConstructor +public class KafkaReactiveHealthIndicator extends AbstractReactiveHealthIndicator { + public static final String VERSION = "version"; + private final String domain; + private final AdminClient adminClient; + + @Override + protected Mono doHealthCheck(Health.Builder builder) { + builder.withDetail("domain", domain); + return checkKafkaHealth() + .map(clusterId -> builder.up().withDetail(VERSION, clusterId).build()) + .onErrorReturn(builder.down().build()); + } + + private Mono checkKafkaHealth() { + return Mono.fromFuture(adminClient.describeCluster().clusterId() + .toCompletionStage() + .toCompletableFuture()) + .doOnError(e -> log.error("Error checking Kafka health in domain {}", domain, e)); + } + +} diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/starter/impl/kafka/RCKafkaConfig.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/starter/impl/kafka/RCKafkaConfig.java new file mode 100644 index 00000000..3b830911 --- /dev/null +++ b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/starter/impl/kafka/RCKafkaConfig.java @@ -0,0 +1,54 @@ +package org.reactivecommons.async.starter.impl.kafka; + +import org.reactivecommons.async.commons.converters.json.ObjectMapperSupplier; +import org.reactivecommons.async.kafka.KafkaBrokerProviderFactory; +import org.reactivecommons.async.kafka.communications.topology.KafkaCustomizations; +import org.reactivecommons.async.kafka.config.KafkaProperties; +import org.reactivecommons.async.kafka.config.KafkaPropertiesAutoConfig; +import org.reactivecommons.async.kafka.config.props.AsyncKafkaPropsDomain; +import org.reactivecommons.async.kafka.config.props.AsyncKafkaPropsDomainProperties; +import org.reactivecommons.async.kafka.converters.json.KafkaJacksonMessageConverter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.ssl.DefaultSslBundleRegistry; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@EnableConfigurationProperties({KafkaPropertiesAutoConfig.class, AsyncKafkaPropsDomainProperties.class}) +@Import({AsyncKafkaPropsDomain.class, KafkaBrokerProviderFactory.class}) +public class RCKafkaConfig { + + @Bean + @ConditionalOnMissingBean(KafkaCustomizations.class) + public KafkaCustomizations defaultKafkaCustomizations() { + return new KafkaCustomizations(); + } + + @Bean + @ConditionalOnMissingBean(KafkaJacksonMessageConverter.class) + public KafkaJacksonMessageConverter kafkaJacksonMessageConverter(ObjectMapperSupplier objectMapperSupplier) { + return new KafkaJacksonMessageConverter(objectMapperSupplier.get()); + } + + @Bean + @ConditionalOnMissingBean(AsyncKafkaPropsDomain.KafkaSecretFiller.class) + public AsyncKafkaPropsDomain.KafkaSecretFiller defaultKafkaSecretFiller() { + return (ignoredDomain, ignoredProps) -> { + }; + } + + @Bean + @ConditionalOnMissingBean(KafkaProperties.class) + public KafkaProperties defaultKafkaProperties(KafkaPropertiesAutoConfig properties, ObjectMapperSupplier supplier) { + return supplier.get().convertValue(properties, KafkaProperties.class); + } + + @Bean + @ConditionalOnMissingBean(SslBundles.class) + public SslBundles defaultSslBundles() { + return new DefaultSslBundleRegistry(); + } +} diff --git a/starters/async-kafka-starter/src/test/java/org/reactivecommons/async/kafka/KafkaBrokerProviderFactoryTest.java b/starters/async-kafka-starter/src/test/java/org/reactivecommons/async/kafka/KafkaBrokerProviderFactoryTest.java new file mode 100644 index 00000000..f0f4463a --- /dev/null +++ b/starters/async-kafka-starter/src/test/java/org/reactivecommons/async/kafka/KafkaBrokerProviderFactoryTest.java @@ -0,0 +1,75 @@ +package org.reactivecommons.async.kafka; + +import io.micrometer.core.instrument.MeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivecommons.async.commons.ext.CustomReporter; +import org.reactivecommons.async.commons.reply.ReactiveReplyRouter; +import org.reactivecommons.async.kafka.communications.topology.KafkaCustomizations; +import org.reactivecommons.async.kafka.config.props.AsyncKafkaProps; +import org.reactivecommons.async.kafka.converters.json.KafkaJacksonMessageConverter; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.broker.BrokerProviderFactory; +import org.reactivecommons.async.starter.broker.DiscardProvider; +import org.springframework.boot.ssl.SslBundles; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@ExtendWith(MockitoExtension.class) +class KafkaBrokerProviderFactoryTest { + private final ReactiveReplyRouter router = new ReactiveReplyRouter(); + @Mock + private KafkaJacksonMessageConverter converter; + @Mock + private MeterRegistry meterRegistry; + @Mock + private CustomReporter errorReporter; + @Mock + private KafkaCustomizations customizations; + @Mock + private SslBundles sslBundles; + + private BrokerProviderFactory providerFactory; + + @BeforeEach + public void setUp() { + providerFactory = new KafkaBrokerProviderFactory(router, converter, meterRegistry, errorReporter, + customizations, sslBundles); + } + + @Test + void shouldReturnBrokerType() { + // Arrange + // Act + String brokerType = providerFactory.getBrokerType(); + // Assert + assertEquals("kafka", brokerType); + } + + @Test + void shouldReturnCreateDiscardProvider() { + // Arrange + AsyncKafkaProps props = new AsyncKafkaProps(); + props.setCheckExistingTopics(false); + // Act + DiscardProvider discardProvider = providerFactory.getDiscardProvider(props); + // Assert + assertThat(discardProvider).isInstanceOf(KafkaDiscardProvider.class); + } + + @Test + void shouldReturnBrokerProvider() { + // Arrange + AsyncKafkaProps props = new AsyncKafkaProps(); + props.setCheckExistingTopics(false); + DiscardProvider discardProvider = providerFactory.getDiscardProvider(props); + // Act + BrokerProvider brokerProvider = providerFactory.getProvider("domain", props, discardProvider); + // Assert + assertThat(brokerProvider).isInstanceOf(KafkaBrokerProvider.class); + } +} diff --git a/starters/async-kafka-starter/src/test/java/org/reactivecommons/async/kafka/KafkaBrokerProviderTest.java b/starters/async-kafka-starter/src/test/java/org/reactivecommons/async/kafka/KafkaBrokerProviderTest.java new file mode 100644 index 00000000..c0f862a4 --- /dev/null +++ b/starters/async-kafka-starter/src/test/java/org/reactivecommons/async/kafka/KafkaBrokerProviderTest.java @@ -0,0 +1,147 @@ +package org.reactivecommons.async.kafka; + +import io.micrometer.core.instrument.MeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivecommons.api.domain.DomainEventBus; +import org.reactivecommons.async.api.DirectAsyncGateway; +import org.reactivecommons.async.commons.DiscardNotifier; +import org.reactivecommons.async.commons.HandlerResolver; +import org.reactivecommons.async.commons.ext.CustomReporter; +import org.reactivecommons.async.commons.reply.ReactiveReplyRouter; +import org.reactivecommons.async.kafka.communications.ReactiveMessageListener; +import org.reactivecommons.async.kafka.communications.ReactiveMessageSender; +import org.reactivecommons.async.kafka.communications.topology.KafkaCustomizations; +import org.reactivecommons.async.kafka.communications.topology.TopologyCreator; +import org.reactivecommons.async.kafka.config.props.AsyncKafkaProps; +import org.reactivecommons.async.kafka.converters.json.KafkaJacksonMessageConverter; +import org.reactivecommons.async.kafka.health.KafkaReactiveHealthIndicator; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.ssl.DefaultSslBundleRegistry; +import org.springframework.boot.ssl.SslBundles; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import javax.sound.midi.Receiver; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; + +@ExtendWith(MockitoExtension.class) +class KafkaBrokerProviderTest { + private final AsyncKafkaProps props = new AsyncKafkaProps(); + private final SslBundles sslBundles = new DefaultSslBundleRegistry(); + private final KafkaCustomizations customizations = new KafkaCustomizations(); + @Mock + private ReactiveMessageListener listener; + @Mock + private TopologyCreator creator; + @Mock + private HandlerResolver handlerResolver; + @Mock + private KafkaJacksonMessageConverter messageConverter; + @Mock + private CustomReporter customReporter; + @Mock + private Receiver receiver; + @Mock + private ReactiveReplyRouter router; + @Mock + private MeterRegistry meterRegistry; + @Mock + private ReactiveMessageSender sender; + @Mock + private DiscardNotifier discardNotifier; + @Mock + private KafkaReactiveHealthIndicator healthIndicator; + + + private BrokerProvider brokerProvider; + + + @BeforeEach + public void init() { + props.setAppName("test"); + brokerProvider = new KafkaBrokerProvider(DEFAULT_DOMAIN, + props, + router, + messageConverter, + meterRegistry, + customReporter, + healthIndicator, + listener, + sender, + discardNotifier, + creator, + customizations, + sslBundles); + } + + @Test + void shouldCreateDomainEventBus() { + // Act + DomainEventBus domainBus = brokerProvider.getDomainBus(); + // Assert + assertThat(domainBus).isExactlyInstanceOf(KafkaDomainEventBus.class); + } + + @Test + void shouldCreateDirectAsyncGateway() { + // Act + DirectAsyncGateway domainBus = brokerProvider.getDirectAsyncGateway(handlerResolver); + // Assert + assertThat(domainBus).isExactlyInstanceOf(KafkaDirectAsyncGateway.class); + } + + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + void shouldListenDomainEvents() { + List mockedListeners = spy(List.of()); + when(mockedListeners.isEmpty()).thenReturn(false); + when(handlerResolver.getEventListeners()).thenReturn(mockedListeners); + when(creator.createTopics(any())).thenReturn(Mono.empty()); + when(listener.getMaxConcurrency()).thenReturn(1); + when(listener.listen(any(String.class), any())).thenReturn(Flux.never()); + // Act + brokerProvider.listenDomainEvents(handlerResolver); + // Assert + verify(listener, times(1)).listen(any(String.class), any()); + } + + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + void shouldListenNotificationEvents() { + List mockedListeners = spy(List.of()); + when(mockedListeners.isEmpty()).thenReturn(false); + when(handlerResolver.getNotificationListeners()).thenReturn(mockedListeners); + when(creator.createTopics(any())).thenReturn(Mono.empty()); + when(listener.getMaxConcurrency()).thenReturn(1); + when(listener.listen(any(String.class), any())).thenReturn(Flux.never()); + // Act + brokerProvider.listenNotificationEvents(handlerResolver); + // Assert + verify(listener, times(1)).listen(any(String.class), any()); + } + + @Test + void shouldProxyHealthCheck() { + when(healthIndicator.health()).thenReturn(Mono.fromSupplier(() -> Health.up().build())); + // Act + Mono flow = brokerProvider.healthCheck(); + // Assert + StepVerifier.create(flow) + .expectNextMatches(health -> health.getStatus().getCode().equals("UP")) + .verifyComplete(); + } +} diff --git a/starters/async-kafka-starter/src/test/java/org/reactivecommons/async/kafka/KafkaDiscardProviderTest.java b/starters/async-kafka-starter/src/test/java/org/reactivecommons/async/kafka/KafkaDiscardProviderTest.java new file mode 100644 index 00000000..9723e0e5 --- /dev/null +++ b/starters/async-kafka-starter/src/test/java/org/reactivecommons/async/kafka/KafkaDiscardProviderTest.java @@ -0,0 +1,38 @@ +package org.reactivecommons.async.kafka; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivecommons.async.commons.DLQDiscardNotifier; +import org.reactivecommons.async.commons.DiscardNotifier; +import org.reactivecommons.async.kafka.communications.topology.KafkaCustomizations; +import org.reactivecommons.async.kafka.config.KafkaProperties; +import org.reactivecommons.async.kafka.config.props.AsyncKafkaProps; +import org.reactivecommons.async.kafka.converters.json.KafkaJacksonMessageConverter; +import org.springframework.boot.ssl.SslBundles; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class KafkaDiscardProviderTest { + @Mock + private KafkaJacksonMessageConverter converter; + @Mock + private KafkaCustomizations customizations; + @Mock + private SslBundles sslBundles; + + @Test + void shouldCreateDiscardNotifier() { + // Arrange + AsyncKafkaProps props = new AsyncKafkaProps(); + props.setCheckExistingTopics(false); + props.setConnectionProperties(new KafkaProperties()); + KafkaDiscardProvider discardProvider = new KafkaDiscardProvider(props, converter, customizations, sslBundles); + // Act + DiscardNotifier notifier = discardProvider.get(); + // Assert + assertThat(notifier).isExactlyInstanceOf(DLQDiscardNotifier.class); + } +} diff --git a/starters/async-kafka-starter/src/test/java/org/reactivecommons/async/kafka/health/KafkaReactiveHealthIndicatorTest.java b/starters/async-kafka-starter/src/test/java/org/reactivecommons/async/kafka/health/KafkaReactiveHealthIndicatorTest.java new file mode 100644 index 00000000..0284ec4c --- /dev/null +++ b/starters/async-kafka-starter/src/test/java/org/reactivecommons/async/kafka/health/KafkaReactiveHealthIndicatorTest.java @@ -0,0 +1,71 @@ +package org.reactivecommons.async.kafka.health; + +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.admin.DescribeClusterResult; +import org.apache.kafka.common.KafkaFuture; +import org.apache.kafka.common.internals.KafkaFutureImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Health.Builder; +import org.springframework.boot.actuate.health.Status; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; +import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; + +@ExtendWith(MockitoExtension.class) +class KafkaReactiveHealthIndicatorTest { + @Mock + private AdminClient adminClient; + @Mock + private DescribeClusterResult describeClusterResult; + + private KafkaReactiveHealthIndicator indicator; + + @BeforeEach + void setup() { + indicator = new KafkaReactiveHealthIndicator(DEFAULT_DOMAIN, adminClient); + } + + @Test + void shouldBeUp() { + // Arrange + when(adminClient.describeCluster()).thenReturn(describeClusterResult); + when(describeClusterResult.clusterId()).thenReturn(KafkaFuture.completedFuture("cluster123")); + // Act + Mono result = indicator.doHealthCheck(new Builder()); + // Assert + StepVerifier.create(result) + .assertNext(health -> { + assertEquals(DEFAULT_DOMAIN, health.getDetails().get("domain")); + assertEquals("cluster123", health.getDetails().get("version")); + assertEquals(Status.UP, health.getStatus()); + }) + .verifyComplete(); + } + + @Test + void shouldBeDown() { + // Arrange + when(adminClient.describeCluster()).thenReturn(describeClusterResult); + KafkaFutureImpl future = new KafkaFutureImpl<>(); + future.completeExceptionally(new RuntimeException("simulate error")); + when(describeClusterResult.clusterId()).thenReturn(future); + // Act + Mono result = indicator.doHealthCheck(new Builder()); + // Assert + StepVerifier.create(result) + .expectNextMatches(health -> { + assertEquals(DEFAULT_DOMAIN, health.getDetails().get("domain")); + assertEquals(Status.DOWN, health.getStatus()); + return true; + }) + .verifyComplete(); + } +} diff --git a/starters/async-kafka-starter/src/test/java/org/reactivecommons/async/starter/impl/rabbit/KafkaConfigTest.java b/starters/async-kafka-starter/src/test/java/org/reactivecommons/async/starter/impl/rabbit/KafkaConfigTest.java new file mode 100644 index 00000000..d9fcac5b --- /dev/null +++ b/starters/async-kafka-starter/src/test/java/org/reactivecommons/async/starter/impl/rabbit/KafkaConfigTest.java @@ -0,0 +1,44 @@ +package org.reactivecommons.async.starter.impl.rabbit; + + +import org.junit.jupiter.api.Test; +import org.reactivecommons.async.kafka.KafkaBrokerProviderFactory; +import org.reactivecommons.async.kafka.config.props.AsyncKafkaPropsDomain; +import org.reactivecommons.async.kafka.converters.json.KafkaJacksonMessageConverter; +import org.reactivecommons.async.starter.config.ConnectionManager; +import org.reactivecommons.async.starter.config.ReactiveCommonsConfig; +import org.reactivecommons.async.starter.impl.kafka.RCKafkaConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(classes = { + RCKafkaConfig.class, + AsyncKafkaPropsDomain.class, + KafkaBrokerProviderFactory.class, + ReactiveCommonsConfig.class}) +class KafkaConfigTest { + @Autowired + private KafkaJacksonMessageConverter converter; + @Autowired + private ConnectionManager manager; + + @Test + void shouldHasConverter() { + // Arrange + // Act + // Assert + assertThat(converter).isNotNull(); + } + + @Test + void shouldHasManager() { + // Arrange + // Act + // Assert + assertThat(manager).isNotNull(); + assertThat(manager.getProviders()).isNotEmpty(); + assertThat(manager.getProviders().get("app").getProps().getAppName()).isEqualTo("async-kafka-starter"); + } +} diff --git a/starters/async-kafka-starter/src/test/resources/application.yaml b/starters/async-kafka-starter/src/test/resources/application.yaml new file mode 100644 index 00000000..a5c0534e --- /dev/null +++ b/starters/async-kafka-starter/src/test/resources/application.yaml @@ -0,0 +1,8 @@ +spring: + application: + name: async-kafka-starter +reactive: + commons: + kafka: + app: + checkExistingTopics: false \ No newline at end of file diff --git a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/config/DirectAsyncGatewayConfig.java b/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/DirectAsyncGatewayConfig.java similarity index 97% rename from starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/config/DirectAsyncGatewayConfig.java rename to starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/DirectAsyncGatewayConfig.java index a68a2e90..4ee66272 100644 --- a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/config/DirectAsyncGatewayConfig.java +++ b/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/DirectAsyncGatewayConfig.java @@ -1,4 +1,4 @@ -package org.reactivecommons.async.rabbit.config; +package org.reactivecommons.async.rabbit.standalone.config; import io.micrometer.core.instrument.MeterRegistry; import lombok.AllArgsConstructor; diff --git a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/config/EventBusConfig.java b/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/EventBusConfig.java similarity index 92% rename from starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/config/EventBusConfig.java rename to starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/EventBusConfig.java index fe01f0c3..d158d48c 100644 --- a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/config/EventBusConfig.java +++ b/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/EventBusConfig.java @@ -1,4 +1,4 @@ -package org.reactivecommons.async.rabbit.config; +package org.reactivecommons.async.rabbit.standalone.config; import org.reactivecommons.api.domain.DomainEventBus; import org.reactivecommons.async.rabbit.RabbitDomainEventBus; diff --git a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/config/RabbitMqConfig.java b/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/RabbitMqConfig.java similarity index 95% rename from starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/config/RabbitMqConfig.java rename to starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/RabbitMqConfig.java index 6000a897..adc2d98d 100644 --- a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/config/RabbitMqConfig.java +++ b/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/RabbitMqConfig.java @@ -1,4 +1,4 @@ -package org.reactivecommons.async.rabbit.config; +package org.reactivecommons.async.rabbit.standalone.config; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; @@ -6,13 +6,14 @@ import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; import org.reactivecommons.async.rabbit.communications.TopologyCreator; import org.reactivecommons.async.commons.converters.MessageConverter; -import org.reactivecommons.async.commons.converters.json.JacksonMessageConverter; import org.reactivecommons.async.commons.converters.json.ObjectMapperSupplier; +import org.reactivecommons.async.rabbit.config.ConnectionFactoryProvider; import org.reactivecommons.async.rabbit.converters.json.RabbitJacksonMessageConverter; import reactor.core.publisher.Mono; import reactor.rabbitmq.*; import reactor.util.retry.Retry; +import java.io.File; import java.time.Duration; import java.util.logging.Level; diff --git a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/config/RabbitProperties.java b/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/RabbitProperties.java similarity index 82% rename from starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/config/RabbitProperties.java rename to starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/RabbitProperties.java index 48045b68..bd7ae2e8 100644 --- a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/config/RabbitProperties.java +++ b/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/RabbitProperties.java @@ -1,4 +1,4 @@ -package org.reactivecommons.async.rabbit.config; +package org.reactivecommons.async.rabbit.standalone.config; import lombok.Data; diff --git a/starters/async-rabbit-starter/async-commons-rabbit-starter.gradle b/starters/async-rabbit-starter/async-commons-rabbit-starter.gradle index 51c58ebb..c4b4025f 100644 --- a/starters/async-rabbit-starter/async-commons-rabbit-starter.gradle +++ b/starters/async-rabbit-starter/async-commons-rabbit-starter.gradle @@ -5,7 +5,7 @@ ext { dependencies { api project(':async-rabbit') - api project(':shared-starter') + api project(':async-commons-starter') compileOnly 'org.springframework.boot:spring-boot-starter' compileOnly 'org.springframework.boot:spring-boot-starter-actuator' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/RabbitEDADirectAsyncGateway.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/RabbitEDADirectAsyncGateway.java deleted file mode 100644 index f4418ec8..00000000 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/RabbitEDADirectAsyncGateway.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.reactivecommons.async; - -import io.micrometer.core.instrument.MeterRegistry; -import org.reactivecommons.async.commons.config.BrokerConfig; -import org.reactivecommons.async.commons.converters.MessageConverter; -import org.reactivecommons.async.commons.reply.ReactiveReplyRouter; -import org.reactivecommons.async.rabbit.RabbitDirectAsyncGateway; -import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; -import org.reactivecommons.async.rabbit.config.ConnectionManager; - -import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; - -public class RabbitEDADirectAsyncGateway extends RabbitDirectAsyncGateway { - private final ConnectionManager manager; - - public RabbitEDADirectAsyncGateway(BrokerConfig config, ReactiveReplyRouter router, ConnectionManager manager, String exchange, MessageConverter converter, MeterRegistry meterRegistry) { - super(config, router, manager.getSender(DEFAULT_DOMAIN), exchange, converter, meterRegistry); - this.manager = manager; - } - - @Override - protected ReactiveMessageSender resolveSender(String domain) { - return manager.getSender(domain); - } -} diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableDomainEventBus.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableDomainEventBus.java deleted file mode 100644 index 6820ab50..00000000 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableDomainEventBus.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.reactivecommons.async.impl.config.annotations; - -import org.reactivecommons.async.rabbit.config.EventBusConfig; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -import java.lang.annotation.*; - - -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE}) -@Documented -@Import(EventBusConfig.class) -@Configuration -public @interface EnableDomainEventBus { -} - - - diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableEventListeners.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableEventListeners.java deleted file mode 100644 index 87791c20..00000000 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableEventListeners.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.reactivecommons.async.impl.config.annotations; - -import org.reactivecommons.async.rabbit.config.EventListenersConfig; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -import java.lang.annotation.*; - -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE}) -@Documented -@Import(EventListenersConfig.class) -@Configuration -public @interface EnableEventListeners { -} - - - diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableMessageListeners.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableMessageListeners.java deleted file mode 100644 index c4bf5a80..00000000 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableMessageListeners.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.reactivecommons.async.impl.config.annotations; - -import org.reactivecommons.async.rabbit.config.CommandListenersConfig; -import org.reactivecommons.async.rabbit.config.EventListenersConfig; -import org.reactivecommons.async.rabbit.config.NotificationListenersConfig; -import org.reactivecommons.async.rabbit.config.QueryListenerConfig; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -import java.lang.annotation.*; - -/** - * This annotation enables all messages listeners (Query, Commands, Events). If you want to enable separately, please use - * EnableCommandListeners, EnableQueryListeners or EnableEventListeners. - * - */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE}) -@Documented -@Import({CommandListenersConfig.class, QueryListenerConfig.class, EventListenersConfig.class, NotificationListenersConfig.class}) -@Configuration -public @interface EnableMessageListeners { -} - - - diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableNotificationListener.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableNotificationListener.java deleted file mode 100644 index 516f479e..00000000 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableNotificationListener.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.reactivecommons.async.impl.config.annotations; - -import org.reactivecommons.async.rabbit.config.NotificationListenersConfig; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -import java.lang.annotation.*; - - -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE}) -@Documented -@Import(NotificationListenersConfig.class) -@Configuration -public @interface EnableNotificationListener { -} - - - diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProvider.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProvider.java new file mode 100644 index 00000000..5110e069 --- /dev/null +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProvider.java @@ -0,0 +1,160 @@ +package org.reactivecommons.async.rabbit; + +import io.micrometer.core.instrument.MeterRegistry; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.java.Log; +import org.reactivecommons.api.domain.DomainEventBus; +import org.reactivecommons.async.api.DirectAsyncGateway; +import org.reactivecommons.async.commons.DiscardNotifier; +import org.reactivecommons.async.commons.HandlerResolver; +import org.reactivecommons.async.commons.config.BrokerConfig; +import org.reactivecommons.async.commons.ext.CustomReporter; +import org.reactivecommons.async.commons.reply.ReactiveReplyRouter; +import org.reactivecommons.async.rabbit.communications.ReactiveMessageListener; +import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; +import org.reactivecommons.async.rabbit.config.props.AsyncProps; +import org.reactivecommons.async.rabbit.converters.json.RabbitJacksonMessageConverter; +import org.reactivecommons.async.rabbit.health.RabbitReactiveHealthIndicator; +import org.reactivecommons.async.rabbit.listeners.ApplicationCommandListener; +import org.reactivecommons.async.rabbit.listeners.ApplicationEventListener; +import org.reactivecommons.async.rabbit.listeners.ApplicationNotificationListener; +import org.reactivecommons.async.rabbit.listeners.ApplicationQueryListener; +import org.reactivecommons.async.rabbit.listeners.ApplicationReplyListener; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.springframework.boot.actuate.health.Health; +import reactor.core.publisher.Mono; + +import static reactor.rabbitmq.ExchangeSpecification.exchange; + +@Log +@Getter +@AllArgsConstructor +public class RabbitMQBrokerProvider implements BrokerProvider { + private final String domain; + private final AsyncProps props; + private final BrokerConfig config; + private final ReactiveReplyRouter router; + private final RabbitJacksonMessageConverter converter; + private final MeterRegistry meterRegistry; + private final CustomReporter errorReporter; + private final RabbitReactiveHealthIndicator healthIndicator; + private final ReactiveMessageListener receiver; + private final ReactiveMessageSender sender; + private final DiscardNotifier discardNotifier; + + @Override + public DomainEventBus getDomainBus() { + final String exchangeName = props.getBrokerConfigProps().getDomainEventsExchangeName(); + if (props.getCreateTopology()) { + sender.getTopologyCreator().declare(exchange(exchangeName).durable(true).type("topic")).subscribe(); + } + return new RabbitDomainEventBus(sender, exchangeName, config); + } + + @Override + public DirectAsyncGateway getDirectAsyncGateway(HandlerResolver resolver) { + String exchangeName = props.getBrokerConfigProps().getDirectMessagesExchangeName(); + if (props.getCreateTopology()) { + sender.getTopologyCreator().declare(exchange(exchangeName).durable(true).type("direct")).subscribe(); + } + listenReplies(resolver); + return new RabbitDirectAsyncGateway(config, router, sender, exchangeName, converter, meterRegistry); + } + + @Override + public void listenDomainEvents(HandlerResolver resolver) { + if (!props.getDomain().isIgnoreThisListener()) { + final ApplicationEventListener listener = new ApplicationEventListener(receiver, + props.getBrokerConfigProps().getEventsQueue(), + props.getBrokerConfigProps().getDomainEventsExchangeName(), + resolver, + converter, + props.getWithDLQRetry(), + props.getCreateTopology(), + props.getMaxRetries(), + props.getRetryDelay(), + props.getDomain().getEvents().getMaxLengthBytes(), + discardNotifier, + errorReporter, + props.getAppName()); + listener.startListener(); + } + } + + @Override + public void listenNotificationEvents(HandlerResolver resolver) { + if (!resolver.getNotificationListeners().isEmpty()) { + final ApplicationNotificationListener listener = new ApplicationNotificationListener( + receiver, + props.getBrokerConfigProps().getDomainEventsExchangeName(), + props.getBrokerConfigProps().getNotificationsQueue(), + props.getCreateTopology(), + resolver, + converter, + discardNotifier, + errorReporter); + listener.startListener(); + } + } + + @Override + public void listenCommands(HandlerResolver resolver) { + ApplicationCommandListener commandListener = new ApplicationCommandListener( + receiver, + props.getBrokerConfigProps().getCommandsQueue(), + resolver, + props.getDirect().getExchange(), + converter, + props.getWithDLQRetry(), + props.getCreateTopology(), + props.getDelayedCommands(), + props.getMaxRetries(), + props.getRetryDelay(), + props.getDirect().getMaxLengthBytes(), + discardNotifier, + errorReporter); + + commandListener.startListener(); + } + + @Override + public void listenQueries(HandlerResolver resolver) { + final ApplicationQueryListener listener = new ApplicationQueryListener( + receiver, + props.getBrokerConfigProps().getQueriesQueue(), + resolver, + sender, + props.getBrokerConfigProps().getDirectMessagesExchangeName(), + converter, + props.getBrokerConfigProps().getGlobalReplyExchangeName(), + props.getWithDLQRetry(), + props.getCreateTopology(), + props.getMaxRetries(), + props.getRetryDelay(), + props.getGlobal().getMaxLengthBytes(), + props.getDirect().isDiscardTimeoutQueries(), + discardNotifier, + errorReporter); + + listener.startListener(); + } + + @Override + public void listenReplies(HandlerResolver resolver) { + if (props.isListenReplies()) { + final ApplicationReplyListener replyListener = + new ApplicationReplyListener(router, + receiver, + props.getBrokerConfigProps().getReplyQueue(), + props.getBrokerConfigProps().getGlobalReplyExchangeName(), + props.getCreateTopology()); + replyListener.startListening(config.getRoutingKey()); + } + } + + @Override + public Mono healthCheck() { + return healthIndicator.health(); + } +} diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProviderFactory.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProviderFactory.java new file mode 100644 index 00000000..8c81db82 --- /dev/null +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProviderFactory.java @@ -0,0 +1,57 @@ +package org.reactivecommons.async.rabbit; + +import io.micrometer.core.instrument.MeterRegistry; +import lombok.AllArgsConstructor; +import org.reactivecommons.async.commons.DiscardNotifier; +import org.reactivecommons.async.commons.config.BrokerConfig; +import org.reactivecommons.async.commons.ext.CustomReporter; +import org.reactivecommons.async.commons.reply.ReactiveReplyRouter; +import org.reactivecommons.async.rabbit.communications.ReactiveMessageListener; +import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; +import org.reactivecommons.async.rabbit.config.ConnectionFactoryProvider; +import org.reactivecommons.async.rabbit.config.RabbitProperties; +import org.reactivecommons.async.rabbit.config.props.AsyncProps; +import org.reactivecommons.async.rabbit.converters.json.RabbitJacksonMessageConverter; +import org.reactivecommons.async.rabbit.health.RabbitReactiveHealthIndicator; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.broker.BrokerProviderFactory; +import org.reactivecommons.async.starter.broker.DiscardProvider; +import org.springframework.stereotype.Service; + +@Service("rabbitmq") +@AllArgsConstructor +public class RabbitMQBrokerProviderFactory implements BrokerProviderFactory { + private final BrokerConfig config; + private final ReactiveReplyRouter router; + private final RabbitJacksonMessageConverter converter; + private final MeterRegistry meterRegistry; + private final CustomReporter errorReporter; + + @Override + public String getBrokerType() { + return "rabbitmq"; + } + + @Override + public DiscardProvider getDiscardProvider(AsyncProps props) { + return new RabbitMQDiscardProvider(props, config, converter); + } + + @Override + public BrokerProvider getProvider(String domain, AsyncProps props, DiscardProvider discardProvider) { + RabbitProperties properties = props.getConnectionProperties(); + ConnectionFactoryProvider provider = RabbitMQSetupUtils.connectionFactoryProvider(properties); + RabbitReactiveHealthIndicator healthIndicator = + new RabbitReactiveHealthIndicator(domain, provider.getConnectionFactory()); + ReactiveMessageSender sender = RabbitMQSetupUtils.createMessageSender(provider, props, converter); + ReactiveMessageListener listener = RabbitMQSetupUtils.createMessageListener(provider, props); + DiscardNotifier discardNotifier; + if (props.isUseDiscardNotifierPerDomain()) { + discardNotifier = RabbitMQSetupUtils.createDiscardNotifier(sender, props, config, converter); + } else { + discardNotifier = discardProvider.get(); + } + return new RabbitMQBrokerProvider(domain, props, config, router, converter, meterRegistry, errorReporter, + healthIndicator, listener, sender, discardNotifier); + } +} diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQDiscardProvider.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQDiscardProvider.java new file mode 100644 index 00000000..36615c4e --- /dev/null +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQDiscardProvider.java @@ -0,0 +1,34 @@ +package org.reactivecommons.async.rabbit; + +import lombok.AllArgsConstructor; +import org.reactivecommons.async.commons.DiscardNotifier; +import org.reactivecommons.async.commons.config.BrokerConfig; +import org.reactivecommons.async.commons.converters.MessageConverter; +import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; +import org.reactivecommons.async.rabbit.config.ConnectionFactoryProvider; +import org.reactivecommons.async.rabbit.config.RabbitProperties; +import org.reactivecommons.async.rabbit.config.props.AsyncProps; +import org.reactivecommons.async.starter.broker.DiscardProvider; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@AllArgsConstructor +public class RabbitMQDiscardProvider implements DiscardProvider { + private final AsyncProps props; + private final BrokerConfig config; + private final MessageConverter converter; + private final Map discardNotifier = new ConcurrentHashMap<>(); + + @Override + public DiscardNotifier get() { + return discardNotifier.computeIfAbsent(true, this::buildDiscardNotifier); + } + + private DiscardNotifier buildDiscardNotifier(boolean ignored) { + RabbitProperties properties = props.getConnectionProperties(); + ConnectionFactoryProvider provider = RabbitMQSetupUtils.connectionFactoryProvider(properties); + ReactiveMessageSender sender = RabbitMQSetupUtils.createMessageSender(provider, props, converter); + return RabbitMQSetupUtils.createDiscardNotifier(sender, props, config, converter); + } +} diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQSetupUtils.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQSetupUtils.java new file mode 100644 index 00000000..0d102085 --- /dev/null +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQSetupUtils.java @@ -0,0 +1,122 @@ +package org.reactivecommons.async.rabbit; + +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import lombok.SneakyThrows; +import lombok.experimental.UtilityClass; +import lombok.extern.java.Log; +import org.reactivecommons.api.domain.DomainEventBus; +import org.reactivecommons.async.commons.DLQDiscardNotifier; +import org.reactivecommons.async.commons.DiscardNotifier; +import org.reactivecommons.async.commons.config.BrokerConfig; +import org.reactivecommons.async.commons.converters.MessageConverter; +import org.reactivecommons.async.rabbit.communications.ReactiveMessageListener; +import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; +import org.reactivecommons.async.rabbit.communications.TopologyCreator; +import org.reactivecommons.async.rabbit.config.ConnectionFactoryProvider; +import org.reactivecommons.async.rabbit.config.RabbitProperties; +import org.reactivecommons.async.rabbit.config.props.AsyncProps; +import org.springframework.boot.context.properties.PropertyMapper; +import reactor.core.publisher.Mono; +import reactor.rabbitmq.ChannelPool; +import reactor.rabbitmq.ChannelPoolFactory; +import reactor.rabbitmq.ChannelPoolOptions; +import reactor.rabbitmq.RabbitFlux; +import reactor.rabbitmq.Receiver; +import reactor.rabbitmq.ReceiverOptions; +import reactor.rabbitmq.Sender; +import reactor.rabbitmq.SenderOptions; +import reactor.rabbitmq.Utils; +import reactor.util.retry.Retry; + +import java.time.Duration; +import java.util.logging.Level; + +@Log +@UtilityClass +public class RabbitMQSetupUtils { + private static final String LISTENER_TYPE = "listener"; + private static final String SENDER_TYPE = "sender"; + + @SneakyThrows + public static ConnectionFactoryProvider connectionFactoryProvider(RabbitProperties properties) { + final ConnectionFactory factory = new ConnectionFactory(); + PropertyMapper map = PropertyMapper.get(); + map.from(properties::determineHost).whenNonNull().to(factory::setHost); + map.from(properties::determinePort).to(factory::setPort); + map.from(properties::determineUsername).whenNonNull().to(factory::setUsername); + map.from(properties::determinePassword).whenNonNull().to(factory::setPassword); + map.from(properties::determineVirtualHost).whenNonNull().to(factory::setVirtualHost); + factory.useNio(); + if (properties.getSsl() != null && properties.getSsl().isEnabled()) { + factory.useSslProtocol(); + } + return () -> factory; + } + + + public static ReactiveMessageSender createMessageSender(ConnectionFactoryProvider provider, + AsyncProps props, + MessageConverter converter) { + final Sender sender = RabbitFlux.createSender(reactiveCommonsSenderOptions(props.getAppName(), provider, + props.getConnectionProperties())); + return new ReactiveMessageSender(sender, props.getAppName(), converter, new TopologyCreator(sender)); + } + + public static ReactiveMessageListener createMessageListener(ConnectionFactoryProvider provider, AsyncProps props) { + final Mono connection = + createConnectionMono(provider.getConnectionFactory(), props.getAppName(), LISTENER_TYPE); + final Receiver receiver = RabbitFlux.createReceiver(new ReceiverOptions().connectionMono(connection)); + final Sender sender = RabbitFlux.createSender(new SenderOptions().connectionMono(connection)); + + return new ReactiveMessageListener(receiver, + new TopologyCreator(sender), + props.getFlux().getMaxConcurrency(), + props.getPrefetchCount()); + } + + public static TopologyCreator createTopologyCreator(AsyncProps props) { + ConnectionFactoryProvider provider = connectionFactoryProvider(props.getConnectionProperties()); + final Mono connection = createConnectionMono(provider.getConnectionFactory(), + props.getAppName(), LISTENER_TYPE); + final Sender sender = RabbitFlux.createSender(new SenderOptions().connectionMono(connection)); + return new TopologyCreator(sender); + } + + public static DiscardNotifier createDiscardNotifier(ReactiveMessageSender sender, AsyncProps props, + BrokerConfig brokerConfig, MessageConverter converter) { + DomainEventBus appDomainEventBus = new RabbitDomainEventBus(sender, + props.getBrokerConfigProps().getDomainEventsExchangeName(), brokerConfig); + return new DLQDiscardNotifier(appDomainEventBus, converter); + } + + private static SenderOptions reactiveCommonsSenderOptions(String appName, ConnectionFactoryProvider provider, RabbitProperties rabbitProperties) { + final Mono senderConnection = createConnectionMono(provider.getConnectionFactory(), appName, SENDER_TYPE); + final ChannelPoolOptions channelPoolOptions = new ChannelPoolOptions(); + final PropertyMapper map = PropertyMapper.get(); + + map.from(rabbitProperties.getCache().getChannel()::getSize).whenNonNull() + .to(channelPoolOptions::maxCacheSize); + + final ChannelPool channelPool = ChannelPoolFactory.createChannelPool( + senderConnection, + channelPoolOptions + ); + + return new SenderOptions() + .channelPool(channelPool) + .resourceManagementChannelMono(channelPool.getChannelMono() + .transform(Utils::cache)); + } + + private static Mono createConnectionMono(ConnectionFactory factory, String connectionPrefix, String connectionType) { + return Mono.fromCallable(() -> factory.newConnection(connectionPrefix + " " + connectionType)) + .doOnError(err -> + log.log(Level.SEVERE, "Error creating connection to RabbitMq Broker. Starting retry process...", err) + ) + .retryWhen(Retry.backoff(Long.MAX_VALUE, Duration.ofMillis(300)) + .maxBackoff(Duration.ofMillis(3000))) + .cache(); + } + +} diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/CommandListenersConfig.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/CommandListenersConfig.java deleted file mode 100644 index 0e1a5477..00000000 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/CommandListenersConfig.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.reactivecommons.async.rabbit.config; - -import lombok.RequiredArgsConstructor; -import org.reactivecommons.async.commons.converters.MessageConverter; -import org.reactivecommons.async.commons.ext.CustomReporter; -import org.reactivecommons.async.rabbit.config.props.AsyncProps; -import org.reactivecommons.async.rabbit.config.props.AsyncPropsDomain; -import org.reactivecommons.async.rabbit.listeners.ApplicationCommandListener; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; - -@Configuration -@RequiredArgsConstructor -@Import(RabbitMqConfig.class) -public class CommandListenersConfig { - private final AsyncPropsDomain asyncPropsDomain; - - @Bean - public ApplicationCommandListener applicationCommandListener(ConnectionManager manager, - DomainHandlers handlers, - MessageConverter converter, - CustomReporter errorReporter) { - AsyncProps asyncProps = asyncPropsDomain.getProps(DEFAULT_DOMAIN); - ApplicationCommandListener commandListener = new ApplicationCommandListener(manager.getListener(DEFAULT_DOMAIN), - asyncProps.getBrokerConfigProps().getCommandsQueue(), handlers.get(DEFAULT_DOMAIN), - asyncProps.getDirect().getExchange(), converter, asyncProps.getWithDLQRetry(), - asyncProps.getCreateTopology(), asyncProps.getDelayedCommands(), asyncProps.getMaxRetries(), - asyncProps.getRetryDelay(), asyncProps.getDirect().getMaxLengthBytes(), - manager.getDiscardNotifier(DEFAULT_DOMAIN), errorReporter); - - commandListener.startListener(); - - return commandListener; - } -} diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/ConnectionManager.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/ConnectionManager.java deleted file mode 100644 index a2ac9760..00000000 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/ConnectionManager.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.reactivecommons.async.rabbit.config; - -import lombok.Builder; -import lombok.Getter; -import lombok.Setter; -import org.reactivecommons.async.commons.DiscardNotifier; -import org.reactivecommons.async.rabbit.communications.ReactiveMessageListener; -import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; - -import java.util.Map; -import java.util.TreeMap; -import java.util.function.BiConsumer; - -public class ConnectionManager { - private final Map connections = new TreeMap<>(); - - @Builder - @Getter - public static class DomainConnections { - private final ReactiveMessageListener listener; - private final ReactiveMessageSender sender; - private final ConnectionFactoryProvider provider; - @Setter - private DiscardNotifier discardNotifier; - } - - public void forSender(BiConsumer consumer) { - connections.forEach((key, conn) -> consumer.accept(key, conn.getSender())); - } - - public void forListener(BiConsumer consumer) { - connections.forEach((key, conn) -> consumer.accept(key, conn.getListener())); - } - - public void setDiscardNotifier(String domain, DiscardNotifier discardNotifier) { - getChecked(domain).setDiscardNotifier(discardNotifier); - } - - public ConnectionManager addDomain(String domain, ReactiveMessageListener listener, ReactiveMessageSender sender, - ConnectionFactoryProvider provider) { - connections.put(domain, DomainConnections.builder() - .listener(listener) - .sender(sender) - .provider(provider) - .build()); - return this; - } - - public ReactiveMessageSender getSender(String domain) { - return getChecked(domain).getSender(); - } - - public ReactiveMessageListener getListener(String domain) { - return getChecked(domain).getListener(); - } - - private DomainConnections getChecked(String domain) { - DomainConnections domainConnections = connections.get(domain); - if (domainConnections == null) { - throw new RuntimeException("You are trying to use the domain " + domain - + " but this connection is not defined"); - } - return domainConnections; - } - - public DiscardNotifier getDiscardNotifier(String domain) { - return getChecked(domain).getDiscardNotifier(); - } - - public Map getProviders() { - return connections; - } -} diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/DirectAsyncGatewayConfig.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/DirectAsyncGatewayConfig.java deleted file mode 100644 index 289a7925..00000000 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/DirectAsyncGatewayConfig.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.reactivecommons.async.rabbit.config; - -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import lombok.RequiredArgsConstructor; -import lombok.extern.java.Log; -import org.reactivecommons.async.RabbitEDADirectAsyncGateway; -import org.reactivecommons.async.commons.config.BrokerConfig; -import org.reactivecommons.async.commons.converters.MessageConverter; -import org.reactivecommons.async.commons.reply.ReactiveReplyRouter; -import org.reactivecommons.async.rabbit.RabbitDirectAsyncGateway; -import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; -import org.reactivecommons.async.rabbit.config.props.AsyncProps; -import org.reactivecommons.async.rabbit.config.props.AsyncPropsDomain; -import org.reactivecommons.async.rabbit.listeners.ApplicationReplyListener; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -import java.util.concurrent.atomic.AtomicReference; -import java.util.logging.Level; - -import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; -import static reactor.rabbitmq.ExchangeSpecification.exchange; - -@Log -@Configuration -@Import(RabbitMqConfig.class) -@RequiredArgsConstructor -public class DirectAsyncGatewayConfig { - - @Bean - public RabbitDirectAsyncGateway rabbitDirectAsyncGateway(BrokerConfig config, ReactiveReplyRouter router, - ConnectionManager manager, MessageConverter converter, - MeterRegistry meterRegistry, - AsyncPropsDomain asyncPropsDomain) { - ReactiveMessageSender sender = manager.getSender(DEFAULT_DOMAIN); - AsyncProps asyncProps = asyncPropsDomain.getProps(DEFAULT_DOMAIN); - String exchangeName = asyncProps.getBrokerConfigProps().getDirectMessagesExchangeName(); - if (asyncProps.getCreateTopology()) { - sender.getTopologyCreator().declare(exchange(exchangeName).durable(true).type("direct")).subscribe(); - } - return new RabbitEDADirectAsyncGateway(config, router, manager, exchangeName, converter, meterRegistry); - } - - @Bean - public ApplicationReplyListener msgListener(ReactiveReplyRouter router, AsyncPropsDomain asyncProps, - BrokerConfig config, ConnectionManager manager) { - AtomicReference localListener = new AtomicReference<>(); - - asyncProps.forEach((domain, props) -> { - if (props.isListenReplies()) { - final ApplicationReplyListener replyListener = - new ApplicationReplyListener(router, manager.getListener(domain), - props.getBrokerConfigProps().getReplyQueue(), - props.getBrokerConfigProps().getGlobalReplyExchangeName(), props.getCreateTopology()); - replyListener.startListening(config.getRoutingKey()); - - if (DEFAULT_DOMAIN.equals(domain)) { - localListener.set(replyListener); - } - } else { - log.log(Level.WARNING,"ApplicationReplyListener is disabled in AsyncProps or app.async." + domain - + ".listenReplies for domain " + domain); - } - }); - - return localListener.get(); - } - - @Bean - public ReactiveReplyRouter router() { - return new ReactiveReplyRouter(); - } - - @Bean - @ConditionalOnMissingBean(MeterRegistry.class) - public MeterRegistry defaultRabbitMeterRegistry() { - return new SimpleMeterRegistry(); - } -} diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/EventBusConfig.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/EventBusConfig.java deleted file mode 100644 index 1a84bf42..00000000 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/EventBusConfig.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.reactivecommons.async.rabbit.config; - -import org.reactivecommons.api.domain.DomainEventBus; -import org.reactivecommons.async.commons.config.BrokerConfig; -import org.reactivecommons.async.rabbit.RabbitDomainEventBus; -import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; -import org.reactivecommons.async.rabbit.config.props.AsyncProps; -import org.reactivecommons.async.rabbit.config.props.AsyncPropsDomain; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; -import static reactor.rabbitmq.ExchangeSpecification.exchange; - -@Configuration -@Import(RabbitMqConfig.class) -public class EventBusConfig { - - @Bean // app connection - public DomainEventBus domainEventBus(ConnectionManager manager, BrokerConfig config, - AsyncPropsDomain asyncPropsDomain) { - ReactiveMessageSender sender = manager.getSender(DEFAULT_DOMAIN); - AsyncProps asyncProps = asyncPropsDomain.getProps(DEFAULT_DOMAIN); - final String exchangeName = asyncProps.getBrokerConfigProps().getDomainEventsExchangeName(); - if (asyncProps.getCreateTopology()) { - sender.getTopologyCreator().declare(exchange(exchangeName).durable(true).type("topic")).subscribe(); - } - return new RabbitDomainEventBus(sender, exchangeName, config); - } -} diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/EventListenersConfig.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/EventListenersConfig.java deleted file mode 100644 index 53466827..00000000 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/EventListenersConfig.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.reactivecommons.async.rabbit.config; - -import lombok.RequiredArgsConstructor; -import org.reactivecommons.async.commons.converters.MessageConverter; -import org.reactivecommons.async.commons.ext.CustomReporter; -import org.reactivecommons.async.rabbit.config.props.AsyncProps; -import org.reactivecommons.async.rabbit.config.props.AsyncPropsDomain; -import org.reactivecommons.async.rabbit.listeners.ApplicationEventListener; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -import java.util.concurrent.atomic.AtomicReference; - -import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; - -@Configuration -@RequiredArgsConstructor -@Import(RabbitMqConfig.class) -public class EventListenersConfig { - - private final AsyncPropsDomain asyncPropsDomain; - - @Bean - public ApplicationEventListener eventListener(MessageConverter messageConverter, - ConnectionManager manager, - DomainHandlers handlers, - CustomReporter errorReporter) { - AtomicReference external = new AtomicReference<>(); - manager.forListener((domain, receiver) -> { - AsyncProps asyncProps = asyncPropsDomain.getProps(domain); - if (!asyncProps.getDomain().isIgnoreThisListener()) { - final ApplicationEventListener listener = new ApplicationEventListener(receiver, - asyncProps.getBrokerConfigProps().getEventsQueue(), - asyncProps.getBrokerConfigProps().getDomainEventsExchangeName(), - handlers.get(domain), - messageConverter, asyncProps.getWithDLQRetry(), - asyncProps.getCreateTopology(), - asyncProps.getMaxRetries(), - asyncProps.getRetryDelay(), - asyncProps.getDomain().getEvents().getMaxLengthBytes(), - manager.getDiscardNotifier(domain), - errorReporter, - asyncProps.getAppName()); - if (DEFAULT_DOMAIN.equals(domain)) { - external.set(listener); - } - listener.startListener(); - } - }); - - return external.get(); - } -} diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/NotificationListenersConfig.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/NotificationListenersConfig.java deleted file mode 100644 index e0c6b8f7..00000000 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/NotificationListenersConfig.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.reactivecommons.async.rabbit.config; - -import lombok.RequiredArgsConstructor; -import org.reactivecommons.async.commons.config.IBrokerConfigProps; -import org.reactivecommons.async.commons.converters.MessageConverter; -import org.reactivecommons.async.commons.ext.CustomReporter; -import org.reactivecommons.async.rabbit.config.props.AsyncProps; -import org.reactivecommons.async.rabbit.config.props.AsyncPropsDomain; -import org.reactivecommons.async.rabbit.listeners.ApplicationNotificationListener; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; - -@Configuration -@RequiredArgsConstructor -@Import(RabbitMqConfig.class) -public class NotificationListenersConfig { - - private final AsyncPropsDomain asyncPropsDomain; - - @Bean - public ApplicationNotificationListener eventNotificationListener(ConnectionManager manager, - DomainHandlers handlers, - MessageConverter messageConverter, - CustomReporter errorReporter) { - AsyncProps asyncProps = asyncPropsDomain.getProps(DEFAULT_DOMAIN); - final ApplicationNotificationListener listener = new ApplicationNotificationListener( - manager.getListener(DEFAULT_DOMAIN), - asyncProps.getBrokerConfigProps().getDomainEventsExchangeName(), - asyncProps.getBrokerConfigProps().getNotificationsQueue(), - asyncProps.getCreateTopology(), - handlers.get(DEFAULT_DOMAIN), - messageConverter, - manager.getDiscardNotifier(DEFAULT_DOMAIN), - errorReporter); - listener.startListener(); - return listener; - } -} diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/QueryListenerConfig.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/QueryListenerConfig.java deleted file mode 100644 index 709b018c..00000000 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/QueryListenerConfig.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.reactivecommons.async.rabbit.config; - -import lombok.RequiredArgsConstructor; -import org.reactivecommons.async.commons.converters.MessageConverter; -import org.reactivecommons.async.commons.ext.CustomReporter; -import org.reactivecommons.async.rabbit.config.props.AsyncProps; -import org.reactivecommons.async.rabbit.config.props.AsyncPropsDomain; -import org.reactivecommons.async.rabbit.listeners.ApplicationQueryListener; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; - -@Configuration -@RequiredArgsConstructor -@Import(RabbitMqConfig.class) -public class QueryListenerConfig { - - private final AsyncPropsDomain asyncPropsDomain; - - @Bean - public ApplicationQueryListener queryListener(MessageConverter converter, - DomainHandlers handlers, - ConnectionManager manager, - CustomReporter errorReporter) { - AsyncProps asyncProps = asyncPropsDomain.getProps(DEFAULT_DOMAIN); - final ApplicationQueryListener listener = new ApplicationQueryListener(manager.getListener(DEFAULT_DOMAIN), - asyncProps.getBrokerConfigProps().getQueriesQueue(), handlers.get(DEFAULT_DOMAIN), - manager.getSender(DEFAULT_DOMAIN), asyncProps.getBrokerConfigProps().getDirectMessagesExchangeName(), - converter, asyncProps.getBrokerConfigProps().getGlobalReplyExchangeName(), asyncProps.getWithDLQRetry(), - asyncProps.getCreateTopology(), asyncProps.getMaxRetries(), - asyncProps.getRetryDelay(), asyncProps.getGlobal().getMaxLengthBytes(), - asyncProps.getDirect().isDiscardTimeoutQueries(), - manager.getDiscardNotifier(DEFAULT_DOMAIN), errorReporter); - - listener.startListener(); - - return listener; - } -} diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/RabbitHealthConfig.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/RabbitHealthConfig.java deleted file mode 100644 index f89906ef..00000000 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/RabbitHealthConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.reactivecommons.async.rabbit.config; - -import org.reactivecommons.async.rabbit.health.DomainRabbitReactiveHealthIndicator; -import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; - -@Configuration -@ConditionalOnClass(AbstractReactiveHealthIndicator.class) -public class RabbitHealthConfig { - - @Bean - public DomainRabbitReactiveHealthIndicator rabbitHealthIndicator(ConnectionManager manager) { - return new DomainRabbitReactiveHealthIndicator(manager); - } -} diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/RabbitMqConfig.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/RabbitMqConfig.java deleted file mode 100644 index 5e4688d1..00000000 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/RabbitMqConfig.java +++ /dev/null @@ -1,253 +0,0 @@ -package org.reactivecommons.async.rabbit.config; - -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import lombok.extern.java.Log; -import org.reactivecommons.api.domain.Command; -import org.reactivecommons.api.domain.DomainEvent; -import org.reactivecommons.api.domain.DomainEventBus; -import org.reactivecommons.async.api.AsyncQuery; -import org.reactivecommons.async.api.DefaultCommandHandler; -import org.reactivecommons.async.api.DefaultQueryHandler; -import org.reactivecommons.async.api.DynamicRegistry; -import org.reactivecommons.async.api.HandlerRegistry; -import org.reactivecommons.async.commons.DLQDiscardNotifier; -import org.reactivecommons.async.commons.DiscardNotifier; -import org.reactivecommons.async.commons.HandlerResolver; -import org.reactivecommons.async.commons.HandlerResolverBuilder; -import org.reactivecommons.async.commons.communications.Message; -import org.reactivecommons.async.commons.config.BrokerConfig; -import org.reactivecommons.async.commons.config.IBrokerConfigProps; -import org.reactivecommons.async.commons.converters.MessageConverter; -import org.reactivecommons.async.commons.converters.json.DefaultObjectMapperSupplier; -import org.reactivecommons.async.commons.converters.json.ObjectMapperSupplier; -import org.reactivecommons.async.commons.ext.CustomReporter; -import org.reactivecommons.async.rabbit.DynamicRegistryImp; -import org.reactivecommons.async.rabbit.RabbitDomainEventBus; -import org.reactivecommons.async.rabbit.communications.ReactiveMessageListener; -import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; -import org.reactivecommons.async.rabbit.communications.TopologyCreator; -import org.reactivecommons.async.rabbit.config.props.AsyncProps; -import org.reactivecommons.async.rabbit.config.props.AsyncPropsDomain; -import org.reactivecommons.async.rabbit.config.props.AsyncRabbitPropsDomainProperties; -import org.reactivecommons.async.rabbit.config.props.BrokerConfigProps; -import org.reactivecommons.async.rabbit.converters.json.RabbitJacksonMessageConverter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import reactor.core.publisher.Mono; -import reactor.rabbitmq.ChannelPool; -import reactor.rabbitmq.ChannelPoolFactory; -import reactor.rabbitmq.ChannelPoolOptions; -import reactor.rabbitmq.RabbitFlux; -import reactor.rabbitmq.Receiver; -import reactor.rabbitmq.ReceiverOptions; -import reactor.rabbitmq.Sender; -import reactor.rabbitmq.SenderOptions; -import reactor.rabbitmq.Utils; -import reactor.util.retry.Retry; - -import java.time.Duration; -import java.util.Map; -import java.util.logging.Level; - -import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; - -@Log -@Configuration -@RequiredArgsConstructor -@EnableConfigurationProperties({RabbitPropertiesAutoConfig.class, AsyncRabbitPropsDomainProperties.class}) -@Import({RabbitHealthConfig.class, AsyncPropsDomain.class}) -public class RabbitMqConfig { - - private static final String LISTENER_TYPE = "listener"; - - private static final String SENDER_TYPE = "sender"; - - - @Bean - public ConnectionManager buildConnectionManager(AsyncPropsDomain props, MessageConverter converter, - BrokerConfig brokerConfig) { - ConnectionManager connectionManager = new ConnectionManager(); - props.forEach((domain, properties) -> { - ConnectionFactoryProvider provider = createConnectionFactoryProvider(properties.getConnectionProperties()); - ReactiveMessageSender sender = createMessageSender(provider, properties, converter); - ReactiveMessageListener listener = createMessageListener(provider, properties); - connectionManager.addDomain(domain, listener, sender, provider); - - ReactiveMessageSender appDomainSender = connectionManager.getSender(domain); - DomainEventBus appDomainEventBus = new RabbitDomainEventBus(appDomainSender, props.getProps(domain) - .getBrokerConfigProps().getDomainEventsExchangeName(), brokerConfig); - DiscardNotifier notifier = new DLQDiscardNotifier(appDomainEventBus, converter); - connectionManager.setDiscardNotifier(domain, notifier); - }); - return connectionManager; - } - - @Bean - public DomainHandlers buildHandlers(AsyncPropsDomain props, ApplicationContext context, - HandlerRegistry primaryRegistry, DefaultCommandHandler commandHandler) { - DomainHandlers handlers = new DomainHandlers(); - final Map registries = context.getBeansOfType(HandlerRegistry.class); - if (!registries.containsValue(primaryRegistry)) { - registries.put("primaryHandlerRegistry", primaryRegistry); - } - props.forEach((domain, properties) -> { - HandlerResolver resolver = HandlerResolverBuilder.buildResolver(domain, registries, commandHandler); - handlers.add(domain, resolver); - }); - return handlers; - } - - - private ReactiveMessageSender createMessageSender(ConnectionFactoryProvider provider, - AsyncProps props, - MessageConverter converter) { - final Sender sender = RabbitFlux.createSender(reactiveCommonsSenderOptions(props.getAppName(), provider, - props.getConnectionProperties())); - return new ReactiveMessageSender(sender, props.getAppName(), converter, new TopologyCreator(sender)); - } - - private SenderOptions reactiveCommonsSenderOptions(String appName, ConnectionFactoryProvider provider, RabbitProperties rabbitProperties) { - final Mono senderConnection = createConnectionMono(provider.getConnectionFactory(), appName, SENDER_TYPE); - final ChannelPoolOptions channelPoolOptions = new ChannelPoolOptions(); - final PropertyMapper map = PropertyMapper.get(); - - map.from(rabbitProperties.getCache().getChannel()::getSize).whenNonNull() - .to(channelPoolOptions::maxCacheSize); - - final ChannelPool channelPool = ChannelPoolFactory.createChannelPool( - senderConnection, - channelPoolOptions - ); - - return new SenderOptions() - .channelPool(channelPool) - .resourceManagementChannelMono(channelPool.getChannelMono() - .transform(Utils::cache)); - } - - public ReactiveMessageListener createMessageListener(ConnectionFactoryProvider provider, AsyncProps props) { - final Mono connection = - createConnectionMono(provider.getConnectionFactory(), props.getAppName(), LISTENER_TYPE); - final Receiver receiver = RabbitFlux.createReceiver(new ReceiverOptions().connectionMono(connection)); - final Sender sender = RabbitFlux.createSender(new SenderOptions().connectionMono(connection)); - - return new ReactiveMessageListener(receiver, - new TopologyCreator(sender), - props.getFlux().getMaxConcurrency(), - props.getPrefetchCount()); - } - - @SneakyThrows - public ConnectionFactoryProvider createConnectionFactoryProvider(RabbitProperties properties) { - final ConnectionFactory factory = new ConnectionFactory(); - PropertyMapper map = PropertyMapper.get(); - map.from(properties::determineHost).whenNonNull().to(factory::setHost); - map.from(properties::determinePort).to(factory::setPort); - map.from(properties::determineUsername).whenNonNull().to(factory::setUsername); - map.from(properties::determinePassword).whenNonNull().to(factory::setPassword); - map.from(properties::determineVirtualHost).whenNonNull().to(factory::setVirtualHost); - factory.useNio(); - if (properties.getSsl() != null && properties.getSsl().isEnabled()) { - factory.useSslProtocol(); - } - return () -> factory; - } - - @Bean - @ConditionalOnMissingBean - public BrokerConfig brokerConfig() { - return new BrokerConfig(); - } - - @Bean - @ConditionalOnMissingBean - public ObjectMapperSupplier objectMapperSupplier() { - return new DefaultObjectMapperSupplier(); - } - - @Bean - @ConditionalOnMissingBean - public MessageConverter messageConverter(ObjectMapperSupplier objectMapperSupplier) { - return new RabbitJacksonMessageConverter(objectMapperSupplier.get()); - } - - @Bean - @ConditionalOnMissingBean - public CustomReporter reactiveCommonsCustomErrorReporter() { - return new CustomReporter() { - @Override - public Mono reportError(Throwable ex, Message rawMessage, Command message, boolean redelivered) { - return Mono.empty(); - } - - @Override - public Mono reportError(Throwable ex, Message rawMessage, DomainEvent message, boolean redelivered) { - return Mono.empty(); - } - - @Override - public Mono reportError(Throwable ex, Message rawMessage, AsyncQuery message, boolean redelivered) { - return Mono.empty(); - } - }; - } - - Mono createConnectionMono(ConnectionFactory factory, String connectionPrefix, String connectionType) { - return Mono.fromCallable(() -> factory.newConnection(connectionPrefix + " " + connectionType)) - .doOnError(err -> - log.log(Level.SEVERE, "Error creating connection to RabbitMq Broker. Starting retry process...", err) - ) - .retryWhen(Retry.backoff(Long.MAX_VALUE, Duration.ofMillis(300)) - .maxBackoff(Duration.ofMillis(3000))) - .cache(); - } - - @Bean - public DynamicRegistry dynamicRegistry(ConnectionManager connectionManager, AsyncPropsDomain asyncPropsDomain, - DomainHandlers handlers) { - IBrokerConfigProps brokerConfigProps = new BrokerConfigProps(asyncPropsDomain.getProps(DEFAULT_DOMAIN)); - return new DynamicRegistryImp(handlers.get(DEFAULT_DOMAIN), - connectionManager.getListener(DEFAULT_DOMAIN).getTopologyCreator(), brokerConfigProps); - } - - @Bean - @ConditionalOnMissingBean - public DefaultQueryHandler defaultHandler() { - return (DefaultQueryHandler) command -> - Mono.error(new RuntimeException("No Handler Registered")); - } - - @Bean - @ConditionalOnMissingBean - public DefaultCommandHandler defaultCommandHandler() { - return message -> Mono.error(new RuntimeException("No Handler Registered")); - } - - @Bean - @ConditionalOnMissingBean(HandlerRegistry.class) - public HandlerRegistry defaultHandlerRegistry() { - return HandlerRegistry.register(); - } - - @Bean - @ConditionalOnMissingBean(AsyncPropsDomain.RabbitSecretFiller.class) - public AsyncPropsDomain.RabbitSecretFiller defaultRabbitSecretFiller() { - return (ignoredDomain, ignoredProps) -> { - }; - } - - @Bean - @ConditionalOnMissingBean(RabbitProperties.class) - public RabbitProperties defaultRabbitProperties(RabbitPropertiesAutoConfig properties, ObjectMapperSupplier supplier) { - return supplier.get().convertValue(properties, RabbitProperties.class); - } - -} diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/RabbitProperties.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/RabbitProperties.java index 6c5991cb..8706565a 100644 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/RabbitProperties.java +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/RabbitProperties.java @@ -1,4 +1,6 @@ package org.reactivecommons.async.rabbit.config; +import org.reactivecommons.async.rabbit.config.spring.RabbitPropertiesBase; + public class RabbitProperties extends RabbitPropertiesBase { } diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/RabbitPropertiesAutoConfig.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/RabbitPropertiesAutoConfig.java index 4742df2f..e05cd797 100644 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/RabbitPropertiesAutoConfig.java +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/RabbitPropertiesAutoConfig.java @@ -1,6 +1,8 @@ package org.reactivecommons.async.rabbit.config; +import org.reactivecommons.async.rabbit.config.spring.RabbitPropertiesBase; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Primary; @ConfigurationProperties(prefix = "spring.rabbitmq") public class RabbitPropertiesAutoConfig extends RabbitPropertiesBase { diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncProps.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncProps.java index 4e7f0bc3..3ed6a8b7 100644 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncProps.java +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncProps.java @@ -8,7 +8,7 @@ import lombok.experimental.SuperBuilder; import org.reactivecommons.async.commons.config.IBrokerConfigProps; import org.reactivecommons.async.rabbit.config.RabbitProperties; -import org.reactivecommons.async.starter.GenericAsyncProps; +import org.reactivecommons.async.starter.props.GenericAsyncProps; import org.springframework.boot.context.properties.NestedConfigurationProperty; @@ -63,4 +63,13 @@ public class AsyncProps extends GenericAsyncProps { @Builder.Default private Boolean createTopology = true; // auto delete queues will always be created and bound + @Builder.Default + private boolean useDiscardNotifierPerDomain = false; + + @Builder.Default + private boolean enabled = true; + + @Builder.Default + private String brokerType = "rabbitmq"; + } diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncPropsDomain.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncPropsDomain.java index 115f7030..0398a55c 100644 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncPropsDomain.java +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncPropsDomain.java @@ -3,7 +3,7 @@ import lombok.Getter; import lombok.Setter; import org.reactivecommons.async.rabbit.config.RabbitProperties; -import org.reactivecommons.async.starter.GenericAsyncPropsDomain; +import org.reactivecommons.async.starter.props.GenericAsyncPropsDomain; import org.springframework.beans.factory.annotation.Value; import java.lang.reflect.Constructor; diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncRabbitPropsDomainProperties.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncRabbitPropsDomainProperties.java index 77f05995..a233dfd0 100644 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncRabbitPropsDomainProperties.java +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncRabbitPropsDomainProperties.java @@ -1,7 +1,7 @@ package org.reactivecommons.async.rabbit.config.props; import org.reactivecommons.async.rabbit.config.RabbitProperties; -import org.reactivecommons.async.starter.GenericAsyncPropsDomainProperties; +import org.reactivecommons.async.starter.props.GenericAsyncPropsDomainProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import java.util.Map; diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/RabbitPropertiesBase.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/spring/RabbitPropertiesBase.java similarity index 99% rename from starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/RabbitPropertiesBase.java rename to starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/spring/RabbitPropertiesBase.java index 4849945b..e2651112 100644 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/RabbitPropertiesBase.java +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/spring/RabbitPropertiesBase.java @@ -1,4 +1,4 @@ -package org.reactivecommons.async.rabbit.config; +package org.reactivecommons.async.rabbit.config.spring; import org.springframework.boot.convert.DurationUnit; import org.springframework.util.CollectionUtils; diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/DomainRabbitReactiveHealthIndicator.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/DomainRabbitReactiveHealthIndicator.java deleted file mode 100644 index 45794c64..00000000 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/DomainRabbitReactiveHealthIndicator.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.reactivecommons.async.rabbit.health; - -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import lombok.SneakyThrows; -import lombok.extern.log4j.Log4j2; -import org.reactivecommons.async.rabbit.config.ConnectionManager; -import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; -import org.springframework.boot.actuate.health.Health; -import reactor.core.publisher.Mono; - -import java.net.SocketException; -import java.util.Map; -import java.util.stream.Collectors; - -@Log4j2 -public class DomainRabbitReactiveHealthIndicator extends AbstractReactiveHealthIndicator { - private static final String VERSION = "version"; - private final Map domainProviders; - - public DomainRabbitReactiveHealthIndicator(ConnectionManager manager) { - this.domainProviders = manager.getProviders().entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, entry -> { - ConnectionFactory connection = entry.getValue().getProvider().getConnectionFactory().clone(); - connection.useBlockingIo(); - return connection; - })); - } - - @Override - protected Mono doHealthCheck(Health.Builder builder) { - return Mono.zip(domainProviders.entrySet().stream() - .map(entry -> checkSingle(entry.getKey(), entry.getValue())) - .collect(Collectors.toList()), this::merge); - } - - private Health merge(Object[] results) { - Health.Builder builder = Health.up(); - for (Object obj : results) { - Status status = (Status) obj; - builder.withDetail(status.getDomain(), status.getVersion()); - } - return builder.build(); - } - - private Mono checkSingle(String domain, ConnectionFactory connectionFactory) { - return Mono.defer(() -> Mono.just(getRawVersion(connectionFactory))) - .map(version -> Status.builder().version(version).domain(domain).build()); - } - - @SneakyThrows - private String getRawVersion(ConnectionFactory factory) { - Connection connection = null; - try { - connection = factory.newConnection(); - return connection.getServerProperties().get(VERSION).toString(); - } catch (SocketException e) { - log.warn("Identified error", e); - throw new RuntimeException(e); - } finally { - if (connection != null) { - try { - connection.close(); - } catch (Exception e) { - log.error("Error closing health connection", e); - } - } - } - } -} diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/RabbitReactiveHealthIndicator.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/RabbitReactiveHealthIndicator.java new file mode 100644 index 00000000..dae49699 --- /dev/null +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/RabbitReactiveHealthIndicator.java @@ -0,0 +1,51 @@ +package org.reactivecommons.async.rabbit.health; + +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import lombok.SneakyThrows; +import lombok.extern.log4j.Log4j2; +import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import reactor.core.publisher.Mono; + +import java.net.SocketException; + +@Log4j2 +public class RabbitReactiveHealthIndicator extends AbstractReactiveHealthIndicator { + public static final String VERSION = "version"; + private final String domain; + private final ConnectionFactory connectionFactory; + + public RabbitReactiveHealthIndicator(String domain, ConnectionFactory connectionFactory) { + this.domain = domain; + this.connectionFactory = connectionFactory.clone(); + this.connectionFactory.useBlockingIo(); + } + + @Override + protected Mono doHealthCheck(Health.Builder builder) { + builder.withDetail("domain", domain); + return Mono.fromCallable(() -> getRawVersion(connectionFactory)) + .map(status -> builder.up().withDetail(VERSION, status).build()); + } + + @SneakyThrows + private String getRawVersion(ConnectionFactory factory) { + Connection connection = null; + try { + connection = factory.newConnection(); + return connection.getServerProperties().get(VERSION).toString(); + } catch (SocketException e) { + log.warn("Identified error", e); + throw new RuntimeException(e); + } finally { + if (connection != null) { + try { + connection.close(); + } catch (Exception e) { + log.error("Error closing health connection", e); + } + } + } + } +} diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/Status.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/Status.java deleted file mode 100644 index b3be58a2..00000000 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/Status.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.reactivecommons.async.rabbit.health; - -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class Status { - private final String version; - private final String domain; -} diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/starter/impl/rabbit/RabbitMQConfig.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/starter/impl/rabbit/RabbitMQConfig.java new file mode 100644 index 00000000..50526cb3 --- /dev/null +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/starter/impl/rabbit/RabbitMQConfig.java @@ -0,0 +1,63 @@ +package org.reactivecommons.async.starter.impl.rabbit; + +import lombok.RequiredArgsConstructor; +import lombok.extern.java.Log; +import org.reactivecommons.async.api.DynamicRegistry; +import org.reactivecommons.async.commons.config.IBrokerConfigProps; +import org.reactivecommons.async.commons.converters.json.ObjectMapperSupplier; +import org.reactivecommons.async.rabbit.DynamicRegistryImp; +import org.reactivecommons.async.rabbit.RabbitMQBrokerProviderFactory; +import org.reactivecommons.async.rabbit.RabbitMQSetupUtils; +import org.reactivecommons.async.rabbit.communications.TopologyCreator; +import org.reactivecommons.async.rabbit.config.RabbitProperties; +import org.reactivecommons.async.rabbit.config.RabbitPropertiesAutoConfig; +import org.reactivecommons.async.rabbit.config.props.AsyncProps; +import org.reactivecommons.async.rabbit.config.props.AsyncPropsDomain; +import org.reactivecommons.async.rabbit.config.props.AsyncRabbitPropsDomainProperties; +import org.reactivecommons.async.rabbit.config.props.BrokerConfigProps; +import org.reactivecommons.async.rabbit.converters.json.RabbitJacksonMessageConverter; +import org.reactivecommons.async.starter.config.DomainHandlers; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; + +@Log +@Configuration +@RequiredArgsConstructor +@EnableConfigurationProperties({RabbitPropertiesAutoConfig.class, AsyncRabbitPropsDomainProperties.class}) +@Import({AsyncPropsDomain.class, RabbitMQBrokerProviderFactory.class}) +public class RabbitMQConfig { + + @Bean + @ConditionalOnMissingBean(RabbitJacksonMessageConverter.class) + public RabbitJacksonMessageConverter messageConverter(ObjectMapperSupplier objectMapperSupplier) { + return new RabbitJacksonMessageConverter(objectMapperSupplier.get()); + } + + @Bean + @ConditionalOnMissingBean(DynamicRegistry.class) + public DynamicRegistry dynamicRegistry(AsyncPropsDomain asyncPropsDomain, DomainHandlers handlers) { + AsyncProps props = asyncPropsDomain.getProps(DEFAULT_DOMAIN); + TopologyCreator topologyCreator = RabbitMQSetupUtils.createTopologyCreator(props); + IBrokerConfigProps brokerConfigProps = new BrokerConfigProps(asyncPropsDomain.getProps(DEFAULT_DOMAIN)); + return new DynamicRegistryImp(handlers.get(DEFAULT_DOMAIN), topologyCreator, brokerConfigProps); + } + + @Bean + @ConditionalOnMissingBean(AsyncPropsDomain.RabbitSecretFiller.class) + public AsyncPropsDomain.RabbitSecretFiller defaultRabbitSecretFiller() { + return (ignoredDomain, ignoredProps) -> { + }; + } + + @Bean + @ConditionalOnMissingBean(RabbitProperties.class) + public RabbitProperties defaultRabbitProperties(RabbitPropertiesAutoConfig properties, ObjectMapperSupplier supplier) { + return supplier.get().convertValue(properties, RabbitProperties.class); + } + +} diff --git a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProviderFactoryTest.java b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProviderFactoryTest.java new file mode 100644 index 00000000..534db7cf --- /dev/null +++ b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProviderFactoryTest.java @@ -0,0 +1,74 @@ +package org.reactivecommons.async.rabbit; + +import io.micrometer.core.instrument.MeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivecommons.async.commons.config.BrokerConfig; +import org.reactivecommons.async.commons.config.IBrokerConfigProps; +import org.reactivecommons.async.commons.ext.CustomReporter; +import org.reactivecommons.async.commons.reply.ReactiveReplyRouter; +import org.reactivecommons.async.rabbit.config.RabbitProperties; +import org.reactivecommons.async.rabbit.config.props.AsyncProps; +import org.reactivecommons.async.rabbit.config.props.BrokerConfigProps; +import org.reactivecommons.async.rabbit.converters.json.RabbitJacksonMessageConverter; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.broker.BrokerProviderFactory; +import org.reactivecommons.async.starter.broker.DiscardProvider; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@ExtendWith(MockitoExtension.class) +class RabbitMQBrokerProviderFactoryTest { + private final BrokerConfig config = new BrokerConfig(); + private final ReactiveReplyRouter router = new ReactiveReplyRouter(); + @Mock + private RabbitJacksonMessageConverter converter; + @Mock + private MeterRegistry meterRegistry; + @Mock + private CustomReporter errorReporter; + + private BrokerProviderFactory providerFactory; + + @BeforeEach + public void setUp() { + providerFactory = new RabbitMQBrokerProviderFactory(config, router, converter, meterRegistry, errorReporter); + } + + @Test + void shouldReturnBrokerType() { + // Arrange + // Act + String brokerType = providerFactory.getBrokerType(); + // Assert + assertEquals("rabbitmq", brokerType); + } + + @Test + void shouldReturnCreateDiscardProvider() { + // Arrange + AsyncProps props = new AsyncProps(); + // Act + DiscardProvider discardProvider = providerFactory.getDiscardProvider(props); + // Assert + assertThat(discardProvider).isInstanceOf(RabbitMQDiscardProvider.class); + } + + @Test + void shouldReturnBrokerProvider() { + // Arrange + AsyncProps props = new AsyncProps(); + props.setConnectionProperties(new RabbitProperties()); + IBrokerConfigProps brokerConfigProps = new BrokerConfigProps(props); + props.setBrokerConfigProps(brokerConfigProps); + DiscardProvider discardProvider = providerFactory.getDiscardProvider(props); + // Act + BrokerProvider brokerProvider = providerFactory.getProvider("domain", props, discardProvider); + // Assert + assertThat(brokerProvider).isInstanceOf(RabbitMQBrokerProvider.class); + } +} diff --git a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProviderTest.java b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProviderTest.java new file mode 100644 index 00000000..5fddb65e --- /dev/null +++ b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProviderTest.java @@ -0,0 +1,192 @@ +package org.reactivecommons.async.rabbit; + +import com.rabbitmq.client.AMQP; +import io.micrometer.core.instrument.MeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivecommons.api.domain.DomainEventBus; +import org.reactivecommons.async.api.DirectAsyncGateway; +import org.reactivecommons.async.commons.DiscardNotifier; +import org.reactivecommons.async.commons.HandlerResolver; +import org.reactivecommons.async.commons.config.BrokerConfig; +import org.reactivecommons.async.commons.config.IBrokerConfigProps; +import org.reactivecommons.async.commons.ext.CustomReporter; +import org.reactivecommons.async.commons.reply.ReactiveReplyRouter; +import org.reactivecommons.async.rabbit.communications.ReactiveMessageListener; +import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; +import org.reactivecommons.async.rabbit.communications.TopologyCreator; +import org.reactivecommons.async.rabbit.config.props.AsyncProps; +import org.reactivecommons.async.rabbit.config.props.BrokerConfigProps; +import org.reactivecommons.async.rabbit.converters.json.RabbitJacksonMessageConverter; +import org.reactivecommons.async.rabbit.health.RabbitReactiveHealthIndicator; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.springframework.boot.actuate.health.Health; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.rabbitmq.BindingSpecification; +import reactor.rabbitmq.ExchangeSpecification; +import reactor.rabbitmq.QueueSpecification; +import reactor.rabbitmq.Receiver; +import reactor.test.StepVerifier; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; + +@ExtendWith(MockitoExtension.class) +class RabbitMQBrokerProviderTest { + private final AsyncProps props = new AsyncProps(); + private final BrokerConfig brokerConfig = new BrokerConfig(); + @Mock + private ReactiveMessageListener listener; + @Mock + private TopologyCreator creator; + @Mock + private HandlerResolver handlerResolver; + @Mock + private RabbitJacksonMessageConverter messageConverter; + @Mock + private CustomReporter customReporter; + @Mock + private Receiver receiver; + @Mock + private ReactiveReplyRouter router; + @Mock + private MeterRegistry meterRegistry; + @Mock + private ReactiveMessageSender sender; + @Mock + private DiscardNotifier discardNotifier; + @Mock + private RabbitReactiveHealthIndicator healthIndicator; + + + private BrokerProvider brokerProvider; + + + @BeforeEach + public void init() { + IBrokerConfigProps configProps = new BrokerConfigProps(props); + props.setBrokerConfigProps(configProps); + props.setAppName("test"); + brokerProvider = new RabbitMQBrokerProvider(DEFAULT_DOMAIN, + props, + brokerConfig, + router, + messageConverter, + meterRegistry, + customReporter, + healthIndicator, + listener, + sender, + discardNotifier); + } + + @Test + void shouldCreateDomainEventBus() { + when(sender.getTopologyCreator()).thenReturn(creator); + when(creator.declare(any(ExchangeSpecification.class))).thenReturn(Mono.just(mock(AMQP.Exchange.DeclareOk.class))); + // Act + DomainEventBus domainBus = brokerProvider.getDomainBus(); + // Assert + assertThat(domainBus).isExactlyInstanceOf(RabbitDomainEventBus.class); + } + + @Test + void shouldCreateDirectAsyncGateway() { + when(sender.getTopologyCreator()).thenReturn(creator); + when(listener.getTopologyCreator()).thenReturn(creator); + when(creator.declare(any(ExchangeSpecification.class))).thenReturn(Mono.just(mock(AMQP.Exchange.DeclareOk.class))); + when(creator.bind(any(BindingSpecification.class))).thenReturn(Mono.just(mock(AMQP.Queue.BindOk.class))); + when(creator.declare(any(QueueSpecification.class))).thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); + when(listener.getReceiver()).thenReturn(receiver); + when(receiver.consumeAutoAck(any(String.class))).thenReturn(Flux.never()); + // Act + DirectAsyncGateway domainBus = brokerProvider.getDirectAsyncGateway(handlerResolver); + // Assert + assertThat(domainBus).isExactlyInstanceOf(RabbitDirectAsyncGateway.class); + } + + @Test + void shouldListenDomainEvents() { + when(listener.getTopologyCreator()).thenReturn(creator); + when(creator.declare(any(ExchangeSpecification.class))).thenReturn(Mono.just(mock(AMQP.Exchange.DeclareOk.class))); + when(creator.declareQueue(any(String.class), any())).thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); + when(listener.getReceiver()).thenReturn(receiver); + when(listener.getMaxConcurrency()).thenReturn(1); + when(receiver.consumeManualAck(any(String.class), any())).thenReturn(Flux.never()); + // Act + brokerProvider.listenDomainEvents(handlerResolver); + // Assert + verify(receiver, times(1)).consumeManualAck(any(String.class), any()); + } + + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + void shouldListenNotificationEvents() { + when(listener.getTopologyCreator()).thenReturn(creator); + when(creator.declare(any(ExchangeSpecification.class))).thenReturn(Mono.just(mock(AMQP.Exchange.DeclareOk.class))); + when(creator.declare(any(QueueSpecification.class))).thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); + when(listener.getReceiver()).thenReturn(receiver); + when(listener.getMaxConcurrency()).thenReturn(1); + when(receiver.consumeManualAck(any(String.class), any())).thenReturn(Flux.never()); + List mockedListeners = spy(List.of()); + when(mockedListeners.isEmpty()).thenReturn(false); + when(handlerResolver.getNotificationListeners()).thenReturn(mockedListeners); + // Act + brokerProvider.listenNotificationEvents(handlerResolver); + // Assert + verify(receiver, times(1)).consumeManualAck(any(String.class), any()); + } + + @Test + void shouldListenCommands() { + when(listener.getTopologyCreator()).thenReturn(creator); + when(creator.declare(any(ExchangeSpecification.class))).thenReturn(Mono.just(mock(AMQP.Exchange.DeclareOk.class))); + when(creator.declareQueue(any(String.class), any())).thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); + when(creator.bind(any(BindingSpecification.class))).thenReturn(Mono.just(mock(AMQP.Queue.BindOk.class))); + when(listener.getReceiver()).thenReturn(receiver); + when(listener.getMaxConcurrency()).thenReturn(1); + when(receiver.consumeManualAck(any(String.class), any())).thenReturn(Flux.never()); + // Act + brokerProvider.listenCommands(handlerResolver); + // Assert + verify(receiver, times(1)).consumeManualAck(any(String.class), any()); + } + + @Test + void shouldListenQueries() { + when(listener.getTopologyCreator()).thenReturn(creator); + when(creator.declare(any(ExchangeSpecification.class))).thenReturn(Mono.just(mock(AMQP.Exchange.DeclareOk.class))); + when(creator.declareQueue(any(String.class), any())).thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); + when(creator.bind(any(BindingSpecification.class))).thenReturn(Mono.just(mock(AMQP.Queue.BindOk.class))); + when(listener.getReceiver()).thenReturn(receiver); + when(listener.getMaxConcurrency()).thenReturn(1); + when(receiver.consumeManualAck(any(String.class), any())).thenReturn(Flux.never()); + // Act + brokerProvider.listenQueries(handlerResolver); + // Assert + verify(receiver, times(1)).consumeManualAck(any(String.class), any()); + } + + @Test + void shouldProxyHealthCheck() { + when(healthIndicator.health()).thenReturn(Mono.fromSupplier(() -> Health.up().build())); + // Act + Mono flow = brokerProvider.healthCheck(); + // Assert + StepVerifier.create(flow) + .expectNextMatches(health -> health.getStatus().getCode().equals("UP")) + .verifyComplete(); + } +} diff --git a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/RabbitMQDiscardProviderTest.java b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/RabbitMQDiscardProviderTest.java new file mode 100644 index 00000000..5243b48a --- /dev/null +++ b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/RabbitMQDiscardProviderTest.java @@ -0,0 +1,37 @@ +package org.reactivecommons.async.rabbit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivecommons.async.commons.DLQDiscardNotifier; +import org.reactivecommons.async.commons.DiscardNotifier; +import org.reactivecommons.async.commons.config.BrokerConfig; +import org.reactivecommons.async.commons.config.IBrokerConfigProps; +import org.reactivecommons.async.rabbit.config.RabbitProperties; +import org.reactivecommons.async.rabbit.config.props.AsyncProps; +import org.reactivecommons.async.rabbit.config.props.BrokerConfigProps; +import org.reactivecommons.async.rabbit.converters.json.RabbitJacksonMessageConverter; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class RabbitMQDiscardProviderTest { + @Mock + private RabbitJacksonMessageConverter converter; + + @Test + void shouldCreateDiscardNotifier() { + // Arrange + AsyncProps props = new AsyncProps(); + props.setConnectionProperties(new RabbitProperties()); + IBrokerConfigProps brokerConfigProps = new BrokerConfigProps(props); + props.setBrokerConfigProps(brokerConfigProps); + BrokerConfig brokerConfig = new BrokerConfig(); + RabbitMQDiscardProvider discardProvider = new RabbitMQDiscardProvider(props, brokerConfig, converter); + // Act + DiscardNotifier notifier = discardProvider.get(); + // Assert + assertThat(notifier).isExactlyInstanceOf(DLQDiscardNotifier.class); + } +} diff --git a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/CommandListenersConfigTest.java b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/CommandListenersConfigTest.java deleted file mode 100644 index cd9169ea..00000000 --- a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/CommandListenersConfigTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.reactivecommons.async.rabbit.config; - -import com.rabbitmq.client.AMQP; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; -import org.reactivecommons.async.commons.HandlerResolver; -import org.reactivecommons.async.commons.converters.MessageConverter; -import org.reactivecommons.async.commons.ext.CustomReporter; -import org.reactivecommons.async.rabbit.communications.ReactiveMessageListener; -import org.reactivecommons.async.rabbit.communications.TopologyCreator; -import org.reactivecommons.async.rabbit.config.props.AsyncProps; -import org.reactivecommons.async.rabbit.config.props.AsyncPropsDomain; -import org.reactivecommons.async.rabbit.listeners.ApplicationCommandListener; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.rabbitmq.BindingSpecification; -import reactor.rabbitmq.ConsumeOptions; -import reactor.rabbitmq.ExchangeSpecification; -import reactor.rabbitmq.Receiver; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; - -@ExtendWith(MockitoExtension.class) -class CommandListenersConfigTest { - - - private final AsyncProps props = new AsyncProps(); - private final AsyncPropsDomain asyncPropsDomain = AsyncPropsDomain.builder() - .withDefaultAppName("appName") - .withDefaultProperties(new RabbitProperties()) - .withDomain(DEFAULT_DOMAIN, props) - .build(); - private final CommandListenersConfig config = new CommandListenersConfig(asyncPropsDomain); - private final ReactiveMessageListener listener = mock(ReactiveMessageListener.class); - private final TopologyCreator creator = mock(TopologyCreator.class); - private final HandlerResolver handlerResolver = mock(HandlerResolver.class); - private final MessageConverter messageConverter = mock(MessageConverter.class); - private final CustomReporter customReporter = mock(CustomReporter.class); - private final Receiver receiver = mock(Receiver.class); - private final ConnectionManager manager = new ConnectionManager(); - private final DomainHandlers handlers = new DomainHandlers(); - - @BeforeEach - public void init() { - when(creator.bind(any(BindingSpecification.class))).thenReturn(Mono.just(mock(AMQP.Queue.BindOk.class))); - when(creator.declare(any(ExchangeSpecification.class))).thenReturn(Mono.just(mock(AMQP.Exchange.DeclareOk.class))); - when(creator.declareQueue(any(String.class), any())).thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); - when(listener.getTopologyCreator()).thenReturn(creator); - when(receiver.consumeManualAck(any(String.class), any(ConsumeOptions.class))).thenReturn(Flux.never()); - when(listener.getReceiver()).thenReturn(receiver); - when(listener.getMaxConcurrency()).thenReturn(20); - manager.addDomain(DEFAULT_DOMAIN, listener, null, null); - handlers.add(DEFAULT_DOMAIN, handlerResolver); - } - - @Test - void applicationCommandListener() { - final ApplicationCommandListener commandListener = config.applicationCommandListener(manager, handlers, messageConverter, customReporter); - Assertions.assertThat(commandListener).isNotNull(); - } -} diff --git a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/EventListenersConfigTest.java b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/EventListenersConfigTest.java deleted file mode 100644 index 82494ee8..00000000 --- a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/EventListenersConfigTest.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.reactivecommons.async.rabbit.config; - -import com.rabbitmq.client.AMQP; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.reactivecommons.async.api.HandlerRegistry; -import org.reactivecommons.async.commons.HandlerResolver; -import org.reactivecommons.async.commons.converters.MessageConverter; -import org.reactivecommons.async.commons.ext.CustomReporter; -import org.reactivecommons.async.rabbit.communications.ReactiveMessageListener; -import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; -import org.reactivecommons.async.rabbit.communications.TopologyCreator; -import org.reactivecommons.async.rabbit.config.props.AsyncProps; -import org.reactivecommons.async.rabbit.config.props.AsyncPropsDomain; -import org.reactivecommons.async.rabbit.listeners.ApplicationEventListener; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.rabbitmq.BindingSpecification; -import reactor.rabbitmq.ConsumeOptions; -import reactor.rabbitmq.ExchangeSpecification; -import reactor.rabbitmq.QueueSpecification; -import reactor.rabbitmq.Receiver; - -import java.util.Collections; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; - -class EventListenersConfigTest { - - private final AsyncProps props = new AsyncProps(); - private final AsyncPropsDomain asyncPropsDomain = AsyncPropsDomain.builder() - .withDefaultAppName("appName") - .withDefaultProperties(new RabbitProperties()) - .withDomain(DEFAULT_DOMAIN, props) - .build(); - private final EventListenersConfig config = new EventListenersConfig(asyncPropsDomain); - private final ReactiveMessageListener listener = mock(ReactiveMessageListener.class); - private final ReactiveMessageSender sender = mock(ReactiveMessageSender.class); - private final TopologyCreator creator = mock(TopologyCreator.class); - private final HandlerResolver handlerResolver = mock(HandlerResolver.class); - private final MessageConverter messageConverter = mock(MessageConverter.class); - private final CustomReporter customReporter = mock(CustomReporter.class); - private final Receiver receiver = mock(Receiver.class); - private ConnectionManager connectionManager; - private final DomainHandlers handlers = new DomainHandlers(); - - @BeforeEach - public void init() { - when(handlerResolver.getEventListeners()).thenReturn(Collections.emptyList()); - when(creator.bind(any(BindingSpecification.class))).thenReturn(Mono.just(mock(AMQP.Queue.BindOk.class))); - when(creator.declare(any(ExchangeSpecification.class))).thenReturn(Mono.just(mock(AMQP.Exchange.DeclareOk.class))); - when(creator.declare(any(QueueSpecification.class))).thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); - when(creator.declareDLQ(any(String.class), any(String.class), any(Integer.class), any())).thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); - when(creator.declareQueue(any(String.class), any())).thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); - when(creator.declareQueue(any(String.class), any(String.class), any())).thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); - when(listener.getTopologyCreator()).thenReturn(creator); - when(receiver.consumeManualAck(any(String.class), any(ConsumeOptions.class))).thenReturn(Flux.never()); - when(listener.getReceiver()).thenReturn(receiver); - when(listener.getMaxConcurrency()).thenReturn(20); - connectionManager = new ConnectionManager(); - connectionManager.addDomain(HandlerRegistry.DEFAULT_DOMAIN, listener, sender, null); - handlers.add(HandlerRegistry.DEFAULT_DOMAIN, handlerResolver); - } - - @Test - void eventListener() { - final ApplicationEventListener eventListener = config.eventListener( - messageConverter, - connectionManager, - handlers, - customReporter - ); - - Assertions.assertThat(eventListener).isNotNull(); - } -} diff --git a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/NotificationListenersConfigTest.java b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/NotificationListenersConfigTest.java deleted file mode 100644 index 30f67fca..00000000 --- a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/NotificationListenersConfigTest.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.reactivecommons.async.rabbit.config; - -import com.rabbitmq.client.AMQP; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.reactivecommons.async.commons.DiscardNotifier; -import org.reactivecommons.async.commons.HandlerResolver; -import org.reactivecommons.async.commons.converters.MessageConverter; -import org.reactivecommons.async.commons.ext.CustomReporter; -import org.reactivecommons.async.rabbit.communications.ReactiveMessageListener; -import org.reactivecommons.async.rabbit.communications.TopologyCreator; -import org.reactivecommons.async.rabbit.config.props.AsyncProps; -import org.reactivecommons.async.rabbit.config.props.AsyncPropsDomain; -import org.reactivecommons.async.rabbit.listeners.ApplicationNotificationListener; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.rabbitmq.BindingSpecification; -import reactor.rabbitmq.ConsumeOptions; -import reactor.rabbitmq.ExchangeSpecification; -import reactor.rabbitmq.QueueSpecification; -import reactor.rabbitmq.Receiver; - -import java.util.Collections; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; - -class NotificationListenersConfigTest { - - private final AsyncProps props = new AsyncProps(); - private final AsyncPropsDomain asyncPropsDomain = AsyncPropsDomain.builder() - .withDefaultAppName("appName") - .withDefaultProperties(new RabbitProperties()) - .withDomain(DEFAULT_DOMAIN, props) - .build(); - private final NotificationListenersConfig config = new NotificationListenersConfig(asyncPropsDomain); - private final ReactiveMessageListener listener = mock(ReactiveMessageListener.class); - private final TopologyCreator creator = mock(TopologyCreator.class); - private final HandlerResolver handlerResolver = mock(HandlerResolver.class); - private final MessageConverter messageConverter = mock(MessageConverter.class); - private final DiscardNotifier discardNotifier = mock(DiscardNotifier.class); - private final CustomReporter customReporter = mock(CustomReporter.class); - private final Receiver receiver = mock(Receiver.class); - private final ConnectionManager manager = new ConnectionManager(); - private final DomainHandlers handlers = new DomainHandlers(); - - @BeforeEach - public void init() { - when(handlerResolver.getEventListeners()).thenReturn(Collections.emptyList()); - when(creator.bind(any(BindingSpecification.class))).thenReturn(Mono.just(mock(AMQP.Queue.BindOk.class))); - when(creator.declare(any(ExchangeSpecification.class))).thenReturn(Mono.just(mock(AMQP.Exchange.DeclareOk.class))); - when(creator.declare(any(QueueSpecification.class))).thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); - when(creator.declareDLQ(any(String.class), any(String.class), any(Integer.class), any())).thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); - when(creator.declareQueue(any(String.class), any())).thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); - when(creator.declareQueue(any(String.class), any(String.class), any())).thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); - when(listener.getTopologyCreator()).thenReturn(creator); - when(receiver.consumeManualAck(any(String.class), any(ConsumeOptions.class))).thenReturn(Flux.never()); - when(listener.getReceiver()).thenReturn(receiver); - when(listener.getMaxConcurrency()).thenReturn(20); - manager.addDomain(DEFAULT_DOMAIN, listener, null, null); - handlers.add(DEFAULT_DOMAIN, handlerResolver); - } - - @Test - void eventNotificationListener() { - final ApplicationNotificationListener applicationEventListener = - config.eventNotificationListener(manager, handlers, messageConverter, customReporter); - Assertions.assertThat(applicationEventListener).isNotNull(); - } -} diff --git a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/QueryListenerConfigTest.java b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/QueryListenerConfigTest.java deleted file mode 100644 index 143f9a32..00000000 --- a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/QueryListenerConfigTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.reactivecommons.async.rabbit.config; - -import com.rabbitmq.client.AMQP; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.reactivecommons.async.commons.HandlerResolver; -import org.reactivecommons.async.commons.converters.MessageConverter; -import org.reactivecommons.async.commons.ext.CustomReporter; -import org.reactivecommons.async.rabbit.communications.ReactiveMessageListener; -import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; -import org.reactivecommons.async.rabbit.communications.TopologyCreator; -import org.reactivecommons.async.rabbit.config.props.AsyncProps; -import org.reactivecommons.async.rabbit.config.props.AsyncPropsDomain; -import org.reactivecommons.async.rabbit.listeners.ApplicationQueryListener; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.rabbitmq.BindingSpecification; -import reactor.rabbitmq.ConsumeOptions; -import reactor.rabbitmq.ExchangeSpecification; -import reactor.rabbitmq.QueueSpecification; -import reactor.rabbitmq.Receiver; - -import java.util.Collections; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; - -class QueryListenerConfigTest { - - private final AsyncProps props = new AsyncProps(); - private final AsyncPropsDomain asyncPropsDomain = AsyncPropsDomain.builder() - .withDefaultAppName("appName") - .withDefaultProperties(new RabbitProperties()) - .withDomain(DEFAULT_DOMAIN, props) - .build(); - private final QueryListenerConfig config = new QueryListenerConfig(asyncPropsDomain); - private final ReactiveMessageListener listener = mock(ReactiveMessageListener.class); - private final TopologyCreator creator = mock(TopologyCreator.class); - private final HandlerResolver handlerResolver = mock(HandlerResolver.class); - private final MessageConverter messageConverter = mock(MessageConverter.class); - private final CustomReporter customReporter = mock(CustomReporter.class); - private final Receiver receiver = mock(Receiver.class); - private final ReactiveMessageSender sender = mock(ReactiveMessageSender.class); - private final ConnectionManager manager = new ConnectionManager(); - private final DomainHandlers handlers = new DomainHandlers(); - - @BeforeEach - public void init() { - when(handlerResolver.getEventListeners()).thenReturn(Collections.emptyList()); - when(creator.bind(any(BindingSpecification.class))).thenReturn(Mono.just(mock(AMQP.Queue.BindOk.class))); - when(creator.declare(any(ExchangeSpecification.class))).thenReturn(Mono.just(mock(AMQP.Exchange.DeclareOk.class))); - when(creator.declare(any(QueueSpecification.class))).thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); - when(creator.declareDLQ(any(String.class), any(String.class), any(Integer.class), any())).thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); - when(creator.declareQueue(any(String.class), any())).thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); - when(creator.declareQueue(any(String.class), any(String.class), any())).thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); - when(listener.getTopologyCreator()).thenReturn(creator); - when(receiver.consumeManualAck(any(String.class), any(ConsumeOptions.class))).thenReturn(Flux.never()); - when(listener.getReceiver()).thenReturn(receiver); - when(listener.getMaxConcurrency()).thenReturn(20); - manager.addDomain(DEFAULT_DOMAIN, listener, sender, null); - handlers.add(DEFAULT_DOMAIN, handlerResolver); - } - - @Test - void queryListener() { - final ApplicationQueryListener queryListener = config.queryListener(messageConverter, handlers, manager, customReporter); - Assertions.assertThat(queryListener).isNotNull(); - } -} diff --git a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/RabbitMqConfigTest.java b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/RabbitMqConfigTest.java deleted file mode 100644 index 450d2f2a..00000000 --- a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/RabbitMqConfigTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.reactivecommons.async.rabbit.config; - -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import org.junit.jupiter.api.Test; -import org.reactivecommons.api.domain.Command; -import org.reactivecommons.api.domain.DomainEvent; -import org.reactivecommons.async.api.AsyncQuery; -import org.reactivecommons.async.commons.communications.Message; -import org.reactivecommons.async.commons.ext.CustomReporter; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -import java.io.IOException; -import java.time.Duration; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class RabbitMqConfigTest { - - RabbitMqConfig config = new RabbitMqConfig(); - - @Test - void retryInitialConnection() throws IOException, TimeoutException { - final String connectionType = "sender"; - final String appName = "appName"; - final String connectionName = "appName sender"; - - final AtomicInteger count = new AtomicInteger(); - final Connection connection = mock(Connection.class); - ConnectionFactory factory = mock(ConnectionFactory.class); - when(factory.newConnection(connectionName)).thenAnswer(invocation -> { - if(count.incrementAndGet() == 10){ - return connection; - } - throw new RuntimeException(); - }); - StepVerifier.withVirtualTime(() -> config.createConnectionMono(factory, appName, connectionType)) - .thenAwait(Duration.ofMinutes(2)) - .expectNext(connection).verifyComplete(); - } - - @Test - void shouldCreateDefaultErrorReporter() { - final CustomReporter errorReporter = config.reactiveCommonsCustomErrorReporter(); - assertThat(errorReporter.reportError(mock(Throwable.class), mock(Message.class), mock(Command.class), true)).isNotNull(); - assertThat(errorReporter.reportError(mock(Throwable.class), mock(Message.class), mock(DomainEvent.class), true)).isNotNull(); - assertThat(errorReporter.reportError(mock(Throwable.class), mock(Message.class), mock(AsyncQuery.class), true)).isNotNull(); - } - - @Test - void shouldGenerateDefaultReeporter() { - final CustomReporter customReporter = config.reactiveCommonsCustomErrorReporter(); - final Mono r1 = customReporter.reportError(mock(Throwable.class), mock(Message.class), mock(Command.class), true); - final Mono r2 = customReporter.reportError(mock(Throwable.class), mock(Message.class), mock(DomainEvent.class), true); - final Mono r3 = customReporter.reportError(mock(Throwable.class), mock(Message.class), mock(AsyncQuery.class), true); - - assertThat(r1).isNotNull(); - assertThat(r2).isNotNull(); - assertThat(r3).isNotNull(); - - } -} diff --git a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/health/DomainRabbitReactiveHealthIndicatorTest.java b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/health/RabbitReactiveHealthIndicatorTest.java similarity index 58% rename from starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/health/DomainRabbitReactiveHealthIndicatorTest.java rename to starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/health/RabbitReactiveHealthIndicatorTest.java index cff1e660..e2f2cc7e 100644 --- a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/health/DomainRabbitReactiveHealthIndicatorTest.java +++ b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/health/RabbitReactiveHealthIndicatorTest.java @@ -7,8 +7,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.reactivecommons.async.rabbit.config.ConnectionFactoryProvider; -import org.reactivecommons.async.rabbit.config.ConnectionManager; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Health.Builder; import org.springframework.boot.actuate.health.Status; @@ -16,35 +14,29 @@ import reactor.test.StepVerifier; import java.io.IOException; +import java.net.SocketException; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.TimeoutException; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; @ExtendWith(MockitoExtension.class) -public class DomainRabbitReactiveHealthIndicatorTest { - @Mock - private ConnectionFactoryProvider provider; +class RabbitReactiveHealthIndicatorTest { @Mock private ConnectionFactory factory; @Mock private Connection connection; - private DomainRabbitReactiveHealthIndicator indicator; + private RabbitReactiveHealthIndicator indicator; @BeforeEach void setup() { - when(provider.getConnectionFactory()).thenReturn(factory); when(factory.clone()).thenReturn(factory); - - ConnectionManager connectionManager = new ConnectionManager(); - connectionManager.addDomain(DEFAULT_DOMAIN, null, null, provider); - connectionManager.addDomain("domain2", null, null, provider); - connectionManager.addDomain("domain3", null, null, provider); - indicator = new DomainRabbitReactiveHealthIndicator(connectionManager); + indicator = new RabbitReactiveHealthIndicator(DEFAULT_DOMAIN, factory); } @Test @@ -59,9 +51,28 @@ void shouldBeUp() throws IOException, TimeoutException { // Assert StepVerifier.create(result) .assertNext(health -> { - assertEquals("1.2.3", health.getDetails().get(DEFAULT_DOMAIN)); - assertEquals("1.2.3", health.getDetails().get("domain2")); - assertEquals("1.2.3", health.getDetails().get("domain3")); + assertEquals(DEFAULT_DOMAIN, health.getDetails().get("domain")); + assertEquals("1.2.3", health.getDetails().get("version")); + assertEquals(Status.UP, health.getStatus()); + }) + .verifyComplete(); + } + + @Test + void shouldBeUpAndIgnoreCloseError() throws IOException, TimeoutException { + // Arrange + Map properties = new TreeMap<>(); + properties.put("version", "1.2.3"); + when(factory.newConnection()).thenReturn(connection); + when(connection.getServerProperties()).thenReturn(properties); + doThrow(new IOException("Error closing connection")).when(connection).close(); + // Act + Mono result = indicator.doHealthCheck(new Builder()); + // Assert + StepVerifier.create(result) + .assertNext(health -> { + assertEquals(DEFAULT_DOMAIN, health.getDetails().get("domain")); + assertEquals("1.2.3", health.getDetails().get("version")); assertEquals(Status.UP, health.getStatus()); }) .verifyComplete(); @@ -78,4 +89,16 @@ void shouldBeDown() throws IOException, TimeoutException { .expectError(TimeoutException.class) .verify(); } + + @Test + void shouldBeDownWhenSocketException() throws IOException, TimeoutException { + // Arrange + when(factory.newConnection()).thenThrow(new SocketException("Connection timeout")); + // Act + Mono result = indicator.doHealthCheck(new Builder()); + // Assert + StepVerifier.create(result) + .expectError(RuntimeException.class) + .verify(); + } } diff --git a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/starter/impl/rabbit/RabbitMQConfigTest.java b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/starter/impl/rabbit/RabbitMQConfigTest.java new file mode 100644 index 00000000..240fe4b7 --- /dev/null +++ b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/starter/impl/rabbit/RabbitMQConfigTest.java @@ -0,0 +1,42 @@ +package org.reactivecommons.async.starter.impl.rabbit; + +import org.junit.jupiter.api.Test; +import org.reactivecommons.async.rabbit.RabbitMQBrokerProviderFactory; +import org.reactivecommons.async.rabbit.config.props.AsyncPropsDomain; +import org.reactivecommons.async.rabbit.converters.json.RabbitJacksonMessageConverter; +import org.reactivecommons.async.starter.config.ConnectionManager; +import org.reactivecommons.async.starter.config.ReactiveCommonsConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(classes = { + RabbitMQConfig.class, + AsyncPropsDomain.class, + RabbitMQBrokerProviderFactory.class, + ReactiveCommonsConfig.class}) +class RabbitMQConfigTest { + @Autowired + private RabbitJacksonMessageConverter converter; + @Autowired + private ConnectionManager manager; + + @Test + void shouldHasConverter() { + // Arrange + // Act + // Assert + assertThat(converter).isNotNull(); + } + + @Test + void shouldHasManager() { + // Arrange + // Act + // Assert + assertThat(manager).isNotNull(); + assertThat(manager.getProviders()).isNotEmpty(); + assertThat(manager.getProviders().get("app").getProps().getAppName()).isEqualTo("test-app"); + } +} diff --git a/starters/shared/shared-starter.gradle b/starters/shared/shared-starter.gradle deleted file mode 100644 index c135d06e..00000000 --- a/starters/shared/shared-starter.gradle +++ /dev/null @@ -1,10 +0,0 @@ -ext { - artifactId = 'shared-starter' - artifactDescription = 'Shared Starter' -} - -dependencies { - compileOnly project(':async-commons-api') - implementation 'com.fasterxml.jackson.core:jackson-databind' - implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' -} \ No newline at end of file From ea68fce517907548bbe4e6db9064745bf06aad8b Mon Sep 17 00:00:00 2001 From: Juan C Galvis <8420868+juancgalvis@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:31:24 -0500 Subject: [PATCH 02/12] build(sonar): change sonar execution rule --- .github/workflows/main.yml | 9 ++- docs/package-lock.json | 114 +++++++++++++++++++++---------------- 2 files changed, 68 insertions(+), 55 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 407bece5..b146e555 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,14 +40,13 @@ jobs: distribution: temurin java-version: 17 - name: Execute build test jacocoTestReport and sonar analysis - if: endsWith(github.REF, '/master') == true + if: endsWith(github.REF, '/master') == true || github.event.pull_request.head.repo.fork == false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew build test jacocoTestReport sonar --refresh-dependencies --no-daemon --continue -Denv.ci=true + run: ./gradlew clean build generateMergedReport sonar --refresh-dependencies --no-daemon --continue -Denv.ci=true - name: Execute build test jacocoTestReport pull request - if: endsWith(github.REF, '/merge') == true + if: github.event.pull_request.head.repo.fork == true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew build test jacocoTestReport --refresh-dependencies --no-daemon --continue -Denv.ci=true \ No newline at end of file + run: ./gradlew clean build generateMergedReport --refresh-dependencies --no-daemon --continue -Denv.ci=true \ No newline at end of file diff --git a/docs/package-lock.json b/docs/package-lock.json index 97b8f37e..82daf899 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -4257,9 +4257,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -4269,7 +4269,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -4948,9 +4948,9 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "engines": { "node": ">= 0.6" } @@ -5797,9 +5797,9 @@ } }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "engines": { "node": ">= 0.8" } @@ -6102,36 +6102,36 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -6167,9 +6167,9 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/express/node_modules/range-parser": { "version": "1.2.1", @@ -6354,12 +6354,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -8615,9 +8615,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -10558,9 +10561,12 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -10888,9 +10894,9 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "dependencies": { "isarray": "0.0.1" } @@ -11696,11 +11702,11 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -12719,9 +12725,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -12754,6 +12760,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -12866,14 +12880,14 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" From 90d08ef3934768b83af485267d6f022f4a3326a6 Mon Sep 17 00:00:00 2001 From: Juan C Galvis <8420868+juancgalvis@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:36:53 -0500 Subject: [PATCH 03/12] build(sonar): change sonar execution rule --- main.gradle | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/main.gradle b/main.gradle index 9249d5ca..2c190ef0 100644 --- a/main.gradle +++ b/main.gradle @@ -12,23 +12,6 @@ allprojects { } group 'org.reactivecommons' - - sonar { - properties { - property "sonar.sourceEncoding", "UTF-8" - property 'sonar.projectKey', 'reactive-commons_reactive-commons-java' - property 'sonar.organization', 'reactive-commons' - property 'sonar.host.url', 'https://sonarcloud.io' - property "sonar.sources", "src/main" - property "sonar.test", "src/test" - property "sonar.java.binaries", "build/classes" - property "sonar.junit.reportPaths", "build/test-results/test" - property "sonar.java-coveragePlugin", "jacoco" - property "sonar.coverage.jacoco.xmlReportPaths", "${rootDir}/build/reports/jacoco/generateMergedReport/generateMergedReport.xml" - property "sonar.exclusions", ".github/**,samples/**/*" - property 'sonar.coverage.exclusions', 'samples/**/*' - } - } } nexusPublishing { @@ -87,6 +70,23 @@ subprojects { group groupId + sonar { + properties { + property "sonar.sourceEncoding", "UTF-8" + property 'sonar.projectKey', 'reactive-commons_reactive-commons-java' + property 'sonar.organization', 'reactive-commons' + property 'sonar.host.url', 'https://sonarcloud.io' + property "sonar.sources", "src/main" + property "sonar.test", "src/test" + property "sonar.java.binaries", "build/classes" + property "sonar.junit.reportPaths", "build/test-results/test" + property "sonar.java-coveragePlugin", "jacoco" + property "sonar.coverage.jacoco.xmlReportPaths", "${rootDir}/build/reports/jacoco/generateMergedReport/generateMergedReport.xml" + property "sonar.exclusions", ".github/**,samples/**/*" + property 'sonar.coverage.exclusions', 'samples/**/*' + } + } + tasks.named("jar") { enabled = true archiveClassifier = '' From 0f03f8f5c72b7ce1be11fa41f6a74ba9b8e928e7 Mon Sep 17 00:00:00 2001 From: Juan C Galvis <8420868+juancgalvis@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:38:12 -0500 Subject: [PATCH 04/12] build(sonar): change sonar execution rule --- main.gradle | 34 +++++++++---------- .../no-springboot-client.gradle | 3 -- 2 files changed, 17 insertions(+), 20 deletions(-) delete mode 100644 samples/async/no-springboot-client/no-springboot-client.gradle diff --git a/main.gradle b/main.gradle index 2c190ef0..9249d5ca 100644 --- a/main.gradle +++ b/main.gradle @@ -12,6 +12,23 @@ allprojects { } group 'org.reactivecommons' + + sonar { + properties { + property "sonar.sourceEncoding", "UTF-8" + property 'sonar.projectKey', 'reactive-commons_reactive-commons-java' + property 'sonar.organization', 'reactive-commons' + property 'sonar.host.url', 'https://sonarcloud.io' + property "sonar.sources", "src/main" + property "sonar.test", "src/test" + property "sonar.java.binaries", "build/classes" + property "sonar.junit.reportPaths", "build/test-results/test" + property "sonar.java-coveragePlugin", "jacoco" + property "sonar.coverage.jacoco.xmlReportPaths", "${rootDir}/build/reports/jacoco/generateMergedReport/generateMergedReport.xml" + property "sonar.exclusions", ".github/**,samples/**/*" + property 'sonar.coverage.exclusions', 'samples/**/*' + } + } } nexusPublishing { @@ -70,23 +87,6 @@ subprojects { group groupId - sonar { - properties { - property "sonar.sourceEncoding", "UTF-8" - property 'sonar.projectKey', 'reactive-commons_reactive-commons-java' - property 'sonar.organization', 'reactive-commons' - property 'sonar.host.url', 'https://sonarcloud.io' - property "sonar.sources", "src/main" - property "sonar.test", "src/test" - property "sonar.java.binaries", "build/classes" - property "sonar.junit.reportPaths", "build/test-results/test" - property "sonar.java-coveragePlugin", "jacoco" - property "sonar.coverage.jacoco.xmlReportPaths", "${rootDir}/build/reports/jacoco/generateMergedReport/generateMergedReport.xml" - property "sonar.exclusions", ".github/**,samples/**/*" - property 'sonar.coverage.exclusions', 'samples/**/*' - } - } - tasks.named("jar") { enabled = true archiveClassifier = '' diff --git a/samples/async/no-springboot-client/no-springboot-client.gradle b/samples/async/no-springboot-client/no-springboot-client.gradle deleted file mode 100644 index 5685503e..00000000 --- a/samples/async/no-springboot-client/no-springboot-client.gradle +++ /dev/null @@ -1,3 +0,0 @@ -dependencies { - implementation project(':async-commons-rabbit-starter') -} \ No newline at end of file From 4dcc2e1c1325a5a7e82229f2494a2e0b09dd84ad Mon Sep 17 00:00:00 2001 From: Juan C Galvis <8420868+juancgalvis@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:42:53 -0500 Subject: [PATCH 05/12] build(sonar): change sonar execution rule --- main.gradle | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/main.gradle b/main.gradle index 9249d5ca..2c190ef0 100644 --- a/main.gradle +++ b/main.gradle @@ -12,23 +12,6 @@ allprojects { } group 'org.reactivecommons' - - sonar { - properties { - property "sonar.sourceEncoding", "UTF-8" - property 'sonar.projectKey', 'reactive-commons_reactive-commons-java' - property 'sonar.organization', 'reactive-commons' - property 'sonar.host.url', 'https://sonarcloud.io' - property "sonar.sources", "src/main" - property "sonar.test", "src/test" - property "sonar.java.binaries", "build/classes" - property "sonar.junit.reportPaths", "build/test-results/test" - property "sonar.java-coveragePlugin", "jacoco" - property "sonar.coverage.jacoco.xmlReportPaths", "${rootDir}/build/reports/jacoco/generateMergedReport/generateMergedReport.xml" - property "sonar.exclusions", ".github/**,samples/**/*" - property 'sonar.coverage.exclusions', 'samples/**/*' - } - } } nexusPublishing { @@ -87,6 +70,23 @@ subprojects { group groupId + sonar { + properties { + property "sonar.sourceEncoding", "UTF-8" + property 'sonar.projectKey', 'reactive-commons_reactive-commons-java' + property 'sonar.organization', 'reactive-commons' + property 'sonar.host.url', 'https://sonarcloud.io' + property "sonar.sources", "src/main" + property "sonar.test", "src/test" + property "sonar.java.binaries", "build/classes" + property "sonar.junit.reportPaths", "build/test-results/test" + property "sonar.java-coveragePlugin", "jacoco" + property "sonar.coverage.jacoco.xmlReportPaths", "${rootDir}/build/reports/jacoco/generateMergedReport/generateMergedReport.xml" + property "sonar.exclusions", ".github/**,samples/**/*" + property 'sonar.coverage.exclusions', 'samples/**/*' + } + } + tasks.named("jar") { enabled = true archiveClassifier = '' From a80a6953c1241a8b6b3526527b0b5b9495e16bc7 Mon Sep 17 00:00:00 2001 From: Juan C Galvis <8420868+juancgalvis@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:49:26 -0500 Subject: [PATCH 06/12] build(sonar): change sonar execution rule --- main.gradle | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/main.gradle b/main.gradle index 2c190ef0..f87daa73 100644 --- a/main.gradle +++ b/main.gradle @@ -11,6 +11,26 @@ allprojects { maven { url 'https://repo.spring.io/milestone' } } + if (toPublish.split(',').contains(project.name) || project.name == 'ReactiveArchitectureCommons') { + + sonar { + properties { + property "sonar.sourceEncoding", "UTF-8" + property 'sonar.projectKey', 'reactive-commons_reactive-commons-java' + property 'sonar.organization', 'reactive-commons' + property 'sonar.host.url', 'https://sonarcloud.io' + property "sonar.sources", "src/main" + property "sonar.test", "src/test" + property "sonar.java.binaries", "build/classes" + property "sonar.junit.reportPaths", "build/test-results/test" + property "sonar.java-coveragePlugin", "jacoco" + property "sonar.coverage.jacoco.xmlReportPaths", "${rootDir}/build/reports/jacoco/generateMergedReport/generateMergedReport.xml" + property "sonar.exclusions", ".github/**,samples/**/*" + property 'sonar.coverage.exclusions', 'samples/**/*' + } + } + } + group 'org.reactivecommons' } @@ -70,23 +90,6 @@ subprojects { group groupId - sonar { - properties { - property "sonar.sourceEncoding", "UTF-8" - property 'sonar.projectKey', 'reactive-commons_reactive-commons-java' - property 'sonar.organization', 'reactive-commons' - property 'sonar.host.url', 'https://sonarcloud.io' - property "sonar.sources", "src/main" - property "sonar.test", "src/test" - property "sonar.java.binaries", "build/classes" - property "sonar.junit.reportPaths", "build/test-results/test" - property "sonar.java-coveragePlugin", "jacoco" - property "sonar.coverage.jacoco.xmlReportPaths", "${rootDir}/build/reports/jacoco/generateMergedReport/generateMergedReport.xml" - property "sonar.exclusions", ".github/**,samples/**/*" - property 'sonar.coverage.exclusions', 'samples/**/*' - } - } - tasks.named("jar") { enabled = true archiveClassifier = '' From 3b4e97bd263074b26a66c1a5e4ad6d59aabbfa20 Mon Sep 17 00:00:00 2001 From: Juan C Galvis <8420868+juancgalvis@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:13:57 -0500 Subject: [PATCH 07/12] build(sonar): Fix some sonar issues --- .../async/kafka/KafkaDirectAsyncGateway.java | 29 +++++++++-------- .../async/kafka/KafkaDomainEventBus.java | 5 +-- main.gradle | 2 +- .../annotations/EnableDirectAsyncGateway.java | 1 - .../starter/config/ConnectionManager.java | 9 ------ .../async/starter/config/DomainHandlers.java | 3 +- .../listeners/AbstractListenerConfig.java | 4 +-- .../starter/props/GenericAsyncProps.java | 10 +++--- .../props/GenericAsyncPropsDomain.java | 17 +++++++++- .../GenericAsyncPropsDomainProperties.java | 2 +- .../async/kafka/KafkaBrokerProvider.java | 32 +++++++++---------- .../async/kafka/KafkaSetupUtils.java | 10 +++--- .../config/DirectAsyncGatewayConfig.java | 16 ++++++---- .../standalone/config/RabbitMqConfig.java | 18 +++++------ .../standalone/config/RabbitProperties.java | 2 +- .../config/spring/RabbitPropertiesBase.java | 8 ++--- .../health/RabbitMQHealthException.java | 7 ++++ .../health/RabbitReactiveHealthIndicator.java | 2 +- 18 files changed, 97 insertions(+), 80 deletions(-) create mode 100644 starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/RabbitMQHealthException.java diff --git a/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/KafkaDirectAsyncGateway.java b/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/KafkaDirectAsyncGateway.java index 910a189d..989f30e7 100644 --- a/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/KafkaDirectAsyncGateway.java +++ b/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/KafkaDirectAsyncGateway.java @@ -8,68 +8,71 @@ import reactor.core.publisher.Mono; public class KafkaDirectAsyncGateway implements DirectAsyncGateway { + + public static final String NOT_IMPLEMENTED_YET = "Not implemented yet"; + @Override public Mono sendCommand(Command command, String targetName) { - throw new UnsupportedOperationException("Not implemented yet"); + throw new UnsupportedOperationException(NOT_IMPLEMENTED_YET); } @Override public Mono sendCommand(Command command, String targetName, long delayMillis) { - throw new UnsupportedOperationException("Not implemented yet"); + throw new UnsupportedOperationException(NOT_IMPLEMENTED_YET); } @Override public Mono sendCommand(Command command, String targetName, String domain) { - throw new UnsupportedOperationException("Not implemented yet"); + throw new UnsupportedOperationException(NOT_IMPLEMENTED_YET); } @Override public Mono sendCommand(Command command, String targetName, long delayMillis, String domain) { - throw new UnsupportedOperationException("Not implemented yet"); + throw new UnsupportedOperationException(NOT_IMPLEMENTED_YET); } @Override public Mono sendCommand(CloudEvent command, String targetName) { - throw new UnsupportedOperationException("Not implemented yet"); + throw new UnsupportedOperationException(NOT_IMPLEMENTED_YET); } @Override public Mono sendCommand(CloudEvent command, String targetName, long delayMillis) { - throw new UnsupportedOperationException("Not implemented yet"); + throw new UnsupportedOperationException(NOT_IMPLEMENTED_YET); } @Override public Mono sendCommand(CloudEvent command, String targetName, String domain) { - throw new UnsupportedOperationException("Not implemented yet"); + throw new UnsupportedOperationException(NOT_IMPLEMENTED_YET); } @Override public Mono sendCommand(CloudEvent command, String targetName, long delayMillis, String domain) { - throw new UnsupportedOperationException("Not implemented yet"); + throw new UnsupportedOperationException(NOT_IMPLEMENTED_YET); } @Override public Mono requestReply(AsyncQuery query, String targetName, Class type) { - throw new UnsupportedOperationException("Not implemented yet"); + throw new UnsupportedOperationException(NOT_IMPLEMENTED_YET); } @Override public Mono requestReply(AsyncQuery query, String targetName, Class type, String domain) { - throw new UnsupportedOperationException("Not implemented yet"); + throw new UnsupportedOperationException(NOT_IMPLEMENTED_YET); } @Override public Mono requestReply(CloudEvent query, String targetName, Class type) { - throw new UnsupportedOperationException("Not implemented yet"); + throw new UnsupportedOperationException(NOT_IMPLEMENTED_YET); } @Override public Mono requestReply(CloudEvent query, String targetName, Class type, String domain) { - throw new UnsupportedOperationException("Not implemented yet"); + throw new UnsupportedOperationException(NOT_IMPLEMENTED_YET); } @Override public Mono reply(T response, From from) { - throw new UnsupportedOperationException("Not implemented yet"); + throw new UnsupportedOperationException(NOT_IMPLEMENTED_YET); } } diff --git a/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/KafkaDomainEventBus.java b/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/KafkaDomainEventBus.java index e986c84e..cbd5a09b 100644 --- a/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/KafkaDomainEventBus.java +++ b/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/KafkaDomainEventBus.java @@ -9,6 +9,7 @@ @AllArgsConstructor public class KafkaDomainEventBus implements DomainEventBus { + public static final String NOT_IMPLEMENTED_YET = "Not implemented yet"; private final ReactiveMessageSender sender; @Override @@ -18,7 +19,7 @@ public Publisher emit(DomainEvent event) { @Override public Publisher emit(String domain, DomainEvent event) { - throw new UnsupportedOperationException("Not implemented yet"); + throw new UnsupportedOperationException(NOT_IMPLEMENTED_YET); } @Override @@ -28,6 +29,6 @@ public Publisher emit(CloudEvent event) { @Override public Publisher emit(String domain, CloudEvent event) { - throw new UnsupportedOperationException("Not implemented yet"); + throw new UnsupportedOperationException(NOT_IMPLEMENTED_YET); } } diff --git a/main.gradle b/main.gradle index f87daa73..3c2bd883 100644 --- a/main.gradle +++ b/main.gradle @@ -11,7 +11,7 @@ allprojects { maven { url 'https://repo.spring.io/milestone' } } - if (toPublish.split(',').contains(project.name) || project.name == 'ReactiveArchitectureCommons') { + if (toPublish.split(',').contains(project.name) || project.name == rootProject.name) { sonar { properties { diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableDirectAsyncGateway.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableDirectAsyncGateway.java index 72ce234f..300297dd 100644 --- a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableDirectAsyncGateway.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/impl/config/annotations/EnableDirectAsyncGateway.java @@ -1,6 +1,5 @@ package org.reactivecommons.async.impl.config.annotations; -import org.reactivecommons.async.starter.config.ReactiveCommonsConfig; import org.reactivecommons.async.starter.senders.DirectAsyncGatewayConfig; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/ConnectionManager.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/ConnectionManager.java index 020e8d0a..92ba2630 100644 --- a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/ConnectionManager.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/ConnectionManager.java @@ -19,15 +19,6 @@ public ConnectionManager addDomain(String domain, BrokerProvider domainConn) { return this; } - private BrokerProvider getChecked(String domain) { - BrokerProvider domainProvider = connections.get(domain); - if (domainProvider == null) { - throw new RuntimeException("You are trying to use the domain " + domain - + " but this connection is not defined"); - } - return domainProvider; - } - public Map getProviders() { return connections; } diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/DomainHandlers.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/DomainHandlers.java index 4e7e7662..0704f715 100644 --- a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/DomainHandlers.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/DomainHandlers.java @@ -1,6 +1,7 @@ package org.reactivecommons.async.starter.config; import org.reactivecommons.async.commons.HandlerResolver; +import org.reactivecommons.async.starter.exceptions.InvalidConfigurationException; import java.util.Map; import java.util.TreeMap; @@ -15,7 +16,7 @@ public void add(String domain, HandlerResolver resolver) { public HandlerResolver get(String domain) { HandlerResolver handlerResolver = handlers.get(domain); if (handlerResolver == null) { - throw new RuntimeException("You are trying to use the domain " + domain + throw new InvalidConfigurationException("You are trying to use the domain " + domain + " but this connection is not defined"); } return handlerResolver; diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/AbstractListenerConfig.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/AbstractListenerConfig.java index 3d6e5664..e17d19e0 100644 --- a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/AbstractListenerConfig.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/listeners/AbstractListenerConfig.java @@ -1,13 +1,13 @@ package org.reactivecommons.async.starter.listeners; import org.reactivecommons.async.commons.HandlerResolver; -import org.reactivecommons.async.starter.config.ConnectionManager; import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.config.ConnectionManager; import org.reactivecommons.async.starter.config.DomainHandlers; public abstract class AbstractListenerConfig { - public AbstractListenerConfig(ConnectionManager manager, DomainHandlers handlers) { + protected AbstractListenerConfig(ConnectionManager manager, DomainHandlers handlers) { manager.forDomain((domain, provider) -> listen(domain, provider, handlers.get(domain))); } diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncProps.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncProps.java index 1e3432d4..20dabcb1 100644 --- a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncProps.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncProps.java @@ -15,13 +15,13 @@ public abstract class GenericAsyncProps

{ private String appName; private String secret; - abstract public void setConnectionProperties(P properties); + public abstract void setConnectionProperties(P properties); - abstract public P getConnectionProperties(); + public abstract P getConnectionProperties(); - abstract public String getBrokerType(); + public abstract String getBrokerType(); - abstract public boolean isEnabled(); + public abstract boolean isEnabled(); - abstract public void setUseDiscardNotifierPerDomain(boolean enabled); + public abstract void setUseDiscardNotifierPerDomain(boolean enabled); } \ No newline at end of file diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomain.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomain.java index bd43a327..8c80b45d 100644 --- a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomain.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomain.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.SneakyThrows; @@ -10,6 +11,7 @@ import java.lang.reflect.Constructor; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; @@ -61,7 +63,7 @@ public GenericAsyncPropsDomain(String defaultAppName, } protected void fillCustoms(T asyncProps) { - + // To be overridden called after the default properties are set } public T getProps(String domain) { @@ -153,4 +155,17 @@ public interface SecretFiller

{ void fillWithSecret(String domain, GenericAsyncProps

props); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + GenericAsyncPropsDomain that = (GenericAsyncPropsDomain) o; + return Objects.equals(asyncPropsClass, that.asyncPropsClass) && Objects.equals(propsClass, that.propsClass); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), asyncPropsClass, propsClass); + } } diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomainProperties.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomainProperties.java index 5e5d8498..023ce1f4 100644 --- a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomainProperties.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomainProperties.java @@ -10,7 +10,7 @@ @Setter public class GenericAsyncPropsDomainProperties, P> extends HashMap { - public GenericAsyncPropsDomainProperties(Map m) { + public GenericAsyncPropsDomainProperties(Map m) { super(m); } diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaBrokerProvider.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaBrokerProvider.java index 434b079c..b2ac1965 100644 --- a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaBrokerProvider.java +++ b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaBrokerProvider.java @@ -52,20 +52,18 @@ public DirectAsyncGateway getDirectAsyncGateway(HandlerResolver resolver) { @Override public void listenDomainEvents(HandlerResolver resolver) { - if (!props.getDomain().isIgnoreThisListener()) { - if (!resolver.getEventListeners().isEmpty()) { - ApplicationEventListener eventListener = new ApplicationEventListener(receiver, - resolver, - converter, - props.getWithDLQRetry(), - props.getCreateTopology(), - props.getMaxRetries(), - props.getRetryDelay(), - discardNotifier, - errorReporter, - props.getAppName()); - eventListener.startListener(topologyCreator); - } + if (!props.getDomain().isIgnoreThisListener() && !resolver.getEventListeners().isEmpty()) { + ApplicationEventListener eventListener = new ApplicationEventListener(receiver, + resolver, + converter, + props.getWithDLQRetry(), + props.getCreateTopology(), + props.getMaxRetries(), + props.getRetryDelay(), + discardNotifier, + errorReporter, + props.getAppName()); + eventListener.startListener(topologyCreator); } } @@ -88,17 +86,17 @@ public void listenNotificationEvents(HandlerResolver resolver) { @Override public void listenCommands(HandlerResolver resolver) { - + // Implemented in the future } @Override public void listenQueries(HandlerResolver resolver) { - + // May be implemented in the future } @Override public void listenReplies(HandlerResolver resolver) { - + // May be implemented in the future } @Override diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaSetupUtils.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaSetupUtils.java index 42fe7fd8..c6670307 100644 --- a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaSetupUtils.java +++ b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/KafkaSetupUtils.java @@ -38,9 +38,9 @@ public static ReactiveMessageSender createMessageSender(AsyncKafkaProps config, TopologyCreator topologyCreator, SslBundles sslBundles) { KafkaProperties props = config.getConnectionProperties(); - props.setClientId(config.getAppName()); // CLIENT_ID_CONFIG - props.getProducer().setKeySerializer(StringSerializer.class); // KEY_SERIALIZER_CLASS_CONFIG; - props.getProducer().setValueSerializer(ByteArraySerializer.class); // VALUE_SERIALIZER_CLASS_CONFIG + props.setClientId(config.getAppName()); + props.getProducer().setKeySerializer(StringSerializer.class); + props.getProducer().setValueSerializer(ByteArraySerializer.class); SenderOptions senderOptions = SenderOptions.create(props.buildProducerProperties(sslBundles)); KafkaSender kafkaSender = KafkaSender.create(senderOptions); return new ReactiveMessageSender(kafkaSender, converter, topologyCreator); @@ -50,8 +50,8 @@ public static ReactiveMessageSender createMessageSender(AsyncKafkaProps config, public static ReactiveMessageListener createMessageListener(AsyncKafkaProps config, SslBundles sslBundles) { KafkaProperties props = config.getConnectionProperties(); - props.getConsumer().setKeyDeserializer(StringDeserializer.class); // KEY_DESERIALIZER_CLASS_CONFIG - props.getConsumer().setValueDeserializer(ByteArrayDeserializer.class); // VALUE_DESERIALIZER_CLASS_CONFIG + props.getConsumer().setKeyDeserializer(StringDeserializer.class); + props.getConsumer().setValueDeserializer(ByteArrayDeserializer.class); ReceiverOptions receiverOptions = ReceiverOptions.create(props.buildConsumerProperties(sslBundles)); return new ReactiveMessageListener(receiverOptions); } diff --git a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/DirectAsyncGatewayConfig.java b/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/DirectAsyncGatewayConfig.java index 4ee66272..2e4ac2ee 100644 --- a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/DirectAsyncGatewayConfig.java +++ b/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/DirectAsyncGatewayConfig.java @@ -22,13 +22,17 @@ public class DirectAsyncGatewayConfig { private String appName; - public RabbitDirectAsyncGateway rabbitDirectAsyncGateway(BrokerConfig config, ReactiveReplyRouter router, ReactiveMessageSender rSender, MessageConverter converter, - MeterRegistry meterRegistry) throws Exception { - return new RabbitDirectAsyncGateway(config, router, rSender, directMessagesExchangeName, converter, meterRegistry); + public RabbitDirectAsyncGateway rabbitDirectAsyncGateway(BrokerConfig config, ReactiveReplyRouter router, + ReactiveMessageSender rSender, MessageConverter converter, + MeterRegistry meterRegistry) { + return new RabbitDirectAsyncGateway(config, router, rSender, directMessagesExchangeName, converter, + meterRegistry); } - public ApplicationReplyListener msgListener(ReactiveReplyRouter router, BrokerConfig config, ReactiveMessageListener listener, boolean createTopology) { - final ApplicationReplyListener replyListener = new ApplicationReplyListener(router, listener, generateName(), globalReplyExchangeName, createTopology); + public ApplicationReplyListener msgListener(ReactiveReplyRouter router, BrokerConfig config, + ReactiveMessageListener listener, boolean createTopology) { + final ApplicationReplyListener replyListener = new ApplicationReplyListener(router, listener, generateName(), + globalReplyExchangeName, createTopology); replyListener.startListening(config.getRoutingKey()); return replyListener; } @@ -50,7 +54,7 @@ public String generateName() { .putLong(uuid.getLeastSignificantBits()); // Convert to base64 and remove trailing = return this.appName + encodeToUrlSafeString(bb.array()) - .replaceAll("=", ""); + .replace("=", ""); } public static String encodeToUrlSafeString(byte[] src) { diff --git a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/RabbitMqConfig.java b/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/RabbitMqConfig.java index adc2d98d..f12d3eaf 100644 --- a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/RabbitMqConfig.java +++ b/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/RabbitMqConfig.java @@ -3,17 +3,21 @@ import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import lombok.extern.java.Log; -import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; -import org.reactivecommons.async.rabbit.communications.TopologyCreator; import org.reactivecommons.async.commons.converters.MessageConverter; import org.reactivecommons.async.commons.converters.json.ObjectMapperSupplier; +import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; +import org.reactivecommons.async.rabbit.communications.TopologyCreator; import org.reactivecommons.async.rabbit.config.ConnectionFactoryProvider; import org.reactivecommons.async.rabbit.converters.json.RabbitJacksonMessageConverter; import reactor.core.publisher.Mono; -import reactor.rabbitmq.*; +import reactor.rabbitmq.ChannelPool; +import reactor.rabbitmq.ChannelPoolFactory; +import reactor.rabbitmq.ChannelPoolOptions; +import reactor.rabbitmq.RabbitFlux; +import reactor.rabbitmq.Sender; +import reactor.rabbitmq.SenderOptions; import reactor.util.retry.Retry; -import java.io.File; import java.time.Duration; import java.util.logging.Level; @@ -40,12 +44,6 @@ public ReactiveMessageSender messageSender(ConnectionFactoryProvider provider, M return new ReactiveMessageSender(sender, appName, converter, new TopologyCreator(sender)); } - /*public ReactiveMessageListener messageListener(ConnectionFactoryProvider provider) { - final Mono connection = createSenderConnectionMono(provider.getConnectionFactory(), "listener"); - Receiver receiver = RabbitFlux.createReceiver(new ReceiverOptions().connectionMono(connection)); - return new ReactiveMessageListener(receiver, new TopologyCreator(connection)); - }*/ - public ConnectionFactoryProvider connectionFactory(RabbitProperties properties) { final ConnectionFactory factory = new ConnectionFactory(); factory.setHost(properties.getHost()); diff --git a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/RabbitProperties.java b/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/RabbitProperties.java index bd7ae2e8..93c9a1f3 100644 --- a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/RabbitProperties.java +++ b/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/RabbitProperties.java @@ -7,7 +7,7 @@ public class RabbitProperties { private String host = "localhost"; private int port = 5672; private String username = "guest"; - private String password = "guest"; + private String password = "guest"; //NOSONAR private String virtualHost; private Integer channelPoolMaxCacheSize; } diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/spring/RabbitPropertiesBase.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/spring/RabbitPropertiesBase.java index e2651112..41346510 100644 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/spring/RabbitPropertiesBase.java +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/spring/RabbitPropertiesBase.java @@ -29,7 +29,7 @@ public class RabbitPropertiesBase { /** * Login to authenticate against the communications. */ - private String password = "guest"; + private String password = "guest"; //NOSONAR /** * SSL configuration. @@ -156,11 +156,11 @@ public void setAddresses(String addresses) { } private List

parseAddresses(String addresses) { - List
parsedAddresses = new ArrayList<>(); + List
parsedAddressesLocal = new ArrayList<>(); for (String address : StringUtils.commaDelimitedListToStringArray(addresses)) { - parsedAddresses.add(new Address(address)); + parsedAddressesLocal.add(new Address(address)); } - return parsedAddresses; + return parsedAddressesLocal; } public String getUsername() { diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/RabbitMQHealthException.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/RabbitMQHealthException.java new file mode 100644 index 00000000..a0255f4a --- /dev/null +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/RabbitMQHealthException.java @@ -0,0 +1,7 @@ +package org.reactivecommons.async.rabbit.health; + +public class RabbitMQHealthException extends RuntimeException { + public RabbitMQHealthException(Throwable throwable) { + super(throwable); + } +} diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/RabbitReactiveHealthIndicator.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/RabbitReactiveHealthIndicator.java index dae49699..c4f7282f 100644 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/RabbitReactiveHealthIndicator.java +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/RabbitReactiveHealthIndicator.java @@ -37,7 +37,7 @@ private String getRawVersion(ConnectionFactory factory) { return connection.getServerProperties().get(VERSION).toString(); } catch (SocketException e) { log.warn("Identified error", e); - throw new RuntimeException(e); + throw new RabbitMQHealthException(e); } finally { if (connection != null) { try { From 42b47fa640de2679649494fbb739315694bf63cb Mon Sep 17 00:00:00 2001 From: Juan C Galvis <8420868+juancgalvis@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:36:27 -0500 Subject: [PATCH 08/12] build(sonar): Fix some sonar issues --- .../kafka/config/props/AsyncKafkaPropsDomainProperties.java | 2 +- .../rabbit/config/props/AsyncRabbitPropsDomainProperties.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaPropsDomainProperties.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaPropsDomainProperties.java index 67df456c..186910d3 100644 --- a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaPropsDomainProperties.java +++ b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaPropsDomainProperties.java @@ -9,7 +9,7 @@ @ConfigurationProperties(prefix = "reactive.commons.kafka") public class AsyncKafkaPropsDomainProperties extends GenericAsyncPropsDomainProperties { - public AsyncKafkaPropsDomainProperties(Map m) { + public AsyncKafkaPropsDomainProperties(Map m) { super(m); } diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncRabbitPropsDomainProperties.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncRabbitPropsDomainProperties.java index a233dfd0..b852a0db 100644 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncRabbitPropsDomainProperties.java +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncRabbitPropsDomainProperties.java @@ -13,7 +13,7 @@ public class AsyncRabbitPropsDomainProperties extends GenericAsyncPropsDomainPro public AsyncRabbitPropsDomainProperties() { } - public AsyncRabbitPropsDomainProperties(Map m) { + public AsyncRabbitPropsDomainProperties(Map m) { super(m); } From 24f378bf8874182c301e7f130fde1f08c7117fd5 Mon Sep 17 00:00:00 2001 From: Juan C Galvis <8420868+juancgalvis@users.noreply.github.com> Date: Thu, 10 Oct 2024 17:30:55 -0500 Subject: [PATCH 09/12] build(test): Fix some sonar issues, add unit test and fix some bugs --- main.gradle | 2 +- .../ReactiveCommonsHealthIndicator.java | 6 +- .../GenericAsyncPropsDomainProperties.java | 4 +- .../senders/GenericDomainEventBus.java | 19 ++- .../config/ReactiveCommonsConfigTest.java | 30 ++++ .../ReactiveCommonsHealthIndicatorTest.java | 74 ++++++++ .../starter/impl/mybroker/MyBrokerConfig.java | 29 ++++ .../starter/mybroker/MyBrokerProvider.java | 63 +++++++ .../mybroker/MyBrokerProviderFactory.java | 27 +++ .../mybroker/MyBrokerSecretFiller.java | 7 + .../AsyncMyBrokerPropsDomainProperties.java | 23 +++ .../mybroker/props/MyBrokerAsyncProps.java | 22 +++ .../props/MyBrokerAsyncPropsDomain.java | 20 +++ .../mybroker/props/MyBrokerConnProps.java | 9 + .../props/GenericAsyncPropsDomainTest.java | 98 +++++++++++ .../senders/DirectAsyncGatewayConfigTest.java | 53 ++++++ .../starter/senders/EventBusConfigTest.java | 45 +++++ .../GenericDirectAsyncGatewayTest.java | 161 ++++++++++++++++++ .../senders/GenericDomainEventBusTest.java | 111 ++++++++++++ .../src/test/resources/application.yaml | 7 + .../health/KafkaReactiveHealthIndicator.java | 6 +- .../health/RabbitReactiveHealthIndicator.java | 6 +- 22 files changed, 810 insertions(+), 12 deletions(-) create mode 100644 starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/config/ReactiveCommonsConfigTest.java create mode 100644 starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/config/health/ReactiveCommonsHealthIndicatorTest.java create mode 100644 starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/impl/mybroker/MyBrokerConfig.java create mode 100644 starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/MyBrokerProvider.java create mode 100644 starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/MyBrokerProviderFactory.java create mode 100644 starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/MyBrokerSecretFiller.java create mode 100644 starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/props/AsyncMyBrokerPropsDomainProperties.java create mode 100644 starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/props/MyBrokerAsyncProps.java create mode 100644 starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/props/MyBrokerAsyncPropsDomain.java create mode 100644 starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/props/MyBrokerConnProps.java create mode 100644 starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomainTest.java create mode 100644 starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/senders/DirectAsyncGatewayConfigTest.java create mode 100644 starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/senders/EventBusConfigTest.java create mode 100644 starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/senders/GenericDirectAsyncGatewayTest.java create mode 100644 starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/senders/GenericDomainEventBusTest.java create mode 100644 starters/async-commons-starter/src/test/resources/application.yaml diff --git a/main.gradle b/main.gradle index 3c2bd883..47a82cce 100644 --- a/main.gradle +++ b/main.gradle @@ -25,7 +25,7 @@ allprojects { property "sonar.junit.reportPaths", "build/test-results/test" property "sonar.java-coveragePlugin", "jacoco" property "sonar.coverage.jacoco.xmlReportPaths", "${rootDir}/build/reports/jacoco/generateMergedReport/generateMergedReport.xml" - property "sonar.exclusions", ".github/**,samples/**/*" + property "sonar.exclusions", ".github/**,samples/**/*,**/mybroker/**/*" property 'sonar.coverage.exclusions', 'samples/**/*' } } diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/health/ReactiveCommonsHealthIndicator.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/health/ReactiveCommonsHealthIndicator.java index 2140b20d..624f651b 100644 --- a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/health/ReactiveCommonsHealthIndicator.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/config/health/ReactiveCommonsHealthIndicator.java @@ -13,6 +13,8 @@ @Log4j2 @AllArgsConstructor public class ReactiveCommonsHealthIndicator extends AbstractReactiveHealthIndicator { + public static final String DOMAIN = "domain"; + public static final String VERSION = "version"; private final ConnectionManager manager; @Override @@ -26,8 +28,8 @@ protected Mono doHealthCheck(Health.Builder builder) { } private Health.Builder reduceHealth(Health.Builder builder, Health status) { - String domain = status.getDetails().get("domain").toString(); - if (!status.getStatus().equals(Status.DOWN)) { + String domain = status.getDetails().get(DOMAIN).toString(); + if (status.getStatus().equals(Status.DOWN)) { log.error("Broker of domain {} is down", domain); return builder.down().withDetail(domain, status.getDetails()); } diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomainProperties.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomainProperties.java index 023ce1f4..c9a784f5 100644 --- a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomainProperties.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomainProperties.java @@ -2,6 +2,7 @@ import lombok.Getter; import lombok.Setter; +import lombok.SneakyThrows; import java.util.HashMap; import java.util.Map; @@ -38,8 +39,9 @@ public AsyncPropsDomainPropertiesBuilder withDomain(String domain, T pr return this; } + @SneakyThrows public X build() { - return returnType.cast(new GenericAsyncPropsDomainProperties<>(domains)); + return returnType.getDeclaredConstructor(Map.class).newInstance(domains); } } } diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/GenericDomainEventBus.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/GenericDomainEventBus.java index 7b659885..0feda04a 100644 --- a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/GenericDomainEventBus.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/senders/GenericDomainEventBus.java @@ -1,19 +1,22 @@ package org.reactivecommons.async.starter.senders; import io.cloudevents.CloudEvent; -import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; import org.reactivecommons.api.domain.DomainEvent; import org.reactivecommons.api.domain.DomainEventBus; +import org.reactivecommons.async.starter.exceptions.InvalidConfigurationException; import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; import java.util.concurrent.ConcurrentMap; import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; -@AllArgsConstructor +@RequiredArgsConstructor public class GenericDomainEventBus implements DomainEventBus { private final ConcurrentMap domainEventBuses; + @Override public Publisher emit(DomainEvent event) { return emit(DEFAULT_DOMAIN, event); @@ -21,7 +24,11 @@ public Publisher emit(DomainEvent event) { @Override public Publisher emit(String domain, DomainEvent event) { - return domainEventBuses.get(domain).emit(event); + DomainEventBus domainEventBus = domainEventBuses.get(domain); + if (domainEventBus == null) { + return Mono.error(() -> new InvalidConfigurationException("Domain not found: " + domain)); + } + return domainEventBus.emit(event); } @Override @@ -31,6 +38,10 @@ public Publisher emit(CloudEvent event) { @Override public Publisher emit(String domain, CloudEvent event) { - return domainEventBuses.get(domain).emit(event); + DomainEventBus domainEventBus = domainEventBuses.get(domain); + if (domainEventBus == null) { + return Mono.error(() -> new InvalidConfigurationException("Domain not found: " + domain)); + } + return domainEventBus.emit(event); } } diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/config/ReactiveCommonsConfigTest.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/config/ReactiveCommonsConfigTest.java new file mode 100644 index 00000000..34722ca4 --- /dev/null +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/config/ReactiveCommonsConfigTest.java @@ -0,0 +1,30 @@ +package org.reactivecommons.async.starter.config; + + +import org.junit.jupiter.api.Test; +import org.mockito.Spy; +import org.reactivecommons.async.starter.impl.mybroker.MyBrokerConfig; +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(classes = { + MyBrokerConfig.class, + ReactiveCommonsConfig.class +}) +class ReactiveCommonsConfigTest { + @Spy + @Autowired + private ApplicationContext context; + + @Test + void shouldCreateConnectionManager() { + // Arrange + // Act + ConnectionManager manager = context.getBean(ConnectionManager.class); + // Assert + assertNotNull(manager); + } +} diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/config/health/ReactiveCommonsHealthIndicatorTest.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/config/health/ReactiveCommonsHealthIndicatorTest.java new file mode 100644 index 00000000..d2c9b014 --- /dev/null +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/config/health/ReactiveCommonsHealthIndicatorTest.java @@ -0,0 +1,74 @@ +package org.reactivecommons.async.starter.config.health; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.config.ConnectionManager; +import org.springframework.boot.actuate.health.Health; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.mockito.Mockito.when; +import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; +import static org.reactivecommons.async.starter.config.health.ReactiveCommonsHealthIndicator.DOMAIN; +import static org.reactivecommons.async.starter.config.health.ReactiveCommonsHealthIndicator.VERSION; + +@ExtendWith(MockitoExtension.class) +class ReactiveCommonsHealthIndicatorTest { + public static final String OTHER = "other"; + @Mock + private BrokerProvider brokerProvider; + @Mock + private BrokerProvider brokerProvider2; + private ReactiveCommonsHealthIndicator healthIndicator; + + @BeforeEach + void setUp() { + ConnectionManager connectionManager = new ConnectionManager(); + connectionManager.addDomain(DEFAULT_DOMAIN, brokerProvider); + connectionManager.addDomain(OTHER, brokerProvider2); + ReactiveCommonsHealthConfig healthConfig = new ReactiveCommonsHealthConfig(); + healthIndicator = healthConfig.reactiveCommonsHealthIndicator(connectionManager); + } + + @Test + void shouldBeUp() { + // Arrange + when(brokerProvider.healthCheck()).thenReturn(Mono.just(Health.up() + .withDetail(DOMAIN, DEFAULT_DOMAIN) + .withDetail(VERSION, "123") + .build())); + when(brokerProvider2.healthCheck()).thenReturn(Mono.just(Health.up() + .withDetail(DOMAIN, OTHER) + .withDetail(VERSION, "1234") + .build())); + // Act + Mono flow = healthIndicator.health(); + // Assert + StepVerifier.create(flow) + .expectNextMatches(health -> health.getStatus().toString().equals("UP")) + .verifyComplete(); + } + + @Test + void shouldBeDown() { + // Arrange + when(brokerProvider.healthCheck()).thenReturn(Mono.just(Health.up() + .withDetail(DOMAIN, DEFAULT_DOMAIN) + .withDetail(VERSION, "123") + .build())); + when(brokerProvider2.healthCheck()).thenReturn(Mono.just(Health.down() + .withDetail(DOMAIN, OTHER) + .withDetail(VERSION, "1234") + .build())); + // Act + Mono flow = healthIndicator.health(); + // Assert + StepVerifier.create(flow) + .expectNextMatches(health -> health.getStatus().toString().equals("DOWN")) + .verifyComplete(); + } +} diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/impl/mybroker/MyBrokerConfig.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/impl/mybroker/MyBrokerConfig.java new file mode 100644 index 00000000..6fff5b03 --- /dev/null +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/impl/mybroker/MyBrokerConfig.java @@ -0,0 +1,29 @@ +package org.reactivecommons.async.starter.impl.mybroker; + +import org.reactivecommons.async.starter.mybroker.MyBrokerProviderFactory; +import org.reactivecommons.async.starter.mybroker.MyBrokerSecretFiller; +import org.reactivecommons.async.starter.mybroker.props.AsyncMyBrokerPropsDomainProperties; +import org.reactivecommons.async.starter.mybroker.props.MyBrokerAsyncPropsDomain; +import org.reactivecommons.async.starter.mybroker.props.MyBrokerConnProps; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +@EnableConfigurationProperties(AsyncMyBrokerPropsDomainProperties.class) +@Import({MyBrokerAsyncPropsDomain.class, MyBrokerProviderFactory.class}) +public class MyBrokerConfig { + + @Bean + public MyBrokerConnProps defaultMyBrokerConnProps() { + MyBrokerConnProps myBrokerConnProps = new MyBrokerConnProps(); + myBrokerConnProps.setHost("localhost"); + myBrokerConnProps.setPort("1234"); + return myBrokerConnProps; + } + + @Bean + public MyBrokerSecretFiller defaultMyBrokerSecretFiller() { + return (domain, props) -> { + }; + } +} diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/MyBrokerProvider.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/MyBrokerProvider.java new file mode 100644 index 00000000..33e6b3ba --- /dev/null +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/MyBrokerProvider.java @@ -0,0 +1,63 @@ +package org.reactivecommons.async.starter.mybroker; + +import lombok.AllArgsConstructor; +import org.reactivecommons.api.domain.DomainEventBus; +import org.reactivecommons.async.api.DirectAsyncGateway; +import org.reactivecommons.async.commons.HandlerResolver; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.broker.DiscardProvider; +import org.reactivecommons.async.starter.mybroker.props.MyBrokerAsyncProps; +import org.springframework.boot.actuate.health.Health; +import reactor.core.publisher.Mono; + +@AllArgsConstructor +public class MyBrokerProvider implements BrokerProvider { + private final String domain; + private final MyBrokerAsyncProps props; + private final DiscardProvider discardProvider; + + @Override + public MyBrokerAsyncProps getProps() { + return null; + } + + @Override + public DomainEventBus getDomainBus() { + return null; + } + + @Override + public DirectAsyncGateway getDirectAsyncGateway(HandlerResolver resolver) { + return null; + } + + @Override + public void listenDomainEvents(HandlerResolver resolver) { + + } + + @Override + public void listenNotificationEvents(HandlerResolver resolver) { + + } + + @Override + public void listenCommands(HandlerResolver resolver) { + + } + + @Override + public void listenQueries(HandlerResolver resolver) { + + } + + @Override + public void listenReplies(HandlerResolver resolver) { + + } + + @Override + public Mono healthCheck() { + return null; + } +} \ No newline at end of file diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/MyBrokerProviderFactory.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/MyBrokerProviderFactory.java new file mode 100644 index 00000000..edecf27e --- /dev/null +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/MyBrokerProviderFactory.java @@ -0,0 +1,27 @@ +package org.reactivecommons.async.starter.mybroker; + +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.broker.BrokerProviderFactory; +import org.reactivecommons.async.starter.broker.DiscardProvider; +import org.reactivecommons.async.starter.mybroker.props.MyBrokerAsyncProps; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +@Service("mybroker") +public class MyBrokerProviderFactory implements BrokerProviderFactory { + + @Override + public String getBrokerType() { + return "mybroker"; + } + + @Override + public DiscardProvider getDiscardProvider(MyBrokerAsyncProps props) { + return () -> message -> Mono.empty(); + } + + @Override + public BrokerProvider getProvider(String domain, MyBrokerAsyncProps props, DiscardProvider discardProvider) { + return new MyBrokerProvider(domain, props, discardProvider); + } +} diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/MyBrokerSecretFiller.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/MyBrokerSecretFiller.java new file mode 100644 index 00000000..2af3a2e8 --- /dev/null +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/MyBrokerSecretFiller.java @@ -0,0 +1,7 @@ +package org.reactivecommons.async.starter.mybroker; + +import org.reactivecommons.async.starter.props.GenericAsyncPropsDomain; +import org.reactivecommons.async.starter.mybroker.props.MyBrokerConnProps; + +public interface MyBrokerSecretFiller extends GenericAsyncPropsDomain.SecretFiller { +} \ No newline at end of file diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/props/AsyncMyBrokerPropsDomainProperties.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/props/AsyncMyBrokerPropsDomainProperties.java new file mode 100644 index 00000000..792ef21e --- /dev/null +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/props/AsyncMyBrokerPropsDomainProperties.java @@ -0,0 +1,23 @@ +package org.reactivecommons.async.starter.mybroker.props; + +import org.reactivecommons.async.starter.props.GenericAsyncPropsDomainProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.Map; + +@ConfigurationProperties(prefix = "my.broker") +public class AsyncMyBrokerPropsDomainProperties + extends GenericAsyncPropsDomainProperties { + + public AsyncMyBrokerPropsDomainProperties() { + } + + public AsyncMyBrokerPropsDomainProperties(Map m) { + super(m); + } + + public static AsyncPropsDomainPropertiesBuilder builder() { + return GenericAsyncPropsDomainProperties.builder(AsyncMyBrokerPropsDomainProperties.class); + } +} \ No newline at end of file diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/props/MyBrokerAsyncProps.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/props/MyBrokerAsyncProps.java new file mode 100644 index 00000000..0b5666ef --- /dev/null +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/props/MyBrokerAsyncProps.java @@ -0,0 +1,22 @@ +package org.reactivecommons.async.starter.mybroker.props; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; +import org.reactivecommons.async.starter.props.GenericAsyncProps; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@SuperBuilder +public class MyBrokerAsyncProps extends GenericAsyncProps { + private MyBrokerConnProps connectionProperties; + @Builder.Default + private String brokerType = "mybroker"; + private boolean enabled; + private boolean useDiscardNotifierPerDomain; +} \ No newline at end of file diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/props/MyBrokerAsyncPropsDomain.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/props/MyBrokerAsyncPropsDomain.java new file mode 100644 index 00000000..d2239eb0 --- /dev/null +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/props/MyBrokerAsyncPropsDomain.java @@ -0,0 +1,20 @@ +package org.reactivecommons.async.starter.mybroker.props; + +import lombok.Getter; +import lombok.Setter; +import org.reactivecommons.async.starter.props.GenericAsyncPropsDomain; +import org.reactivecommons.async.starter.mybroker.MyBrokerSecretFiller; +import org.springframework.beans.factory.annotation.Value; + +@Getter +@Setter +public class MyBrokerAsyncPropsDomain extends GenericAsyncPropsDomain { + + public MyBrokerAsyncPropsDomain(@Value("${spring.application.name}") String defaultAppName, + MyBrokerConnProps defaultRabbitProperties, + AsyncMyBrokerPropsDomainProperties configured, + MyBrokerSecretFiller secretFiller) { + super(defaultAppName, defaultRabbitProperties, configured, secretFiller, MyBrokerAsyncProps.class, + MyBrokerConnProps.class); + } +} \ No newline at end of file diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/props/MyBrokerConnProps.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/props/MyBrokerConnProps.java new file mode 100644 index 00000000..29ba7345 --- /dev/null +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/props/MyBrokerConnProps.java @@ -0,0 +1,9 @@ +package org.reactivecommons.async.starter.mybroker.props; + +import lombok.Data; + +@Data +public class MyBrokerConnProps { + private String host; + private String port; +} \ No newline at end of file diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomainTest.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomainTest.java new file mode 100644 index 00000000..69a0ad60 --- /dev/null +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomainTest.java @@ -0,0 +1,98 @@ +package org.reactivecommons.async.starter.props; + +import org.junit.jupiter.api.Test; +import org.reactivecommons.async.starter.exceptions.InvalidConfigurationException; +import org.reactivecommons.async.starter.mybroker.props.AsyncMyBrokerPropsDomainProperties; +import org.reactivecommons.async.starter.mybroker.props.MyBrokerAsyncProps; +import org.reactivecommons.async.starter.mybroker.props.MyBrokerConnProps; +import org.reactivecommons.async.starter.mybroker.props.MyBrokerAsyncPropsDomain; +import org.reactivecommons.async.starter.mybroker.MyBrokerSecretFiller; + +import java.lang.reflect.Constructor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; + +@SuppressWarnings("unchecked") +class GenericAsyncPropsDomainTest { + + public static final String OTHER = "other"; + + @Test + void shouldCreateProps() { + // Arrange + String defaultAppName = "sample"; + MyBrokerConnProps defaultMyBrokerProps = new MyBrokerConnProps(); + AsyncMyBrokerPropsDomainProperties configured = new AsyncMyBrokerPropsDomainProperties(); + MyBrokerAsyncProps other = new MyBrokerAsyncProps(); + other.setAppName(OTHER); + configured.put(OTHER, other); + MyBrokerSecretFiller secretFiller = (domain, props) -> { + }; + MyBrokerAsyncPropsDomain propsDomain = new MyBrokerAsyncPropsDomain(defaultAppName, defaultMyBrokerProps, configured, + secretFiller); + // Act + MyBrokerAsyncProps props = propsDomain.getProps(DEFAULT_DOMAIN); + MyBrokerAsyncProps otherProps = propsDomain.getProps(OTHER); + // Assert + assertEquals("sample", props.getAppName()); + assertEquals(OTHER, otherProps.getAppName()); + assertThrows(InvalidConfigurationException.class, () -> propsDomain.getProps("non-existing-domain")); + } + + @Test + void shouldCreatePropsWithDefaultConnectionProperties() { + // Arrange + String defaultAppName = "sample"; + MyBrokerConnProps defaultMyBrokerProps = new MyBrokerConnProps(); + MyBrokerAsyncProps propsConfigured = new MyBrokerAsyncProps(); + MyBrokerAsyncPropsDomain propsDomain = MyBrokerAsyncPropsDomain.builder(MyBrokerAsyncProps.class, MyBrokerConnProps.class, + AsyncMyBrokerPropsDomainProperties.class, + (Constructor) MyBrokerAsyncPropsDomain.class.getDeclaredConstructors()[0]) + .withDefaultAppName(defaultAppName) + .withDefaultProperties(defaultMyBrokerProps) + .withDomain(DEFAULT_DOMAIN, propsConfigured) + .withSecretFiller(null) + .build(); + // Act + MyBrokerAsyncProps props = propsDomain.getProps(DEFAULT_DOMAIN); + // Assert + assertEquals("sample", props.getAppName()); + assertEquals(defaultMyBrokerProps, props.getConnectionProperties()); + } + + @Test + void shouldFailCreatePropsWhenAppNameIsNullOrEmpty() { + // Arrange + MyBrokerConnProps defaultMyBrokerProps = new MyBrokerConnProps(); + AsyncMyBrokerPropsDomainProperties configured = new AsyncMyBrokerPropsDomainProperties(); + MyBrokerSecretFiller secretFiller = (domain, props) -> { + }; + // Assert + assertThrows(InvalidConfigurationException.class, + // Act + () -> new MyBrokerAsyncPropsDomain(null, defaultMyBrokerProps, configured, secretFiller)); + assertThrows(InvalidConfigurationException.class, + // Act + () -> new MyBrokerAsyncPropsDomain("", defaultMyBrokerProps, configured, secretFiller)); + } + + @Test + void shouldFailCreatePropsWhenDefaultConnectionPropertiesAreNul() { + // Arrange + String defaultAppName = "sample"; + AsyncMyBrokerPropsDomainProperties configured = AsyncMyBrokerPropsDomainProperties + .builder(AsyncMyBrokerPropsDomainProperties.class) + .withDomain(OTHER, new MyBrokerAsyncProps()) + .build(); + MyBrokerSecretFiller secretFiller = (domain, props) -> { + }; + // Assert + assertThrows(InvalidConfigurationException.class, + // Act + () -> new MyBrokerAsyncPropsDomain(defaultAppName, null, configured, secretFiller)); + } + + +} diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/senders/DirectAsyncGatewayConfigTest.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/senders/DirectAsyncGatewayConfigTest.java new file mode 100644 index 00000000..6a02949c --- /dev/null +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/senders/DirectAsyncGatewayConfigTest.java @@ -0,0 +1,53 @@ +package org.reactivecommons.async.starter.senders; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivecommons.async.api.DirectAsyncGateway; +import org.reactivecommons.async.commons.HandlerResolver; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.config.ConnectionManager; +import org.reactivecommons.async.starter.config.DomainHandlers; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DirectAsyncGatewayConfigTest { + @Mock + private DirectAsyncGateway domainEventBus; + @Mock + private BrokerProvider brokerProvider; + @Mock + private HandlerResolver resolver; + + private ConnectionManager manager; + private DirectAsyncGatewayConfig directAsyncGatewayConfig; + + @BeforeEach + void setUp() { + directAsyncGatewayConfig = new DirectAsyncGatewayConfig(); + manager = new ConnectionManager(); + manager.addDomain("domain", brokerProvider); + manager.addDomain("domain2", brokerProvider); + } + + @Test + void shouldCreateAllDomainEventBuses() { + // Arrange + when(brokerProvider.getDirectAsyncGateway(any())).thenReturn(domainEventBus); + DomainHandlers handlers = new DomainHandlers(); + handlers.add("domain", resolver); + handlers.add("domain2", resolver); + // Act + DirectAsyncGateway genericDomainEventBus = directAsyncGatewayConfig.genericDirectAsyncGateway(manager, handlers); + // Assert + assertNotNull(genericDomainEventBus); + verify(brokerProvider, times(2)).getDirectAsyncGateway(resolver); + } +} diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/senders/EventBusConfigTest.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/senders/EventBusConfigTest.java new file mode 100644 index 00000000..1be56082 --- /dev/null +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/senders/EventBusConfigTest.java @@ -0,0 +1,45 @@ +package org.reactivecommons.async.starter.senders; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivecommons.api.domain.DomainEventBus; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.config.ConnectionManager; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class EventBusConfigTest { + @Mock + private DomainEventBus domainEventBus; + @Mock + private BrokerProvider brokerProvider; + + private ConnectionManager manager; + private EventBusConfig eventBusConfig; + + @BeforeEach + void setUp() { + eventBusConfig = new EventBusConfig(); + manager = new ConnectionManager(); + manager.addDomain("domain", brokerProvider); + manager.addDomain("domain2", brokerProvider); + } + + @Test + void shouldCreateAllDomainEventBuses() { + // Arrange + when(brokerProvider.getDomainBus()).thenReturn(domainEventBus); + // Act + DomainEventBus genericDomainEventBus = eventBusConfig.genericDomainEventBus(manager); + // Assert + assertNotNull(genericDomainEventBus); + verify(brokerProvider, times(2)).getDomainBus(); + } +} diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/senders/GenericDirectAsyncGatewayTest.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/senders/GenericDirectAsyncGatewayTest.java new file mode 100644 index 00000000..5b6ca9bd --- /dev/null +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/senders/GenericDirectAsyncGatewayTest.java @@ -0,0 +1,161 @@ +package org.reactivecommons.async.starter.senders; + +import io.cloudevents.CloudEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivecommons.api.domain.Command; +import org.reactivecommons.async.api.AsyncQuery; +import org.reactivecommons.async.api.DirectAsyncGateway; +import org.reactivecommons.async.api.From; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.concurrent.ConcurrentHashMap; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; + +@ExtendWith(MockitoExtension.class) +class GenericDirectAsyncGatewayTest { + + public static final String DOMAIN_2 = "domain2"; + + @Mock + private DirectAsyncGateway directAsyncGateway1; + + @Mock + private DirectAsyncGateway directAsyncGateway2; + + @Mock + private CloudEvent cloudEvent; + + @Mock + private Command command; + + @Mock + private AsyncQuery asyncQuery; + + @Mock + private From from; + + private final long delayMillis = 100L; + + private GenericDirectAsyncGateway genericDirectAsyncGateway; + + @BeforeEach + void setUp() { + ConcurrentHashMap directAsyncGateways = new ConcurrentHashMap<>(); + directAsyncGateways.put(DEFAULT_DOMAIN, directAsyncGateway1); + directAsyncGateways.put(DOMAIN_2, directAsyncGateway2); + genericDirectAsyncGateway = new GenericDirectAsyncGateway(directAsyncGateways); + } + + @Test + void shouldSendCommandWithDefaultDomain() { + when(directAsyncGateway1.sendCommand(command, "target")).thenReturn(Mono.empty()); + Mono flow = genericDirectAsyncGateway.sendCommand(command, "target"); + StepVerifier.create(flow).verifyComplete(); + verify(directAsyncGateway1).sendCommand(command, "target"); + } + + @Test + void shouldSendCommandWithDefaultDomainWithDelay() { + when(directAsyncGateway1.sendCommand(command, "target", delayMillis)).thenReturn(Mono.empty()); + Mono flow = genericDirectAsyncGateway.sendCommand(command, "target", delayMillis); + StepVerifier.create(flow).verifyComplete(); + verify(directAsyncGateway1).sendCommand(command, "target", delayMillis); + } + + @Test + void shouldSendCommandWithSpecificDomain() { + when(directAsyncGateway2.sendCommand(command, "target")).thenReturn(Mono.empty()); + Mono flow = genericDirectAsyncGateway.sendCommand(command, "target", DOMAIN_2); + StepVerifier.create(flow).verifyComplete(); + verify(directAsyncGateway2).sendCommand(command, "target"); + } + + @Test + void shouldSendCommandWithSpecificDomainWithDelay() { + when(directAsyncGateway2.sendCommand(command, "target", delayMillis)).thenReturn(Mono.empty()); + Mono flow = genericDirectAsyncGateway.sendCommand(command, "target", delayMillis, DOMAIN_2); + StepVerifier.create(flow).verifyComplete(); + verify(directAsyncGateway2).sendCommand(command, "target", delayMillis); + } + + @Test + void shouldSendCloudEventWithDefaultDomain() { + when(directAsyncGateway1.sendCommand(cloudEvent, "target")).thenReturn(Mono.empty()); + Mono flow = genericDirectAsyncGateway.sendCommand(cloudEvent, "target"); + StepVerifier.create(flow).verifyComplete(); + verify(directAsyncGateway1).sendCommand(cloudEvent, "target"); + } + + @Test + void shouldSendCloudEventWithDefaultDomainWithDelay() { + when(directAsyncGateway1.sendCommand(cloudEvent, "target", delayMillis)).thenReturn(Mono.empty()); + Mono flow = genericDirectAsyncGateway.sendCommand(cloudEvent, "target", delayMillis); + StepVerifier.create(flow).verifyComplete(); + verify(directAsyncGateway1).sendCommand(cloudEvent, "target", delayMillis); + } + + @Test + void shouldSendCloudEventWithSpecificDomain() { + when(directAsyncGateway2.sendCommand(cloudEvent, "target")).thenReturn(Mono.empty()); + Mono flow = genericDirectAsyncGateway.sendCommand(cloudEvent, "target", DOMAIN_2); + StepVerifier.create(flow).verifyComplete(); + verify(directAsyncGateway2).sendCommand(cloudEvent, "target"); + } + + @Test + void shouldSendCloudEventWithSpecificDomainWithDelay() { + when(directAsyncGateway2.sendCommand(cloudEvent, "target", delayMillis)).thenReturn(Mono.empty()); + Mono flow = genericDirectAsyncGateway.sendCommand(cloudEvent, "target", delayMillis, DOMAIN_2); + StepVerifier.create(flow).verifyComplete(); + verify(directAsyncGateway2).sendCommand(cloudEvent, "target", delayMillis); + } + + @Test + void shouldRequestReplyWithDefaultDomain() { + when(directAsyncGateway1.requestReply(asyncQuery, "target", String.class)).thenReturn(Mono.just("response")); + Mono flow = genericDirectAsyncGateway.requestReply(asyncQuery, "target", String.class); + StepVerifier.create(flow).expectNext("response").verifyComplete(); + verify(directAsyncGateway1).requestReply(asyncQuery, "target", String.class); + } + + + @Test + void shouldRequestReplyWithDefaultDomainCloudEvent() { + when(directAsyncGateway1.requestReply(cloudEvent, "target", CloudEvent.class)).thenReturn(Mono.just(cloudEvent)); + Mono flow = genericDirectAsyncGateway.requestReply(cloudEvent, "target", CloudEvent.class); + StepVerifier.create(flow).expectNext(cloudEvent).verifyComplete(); + verify(directAsyncGateway1).requestReply(cloudEvent, "target", CloudEvent.class); + } + + @Test + void shouldRequestReplyWithSpecificDomain() { + when(directAsyncGateway2.requestReply(asyncQuery, "target", String.class)).thenReturn(Mono.just("response")); + Mono flow = genericDirectAsyncGateway.requestReply(asyncQuery, "target", String.class, DOMAIN_2); + StepVerifier.create(flow).expectNext("response").verifyComplete(); + verify(directAsyncGateway2).requestReply(asyncQuery, "target", String.class); + } + + @Test + void shouldRequestReplyWithSpecificDomainCloudEvent() { + when(directAsyncGateway2.requestReply(cloudEvent, "target", CloudEvent.class)).thenReturn(Mono.just(cloudEvent)); + Mono flow = genericDirectAsyncGateway.requestReply(cloudEvent, "target", CloudEvent.class, DOMAIN_2); + StepVerifier.create(flow).expectNext(cloudEvent).verifyComplete(); + verify(directAsyncGateway2).requestReply(cloudEvent, "target", CloudEvent.class); + } + + @Test + void shouldReplyWithDefaultDomain() { + when(directAsyncGateway1.reply("response", from)).thenReturn(Mono.empty()); + Mono flow = genericDirectAsyncGateway.reply("response", from); + StepVerifier.create(flow).verifyComplete(); + verify(directAsyncGateway1).reply("response", from); + } +} \ No newline at end of file diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/senders/GenericDomainEventBusTest.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/senders/GenericDomainEventBusTest.java new file mode 100644 index 00000000..c2f23fe6 --- /dev/null +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/senders/GenericDomainEventBusTest.java @@ -0,0 +1,111 @@ +package org.reactivecommons.async.starter.senders; + +import io.cloudevents.CloudEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivecommons.api.domain.DomainEvent; +import org.reactivecommons.api.domain.DomainEventBus; +import org.reactivecommons.async.starter.exceptions.InvalidConfigurationException; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.concurrent.ConcurrentHashMap; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; + +@ExtendWith(MockitoExtension.class) +class GenericDomainEventBusTest { + public static final String DOMAIN_2 = "domain2"; + @Mock + private DomainEventBus domainEventBus1; + @Mock + private DomainEventBus domainEventBus2; + @Mock + private CloudEvent cloudEvent; + @Mock + private DomainEvent domainEvent; + private GenericDomainEventBus genericDomainEventBus; + + @BeforeEach + void setUp() { + ConcurrentHashMap domainEventBuses = new ConcurrentHashMap<>(); + domainEventBuses.put(DEFAULT_DOMAIN, domainEventBus1); + domainEventBuses.put(DOMAIN_2, domainEventBus2); + genericDomainEventBus = new GenericDomainEventBus(domainEventBuses); + } + + @Test + void shouldEmitWithDefaultDomain() { + // Arrange + when(domainEventBus1.emit(domainEvent)).thenReturn(Mono.empty()); + // Act + Mono flow = Mono.from(genericDomainEventBus.emit(domainEvent)); + // Assert + StepVerifier.create(flow) + .verifyComplete(); + verify(domainEventBus1).emit(domainEvent); + } + + @Test + void shouldEmitCloudEventWithDefaultDomain() { + // Arrange + when(domainEventBus1.emit(cloudEvent)).thenReturn(Mono.empty()); + // Act + Mono flow = Mono.from(genericDomainEventBus.emit(cloudEvent)); + // Assert + StepVerifier.create(flow) + .verifyComplete(); + verify(domainEventBus1).emit(cloudEvent); + } + + @Test + void shouldEmitWithSpecificDomain() { + // Arrange + when(domainEventBus2.emit(domainEvent)).thenReturn(Mono.empty()); + // Act + Mono flow = Mono.from(genericDomainEventBus.emit(DOMAIN_2, domainEvent)); + // Assert + StepVerifier.create(flow) + .verifyComplete(); + verify(domainEventBus2).emit(domainEvent); + } + + @Test + void shouldEmitCloudEventWithSpecificDomain() { + // Arrange + when(domainEventBus2.emit(cloudEvent)).thenReturn(Mono.empty()); + // Act + Mono flow = Mono.from(genericDomainEventBus.emit(DOMAIN_2, cloudEvent)); + // Assert + StepVerifier.create(flow) + .verifyComplete(); + verify(domainEventBus2).emit(cloudEvent); + } + + @Test + void shouldFailWhenNoDomainFound() { + // Arrange + // Act + Mono flow = Mono.from(genericDomainEventBus.emit("another", domainEvent)); + // Assert + StepVerifier.create(flow) + .expectError(InvalidConfigurationException.class) + .verify(); + } + + @Test + void shouldFailWhenNoDomainFoundWithCloudEvent() { + // Arrange + // Act + Mono flow = Mono.from(genericDomainEventBus.emit("another", cloudEvent)); + // Assert + StepVerifier.create(flow) + .expectError(InvalidConfigurationException.class) + .verify(); + } +} diff --git a/starters/async-commons-starter/src/test/resources/application.yaml b/starters/async-commons-starter/src/test/resources/application.yaml new file mode 100644 index 00000000..30c7fd10 --- /dev/null +++ b/starters/async-commons-starter/src/test/resources/application.yaml @@ -0,0 +1,7 @@ +spring: + application: + name: test-app +my: + broker: + app: + enabled: true \ No newline at end of file diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/health/KafkaReactiveHealthIndicator.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/health/KafkaReactiveHealthIndicator.java index a2d4d3d4..2e2e8c30 100644 --- a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/health/KafkaReactiveHealthIndicator.java +++ b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/health/KafkaReactiveHealthIndicator.java @@ -7,16 +7,18 @@ import org.springframework.boot.actuate.health.Health; import reactor.core.publisher.Mono; +import static org.reactivecommons.async.starter.config.health.ReactiveCommonsHealthIndicator.DOMAIN; +import static org.reactivecommons.async.starter.config.health.ReactiveCommonsHealthIndicator.VERSION; + @Log4j2 @AllArgsConstructor public class KafkaReactiveHealthIndicator extends AbstractReactiveHealthIndicator { - public static final String VERSION = "version"; private final String domain; private final AdminClient adminClient; @Override protected Mono doHealthCheck(Health.Builder builder) { - builder.withDetail("domain", domain); + builder.withDetail(DOMAIN, domain); return checkKafkaHealth() .map(clusterId -> builder.up().withDetail(VERSION, clusterId).build()) .onErrorReturn(builder.down().build()); diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/RabbitReactiveHealthIndicator.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/RabbitReactiveHealthIndicator.java index c4f7282f..d8d0178e 100644 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/RabbitReactiveHealthIndicator.java +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/health/RabbitReactiveHealthIndicator.java @@ -10,9 +10,11 @@ import java.net.SocketException; +import static org.reactivecommons.async.starter.config.health.ReactiveCommonsHealthIndicator.DOMAIN; +import static org.reactivecommons.async.starter.config.health.ReactiveCommonsHealthIndicator.VERSION; + @Log4j2 public class RabbitReactiveHealthIndicator extends AbstractReactiveHealthIndicator { - public static final String VERSION = "version"; private final String domain; private final ConnectionFactory connectionFactory; @@ -24,7 +26,7 @@ public RabbitReactiveHealthIndicator(String domain, ConnectionFactory connection @Override protected Mono doHealthCheck(Health.Builder builder) { - builder.withDetail("domain", domain); + builder.withDetail(DOMAIN, domain); return Mono.fromCallable(() -> getRawVersion(connectionFactory)) .map(status -> builder.up().withDetail(VERSION, status).build()); } From 997e73359a075a70b9465163d9d8643c4b726c25 Mon Sep 17 00:00:00 2001 From: Juan C Galvis <8420868+juancgalvis@users.noreply.github.com> Date: Fri, 11 Oct 2024 07:38:34 -0500 Subject: [PATCH 10/12] build(test): Add unit tests, update some dependencies --- async/async-commons/async-commons.gradle | 4 +- build.gradle | 4 +- gradle/wrapper/gradle-wrapper.jar | Bin 43462 -> 43583 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 7 ++- gradlew.bat | 22 +++++----- main.gradle | 4 +- .../async/starter/broker/Status.java | 12 ------ .../listeners/CommandsListenerConfigTest.java | 40 ++++++++++++++++++ .../listeners/EventsListenerConfigTest.java | 40 ++++++++++++++++++ .../NotificationEventsListenerConfigTest.java | 40 ++++++++++++++++++ .../listeners/QueriesListenerConfigTest.java | 40 ++++++++++++++++++ 12 files changed, 184 insertions(+), 31 deletions(-) delete mode 100644 starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/Status.java create mode 100644 starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/listeners/CommandsListenerConfigTest.java create mode 100644 starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/listeners/EventsListenerConfigTest.java create mode 100644 starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/listeners/NotificationEventsListenerConfigTest.java create mode 100644 starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/listeners/QueriesListenerConfigTest.java diff --git a/async/async-commons/async-commons.gradle b/async/async-commons/async-commons.gradle index 12176cf5..58d5e99b 100644 --- a/async/async-commons/async-commons.gradle +++ b/async/async-commons/async-commons.gradle @@ -10,8 +10,8 @@ dependencies { compileOnly 'io.projectreactor:reactor-core' api 'com.fasterxml.jackson.core:jackson-databind' api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' - implementation 'commons-io:commons-io:2.16.1' + implementation 'commons-io:commons-io:2.17.0' implementation 'io.cloudevents:cloudevents-json-jackson:4.0.1' testImplementation 'io.projectreactor:reactor-test' -} +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 47a10da8..67426f4e 100644 --- a/build.gradle +++ b/build.gradle @@ -13,9 +13,9 @@ buildscript { plugins { id 'jacoco' id 'org.sonarqube' version '5.1.0.4882' - id 'org.springframework.boot' version '3.3.1' apply false + id 'org.springframework.boot' version '3.3.4' apply false id 'io.github.gradle-nexus.publish-plugin' version '2.0.0' - id 'co.com.bancolombia.cleanArchitecture' version '3.17.13' + id 'co.com.bancolombia.cleanArchitecture' version '3.17.26' } repositories { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd4917707c1f8861d8cb53dd15194d4248596..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch delta 34592 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 delta 34555 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a4413138..df97d72b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a42..f5feea6d 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 6689b85b..9b42019c 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/main.gradle b/main.gradle index 47a82cce..447b475d 100644 --- a/main.gradle +++ b/main.gradle @@ -80,7 +80,7 @@ subprojects { dependencyManagement { imports { - mavenBom 'org.springframework.boot:spring-boot-dependencies:3.3.1' + mavenBom 'org.springframework.boot:spring-boot-dependencies:3.3.4' } } @@ -176,5 +176,5 @@ tasks.register('generateMergedReport', JacocoReport) { } tasks.named('wrapper') { - gradleVersion = '8.8' + gradleVersion = '8.10.2' } \ No newline at end of file diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/Status.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/Status.java deleted file mode 100644 index 9c6cff48..00000000 --- a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/broker/Status.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.reactivecommons.async.starter.broker; - -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class Status { - private final boolean up; - private final String domain; - private final String details; // version or error -} diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/listeners/CommandsListenerConfigTest.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/listeners/CommandsListenerConfigTest.java new file mode 100644 index 00000000..986c0081 --- /dev/null +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/listeners/CommandsListenerConfigTest.java @@ -0,0 +1,40 @@ +package org.reactivecommons.async.starter.listeners; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivecommons.async.commons.HandlerResolver; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.config.ConnectionManager; +import org.reactivecommons.async.starter.config.DomainHandlers; + +import static org.mockito.Mockito.verify; +import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; + +@ExtendWith(MockitoExtension.class) +class CommandsListenerConfigTest { + @Mock + private BrokerProvider provider; + @Mock + private HandlerResolver resolver; + + @BeforeEach + void setUp() { + ConnectionManager manager = new ConnectionManager(); + manager.addDomain(DEFAULT_DOMAIN, provider); + DomainHandlers handlers = new DomainHandlers(); + handlers.add(DEFAULT_DOMAIN, resolver); + new CommandsListenerConfig(manager, handlers); + } + + @Test + void shouldListen() { + // Arrange + // Act + // Assert + verify(provider).listenCommands(resolver); + } + +} diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/listeners/EventsListenerConfigTest.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/listeners/EventsListenerConfigTest.java new file mode 100644 index 00000000..bfcab084 --- /dev/null +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/listeners/EventsListenerConfigTest.java @@ -0,0 +1,40 @@ +package org.reactivecommons.async.starter.listeners; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivecommons.async.commons.HandlerResolver; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.config.ConnectionManager; +import org.reactivecommons.async.starter.config.DomainHandlers; + +import static org.mockito.Mockito.verify; +import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; + +@ExtendWith(MockitoExtension.class) +class EventsListenerConfigTest { + @Mock + private BrokerProvider provider; + @Mock + private HandlerResolver resolver; + + @BeforeEach + void setUp() { + ConnectionManager manager = new ConnectionManager(); + manager.addDomain(DEFAULT_DOMAIN, provider); + DomainHandlers handlers = new DomainHandlers(); + handlers.add(DEFAULT_DOMAIN, resolver); + new EventsListenerConfig(manager, handlers); + } + + @Test + void shouldListen() { + // Arrange + // Act + // Assert + verify(provider).listenDomainEvents(resolver); + } + +} diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/listeners/NotificationEventsListenerConfigTest.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/listeners/NotificationEventsListenerConfigTest.java new file mode 100644 index 00000000..ece9dbd6 --- /dev/null +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/listeners/NotificationEventsListenerConfigTest.java @@ -0,0 +1,40 @@ +package org.reactivecommons.async.starter.listeners; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivecommons.async.commons.HandlerResolver; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.config.ConnectionManager; +import org.reactivecommons.async.starter.config.DomainHandlers; + +import static org.mockito.Mockito.verify; +import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; + +@ExtendWith(MockitoExtension.class) +class NotificationEventsListenerConfigTest { + @Mock + private BrokerProvider provider; + @Mock + private HandlerResolver resolver; + + @BeforeEach + void setUp() { + ConnectionManager manager = new ConnectionManager(); + manager.addDomain(DEFAULT_DOMAIN, provider); + DomainHandlers handlers = new DomainHandlers(); + handlers.add(DEFAULT_DOMAIN, resolver); + new NotificationEventsListenerConfig(manager, handlers); + } + + @Test + void shouldListen() { + // Arrange + // Act + // Assert + verify(provider).listenNotificationEvents(resolver); + } + +} diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/listeners/QueriesListenerConfigTest.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/listeners/QueriesListenerConfigTest.java new file mode 100644 index 00000000..d327c9a1 --- /dev/null +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/listeners/QueriesListenerConfigTest.java @@ -0,0 +1,40 @@ +package org.reactivecommons.async.starter.listeners; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivecommons.async.commons.HandlerResolver; +import org.reactivecommons.async.starter.broker.BrokerProvider; +import org.reactivecommons.async.starter.config.ConnectionManager; +import org.reactivecommons.async.starter.config.DomainHandlers; + +import static org.mockito.Mockito.verify; +import static org.reactivecommons.async.api.HandlerRegistry.DEFAULT_DOMAIN; + +@ExtendWith(MockitoExtension.class) +class QueriesListenerConfigTest { + @Mock + private BrokerProvider provider; + @Mock + private HandlerResolver resolver; + + @BeforeEach + void setUp() { + ConnectionManager manager = new ConnectionManager(); + manager.addDomain(DEFAULT_DOMAIN, provider); + DomainHandlers handlers = new DomainHandlers(); + handlers.add(DEFAULT_DOMAIN, resolver); + new QueriesListenerConfig(manager, handlers); + } + + @Test + void shouldListen() { + // Arrange + // Act + // Assert + verify(provider).listenQueries(resolver); + } + +} From b2816df977255bcc894d5fd17bedf215556ac9e2 Mon Sep 17 00:00:00 2001 From: Juan C Galvis <8420868+juancgalvis@users.noreply.github.com> Date: Fri, 11 Oct 2024 08:13:44 -0500 Subject: [PATCH 11/12] build(test): Add unit tests, update some dependencies --- main.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.gradle b/main.gradle index 447b475d..64978624 100644 --- a/main.gradle +++ b/main.gradle @@ -25,7 +25,7 @@ allprojects { property "sonar.junit.reportPaths", "build/test-results/test" property "sonar.java-coveragePlugin", "jacoco" property "sonar.coverage.jacoco.xmlReportPaths", "${rootDir}/build/reports/jacoco/generateMergedReport/generateMergedReport.xml" - property "sonar.exclusions", ".github/**,samples/**/*,**/mybroker/**/*" + property "sonar.exclusions", ".github/**,samples/**/*,**/mybroker/**/*,**/standalone/**/*" property 'sonar.coverage.exclusions', 'samples/**/*' } } From 03669642b1acc37d799b39fc3ce05e18a56407bc Mon Sep 17 00:00:00 2001 From: Juan C Galvis <8420868+juancgalvis@users.noreply.github.com> Date: Fri, 11 Oct 2024 09:00:06 -0500 Subject: [PATCH 12/12] build(sonar): Fix some sonar issues and add unit tests --- .../kafka/KafkaDirectAsyncGatewayTest.java | 48 +++++++++++++ .../async/kafka/KafkaDomainEventBusTest.java | 56 ++++++++++++++++ .../rabbit/RabbitDomainEventBusTest.java | 67 +++++++++++++++++++ .../props/GenericAsyncPropsDomain.java | 26 ++++--- .../starter/mybroker/MyBrokerProvider.java | 10 +-- .../props/GenericAsyncPropsDomainTest.java | 6 +- .../config/props/AsyncKafkaPropsDomain.java | 3 +- .../rabbit/config/props/AsyncPropsDomain.java | 3 +- 8 files changed, 193 insertions(+), 26 deletions(-) create mode 100644 async/async-kafka/src/test/java/org/reactivecommons/async/kafka/KafkaDirectAsyncGatewayTest.java create mode 100644 async/async-kafka/src/test/java/org/reactivecommons/async/kafka/KafkaDomainEventBusTest.java create mode 100644 async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/RabbitDomainEventBusTest.java diff --git a/async/async-kafka/src/test/java/org/reactivecommons/async/kafka/KafkaDirectAsyncGatewayTest.java b/async/async-kafka/src/test/java/org/reactivecommons/async/kafka/KafkaDirectAsyncGatewayTest.java new file mode 100644 index 00000000..d1229f58 --- /dev/null +++ b/async/async-kafka/src/test/java/org/reactivecommons/async/kafka/KafkaDirectAsyncGatewayTest.java @@ -0,0 +1,48 @@ +package org.reactivecommons.async.kafka; + +import io.cloudevents.CloudEvent; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivecommons.api.domain.Command; +import org.reactivecommons.async.api.AsyncQuery; +import org.reactivecommons.async.api.DirectAsyncGateway; +import org.reactivecommons.async.api.From; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ExtendWith(MockitoExtension.class) +class KafkaDirectAsyncGatewayTest { + private final DirectAsyncGateway directAsyncGateway = new KafkaDirectAsyncGateway(); + private final String targetName = "targetName"; + private final String domain = "domain"; + private final long delay = 1000L; + @Mock + private CloudEvent cloudEvent; + @Mock + private Command command; + @Mock + private AsyncQuery query; + @Mock + private From from; + + @Test + void allMethodsAreNotImplemented() { + assertThrows(UnsupportedOperationException.class, () -> directAsyncGateway.sendCommand(cloudEvent, targetName)); + assertThrows(UnsupportedOperationException.class, () -> directAsyncGateway.sendCommand(cloudEvent, targetName, domain)); + assertThrows(UnsupportedOperationException.class, () -> directAsyncGateway.sendCommand(cloudEvent, targetName, delay)); + assertThrows(UnsupportedOperationException.class, () -> directAsyncGateway.sendCommand(cloudEvent, targetName, delay, domain)); + assertThrows(UnsupportedOperationException.class, () -> directAsyncGateway.sendCommand(command, targetName)); + assertThrows(UnsupportedOperationException.class, () -> directAsyncGateway.sendCommand(command, targetName, domain)); + assertThrows(UnsupportedOperationException.class, () -> directAsyncGateway.sendCommand(command, targetName, delay)); + assertThrows(UnsupportedOperationException.class, () -> directAsyncGateway.sendCommand(command, targetName, delay, domain)); + + assertThrows(UnsupportedOperationException.class, () -> directAsyncGateway.requestReply(cloudEvent, targetName, CloudEvent.class)); + assertThrows(UnsupportedOperationException.class, () -> directAsyncGateway.requestReply(cloudEvent, targetName, CloudEvent.class, domain)); + assertThrows(UnsupportedOperationException.class, () -> directAsyncGateway.requestReply(query, targetName, CloudEvent.class)); + assertThrows(UnsupportedOperationException.class, () -> directAsyncGateway.requestReply(query, targetName, CloudEvent.class, domain)); + + assertThrows(UnsupportedOperationException.class, () -> directAsyncGateway.reply(targetName, from)); + } +} diff --git a/async/async-kafka/src/test/java/org/reactivecommons/async/kafka/KafkaDomainEventBusTest.java b/async/async-kafka/src/test/java/org/reactivecommons/async/kafka/KafkaDomainEventBusTest.java new file mode 100644 index 00000000..0dc04cdf --- /dev/null +++ b/async/async-kafka/src/test/java/org/reactivecommons/async/kafka/KafkaDomainEventBusTest.java @@ -0,0 +1,56 @@ +package org.reactivecommons.async.kafka; + +import io.cloudevents.CloudEvent; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivecommons.api.domain.DomainEvent; +import org.reactivecommons.async.kafka.communications.ReactiveMessageSender; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class KafkaDomainEventBusTest { + @Mock + private DomainEvent domainEvent; + @Mock + private CloudEvent cloudEvent; + @Mock + private ReactiveMessageSender sender; + @InjectMocks + private KafkaDomainEventBus kafkaDomainEventBus; + private final String domain = "domain"; + + @Test + void shouldEmitDomainEvent() { + // Arrange + when(sender.send(domainEvent)).thenReturn(Mono.empty()); + // Act + Mono flow = Mono.from(kafkaDomainEventBus.emit(domainEvent)); + // Assert + StepVerifier.create(flow) + .verifyComplete(); + } + + @Test + void shouldEmitCloudEvent() { + // Arrange + when(sender.send(cloudEvent)).thenReturn(Mono.empty()); + // Act + Mono flow = Mono.from(kafkaDomainEventBus.emit(cloudEvent)); + // Assert + StepVerifier.create(flow) + .verifyComplete(); + } + + @Test + void operationsShouldNotBeAbleForDomains() { + assertThrows(UnsupportedOperationException.class, () -> kafkaDomainEventBus.emit(domain, domainEvent)); + assertThrows(UnsupportedOperationException.class, () -> kafkaDomainEventBus.emit(domain, cloudEvent)); + } +} diff --git a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/RabbitDomainEventBusTest.java b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/RabbitDomainEventBusTest.java new file mode 100644 index 00000000..38ed6e4c --- /dev/null +++ b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/RabbitDomainEventBusTest.java @@ -0,0 +1,67 @@ +package org.reactivecommons.async.rabbit; + +import io.cloudevents.CloudEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.reactivecommons.api.domain.DomainEvent; +import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RabbitDomainEventBusTest { + @Mock + private DomainEvent domainEvent; + @Mock + private CloudEvent cloudEvent; + @Mock + private ReactiveMessageSender sender; + private RabbitDomainEventBus rabbitDomainEventBus; + private final String domain = "domain"; + + @BeforeEach + void setUp() { + rabbitDomainEventBus = new RabbitDomainEventBus(sender, "exchange"); + } + + @Test + void shouldEmitDomainEvent() { + // Arrange + when(domainEvent.getName()).thenReturn("event"); + when(sender.sendWithConfirm(any(DomainEvent.class), anyString(), anyString(), any(), anyBoolean())) + .thenReturn(Mono.empty()); + // Act + Mono flow = Mono.from(rabbitDomainEventBus.emit(domainEvent)); + // Assert + StepVerifier.create(flow) + .verifyComplete(); + } + + @Test + void shouldEmitCloudEvent() { + // Arrange + when(cloudEvent.getType()).thenReturn("event"); + when(sender.sendWithConfirm(any(CloudEvent.class), anyString(), anyString(), any(), anyBoolean())) + .thenReturn(Mono.empty()); + // Act + Mono flow = Mono.from(rabbitDomainEventBus.emit(cloudEvent)); + // Assert + StepVerifier.create(flow) + .verifyComplete(); + } + + @Test + void operationsShouldNotBeAbleForDomains() { + assertThrows(UnsupportedOperationException.class, () -> rabbitDomainEventBus.emit(domain, domainEvent)); + assertThrows(UnsupportedOperationException.class, () -> rabbitDomainEventBus.emit(domain, cloudEvent)); + } +} diff --git a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomain.java b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomain.java index 8c80b45d..a6717026 100644 --- a/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomain.java +++ b/starters/async-commons-starter/src/main/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomain.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.SneakyThrows; @@ -34,7 +33,7 @@ public GenericAsyncPropsDomain(String defaultAppName, ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); this.computeIfAbsent(DEFAULT_DOMAIN, k -> { - T defaultApp = GenericAsyncPropsDomain.instantiate(asyncPropsClass); + T defaultApp = AsyncPropsDomainBuilder.instantiate(asyncPropsClass); defaultApp.setConnectionProperties(mapper.convertValue(defaultProperties, propsClass)); return defaultApp; }); @@ -81,11 +80,10 @@ public T getProps(String domain) { P, X extends GenericAsyncPropsDomainProperties, R extends GenericAsyncPropsDomain> - AsyncPropsDomainBuilder builder(Class asyncPropsClass, - Class

    propsClass, + AsyncPropsDomainBuilder builder(Class

    propsClass, Class asyncPropsDomainClass, Constructor returnType) { - return new AsyncPropsDomainBuilder<>(asyncPropsClass, propsClass, asyncPropsDomainClass, returnType); + return new AsyncPropsDomainBuilder<>(propsClass, asyncPropsDomainClass, returnType); } public static class AsyncPropsDomainBuilder< @@ -101,7 +99,7 @@ public static class AsyncPropsDomainBuilder< private P defaultProperties; private SecretFiller

    secretFiller; - public AsyncPropsDomainBuilder(Class asynPropsClass, Class

    propsClass, Class asyncPropsDomainClass, + public AsyncPropsDomainBuilder(Class

    propsClass, Class asyncPropsDomainClass, Constructor returnType) { this.propsClass = propsClass; this.asyncPropsDomainClass = asyncPropsDomainClass; @@ -139,16 +137,16 @@ public R build() { return returnType.newInstance(defaultAppName, defaultProperties, domainProperties, secretFiller); } - } + @SneakyThrows + private static X instantiate(Class xClass) { + return xClass.getDeclaredConstructor().newInstance(); + } - @SneakyThrows - private static X instantiate(Class xClass) { - return xClass.getDeclaredConstructor().newInstance(); - } + @SneakyThrows + private static X instantiate(Class xClass, Map arg) { + return xClass.getDeclaredConstructor(Map.class).newInstance(arg); + } - @SneakyThrows - private static X instantiate(Class xClass, Map arg) { - return xClass.getDeclaredConstructor(Map.class).newInstance(arg); } public interface SecretFiller

    { diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/MyBrokerProvider.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/MyBrokerProvider.java index 33e6b3ba..95f5e976 100644 --- a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/MyBrokerProvider.java +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/mybroker/MyBrokerProvider.java @@ -33,27 +33,27 @@ public DirectAsyncGateway getDirectAsyncGateway(HandlerResolver resolver) { @Override public void listenDomainEvents(HandlerResolver resolver) { - + // for testing purposes } @Override public void listenNotificationEvents(HandlerResolver resolver) { - + // for testing purposes } @Override public void listenCommands(HandlerResolver resolver) { - + // for testing purposes } @Override public void listenQueries(HandlerResolver resolver) { - + // for testing purposes } @Override public void listenReplies(HandlerResolver resolver) { - + // for testing purposes } @Override diff --git a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomainTest.java b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomainTest.java index 69a0ad60..8e4dac67 100644 --- a/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomainTest.java +++ b/starters/async-commons-starter/src/test/java/org/reactivecommons/async/starter/props/GenericAsyncPropsDomainTest.java @@ -2,11 +2,11 @@ import org.junit.jupiter.api.Test; import org.reactivecommons.async.starter.exceptions.InvalidConfigurationException; +import org.reactivecommons.async.starter.mybroker.MyBrokerSecretFiller; import org.reactivecommons.async.starter.mybroker.props.AsyncMyBrokerPropsDomainProperties; import org.reactivecommons.async.starter.mybroker.props.MyBrokerAsyncProps; -import org.reactivecommons.async.starter.mybroker.props.MyBrokerConnProps; import org.reactivecommons.async.starter.mybroker.props.MyBrokerAsyncPropsDomain; -import org.reactivecommons.async.starter.mybroker.MyBrokerSecretFiller; +import org.reactivecommons.async.starter.mybroker.props.MyBrokerConnProps; import java.lang.reflect.Constructor; @@ -47,7 +47,7 @@ void shouldCreatePropsWithDefaultConnectionProperties() { String defaultAppName = "sample"; MyBrokerConnProps defaultMyBrokerProps = new MyBrokerConnProps(); MyBrokerAsyncProps propsConfigured = new MyBrokerAsyncProps(); - MyBrokerAsyncPropsDomain propsDomain = MyBrokerAsyncPropsDomain.builder(MyBrokerAsyncProps.class, MyBrokerConnProps.class, + MyBrokerAsyncPropsDomain propsDomain = MyBrokerAsyncPropsDomain.builder(MyBrokerConnProps.class, AsyncMyBrokerPropsDomainProperties.class, (Constructor) MyBrokerAsyncPropsDomain.class.getDeclaredConstructors()[0]) .withDefaultAppName(defaultAppName) diff --git a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaPropsDomain.java b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaPropsDomain.java index babe8d05..ea9f9496 100644 --- a/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaPropsDomain.java +++ b/starters/async-kafka-starter/src/main/java/org/reactivecommons/async/kafka/config/props/AsyncKafkaPropsDomain.java @@ -23,8 +23,7 @@ public AsyncKafkaPropsDomain(@Value("${spring.application.name}") String default @SuppressWarnings("unchecked") public static AsyncPropsDomainBuilder builder() { - return GenericAsyncPropsDomain.builder(AsyncKafkaProps.class, - KafkaProperties.class, + return GenericAsyncPropsDomain.builder(KafkaProperties.class, AsyncKafkaPropsDomainProperties.class, (Constructor) AsyncKafkaPropsDomain.class.getDeclaredConstructors()[0]); } diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncPropsDomain.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncPropsDomain.java index 0398a55c..d72aaff8 100644 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncPropsDomain.java +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/config/props/AsyncPropsDomain.java @@ -22,8 +22,7 @@ public AsyncPropsDomain(@Value("${spring.application.name}") String defaultAppNa @SuppressWarnings("unchecked") public static AsyncPropsDomainBuilder builder() { - return GenericAsyncPropsDomain.builder(AsyncProps.class, - RabbitProperties.class, + return GenericAsyncPropsDomain.builder(RabbitProperties.class, AsyncRabbitPropsDomainProperties.class, (Constructor) AsyncPropsDomain.class.getDeclaredConstructors()[0]); }