From 96d1faedea5a6c4aaba6a9b7efd1323b7669c488 Mon Sep 17 00:00:00 2001 From: Michael Fomenko Date: Sun, 22 May 2022 14:15:22 +0300 Subject: [PATCH 1/2] issue-4. in case many event listeners present call all of them event if one fails --- .../UndoListenerInvocationException.java | 15 ++++ .../springundocore/service/UndoService.java | 23 ++++-- .../fomenko/springundocore/UndoBaseTest.java | 76 ++++++++++++++++--- .../config/UndoTestConfiguration.java | 49 +++++++----- .../fomenko/springundocore/dto/TestDtoC.java | 12 +++ 5 files changed, 138 insertions(+), 37 deletions(-) create mode 100644 spring-undo-core/src/main/java/dev/fomenko/springundocore/UndoListenerInvocationException.java create mode 100644 spring-undo-core/src/test/java/dev/fomenko/springundocore/dto/TestDtoC.java diff --git a/spring-undo-core/src/main/java/dev/fomenko/springundocore/UndoListenerInvocationException.java b/spring-undo-core/src/main/java/dev/fomenko/springundocore/UndoListenerInvocationException.java new file mode 100644 index 0000000..880f80e --- /dev/null +++ b/spring-undo-core/src/main/java/dev/fomenko/springundocore/UndoListenerInvocationException.java @@ -0,0 +1,15 @@ +package dev.fomenko.springundocore; + +import lombok.Getter; + +import java.util.Collection; + +public class UndoListenerInvocationException extends RuntimeException { + @Getter + private final Collection causes; + + public UndoListenerInvocationException(String msg, Collection causes) { + super(msg, causes.iterator().next()); + this.causes = causes; + } +} diff --git a/spring-undo-core/src/main/java/dev/fomenko/springundocore/service/UndoService.java b/spring-undo-core/src/main/java/dev/fomenko/springundocore/service/UndoService.java index 3776737..3349c1b 100644 --- a/spring-undo-core/src/main/java/dev/fomenko/springundocore/service/UndoService.java +++ b/spring-undo-core/src/main/java/dev/fomenko/springundocore/service/UndoService.java @@ -1,7 +1,8 @@ package dev.fomenko.springundocore.service; -import dev.fomenko.springundocore.dto.ActionRecord; import dev.fomenko.springundocore.UndoEventListener; +import dev.fomenko.springundocore.UndoListenerInvocationException; +import dev.fomenko.springundocore.dto.ActionRecord; import dev.fomenko.springundocore.property.UndoProperties; import lombok.RequiredArgsConstructor; import lombok.extern.apachecommons.CommonsLog; @@ -9,6 +10,7 @@ import java.time.Duration; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; @@ -69,12 +71,23 @@ private void invokeListenersForRecord(ActionRecord record, Object action = record.getAction(); List> undoEventListeners = listeners.get(action.getClass()); - if (!CollectionUtils.isEmpty(undoEventListeners)) { - for (UndoEventListener listener : undoEventListeners) { + + if (CollectionUtils.isEmpty(undoEventListeners)) { + log.warn("There are no listeners for " + action.getClass()); + return; + } + + var errors = new ArrayList(); + for (UndoEventListener listener : undoEventListeners) { + try { methodReference.accept((UndoEventListener) listener, action); + } catch (Throwable e) { + errors.add(e); } - } else { - log.warn("There are no listeners for " + action.getClass()); + } + + if (!errors.isEmpty()) { + throw new UndoListenerInvocationException("There were errors while invoking undo listeners", errors); } } } diff --git a/spring-undo-core/src/test/java/dev/fomenko/springundocore/UndoBaseTest.java b/spring-undo-core/src/test/java/dev/fomenko/springundocore/UndoBaseTest.java index 5984f11..cfaa35f 100644 --- a/spring-undo-core/src/test/java/dev/fomenko/springundocore/UndoBaseTest.java +++ b/spring-undo-core/src/test/java/dev/fomenko/springundocore/UndoBaseTest.java @@ -1,11 +1,20 @@ package dev.fomenko.springundocore; import dev.fomenko.springundocore.config.UndoTestConfiguration; +import dev.fomenko.springundocore.config.UndoTestConfiguration.FaultyListenerC; +import dev.fomenko.springundocore.config.UndoTestConfiguration.FirstListenerA; +import dev.fomenko.springundocore.config.UndoTestConfiguration.FirstListenerB; +import dev.fomenko.springundocore.config.UndoTestConfiguration.SecondListenerA; +import dev.fomenko.springundocore.config.UndoTestConfiguration.SecondListenerB; +import dev.fomenko.springundocore.config.UndoTestConfiguration.SecondListenerC; +import dev.fomenko.springundocore.config.UndoTestConfiguration.ThirdListenerC; import dev.fomenko.springundocore.dto.ActionRecord; import dev.fomenko.springundocore.dto.TestDtoA; +import dev.fomenko.springundocore.dto.TestDtoC; import dev.fomenko.springundocore.service.ActionIdGenerator; import dev.fomenko.springundocore.service.EventRecorder; import dev.fomenko.springundocore.service.UndoService; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -35,44 +44,88 @@ class UndoBaseTest { @SpyBean private ActionIdGenerator idGenerator; @SpyBean - private UndoTestConfiguration.FirstListenerA firstListenerA; + private FirstListenerA firstListenerA; @SpyBean - private UndoTestConfiguration.SecondListenerA secondListenerA; + private SecondListenerA secondListenerA; @SpyBean - private UndoTestConfiguration.FirstListenerB firstListenerB; + private FirstListenerB firstListenerB; @SpyBean - private UndoTestConfiguration.SecondListenerB secondListenerB; + private SecondListenerB secondListenerB; + @SpyBean + private FaultyListenerC faultyListenerC; + @SpyBean + private SecondListenerC secondListenerC; + @SpyBean + private ThirdListenerC thirdListenerB; @Test void shouldInvokeListeners() { - Mockito.when(eventRecorder.deleteRecordById(Mockito.eq("1"))).thenReturn(true); + // given TestDtoA dtoA = new TestDtoA("a"); + Mockito.when(eventRecorder.deleteRecordById(Mockito.eq("1"))).thenReturn(true); Mockito.when(eventRecorder.getRecordById(Mockito.eq("1"))).thenReturn( Optional.ofNullable( ActionRecord.builder() .expiresAt(LocalDateTime.now()) .action(dtoA) .build())); - recordsService.invokeListenerByRecordId("1"); + // when + undo.undo("1"); + // then Mockito.verify(firstListenerB, Mockito.never()).onUndo(Mockito.any()); Mockito.verify(secondListenerB, Mockito.never()).onUndo(Mockito.any()); - Mockito.verify(firstListenerA, Mockito.times(1)).onUndo(Mockito.eq(dtoA)); - Mockito.verify(secondListenerA, Mockito.times(1)).onUndo(Mockito.eq(dtoA)); + Mockito.verify(firstListenerA).onUndo(Mockito.eq(dtoA)); + Mockito.verify(secondListenerA).onUndo(Mockito.eq(dtoA)); } @Test void shouldSaveEventWithRecorder() { + // given TestDtoA action = new TestDtoA("testA"); Mockito.when(idGenerator.generateId()).thenReturn("eventId"); + + // when undo.publish(action, Duration.ofSeconds(1)); - Mockito.verify(eventRecorder, Mockito.times(1)) - .saveRecord(Mockito.eq(new ActionRecord<>("eventId", action, LocalDateTime.parse("2019-02-24T09:33:13")))); + // then + var expectedRecord = new ActionRecord<>("eventId", action, LocalDateTime.parse("2019-02-24T09:33:13")); + Mockito.verify(eventRecorder).saveRecord(Mockito.eq(expectedRecord)); } @Test - @Disabled + void shouldInvokeAllUndoListenersEvenIfOneFails() { + // given + + var testDto = new TestDtoC("testA"); + String eventId = "eventId"; + + Mockito.when(idGenerator.generateId()).thenReturn(eventId); + Mockito.when(eventRecorder.deleteRecordById(Mockito.eq(eventId))).thenReturn(true); + Mockito.when(eventRecorder.getRecordById(Mockito.eq(eventId))).thenReturn( + Optional.ofNullable( + ActionRecord.builder() + .expiresAt(LocalDateTime.now()) + .action(testDto) + .build())); + + // when + undo.publish(testDto, Duration.ofSeconds(1)); + var exception = Assertions.assertThrows(UndoListenerInvocationException.class, + () -> undo.undo(eventId)); + + // then + Assertions.assertEquals(1, exception.getCauses().size()); + + var inOrder = Mockito.inOrder(faultyListenerC, secondListenerC, thirdListenerB); + + inOrder.verify(faultyListenerC).onUndo(Mockito.eq(testDto)); + inOrder.verify(secondListenerC).onUndo(Mockito.eq(testDto)); + inOrder.verify(thirdListenerB).onUndo(Mockito.eq(testDto)); + } + + @Test + @Disabled("for demo purposes") void example() { /// user will use only Undo component @@ -86,5 +139,4 @@ void example() { // then all the listeners being invoked } - } diff --git a/spring-undo-core/src/test/java/dev/fomenko/springundocore/config/UndoTestConfiguration.java b/spring-undo-core/src/test/java/dev/fomenko/springundocore/config/UndoTestConfiguration.java index 533d78f..f7029d3 100644 --- a/spring-undo-core/src/test/java/dev/fomenko/springundocore/config/UndoTestConfiguration.java +++ b/spring-undo-core/src/test/java/dev/fomenko/springundocore/config/UndoTestConfiguration.java @@ -1,6 +1,7 @@ package dev.fomenko.springundocore.config; import dev.fomenko.springundocore.UndoEventListener; +import dev.fomenko.springundocore.dto.TestDtoC; import dev.fomenko.springundocore.dto.TestDtoA; import dev.fomenko.springundocore.dto.TestDtoB; import dev.fomenko.springundocore.service.TimeSupplier; @@ -8,9 +9,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; import java.time.Clock; -import java.time.Duration; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; @@ -26,26 +27,8 @@ public TimeSupplier timeSupplier() { } // action listeners for several events - @Bean - public FirstListenerA firstA() { - return new FirstListenerA(); - } - - @Bean - public SecondListenerA secondA() { - return new SecondListenerA(); - } - - @Bean - public FirstListenerB firstB() { - return new FirstListenerB(); - } - - @Bean - public SecondListenerB secondB() { - return new SecondListenerB(); - } + @Component public static class FirstListenerA extends UndoEventListener { @Override public void onUndo(TestDtoA action) { @@ -53,6 +36,7 @@ public void onUndo(TestDtoA action) { } } + @Component public static class SecondListenerA extends UndoEventListener { @Override public void onUndo(TestDtoA action) { @@ -60,6 +44,7 @@ public void onUndo(TestDtoA action) { } } + @Component public static class FirstListenerB extends UndoEventListener { @Override public void onUndo(TestDtoB action) { @@ -67,6 +52,7 @@ public void onUndo(TestDtoB action) { } } + @Component public static class SecondListenerB extends UndoEventListener { @Override public void onUndo(TestDtoB action) { @@ -74,4 +60,27 @@ public void onUndo(TestDtoB action) { } } + @Component + public static class FaultyListenerC extends UndoEventListener { + @Override + public void onUndo(TestDtoC action) { + throw new RuntimeException("FaultyDtoListener"); + } + } + + @Component + public static class SecondListenerC extends UndoEventListener { + @Override + public void onUndo(TestDtoC action) { + System.err.println("SecondListenerC"); + } + } + + @Component + public static class ThirdListenerC extends UndoEventListener { + @Override + public void onUndo(TestDtoC action) { + System.err.println("ThirdListenerC"); + } + } } diff --git a/spring-undo-core/src/test/java/dev/fomenko/springundocore/dto/TestDtoC.java b/spring-undo-core/src/test/java/dev/fomenko/springundocore/dto/TestDtoC.java new file mode 100644 index 0000000..f2b3b06 --- /dev/null +++ b/spring-undo-core/src/test/java/dev/fomenko/springundocore/dto/TestDtoC.java @@ -0,0 +1,12 @@ +package dev.fomenko.springundocore.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TestDtoC { + private String data; +} From 7b96ca668b35727ec568cf04891c4419e63638e7 Mon Sep 17 00:00:00 2001 From: Michael Fomenko Date: Mon, 23 May 2022 21:11:23 +0300 Subject: [PATCH 2/2] fixed lift issue --- .../springundocore/UndoListenerInvocationException.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spring-undo-core/src/main/java/dev/fomenko/springundocore/UndoListenerInvocationException.java b/spring-undo-core/src/main/java/dev/fomenko/springundocore/UndoListenerInvocationException.java index 880f80e..6f69043 100644 --- a/spring-undo-core/src/main/java/dev/fomenko/springundocore/UndoListenerInvocationException.java +++ b/spring-undo-core/src/main/java/dev/fomenko/springundocore/UndoListenerInvocationException.java @@ -5,11 +5,15 @@ import java.util.Collection; public class UndoListenerInvocationException extends RuntimeException { - @Getter private final Collection causes; + public UndoListenerInvocationException(String msg, Collection causes) { super(msg, causes.iterator().next()); this.causes = causes; } + + public Collection getCauses() { + return causes; + } }