diff --git a/spring-boot-autoconfigure/pom.xml b/spring-boot-autoconfigure/pom.xml
index ccab61becd..2f7f0401e5 100644
--- a/spring-boot-autoconfigure/pom.xml
+++ b/spring-boot-autoconfigure/pom.xml
@@ -160,6 +160,11 @@
jetty-webapptrue
+
+ org.eclipse.jetty.websocket
+ javax-websocket-server-impl
+ true
+ io.undertowundertow-servlet
@@ -403,6 +408,11 @@
test-jartest
+
+ org.apache.tomcat.embed
+ tomcat-embed-websocket
+ test
+ org.hsqldbhsqldb
diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/WebSocketAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/WebSocketAutoConfiguration.java
index 610f6edd23..a214b9d534 100644
--- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/WebSocketAutoConfiguration.java
+++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/WebSocketAutoConfiguration.java
@@ -22,6 +22,9 @@ import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
+import org.eclipse.jetty.webapp.AbstractConfiguration;
+import org.eclipse.jetty.webapp.WebAppContext;
+import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -29,97 +32,157 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
+import org.springframework.boot.context.embedded.jetty.JettyEmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.boot.context.web.NonEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.socket.WebSocketHandler;
/**
- * Auto configuration for websocket server in embedded Tomcat. If
- * spring-websocket is detected on the classpath then we add a listener that
+ * Auto configuration for websocket server in embedded Tomcat or Jetty. Requires
+ * spring-websocket and either Tomcat or Jetty with their WebSocket modules
+ * to be on the classpath.
+ *
+ * If Tomcat's WebSocket support is detected on the classpath we add a listener that
* installs the Tomcat Websocket initializer. In a non-embedded container it should
* already be there.
+ *
+ * If Jetty's WebSocket support is detected on the classpath we add a configuration that
+ * configures the context with WebSocket support. In a non-embedded container it should
+ * already be there.
*
* @author Dave Syer
* @author Phillip Webb
+ * @author Andy Wilkinson
*/
@Configuration
-@ConditionalOnClass(name = "org.apache.tomcat.websocket.server.WsSci", value = {
- Servlet.class, Tomcat.class, WebSocketHandler.class })
+@ConditionalOnClass({ Servlet.class, WebSocketHandler.class })
@AutoConfigureBefore(EmbeddedServletContainerAutoConfiguration.class)
public class WebSocketAutoConfiguration {
- private static final String TOMCAT_7_LISTENER_TYPE = "org.apache.catalina.deploy.ApplicationListener";
+ @Configuration
+ @ConditionalOnClass(name = "org.apache.tomcat.websocket.server.WsSci", value = Tomcat.class)
+ static class TomcatWebSocketConfiguration {
- private static final String TOMCAT_8_LISTENER_TYPE = "org.apache.tomcat.util.descriptor.web.ApplicationListener";
+ private static final String TOMCAT_7_LISTENER_TYPE = "org.apache.catalina.deploy.ApplicationListener";
- private static final String WS_LISTENER = "org.apache.tomcat.websocket.server.WsContextListener";
+ private static final String TOMCAT_8_LISTENER_TYPE = "org.apache.tomcat.util.descriptor.web.ApplicationListener";
- private static Log logger = LogFactory.getLog(WebSocketAutoConfiguration.class);
+ private static final String WS_LISTENER = "org.apache.tomcat.websocket.server.WsContextListener";
- @Bean
- @ConditionalOnMissingBean(name = "websocketContainerCustomizer")
- public EmbeddedServletContainerCustomizer websocketContainerCustomizer() {
- return new EmbeddedServletContainerCustomizer() {
+ @Bean
+ @ConditionalOnMissingBean(name = "websocketContainerCustomizer")
+ public EmbeddedServletContainerCustomizer websocketContainerCustomizer() {
+ return new WebSocketContainerCustomizer(
+ TomcatEmbeddedServletContainerFactory.class) {
- @Override
- public void customize(ConfigurableEmbeddedServletContainer container) {
- if (container instanceof NonEmbeddedServletContainerFactory) {
- logger.info("NonEmbeddedServletContainerFactory detected. Websockets "
- + "support should be native so this normally is not a problem.");
- return;
+ @Override
+ public void doCustomize(
+ TomcatEmbeddedServletContainerFactory tomcatContainer) {
+ tomcatContainer.addContextCustomizers(new TomcatContextCustomizer() {
+ @Override
+ public void customize(Context context) {
+ addListener(context, findListenerType());
+ }
+ });
}
- Assert.state(container instanceof TomcatEmbeddedServletContainerFactory,
- "Websockets are currently only supported in Tomcat (found "
- + container.getClass() + "). ");
- TomcatEmbeddedServletContainerFactory tomcatContainer = (TomcatEmbeddedServletContainerFactory) container;
- tomcatContainer.addContextCustomizers(new TomcatContextCustomizer() {
- @Override
- public void customize(Context context) {
- addListener(context, findListenerType());
- }
- });
+
+ };
+ }
+
+ private static Class> findListenerType() {
+ if (ClassUtils.isPresent(TOMCAT_7_LISTENER_TYPE, null)) {
+ return ClassUtils.resolveClassName(TOMCAT_7_LISTENER_TYPE, null);
}
-
- };
- }
-
- private static Class> findListenerType() {
- if (ClassUtils.isPresent(TOMCAT_7_LISTENER_TYPE, null)) {
- return ClassUtils.resolveClassName(TOMCAT_7_LISTENER_TYPE, null);
+ if (ClassUtils.isPresent(TOMCAT_8_LISTENER_TYPE, null)) {
+ return ClassUtils.resolveClassName(TOMCAT_8_LISTENER_TYPE, null);
+ }
+ // With Tomcat 8.0.8 ApplicationListener is not required
+ return null;
}
- if (ClassUtils.isPresent(TOMCAT_8_LISTENER_TYPE, null)) {
- return ClassUtils.resolveClassName(TOMCAT_8_LISTENER_TYPE, null);
- }
- // With Tomcat 8.0.8 ApplicationListener is not required
- return null;
- }
- /**
- * Instead of registering the WsSci directly as a ServletContainerInitializer, we use
- * the ApplicationListener provided by Tomcat. Unfortunately the ApplicationListener
- * class moved packages in Tomcat 8 and been deleted in 8.0.8 so we have to use
- * reflection.
- * @param context the current context
- * @param listenerType the type of listener to add
- */
- private static void addListener(Context context, Class> listenerType) {
- if (listenerType == null) {
- ReflectionUtils.invokeMethod(ClassUtils.getMethod(context.getClass(),
- "addApplicationListener", String.class), context, WS_LISTENER);
+ /**
+ * Instead of registering the WsSci directly as a ServletContainerInitializer, we
+ * use the ApplicationListener provided by Tomcat. Unfortunately the
+ * ApplicationListener class moved packages in Tomcat 8 and been deleted in 8.0.8
+ * so we have to use reflection.
+ * @param context the current context
+ * @param listenerType the type of listener to add
+ */
+ private static void addListener(Context context, Class> listenerType) {
+ if (listenerType == null) {
+ ReflectionUtils.invokeMethod(ClassUtils.getMethod(context.getClass(),
+ "addApplicationListener", String.class), context, WS_LISTENER);
- }
- else {
- Object instance = BeanUtils.instantiateClass(
- ClassUtils.getConstructorIfAvailable(listenerType, String.class,
- boolean.class), WS_LISTENER, false);
- ReflectionUtils.invokeMethod(ClassUtils.getMethod(context.getClass(),
- "addApplicationListener", listenerType), context, instance);
+ }
+ else {
+ Object instance = BeanUtils.instantiateClass(ClassUtils
+ .getConstructorIfAvailable(listenerType, String.class,
+ boolean.class), WS_LISTENER, false);
+ ReflectionUtils.invokeMethod(ClassUtils.getMethod(context.getClass(),
+ "addApplicationListener", listenerType), context, instance);
+ }
}
}
+
+ @Configuration
+ @ConditionalOnClass(WebSocketServerContainerInitializer.class)
+ static class JettyWebSocketConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean(name = "websocketContainerCustomizer")
+ public EmbeddedServletContainerCustomizer websocketContainerCustomizer() {
+ return new WebSocketContainerCustomizer(
+ JettyEmbeddedServletContainerFactory.class) {
+
+ @Override
+ protected void doCustomize(JettyEmbeddedServletContainerFactory container) {
+ container.addConfigurations(new AbstractConfiguration() {
+
+ @Override
+ public void configure(WebAppContext context) throws Exception {
+ WebSocketServerContainerInitializer.configureContext(context);
+ }
+
+ });
+ }
+
+ };
+ }
+
+ }
+
+ abstract static class WebSocketContainerCustomizer
+ implements EmbeddedServletContainerCustomizer {
+
+ private Log logger = LogFactory.getLog(getClass());
+
+ private final Class containerType;
+
+ protected WebSocketContainerCustomizer(Class containerType) {
+ this.containerType = containerType;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void customize(ConfigurableEmbeddedServletContainer container) {
+ if (container instanceof NonEmbeddedServletContainerFactory) {
+ this.logger
+ .info("NonEmbeddedServletContainerFactory detected. Websockets "
+ + "support should be native so this normally is not a problem.");
+ return;
+ }
+ if (this.containerType.isAssignableFrom(container.getClass())) {
+ doCustomize((T) container);
+ }
+ }
+
+ protected abstract void doCustomize(T container);
+
+ }
+
}
diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnJndiTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnJndiTests.java
index 460390e707..1049479127 100644
--- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnJndiTests.java
+++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnJndiTests.java
@@ -16,6 +16,10 @@
package org.springframework.boot.autoconfigure.condition;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
@@ -27,6 +31,7 @@ import javax.naming.spi.InitialContextFactory;
import org.hamcrest.Matcher;
import org.junit.After;
+import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.test.EnvironmentTestUtils;
import org.springframework.context.ConfigurableApplicationContext;
@@ -47,15 +52,25 @@ import static org.mockito.Mockito.mock;
*
* @author Stephane Nicoll
* @author Phillip Webb
+ * @author Andy Wilkinson
*/
public class ConditionalOnJndiTests {
+ private ClassLoader threadContextClassLoader;
+
private String initialContextFactory;
private ConfigurableApplicationContext context;
private MockableOnJndi condition = new MockableOnJndi();
+ @Before
+ public void setupThreadContextClassLoader() {
+ this.threadContextClassLoader = Thread.currentThread().getContextClassLoader();
+ Thread.currentThread().setContextClassLoader(
+ new JndiPropertiesHidingClassLoader(getClass().getClassLoader()));
+ }
+
@After
public void close() {
TestableInitialContextFactory.clearAll();
@@ -69,6 +84,7 @@ public class ConditionalOnJndiTests {
if (this.context != null) {
this.context.close();
}
+ Thread.currentThread().setContextClassLoader(this.threadContextClassLoader);
}
@Test
@@ -255,4 +271,27 @@ public class ConditionalOnJndiTests {
}
}
+ /**
+ * Used as the thread context classloader to prevent jndi.properties resources found
+ * on the classpath from triggering configuration of an InitialContextFactory that is
+ * outside the control of these tests.
+ */
+ private static class JndiPropertiesHidingClassLoader extends ClassLoader {
+
+ public JndiPropertiesHidingClassLoader(ClassLoader parent) {
+ super(parent);
+ }
+
+ @Override
+ public Enumeration getResources(String name) throws IOException {
+ if ("jndi.properties".equals(name)) {
+ return Collections.enumeration(Collections. emptyList());
+ }
+ else {
+ return super.getResources(name);
+ }
+ }
+
+ }
+
}
diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/WebSocketAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/WebSocketAutoConfigurationTests.java
new file mode 100644
index 0000000000..09f04309e4
--- /dev/null
+++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/WebSocketAutoConfigurationTests.java
@@ -0,0 +1,108 @@
+/*
+ * 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 org.springframework.boot.autoconfigure.websocket;
+
+import javax.websocket.server.ServerContainer;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext;
+import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizerBeanPostProcessor;
+import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
+import org.springframework.boot.context.embedded.jetty.JettyEmbeddedServletContainerFactory;
+import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Tests for {@link WebSocketAutoConfiguration}
+ *
+ * @author Andy Wilkinson
+ */
+public class WebSocketAutoConfigurationTests {
+
+ private AnnotationConfigEmbeddedWebApplicationContext context;
+
+ @Before
+ public void createContext() {
+ this.context = new AnnotationConfigEmbeddedWebApplicationContext();
+ }
+
+ @After
+ public void close() {
+ if (this.context != null) {
+ this.context.close();
+ }
+ }
+
+ @Test
+ public void tomcatServerContainerIsAvailableFromTheServletContext() {
+ serverContainerIsAvailableFromTheServletContext(TomcatConfiguration.class,
+ WebSocketAutoConfiguration.TomcatWebSocketConfiguration.class);
+ }
+
+ @Test
+ public void jettyServerContainerIsAvailableFromTheServletContext() {
+ serverContainerIsAvailableFromTheServletContext(JettyConfiguration.class,
+ WebSocketAutoConfiguration.JettyWebSocketConfiguration.class);
+ }
+
+ private void serverContainerIsAvailableFromTheServletContext(
+ Class>... configuration) {
+ this.context.register(configuration);
+ this.context.refresh();
+ Object serverContainer = this.context.getServletContext().getAttribute(
+ "javax.websocket.server.ServerContainer");
+ assertThat(serverContainer, is(instanceOf(ServerContainer.class)));
+
+ }
+
+ static class CommonConfiguration {
+
+ @Bean
+ public EmbeddedServletContainerCustomizerBeanPostProcessor embeddedServletContainerCustomizerBeanPostProcessor() {
+ return new EmbeddedServletContainerCustomizerBeanPostProcessor();
+ }
+
+ }
+
+ @Configuration
+ static class TomcatConfiguration extends CommonConfiguration {
+
+ @Bean
+ public EmbeddedServletContainerFactory servletContainerFactory() {
+ return new TomcatEmbeddedServletContainerFactory();
+ }
+
+ }
+
+ @Configuration
+ static class JettyConfiguration extends CommonConfiguration {
+
+ @Bean
+ public EmbeddedServletContainerFactory servletContainerFactory() {
+ return new JettyEmbeddedServletContainerFactory();
+ }
+
+ }
+
+}
diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml
index 3154aab7ac..2a6d74733f 100644
--- a/spring-boot-dependencies/pom.xml
+++ b/spring-boot-dependencies/pom.xml
@@ -925,6 +925,16 @@
jetty-webapp${jetty.version}
+
+ org.eclipse.jetty.websocket
+ javax-websocket-server-impl
+ ${jetty.version}
+
+
+ org.eclipse.jetty.websocket
+ websocket-server
+ ${jetty.version}
+ org.flywaydbflyway-core
diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-cli.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-cli.adoc
index 2872d2a096..287d56d3ea 100644
--- a/spring-boot-docs/src/main/asciidoc/spring-boot-cli.adoc
+++ b/spring-boot-docs/src/main/asciidoc/spring-boot-cli.adoc
@@ -381,7 +381,7 @@ capabilities of the service using the `--list` flag
actuator - Actuator: Production ready features to help you monitor and manage your application
...
web - Web: Support for full-stack web development, including Tomcat and spring-webmvc
- websocket - Websocket: Support for websocket development with Tomcat
+ websocket - Websocket: Support for WebSocket development
ws - WS: Support for Spring Web Services
Available project types:
diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc
index 9ad68ac2e1..13d1a9e915 100644
--- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc
+++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc
@@ -2550,6 +2550,18 @@ result of a {spring-reference}/#expressions[SpEL expression].
+[[boot-features-websockets]]
+== WebSockets
+Spring Boot provides WebSockets auto-configuration for embedded Tomcat (7 and 8) and
+embedded Jetty 9. If you're deploying a war file to a standalone container, Spring Boot
+assumes that the container will be responsible for the configuration of its WebSocket
+support.
+
+Spring Framework provides {spring-reference}/#websocket[rich WebSocket support] that can
+be easily accessed via the `spring-boot-starter-websocket` module.
+
+
+
[[boot-features-whats-next]]
== What to read next
If you want to learn more about any of the classes discussed in this section you can
diff --git a/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc b/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc
index 64f62a23c8..a9ba333c97 100644
--- a/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc
+++ b/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc
@@ -310,7 +310,7 @@ and Hibernate.
|Support for full-stack web development, including Tomcat and `spring-webmvc`.
|`spring-boot-starter-websocket`
-|Support for websocket development with Tomcat.
+|Support for WebSocket development.
|`spring-boot-starter-ws`
|Support for Spring Web Services
diff --git a/spring-boot-integration-tests/src/test/java/org/springframework/boot/gradle/WarPackagingTests.java b/spring-boot-integration-tests/src/test/java/org/springframework/boot/gradle/WarPackagingTests.java
index e61710424c..c135e2741e 100644
--- a/spring-boot-integration-tests/src/test/java/org/springframework/boot/gradle/WarPackagingTests.java
+++ b/spring-boot-integration-tests/src/test/java/org/springframework/boot/gradle/WarPackagingTests.java
@@ -57,7 +57,13 @@ public class WarPackagingTests {
"jetty-server-", "jetty-security-", "jetty-servlet-",
"jetty-webapp-", "javax.servlet.jsp-2", "javax.servlet.jsp-api-",
"javax.servlet.jsp.jstl-1.2.2", "javax.servlet.jsp.jstl-1.2.0",
- "javax.el-", "org.eclipse.jdt.core-", "jetty-jsp-"));
+ "javax.el-", "org.eclipse.jdt.core-", "jetty-jsp-", "websocket-api",
+ "javax.annotation-api", "jetty-plus", "javax-websocket-server-impl-",
+ "asm-", "javax.websocket-api-", "asm-tree-", "asm-commons-",
+ "websocket-common-", "jetty-annotations-",
+ "javax-websocket-client-impl-", "websocket-client-",
+ "websocket-server-", "jetty-jndi-", "jetty-xml-",
+ "websocket-servlet-"));
private static final String BOOT_VERSION = ManagedDependencies.get()
.find("spring-boot").getVersion();
diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml
index 0bda3a1d39..d5b6acda5b 100644
--- a/spring-boot-samples/pom.xml
+++ b/spring-boot-samples/pom.xml
@@ -73,9 +73,10 @@
spring-boot-sample-web-uispring-boot-sample-web-velocityspring-boot-sample-websocket
+ spring-boot-sample-websocket-jettyspring-boot-sample-wsspring-boot-sample-xml
-
+
diff --git a/spring-boot-samples/spring-boot-sample-jetty8-ssl/pom.xml b/spring-boot-samples/spring-boot-sample-jetty8-ssl/pom.xml
index f157d309fe..6cf97cce99 100644
--- a/spring-boot-samples/spring-boot-sample-jetty8-ssl/pom.xml
+++ b/spring-boot-samples/spring-boot-sample-jetty8-ssl/pom.xml
@@ -29,6 +29,12 @@
org.springframework.bootspring-boot-starter-jetty
+
+
+ org.eclipse.jetty.websocket
+ *
+
+ org.springframework
diff --git a/spring-boot-samples/spring-boot-sample-jetty8/pom.xml b/spring-boot-samples/spring-boot-sample-jetty8/pom.xml
index 81de7f9362..dcf3e19e16 100644
--- a/spring-boot-samples/spring-boot-sample-jetty8/pom.xml
+++ b/spring-boot-samples/spring-boot-sample-jetty8/pom.xml
@@ -29,6 +29,12 @@
org.springframework.bootspring-boot-starter-jetty
+
+
+ org.eclipse.jetty.websocket
+ *
+
+ org.springframework
diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty/pom.xml b/spring-boot-samples/spring-boot-sample-websocket-jetty/pom.xml
new file mode 100755
index 0000000000..abe845ed90
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/pom.xml
@@ -0,0 +1,55 @@
+
+
+ 4.0.0
+ spring-boot-sample-websocket-jetty
+
+
+ org.springframework.boot
+ spring-boot-samples
+ 1.2.0.BUILD-SNAPSHOT
+
+ Spring Boot WebSocket Jetty Sample
+ Spring Boot WebSocket Jetty 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-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-jetty/src/main/java/samples/websocket/client/GreetingService.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/client/GreetingService.java
new file mode 100644
index 0000000000..de25204e0c
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/client/GreetingService.java
@@ -0,0 +1,23 @@
+/*
+ * 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 samples.websocket.client;
+
+public interface GreetingService {
+
+ String getGreeting();
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/client/SimpleClientWebSocketHandler.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/client/SimpleClientWebSocketHandler.java
new file mode 100644
index 0000000000..a3bbfc2f1c
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/client/SimpleClientWebSocketHandler.java
@@ -0,0 +1,62 @@
+/*
+ * 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 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-jetty/src/main/java/samples/websocket/client/SimpleGreetingService.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/client/SimpleGreetingService.java
new file mode 100644
index 0000000000..6a1e994c81
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/client/SimpleGreetingService.java
@@ -0,0 +1,26 @@
+/*
+ * 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 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-jetty/src/main/java/samples/websocket/config/SampleWebSocketsApplication.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/config/SampleWebSocketsApplication.java
new file mode 100644
index 0000000000..2ae2ddfb56
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/config/SampleWebSocketsApplication.java
@@ -0,0 +1,89 @@
+/*
+ * 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 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 SampleWebSocketsApplication 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(SampleWebSocketsApplication.class);
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(SampleWebSocketsApplication.class, args);
+ }
+
+ @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();
+ }
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/echo/DefaultEchoService.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/echo/DefaultEchoService.java
new file mode 100644
index 0000000000..87da340e54
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/echo/DefaultEchoService.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 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-jetty/src/main/java/samples/websocket/echo/EchoService.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/echo/EchoService.java
new file mode 100644
index 0000000000..59c0f06012
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/echo/EchoService.java
@@ -0,0 +1,23 @@
+/*
+ * 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 samples.websocket.echo;
+
+public interface EchoService {
+
+ String getMessage(String message);
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/echo/EchoWebSocketHandler.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/echo/EchoWebSocketHandler.java
new file mode 100644
index 0000000000..4f01f1288e
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/echo/EchoWebSocketHandler.java
@@ -0,0 +1,61 @@
+/*
+ * 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 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-jetty/src/main/java/samples/websocket/reverse/ReverseWebSocketEndpoint.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/reverse/ReverseWebSocketEndpoint.java
new file mode 100644
index 0000000000..a7802edcd2
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/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-jetty/src/main/java/samples/websocket/snake/Direction.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/snake/Direction.java
new file mode 100644
index 0000000000..b5295270e2
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/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-jetty/src/main/java/samples/websocket/snake/Location.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/snake/Location.java
new file mode 100644
index 0000000000..55a5085340
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/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-jetty/src/main/java/samples/websocket/snake/Snake.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/snake/Snake.java
new file mode 100644
index 0000000000..1bba004732
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/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-jetty/src/main/java/samples/websocket/snake/SnakeTimer.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/snake/SnakeTimer.java
new file mode 100644
index 0000000000..a11b12901e
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/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-jetty/src/main/java/samples/websocket/snake/SnakeUtils.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/snake/SnakeUtils.java
new file mode 100644
index 0000000000..b114992ce8
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/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-jetty/src/main/java/samples/websocket/snake/SnakeWebSocketHandler.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/java/samples/websocket/snake/SnakeWebSocketHandler.java
new file mode 100644
index 0000000000..a278eb09be
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/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-jetty/src/main/resources/static/echo.html b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/resources/static/echo.html
new file mode 100644
index 0000000000..0ffca05b4d
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/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-jetty/src/main/resources/static/index.html b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/resources/static/index.html
new file mode 100644
index 0000000000..e2b76b6e44
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/resources/static/index.html
@@ -0,0 +1,32 @@
+
+
+
+
+ Apache Tomcat WebSocket Examples: Index
+
+
+
+