diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml
index 16c5c4e79ed..e5e51659e29 100644
--- a/spring-boot-samples/pom.xml
+++ b/spring-boot-samples/pom.xml
@@ -75,6 +75,7 @@
spring-boot-sample-web-velocityspring-boot-sample-websocketspring-boot-sample-websocket-jetty
+ spring-boot-sample-websocket-undertowspring-boot-sample-wsspring-boot-sample-xml
diff --git a/spring-boot-samples/spring-boot-sample-websocket-undertow/pom.xml b/spring-boot-samples/spring-boot-sample-websocket-undertow/pom.xml
new file mode 100755
index 00000000000..eef085f6e47
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-undertow/pom.xml
@@ -0,0 +1,55 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-samples
+ 1.2.0.BUILD-SNAPSHOT
+
+ spring-boot-sample-websocket-undertow
+ Spring Boot WebSocket Undertow Sample
+ Spring Boot WebSocket Undertow Sample
+ http://projects.spring.io/spring-boot/
+
+ Pivotal Software, Inc.
+ http://www.spring.io
+
+
+ ${basedir}/../..
+ 1.7
+
+
+
+ org.springframework.boot
+ spring-boot-starter-websocket
+
+
+ org.springframework.boot
+ spring-boot-starter-tomcat
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-undertow
+
+
+ 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-undertow/src/main/java/samples/websocket/client/GreetingService.java b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/client/GreetingService.java
new file mode 100644
index 00000000000..e03d15d76ad
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/client/GreetingService.java
@@ -0,0 +1,23 @@
+/*
+ * 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 samples.websocket.client;
+
+public interface GreetingService {
+
+ String getGreeting();
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/client/SimpleClientWebSocketHandler.java b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/client/SimpleClientWebSocketHandler.java
new file mode 100644
index 00000000000..828291de186
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/client/SimpleClientWebSocketHandler.java
@@ -0,0 +1,62 @@
+/*
+ * 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 samples.websocket.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.beans.factory.annotation.Autowired;
+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;
+
+ @Autowired
+ 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-undertow/src/main/java/samples/websocket/client/SimpleGreetingService.java b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/client/SimpleGreetingService.java
new file mode 100644
index 00000000000..e08ec78e34d
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/client/SimpleGreetingService.java
@@ -0,0 +1,26 @@
+/*
+ * 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 samples.websocket.client;
+
+public class SimpleGreetingService implements GreetingService {
+
+ @Override
+ public String getGreeting() {
+ return "Hello world!";
+ }
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/config/SampleUndertowWebSocketsApplication.java b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/config/SampleUndertowWebSocketsApplication.java
new file mode 100644
index 00000000000..054a5eb3b06
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/config/SampleUndertowWebSocketsApplication.java
@@ -0,0 +1,89 @@
+/*
+ * 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 samples.websocket.config;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.context.web.SpringBootServletInitializer;
+import org.springframework.context.annotation.Bean;
+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;
+
+import samples.websocket.client.GreetingService;
+import samples.websocket.client.SimpleGreetingService;
+import samples.websocket.echo.DefaultEchoService;
+import samples.websocket.echo.EchoService;
+import samples.websocket.echo.EchoWebSocketHandler;
+import samples.websocket.reverse.ReverseWebSocketEndpoint;
+import samples.websocket.snake.SnakeWebSocketHandler;
+
+@SpringBootApplication
+@EnableWebSocket
+public class SampleUndertowWebSocketsApplication 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(SampleUndertowWebSocketsApplication.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(SampleUndertowWebSocketsApplication.class, args);
+ }
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/echo/DefaultEchoService.java b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/echo/DefaultEchoService.java
new file mode 100644
index 00000000000..c967613eb78
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/echo/DefaultEchoService.java
@@ -0,0 +1,32 @@
+/*
+ * 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 samples.websocket.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-undertow/src/main/java/samples/websocket/echo/EchoService.java b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/echo/EchoService.java
new file mode 100644
index 00000000000..c07323cd567
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/echo/EchoService.java
@@ -0,0 +1,23 @@
+/*
+ * 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 samples.websocket.echo;
+
+public interface EchoService {
+
+ String getMessage(String message);
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/echo/EchoWebSocketHandler.java b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/echo/EchoWebSocketHandler.java
new file mode 100644
index 00000000000..b97a5e01c53
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/echo/EchoWebSocketHandler.java
@@ -0,0 +1,61 @@
+/*
+ * 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 samples.websocket.echo;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+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;
+
+ @Autowired
+ 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-undertow/src/main/java/samples/websocket/reverse/ReverseWebSocketEndpoint.java b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/reverse/ReverseWebSocketEndpoint.java
new file mode 100644
index 00000000000..a7802edcd22
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/reverse/ReverseWebSocketEndpoint.java
@@ -0,0 +1,33 @@
+/*
+ * 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 samples.websocket.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-undertow/src/main/java/samples/websocket/snake/Direction.java b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/snake/Direction.java
new file mode 100644
index 00000000000..b5295270e26
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/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.snake;
+
+public enum Direction {
+ NONE, NORTH, SOUTH, EAST, WEST
+}
diff --git a/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/snake/Location.java b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/snake/Location.java
new file mode 100644
index 00000000000..55a50853405
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/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.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-undertow/src/main/java/samples/websocket/snake/Snake.java b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/snake/Snake.java
new file mode 100644
index 00000000000..1bba0047323
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/snake/Snake.java
@@ -0,0 +1,139 @@
+/*
+ * 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.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 int id;
+ private final WebSocketSession session;
+
+ private Direction direction;
+ private int length = DEFAULT_LENGTH;
+ private Location head;
+ private final Deque tail = new ArrayDeque();
+ private final String hexColor;
+
+ 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 synchronized void kill() throws Exception {
+ resetState();
+ sendMessage("{'type': 'dead'}");
+ }
+
+ private synchronized void reward() throws Exception {
+ this.length++;
+ sendMessage("{'type': 'kill'}");
+ }
+
+ protected void sendMessage(String msg) throws Exception {
+ this.session.sendMessage(new TextMessage(msg));
+ }
+
+ public synchronized void update(Collection snakes) throws Exception {
+ 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 synchronized Location getHead() {
+ return this.head;
+ }
+
+ public synchronized Collection getTail() {
+ return this.tail;
+ }
+
+ public synchronized void setDirection(Direction direction) {
+ this.direction = direction;
+ }
+
+ public synchronized String getLocationsJson() {
+ 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-undertow/src/main/java/samples/websocket/snake/SnakeTimer.java b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/snake/SnakeTimer.java
new file mode 100644
index 00000000000..a11b12901e5
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/snake/SnakeTimer.java
@@ -0,0 +1,109 @@
+/*
+ * 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.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 Logger log = LoggerFactory.getLogger(SnakeTimer.class);
+
+ private static Timer gameTimer = null;
+
+ private static final long TICK_DELAY = 100;
+
+ private static final ConcurrentHashMap snakes = new ConcurrentHashMap();
+
+ public static synchronized void addSnake(Snake snake) {
+ if (snakes.size() == 0) {
+ startTimer();
+ }
+ snakes.put(Integer.valueOf(snake.getId()), snake);
+ }
+
+ public static Collection getSnakes() {
+ return Collections.unmodifiableCollection(snakes.values());
+ }
+
+ public static synchronized void removeSnake(Snake snake) {
+ snakes.remove(Integer.valueOf(snake.getId()));
+ if (snakes.size() == 0) {
+ 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-undertow/src/main/java/samples/websocket/snake/SnakeUtils.java b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/snake/SnakeUtils.java
new file mode 100644
index 00000000000..b114992ce89
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/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.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-undertow/src/main/java/samples/websocket/snake/SnakeWebSocketHandler.java b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/snake/SnakeWebSocketHandler.java
new file mode 100644
index 00000000000..a278eb09bee
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/java/samples/websocket/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.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-undertow/src/main/resources/static/echo.html b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/resources/static/echo.html
new file mode 100644
index 00000000000..0ffca05b4d0
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/resources/static/echo.html
@@ -0,0 +1,133 @@
+
+
+
+
+ Apache Tomcat WebSocket Examples: Echo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/resources/static/index.html b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/resources/static/index.html
new file mode 100644
index 00000000000..e2b76b6e445
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-undertow/src/main/resources/static/index.html
@@ -0,0 +1,32 @@
+
+
+
+
+ Apache Tomcat WebSocket Examples: Index
+
+
+
+