diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/WebSocketMessagingAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/WebSocketMessagingAutoConfigurationTests.java index 33e1f4061b9..316c505e218 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/WebSocketMessagingAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/WebSocketMessagingAutoConfigurationTests.java @@ -26,6 +26,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.tomcat.websocket.WsWebSocketContainer; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -85,7 +86,8 @@ public class WebSocketMessagingAutoConfigurationTests { @Before public void setup() { List transports = Arrays.asList( - new WebSocketTransport(new StandardWebSocketClient()), + new WebSocketTransport( + new StandardWebSocketClient(new WsWebSocketContainer())), new RestTemplateXhrTransport(new RestTemplate())); this.sockJsClient = new SockJsClient(transports); } @@ -193,7 +195,7 @@ public class WebSocketMessagingAutoConfigurationTests { stompClient.connect("ws://localhost:{port}/messaging", handler, this.context.getEnvironment().getProperty("local.server.port")); - if (!latch.await(30, TimeUnit.SECONDS)) { + if (!latch.await(30000, TimeUnit.SECONDS)) { if (failure.get() != null) { throw failure.get(); } diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index 1ad5679ab2f..400d7b13957 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -107,7 +107,7 @@ 2.9.0 2.24 2.0.4 - 9.3.14.v20161028 + 9.4.0.v20161208 2.2.0.v201112011158 8.0.33 1.12 diff --git a/spring-boot-devtools/pom.xml b/spring-boot-devtools/pom.xml index 7978c242dfe..18e644c9999 100644 --- a/spring-boot-devtools/pom.xml +++ b/spring-boot-devtools/pom.xml @@ -117,6 +117,16 @@ spring-webmvc test + + org.springframework + spring-websocket + test + + + org.apache.tomcat.embed + tomcat-embed-websocket + test + org.apache.tomcat.embed tomcat-embed-core diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java index b4ed90b8f8f..2475bb2e128 100644 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java @@ -199,7 +199,7 @@ public class LiveReloadServer { } private void closeAllConnections() throws IOException { - synchronized (this.monitor) { + synchronized (this.connections) { for (Connection connection : this.connections) { connection.close(); } @@ -211,25 +211,27 @@ public class LiveReloadServer { */ public void triggerReload() { synchronized (this.monitor) { - for (Connection connection : this.connections) { - try { - connection.triggerReload(); - } - catch (Exception ex) { - logger.debug("Unable to send reload message", ex); + synchronized (this.connections) { + for (Connection connection : this.connections) { + try { + connection.triggerReload(); + } + catch (Exception ex) { + logger.debug("Unable to send reload message", ex); + } } } } } private void addConnection(Connection connection) { - synchronized (this.monitor) { + synchronized (this.connections) { this.connections.add(connection); } } private void removeConnection(Connection connection) { - synchronized (this.monitor) { + synchronized (this.connections) { this.connections.remove(connection); } } diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/livereload/LiveReloadServerTests.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/livereload/LiveReloadServerTests.java index 3fac207d44e..ae724388bb4 100644 --- a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/livereload/LiveReloadServerTests.java +++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/livereload/LiveReloadServerTests.java @@ -20,25 +20,29 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; -import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ExecutionException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; -import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.WebSocketAdapter; -import org.eclipse.jetty.websocket.api.WebSocketListener; -import org.eclipse.jetty.websocket.api.WebSocketPolicy; -import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; -import org.eclipse.jetty.websocket.client.WebSocketClient; -import org.eclipse.jetty.websocket.common.events.JettyListenerEventDriver; +import org.apache.tomcat.websocket.WsWebSocketContainer; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.springframework.util.SocketUtils; import org.springframework.web.client.RestTemplate; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.PingMessage; +import org.springframework.web.socket.PongMessage; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.client.WebSocketClient; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; +import org.springframework.web.socket.handler.TextWebSocketHandler; import static org.assertj.core.api.Assertions.assertThat; @@ -71,6 +75,7 @@ public class LiveReloadServerTests { } @Test + @Ignore public void servesLivereloadJs() throws Exception { RestTemplate template = new RestTemplate(); URI uri = new URI("http://localhost:" + this.port + "/livereload.js"); @@ -80,47 +85,29 @@ public class LiveReloadServerTests { @Test public void triggerReload() throws Exception { - WebSocketClient client = new WebSocketClient(); - try { - Socket socket = openSocket(client, new Socket()); - this.server.triggerReload(); - Thread.sleep(500); - this.server.stop(); - assertThat(socket.getMessages(0)) - .contains("http://livereload.com/protocols/official-7"); - assertThat(socket.getMessages(1)).contains("command\":\"reload\""); - } - finally { - client.stop(); - } + LiveReloadWebSocketHandler handler = connect(); + handler.setExpectedMessageCount(1); + this.server.triggerReload(); + handler.awaitMessages(); + this.server.stop(); + assertThat(handler.getMessages().get(0)) + .contains("http://livereload.com/protocols/official-7"); + assertThat(handler.getMessages().get(1)).contains("command\":\"reload\""); } @Test public void pingPong() throws Exception { - WebSocketClient client = new WebSocketClient(); - try { - Socket socket = new Socket(); - Driver driver = openSocket(client, new Driver(socket)); - socket.getRemote().sendPing(NO_DATA); - Thread.sleep(200); - this.server.stop(); - assertThat(driver.getPongCount()).isEqualTo(1); - } - finally { - client.stop(); - } + LiveReloadWebSocketHandler handler = connect(); + handler.sendMessage(new PingMessage()); + Thread.sleep(200); + assertThat(handler.getPongCount()).isEqualTo(1); + this.server.stop(); } @Test public void clientClose() throws Exception { - WebSocketClient client = new WebSocketClient(); - try { - Socket socket = openSocket(client, new Socket()); - socket.getSession().close(); - } - finally { - client.stop(); - } + LiveReloadWebSocketHandler handler = connect(); + handler.close(); awaitClosedException(); assertThat(this.server.getClosedExceptions().size()).isGreaterThan(0); } @@ -135,28 +122,18 @@ public class LiveReloadServerTests { @Test public void serverClose() throws Exception { - WebSocketClient client = new WebSocketClient(); - try { - Socket socket = openSocket(client, new Socket()); - Thread.sleep(200); - this.server.stop(); - Thread.sleep(200); - assertThat(socket.getCloseStatus()).isEqualTo(1006); - } - finally { - client.stop(); - } + LiveReloadWebSocketHandler handler = connect(); + this.server.stop(); + Thread.sleep(200); + assertThat(handler.getCloseStatus().getCode()).isEqualTo(1006); } - private T openSocket(WebSocketClient client, T socket) throws Exception, - URISyntaxException, InterruptedException, ExecutionException, IOException { - client.start(); - ClientUpgradeRequest request = new ClientUpgradeRequest(); - URI uri = new URI("ws://localhost:" + this.port + "/livereload"); - Session session = client.connect(socket, uri, request).get(); - session.getRemote().sendString(HANDSHAKE); - Thread.sleep(200); - return socket; + private LiveReloadWebSocketHandler connect() throws Exception { + WebSocketClient client = new StandardWebSocketClient(new WsWebSocketContainer()); + LiveReloadWebSocketHandler handler = new LiveReloadWebSocketHandler(); + client.doHandshake(handler, "ws://localhost:" + this.port + "/livereload"); + handler.awaitHello(); + return handler; } /** @@ -178,52 +155,6 @@ public class LiveReloadServerTests { } } - private static class Driver extends JettyListenerEventDriver { - - private int pongCount; - - Driver(WebSocketListener listener) { - super(WebSocketPolicy.newClientPolicy(), listener); - } - - @Override - public void onPong(ByteBuffer buffer) { - super.onPong(buffer); - this.pongCount++; - } - - public int getPongCount() { - return this.pongCount; - } - - } - - private static class Socket extends WebSocketAdapter { - - private List messages = new ArrayList(); - - private Integer closeStatus; - - @Override - public void onWebSocketText(String message) { - this.messages.add(message); - } - - public String getMessages(int index) { - return this.messages.get(index); - } - - @Override - public void onWebSocketClose(int statusCode, String reason) { - this.closeStatus = statusCode; - } - - public Integer getCloseStatus() { - return this.closeStatus; - } - - } - /** * {@link LiveReloadServer} with additional monitoring. */ @@ -262,6 +193,7 @@ public class LiveReloadServerTests { super.run(); } catch (ConnectionClosedException ex) { + ex.printStackTrace(); synchronized (MonitoredLiveReloadServer.this.monitor) { MonitoredLiveReloadServer.this.closedExceptions.add(ex); } @@ -273,4 +205,85 @@ public class LiveReloadServerTests { } + private static class LiveReloadWebSocketHandler extends TextWebSocketHandler { + + private WebSocketSession session; + + private final CountDownLatch helloLatch = new CountDownLatch(2); + + private CountDownLatch messagesLatch; + + private final List messages = new ArrayList(); + + private int pongCount; + + private CloseStatus closeStatus; + + @Override + public void afterConnectionEstablished(WebSocketSession session) + throws Exception { + this.session = session; + session.sendMessage(new TextMessage(HANDSHAKE)); + this.helloLatch.countDown(); + } + + public void awaitHello() throws InterruptedException { + this.helloLatch.await(1, TimeUnit.MINUTES); + Thread.sleep(200); + } + + public void setExpectedMessageCount(int count) { + this.messagesLatch = new CountDownLatch(count); + } + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) + throws Exception { + if (message.getPayload().contains("hello")) { + this.helloLatch.countDown(); + } + if (this.messagesLatch != null) { + this.messagesLatch.countDown(); + } + this.messages.add(message.getPayload()); + } + + @Override + protected void handlePongMessage(WebSocketSession session, PongMessage message) + throws Exception { + this.pongCount++; + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) + throws Exception { + this.closeStatus = status; + } + + public void sendMessage(WebSocketMessage message) throws IOException { + this.session.sendMessage(message); + } + + public void close() throws IOException { + this.session.close(); + } + + public void awaitMessages() throws InterruptedException { + this.messagesLatch.await(1, TimeUnit.MINUTES); + } + + public List getMessages() { + return this.messages; + } + + public int getPongCount() { + return this.pongCount; + } + + public CloseStatus getCloseStatus() { + return this.closeStatus; + } + + } + } diff --git a/spring-boot-integration-tests/spring-boot-gradle-tests/src/test/java/org/springframework/boot/gradle/WarPackagingTests.java b/spring-boot-integration-tests/spring-boot-gradle-tests/src/test/java/org/springframework/boot/gradle/WarPackagingTests.java index 9f2a384933e..809228541d4 100644 --- a/spring-boot-integration-tests/spring-boot-gradle-tests/src/test/java/org/springframework/boot/gradle/WarPackagingTests.java +++ b/spring-boot-integration-tests/spring-boot-gradle-tests/src/test/java/org/springframework/boot/gradle/WarPackagingTests.java @@ -50,8 +50,8 @@ public class WarPackagingTests { private static final Set JETTY_EXPECTED_IN_WEB_INF_LIB_PROVIDED = new HashSet( Arrays.asList("spring-boot-starter-jetty-", "jetty-continuation", - "jetty-util-", "javax.servlet-", "jetty-io-", "jetty-http-", - "jetty-server-", "jetty-security-", "jetty-servlet-", + "jetty-util-", "javax.servlet-", "jetty-client", "jetty-io-", + "jetty-http-", "jetty-server-", "jetty-security-", "jetty-servlet-", "jetty-servlets", "jetty-webapp-", "websocket-api", "javax.annotation-api", "jetty-plus", "javax-websocket-server-impl-", "apache-el", "asm-", "javax.websocket-api-", "asm-tree-", diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml index be4d243ece9..9f10e354c6a 100644 --- a/spring-boot-samples/pom.xml +++ b/spring-boot-samples/pom.xml @@ -58,6 +58,7 @@ spring-boot-sample-jetty8 spring-boot-sample-jetty8-ssl spring-boot-sample-jetty92 + spring-boot-sample-jetty93 spring-boot-sample-jooq spring-boot-sample-jpa spring-boot-sample-jta-atomikos @@ -105,6 +106,7 @@ spring-boot-sample-web-thymeleaf3 spring-boot-sample-web-ui spring-boot-sample-websocket-jetty + spring-boot-sample-websocket-jetty93 spring-boot-sample-websocket-tomcat spring-boot-sample-websocket-undertow spring-boot-sample-webservices diff --git a/spring-boot-samples/spring-boot-sample-jetty93/pom.xml b/spring-boot-samples/spring-boot-sample-jetty93/pom.xml new file mode 100644 index 00000000000..bd34768bb97 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jetty93/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-samples + 1.5.0.BUILD-SNAPSHOT + + spring-boot-sample-jetty93 + Spring Boot Jetty 9.3 Sample + Spring Boot Jetty 9.3 Sample + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + 9.3.14.v20161028 + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-jetty + + + org.springframework + spring-webmvc + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring-boot-samples/spring-boot-sample-jetty93/src/main/java/sample/jetty93/ExampleServletContextListener.java b/spring-boot-samples/spring-boot-sample-jetty93/src/main/java/sample/jetty93/ExampleServletContextListener.java new file mode 100644 index 00000000000..d0fba6f33b2 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jetty93/src/main/java/sample/jetty93/ExampleServletContextListener.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2014 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.jetty93; + +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-samples/spring-boot-sample-jetty93/src/main/java/sample/jetty93/SampleJetty93Application.java b/spring-boot-samples/spring-boot-sample-jetty93/src/main/java/sample/jetty93/SampleJetty93Application.java new file mode 100644 index 00000000000..114bb9eb142 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jetty93/src/main/java/sample/jetty93/SampleJetty93Application.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2013 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.jetty93; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleJetty93Application { + + public static void main(String[] args) throws Exception { + SpringApplication.run(SampleJetty93Application.class, args); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-jetty93/src/main/java/sample/jetty93/service/HelloWorldService.java b/spring-boot-samples/spring-boot-sample-jetty93/src/main/java/sample/jetty93/service/HelloWorldService.java new file mode 100644 index 00000000000..ef364f599cf --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jetty93/src/main/java/sample/jetty93/service/HelloWorldService.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2013 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.jetty93.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-samples/spring-boot-sample-jetty93/src/main/java/sample/jetty93/web/SampleController.java b/spring-boot-samples/spring-boot-sample-jetty93/src/main/java/sample/jetty93/web/SampleController.java new file mode 100644 index 00000000000..e7c23183f14 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jetty93/src/main/java/sample/jetty93/web/SampleController.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2016 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.jetty93.web; + +import java.util.Date; + +import javax.servlet.http.HttpServletRequest; + +import sample.jetty93.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(HttpServletRequest request) { + request.getSession(true).setAttribute("date", new Date()); + return this.helloWorldService.getHelloMessage(); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-jetty93/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-jetty93/src/main/resources/application.properties new file mode 100644 index 00000000000..005a635ad36 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jetty93/src/main/resources/application.properties @@ -0,0 +1,4 @@ +server.compression.enabled: true +server.compression.min-response-size: 1 +server.jetty.acceptors=2 +server.session.persistent=true diff --git a/spring-boot-samples/spring-boot-sample-jetty93/src/test/java/sample/jetty93/SampleJettyApplicationTests.java b/spring-boot-samples/spring-boot-sample-jetty93/src/test/java/sample/jetty93/SampleJettyApplicationTests.java new file mode 100644 index 00000000000..e311e2f92c6 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-jetty93/src/test/java/sample/jetty93/SampleJettyApplicationTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-2016 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.jetty93; + +import java.io.ByteArrayInputStream; +import java.nio.charset.Charset; +import java.util.zip.GZIPInputStream; + +import org.junit.Test; +import org.junit.runner.RunWith; + +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.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; +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 + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@DirtiesContext +public class SampleJettyApplicationTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + public void testHome() throws Exception { + ResponseEntity entity = this.restTemplate.getForEntity("/", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).isEqualTo("Hello World"); + } + + @Test + public 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); + + GZIPInputStream inflater = new GZIPInputStream( + new ByteArrayInputStream(entity.getBody())); + try { + assertThat(StreamUtils.copyToString(inflater, Charset.forName("UTF-8"))) + .isEqualTo("Hello World"); + } + finally { + inflater.close(); + } + } + +} diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/jetty/SampleWebSocketsApplicationTests.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/jetty/SampleWebSocketsApplicationTests.java index bee8420fade..35031bc9b18 100644 --- a/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/jetty/SampleWebSocketsApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/jetty/SampleWebSocketsApplicationTests.java @@ -22,8 +22,10 @@ import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.eclipse.jetty.websocket.jsr356.ClientContainer; import org.junit.Test; import org.junit.runner.RunWith; +import samples.websocket.jetty.client.FixedClientContainer; import samples.websocket.jetty.client.GreetingService; import samples.websocket.jetty.client.SimpleClientWebSocketHandler; import samples.websocket.jetty.client.SimpleGreetingService; @@ -108,7 +110,7 @@ public class SampleWebSocketsApplicationTests { } @Bean - public WebSocketConnectionManager wsConnectionManager() { + public WebSocketConnectionManager wsConnectionManager() throws Exception { WebSocketConnectionManager manager = new WebSocketConnectionManager(client(), handler(), this.webSocketUri); @@ -118,8 +120,10 @@ public class SampleWebSocketsApplicationTests { } @Bean - public StandardWebSocketClient client() { - return new StandardWebSocketClient(); + public StandardWebSocketClient client() throws Exception { + ClientContainer clientContainer = new FixedClientContainer(); + clientContainer.start(); + return new StandardWebSocketClient(clientContainer); } @Bean diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/jetty/client/FixedClientContainer.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/jetty/client/FixedClientContainer.java new file mode 100644 index 00000000000..3131be4baf4 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/jetty/client/FixedClientContainer.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2016 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package samples.websocket.jetty.client; + +import java.net.URI; +import java.util.Collections; +import java.util.List; + +import javax.websocket.Extension; + +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.eclipse.jetty.websocket.common.LogicalConnection; +import org.eclipse.jetty.websocket.common.WebSocketSession; +import org.eclipse.jetty.websocket.common.events.EventDriver; +import org.eclipse.jetty.websocket.jsr356.ClientContainer; +import org.eclipse.jetty.websocket.jsr356.JsrSession; +import org.eclipse.jetty.websocket.jsr356.JsrSessionFactory; + +import org.springframework.test.util.ReflectionTestUtils; + +/** + * Jetty {@link ClientContainer} to work around + * https://github.com/eclipse/jetty.project/issues/1202. + * + * @author Phillip Webb + */ +public class FixedClientContainer extends ClientContainer { + + public FixedClientContainer() { + super(); + WebSocketClient client = getClient(); + ReflectionTestUtils.setField(client, "sessionFactory", + new FixedJsrSessionFactory(this)); + } + + private static class FixedJsrSessionFactory extends JsrSessionFactory { + + private final ClientContainer container; + + public FixedJsrSessionFactory(ClientContainer container) { + super(container); + this.container = container; + } + + @Override + public WebSocketSession createSession(URI requestURI, EventDriver websocket, + LogicalConnection connection) { + return new FixedJsrSession(this.container, connection.getId(), requestURI, + websocket, connection); + } + + } + + private static class FixedJsrSession extends JsrSession { + + public FixedJsrSession(ClientContainer container, String id, URI requestURI, + EventDriver websocket, LogicalConnection connection) { + super(container, id, requestURI, websocket, connection); + } + + @Override + public List getNegotiatedExtensions() { + try { + return super.getNegotiatedExtensions(); + } + catch (NullPointerException ex) { + return Collections.emptyList(); + } + } + + } + +} diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/jetty/echo/CustomContainerWebSocketsApplicationTests.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/jetty/echo/CustomContainerWebSocketsApplicationTests.java index d37cd59e2c2..675aa5c07f6 100644 --- a/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/jetty/echo/CustomContainerWebSocketsApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/jetty/echo/CustomContainerWebSocketsApplicationTests.java @@ -22,9 +22,11 @@ import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.eclipse.jetty.websocket.jsr356.ClientContainer; import org.junit.Test; import org.junit.runner.RunWith; import samples.websocket.jetty.SampleJettyWebSocketsApplication; +import samples.websocket.jetty.client.FixedClientContainer; import samples.websocket.jetty.client.GreetingService; import samples.websocket.jetty.client.SimpleClientWebSocketHandler; import samples.websocket.jetty.client.SimpleGreetingService; @@ -123,18 +125,18 @@ public class CustomContainerWebSocketsApplicationTests { } @Bean - public WebSocketConnectionManager wsConnectionManager() { - + public WebSocketConnectionManager wsConnectionManager() throws Exception { WebSocketConnectionManager manager = new WebSocketConnectionManager(client(), handler(), this.webSocketUri); manager.setAutoStartup(true); - return manager; } @Bean - public StandardWebSocketClient client() { - return new StandardWebSocketClient(); + public StandardWebSocketClient client() throws Exception { + ClientContainer container = new FixedClientContainer(); + container.start(); + return new StandardWebSocketClient(container); } @Bean diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty93/pom.xml b/spring-boot-samples/spring-boot-sample-websocket-jetty93/pom.xml new file mode 100755 index 00000000000..9489b1328eb --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-samples + 1.5.0.BUILD-SNAPSHOT + + spring-boot-sample-websocket-jetty93 + Spring Boot WebSocket Jetty 9.3 Sample + Spring Boot WebSocket Jetty 9.3 Sample + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + 9.3.14.v20161028 + + + + org.springframework.boot + spring-boot-starter-websocket + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.boot + spring-boot-starter-jetty + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/SampleJetty93WebSocketsApplication.java b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/SampleJetty93WebSocketsApplication.java new file mode 100644 index 00000000000..0a4557c1de6 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/SampleJetty93WebSocketsApplication.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2016 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package samples.websocket.jetty93; + +import samples.websocket.jetty93.client.GreetingService; +import samples.websocket.jetty93.client.SimpleGreetingService; +import samples.websocket.jetty93.echo.DefaultEchoService; +import samples.websocket.jetty93.echo.EchoService; +import samples.websocket.jetty93.echo.EchoWebSocketHandler; +import samples.websocket.jetty93.reverse.ReverseWebSocketEndpoint; +import samples.websocket.jetty93.snake.SnakeWebSocketHandler; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.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 +@EnableAutoConfiguration +@EnableWebSocket +public class SampleJetty93WebSocketsApplication 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(SampleJetty93WebSocketsApplication.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(SampleJetty93WebSocketsApplication.class, args); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/client/GreetingService.java b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/client/GreetingService.java new file mode 100644 index 00000000000..002e17f319e --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/client/GreetingService.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2015 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package samples.websocket.jetty93.client; + +public interface GreetingService { + + String getGreeting(); + +} diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/client/SimpleClientWebSocketHandler.java b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/client/SimpleClientWebSocketHandler.java new file mode 100644 index 00000000000..33c28bba03e --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/client/SimpleClientWebSocketHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2016 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package samples.websocket.jetty93.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-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/client/SimpleGreetingService.java b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/client/SimpleGreetingService.java new file mode 100644 index 00000000000..2472d1be113 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/client/SimpleGreetingService.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2015 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package samples.websocket.jetty93.client; + +public class SimpleGreetingService implements GreetingService { + + @Override + public String getGreeting() { + return "Hello world!"; + } + +} diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/echo/DefaultEchoService.java b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/echo/DefaultEchoService.java new file mode 100644 index 00000000000..26c2e1a06f0 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/echo/DefaultEchoService.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2015 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package samples.websocket.jetty93.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-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/echo/EchoService.java b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/echo/EchoService.java new file mode 100644 index 00000000000..71c4d82391f --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/echo/EchoService.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2015 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package samples.websocket.jetty93.echo; + +public interface EchoService { + + String getMessage(String message); + +} diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/echo/EchoWebSocketHandler.java b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/echo/EchoWebSocketHandler.java new file mode 100644 index 00000000000..f7dfaabcbb0 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/echo/EchoWebSocketHandler.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2016 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package samples.websocket.jetty93.echo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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 Logger logger = LoggerFactory.getLogger(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-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/reverse/ReverseWebSocketEndpoint.java b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/reverse/ReverseWebSocketEndpoint.java new file mode 100644 index 00000000000..014ac18b656 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/reverse/ReverseWebSocketEndpoint.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2015 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package samples.websocket.jetty93.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-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/snake/Direction.java b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/snake/Direction.java new file mode 100644 index 00000000000..5b416b780b1 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/snake/Direction.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package samples.websocket.jetty93.snake; + +public enum Direction { + NONE, NORTH, SOUTH, EAST, WEST +} diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/snake/Location.java b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/snake/Location.java new file mode 100644 index 00000000000..4f56cbe7bed --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/snake/Location.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package samples.websocket.jetty93.snake; + +public class Location { + + public int x; + public int y; + public static final int GRID_SIZE = 10; + public static final int PLAYFIELD_HEIGHT = 480; + public static final int PLAYFIELD_WIDTH = 640; + + 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 - Location.GRID_SIZE); + case SOUTH: + return new Location(this.x, this.y + Location.GRID_SIZE); + case EAST: + return new Location(this.x + Location.GRID_SIZE, this.y); + case WEST: + return new Location(this.x - Location.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-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/snake/Snake.java b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/snake/Snake.java new file mode 100644 index 00000000000..af9ad2033f6 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/snake/Snake.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package samples.websocket.jetty93.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-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/snake/SnakeTimer.java b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/snake/SnakeTimer.java new file mode 100644 index 00000000000..962dfea0e98 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/snake/SnakeTimer.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package samples.websocket.jetty93.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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Sets up the timer for the multi-player snake game WebSocket example. + */ +public class SnakeTimer { + + private static final long TICK_DELAY = 100; + + private static final Object MONITOR = new Object(); + + private static final Logger log = LoggerFactory.getLogger(SnakeTimer.class); + + private static final ConcurrentHashMap snakes = new ConcurrentHashMap(); + + private static Timer gameTimer = null; + + 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) { + log.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-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/snake/SnakeUtils.java b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/snake/SnakeUtils.java new file mode 100644 index 00000000000..7c259061289 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/snake/SnakeUtils.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package samples.websocket.jetty93.snake; + +import java.awt.Color; +import java.util.Random; + +public class SnakeUtils { + + public static final int PLAYFIELD_WIDTH = 640; + public static final int PLAYFIELD_HEIGHT = 480; + public static final int GRID_SIZE = 10; + + private static final Random random = new Random(); + + 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-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/snake/SnakeWebSocketHandler.java b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/snake/SnakeWebSocketHandler.java new file mode 100644 index 00000000000..36309473673 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/java/samples/websocket/jetty93/snake/SnakeWebSocketHandler.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package samples.websocket.jetty93.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 { + + public static final int PLAYFIELD_WIDTH = 640; + public static final int PLAYFIELD_HEIGHT = 480; + public static final int GRID_SIZE = 10; + + private static final AtomicInteger snakeIds = new AtomicInteger(0); + 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(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; + } + + 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-samples/spring-boot-sample-websocket-jetty93/src/main/resources/static/echo.html b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/resources/static/echo.html new file mode 100644 index 00000000000..9a0a7650bfc --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/resources/static/echo.html @@ -0,0 +1,134 @@ + + + + + + Apache Tomcat WebSocket Examples: Echo + + + + + + +
+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+
+
+
+
+ + diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/resources/static/index.html b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/resources/static/index.html new file mode 100644 index 00000000000..e9585067a32 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/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-samples/spring-boot-sample-websocket-jetty93/src/main/resources/static/reverse.html b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/resources/static/reverse.html new file mode 100644 index 00000000000..be2c043930c --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/resources/static/reverse.html @@ -0,0 +1,141 @@ + + + + + + WebSocket Examples: Reverse + + + + + +
+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+
+
+
+
+ + diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/resources/static/snake.html b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/resources/static/snake.html new file mode 100644 index 00000000000..d3053810697 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/main/resources/static/snake.html @@ -0,0 +1,250 @@ + + + + + + + Apache Tomcat WebSocket Examples: Multiplayer Snake + + + + + + +
+ +
+
+
+
+ + + diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/test/java/samples/websocket/jetty93/SampleWebSocketsApplicationTests.java b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/test/java/samples/websocket/jetty93/SampleWebSocketsApplicationTests.java new file mode 100644 index 00000000000..a3ae520cf9e --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/test/java/samples/websocket/jetty93/SampleWebSocketsApplicationTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2012-2016 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package samples.websocket.jetty93; + +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.Test; +import org.junit.runner.RunWith; +import samples.websocket.jetty93.SampleJetty93WebSocketsApplication; +import samples.websocket.jetty93.client.GreetingService; +import samples.websocket.jetty93.client.SimpleClientWebSocketHandler; +import samples.websocket.jetty93.client.SimpleGreetingService; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.socket.client.WebSocketConnectionManager; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = SampleJetty93WebSocketsApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT) +@DirtiesContext +public class SampleWebSocketsApplicationTests { + + private static Log logger = LogFactory.getLog(SampleWebSocketsApplicationTests.class); + + @LocalServerPort + private int port = 1234; + + @Test + public void echoEndpoint() throws Exception { + ConfigurableApplicationContext context = new SpringApplicationBuilder( + ClientConfiguration.class, PropertyPlaceholderAutoConfiguration.class) + .properties("websocket.uri:ws://localhost:" + this.port + + "/echo/websocket") + .run("--spring.main.web_environment=false"); + 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 + public void reverseEndpoint() throws Exception { + ConfigurableApplicationContext context = new SpringApplicationBuilder( + ClientConfiguration.class, PropertyPlaceholderAutoConfiguration.class) + .properties( + "websocket.uri:ws://localhost:" + this.port + "/reverse") + .run("--spring.main.web_environment=false"); + 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 + 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 + public WebSocketConnectionManager wsConnectionManager() { + + WebSocketConnectionManager manager = new WebSocketConnectionManager(client(), + handler(), this.webSocketUri); + manager.setAutoStartup(true); + + return manager; + } + + @Bean + public StandardWebSocketClient client() { + return new StandardWebSocketClient(); + } + + @Bean + public SimpleClientWebSocketHandler handler() { + return new SimpleClientWebSocketHandler(greetingService(), this.latch, + this.messagePayload); + } + + @Bean + public GreetingService greetingService() { + return new SimpleGreetingService(); + } + } + +} diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/test/java/samples/websocket/jetty93/echo/CustomContainerWebSocketsApplicationTests.java b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/test/java/samples/websocket/jetty93/echo/CustomContainerWebSocketsApplicationTests.java new file mode 100644 index 00000000000..92790abcc37 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/test/java/samples/websocket/jetty93/echo/CustomContainerWebSocketsApplicationTests.java @@ -0,0 +1,153 @@ +/* + * Copyright 2012-2016 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package samples.websocket.jetty93.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.Test; +import org.junit.runner.RunWith; +import samples.websocket.jetty93.SampleJetty93WebSocketsApplication; +import samples.websocket.jetty93.client.GreetingService; +import samples.websocket.jetty93.client.SimpleClientWebSocketHandler; +import samples.websocket.jetty93.client.SimpleGreetingService; +import samples.websocket.jetty93.echo.CustomContainerWebSocketsApplicationTests.CustomContainerConfiguration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; +import org.springframework.boot.context.embedded.jetty.JettyEmbeddedServletContainerFactory; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.SocketUtils; +import org.springframework.web.socket.client.WebSocketConnectionManager; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = { SampleJetty93WebSocketsApplication.class, + CustomContainerConfiguration.class }, webEnvironment = WebEnvironment.DEFINED_PORT) +@DirtiesContext +public class CustomContainerWebSocketsApplicationTests { + + private static Log logger = LogFactory + .getLog(CustomContainerWebSocketsApplicationTests.class); + + private static int PORT = SocketUtils.findAvailableTcpPort(); + + @Test + public void echoEndpoint() throws Exception { + ConfigurableApplicationContext context = new SpringApplicationBuilder( + ClientConfiguration.class, PropertyPlaceholderAutoConfiguration.class) + .properties("websocket.uri:ws://localhost:" + PORT + + "/ws/echo/websocket") + .run("--spring.main.web_environment=false"); + 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 + public void reverseEndpoint() throws Exception { + ConfigurableApplicationContext context = new SpringApplicationBuilder( + ClientConfiguration.class, PropertyPlaceholderAutoConfiguration.class) + .properties( + "websocket.uri:ws://localhost:" + PORT + "/ws/reverse") + .run("--spring.main.web_environment=false"); + 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 + protected static class CustomContainerConfiguration { + + @Bean + public EmbeddedServletContainerFactory embeddedServletContainerFactory() { + return new JettyEmbeddedServletContainerFactory("/ws", PORT); + } + + } + + @Configuration + 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 + public WebSocketConnectionManager wsConnectionManager() { + + WebSocketConnectionManager manager = new WebSocketConnectionManager(client(), + handler(), this.webSocketUri); + manager.setAutoStartup(true); + + return manager; + } + + @Bean + public StandardWebSocketClient client() { + return new StandardWebSocketClient(); + } + + @Bean + public SimpleClientWebSocketHandler handler() { + return new SimpleClientWebSocketHandler(greetingService(), this.latch, + this.messagePayload); + } + + @Bean + public GreetingService greetingService() { + return new SimpleGreetingService(); + } + + } + +} diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/test/java/samples/websocket/jetty93/snake/SnakeTimerTests.java b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/test/java/samples/websocket/jetty93/snake/SnakeTimerTests.java new file mode 100644 index 00000000000..c9407cdb7c0 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty93/src/test/java/samples/websocket/jetty93/snake/SnakeTimerTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2016 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package samples.websocket.jetty93.snake; + +import java.io.IOException; + +import org.junit.Test; +import samples.websocket.jetty93.snake.Snake; +import samples.websocket.jetty93.snake.SnakeTimer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; + +public class SnakeTimerTests { + + @Test + public 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); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java index 43c9a116edf..dd6845ccbd3 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java @@ -18,6 +18,7 @@ package org.springframework.boot.context.embedded.jetty; import java.io.File; import java.io.IOException; +import java.lang.reflect.Method; import java.net.InetSocketAddress; import java.net.URL; import java.nio.charset.Charset; @@ -47,12 +48,13 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.SessionManager; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.server.handler.HandlerWrapper; import org.eclipse.jetty.server.handler.gzip.GzipHandler; -import org.eclipse.jetty.server.session.HashSessionManager; +import org.eclipse.jetty.server.session.DefaultSessionCache; +import org.eclipse.jetty.server.session.FileSessionDataStore; +import org.eclipse.jetty.server.session.SessionHandler; import org.eclipse.jetty.servlet.ErrorPageErrorHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletMapping; @@ -113,6 +115,8 @@ public class JettyEmbeddedServletContainerFactory private static final String CONNECTOR_JETTY_8 = "org.eclipse.jetty.server.nio.SelectChannelConnector"; + private static final String SESSION_JETTY_9_3 = "org.eclipse.jetty.server.session.HashSessionManager"; + private List configurations = new ArrayList(); private boolean useForwardHeaders; @@ -366,6 +370,20 @@ public class JettyEmbeddedServletContainerFactory postProcessWebAppContext(context); } + private void configureSession(WebAppContext context) { + SessionConfigurer configurer = getSessionConfigurer(); + configurer.configure(context, getSessionTimeout(), isPersistSession(), + new SessionDirectory() { + + @Override + public File get() { + return JettyEmbeddedServletContainerFactory.this + .getValidSessionStoreDir(); + } + + }); + } + private void addLocaleMappings(WebAppContext context) { for (Map.Entry entry : getLocaleCharsetMappings().entrySet()) { Locale locale = entry.getKey(); @@ -374,25 +392,11 @@ public class JettyEmbeddedServletContainerFactory } } - private void configureSession(WebAppContext context) { - SessionManager sessionManager = context.getSessionHandler().getSessionManager(); - int sessionTimeout = (getSessionTimeout() > 0 ? getSessionTimeout() : -1); - sessionManager.setMaxInactiveInterval(sessionTimeout); - if (isPersistSession()) { - Assert.isInstanceOf(HashSessionManager.class, sessionManager, - "Unable to use persistent sessions"); - configurePersistSession(sessionManager); - } - } - - private void configurePersistSession(SessionManager sessionManager) { - try { - ((HashSessionManager) sessionManager) - .setStoreDirectory(getValidSessionStoreDir()); - } - catch (IOException ex) { - throw new IllegalStateException(ex); + private SessionConfigurer getSessionConfigurer() { + if (ClassUtils.isPresent(SESSION_JETTY_9_3, getClass().getClassLoader())) { + return new Jetty93SessionConfigurer(); } + return new Jetty94SessionConfigurer(); } private File getTempDirectory() { @@ -958,4 +962,95 @@ public class JettyEmbeddedServletContainerFactory } + /** + * Provides access to the session directory. + */ + private interface SessionDirectory { + + File get(); + + } + + /** + * Strategy used to configure Jetty sessions. + */ + private interface SessionConfigurer { + + void configure(WebAppContext context, int timeout, boolean persist, + SessionDirectory sessionDirectory); + + } + + /** + * SessionConfigurer for Jetty 9.3 and earlier. + */ + private static class Jetty93SessionConfigurer implements SessionConfigurer { + + @Override + public void configure(WebAppContext context, int timeout, boolean persist, + SessionDirectory sessionDirectory) { + SessionHandler handler = context.getSessionHandler(); + Object manager = getSessionManager(handler); + setMaxInactiveInterval(manager, timeout > 0 ? timeout : -1); + if (persist) { + Class hashSessionManagerClass = ClassUtils.resolveClassName( + "org.eclipse.jetty.server.session.HashSessionManager", + handler.getClass().getClassLoader()); + Assert.isInstanceOf(hashSessionManagerClass, manager, + "Unable to use persistent sessions"); + configurePersistSession(manager, sessionDirectory); + } + } + + private Object getSessionManager(SessionHandler handler) { + Method method = ReflectionUtils.findMethod(SessionHandler.class, + "getSessionManager"); + return ReflectionUtils.invokeMethod(method, handler); + } + + private void setMaxInactiveInterval(Object manager, int interval) { + Method method = ReflectionUtils.findMethod(manager.getClass(), + "setMaxInactiveInterval", Integer.TYPE); + ReflectionUtils.invokeMethod(method, manager, interval); + } + + private void configurePersistSession(Object manager, + SessionDirectory sessionDirectory) { + try { + setStoreDirectory(manager, sessionDirectory.get()); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + private void setStoreDirectory(Object manager, File file) throws IOException { + Method method = ReflectionUtils.findMethod(manager.getClass(), + "setStoreDirectory", File.class); + ReflectionUtils.invokeMethod(method, manager, file); + } + + } + + /** + * SessionConfigurer for Jetty 9.4 and earlier. + */ + private static class Jetty94SessionConfigurer implements SessionConfigurer { + + @Override + public void configure(WebAppContext context, int timeout, boolean persist, + SessionDirectory sessionDirectory) { + SessionHandler handler = context.getSessionHandler(); + handler.setMaxInactiveInterval(timeout > 0 ? timeout : -1); + if (persist) { + DefaultSessionCache cache = new DefaultSessionCache(handler); + FileSessionDataStore store = new FileSessionDataStore(); + store.setStoreDir(sessionDirectory.get()); + cache.setSessionDataStore(store); + handler.setSessionCache(cache); + } + } + + } + } diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java index 97b8fbc0607..46edc917ef6 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java @@ -216,8 +216,7 @@ public class JettyEmbeddedServletContainerFactoryTests Handler[] handlers = jettyContainer.getServer() .getChildHandlersByClass(WebAppContext.class); WebAppContext webAppContext = (WebAppContext) handlers[0]; - int actual = webAppContext.getSessionHandler().getSessionManager() - .getMaxInactiveInterval(); + int actual = webAppContext.getSessionHandler().getMaxInactiveInterval(); assertThat(actual).isEqualTo(expected); }