diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierService.java index 7c57c9eb7..d666b0eb6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierService.java @@ -47,4 +47,7 @@ public interface BusinessEventNotifierService { */ > void addPostBusinessEventListener(Class eventType, BusinessEventListener listener); + void startExternalEventRecording(); + + void stopExternalEventRecording(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java index fc8dbf4a0..b0c2ac850 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java @@ -26,6 +26,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.config.FineractProperties; import org.apache.fineract.infrastructure.event.business.BusinessEventListener; +import org.apache.fineract.infrastructure.event.business.domain.BulkBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent; import org.apache.fineract.infrastructure.event.external.service.ExternalEventService; import org.springframework.beans.factory.InitializingBean; @@ -40,6 +41,9 @@ public class BusinessEventNotifierServiceImpl implements BusinessEventNotifierSe private final Map> preListeners = new HashMap<>(); private final Map> postListeners = new HashMap<>(); + private final ThreadLocal eventRecordingEnabled = ThreadLocal.withInitial(() -> false); + private final ThreadLocal>> recordedEvents = ThreadLocal.withInitial(ArrayList::new); + private final ExternalEventService externalEventService; private final FineractProperties fineractProperties; @@ -54,6 +58,7 @@ public class BusinessEventNotifierServiceImpl implements BusinessEventNotifierSe @Override public void notifyPreBusinessEvent(BusinessEvent businessEvent) { + throwExceptionIfBulkEvent(businessEvent); List businessEventListeners = preListeners.get(businessEvent.getClass()); if (businessEventListeners != null) { for (BusinessEventListener eventListener : businessEventListeners) { @@ -74,6 +79,7 @@ public class BusinessEventNotifierServiceImpl implements BusinessEventNotifierSe @Override public void notifyPostBusinessEvent(BusinessEvent businessEvent) { + throwExceptionIfBulkEvent(businessEvent); List businessEventListeners = postListeners.get(businessEvent.getClass()); if (businessEventListeners != null) { for (BusinessEventListener eventListener : businessEventListeners) { @@ -82,14 +88,14 @@ public class BusinessEventNotifierServiceImpl implements BusinessEventNotifierSe } if (isExternalEventPostingEnabled()) { // we only want to create external events for operations that were successful, hence the post listener - externalEventService.postEvent(businessEvent); + if (isExternalEventRecordingEnabled()) { + recordedEvents.get().add(businessEvent); + } else { + externalEventService.postEvent(businessEvent); + } } } - private boolean isExternalEventPostingEnabled() { - return fineractProperties.getEvents().getExternal().isEnabled(); - } - @Override public > void addPostBusinessEventListener(Class eventType, BusinessEventListener listener) { List businessEventListeners = postListeners.get(eventType); @@ -99,4 +105,37 @@ public class BusinessEventNotifierServiceImpl implements BusinessEventNotifierSe } businessEventListeners.add(listener); } + + private boolean isExternalEventRecordingEnabled() { + return eventRecordingEnabled.get(); + } + + private boolean isExternalEventPostingEnabled() { + return fineractProperties.getEvents().getExternal().isEnabled(); + } + + private void throwExceptionIfBulkEvent(BusinessEvent businessEvent) { + if (businessEvent instanceof BulkBusinessEvent) { + throw new IllegalArgumentException("BulkBusinessEvent cannot be raised directly"); + } + } + + @Override + public void startExternalEventRecording() { + eventRecordingEnabled.set(true); + } + + @Override + public void stopExternalEventRecording() { + eventRecordingEnabled.set(false); + try { + List> recordedBusinessEvents = recordedEvents.get(); + if (isExternalEventPostingEnabled()) { + log.debug("Posting the BulkBusinessEvent for the recorded {} events", recordedBusinessEvents.size()); + externalEventService.postEvent(new BulkBusinessEvent(recordedBusinessEvents)); + } + } finally { + recordedEvents.remove(); + } + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/DelayedExternalEventService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/DelayedExternalEventService.java deleted file mode 100644 index fd82f36ab..000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/DelayedExternalEventService.java +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.infrastructure.event.external.service; - -import java.util.ArrayList; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.apache.fineract.infrastructure.event.business.domain.BulkBusinessEvent; -import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class DelayedExternalEventService { - - private final ThreadLocal>> localEventStorage = ThreadLocal.withInitial(ArrayList::new); - - private final ExternalEventService delegate; - - public void enqueueEvent(BusinessEvent event) { - if (event == null) { - throw new IllegalArgumentException("event cannot be null"); - } - - localEventStorage.get().add(event); - } - - public boolean hasEnqueuedEvents() { - return !localEventStorage.get().isEmpty(); - } - - public void clearEnqueuedEvents() { - localEventStorage.get().clear(); - } - - public List> getEnqueuedEvents() { - return List.copyOf(localEventStorage.get()); - } - - public void postEnqueuedEvents() { - List> enqueuedEvents = localEventStorage.get(); - if (enqueuedEvents.isEmpty()) { - throw new IllegalStateException("No events have been enqueued"); - } - - delegate.postEvent(new BulkBusinessEvent(enqueuedEvents)); - } -} diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImplTest.java new file mode 100644 index 000000000..e1a0ad814 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImplTest.java @@ -0,0 +1,161 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.event.business.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import org.apache.fineract.infrastructure.core.config.FineractProperties; +import org.apache.fineract.infrastructure.event.business.BusinessEventListener; +import org.apache.fineract.infrastructure.event.business.domain.BulkBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent; +import org.apache.fineract.infrastructure.event.external.service.ExternalEventService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +@ExtendWith(MockitoExtension.class) +@SuppressWarnings({ "rawtypes", "unchecked" }) +@MockitoSettings(strictness = Strictness.LENIENT) +class BusinessEventNotifierServiceImplTest { + + @Mock + private ExternalEventService externalEventService; + @Mock + private FineractProperties fineractProperties; + + @InjectMocks + private BusinessEventNotifierServiceImpl underTest; + + @Test + public void testNotifyPostBusinessEventShouldNotifyPostListeners() { + // given + configureExternalEventsProperties(false); + + MockBusinessEvent event = new MockBusinessEvent(); + BusinessEventListener postListener = mockListener(); + underTest.addPostBusinessEventListener(MockBusinessEvent.class, postListener); + // when + underTest.notifyPostBusinessEvent(event); + // then + verify(postListener).onBusinessEvent(event); + verifyNoInteractions(externalEventService); + } + + @Test + public void testNotifyPostBusinessEventShouldNotifyPostListenersAndPostAnExternalEvent() { + // given + configureExternalEventsProperties(true); + + MockBusinessEvent event = new MockBusinessEvent(); + BusinessEventListener postListener = mockListener(); + underTest.addPostBusinessEventListener(MockBusinessEvent.class, postListener); + // when + underTest.notifyPostBusinessEvent(event); + // then + verify(postListener).onBusinessEvent(event); + verify(externalEventService).postEvent(event); + } + + @Test + public void testNotifyPostBusinessEventShouldNotifyPostListenersAndPostAnBulkExternalEventWhenRecordingEnabled() { + // given + configureExternalEventsProperties(true); + + MockBusinessEvent event = new MockBusinessEvent(); + BusinessEventListener postListener = mockListener(); + underTest.addPostBusinessEventListener(MockBusinessEvent.class, postListener); + underTest.startExternalEventRecording(); + underTest.notifyPostBusinessEvent(event); + // when + underTest.stopExternalEventRecording(); + // then + verify(postListener).onBusinessEvent(event); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(BulkBusinessEvent.class); + verify(externalEventService).postEvent(argumentCaptor.capture()); + BulkBusinessEvent capturedEvent = argumentCaptor.getValue(); + assertThat(capturedEvent.get()).hasSize(1); + assertThat(capturedEvent.get().get(0)).isEqualTo(event); + } + + @Test + public void testNotifyPreBusinessEventShouldNotifyPreListeners() { + // given + configureExternalEventsProperties(false); + + MockBusinessEvent event = new MockBusinessEvent(); + BusinessEventListener preListener = mockListener(); + underTest.addPreBusinessEventListener(MockBusinessEvent.class, preListener); + // when + underTest.notifyPreBusinessEvent(event); + // then + verify(preListener).onBusinessEvent(event); + verifyNoInteractions(externalEventService); + } + + @Test + public void testNotifyPreBusinessEventShouldNotifyPreListenersWithoutPostingAnExternalEvent() { + // given + configureExternalEventsProperties(true); + + MockBusinessEvent event = new MockBusinessEvent(); + BusinessEventListener preListener = mockListener(); + underTest.addPreBusinessEventListener(MockBusinessEvent.class, preListener); + // when + underTest.notifyPreBusinessEvent(event); + // then + verify(preListener).onBusinessEvent(event); + verifyNoInteractions(externalEventService); + } + + private void configureExternalEventsProperties(boolean isExternalEventsEnabled) { + FineractProperties.FineractEventsProperties eventsProperties = new FineractProperties.FineractEventsProperties(); + FineractProperties.FineractExternalEventsProperties externalProperties = new FineractProperties.FineractExternalEventsProperties(); + eventsProperties.setExternal(externalProperties); + externalProperties.setEnabled(isExternalEventsEnabled); + given(fineractProperties.getEvents()).willReturn(eventsProperties); + } + + private BusinessEventListener mockListener() { + return (BusinessEventListener) mock(BusinessEventListener.class); + } + + private static class MockBusinessEvent implements BusinessEvent { + + @Override + public Object get() { + return null; + } + + @Override + public String getType() { + return null; + } + } + +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/DelayedExternalEventServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/DelayedExternalEventServiceTest.java deleted file mode 100644 index 8ecf62f46..000000000 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/DelayedExternalEventServiceTest.java +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.infrastructure.event.external.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import java.util.List; -import org.apache.fineract.infrastructure.event.business.domain.BulkBusinessEvent; -import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith(MockitoExtension.class) -@SuppressWarnings({ "rawtypes", "unchecked" }) -class DelayedExternalEventServiceTest { - - @Mock - private ExternalEventService delegate; - - @InjectMocks - private DelayedExternalEventService underTest; - - @BeforeEach - public void setUp() { - underTest.clearEnqueuedEvents(); - } - - @Test - public void testEnqueueEventFailsWhenNullEventIsGiven() { - // given - // when & then - assertThatThrownBy(() -> underTest.enqueueEvent(null)).isExactlyInstanceOf(IllegalArgumentException.class); - } - - @Test - public void testEnqueueEventWorks() { - // given - BusinessEvent event = mock(BusinessEvent.class); - // when - underTest.enqueueEvent(event); - // then - List> enqueuedEvents = underTest.getEnqueuedEvents(); - assertThat(enqueuedEvents).hasSize(1); - assertThat(enqueuedEvents.get(0)).isEqualTo(event); - } - - @Test - public void testHasEnqueuedEventsReturnsTrueWhenEventIsEnqueued() { - // given - BusinessEvent event = mock(BusinessEvent.class); - underTest.enqueueEvent(event); - // when - boolean result = underTest.hasEnqueuedEvents(); - // then - assertThat(result).isTrue(); - } - - @Test - public void testHasEnqueuedEventsReturnsFalseWhenNoEventIsEnqueued() { - // given - // when - boolean result = underTest.hasEnqueuedEvents(); - // then - assertThat(result).isFalse(); - } - - @Test - public void testPostEnqueuedEventsFailsWhenNoEventIsEnqueued() { - // given - // when & then - assertThatThrownBy(() -> underTest.postEnqueuedEvents()).isExactlyInstanceOf(IllegalStateException.class); - } - - @Test - public void testPostEnqueuedEventsWorks() { - // given - ArgumentCaptor delegateEventCaptor = ArgumentCaptor.forClass(BusinessEvent.class); - - BusinessEvent event = mock(BusinessEvent.class); - underTest.enqueueEvent(event); - // when - underTest.postEnqueuedEvents(); - // then - verify(delegate).postEvent(delegateEventCaptor.capture()); - BusinessEvent delegateEvent = delegateEventCaptor.getValue(); - assertThat(delegateEvent).isExactlyInstanceOf(BulkBusinessEvent.class); - BulkBusinessEvent bulkDelegateEvent = (BulkBusinessEvent) delegateEvent; - List> enqueuedEvents = bulkDelegateEvent.get(); - assertThat(enqueuedEvents).hasSize(1); - assertThat(enqueuedEvents.get(0)).isEqualTo(event); - } -}