From 452fb31d45f14cd4252f9f9f6cc75d3224f13b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20K=C3=A4sler?= <tom.kaesler@mni.thm.de> Date: Tue, 4 Dec 2018 13:40:42 +0100 Subject: [PATCH] add WebSockets with STOMP on top add WebSocketMessage entity structure add basic feedback logic add tests for Feedback messages --- pom.xml | 31 +++++- .../java/de/thm/arsnova/config/AppConfig.java | 1 + .../de/thm/arsnova/config/AppInitializer.java | 3 +- .../thm/arsnova/config/WebSocketConfig.java | 26 +++++ .../handler/FeedbackCommandHandler.java | 101 ++++++++++++++++++ .../controller/handler/FeedbackHandler.java | 43 ++++++++ .../websocket/message/CreateFeedback.java | 7 ++ .../message/CreateFeedbackPayload.java | 20 ++++ .../websocket/message/FeedbackChanged.java | 15 +++ .../message/FeedbackChangedPayload.java | 28 +++++ .../websocket/message/GetFeedback.java | 8 ++ .../arsnova/websocket/message/Patched.java | 7 ++ .../websocket/message/PatchedPayload.java | 47 ++++++++ .../websocket/message/WebSocketMessage.java | 27 +++++ .../websocket/message/WebSocketPayload.java | 4 + .../websocket/FeedbackCommandHandlerTest.java | 76 +++++++++++++ .../websocket/FeedbackHandlerTest.java | 44 ++++++++ 17 files changed, 485 insertions(+), 3 deletions(-) create mode 100644 src/main/java/de/thm/arsnova/config/WebSocketConfig.java create mode 100644 src/main/java/de/thm/arsnova/controller/handler/FeedbackCommandHandler.java create mode 100644 src/main/java/de/thm/arsnova/controller/handler/FeedbackHandler.java create mode 100644 src/main/java/de/thm/arsnova/websocket/message/CreateFeedback.java create mode 100644 src/main/java/de/thm/arsnova/websocket/message/CreateFeedbackPayload.java create mode 100644 src/main/java/de/thm/arsnova/websocket/message/FeedbackChanged.java create mode 100644 src/main/java/de/thm/arsnova/websocket/message/FeedbackChangedPayload.java create mode 100644 src/main/java/de/thm/arsnova/websocket/message/GetFeedback.java create mode 100644 src/main/java/de/thm/arsnova/websocket/message/Patched.java create mode 100644 src/main/java/de/thm/arsnova/websocket/message/PatchedPayload.java create mode 100644 src/main/java/de/thm/arsnova/websocket/message/WebSocketMessage.java create mode 100644 src/main/java/de/thm/arsnova/websocket/message/WebSocketPayload.java create mode 100644 src/test/java/de/thm/arsnova/websocket/FeedbackCommandHandlerTest.java create mode 100644 src/test/java/de/thm/arsnova/websocket/FeedbackHandlerTest.java diff --git a/pom.xml b/pom.xml index dd80def22..ccd6a0d59 100644 --- a/pom.xml +++ b/pom.xml @@ -169,6 +169,16 @@ <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-mail</artifactId> </dependency> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-messaging</artifactId> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-websocket</artifactId> + <scope>compile</scope> + </dependency> <!-- Security --> <dependency> <groupId>org.springframework.security</groupId> @@ -235,8 +245,8 @@ <artifactId>javax.mail</artifactId> </dependency> <dependency> - <groupId>org.springframework</groupId> - <artifactId>spring-test</artifactId> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-test</artifactId> <scope>test</scope> </dependency> <dependency> @@ -244,6 +254,17 @@ <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <version>3.11.1</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-test</artifactId> + <scope>test</scope> + </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> @@ -342,6 +363,12 @@ <artifactId>checker-qual</artifactId> <version>2.5.8</version> </dependency> + <dependency> + <groupId>javax.websocket</groupId> + <artifactId>javax.websocket-api</artifactId> + <version>1.1</version> + <scope>provided</scope> + </dependency> </dependencies> <build> diff --git a/src/main/java/de/thm/arsnova/config/AppConfig.java b/src/main/java/de/thm/arsnova/config/AppConfig.java index f1b869f1a..50b9af427 100644 --- a/src/main/java/de/thm/arsnova/config/AppConfig.java +++ b/src/main/java/de/thm/arsnova/config/AppConfig.java @@ -82,6 +82,7 @@ import java.util.List; @ComponentScan({ "de.thm.arsnova.cache", "de.thm.arsnova.controller", + "de.thm.arsnova.controller.handler", "de.thm.arsnova.event", "de.thm.arsnova.security", "de.thm.arsnova.service", diff --git a/src/main/java/de/thm/arsnova/config/AppInitializer.java b/src/main/java/de/thm/arsnova/config/AppInitializer.java index 6cf48b595..26aae5722 100644 --- a/src/main/java/de/thm/arsnova/config/AppInitializer.java +++ b/src/main/java/de/thm/arsnova/config/AppInitializer.java @@ -30,7 +30,8 @@ public class AppInitializer extends AbstractAnnotationConfigDispatcherServletIni return new Class[] { AppConfig.class, PersistenceConfig.class, - SecurityConfig.class + SecurityConfig.class, + WebSocketConfig.class, }; } diff --git a/src/main/java/de/thm/arsnova/config/WebSocketConfig.java b/src/main/java/de/thm/arsnova/config/WebSocketConfig.java new file mode 100644 index 000000000..c81588390 --- /dev/null +++ b/src/main/java/de/thm/arsnova/config/WebSocketConfig.java @@ -0,0 +1,26 @@ +package de.thm.arsnova.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import org.springframework.web.socket.server.jetty.JettyRequestUpgradeStrategy; + +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + config.enableSimpleBroker("/room"); + config.setApplicationDestinationPrefixes("/backend"); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws").setAllowedOrigins("*").withSockJS(); + } + +} diff --git a/src/main/java/de/thm/arsnova/controller/handler/FeedbackCommandHandler.java b/src/main/java/de/thm/arsnova/controller/handler/FeedbackCommandHandler.java new file mode 100644 index 000000000..e1d5bea1d --- /dev/null +++ b/src/main/java/de/thm/arsnova/controller/handler/FeedbackCommandHandler.java @@ -0,0 +1,101 @@ +package de.thm.arsnova.controller.handler; + +import de.thm.arsnova.websocket.message.CreateFeedback; +import de.thm.arsnova.websocket.message.FeedbackChanged; +import de.thm.arsnova.websocket.message.FeedbackChangedPayload; +import de.thm.arsnova.websocket.message.GetFeedback; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; + +import java.util.HashMap; + +@Component +public class FeedbackCommandHandler { + + HashMap<String, int[]> roomValues = new HashMap<>(); + + private final SimpMessagingTemplate messagingTemplate; + + @Autowired + public FeedbackCommandHandler(SimpMessagingTemplate messagingTemplate) { + this.messagingTemplate = messagingTemplate; + } + + synchronized private int[] updateFeedbackForRoom(String roomId, int index) { + int[] values = roomValues.getOrDefault(roomId, new int[4]); + values[index]++; + roomValues.put(roomId, values); + return values; + } + + public void handle(CreateFeedbackCommand command) { + int updatedIndex = command.getPayload().getPayload().getValue(); + int[] newVals = updateFeedbackForRoom(command.getRoomId(), updatedIndex); + + FeedbackChanged feedbackChanged = new FeedbackChanged(); + FeedbackChangedPayload feedbackChangedPayload = new FeedbackChangedPayload(); + feedbackChangedPayload.setValues(newVals); + feedbackChanged.setPayload(feedbackChangedPayload); + + messagingTemplate.convertAndSend( + "/room/" + command.getRoomId() + "/feedback.stream", + feedbackChanged + ); + } + + public void handle(GetFeedbackCommand command) { + int[] currentVals = roomValues.getOrDefault(command.getRoomId(), new int[4]); + + FeedbackChanged feedbackChanged = new FeedbackChanged(); + FeedbackChangedPayload feedbackChangedPayload = new FeedbackChangedPayload(); + feedbackChangedPayload.setValues(currentVals); + feedbackChanged.setPayload(feedbackChangedPayload); + + messagingTemplate.convertAndSend( + "/room/" + command.getRoomId() + "/feedback.stream", + feedbackChanged + ); + } + + + + public static class CreateFeedbackCommand { + + private String roomId; + private CreateFeedback payload; + + public CreateFeedbackCommand(String roomId, CreateFeedback payload) { + this.roomId = roomId; + this.payload = payload; + } + + public CreateFeedback getPayload() { + return payload; + } + + public String getRoomId() { + return roomId; + } + } + + public static class GetFeedbackCommand { + + private String roomId; + private GetFeedback payload; + + public GetFeedbackCommand(String roomId, GetFeedback payload) { + this.roomId = roomId; + this.payload = payload; + } + + public GetFeedback getPayload() { + return payload; + } + + public String getRoomId() { + return roomId; + } + } + +} diff --git a/src/main/java/de/thm/arsnova/controller/handler/FeedbackHandler.java b/src/main/java/de/thm/arsnova/controller/handler/FeedbackHandler.java new file mode 100644 index 000000000..ad7946f93 --- /dev/null +++ b/src/main/java/de/thm/arsnova/controller/handler/FeedbackHandler.java @@ -0,0 +1,43 @@ +package de.thm.arsnova.controller.handler; + +import de.thm.arsnova.websocket.message.CreateFeedback; +import de.thm.arsnova.websocket.message.GetFeedback; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.stereotype.Controller; + +@Controller +public class FeedbackHandler { + private final FeedbackCommandHandler commandHandler; + + @Autowired + public FeedbackHandler(FeedbackCommandHandler commandHandler) { + this.commandHandler = commandHandler; + } + + @MessageMapping("/room/{roomId}/feedback.command") + public void send( + @DestinationVariable("roomId") String roomId, + CreateFeedback value + ) throws Exception { + + commandHandler.handle( + new FeedbackCommandHandler.CreateFeedbackCommand(roomId, value) + ); + + } + + @MessageMapping("/room/{roomId}/feedback.query") + public void send( + @DestinationVariable("roomId") String roomId, + GetFeedback value + ) throws Exception { + + commandHandler.handle( + new FeedbackCommandHandler.GetFeedbackCommand(roomId, value) + ); + + } + +} diff --git a/src/main/java/de/thm/arsnova/websocket/message/CreateFeedback.java b/src/main/java/de/thm/arsnova/websocket/message/CreateFeedback.java new file mode 100644 index 000000000..f329d5f5c --- /dev/null +++ b/src/main/java/de/thm/arsnova/websocket/message/CreateFeedback.java @@ -0,0 +1,7 @@ +package de.thm.arsnova.websocket.message; + +public class CreateFeedback extends WebSocketMessage<CreateFeedbackPayload> { + public CreateFeedback() { + super(CreateFeedback.class.getSimpleName()); + } +} diff --git a/src/main/java/de/thm/arsnova/websocket/message/CreateFeedbackPayload.java b/src/main/java/de/thm/arsnova/websocket/message/CreateFeedbackPayload.java new file mode 100644 index 000000000..ee74510fd --- /dev/null +++ b/src/main/java/de/thm/arsnova/websocket/message/CreateFeedbackPayload.java @@ -0,0 +1,20 @@ +package de.thm.arsnova.websocket.message; + +public class CreateFeedbackPayload implements WebSocketPayload { + int value; + + public CreateFeedbackPayload() { + } + + public CreateFeedbackPayload(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } +} diff --git a/src/main/java/de/thm/arsnova/websocket/message/FeedbackChanged.java b/src/main/java/de/thm/arsnova/websocket/message/FeedbackChanged.java new file mode 100644 index 000000000..8b0c6e62e --- /dev/null +++ b/src/main/java/de/thm/arsnova/websocket/message/FeedbackChanged.java @@ -0,0 +1,15 @@ +package de.thm.arsnova.websocket.message; + +public class FeedbackChanged extends WebSocketMessage<FeedbackChangedPayload> { + public FeedbackChanged() { + super(FeedbackChanged.class.getSimpleName()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FeedbackChanged that = (FeedbackChanged) o; + return this.getPayload().equals(that.getPayload()); + } +} diff --git a/src/main/java/de/thm/arsnova/websocket/message/FeedbackChangedPayload.java b/src/main/java/de/thm/arsnova/websocket/message/FeedbackChangedPayload.java new file mode 100644 index 000000000..cb7e738ba --- /dev/null +++ b/src/main/java/de/thm/arsnova/websocket/message/FeedbackChangedPayload.java @@ -0,0 +1,28 @@ +package de.thm.arsnova.websocket.message; + +import java.util.Arrays; + +public class FeedbackChangedPayload implements WebSocketPayload { + int[] values = new int[4]; + + public int[] getValues() { + return values; + } + + public void setValues(int[] values) { + this.values = values; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FeedbackChangedPayload that = (FeedbackChangedPayload) o; + return Arrays.equals(values, that.values); + } + + @Override + public int hashCode() { + return Arrays.hashCode(values); + } +} diff --git a/src/main/java/de/thm/arsnova/websocket/message/GetFeedback.java b/src/main/java/de/thm/arsnova/websocket/message/GetFeedback.java new file mode 100644 index 000000000..f6cd9ea26 --- /dev/null +++ b/src/main/java/de/thm/arsnova/websocket/message/GetFeedback.java @@ -0,0 +1,8 @@ +package de.thm.arsnova.websocket.message; + +public class GetFeedback extends WebSocketMessage<WebSocketPayload> { + public GetFeedback() { + super(GetFeedback.class.getSimpleName()); + } +} + diff --git a/src/main/java/de/thm/arsnova/websocket/message/Patched.java b/src/main/java/de/thm/arsnova/websocket/message/Patched.java new file mode 100644 index 000000000..87f84cab6 --- /dev/null +++ b/src/main/java/de/thm/arsnova/websocket/message/Patched.java @@ -0,0 +1,7 @@ +package de.thm.arsnova.websocket.message; + +public class Patched extends WebSocketMessage<PatchedPayload> { + public Patched(String type) { + super(type); + } +} diff --git a/src/main/java/de/thm/arsnova/websocket/message/PatchedPayload.java b/src/main/java/de/thm/arsnova/websocket/message/PatchedPayload.java new file mode 100644 index 000000000..1c94099a0 --- /dev/null +++ b/src/main/java/de/thm/arsnova/websocket/message/PatchedPayload.java @@ -0,0 +1,47 @@ +package de.thm.arsnova.websocket.message; + +public class PatchedPayload implements WebSocketPayload { + String type; + + String id; + + String propertyName; + + boolean propertyValue; + + public PatchedPayload(String type) { + this.type = type; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getPropertyName() { + return propertyName; + } + + public void setPropertyName(String propertyName) { + this.propertyName = propertyName; + } + + public boolean isPropertyValue() { + return propertyValue; + } + + public void setPropertyValue(boolean propertyValue) { + this.propertyValue = propertyValue; + } +} diff --git a/src/main/java/de/thm/arsnova/websocket/message/WebSocketMessage.java b/src/main/java/de/thm/arsnova/websocket/message/WebSocketMessage.java new file mode 100644 index 000000000..6a0b6d415 --- /dev/null +++ b/src/main/java/de/thm/arsnova/websocket/message/WebSocketMessage.java @@ -0,0 +1,27 @@ +package de.thm.arsnova.websocket.message; + +public class WebSocketMessage<P extends WebSocketPayload> { + private String type; + + private P payload; + + public WebSocketMessage(String type) { + this.type = type; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public P getPayload() { + return payload; + } + + public void setPayload(P payload) { + this.payload = payload; + } +} diff --git a/src/main/java/de/thm/arsnova/websocket/message/WebSocketPayload.java b/src/main/java/de/thm/arsnova/websocket/message/WebSocketPayload.java new file mode 100644 index 000000000..3ca21c77c --- /dev/null +++ b/src/main/java/de/thm/arsnova/websocket/message/WebSocketPayload.java @@ -0,0 +1,4 @@ +package de.thm.arsnova.websocket.message; + +public interface WebSocketPayload { +} diff --git a/src/test/java/de/thm/arsnova/websocket/FeedbackCommandHandlerTest.java b/src/test/java/de/thm/arsnova/websocket/FeedbackCommandHandlerTest.java new file mode 100644 index 000000000..f99e9026e --- /dev/null +++ b/src/test/java/de/thm/arsnova/websocket/FeedbackCommandHandlerTest.java @@ -0,0 +1,76 @@ +package de.thm.arsnova.websocket; + +import de.thm.arsnova.controller.handler.FeedbackCommandHandler; +import de.thm.arsnova.websocket.message.CreateFeedback; +import de.thm.arsnova.websocket.message.CreateFeedbackPayload; +import de.thm.arsnova.websocket.message.FeedbackChanged; +import de.thm.arsnova.websocket.message.FeedbackChangedPayload; +import de.thm.arsnova.websocket.message.GetFeedback; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; + +@RunWith(SpringRunner.class) +public class FeedbackCommandHandlerTest { + + @MockBean + private SimpMessagingTemplate messagingTemplate; + + private FeedbackCommandHandler commandHandler; + + @Before + public void setUp() { + this.commandHandler = new FeedbackCommandHandler(messagingTemplate); + } + + @Test + public void getFeedback() { + String roomId = "12345678"; + GetFeedback getFeedback = new GetFeedback(); + FeedbackCommandHandler.GetFeedbackCommand getFeedbackCommand = + new FeedbackCommandHandler.GetFeedbackCommand(roomId, null); + + commandHandler.handle(getFeedbackCommand); + + FeedbackChangedPayload feedbackChangedPayload = new FeedbackChangedPayload(); + int[] expectedVals = new int[]{0, 0, 0, 0}; + feedbackChangedPayload.setValues(expectedVals); + FeedbackChanged feedbackChanged = new FeedbackChanged(); + feedbackChanged.setPayload(feedbackChangedPayload); + + ArgumentCaptor<String> topicCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor<FeedbackChanged> messageCaptor = + ArgumentCaptor.forClass(FeedbackChanged.class); + + verify(messagingTemplate).convertAndSend(topicCaptor.capture(), messageCaptor.capture()); + assertThat(topicCaptor.getValue()).isEqualTo("/room/" + roomId + "/feedback"); + assertThat(messageCaptor.getValue()).isEqualTo(feedbackChanged); + } + + @Test + public void sendFeedback() { + String roomId = "12345678"; + CreateFeedbackPayload createFeedbackPayload = new CreateFeedbackPayload(1); + createFeedbackPayload.setValue(1); + CreateFeedback createFeedback = new CreateFeedback(); + createFeedback.setPayload(createFeedbackPayload); + FeedbackCommandHandler.CreateFeedbackCommand createFeedbackCommand = + new FeedbackCommandHandler.CreateFeedbackCommand(roomId, createFeedback); + + commandHandler.handle(createFeedbackCommand); + + ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); + verify(messagingTemplate).convertAndSend(captor.capture(), any(FeedbackChanged.class)); + assertThat(captor.getValue()).isEqualTo("/room/" + roomId + "/feedback"); + } +} + + diff --git a/src/test/java/de/thm/arsnova/websocket/FeedbackHandlerTest.java b/src/test/java/de/thm/arsnova/websocket/FeedbackHandlerTest.java new file mode 100644 index 000000000..6a5748995 --- /dev/null +++ b/src/test/java/de/thm/arsnova/websocket/FeedbackHandlerTest.java @@ -0,0 +1,44 @@ +package de.thm.arsnova.websocket; + +import de.thm.arsnova.controller.handler.FeedbackCommandHandler; +import de.thm.arsnova.controller.handler.FeedbackHandler; +import de.thm.arsnova.websocket.message.CreateFeedback; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@RunWith(SpringRunner.class) +public class FeedbackHandlerTest { + + @MockBean + private FeedbackCommandHandler feedbackCommandHandler; + + private FeedbackHandler feedbackHandler; + + @Before + public void setUp() { + this.feedbackHandler = new FeedbackHandler(feedbackCommandHandler); + } + + @Test + public void sendFeedback() throws Exception { + feedbackHandler.send( + "12345678", + new CreateFeedback() + ); + + ArgumentCaptor<FeedbackCommandHandler.CreateFeedbackCommand> captor = + ArgumentCaptor.forClass(FeedbackCommandHandler.CreateFeedbackCommand.class); + verify(feedbackCommandHandler, times(1)).handle(captor.capture()); + + assertThat(captor.getValue().getRoomId()).isEqualTo("12345678"); + assertThat(captor.getValue().getPayload()).isInstanceOf(CreateFeedback.class); + } +} -- GitLab