diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml index b0d670ebc2..2615b09a9a 100644 --- a/spring-boot-samples/pom.xml +++ b/spring-boot-samples/pom.xml @@ -26,6 +26,7 @@ spring-boot-sample-traditional spring-boot-sample-web-static spring-boot-sample-web-ui + spring-boot-sample-websocket spring-boot-sample-xml diff --git a/spring-boot-samples/spring-boot-sample-websocket/pom.xml b/spring-boot-samples/spring-boot-sample-websocket/pom.xml new file mode 100644 index 0000000000..bddfd5fc57 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + spring-boot-sample-websocket + war + + + org.springframework.boot + spring-boot-starter-parent + 0.5.0.BUILD-SNAPSHOT + + + + 1.7 + 8.0-SNAPSHOT + org.springframework.boot.samples.websocket.config.ApplicationConfiguration + + + + + + org.springframework.boot + spring-boot-starter-websocket + ${spring.boot.version} + + + + org.eclipse.jetty.websocket + websocket-client + 9.0.3.v20130506 + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + tomcat-snapshots + https://repository.apache.org/content/repositories/snapshots + + true + + + false + + + + + diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/client/GreetingService.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/client/GreetingService.java new file mode 100644 index 0000000000..1b6793f1f8 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/client/GreetingService.java @@ -0,0 +1,22 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.samples.websocket.client; + +public interface GreetingService { + + String getGreeting(); + +} diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/client/SimpleClientWebSocketHandler.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/client/SimpleClientWebSocketHandler.java new file mode 100644 index 0000000000..4b8ca4a838 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/client/SimpleClientWebSocketHandler.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.samples.websocket.client; + +import java.util.concurrent.CountDownLatch; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.adapter.TextWebSocketHandlerAdapter; + +public class SimpleClientWebSocketHandler extends TextWebSocketHandlerAdapter { + + protected Log logger = LogFactory.getLog(SimpleClientWebSocketHandler.class); + + private final GreetingService greetingService; + + private CountDownLatch latch; + + @Autowired + public SimpleClientWebSocketHandler(GreetingService greetingService, CountDownLatch latch) { + this.greetingService = greetingService; + this.latch = latch; + } + + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + TextMessage message = new TextMessage(this.greetingService.getGreeting()); + session.sendMessage(message); + } + + @Override + public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + logger.info("Received: " + message + " (" + latch.getCount() + ")"); + session.close(); + latch.countDown(); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/client/SimpleGreetingService.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/client/SimpleGreetingService.java new file mode 100644 index 0000000000..726dd879f4 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/client/SimpleGreetingService.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.samples.websocket.client; + +public class SimpleGreetingService implements GreetingService { + + @Override + public String getGreeting() { + return "Hello world!"; + } + +} diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/config/SampleWebSocketsApplication.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/config/SampleWebSocketsApplication.java new file mode 100644 index 0000000000..f8f2aad3a5 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/config/SampleWebSocketsApplication.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.samples.websocket.config; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.websocket.server.WsSci; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; +import org.springframework.boot.samples.websocket.client.GreetingService; +import org.springframework.boot.samples.websocket.client.SimpleGreetingService; +import org.springframework.boot.samples.websocket.echo.DefaultEchoService; +import org.springframework.boot.samples.websocket.echo.EchoService; +import org.springframework.boot.samples.websocket.echo.EchoWebSocketHandler; +import org.springframework.boot.samples.websocket.snake.SnakeWebSocketHandler; +import org.springframework.boot.web.SpringServletInitializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler; +import org.springframework.web.socket.sockjs.SockJsService; +import org.springframework.web.socket.sockjs.support.DefaultSockJsService; +import org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler; +import org.springframework.web.socket.support.PerConnectionWebSocketHandler; + +@Configuration +public class SampleWebSocketsApplication extends SpringServletInitializer { + + @Override + protected Class[] getConfigClasses() { + return new Class[] { SampleWebSocketsApplication.class }; + } + + public static void main(String[] args) { + SpringApplication.run(SampleWebSocketsApplication.class, args); + } + + @ConditionalOnClass(Tomcat.class) + @Configuration + @EnableAutoConfiguration + protected static class InitializationConfiguration { + @Bean + public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() { + TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory() { + @Override + protected void postProcessContext(Context context) { + context.addServletContainerInitializer(new WsSci(), null); + } + }; + return factory; + } + } + + @Bean + public EchoService echoService() { + return new DefaultEchoService("Did you say \"%s\"?"); + } + + @Bean + public GreetingService greetingService() { + return new SimpleGreetingService(); + } + + @Bean + public SimpleUrlHandlerMapping handlerMapping() { + + SockJsService sockJsService = new DefaultSockJsService(sockJsTaskScheduler()); + + Map urlMap = new HashMap(); + + urlMap.put("/echo", new WebSocketHttpRequestHandler(echoWebSocketHandler())); + urlMap.put("/snake", new WebSocketHttpRequestHandler(snakeWebSocketHandler())); + + urlMap.put("/sockjs/echo/**", new SockJsHttpRequestHandler(sockJsService, echoWebSocketHandler())); + urlMap.put("/sockjs/snake/**", new SockJsHttpRequestHandler(sockJsService, snakeWebSocketHandler())); + + SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping(); + handlerMapping.setOrder(-1); + handlerMapping.setUrlMap(urlMap); + + return handlerMapping; + } + + @Bean + public DispatcherServlet dispatcherServlet() { + DispatcherServlet servlet = new DispatcherServlet(); + servlet.setDispatchOptionsRequest(true); + return servlet; + } + + @Bean + public WebSocketHandler echoWebSocketHandler() { + return new PerConnectionWebSocketHandler(EchoWebSocketHandler.class); + } + + @Bean + public WebSocketHandler snakeWebSocketHandler() { + return new SnakeWebSocketHandler(); + } + + @Bean + public ThreadPoolTaskScheduler sockJsTaskScheduler() { + ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); + taskScheduler.setThreadNamePrefix("SockJS-"); + return taskScheduler; + } +} diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/echo/DefaultEchoService.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/echo/DefaultEchoService.java new file mode 100644 index 0000000000..bfc3b32474 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/echo/DefaultEchoService.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.samples.websocket.echo; + +public class DefaultEchoService implements EchoService { + + private final String echoFormat; + + public DefaultEchoService(String echoFormat) { + this.echoFormat = (echoFormat != null) ? echoFormat : "%s"; + } + + @Override + public String getMessage(String message) { + return String.format(this.echoFormat, message); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/echo/EchoService.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/echo/EchoService.java new file mode 100644 index 0000000000..60751cccbb --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/echo/EchoService.java @@ -0,0 +1,22 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.samples.websocket.echo; + +public interface EchoService { + + String getMessage(String message); + +} diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/echo/EchoWebSocketHandler.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/echo/EchoWebSocketHandler.java new file mode 100644 index 0000000000..fe76ee9686 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/echo/EchoWebSocketHandler.java @@ -0,0 +1,43 @@ +package org.springframework.boot.samples.websocket.echo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.adapter.TextWebSocketHandlerAdapter; + +/** + * Echo messages by implementing a Spring {@link WebSocketHandler} abstraction. + */ +public class EchoWebSocketHandler extends TextWebSocketHandlerAdapter { + + private static Logger logger = LoggerFactory.getLogger(EchoWebSocketHandler.class); + + private final EchoService echoService; + + @Autowired + public EchoWebSocketHandler(EchoService echoService) { + this.echoService = echoService; + } + + @Override + public void afterConnectionEstablished(WebSocketSession session) { + logger.debug("Opened new session in instance " + this); + } + + @Override + public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + String echoMessage = this.echoService.getMessage(message.getPayload()); + logger.debug(echoMessage); + session.sendMessage(new TextMessage(echoMessage)); + } + + @Override + public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { + session.close(CloseStatus.SERVER_ERROR); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/Direction.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/Direction.java new file mode 100644 index 0000000000..60e18f7b03 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/Direction.java @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.samples.websocket.snake; + +public enum Direction { + NONE, NORTH, SOUTH, EAST, WEST +} diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/Location.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/Location.java new file mode 100644 index 0000000000..493b9c69ae --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/Location.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.samples.websocket.snake; + +import org.springframework.boot.samples.websocket.snake.Direction; + + +public class Location { + + public int x; + public int y; + public static final int GRID_SIZE = 10; + public static final int PLAYFIELD_HEIGHT = 480; + public static final int PLAYFIELD_WIDTH = 640; + + public Location(int x, int y) { + this.x = x; + this.y = y; + } + + public Location getAdjacentLocation(Direction direction) { + switch (direction) { + case NORTH: + return new Location(x, y - Location.GRID_SIZE); + case SOUTH: + return new Location(x, y + Location.GRID_SIZE); + case EAST: + return new Location(x + Location.GRID_SIZE, y); + case WEST: + return new Location(x - Location.GRID_SIZE, y); + case NONE: + // fall through + default: + return this; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Location location = (Location) o; + + if (x != location.x) return false; + if (y != location.y) return false; + + return true; + } + + @Override + public int hashCode() { + int result = x; + result = 31 * result + y; + return result; + } +} diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/Snake.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/Snake.java new file mode 100644 index 0000000000..0ccf717281 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/Snake.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.samples.websocket.snake; + +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Deque; + +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + + +public class Snake { + + private static final int DEFAULT_LENGTH = 5; + + private final int id; + private final WebSocketSession session; + + private Direction direction; + private int length = DEFAULT_LENGTH; + private Location head; + private final Deque tail = new ArrayDeque(); + private final String hexColor; + + public Snake(int id, WebSocketSession session) { + this.id = id; + this.session = session; + this.hexColor = SnakeUtils.getRandomHexColor(); + resetState(); + } + + private void resetState() { + this.direction = Direction.NONE; + this.head = SnakeUtils.getRandomLocation(); + this.tail.clear(); + this.length = DEFAULT_LENGTH; + } + + private synchronized void kill() throws Exception { + resetState(); + sendMessage("{'type': 'dead'}"); + } + + private synchronized void reward() throws Exception { + length++; + sendMessage("{'type': 'kill'}"); + } + + + protected void sendMessage(String msg) throws Exception { + session.sendMessage(new TextMessage(msg)); + } + + public synchronized void update(Collection snakes) throws Exception { + Location nextLocation = head.getAdjacentLocation(direction); + if (nextLocation.x >= SnakeUtils.PLAYFIELD_WIDTH) { + nextLocation.x = 0; + } + if (nextLocation.y >= SnakeUtils.PLAYFIELD_HEIGHT) { + nextLocation.y = 0; + } + if (nextLocation.x < 0) { + nextLocation.x = SnakeUtils.PLAYFIELD_WIDTH; + } + if (nextLocation.y < 0) { + nextLocation.y = SnakeUtils.PLAYFIELD_HEIGHT; + } + if (direction != Direction.NONE) { + tail.addFirst(head); + if (tail.size() > length) { + tail.removeLast(); + } + head = nextLocation; + } + + handleCollisions(snakes); + } + + private void handleCollisions(Collection snakes) throws Exception { + for (Snake snake : snakes) { + boolean headCollision = id != snake.id && snake.getHead().equals(head); + boolean tailCollision = snake.getTail().contains(head); + if (headCollision || tailCollision) { + kill(); + if (id != snake.id) { + snake.reward(); + } + } + } + } + + public synchronized Location getHead() { + return head; + } + + public synchronized Collection getTail() { + return tail; + } + + public synchronized void setDirection(Direction direction) { + this.direction = direction; + } + + public synchronized String getLocationsJson() { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("{x: %d, y: %d}", + Integer.valueOf(head.x), Integer.valueOf(head.y))); + for (Location location : tail) { + sb.append(','); + sb.append(String.format("{x: %d, y: %d}", + Integer.valueOf(location.x), Integer.valueOf(location.y))); + } + return String.format("{'id':%d,'body':[%s]}", + Integer.valueOf(id), sb.toString()); + } + + public int getId() { + return id; + } + + public String getHexColor() { + return hexColor; + } +} diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/SnakeTimer.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/SnakeTimer.java new file mode 100644 index 0000000000..e0c70a6b07 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/SnakeTimer.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.samples.websocket.snake; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Sets up the timer for the multi-player snake game WebSocket example. + */ +public class SnakeTimer { + + private static final Log log = + LogFactory.getLog(SnakeTimer.class); + + private static Timer gameTimer = null; + + private static final long TICK_DELAY = 100; + + private static final ConcurrentHashMap snakes = + new ConcurrentHashMap(); + + public static synchronized void addSnake(Snake snake) { + if (snakes.size() == 0) { + startTimer(); + } + snakes.put(Integer.valueOf(snake.getId()), snake); + } + + + public static Collection getSnakes() { + return Collections.unmodifiableCollection(snakes.values()); + } + + + public static synchronized void removeSnake(Snake snake) { + snakes.remove(Integer.valueOf(snake.getId())); + if (snakes.size() == 0) { + stopTimer(); + } + } + + + public static void tick() throws Exception { + StringBuilder sb = new StringBuilder(); + for (Iterator iterator = SnakeTimer.getSnakes().iterator(); + iterator.hasNext();) { + Snake snake = iterator.next(); + snake.update(SnakeTimer.getSnakes()); + sb.append(snake.getLocationsJson()); + if (iterator.hasNext()) { + sb.append(','); + } + } + broadcast(String.format("{'type': 'update', 'data' : [%s]}", + sb.toString())); + } + + public static void broadcast(String message) throws Exception { + for (Snake snake : SnakeTimer.getSnakes()) { + snake.sendMessage(message); + } + } + + + public static void startTimer() { + gameTimer = new Timer(SnakeTimer.class.getSimpleName() + " Timer"); + gameTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + try { + tick(); + } catch (Throwable e) { + log.error("Caught to prevent timer from shutting down", e); + } + } + }, TICK_DELAY, TICK_DELAY); + } + + + public static void stopTimer() { + if (gameTimer != null) { + gameTimer.cancel(); + } + } +} diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/SnakeUtils.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/SnakeUtils.java new file mode 100644 index 0000000000..81527238fd --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/SnakeUtils.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.samples.websocket.snake; + +import java.awt.Color; +import java.util.Random; + +public class SnakeUtils { + + public static final int PLAYFIELD_WIDTH = 640; + public static final int PLAYFIELD_HEIGHT = 480; + public static final int GRID_SIZE = 10; + + private static final Random random = new Random(); + + + public static String getRandomHexColor() { + float hue = random.nextFloat(); + // sat between 0.1 and 0.3 + float saturation = (random.nextInt(2000) + 1000) / 10000f; + float luminance = 0.9f; + Color color = Color.getHSBColor(hue, saturation, luminance); + return '#' + Integer.toHexString( + (color.getRGB() & 0xffffff) | 0x1000000).substring(1); + } + + + public static Location getRandomLocation() { + int x = roundByGridSize(random.nextInt(PLAYFIELD_WIDTH)); + int y = roundByGridSize(random.nextInt(PLAYFIELD_HEIGHT)); + return new Location(x, y); + } + + + private static int roundByGridSize(int value) { + value = value + (GRID_SIZE / 2); + value = value / GRID_SIZE; + value = value * GRID_SIZE; + return value; + } + +} diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/SnakeWebSocketHandler.java b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/SnakeWebSocketHandler.java new file mode 100644 index 0000000000..f2eba5c349 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/java/org/springframework/boot/samples/websocket/snake/SnakeWebSocketHandler.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.samples.websocket.snake; + +import java.awt.Color; +import java.util.Iterator; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.adapter.TextWebSocketHandlerAdapter; + +public class SnakeWebSocketHandler extends TextWebSocketHandlerAdapter { + + public static final int PLAYFIELD_WIDTH = 640; + public static final int PLAYFIELD_HEIGHT = 480; + public static final int GRID_SIZE = 10; + + private static final AtomicInteger snakeIds = new AtomicInteger(0); + private static final Random random = new Random(); + + + private final int id; + private Snake snake; + + public static String getRandomHexColor() { + float hue = random.nextFloat(); + // sat between 0.1 and 0.3 + float saturation = (random.nextInt(2000) + 1000) / 10000f; + float luminance = 0.9f; + Color color = Color.getHSBColor(hue, saturation, luminance); + return '#' + Integer.toHexString( + (color.getRGB() & 0xffffff) | 0x1000000).substring(1); + } + + + public static Location getRandomLocation() { + int x = roundByGridSize(random.nextInt(PLAYFIELD_WIDTH)); + int y = roundByGridSize(random.nextInt(PLAYFIELD_HEIGHT)); + return new Location(x, y); + } + + + private static int roundByGridSize(int value) { + value = value + (GRID_SIZE / 2); + value = value / GRID_SIZE; + value = value * GRID_SIZE; + return value; + } + + public SnakeWebSocketHandler() { + this.id = snakeIds.getAndIncrement(); + } + + + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + this.snake = new Snake(id, session); + SnakeTimer.addSnake(snake); + StringBuilder sb = new StringBuilder(); + for (Iterator iterator = SnakeTimer.getSnakes().iterator(); + iterator.hasNext();) { + Snake snake = iterator.next(); + sb.append(String.format("{id: %d, color: '%s'}", + Integer.valueOf(snake.getId()), snake.getHexColor())); + if (iterator.hasNext()) { + sb.append(','); + } + } + SnakeTimer.broadcast(String.format("{'type': 'join','data':[%s]}", + sb.toString())); + } + + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + String payload = message.getPayload(); + if ("west".equals(payload)) { + snake.setDirection(Direction.WEST); + } else if ("north".equals(payload)) { + snake.setDirection(Direction.NORTH); + } else if ("east".equals(payload)) { + snake.setDirection(Direction.EAST); + } else if ("south".equals(payload)) { + snake.setDirection(Direction.SOUTH); + } + } + + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + SnakeTimer.removeSnake(snake); + SnakeTimer.broadcast(String.format("{'type': 'leave', 'id': %d}", + Integer.valueOf(id))); + } +} diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/echo.html b/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/echo.html new file mode 100644 index 0000000000..cbaef719dd --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/echo.html @@ -0,0 +1,140 @@ + + + + + Apache Tomcat WebSocket Examples: Echo + + + + + +
+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/index.html b/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/index.html new file mode 100644 index 0000000000..39069b15d7 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/index.html @@ -0,0 +1,31 @@ + + + + + Apache Tomcat WebSocket Examples: Index + + + +

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/src/main/resources/static/snake.html b/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/snake.html new file mode 100644 index 0000000000..b00debf570 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/src/main/resources/static/snake.html @@ -0,0 +1,259 @@ + + + + + + Apache Tomcat WebSocket Examples: Multiplayer Snake + + + + + +
+ +
+
+
+
+ + + diff --git a/spring-boot-samples/spring-boot-sample-websocket/src/test/java/org/springframework/boot/samples/websocket/echo/StandardClientApp.java b/spring-boot-samples/spring-boot-sample-websocket/src/test/java/org/springframework/boot/samples/websocket/echo/StandardClientApp.java new file mode 100644 index 0000000000..76c5dfd513 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-websocket/src/test/java/org/springframework/boot/samples/websocket/echo/StandardClientApp.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.samples.websocket.echo; + +import static org.junit.Assert.assertEquals; + +import java.util.concurrent.CountDownLatch; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Test; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.samples.websocket.client.GreetingService; +import org.springframework.boot.samples.websocket.client.SimpleClientWebSocketHandler; +import org.springframework.boot.samples.websocket.client.SimpleGreetingService; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.client.WebSocketConnectionManager; +import org.springframework.web.socket.client.endpoint.StandardWebSocketClient; + +public class StandardClientApp { + + private static Log logger = LogFactory.getLog(StandardClientApp.class); + + private static final String WS_URI = "ws://localhost:8080/echo"; + + @Test + public void runAndWait() throws Exception { + ApplicationContext context = SpringApplication.run(ClientConfiguration.class, "--spring.main.web_environment=false"); + assertEquals(0, context.getBean(ClientConfiguration.class).latch.getCount()); + } + + @Configuration + static class ClientConfiguration implements CommandLineRunner { + + private CountDownLatch latch = new CountDownLatch(1); + + @Override + public void run(String... args) throws Exception { + logger.info("Waiting for response: latch=" + latch.getCount()); + latch.await(); + logger.info("Got response: latch=" + latch.getCount()); + } + + @Bean + public WebSocketConnectionManager wsConnectionManager() { + + WebSocketConnectionManager manager = new WebSocketConnectionManager(client(), handler(), WS_URI); + manager.setAutoStartup(true); + + return manager; + } + + @Bean + public StandardWebSocketClient client() { + return new StandardWebSocketClient(); + } + + @Bean + public SimpleClientWebSocketHandler handler() { + return new SimpleClientWebSocketHandler(greetingService(), latch); + } + + @Bean + public GreetingService greetingService() { + return new SimpleGreetingService(); + } + } + +}