diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/Jetty10WebSocketServletWebServerCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/Jetty10WebSocketServletWebServerCustomizer.java new file mode 100644 index 0000000000..c378d6683d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/Jetty10WebSocketServletWebServerCustomizer.java @@ -0,0 +1,110 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.boot.autoconfigure.websocket.servlet; + +import java.lang.reflect.Method; + +import javax.servlet.ServletContext; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.webapp.AbstractConfiguration; +import org.eclipse.jetty.webapp.WebAppContext; + +import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.core.Ordered; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * WebSocket customizer for {@link JettyServletWebServerFactory} with Jetty 10. + * + * @author Andy Wilkinson + */ +class Jetty10WebSocketServletWebServerCustomizer + implements WebServerFactoryCustomizer, Ordered { + + static final String JETTY_WEB_SOCKET_SERVER_CONTAINER = "org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer"; + + static final String JAVAX_WEB_SOCKET_SERVER_CONTAINER = "org.eclipse.jetty.websocket.javax.server.internal.JavaxWebSocketServerContainer"; + + @Override + public void customize(JettyServletWebServerFactory factory) { + factory.addConfigurations(new AbstractConfiguration() { + + @Override + public void configure(WebAppContext context) throws Exception { + ServletContext servletContext = context.getServletContext(); + Class jettyContainer = ClassUtils.forName(JETTY_WEB_SOCKET_SERVER_CONTAINER, null); + Method getJettyContainer = ReflectionUtils.findMethod(jettyContainer, "getContainer", + ServletContext.class); + Server server = context.getServer(); + if (ReflectionUtils.invokeMethod(getJettyContainer, null, servletContext) == null) { + ensureWebSocketComponents(server, servletContext); + ensureContainer(jettyContainer, servletContext); + } + Class javaxContainer = ClassUtils.forName(JAVAX_WEB_SOCKET_SERVER_CONTAINER, null); + Method getJavaxContainer = ReflectionUtils.findMethod(javaxContainer, "getContainer", + ServletContext.class); + if (ReflectionUtils.invokeMethod(getJavaxContainer, "getContainer", servletContext) == null) { + ensureWebSocketComponents(server, servletContext); + ensureUpgradeFilter(servletContext); + ensureMappings(servletContext); + ensureContainer(javaxContainer, servletContext); + } + } + + private void ensureWebSocketComponents(Server server, ServletContext servletContext) + throws ClassNotFoundException { + Class webSocketServerComponents = ClassUtils + .forName("org.eclipse.jetty.websocket.core.server.WebSocketServerComponents", null); + Method ensureWebSocketComponents = ReflectionUtils.findMethod(webSocketServerComponents, + "ensureWebSocketComponents", Server.class, ServletContext.class); + ReflectionUtils.invokeMethod(ensureWebSocketComponents, null, server, servletContext); + } + + private void ensureContainer(Class container, ServletContext servletContext) + throws ClassNotFoundException { + Method ensureContainer = ReflectionUtils.findMethod(container, "ensureContainer", ServletContext.class); + ReflectionUtils.invokeMethod(ensureContainer, null, servletContext); + } + + private void ensureUpgradeFilter(ServletContext servletContext) throws ClassNotFoundException { + Class webSocketUpgradeFilter = ClassUtils + .forName("org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter", null); + Method ensureFilter = ReflectionUtils.findMethod(webSocketUpgradeFilter, "ensureFilter", + ServletContext.class); + ReflectionUtils.invokeMethod(ensureFilter, null, servletContext); + } + + private void ensureMappings(ServletContext servletContext) throws ClassNotFoundException { + Class webSocketMappings = ClassUtils + .forName("org.eclipse.jetty.websocket.core.server.WebSocketMappings", null); + Method ensureMappings = ReflectionUtils.findMethod(webSocketMappings, "ensureMappings", + ServletContext.class); + ReflectionUtils.invokeMethod(ensureMappings, null, servletContext); + } + + }); + } + + @Override + public int getOrder() { + return 0; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfiguration.java index de8a544e0b..70f0d2bf4e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -83,6 +83,19 @@ public class WebSocketServletAutoConfiguration { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = { Jetty10WebSocketServletWebServerCustomizer.JAVAX_WEB_SOCKET_SERVER_CONTAINER, + Jetty10WebSocketServletWebServerCustomizer.JETTY_WEB_SOCKET_SERVER_CONTAINER }) + static class Jetty10WebSocketConfiguration { + + @Bean + @ConditionalOnMissingBean(name = "websocketServletWebServerCustomizer") + Jetty10WebSocketServletWebServerCustomizer websocketServletWebServerCustomizer() { + return new Jetty10WebSocketServletWebServerCustomizer(); + } + + } + @Configuration(proxyBeanMethods = false) @ConditionalOnClass(io.undertow.websockets.jsr.Bootstrap.class) static class UndertowWebSocketConfiguration { diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/build.gradle new file mode 100644 index 0000000000..a8b3171a87 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/build.gradle @@ -0,0 +1,19 @@ +plugins { + id "java" + id "org.springframework.boot.conventions" +} + +description = "Spring Boot Jetty 10 smoke test" + +dependencies { + implementation(enforcedPlatform("org.eclipse.jetty:jetty-bom:10.0.5")) + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-jetty")) { + exclude group: "org.eclipse.jetty.websocket", module: "websocket-server" + exclude group: "org.eclipse.jetty.websocket", module: "javax-websocket-server-impl" + } + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) { + exclude module: "spring-boot-starter-tomcat" + } + + testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/src/main/java/smoketest/jetty/ExampleServletContextListener.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/src/main/java/smoketest/jetty/ExampleServletContextListener.java new file mode 100644 index 0000000000..2b408390f8 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/src/main/java/smoketest/jetty/ExampleServletContextListener.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.jetty10; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +import org.springframework.stereotype.Component; + +/** + * Simple {@link ServletContextListener} to test gh-2058. + */ +@Component +public class ExampleServletContextListener implements ServletContextListener { + + @Override + public void contextInitialized(ServletContextEvent sce) { + System.out.println("*** contextInitialized"); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + System.out.println("*** contextDestroyed"); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/src/main/java/smoketest/jetty/SampleJetty10Application.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/src/main/java/smoketest/jetty/SampleJetty10Application.java new file mode 100644 index 0000000000..4cee564618 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/src/main/java/smoketest/jetty/SampleJetty10Application.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.jetty10; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleJetty10Application { + + public static void main(String[] args) { + SpringApplication.run(SampleJetty10Application.class, args); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/src/main/java/smoketest/jetty/service/HelloWorldService.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/src/main/java/smoketest/jetty/service/HelloWorldService.java new file mode 100644 index 0000000000..732de935ba --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/src/main/java/smoketest/jetty/service/HelloWorldService.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.jetty10.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class HelloWorldService { + + @Value("${name:World}") + private String name; + + public String getHelloMessage() { + return "Hello " + this.name; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/src/main/java/smoketest/jetty/web/SampleController.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/src/main/java/smoketest/jetty/web/SampleController.java new file mode 100644 index 0000000000..d089c836d4 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/src/main/java/smoketest/jetty/web/SampleController.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.jetty10.web; + +import smoketest.jetty10.service.HelloWorldService; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +public class SampleController { + + @Autowired + private HelloWorldService helloWorldService; + + @GetMapping("/") + @ResponseBody + public String helloWorld() { + return this.helloWorldService.getHelloMessage(); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/src/main/resources/application.properties new file mode 100644 index 0000000000..eab83fbdfd --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/src/main/resources/application.properties @@ -0,0 +1,3 @@ +server.compression.enabled: true +server.compression.min-response-size: 1 +server.jetty.threads.acceptors=2 diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/src/test/java/smoketest/jetty/SampleJetty10ApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/src/test/java/smoketest/jetty/SampleJetty10ApplicationTests.java new file mode 100644 index 0000000000..dafc112154 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty10/src/test/java/smoketest/jetty/SampleJetty10ApplicationTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.jetty10; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.zip.GZIPInputStream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Basic integration tests for demo application. + * + * @author Dave Syer + * @author Andy Wilkinson + */ +@EnabledForJreRange(min = JRE.JAVA_11) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class SampleJetty10ApplicationTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void testHome() { + ResponseEntity entity = this.restTemplate.getForEntity("/", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).isEqualTo("Hello World"); + } + + @Test + void testCompression() throws Exception { + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.set("Accept-Encoding", "gzip"); + HttpEntity requestEntity = new HttpEntity<>(requestHeaders); + ResponseEntity entity = this.restTemplate.exchange("/", HttpMethod.GET, requestEntity, byte[].class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + try (GZIPInputStream inflater = new GZIPInputStream(new ByteArrayInputStream(entity.getBody()))) { + assertThat(StreamUtils.copyToString(inflater, StandardCharsets.UTF_8)).isEqualTo("Hello World"); + } + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/build.gradle new file mode 100644 index 0000000000..ea3d1b7b68 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/build.gradle @@ -0,0 +1,22 @@ +plugins { + id "java" + id "org.springframework.boot.conventions" +} + +description = "Spring Boot WebSocket Jetty 10 smoke test" + +dependencies { + implementation(enforcedPlatform("org.eclipse.jetty:jetty-bom:10.0.5")) + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-jetty")) { + exclude group: "org.eclipse.jetty.websocket", module: "websocket-server" + exclude group: "org.eclipse.jetty.websocket", module: "javax-websocket-server-impl" + } + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-websocket")) { + exclude module: "spring-boot-starter-tomcat" + } + + runtimeOnly ("org.eclipse.jetty.websocket:websocket-javax-server") + runtimeOnly ("org.eclipse.jetty.websocket:websocket-jetty-server") + + testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/SampleJetty10WebSocketsApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/SampleJetty10WebSocketsApplication.java new file mode 100644 index 0000000000..e2711206b2 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/SampleJetty10WebSocketsApplication.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.websocket.jetty10; + +import smoketest.websocket.jetty10.client.GreetingService; +import smoketest.websocket.jetty10.client.SimpleGreetingService; +import smoketest.websocket.jetty10.echo.DefaultEchoService; +import smoketest.websocket.jetty10.echo.EchoService; +import smoketest.websocket.jetty10.echo.EchoWebSocketHandler; +import smoketest.websocket.jetty10.reverse.ReverseWebSocketEndpoint; +import smoketest.websocket.jetty10.snake.SnakeWebSocketHandler; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +import org.springframework.web.socket.handler.PerConnectionWebSocketHandler; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +@Configuration(proxyBeanMethods = false) +@EnableAutoConfiguration +@EnableWebSocket +public class SampleJetty10WebSocketsApplication extends SpringBootServletInitializer implements WebSocketConfigurer { + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(echoWebSocketHandler(), "/echo").withSockJS(); + registry.addHandler(snakeWebSocketHandler(), "/snake").withSockJS(); + } + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(SampleJetty10WebSocketsApplication.class); + } + + @Bean + public EchoService echoService() { + return new DefaultEchoService("Did you say \"%s\"?"); + } + + @Bean + public GreetingService greetingService() { + return new SimpleGreetingService(); + } + + @Bean + public WebSocketHandler echoWebSocketHandler() { + return new EchoWebSocketHandler(echoService()); + } + + @Bean + public WebSocketHandler snakeWebSocketHandler() { + return new PerConnectionWebSocketHandler(SnakeWebSocketHandler.class); + } + + @Bean + public ReverseWebSocketEndpoint reverseWebSocketEndpoint() { + return new ReverseWebSocketEndpoint(); + } + + @Bean + public ServerEndpointExporter serverEndpointExporter() { + return new ServerEndpointExporter(); + } + + public static void main(String[] args) { + SpringApplication.run(SampleJetty10WebSocketsApplication.class, args); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/client/GreetingService.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/client/GreetingService.java new file mode 100644 index 0000000000..f0a8ff30da --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/client/GreetingService.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.websocket.jetty10.client; + +public interface GreetingService { + + String getGreeting(); + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/client/SimpleClientWebSocketHandler.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/client/SimpleClientWebSocketHandler.java new file mode 100644 index 0000000000..5e628fb651 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/client/SimpleClientWebSocketHandler.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.websocket.jetty10.client; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +public class SimpleClientWebSocketHandler extends TextWebSocketHandler { + + protected Log logger = LogFactory.getLog(SimpleClientWebSocketHandler.class); + + private final GreetingService greetingService; + + private final CountDownLatch latch; + + private final AtomicReference messagePayload; + + public SimpleClientWebSocketHandler(GreetingService greetingService, CountDownLatch latch, + AtomicReference message) { + this.greetingService = greetingService; + this.latch = latch; + this.messagePayload = message; + } + + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + TextMessage message = new TextMessage(this.greetingService.getGreeting()); + session.sendMessage(message); + } + + @Override + public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + this.logger.info("Received: " + message + " (" + this.latch.getCount() + ")"); + session.close(); + this.messagePayload.set(message.getPayload()); + this.latch.countDown(); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/client/SimpleGreetingService.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/client/SimpleGreetingService.java new file mode 100644 index 0000000000..5b5669db00 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/client/SimpleGreetingService.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.websocket.jetty10.client; + +public class SimpleGreetingService implements GreetingService { + + @Override + public String getGreeting() { + return "Hello world!"; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/echo/DefaultEchoService.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/echo/DefaultEchoService.java new file mode 100644 index 0000000000..d5df9473e3 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/echo/DefaultEchoService.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.websocket.jetty10.echo; + +public class DefaultEchoService implements EchoService { + + private final String echoFormat; + + public DefaultEchoService(String echoFormat) { + this.echoFormat = (echoFormat != null) ? echoFormat : "%s"; + } + + @Override + public String getMessage(String message) { + return String.format(this.echoFormat, message); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/echo/EchoService.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/echo/EchoService.java new file mode 100644 index 0000000000..b0d83dcd9f --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/echo/EchoService.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.websocket.jetty10.echo; + +public interface EchoService { + + String getMessage(String message); + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/echo/EchoWebSocketHandler.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/echo/EchoWebSocketHandler.java new file mode 100644 index 0000000000..c7cd9186c2 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/echo/EchoWebSocketHandler.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.websocket.jetty10.echo; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +/** + * Echo messages by implementing a Spring {@link WebSocketHandler} abstraction. + */ +public class EchoWebSocketHandler extends TextWebSocketHandler { + + private static Log logger = LogFactory.getLog(EchoWebSocketHandler.class); + + private final EchoService echoService; + + public EchoWebSocketHandler(EchoService echoService) { + this.echoService = echoService; + } + + @Override + public void afterConnectionEstablished(WebSocketSession session) { + logger.debug("Opened new session in instance " + this); + } + + @Override + public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + String echoMessage = this.echoService.getMessage(message.getPayload()); + logger.debug(echoMessage); + session.sendMessage(new TextMessage(echoMessage)); + } + + @Override + public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { + session.close(CloseStatus.SERVER_ERROR); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/reverse/ReverseWebSocketEndpoint.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/reverse/ReverseWebSocketEndpoint.java new file mode 100644 index 0000000000..c971981a4c --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/reverse/ReverseWebSocketEndpoint.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.websocket.jetty10.reverse; + +import java.io.IOException; + +import javax.websocket.OnMessage; +import javax.websocket.Session; +import javax.websocket.server.ServerEndpoint; + +@ServerEndpoint("/reverse") +public class ReverseWebSocketEndpoint { + + @OnMessage + public void handleMessage(Session session, String message) throws IOException { + session.getBasicRemote().sendText("Reversed: " + new StringBuilder(message).reverse()); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/snake/Direction.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/snake/Direction.java new file mode 100644 index 0000000000..76c1378ab5 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/snake/Direction.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.websocket.jetty10.snake; + +public enum Direction { + + NONE, NORTH, SOUTH, EAST, WEST + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/snake/Location.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/snake/Location.java new file mode 100644 index 0000000000..8121a754ad --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/snake/Location.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.websocket.jetty10.snake; + +public class Location { + + /** + * The X location. + */ + public int x; + + /** + * The Y location. + */ + public int y; + + public Location(int x, int y) { + this.x = x; + this.y = y; + } + + public Location getAdjacentLocation(Direction direction) { + switch (direction) { + case NORTH: + return new Location(this.x, this.y - SnakeUtils.GRID_SIZE); + case SOUTH: + return new Location(this.x, this.y + SnakeUtils.GRID_SIZE); + case EAST: + return new Location(this.x + SnakeUtils.GRID_SIZE, this.y); + case WEST: + return new Location(this.x - SnakeUtils.GRID_SIZE, this.y); + case NONE: + // fall through + default: + return this; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Location location = (Location) o; + if (this.x != location.x) { + return false; + } + if (this.y != location.y) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int result = this.x; + result = 31 * result + this.y; + return result; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/snake/Snake.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/snake/Snake.java new file mode 100644 index 0000000000..8f4cc363ea --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/snake/Snake.java @@ -0,0 +1,156 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.websocket.jetty10.snake; + +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Deque; + +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + +public class Snake { + + private static final int DEFAULT_LENGTH = 5; + + private final Deque tail = new ArrayDeque<>(); + + private final Object monitor = new Object(); + + private final int id; + + private final WebSocketSession session; + + private final String hexColor; + + private Direction direction; + + private int length = DEFAULT_LENGTH; + + private Location head; + + public Snake(int id, WebSocketSession session) { + this.id = id; + this.session = session; + this.hexColor = SnakeUtils.getRandomHexColor(); + resetState(); + } + + private void resetState() { + this.direction = Direction.NONE; + this.head = SnakeUtils.getRandomLocation(); + this.tail.clear(); + this.length = DEFAULT_LENGTH; + } + + private void kill() throws Exception { + synchronized (this.monitor) { + resetState(); + sendMessage("{'type': 'dead'}"); + } + } + + private void reward() throws Exception { + synchronized (this.monitor) { + this.length++; + sendMessage("{'type': 'kill'}"); + } + } + + protected void sendMessage(String msg) throws Exception { + this.session.sendMessage(new TextMessage(msg)); + } + + public void update(Collection snakes) throws Exception { + synchronized (this.monitor) { + Location nextLocation = this.head.getAdjacentLocation(this.direction); + if (nextLocation.x >= SnakeUtils.PLAYFIELD_WIDTH) { + nextLocation.x = 0; + } + if (nextLocation.y >= SnakeUtils.PLAYFIELD_HEIGHT) { + nextLocation.y = 0; + } + if (nextLocation.x < 0) { + nextLocation.x = SnakeUtils.PLAYFIELD_WIDTH; + } + if (nextLocation.y < 0) { + nextLocation.y = SnakeUtils.PLAYFIELD_HEIGHT; + } + if (this.direction != Direction.NONE) { + this.tail.addFirst(this.head); + if (this.tail.size() > this.length) { + this.tail.removeLast(); + } + this.head = nextLocation; + } + + handleCollisions(snakes); + } + } + + private void handleCollisions(Collection snakes) throws Exception { + for (Snake snake : snakes) { + boolean headCollision = this.id != snake.id && snake.getHead().equals(this.head); + boolean tailCollision = snake.getTail().contains(this.head); + if (headCollision || tailCollision) { + kill(); + if (this.id != snake.id) { + snake.reward(); + } + } + } + } + + public Location getHead() { + synchronized (this.monitor) { + return this.head; + } + } + + public Collection getTail() { + synchronized (this.monitor) { + return this.tail; + } + } + + public void setDirection(Direction direction) { + synchronized (this.monitor) { + this.direction = direction; + } + } + + public String getLocationsJson() { + synchronized (this.monitor) { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("{x: %d, y: %d}", Integer.valueOf(this.head.x), Integer.valueOf(this.head.y))); + for (Location location : this.tail) { + sb.append(','); + sb.append(String.format("{x: %d, y: %d}", Integer.valueOf(location.x), Integer.valueOf(location.y))); + } + return String.format("{'id':%d,'body':[%s]}", Integer.valueOf(this.id), sb.toString()); + } + } + + public int getId() { + return this.id; + } + + public String getHexColor() { + return this.hexColor; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/snake/SnakeTimer.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/snake/SnakeTimer.java new file mode 100644 index 0000000000..c140a4703b --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/snake/SnakeTimer.java @@ -0,0 +1,117 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.websocket.jetty10.snake; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Sets up the timer for the multi-player snake game WebSocket example. + */ +public final class SnakeTimer { + + private static final long TICK_DELAY = 100; + + private static final Object MONITOR = new Object(); + + private static final Log logger = LogFactory.getLog(SnakeTimer.class); + + private static final ConcurrentHashMap snakes = new ConcurrentHashMap<>(); + + private static Timer gameTimer = null; + + private SnakeTimer() { + } + + public static void addSnake(Snake snake) { + synchronized (MONITOR) { + if (snakes.isEmpty()) { + startTimer(); + } + snakes.put(Integer.valueOf(snake.getId()), snake); + } + } + + public static Collection getSnakes() { + return Collections.unmodifiableCollection(snakes.values()); + } + + public static void removeSnake(Snake snake) { + synchronized (MONITOR) { + snakes.remove(Integer.valueOf(snake.getId())); + if (snakes.isEmpty()) { + stopTimer(); + } + } + } + + public static void tick() throws Exception { + StringBuilder sb = new StringBuilder(); + for (Iterator iterator = SnakeTimer.getSnakes().iterator(); iterator.hasNext();) { + Snake snake = iterator.next(); + snake.update(SnakeTimer.getSnakes()); + sb.append(snake.getLocationsJson()); + if (iterator.hasNext()) { + sb.append(','); + } + } + broadcast(String.format("{'type': 'update', 'data' : [%s]}", sb.toString())); + } + + public static void broadcast(String message) throws Exception { + Collection snakes = new CopyOnWriteArrayList<>(SnakeTimer.getSnakes()); + for (Snake snake : snakes) { + try { + snake.sendMessage(message); + } + catch (Throwable ex) { + // if Snake#sendMessage fails the client is removed + removeSnake(snake); + } + } + } + + public static void startTimer() { + gameTimer = new Timer(SnakeTimer.class.getSimpleName() + " Timer"); + gameTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + try { + tick(); + } + catch (Throwable ex) { + logger.error("Caught to prevent timer from shutting down", ex); + } + } + }, TICK_DELAY, TICK_DELAY); + } + + public static void stopTimer() { + if (gameTimer != null) { + gameTimer.cancel(); + } + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/snake/SnakeUtils.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/snake/SnakeUtils.java new file mode 100644 index 0000000000..081e1a6d75 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/snake/SnakeUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.websocket.jetty10.snake; + +import java.awt.Color; +import java.util.Random; + +public final class SnakeUtils { + + /** + * The width of the playfield. + */ + public static final int PLAYFIELD_WIDTH = 640; + + /** + * The height of the playfield. + */ + public static final int PLAYFIELD_HEIGHT = 480; + + /** + * The grid size. + */ + public static final int GRID_SIZE = 10; + + private static final Random random = new Random(); + + private SnakeUtils() { + } + + public static String getRandomHexColor() { + float hue = random.nextFloat(); + // sat between 0.1 and 0.3 + float saturation = (random.nextInt(2000) + 1000) / 10000f; + float luminance = 0.9f; + Color color = Color.getHSBColor(hue, saturation, luminance); + return '#' + Integer.toHexString((color.getRGB() & 0xffffff) | 0x1000000).substring(1); + } + + public static Location getRandomLocation() { + int x = roundByGridSize(random.nextInt(PLAYFIELD_WIDTH)); + int y = roundByGridSize(random.nextInt(PLAYFIELD_HEIGHT)); + return new Location(x, y); + } + + private static int roundByGridSize(int value) { + value = value + (GRID_SIZE / 2); + value = value / GRID_SIZE; + value = value * GRID_SIZE; + return value; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/snake/SnakeWebSocketHandler.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/snake/SnakeWebSocketHandler.java new file mode 100644 index 0000000000..b7d4476550 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/java/smoketest/websocket/jetty/snake/SnakeWebSocketHandler.java @@ -0,0 +1,103 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.websocket.jetty10.snake; + +import java.awt.Color; +import java.util.Iterator; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +public class SnakeWebSocketHandler extends TextWebSocketHandler { + + private static final AtomicInteger snakeIds = new AtomicInteger(); + + private static final Random random = new Random(); + + private final int id; + + private Snake snake; + + public static String getRandomHexColor() { + float hue = random.nextFloat(); + // sat between 0.1 and 0.3 + float saturation = (random.nextInt(2000) + 1000) / 10000f; + float luminance = 0.9f; + Color color = Color.getHSBColor(hue, saturation, luminance); + return '#' + Integer.toHexString((color.getRGB() & 0xffffff) | 0x1000000).substring(1); + } + + public static Location getRandomLocation() { + int x = roundByGridSize(random.nextInt(SnakeUtils.PLAYFIELD_WIDTH)); + int y = roundByGridSize(random.nextInt(SnakeUtils.PLAYFIELD_HEIGHT)); + return new Location(x, y); + } + + private static int roundByGridSize(int value) { + value = value + (SnakeUtils.GRID_SIZE / 2); + value = value / SnakeUtils.GRID_SIZE; + value = value * SnakeUtils.GRID_SIZE; + return value; + } + + public SnakeWebSocketHandler() { + this.id = snakeIds.getAndIncrement(); + } + + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + this.snake = new Snake(this.id, session); + SnakeTimer.addSnake(this.snake); + StringBuilder sb = new StringBuilder(); + for (Iterator iterator = SnakeTimer.getSnakes().iterator(); iterator.hasNext();) { + Snake snake = iterator.next(); + sb.append(String.format("{id: %d, color: '%s'}", Integer.valueOf(snake.getId()), snake.getHexColor())); + if (iterator.hasNext()) { + sb.append(','); + } + } + SnakeTimer.broadcast(String.format("{'type': 'join','data':[%s]}", sb.toString())); + } + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + String payload = message.getPayload(); + if ("west".equals(payload)) { + this.snake.setDirection(Direction.WEST); + } + else if ("north".equals(payload)) { + this.snake.setDirection(Direction.NORTH); + } + else if ("east".equals(payload)) { + this.snake.setDirection(Direction.EAST); + } + else if ("south".equals(payload)) { + this.snake.setDirection(Direction.SOUTH); + } + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + SnakeTimer.removeSnake(this.snake); + SnakeTimer.broadcast(String.format("{'type': 'leave', 'id': %d}", Integer.valueOf(this.id))); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/resources/static/echo.html b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/resources/static/echo.html new file mode 100644 index 0000000000..54d33f55bd --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/resources/static/echo.html @@ -0,0 +1,134 @@ + + + + + + Apache Tomcat WebSocket Examples: Echo + + + + + + +
+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+
+
+
+
+ + diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/resources/static/index.html b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/resources/static/index.html new file mode 100644 index 0000000000..6bab9d6237 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/resources/static/index.html @@ -0,0 +1,33 @@ + + + + + + Apache Tomcat WebSocket Examples: Index + + + +

Please select the sample you would like to try.

+ + + diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/resources/static/reverse.html b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/resources/static/reverse.html new file mode 100644 index 0000000000..60d7ee4978 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/resources/static/reverse.html @@ -0,0 +1,141 @@ + + + + + + WebSocket Examples: Reverse + + + + + +
+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+
+
+
+
+ + diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/resources/static/snake.html b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/resources/static/snake.html new file mode 100644 index 0000000000..fe0a2ea88e --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/main/resources/static/snake.html @@ -0,0 +1,250 @@ + + + + + + + Apache Tomcat WebSocket Examples: Multiplayer Snake + + + + + + +
+ +
+
+
+
+ + + diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/test/java/smoketest/websocket/jetty/SampleJetty10WebSocketsApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/test/java/smoketest/websocket/jetty/SampleJetty10WebSocketsApplicationTests.java new file mode 100644 index 0000000000..da3836be7a --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/test/java/smoketest/websocket/jetty/SampleJetty10WebSocketsApplicationTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.websocket.jetty10; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; +import smoketest.websocket.jetty10.client.GreetingService; +import smoketest.websocket.jetty10.client.SimpleClientWebSocketHandler; +import smoketest.websocket.jetty10.client.SimpleGreetingService; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.client.WebSocketConnectionManager; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; + +import static org.assertj.core.api.Assertions.assertThat; + +@EnabledForJreRange(min = JRE.JAVA_11) +@SpringBootTest(classes = SampleJetty10WebSocketsApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT, + properties = "debug: true") +class SampleJetty10WebSocketsApplicationTests { + + private static Log logger = LogFactory.getLog(SampleJetty10WebSocketsApplicationTests.class); + + @LocalServerPort + private int port = 1234; + + @Test + void echoEndpoint() { + ConfigurableApplicationContext context = new SpringApplicationBuilder(ClientConfiguration.class, + PropertyPlaceholderAutoConfiguration.class) + .properties("websocket.uri:ws://localhost:" + this.port + "/echo/websocket") + .run("--spring.main.web-application-type=none"); + long count = context.getBean(ClientConfiguration.class).latch.getCount(); + AtomicReference messagePayloadReference = context.getBean(ClientConfiguration.class).messagePayload; + context.close(); + assertThat(count).isEqualTo(0); + assertThat(messagePayloadReference.get()).isEqualTo("Did you say \"Hello world!\"?"); + } + + @Test + void reverseEndpoint() { + ConfigurableApplicationContext context = new SpringApplicationBuilder(ClientConfiguration.class, + PropertyPlaceholderAutoConfiguration.class) + .properties("websocket.uri:ws://localhost:" + this.port + "/reverse") + .run("--spring.main.web-application-type=none"); + long count = context.getBean(ClientConfiguration.class).latch.getCount(); + AtomicReference messagePayloadReference = context.getBean(ClientConfiguration.class).messagePayload; + context.close(); + assertThat(count).isEqualTo(0); + assertThat(messagePayloadReference.get()).isEqualTo("Reversed: !dlrow olleH"); + } + + @Configuration(proxyBeanMethods = false) + static class ClientConfiguration implements CommandLineRunner { + + @Value("${websocket.uri}") + private String webSocketUri; + + private final CountDownLatch latch = new CountDownLatch(1); + + private final AtomicReference messagePayload = new AtomicReference<>(); + + @Override + public void run(String... args) throws Exception { + logger.info("Waiting for response: latch=" + this.latch.getCount()); + if (this.latch.await(10, TimeUnit.SECONDS)) { + logger.info("Got response: " + this.messagePayload.get()); + } + else { + logger.info("Response not received: latch=" + this.latch.getCount()); + } + } + + @Bean + WebSocketConnectionManager wsConnectionManager() { + WebSocketConnectionManager manager = new WebSocketConnectionManager(client(), handler(), this.webSocketUri); + manager.setAutoStartup(true); + return manager; + } + + @Bean + StandardWebSocketClient client() { + return new StandardWebSocketClient(); + } + + @Bean + SimpleClientWebSocketHandler handler() { + return new SimpleClientWebSocketHandler(greetingService(), this.latch, this.messagePayload); + } + + @Bean + GreetingService greetingService() { + return new SimpleGreetingService(); + } + + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/test/java/smoketest/websocket/jetty/echo/CustomContainerWebSocketsApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/test/java/smoketest/websocket/jetty/echo/CustomContainerWebSocketsApplicationTests.java new file mode 100644 index 0000000000..7f581051c9 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/test/java/smoketest/websocket/jetty/echo/CustomContainerWebSocketsApplicationTests.java @@ -0,0 +1,142 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.websocket.jetty10.echo; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; +import smoketest.websocket.jetty10.SampleJetty10WebSocketsApplication; +import smoketest.websocket.jetty10.client.GreetingService; +import smoketest.websocket.jetty10.client.SimpleClientWebSocketHandler; +import smoketest.websocket.jetty10.client.SimpleGreetingService; +import smoketest.websocket.jetty10.echo.CustomContainerWebSocketsApplicationTests.CustomContainerConfiguration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.boot.web.servlet.server.ServletWebServerFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.client.WebSocketConnectionManager; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; + +import static org.assertj.core.api.Assertions.assertThat; + +@EnabledForJreRange(min = JRE.JAVA_11) +@SpringBootTest(classes = { SampleJetty10WebSocketsApplication.class, CustomContainerConfiguration.class }, + webEnvironment = WebEnvironment.RANDOM_PORT) +class CustomContainerWebSocketsApplicationTests { + + private static Log logger = LogFactory.getLog(CustomContainerWebSocketsApplicationTests.class); + + @LocalServerPort + private int port; + + @Test + void echoEndpoint() { + ConfigurableApplicationContext context = new SpringApplicationBuilder(ClientConfiguration.class, + PropertyPlaceholderAutoConfiguration.class) + .properties("websocket.uri:ws://localhost:" + this.port + "/ws/echo/websocket") + .run("--spring.main.web-application-type=none"); + long count = context.getBean(ClientConfiguration.class).latch.getCount(); + AtomicReference messagePayloadReference = context.getBean(ClientConfiguration.class).messagePayload; + context.close(); + assertThat(count).isEqualTo(0); + assertThat(messagePayloadReference.get()).isEqualTo("Did you say \"Hello world!\"?"); + } + + @Test + void reverseEndpoint() { + ConfigurableApplicationContext context = new SpringApplicationBuilder(ClientConfiguration.class, + PropertyPlaceholderAutoConfiguration.class) + .properties("websocket.uri:ws://localhost:" + this.port + "/ws/reverse") + .run("--spring.main.web-application-type=none"); + long count = context.getBean(ClientConfiguration.class).latch.getCount(); + AtomicReference messagePayloadReference = context.getBean(ClientConfiguration.class).messagePayload; + context.close(); + assertThat(count).isEqualTo(0); + assertThat(messagePayloadReference.get()).isEqualTo("Reversed: !dlrow olleH"); + } + + @Configuration(proxyBeanMethods = false) + protected static class CustomContainerConfiguration { + + @Bean + public ServletWebServerFactory webServerFactory() { + return new JettyServletWebServerFactory("/ws", 0); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ClientConfiguration implements CommandLineRunner { + + @Value("${websocket.uri}") + private String webSocketUri; + + private final CountDownLatch latch = new CountDownLatch(1); + + private final AtomicReference messagePayload = new AtomicReference<>(); + + @Override + public void run(String... args) throws Exception { + logger.info("Waiting for response: latch=" + this.latch.getCount()); + if (this.latch.await(10, TimeUnit.SECONDS)) { + logger.info("Got response: " + this.messagePayload.get()); + } + else { + logger.info("Response not received: latch=" + this.latch.getCount()); + } + } + + @Bean + WebSocketConnectionManager wsConnectionManager() { + WebSocketConnectionManager manager = new WebSocketConnectionManager(client(), handler(), this.webSocketUri); + manager.setAutoStartup(true); + return manager; + } + + @Bean + StandardWebSocketClient client() { + return new StandardWebSocketClient(); + } + + @Bean + SimpleClientWebSocketHandler handler() { + return new SimpleClientWebSocketHandler(greetingService(), this.latch, this.messagePayload); + } + + @Bean + GreetingService greetingService() { + return new SimpleGreetingService(); + } + + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/test/java/smoketest/websocket/jetty/snake/SnakeTimerTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/test/java/smoketest/websocket/jetty/snake/SnakeTimerTests.java new file mode 100644 index 0000000000..c45ef584a2 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty10/src/test/java/smoketest/websocket/jetty/snake/SnakeTimerTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 smoketest.websocket.jetty10.snake; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; + +class SnakeTimerTests { + + @Test + void removeDysfunctionalSnakes() throws Exception { + Snake snake = mock(Snake.class); + willThrow(new IOException()).given(snake).sendMessage(anyString()); + SnakeTimer.addSnake(snake); + + SnakeTimer.broadcast(""); + assertThat(SnakeTimer.getSnakes()).hasSize(0); + } + +}