diff --git a/pom.xml b/pom.xml index dd80def2243f4570ece4da109678d81ba934f227..ccd6a0d596f9507b9cc726e4adcbd6ca8f2f99a2 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 f1b869f1a9f87044dc46592b8d73f22430c489f2..50b9af42706f918dd23c79d6abdd2cd622710243 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 6cf48b595e73a784bd5e3f7c7c5bab6316d827d8..26aae5722848cbc0a48f75fb1e332d7a8970b134 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 0000000000000000000000000000000000000000..c81588390702a23c946c7d4851416b7f428dabbe --- /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 0000000000000000000000000000000000000000..e1d5bea1ddd1f24b198e4d7482a94a54a286461e --- /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 0000000000000000000000000000000000000000..ad7946f937d8ef062cdc45f40747ec294609ff53 --- /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 0000000000000000000000000000000000000000..f329d5f5c7de7b07e19eaa7760a22af406e6553a --- /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 0000000000000000000000000000000000000000..ee74510fd0e835804bc32a152270c713c689a979 --- /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 0000000000000000000000000000000000000000..8b0c6e62e0b30c2cdb9392fb0d97f26394028d3b --- /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 0000000000000000000000000000000000000000..cb7e738baf2dde2e94c98e1d126f0b9b5017ebba --- /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 0000000000000000000000000000000000000000..f6cd9ea267cc809cdc53b0c7ee5e0475cb236580 --- /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 0000000000000000000000000000000000000000..87f84cab6e5bbd32db75c2dc5599ace34c8ae1c7 --- /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 0000000000000000000000000000000000000000..1c94099a0bc4448eaac3c9ed70d382f99e55fddc --- /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 0000000000000000000000000000000000000000..6a0b6d4152f821554a36ae15f25c113b4ee1561f --- /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 0000000000000000000000000000000000000000..3ca21c77ca38cb5e9340f094385a4189ecc78744 --- /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 0000000000000000000000000000000000000000..f99e9026eabe2c3599a792e0e3798ec709882bfb --- /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 0000000000000000000000000000000000000000..6a5748995768aeb1719334e8bf8a4a1b76206135 --- /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); + } +}