diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml
index b0d670ebc2..2615b09a9a 100644
--- a/spring-boot-samples/pom.xml
+++ b/spring-boot-samples/pom.xml
@@ -26,6 +26,7 @@
spring-boot-sample-traditionalspring-boot-sample-web-staticspring-boot-sample-web-ui
+ spring-boot-sample-websocketspring-boot-sample-xml
diff --git a/spring-boot-samples/spring-boot-sample-websocket/pom.xml b/spring-boot-samples/spring-boot-sample-websocket/pom.xml
new file mode 100644
index 0000000000..bddfd5fc57
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket/pom.xml
@@ -0,0 +1,58 @@
+
+
+ 4.0.0
+ spring-boot-sample-websocket
+ war
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 0.5.0.BUILD-SNAPSHOT
+
+
+
+ 1.7
+ 8.0-SNAPSHOT
+ org.springframework.boot.samples.websocket.config.ApplicationConfiguration
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-websocket
+ ${spring.boot.version}
+
+
+
+ org.eclipse.jetty.websocket
+ websocket-client
+ 9.0.3.v20130506
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
+ tomcat-snapshots
+ https://repository.apache.org/content/repositories/snapshots
+
+ true
+
+
+ false
+
+
+
+
+
diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/client/GreetingService.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/client/GreetingService.java
new file mode 100644
index 0000000000..1b6793f1f8
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/client/GreetingService.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2002-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 org.springframework.boot.samples.websocket.client;
+
+public interface GreetingService {
+
+ String getGreeting();
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/client/SimpleClientWebSocketHandler.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/client/SimpleClientWebSocketHandler.java
new file mode 100644
index 0000000000..4b8ca4a838
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/client/SimpleClientWebSocketHandler.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2002-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 org.springframework.boot.samples.websocket.client;
+
+import java.util.concurrent.CountDownLatch;
+
+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.adapter.TextWebSocketHandlerAdapter;
+
+public class SimpleClientWebSocketHandler extends TextWebSocketHandlerAdapter {
+
+ protected Log logger = LogFactory.getLog(SimpleClientWebSocketHandler.class);
+
+ private final GreetingService greetingService;
+
+ private CountDownLatch latch;
+
+ @Autowired
+ public SimpleClientWebSocketHandler(GreetingService greetingService, CountDownLatch latch) {
+ this.greetingService = greetingService;
+ this.latch = latch;
+ }
+
+ @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 {
+ logger.info("Received: " + message + " (" + latch.getCount() + ")");
+ session.close();
+ latch.countDown();
+ }
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/client/SimpleGreetingService.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/client/SimpleGreetingService.java
new file mode 100644
index 0000000000..726dd879f4
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/client/SimpleGreetingService.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2002-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 org.springframework.boot.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/src/main/java/org/springframework/boot/samples/websocket/config/SampleWebSocketsApplication.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/config/SampleWebSocketsApplication.java
new file mode 100644
index 0000000000..f8f2aad3a5
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/config/SampleWebSocketsApplication.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2002-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 org.springframework.boot.samples.websocket.config;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.tomcat.websocket.server.WsSci;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
+import org.springframework.boot.samples.websocket.client.GreetingService;
+import org.springframework.boot.samples.websocket.client.SimpleGreetingService;
+import org.springframework.boot.samples.websocket.echo.DefaultEchoService;
+import org.springframework.boot.samples.websocket.echo.EchoService;
+import org.springframework.boot.samples.websocket.echo.EchoWebSocketHandler;
+import org.springframework.boot.samples.websocket.snake.SnakeWebSocketHandler;
+import org.springframework.boot.web.SpringServletInitializer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+import org.springframework.web.servlet.DispatcherServlet;
+import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler;
+import org.springframework.web.socket.sockjs.SockJsService;
+import org.springframework.web.socket.sockjs.support.DefaultSockJsService;
+import org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler;
+import org.springframework.web.socket.support.PerConnectionWebSocketHandler;
+
+@Configuration
+public class SampleWebSocketsApplication extends SpringServletInitializer {
+
+ @Override
+ protected Class>[] getConfigClasses() {
+ return new Class>[] { SampleWebSocketsApplication.class };
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(SampleWebSocketsApplication.class, args);
+ }
+
+ @ConditionalOnClass(Tomcat.class)
+ @Configuration
+ @EnableAutoConfiguration
+ protected static class InitializationConfiguration {
+ @Bean
+ public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
+ TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory() {
+ @Override
+ protected void postProcessContext(Context context) {
+ context.addServletContainerInitializer(new WsSci(), null);
+ }
+ };
+ return factory;
+ }
+ }
+
+ @Bean
+ public EchoService echoService() {
+ return new DefaultEchoService("Did you say \"%s\"?");
+ }
+
+ @Bean
+ public GreetingService greetingService() {
+ return new SimpleGreetingService();
+ }
+
+ @Bean
+ public SimpleUrlHandlerMapping handlerMapping() {
+
+ SockJsService sockJsService = new DefaultSockJsService(sockJsTaskScheduler());
+
+ Map urlMap = new HashMap();
+
+ urlMap.put("/echo", new WebSocketHttpRequestHandler(echoWebSocketHandler()));
+ urlMap.put("/snake", new WebSocketHttpRequestHandler(snakeWebSocketHandler()));
+
+ urlMap.put("/sockjs/echo/**", new SockJsHttpRequestHandler(sockJsService, echoWebSocketHandler()));
+ urlMap.put("/sockjs/snake/**", new SockJsHttpRequestHandler(sockJsService, snakeWebSocketHandler()));
+
+ SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
+ handlerMapping.setOrder(-1);
+ handlerMapping.setUrlMap(urlMap);
+
+ return handlerMapping;
+ }
+
+ @Bean
+ public DispatcherServlet dispatcherServlet() {
+ DispatcherServlet servlet = new DispatcherServlet();
+ servlet.setDispatchOptionsRequest(true);
+ return servlet;
+ }
+
+ @Bean
+ public WebSocketHandler echoWebSocketHandler() {
+ return new PerConnectionWebSocketHandler(EchoWebSocketHandler.class);
+ }
+
+ @Bean
+ public WebSocketHandler snakeWebSocketHandler() {
+ return new SnakeWebSocketHandler();
+ }
+
+ @Bean
+ public ThreadPoolTaskScheduler sockJsTaskScheduler() {
+ ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
+ taskScheduler.setThreadNamePrefix("SockJS-");
+ return taskScheduler;
+ }
+}
diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/echo/DefaultEchoService.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/echo/DefaultEchoService.java
new file mode 100644
index 0000000000..bfc3b32474
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/echo/DefaultEchoService.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2002-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 org.springframework.boot.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/src/main/java/org/springframework/boot/samples/websocket/echo/EchoService.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/echo/EchoService.java
new file mode 100644
index 0000000000..60751cccbb
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/echo/EchoService.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2002-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 org.springframework.boot.samples.websocket.echo;
+
+public interface EchoService {
+
+ String getMessage(String message);
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/echo/EchoWebSocketHandler.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/echo/EchoWebSocketHandler.java
new file mode 100644
index 0000000000..fe76ee9686
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/echo/EchoWebSocketHandler.java
@@ -0,0 +1,43 @@
+package org.springframework.boot.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.adapter.TextWebSocketHandlerAdapter;
+
+/**
+ * Echo messages by implementing a Spring {@link WebSocketHandler} abstraction.
+ */
+public class EchoWebSocketHandler extends TextWebSocketHandlerAdapter {
+
+ 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/src/main/java/org/springframework/boot/samples/websocket/snake/Direction.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/Direction.java
new file mode 100644
index 0000000000..60e18f7b03
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/Direction.java
@@ -0,0 +1,21 @@
+/*
+ * 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 org.springframework.boot.samples.websocket.snake;
+
+public enum Direction {
+ NONE, NORTH, SOUTH, EAST, WEST
+}
diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/Location.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/Location.java
new file mode 100644
index 0000000000..493b9c69ae
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/Location.java
@@ -0,0 +1,71 @@
+/*
+ * 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 org.springframework.boot.samples.websocket.snake;
+
+import org.springframework.boot.samples.websocket.snake.Direction;
+
+
+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(x, y - Location.GRID_SIZE);
+ case SOUTH:
+ return new Location(x, y + Location.GRID_SIZE);
+ case EAST:
+ return new Location(x + Location.GRID_SIZE, y);
+ case WEST:
+ return new Location(x - Location.GRID_SIZE, 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 (x != location.x) return false;
+ if (y != location.y) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = x;
+ result = 31 * result + y;
+ return result;
+ }
+}
diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/Snake.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/Snake.java
new file mode 100644
index 0000000000..0ccf717281
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/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 org.springframework.boot.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 {
+ length++;
+ sendMessage("{'type': 'kill'}");
+ }
+
+
+ protected void sendMessage(String msg) throws Exception {
+ session.sendMessage(new TextMessage(msg));
+ }
+
+ public synchronized void update(Collection snakes) throws Exception {
+ Location nextLocation = head.getAdjacentLocation(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 (direction != Direction.NONE) {
+ tail.addFirst(head);
+ if (tail.size() > length) {
+ tail.removeLast();
+ }
+ head = nextLocation;
+ }
+
+ handleCollisions(snakes);
+ }
+
+ private void handleCollisions(Collection snakes) throws Exception {
+ for (Snake snake : snakes) {
+ boolean headCollision = id != snake.id && snake.getHead().equals(head);
+ boolean tailCollision = snake.getTail().contains(head);
+ if (headCollision || tailCollision) {
+ kill();
+ if (id != snake.id) {
+ snake.reward();
+ }
+ }
+ }
+ }
+
+ public synchronized Location getHead() {
+ return head;
+ }
+
+ public synchronized Collection getTail() {
+ return 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(head.x), Integer.valueOf(head.y)));
+ for (Location location : 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(id), sb.toString());
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public String getHexColor() {
+ return hexColor;
+ }
+}
diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/SnakeTimer.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/SnakeTimer.java
new file mode 100644
index 0000000000..e0c70a6b07
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/SnakeTimer.java
@@ -0,0 +1,107 @@
+/*
+ * 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 org.springframework.boot.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 org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+
+/**
+ * Sets up the timer for the multi-player snake game WebSocket example.
+ */
+public class SnakeTimer {
+
+ private static final Log log =
+ LogFactory.getLog(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 {
+ for (Snake snake : SnakeTimer.getSnakes()) {
+ snake.sendMessage(message);
+ }
+ }
+
+
+ public static void startTimer() {
+ gameTimer = new Timer(SnakeTimer.class.getSimpleName() + " Timer");
+ gameTimer.scheduleAtFixedRate(new TimerTask() {
+ @Override
+ public void run() {
+ try {
+ tick();
+ } catch (Throwable e) {
+ log.error("Caught to prevent timer from shutting down", e);
+ }
+ }
+ }, TICK_DELAY, TICK_DELAY);
+ }
+
+
+ public static void stopTimer() {
+ if (gameTimer != null) {
+ gameTimer.cancel();
+ }
+ }
+}
diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/SnakeUtils.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/SnakeUtils.java
new file mode 100644
index 0000000000..81527238fd
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/SnakeUtils.java
@@ -0,0 +1,56 @@
+/*
+ * 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 org.springframework.boot.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/src/main/java/org/springframework/boot/samples/websocket/snake/SnakeWebSocketHandler.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/SnakeWebSocketHandler.java
new file mode 100644
index 0000000000..f2eba5c349
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/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 org.springframework.boot.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.adapter.TextWebSocketHandlerAdapter;
+
+public class SnakeWebSocketHandler extends TextWebSocketHandlerAdapter {
+
+ 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(id, session);
+ SnakeTimer.addSnake(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)) {
+ snake.setDirection(Direction.WEST);
+ } else if ("north".equals(payload)) {
+ snake.setDirection(Direction.NORTH);
+ } else if ("east".equals(payload)) {
+ snake.setDirection(Direction.EAST);
+ } else if ("south".equals(payload)) {
+ snake.setDirection(Direction.SOUTH);
+ }
+ }
+
+
+ @Override
+ public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
+ SnakeTimer.removeSnake(snake);
+ SnakeTimer.broadcast(String.format("{'type': 'leave', 'id': %d}",
+ Integer.valueOf(id)));
+ }
+}
diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/echo.html b/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/echo.html
new file mode 100644
index 0000000000..cbaef719dd
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/echo.html
@@ -0,0 +1,140 @@
+
+
+
+
+ Apache Tomcat WebSocket Examples: Echo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/index.html b/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/index.html
new file mode 100644
index 0000000000..39069b15d7
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/index.html
@@ -0,0 +1,31 @@
+
+
+
+
+ Apache Tomcat WebSocket Examples: Index
+
+
+
+