diff --git a/spring-boot-autoconfigure/pom.xml b/spring-boot-autoconfigure/pom.xml index ccab61becd1..2f7f0401e5f 100644 --- a/spring-boot-autoconfigure/pom.xml +++ b/spring-boot-autoconfigure/pom.xml @@ -160,6 +160,11 @@ jetty-webapp true + + org.eclipse.jetty.websocket + javax-websocket-server-impl + true + io.undertow undertow-servlet @@ -403,6 +408,11 @@ test-jar test + + org.apache.tomcat.embed + tomcat-embed-websocket + test + org.hsqldb hsqldb 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 610f6edd23d..a214b9d534b 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 460390e7073..10494791277 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 00000000000..09f04309e44 --- /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 3154aab7ace..2a6d74733f0 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.flywaydb flyway-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 2872d2a096d..287d56d3ea0 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 9ad68ac2e1a..13d1a9e915a 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 64f62a23c8f..a9ba333c974 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 e61710424c5..c135e2741e3 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 0bda3a1d39a..d5b6acda5bc 100644 --- a/spring-boot-samples/pom.xml +++ b/spring-boot-samples/pom.xml @@ -73,9 +73,10 @@ spring-boot-sample-web-ui spring-boot-sample-web-velocity spring-boot-sample-websocket + spring-boot-sample-websocket-jetty spring-boot-sample-ws spring-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 f157d309fe1..6cf97cce99f 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.boot spring-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 81de7f93628..dcf3e19e169 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.boot spring-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 00000000000..abe845ed90e --- /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 00000000000..de25204e0c1 --- /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 00000000000..a3bbfc2f1cf --- /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 00000000000..6a1e994c81c --- /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 00000000000..2ae2ddfb56c --- /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 00000000000..87da340e54a --- /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 00000000000..59c0f060128 --- /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 00000000000..4f01f1288e4 --- /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 00000000000..a7802edcd22 --- /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 00000000000..b5295270e26 --- /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 00000000000..55a50853405 --- /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 00000000000..1bba0047323 --- /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 00000000000..a11b12901e5 --- /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 00000000000..b114992ce89 --- /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 00000000000..a278eb09bee --- /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 00000000000..0ffca05b4d0 --- /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 + + + + + +

Seems your browser doesn't support Javascript! Websockets rely on Javascript being enabled. Please enable + Javascript and reload this page!

+
+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+
+
+
+
+ + \ 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 00000000000..e2b76b6e445 --- /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 + + + +

Please select the sample you would like to try.

+ + + \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/resources/static/reverse.html b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/resources/static/reverse.html new file mode 100644 index 00000000000..96aaf25de7f --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/resources/static/reverse.html @@ -0,0 +1,140 @@ + + + + + WebSocket Examples: Reverse + + + + + +
+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/resources/static/snake.html b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/resources/static/snake.html new file mode 100644 index 00000000000..5728fc01443 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/main/resources/static/snake.html @@ -0,0 +1,249 @@ + + + + + + Apache Tomcat WebSocket Examples: Multiplayer Snake + + + + + + +
+ +
+
+
+
+ + + diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/SampleWebSocketsApplicationTests.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/SampleWebSocketsApplicationTests.java new file mode 100644 index 00000000000..cff0db54907 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/SampleWebSocketsApplicationTests.java @@ -0,0 +1,138 @@ +/* + * 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; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.web.socket.client.WebSocketConnectionManager; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; + +import samples.websocket.client.GreetingService; +import samples.websocket.client.SimpleClientWebSocketHandler; +import samples.websocket.client.SimpleGreetingService; +import samples.websocket.config.SampleWebSocketsApplication; + +import static org.junit.Assert.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SampleWebSocketsApplication.class) +@WebAppConfiguration +@IntegrationTest("server.port:0") +@DirtiesContext +public class SampleWebSocketsApplicationTests { + + private static Log logger = LogFactory.getLog(SampleWebSocketsApplicationTests.class); + + @Value("${local.server.port}") + private int port = 1234; + + @Test + public void echoEndpoint() throws Exception { + ConfigurableApplicationContext context = new SpringApplicationBuilder( + ClientConfiguration.class, PropertyPlaceholderAutoConfiguration.class) + .properties( + "websocket.uri:ws://localhost:" + this.port + "/echo/websocket") + .run("--spring.main.web_environment=false"); + long count = context.getBean(ClientConfiguration.class).latch.getCount(); + AtomicReference messagePayloadReference = context + .getBean(ClientConfiguration.class).messagePayload; + context.close(); + assertEquals(0, count); + assertEquals("Did you say \"Hello world!\"?", messagePayloadReference.get()); + } + + @Test + public void reverseEndpoint() throws Exception { + ConfigurableApplicationContext context = new SpringApplicationBuilder( + ClientConfiguration.class, PropertyPlaceholderAutoConfiguration.class) + .properties("websocket.uri:ws://localhost:" + this.port + "/reverse") + .run("--spring.main.web_environment=false"); + long count = context.getBean(ClientConfiguration.class).latch.getCount(); + AtomicReference messagePayloadReference = context + .getBean(ClientConfiguration.class).messagePayload; + context.close(); + assertEquals(0, count); + assertEquals("Reversed: !dlrow olleH", messagePayloadReference.get()); + } + + @Configuration + static class ClientConfiguration implements CommandLineRunner { + + @Value("${websocket.uri}") + private String webSocketUri; + + private final CountDownLatch latch = new CountDownLatch(1); + + private final AtomicReference messagePayload = new AtomicReference(); + + @Override + public void run(String... args) throws Exception { + logger.info("Waiting for response: latch=" + this.latch.getCount()); + if (this.latch.await(10, TimeUnit.SECONDS)) { + logger.info("Got response: " + this.messagePayload.get()); + } + else { + logger.info("Response not received: latch=" + this.latch.getCount()); + } + } + + @Bean + public WebSocketConnectionManager wsConnectionManager() { + + WebSocketConnectionManager manager = new WebSocketConnectionManager(client(), + handler(), this.webSocketUri); + manager.setAutoStartup(true); + + return manager; + } + + @Bean + public StandardWebSocketClient client() { + return new StandardWebSocketClient(); + } + + @Bean + public SimpleClientWebSocketHandler handler() { + return new SimpleClientWebSocketHandler(greetingService(), this.latch, + this.messagePayload); + } + + @Bean + public GreetingService greetingService() { + return new SimpleGreetingService(); + } + } + +} diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/echo/CustomContainerWebSocketsApplicationTests.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/echo/CustomContainerWebSocketsApplicationTests.java new file mode 100644 index 00000000000..bcc020e6464 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/echo/CustomContainerWebSocketsApplicationTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package samples.websocket.echo; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; +import org.springframework.boot.context.embedded.jetty.JettyEmbeddedServletContainerFactory; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.util.SocketUtils; +import org.springframework.web.socket.client.WebSocketConnectionManager; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; + +import samples.websocket.client.GreetingService; +import samples.websocket.client.SimpleClientWebSocketHandler; +import samples.websocket.client.SimpleGreetingService; +import samples.websocket.config.SampleWebSocketsApplication; +import samples.websocket.echo.CustomContainerWebSocketsApplicationTests.CustomContainerConfiguration; + +import static org.junit.Assert.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = { SampleWebSocketsApplication.class, + CustomContainerConfiguration.class }) +@WebAppConfiguration +@IntegrationTest +@DirtiesContext +public class CustomContainerWebSocketsApplicationTests { + + private static Log logger = LogFactory + .getLog(CustomContainerWebSocketsApplicationTests.class); + + private static int PORT = SocketUtils.findAvailableTcpPort(); + + @Configuration + protected static class CustomContainerConfiguration { + @Bean + public EmbeddedServletContainerFactory embeddedServletContainerFactory() { + return new JettyEmbeddedServletContainerFactory("/ws", PORT); + } + } + + @Test + public void echoEndpoint() throws Exception { + ConfigurableApplicationContext context = new SpringApplicationBuilder( + ClientConfiguration.class, PropertyPlaceholderAutoConfiguration.class) + .properties("websocket.uri:ws://localhost:" + PORT + "/ws/echo/websocket") + .run("--spring.main.web_environment=false"); + long count = context.getBean(ClientConfiguration.class).latch.getCount(); + AtomicReference messagePayloadReference = context + .getBean(ClientConfiguration.class).messagePayload; + context.close(); + assertEquals(0, count); + assertEquals("Did you say \"Hello world!\"?", messagePayloadReference.get()); + } + + @Test + public void reverseEndpoint() throws Exception { + ConfigurableApplicationContext context = new SpringApplicationBuilder( + ClientConfiguration.class, PropertyPlaceholderAutoConfiguration.class) + .properties("websocket.uri:ws://localhost:" + PORT + "/ws/reverse").run( + "--spring.main.web_environment=false"); + long count = context.getBean(ClientConfiguration.class).latch.getCount(); + AtomicReference messagePayloadReference = context + .getBean(ClientConfiguration.class).messagePayload; + context.close(); + assertEquals(0, count); + assertEquals("Reversed: !dlrow olleH", messagePayloadReference.get()); + } + + @Configuration + static class ClientConfiguration implements CommandLineRunner { + + @Value("${websocket.uri}") + private String webSocketUri; + + private final CountDownLatch latch = new CountDownLatch(1); + + private final AtomicReference messagePayload = new AtomicReference(); + + @Override + public void run(String... args) throws Exception { + logger.info("Waiting for response: latch=" + this.latch.getCount()); + if (this.latch.await(10, TimeUnit.SECONDS)) { + logger.info("Got response: " + this.messagePayload.get()); + } + else { + logger.info("Response not received: latch=" + this.latch.getCount()); + } + } + + @Bean + public WebSocketConnectionManager wsConnectionManager() { + + WebSocketConnectionManager manager = new WebSocketConnectionManager(client(), + handler(), this.webSocketUri); + manager.setAutoStartup(true); + + return manager; + } + + @Bean + public StandardWebSocketClient client() { + return new StandardWebSocketClient(); + } + + @Bean + public SimpleClientWebSocketHandler handler() { + return new SimpleClientWebSocketHandler(greetingService(), this.latch, + this.messagePayload); + } + + @Bean + public GreetingService greetingService() { + return new SimpleGreetingService(); + } + } + +} diff --git a/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/snake/SnakeTimerTests.java b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/snake/SnakeTimerTests.java new file mode 100644 index 00000000000..9a8305e46bd --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket-jetty/src/test/java/samples/websocket/snake/SnakeTimerTests.java @@ -0,0 +1,40 @@ +/* + * 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.snake; + +import java.io.IOException; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; + +public class SnakeTimerTests { + + @Test + public void removeDysfunctionalSnakes() throws Exception { + Snake snake = mock(Snake.class); + willThrow(new IOException()).given(snake).sendMessage(anyString()); + SnakeTimer.addSnake(snake); + + SnakeTimer.broadcast(""); + assertThat(SnakeTimer.getSnakes().size(), is(0)); + } +} diff --git a/spring-boot-starters/spring-boot-starter-jetty/pom.xml b/spring-boot-starters/spring-boot-starter-jetty/pom.xml index 9519207f1a7..b0ee025a497 100644 --- a/spring-boot-starters/spring-boot-starter-jetty/pom.xml +++ b/spring-boot-starters/spring-boot-starter-jetty/pom.xml @@ -26,5 +26,13 @@ org.eclipse.jetty jetty-jsp
+ + org.eclipse.jetty.websocket + websocket-server + + + org.eclipse.jetty.websocket + javax-websocket-server-impl +