Răsfoiți Sursa

First draft

Craig Walls 12 ani în urmă
părinte
comite
1f14f98963

+ 142 - 0
README.ftl.md

@@ -0,0 +1,142 @@
+<#assign project_id="gs-rest-service">
+
+# Getting Started: Messaging with WebSocket and STOMP
+
+This guide walks you through creating a "hello world" STOMP messaging server with Spring. 
+
+What you'll build
+-----------------
+
+The server will accept a message carying the user's name. In response, it will push a greeting into a queue that the client is subscribed to.
+
+What you'll need
+----------------
+
+ - About 15 minutes
+ - Tomcat 8 ([Tomcat 8.0.0-RC1][tomcat8] is available)
+ - <@prereq_editor_jdk_buildtools/>
+
+
+## <@how_to_complete_this_guide/>
+
+
+<a name="scratch"></a>
+Set up the project
+------------------
+
+<@build_system_intro/>
+
+<@create_directory_structure_hello/>
+
+### Create a Maven POM
+
+    <@snippet path="pom.xml" prefix="initial"/>
+
+<@bootstrap_starter_pom_disclaimer/>
+
+
+<a name="initial"></a>
+Create a resource representation class
+--------------------------------------
+
+Now that you've set up the project and build system, you can create your STOMP message service.
+
+Begin the process by thinking about service interactions.
+
+The service will accept messages containing a name in a STOMP message whose body is a [JSON][u-json] object. If the name given is "Fred", then the message might look something like this:
+
+    {
+        "name": "Fred"
+    }
+
+To model the message carrying the name, you can create a plain old Java object with a `name` property and a corresponding `getName()` method:
+
+    <@snippet path="src/main/java/hello/HelloMessage.java" prefix="complete"/>
+
+Upon receiving the message and extracting the name, the service will process it by creating a greeting and publishing that greeting on a separate queue that the client is subscribed to. The greeting will also be a JSON object, which might look something like this:
+
+    {
+        "content": "Hello, Fred!"
+    }
+
+To model the greeting representation, you another plain old Java object with a `content` property and corresponding `getContent()` method:
+
+    <@snippet path="src/main/java/hello/Greeting.java" prefix="complete"/>
+
+> **Note:** As you see in steps below, Spring uses the [Jackson JSON][jackson] library to automatically marshal instances of type `Greeting` into JSON.
+
+Next, you'll create the controller to receive the hello message and send a greeting message.
+
+Create a message-handling controller
+------------------------------------
+
+In Spring's approach to working with STOMP messaging, STOMP messages can be handled by a controller. These components are easily identified by the [`@Controller`][AtController] annotation, and the `GreetingController` below is mapped to handle messages published on the "/app/hello" destination.
+
+    <@snippet path="src/main/java/hello/GreetingController.java" prefix="complete"/>
+
+This controller is concise and simple, but there's plenty going on under the hood. Let's break it down step by step.
+
+The `@MessageMapping` annotation ensures that if a message is published on the "/app/hello" destination, then the `greeting()` method will called.
+
+`@RequestBody` binds the payload of the message to a `HelloMessage` object which is passed into `greeting()`. 
+
+Internally, the implementation of the method simulates a processing delay by causing the thread to sleep for 3 seconds. This is to demonstrate that after the client sends a message, the server can take as long as it needs to process the message asynchronously.  The client may continue with whatever work it needs to do without waiting on the response.
+
+After the 3 second delay, the `greeting()` method creates a new `Greeting` object, setting its content to say "Hello" to the name from the `HelloMessage`. It then calls `convertAndSend()` on the injected `SimpMessageSendingOperations` to send the `Greeting` on the "/queue/greetings" destination.
+
+The `Greeting` object must be converted to JSON. Thanks to Spring's HTTP message converter support, you don't need to do this conversion manually. When you configure Spring for STOMP messaging, you'll inject `SimpMessagingTemplate` with an instance of [`MappingJackson2MessageConverter`][MappingJackson2MessageConverter]. It will be used to convert the `Greeting` instance to JSON.
+
+Configuring Spring for STOMP messaging
+--------------------------------------
+
+TODO: This is extremely ugly at the moment, with many beans in play. Rossen says that SPR-10835 will be resolved in time for RC1, so fill in this section then.
+
+
+Make the application executable
+-------------------------------
+
+In order to deploy the application to Tomcat, you'll need to add a bit more configuration.
+
+First, you'll need to configure Spring's [`DispatcherServlet`][DispatcherServlet] to serve static resources so that it will serve index.html. This can be done by creating a configuration class that overrides the `configureDefaultServletHandling()` method of `WebMvcConfigurerAdapter` and calls `enable()` on the given `DefaultServletHandlerConfigurer`:
+
+    <@snippet path="src/main/java/hello/WebConfig.java" prefix="complete"/>
+
+You'll also need to configure `DispatcherServlet`. This is most easily done by creating a class that extends `AbstractAnnotationConfigDispatcherServletInitializer`:
+
+    <@snippet path="src/main/java/hello/HelloServletInitializer.java" prefix="complete"/>
+
+Here, `DispatcherServlet` is mapped to "/". Also, `WebConfig` and `EndpointConfig` are specified, respectively, as the servlet and root configuration classes.
+
+Now you're ready to build and deploy the application to Tomcat 8. Start by building the WAR file:
+
+```sh
+mvn package
+```
+
+Then copy the WAR file to Tomcat 8's `trunk/output/webapps` directory. 
+
+Finally, restart Tomcat 8:
+
+```sh
+output/bin/shutdown.sh
+output/bin/startup.sh
+```
+
+After the application starts, point your browser at http://localhost:8080/gs-messaging-stomp-websocket and click the "Connect" button.
+
+Upon opening a connection, you will be asked for your name. Enter your name and click "Send". Your name will be sent to the server as a JSON message over STOMP. The server will send a message back with a "Hello" greeting that will be displayed on the page. At this point, you may choose to send another name or you can click the "Disconnect" button to close the connection.
+
+
+Summary
+-------
+
+Congratulations! You've just developed a STOMP-based messaging service with Spring. 
+
+
+[tomcat8]: http://tomcat.apache.org/download-80.cgi
+[u-rest]: /understanding/rest
+[u-json]: /understanding/json
+[jackson]: http://wiki.fasterxml.com/JacksonHome
+[MappingJackson2MessageConverter]: http://static.springsource.org/spring/docs/4.0.x/javadoc-api/org/springframework/messaging/support/converter/MappingJackson2MessageConverter.html
+[`AtController`]: http://static.springsource.org/spring/docs/current/javadoc-api/org/springframework/stereotype/Controller.html
+[`DispatcherServlet`]: http://static.springsource.org/spring/docs/current/javadoc-api/org/springframework/web/servlet/DispatcherServlet.html

+ 560 - 0
README.md

@@ -0,0 +1,560 @@
+
+# Getting Started: Messaging with WebSocket and STOMP
+
+This guide walks you through creating a "hello world" STOMP messaging server with Spring. 
+
+What you'll build
+-----------------
+
+The server will accept a message carying the user's name. In response, it will push a greeting into a queue that the client is subscribed to.
+
+What you'll need
+----------------
+
+ - About 15 minutes
+ - Tomcat 8 ([Tomcat 8.0.0-RC1][tomcat8] is available)
+ - A favorite text editor or IDE
+ - [JDK 6][jdk] or later
+ - [Maven 3.0][mvn] or later
+
+[jdk]: http://www.oracle.com/technetwork/java/javase/downloads/index.html
+[mvn]: http://maven.apache.org/download.cgi
+
+
+How to complete this guide
+--------------------------
+
+Like all Spring's [Getting Started guides](/guides/gs), you can start from scratch and complete each step, or you can bypass basic setup steps that are already familiar to you. Either way, you end up with working code.
+
+To **start from scratch**, move on to [Set up the project](#scratch).
+
+To **skip the basics**, do the following:
+
+ - [Download][zip] and unzip the source repository for this guide, or clone it using [git](/understanding/git):
+`git clone https://github.com/springframework-meta/gs-rest-service.git`
+ - cd into `gs-rest-service/initial`.
+ - Jump ahead to [Create a resource representation class](#initial).
+
+**When you're finished**, you can check your results against the code in `gs-rest-service/complete`.
+[zip]: https://github.com/springframework-meta/gs-rest-service/archive/master.zip
+
+
+<a name="scratch"></a>
+Set up the project
+------------------
+
+First you set up a basic build script. You can use any build system you like when building apps with Spring, but the code you need to work with [Maven](https://maven.apache.org) and [Gradle](http://gradle.org) is included here. If you're not familiar with either, refer to [Building Java Projects with Maven](/guides/gs/maven) or [Building Java Projects with Gradle](/guides/gs/gradle/).
+
+### Create the directory structure
+
+In a project directory of your choosing, create the following subdirectory structure; for example, with `mkdir -p src/main/java/hello` on *nix systems:
+
+    └── src
+        └── main
+            └── java
+                └── hello
+
+### Create a Maven POM
+
+`pom.xml`
+```xml
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.springframework.samples</groupId>
+    <artifactId>gs-messaging-stomp-websocket</artifactId>
+    <packaging>war</packaging>
+    <version>0.1.0</version>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-webmvc</artifactId>
+            <version>4.0.0.BUILD-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-messaging</artifactId>
+            <version>4.0.0.BUILD-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-websocket</artifactId>
+            <version>4.0.0.BUILD-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.2.2</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.websocket</groupId>
+            <artifactId>javax.websocket-api</artifactId>
+            <version>1.0-rc5</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <version>3.1-b09</version>
+            <scope>provided</scope>
+        </dependency>
+        
+        
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.6.4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>jcl-over-slf4j</artifactId>
+            <version>1.6.4</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+            <version>1.6.4</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>log4j</groupId>
+            <artifactId>log4j</artifactId>
+            <version>1.2.16</version>
+            <scope>runtime</scope>
+        </dependency>
+        
+        <dependency>
+            <groupId>org.projectreactor</groupId>
+            <artifactId>reactor-core</artifactId>
+            <version>1.0.0.M1</version>
+        </dependency>
+
+        <!-- Required when the "stomp-broker-relay" profile is enabled -->
+        <dependency>
+            <groupId>org.projectreactor</groupId>
+            <artifactId>reactor-tcp</artifactId>
+            <version>1.0.0.M1</version>
+        </dependency>
+        
+
+        <dependency>
+            <groupId>commons-logging</groupId>
+            <artifactId>commons-logging</artifactId>
+            <version>1.1</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>javax.servlet</groupId>
+                    <artifactId>servlet-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        
+    </dependencies>
+
+    <repositories>
+        <repository>
+            <id>spring-snapshots</id>
+            <url>http://repo.springsource.org/libs-snapshot</url>
+            <snapshots>
+                <enabled>true</enabled>
+            </snapshots>
+        </repository>
+        <repository>
+            <id>tomcat-snapshots</id>
+            <url>https://repository.apache.org/content/repositories/snapshots</url>
+            <snapshots><enabled>true</enabled></snapshots>
+            <releases><enabled>false</enabled></releases>
+        </repository>
+        <repository>
+            <id>java-net-snapshots</id>
+            <url>https://maven.java.net/content/repositories/snapshots</url>
+            <snapshots><enabled>true</enabled></snapshots>
+            <releases><enabled>false</enabled></releases>
+        </repository>
+    </repositories>
+
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>2.3.2</version>
+                <configuration>
+                    <source>1.7</source>
+                    <target>1.7</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
+                <version>2.2</version>
+                <configuration>
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
+```
+
+This guide is using [Spring Boot's starter POMs](/guides/gs/spring-boot/).
+
+Note to experienced Maven users who are unaccustomed to using an external parent project: you can take it out later, it's just there to reduce the amount of code you have to write to get started.
+
+
+<a name="initial"></a>
+Create a resource representation class
+--------------------------------------
+
+Now that you've set up the project and build system, you can create your STOMP message service.
+
+Begin the process by thinking about service interactions.
+
+The service will accept messages containing a name in a STOMP message whose body is a [JSON][u-json] object. If the name given is "Fred", then the message might look something like this:
+
+    {
+        "name": "Fred"
+    }
+
+To model the message carrying the name, you can create a plain old Java object with a `name` property and a corresponding `getName()` method:
+
+`src/main/java/hello/HelloMessage.java`
+```java
+package hello;
+
+public class HelloMessage {
+
+    private String name;
+    
+    public String getName() {
+        return name;
+    }
+
+}
+```
+
+Upon receiving the message and extracting the name, the service will process it by creating a greeting and publishing that greeting on a separate queue that the client is subscribed to. The greeting will also be a JSON object, which might look something like this:
+
+    {
+        "content": "Hello, Fred!"
+    }
+
+To model the greeting representation, you another plain old Java object with a `content` property and corresponding `getContent()` method:
+
+`src/main/java/hello/Greeting.java`
+```java
+package hello;
+
+public class Greeting {
+    
+    private String content;
+
+    public Greeting(String content) {
+        this.content = content;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+}
+```
+
+> **Note:** As you see in steps below, Spring uses the [Jackson JSON][jackson] library to automatically marshal instances of type `Greeting` into JSON.
+
+Next, you'll create the controller to receive the hello message and send a greeting message.
+
+Create a message-handling controller
+------------------------------------
+
+In Spring's approach to working with STOMP messaging, STOMP messages can be handled by a controller. These components are easily identified by the [`@Controller`][AtController] annotation, and the `GreetingController` below is mapped to handle messages published on the "/app/hello" destination.
+
+`src/main/java/hello/GreetingController.java`
+```java
+package hello;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.messaging.handler.annotation.MessageMapping;
+import org.springframework.messaging.simp.SimpMessageSendingOperations;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestBody;
+
+@Controller
+public class GreetingController {
+
+    private SimpMessageSendingOperations messagingTemplate;
+
+    @Autowired
+    public GreetingController(SimpMessageSendingOperations messagingTemplate) {
+        this.messagingTemplate = messagingTemplate;
+    }
+    
+    @MessageMapping("/app/hello")
+    public void greeting(@RequestBody HelloMessage message) throws Exception {
+        Thread.sleep(3000); // simulated delay
+        Greeting greeting = new Greeting("Hello, " + message.getName() + "!");
+        messagingTemplate.convertAndSend("/queue/greetings", greeting);
+    }
+}
+```
+
+This controller is concise and simple, but there's plenty going on under the hood. Let's break it down step by step.
+
+The `@MessageMapping` annotation ensures that if a message is published on the "/app/hello" destination, then the `greeting()` method will called.
+
+`@RequestBody` binds the payload of the message to a `HelloMessage` object which is passed into `greeting()`. 
+
+Internally, the implementation of the method simulates a processing delay by causing the thread to sleep for 3 seconds. This is to demonstrate that after the client sends a message, the server can take as long as it needs to process the message asynchronously.  The client may continue with whatever work it needs to do without waiting on the response.
+
+After the 3 second delay, the `greeting()` method creates a new `Greeting` object, setting its content to say "Hello" to the name from the `HelloMessage`. It then calls `convertAndSend()` on the injected `SimpMessageSendingOperations` to send the `Greeting` on the "/queue/greetings" destination.
+
+The `Greeting` object must be converted to JSON. Thanks to Spring's HTTP message converter support, you don't need to do this conversion manually. When you configure Spring for STOMP messaging, you'll inject `SimpMessagingTemplate` with an instance of [`MappingJackson2MessageConverter`][MappingJackson2MessageConverter]. It will be used to convert the `Greeting` instance to JSON.
+
+Configuring Spring for STOMP messaging
+--------------------------------------
+
+TODO: This is extremely ugly at the moment, with many beans in play. Rossen says that SPR-10835 will be resolved in time for RC1, so fill in this section then.
+
+
+Make the application executable
+-------------------------------
+
+In order to deploy the application to Tomcat, you'll need to add a bit more configuration.
+
+First, you'll need to configure Spring's [`DispatcherServlet`[DispatcherServlet] to serve static resources so that it will serve index.html. This can be done by creating a configuration class that overrides the `configureDefaultServletHandling()` method of `WebMvcConfigurerAdapter` and calls `enable()` on the given `DefaultServletHandlerConfigurer`:
+
+`src/main/java/hello/WebConfig.java`
+```java
+package hello;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.messaging.SubscribableChannel;
+import org.springframework.messaging.handler.websocket.SubProtocolWebSocketHandler;
+import org.springframework.messaging.simp.SimpMessageSendingOperations;
+import org.springframework.messaging.simp.SimpMessagingTemplate;
+import org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler;
+import org.springframework.messaging.simp.handler.SimpleBrokerMessageHandler;
+import org.springframework.messaging.simp.handler.SimpleUserQueueSuffixResolver;
+import org.springframework.messaging.simp.handler.UserDestinationMessageHandler;
+import org.springframework.messaging.simp.stomp.StompProtocolHandler;
+import org.springframework.messaging.support.channel.ExecutorSubscribableChannel;
+import org.springframework.messaging.support.converter.MappingJackson2MessageConverter;
+import org.springframework.messaging.support.converter.MessageConverter;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+import org.springframework.web.HttpRequestHandler;
+import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
+import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.sockjs.SockJsHttpRequestHandler;
+import org.springframework.web.socket.sockjs.SockJsService;
+import org.springframework.web.socket.sockjs.transport.handler.DefaultSockJsService;
+
+@Configuration
+@EnableWebMvc
+@ComponentScan
+@EnableAsync
+public class WebConfig extends WebMvcConfigurerAdapter {
+
+    private final MessageConverter<?> messageConverter = new MappingJackson2MessageConverter();
+
+    private final SimpleUserQueueSuffixResolver userQueueSuffixResolver = new SimpleUserQueueSuffixResolver();
+
+
+    @Bean
+    public SimpleUrlHandlerMapping handlerMapping() {
+
+        SockJsService sockJsService = new DefaultSockJsService(taskScheduler());
+        HttpRequestHandler requestHandler = new SockJsHttpRequestHandler(sockJsService, webSocketHandler());
+
+        SimpleUrlHandlerMapping hm = new SimpleUrlHandlerMapping();
+        hm.setOrder(-1);
+        hm.setUrlMap(Collections.singletonMap("/hello/**", requestHandler));
+        return hm;
+    }
+
+    // WebSocketHandler supporting STOMP messages
+
+    @Bean
+    public WebSocketHandler webSocketHandler() {
+
+        StompProtocolHandler stompHandler = new StompProtocolHandler();
+        stompHandler.setUserQueueSuffixResolver(this.userQueueSuffixResolver);
+
+        SubProtocolWebSocketHandler webSocketHandler = new SubProtocolWebSocketHandler(dispatchChannel());
+        webSocketHandler.setDefaultProtocolHandler(stompHandler);
+        webSocketHandlerChannel().subscribe(webSocketHandler);
+
+        return webSocketHandler;
+    }
+
+    // MessageHandler for processing messages by delegating to @Controller annotated methods
+
+    @Bean
+    public AnnotationMethodMessageHandler annotationMessageHandler() {
+
+        AnnotationMethodMessageHandler handler =
+                new AnnotationMethodMessageHandler(dispatchMessagingTemplate(), webSocketHandlerChannel());
+
+        handler.setDestinationPrefixes(Arrays.asList("/app/"));
+        handler.setMessageConverter(this.messageConverter);
+        dispatchChannel().subscribe(handler);
+        return handler;
+    }
+
+    // MessageHandler that acts as a "simple" message broker
+    // See DispatcherServletInitializer for enabling/disabling the "simple-broker" profile
+
+    @Bean
+    public SimpleBrokerMessageHandler simpleBrokerMessageHandler() {
+        SimpleBrokerMessageHandler handler = new SimpleBrokerMessageHandler(webSocketHandlerChannel());
+        handler.setDestinationPrefixes(Arrays.asList("/topic/", "/queue/"));
+        dispatchChannel().subscribe(handler);
+        return handler;
+    }
+
+    // MessageHandler that resolves destinations prefixed with "/user/{user}"
+    // See the Javadoc of UserDestinationMessageHandler for details
+
+    @Bean
+    public UserDestinationMessageHandler userMessageHandler() {
+        UserDestinationMessageHandler handler = new UserDestinationMessageHandler(
+                dispatchMessagingTemplate(), this.userQueueSuffixResolver);
+        dispatchChannel().subscribe(handler);
+        return handler;
+    }
+
+    // MessagingTemplate (and MessageChannel) to dispatch messages to for further processing
+    // All MessageHandler beans above subscribe to this channel
+
+    @Bean
+    public SimpMessageSendingOperations dispatchMessagingTemplate() {
+        SimpMessagingTemplate template = new SimpMessagingTemplate(dispatchChannel());
+        template.setMessageConverter(this.messageConverter);
+        return template;
+    }
+
+    @Bean
+    public SubscribableChannel dispatchChannel() {
+        return new ExecutorSubscribableChannel(asyncExecutor());
+    }
+
+    // Channel for sending STOMP messages to connected WebSocket sessions (mostly for internal use)
+
+    @Bean
+    public SubscribableChannel webSocketHandlerChannel() {
+        return new ExecutorSubscribableChannel(asyncExecutor());
+    }
+
+    // Executor for message passing via MessageChannel
+
+    @Bean
+    public ThreadPoolTaskExecutor asyncExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(4);
+        executor.setCorePoolSize(8);
+        executor.setThreadNamePrefix("MessageChannel-");
+        return executor;
+    }
+
+    // Task executor for use in SockJS (heartbeat frames, session timeouts)
+
+    @Bean
+    public ThreadPoolTaskScheduler taskScheduler() {
+        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
+        taskScheduler.setThreadNamePrefix("SockJS-");
+        taskScheduler.setPoolSize(4);
+        return taskScheduler;
+    }
+
+    // Allow serving HTML files through the default Servlet
+
+    @Override
+    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
+        configurer.enable();
+    }
+
+}
+```
+
+You'll also need to configure `DispatcherServlet`. This is most easily done by creating a class that extends `AbstractAnnotationConfigDispatcherServletInitializer`:
+
+`src/main/java/hello/HelloServletInitializer.java`
+```java
+package hello;
+
+import javax.servlet.ServletRegistration.Dynamic;
+
+import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
+
+public class HelloServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
+
+    @Override
+    protected Class<?>[] getRootConfigClasses() {
+        return new Class<?>[] {};
+    }
+
+    @Override
+    protected Class<?>[] getServletConfigClasses() {
+        return new Class<?>[] { WebConfig.class };
+    }
+
+    @Override
+    protected String[] getServletMappings() {
+        return new String[] { "/" };
+    }
+
+    @Override
+    protected void customizeRegistration(Dynamic registration) {
+        registration.setInitParameter("dispatchOptionsRequest", "true");
+    }
+
+}
+```
+
+Here, `DispatcherServlet` is mapped to "/". Also, `WebConfig` and `EndpointConfig` are specified, respectively, as the servlet and root configuration classes.
+
+Now you're ready to build and deploy the application to Tomcat 8. Start by building the WAR file:
+
+```sh
+mvn package
+```
+
+Then copy the WAR file to Tomcat 8's `trunk/output/webapps` directory. 
+
+Finally, restart Tomcat 8:
+
+```sh
+output/bin/shutdown.sh
+output/bin/startup.sh
+```
+
+After the application starts, point your browser at http://localhost:8080/gs-messaging-stomp-websocket and click the "Connect" button.
+
+Upon opening a connection, you will be asked for your name. Enter your name and click "Send". Your name will be sent to the server as a JSON message over STOMP. The server will send a message back with a "Hello" greeting that will be displayed on the page. At this point, you may choose to send another name or you can click the "Disconnect" button to close the connection.
+
+
+Summary
+-------
+
+Congratulations! You've just developed a STOMP-based messaging service with Spring. 
+
+
+[tomcat8]: http://tomcat.apache.org/download-80.cgi
+[u-rest]: /understanding/rest
+[u-json]: /understanding/json
+[jackson]: http://wiki.fasterxml.com/JacksonHome
+[MappingJackson2MessageConverter]: http://static.springsource.org/spring/docs/4.0.x/javadoc-api/org/springframework/messaging/support/converter/MappingJackson2MessageConverter.html
+[`AtController`]: http://static.springsource.org/spring/docs/current/javadoc-api/org/springframework/stereotype/Controller.html
+[`DispatcherServlet`]: http://static.springsource.org/spring/docs/current/javadoc-api/org/springframework/web/servlet/DispatcherServlet.html

+ 4 - 0
complete/.gitignore

@@ -0,0 +1,4 @@
+/target
+.classpath
+.project
+.settings

+ 26 - 0
complete/build.gradle

@@ -0,0 +1,26 @@
+apply plugin: 'java'
+apply plugin: 'war'
+apply plugin: 'eclipse'
+apply plugin: 'idea'
+
+war {
+    baseName = 'gs-messaging-websocket'
+    version =  '0.1.0'
+}
+
+repositories {
+    mavenCentral()
+    maven { url "http://repo.springsource.org/libs-snapshot" }
+}
+
+dependencies {
+    compile("org.springframework:spring-webmvc:4.0.0.BUILD-SNAPSHOT")
+    compile("org.springframework:spring-websocket:4.0.0.BUILD-SNAPSHOT")
+    providedCompile("javax.websocket:javax.websocket-api:1.0-rc5")
+    providedCompile("javax.servlet:javax.servlet-api:3.1-b09")
+    testCompile("junit:junit:4.11")
+}
+
+task wrapper(type: Wrapper) {
+    gradleVersion = '1.6'
+}

BIN
complete/gradle/wrapper/gradle-wrapper.jar


+ 6 - 0
complete/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Thu Aug 08 14:38:58 EDT 2013
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=http\://services.gradle.org/distributions/gradle-1.6-bin.zip

+ 164 - 0
complete/gradlew

@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+    [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

+ 90 - 0
complete/gradlew.bat

@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 140 - 0
complete/pom.xml

@@ -0,0 +1,140 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.springframework.samples</groupId>
+    <artifactId>gs-messaging-stomp-websocket</artifactId>
+    <packaging>war</packaging>
+    <version>0.1.0</version>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-webmvc</artifactId>
+            <version>4.0.0.BUILD-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-messaging</artifactId>
+            <version>4.0.0.BUILD-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-websocket</artifactId>
+            <version>4.0.0.BUILD-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.2.2</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.websocket</groupId>
+            <artifactId>javax.websocket-api</artifactId>
+            <version>1.0-rc5</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <version>3.1-b09</version>
+            <scope>provided</scope>
+        </dependency>
+        
+        
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.6.4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>jcl-over-slf4j</artifactId>
+            <version>1.6.4</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+            <version>1.6.4</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>log4j</groupId>
+            <artifactId>log4j</artifactId>
+            <version>1.2.16</version>
+            <scope>runtime</scope>
+        </dependency>
+        
+        <dependency>
+            <groupId>org.projectreactor</groupId>
+            <artifactId>reactor-core</artifactId>
+            <version>1.0.0.M1</version>
+        </dependency>
+
+        <!-- Required when the "stomp-broker-relay" profile is enabled -->
+        <dependency>
+            <groupId>org.projectreactor</groupId>
+            <artifactId>reactor-tcp</artifactId>
+            <version>1.0.0.M1</version>
+        </dependency>
+        
+
+        <dependency>
+            <groupId>commons-logging</groupId>
+            <artifactId>commons-logging</artifactId>
+            <version>1.1</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>javax.servlet</groupId>
+                    <artifactId>servlet-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        
+    </dependencies>
+
+    <repositories>
+        <repository>
+            <id>spring-snapshots</id>
+            <url>http://repo.springsource.org/libs-snapshot</url>
+            <snapshots>
+                <enabled>true</enabled>
+            </snapshots>
+        </repository>
+        <repository>
+            <id>tomcat-snapshots</id>
+            <url>https://repository.apache.org/content/repositories/snapshots</url>
+            <snapshots><enabled>true</enabled></snapshots>
+            <releases><enabled>false</enabled></releases>
+        </repository>
+        <repository>
+            <id>java-net-snapshots</id>
+            <url>https://maven.java.net/content/repositories/snapshots</url>
+            <snapshots><enabled>true</enabled></snapshots>
+            <releases><enabled>false</enabled></releases>
+        </repository>
+    </repositories>
+
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>2.3.2</version>
+                <configuration>
+                    <source>1.7</source>
+                    <target>1.7</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
+                <version>2.2</version>
+                <configuration>
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 15 - 0
complete/redeploy-tomcat.sh

@@ -0,0 +1,15 @@
+set -v
+
+mvn -DskipTests clean package
+
+# Change the line below to the location of Tomcat built from trunk
+TOMCAT=~/Applications/apache-tomcat-trunk/output/build
+
+rm -rf $TOMCAT/webapps/gs-websocket*
+
+cp target/gs-websocket.war $TOMCAT/webapps/
+
+$TOMCAT/bin/shutdown.sh
+sleep 3
+
+$TOMCAT/bin/startup.sh

+ 15 - 0
complete/src/main/java/hello/Greeting.java

@@ -0,0 +1,15 @@
+package hello;
+
+public class Greeting {
+    
+    private String content;
+
+    public Greeting(String content) {
+        this.content = content;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+}

+ 25 - 0
complete/src/main/java/hello/GreetingController.java

@@ -0,0 +1,25 @@
+package hello;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.messaging.handler.annotation.MessageMapping;
+import org.springframework.messaging.simp.SimpMessageSendingOperations;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestBody;
+
+@Controller
+public class GreetingController {
+
+    private SimpMessageSendingOperations messagingTemplate;
+
+    @Autowired
+    public GreetingController(SimpMessageSendingOperations messagingTemplate) {
+        this.messagingTemplate = messagingTemplate;
+    }
+    
+    @MessageMapping("/app/hello")
+    public void greeting(@RequestBody HelloMessage message) throws Exception {
+        Thread.sleep(3000); // simulated delay
+        Greeting greeting = new Greeting("Hello, " + message.getName() + "!");
+        messagingTemplate.convertAndSend("/queue/greetings", greeting);
+    }
+}

+ 11 - 0
complete/src/main/java/hello/HelloMessage.java

@@ -0,0 +1,11 @@
+package hello;
+
+public class HelloMessage {
+
+    private String name;
+    
+    public String getName() {
+        return name;
+    }
+
+}

+ 29 - 0
complete/src/main/java/hello/HelloServletInitializer.java

@@ -0,0 +1,29 @@
+package hello;
+
+import javax.servlet.ServletRegistration.Dynamic;
+
+import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
+
+public class HelloServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
+
+    @Override
+    protected Class<?>[] getRootConfigClasses() {
+        return new Class<?>[] {};
+    }
+
+    @Override
+    protected Class<?>[] getServletConfigClasses() {
+        return new Class<?>[] { WebConfig.class };
+    }
+
+    @Override
+    protected String[] getServletMappings() {
+        return new String[] { "/" };
+    }
+
+    @Override
+    protected void customizeRegistration(Dynamic registration) {
+        registration.setInitParameter("dispatchOptionsRequest", "true");
+    }
+
+}

+ 142 - 0
complete/src/main/java/hello/StompConfig.java

@@ -0,0 +1,142 @@
+package hello;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.messaging.SubscribableChannel;
+import org.springframework.messaging.handler.websocket.SubProtocolWebSocketHandler;
+import org.springframework.messaging.simp.SimpMessageSendingOperations;
+import org.springframework.messaging.simp.SimpMessagingTemplate;
+import org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler;
+import org.springframework.messaging.simp.handler.SimpleBrokerMessageHandler;
+import org.springframework.messaging.simp.handler.SimpleUserQueueSuffixResolver;
+import org.springframework.messaging.simp.handler.UserDestinationMessageHandler;
+import org.springframework.messaging.simp.stomp.StompProtocolHandler;
+import org.springframework.messaging.support.channel.ExecutorSubscribableChannel;
+import org.springframework.messaging.support.converter.MappingJackson2MessageConverter;
+import org.springframework.messaging.support.converter.MessageConverter;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+import org.springframework.web.HttpRequestHandler;
+import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.sockjs.SockJsHttpRequestHandler;
+import org.springframework.web.socket.sockjs.SockJsService;
+import org.springframework.web.socket.sockjs.transport.handler.DefaultSockJsService;
+
+@Configuration
+public class StompConfig {
+    private final MessageConverter<?> messageConverter = new MappingJackson2MessageConverter();
+
+    private final SimpleUserQueueSuffixResolver userQueueSuffixResolver = new SimpleUserQueueSuffixResolver();
+
+
+    @Bean
+    public SimpleUrlHandlerMapping handlerMapping() {
+
+        SockJsService sockJsService = new DefaultSockJsService(taskScheduler());
+        HttpRequestHandler requestHandler = new SockJsHttpRequestHandler(sockJsService, webSocketHandler());
+
+        SimpleUrlHandlerMapping hm = new SimpleUrlHandlerMapping();
+        hm.setOrder(-1);
+        hm.setUrlMap(Collections.singletonMap("/hello/**", requestHandler));
+        return hm;
+    }
+
+    // WebSocketHandler supporting STOMP messages
+
+    @Bean
+    public WebSocketHandler webSocketHandler() {
+
+        StompProtocolHandler stompHandler = new StompProtocolHandler();
+        stompHandler.setUserQueueSuffixResolver(this.userQueueSuffixResolver);
+
+        SubProtocolWebSocketHandler webSocketHandler = new SubProtocolWebSocketHandler(dispatchChannel());
+        webSocketHandler.setDefaultProtocolHandler(stompHandler);
+        webSocketHandlerChannel().subscribe(webSocketHandler);
+
+        return webSocketHandler;
+    }
+
+    // MessageHandler for processing messages by delegating to @Controller annotated methods
+
+    @Bean
+    public AnnotationMethodMessageHandler annotationMessageHandler() {
+
+        AnnotationMethodMessageHandler handler =
+                new AnnotationMethodMessageHandler(dispatchMessagingTemplate(), webSocketHandlerChannel());
+
+        handler.setDestinationPrefixes(Arrays.asList("/app/"));
+        handler.setMessageConverter(this.messageConverter);
+        dispatchChannel().subscribe(handler);
+        return handler;
+    }
+
+    // MessageHandler that acts as a "simple" message broker
+    // See DispatcherServletInitializer for enabling/disabling the "simple-broker" profile
+
+    @Bean
+    public SimpleBrokerMessageHandler simpleBrokerMessageHandler() {
+        SimpleBrokerMessageHandler handler = new SimpleBrokerMessageHandler(webSocketHandlerChannel());
+        handler.setDestinationPrefixes(Arrays.asList("/topic/", "/queue/"));
+        dispatchChannel().subscribe(handler);
+        return handler;
+    }
+
+    // MessageHandler that resolves destinations prefixed with "/user/{user}"
+    // See the Javadoc of UserDestinationMessageHandler for details
+
+    @Bean
+    public UserDestinationMessageHandler userMessageHandler() {
+        UserDestinationMessageHandler handler = new UserDestinationMessageHandler(
+                dispatchMessagingTemplate(), this.userQueueSuffixResolver);
+        dispatchChannel().subscribe(handler);
+        return handler;
+    }
+
+    // MessagingTemplate (and MessageChannel) to dispatch messages to for further processing
+    // All MessageHandler beans above subscribe to this channel
+
+    @Bean
+    public SimpMessageSendingOperations dispatchMessagingTemplate() {
+        SimpMessagingTemplate template = new SimpMessagingTemplate(dispatchChannel());
+        template.setMessageConverter(this.messageConverter);
+        return template;
+    }
+
+    @Bean
+    public SubscribableChannel dispatchChannel() {
+        return new ExecutorSubscribableChannel(asyncExecutor());
+    }
+
+    // Channel for sending STOMP messages to connected WebSocket sessions (mostly for internal use)
+
+    @Bean
+    public SubscribableChannel webSocketHandlerChannel() {
+        return new ExecutorSubscribableChannel(asyncExecutor());
+    }
+
+    // Executor for message passing via MessageChannel
+
+    @Bean
+    public ThreadPoolTaskExecutor asyncExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(4);
+        executor.setCorePoolSize(8);
+        executor.setThreadNamePrefix("MessageChannel-");
+        return executor;
+    }
+
+    // Task executor for use in SockJS (heartbeat frames, session timeouts)
+
+    @Bean
+    public ThreadPoolTaskScheduler taskScheduler() {
+        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
+        taskScheduler.setThreadNamePrefix("SockJS-");
+        taskScheduler.setPoolSize(4);
+        return taskScheduler;
+    }
+
+}

+ 22 - 0
complete/src/main/java/hello/WebConfig.java

@@ -0,0 +1,22 @@
+package hello;
+
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
+
+@Configuration
+@EnableWebMvc
+@ComponentScan
+@Import(StompConfig.class)
+public class WebConfig extends WebMvcConfigurerAdapter {
+    
+    // Allow serving HTML files through the default Servlet
+    @Override
+    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
+        configurer.enable();
+    }
+
+}

+ 38 - 0
complete/src/main/resources/log4j.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN" "log4j.dtd">
+<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
+
+    <!-- Appenders -->
+    <appender name="console" class="org.apache.log4j.ConsoleAppender">
+        <param name="Target" value="System.out" />
+        <layout class="org.apache.log4j.PatternLayout">
+            <param name="ConversionPattern" value="%d{HH:mm:ss} [%t] %c{1} - %m%n" />
+        </layout>
+    </appender>
+    
+    <logger name="org.springframework.samples">
+        <level value="debug" />
+    </logger>
+
+    <logger name="org.springframework.messaging">
+        <level value="debug" />
+    </logger>
+
+    <logger name="org.springframework.web">
+        <level value="debug" />
+    </logger>
+
+    <logger name="org.springframework.web.socket">
+        <level value="debug" />
+    </logger>
+
+    <logger name="org.springframework.security">
+        <level value="warn" />
+    </logger>
+
+    <root>
+        <priority value="warn" />
+        <appender-ref ref="console" />
+    </root>
+    
+</log4j:configuration>

+ 64 - 0
complete/src/main/webapp/index.html

@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Hello WebSocket</title>
+    <script src="sockjs-0.3.4.js"></script>
+    <script src="stomp.js"></script>
+    <script type="text/javascript">
+        var stompClient = null;
+        
+        function setConnected(connected) {
+            document.getElementById('connect').disabled = connected;
+            document.getElementById('disconnect').disabled = !connected;
+            document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
+            document.getElementById('response').innerHTML = '';
+        }
+        
+        function connect() {
+            var socket = new SockJS('/gs-messaging-stomp-websocket/hello');
+            stompClient = Stomp.over(socket);            
+            stompClient.connect('', '', function(frame) {
+                setConnected(true);
+                console.log('Connected: ' + frame);
+                stompClient.subscribe('/queue/greetings', function(greeting){
+                    showGreeting(JSON.parse(greeting.body).content);
+                });
+            });
+        }
+        
+        function disconnect() {
+            stompClient.disconnect();
+            setConnected(false);
+            console.log("Disconnected");
+        }
+        
+        function sendName() {
+            var name = document.getElementById('name').value;
+            stompClient.send("/app/hello", {}, JSON.stringify({ 'name': name }));
+        }
+        
+        function showGreeting(message) {
+            var response = document.getElementById('response');
+            var p = document.createElement('p');
+            p.style.wordWrap = 'break-word';
+            p.appendChild(document.createTextNode(message));
+            response.appendChild(p);
+        }
+    </script>
+</head>
+<body>
+<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable
+    Javascript and reload this page!</h2></noscript>
+<div>
+    <div>
+        <button id="connect" onclick="connect();">Connect</button>
+        <button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
+    </div>
+    <div id="conversationDiv">
+        <label>What is your name?</label><input type="text" id="name" />
+        <button id="sendName" onclick="sendName();">Send</button>
+        <p id="response"></p>
+    </div>
+</div>
+</body>
+</html>

Fișier diff suprimat deoarece este prea mare
+ 24 - 0
complete/src/main/webapp/sockjs-0.3.4.js


+ 373 - 0
complete/src/main/webapp/stomp.js

@@ -0,0 +1,373 @@
+// Generated by CoffeeScript 1.4.0
+(function() {
+  var Byte, Client, Frame, Stomp,
+    __hasProp = {}.hasOwnProperty;
+
+  Byte = {
+    LF: '\x0A',
+    NULL: '\x00'
+  };
+
+  Frame = (function() {
+
+    function Frame(command, headers, body) {
+      this.command = command;
+      this.headers = headers != null ? headers : {};
+      this.body = body != null ? body : '';
+    }
+
+    Frame.prototype.toString = function() {
+      var lines, name, value, _ref;
+      lines = [this.command];
+      _ref = this.headers;
+      for (name in _ref) {
+        if (!__hasProp.call(_ref, name)) continue;
+        value = _ref[name];
+        lines.push("" + name + ":" + value);
+      }
+      if (this.body) {
+        lines.push("content-length:" + ('' + this.body).length);
+      }
+      lines.push(Byte.LF + this.body);
+      return lines.join(Byte.LF);
+    };
+
+    Frame._unmarshallSingle = function(data) {
+      var body, chr, command, divider, headerLines, headers, i, idx, len, line, start, trim, _i, _j, _ref, _ref1;
+      divider = data.search(RegExp("" + Byte.LF + Byte.LF));
+      headerLines = data.substring(0, divider).split(Byte.LF);
+      command = headerLines.shift();
+      headers = {};
+      trim = function(str) {
+        return str.replace(/^\s+|\s+$/g, '');
+      };
+      line = idx = null;
+      for (i = _i = 0, _ref = headerLines.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
+        line = headerLines[i];
+        idx = line.indexOf(':');
+        headers[trim(line.substring(0, idx))] = trim(line.substring(idx + 1));
+      }
+      body = '';
+      start = divider + 2;
+      if (headers['content-length']) {
+        len = parseInt(headers['content-length']);
+        body = ('' + data).substring(start, start + len);
+      } else {
+        chr = null;
+        for (i = _j = start, _ref1 = data.length; start <= _ref1 ? _j < _ref1 : _j > _ref1; i = start <= _ref1 ? ++_j : --_j) {
+          chr = data.charAt(i);
+          if (chr === Byte.NULL) {
+            break;
+          }
+          body += chr;
+        }
+      }
+      return new Frame(command, headers, body);
+    };
+
+    Frame.unmarshall = function(datas) {
+      var data;
+      return (function() {
+        var _i, _len, _ref, _results;
+        _ref = datas.split(RegExp("" + Byte.NULL + Byte.LF + "*"));
+        _results = [];
+        for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+          data = _ref[_i];
+          if ((data != null ? data.length : void 0) > 0) {
+            _results.push(Frame._unmarshallSingle(data));
+          }
+        }
+        return _results;
+      })();
+    };
+
+    Frame.marshall = function(command, headers, body) {
+      var frame;
+      frame = new Frame(command, headers, body);
+      return frame.toString() + Byte.NULL;
+    };
+
+    return Frame;
+
+  })();
+
+  Client = (function() {
+
+    function Client(ws) {
+      this.ws = ws;
+      this.ws.binaryType = "arraybuffer";
+      this.counter = 0;
+      this.connected = false;
+      this.heartbeat = {
+        outgoing: 10000,
+        incoming: 10000
+      };
+      this.subscriptions = {};
+    }
+
+    Client.prototype._transmit = function(command, headers, body) {
+      var out;
+      out = Frame.marshall(command, headers, body);
+      if (typeof this.debug === "function") {
+        this.debug(">>> " + out);
+      }
+      return this.ws.send(out);
+    };
+
+    Client.prototype._setupHeartbeat = function(headers) {
+      var serverIncoming, serverOutgoing, ttl, v, _ref, _ref1,
+        _this = this;
+      if ((_ref = headers.version) !== Stomp.VERSIONS.V1_1 && _ref !== Stomp.VERSIONS.V1_2) {
+        return;
+      }
+      _ref1 = (function() {
+        var _i, _len, _ref1, _results;
+        _ref1 = headers['heart-beat'].split(",");
+        _results = [];
+        for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
+          v = _ref1[_i];
+          _results.push(parseInt(v));
+        }
+        return _results;
+      })(), serverOutgoing = _ref1[0], serverIncoming = _ref1[1];
+      if (!(this.heartbeat.outgoing === 0 || serverIncoming === 0)) {
+        ttl = Math.max(this.heartbeat.outgoing, serverIncoming);
+        if (typeof this.debug === "function") {
+          this.debug("send PING every " + ttl + "ms");
+        }
+        this.pinger = typeof window !== "undefined" && window !== null ? window.setInterval(function() {
+          _this.ws.send(Byte.LF);
+          return typeof _this.debug === "function" ? _this.debug(">>> PING") : void 0;
+        }, ttl) : void 0;
+      }
+      if (!(this.heartbeat.incoming === 0 || serverOutgoing === 0)) {
+        ttl = Math.max(this.heartbeat.incoming, serverOutgoing);
+        if (typeof this.debug === "function") {
+          this.debug("check PONG every " + ttl + "ms");
+        }
+        return this.ponger = typeof window !== "undefined" && window !== null ? window.setInterval(function() {
+          var delta;
+          delta = Date.now() - _this.serverActivity;
+          if (delta > ttl * 2) {
+            if (typeof _this.debug === "function") {
+              _this.debug("did not receive server activity for the last " + delta + "ms");
+            }
+            return _this.ws.close();
+          }
+        }, ttl) : void 0;
+      }
+    };
+
+    Client.prototype.connect = function(login, passcode, connectCallback, errorCallback, vhost) {
+      var _this = this;
+      this.connectCallback = connectCallback;
+      if (typeof this.debug === "function") {
+        this.debug("Opening Web Socket...");
+      }
+      this.ws.onmessage = function(evt) {
+        var arr, c, data, frame, onreceive, _i, _len, _ref, _results;
+        data = typeof ArrayBuffer !== 'undefined' && evt.data instanceof ArrayBuffer ? (arr = new Uint8Array(evt.data), typeof _this.debug === "function" ? _this.debug("--- got data length: " + arr.length) : void 0, ((function() {
+          var _i, _len, _results;
+          _results = [];
+          for (_i = 0, _len = arr.length; _i < _len; _i++) {
+            c = arr[_i];
+            _results.push(String.fromCharCode(c));
+          }
+          return _results;
+        })()).join('')) : evt.data;
+        _this.serverActivity = Date.now();
+        if (data === Byte.LF) {
+          if (typeof _this.debug === "function") {
+            _this.debug("<<< PONG");
+          }
+          return;
+        }
+        if (typeof _this.debug === "function") {
+          _this.debug("<<< " + data);
+        }
+        _ref = Frame.unmarshall(data);
+        _results = [];
+        for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+          frame = _ref[_i];
+          switch (frame.command) {
+            case "CONNECTED":
+              if (typeof _this.debug === "function") {
+                _this.debug("connected to server " + frame.headers.server);
+              }
+              _this.connected = true;
+              _this._setupHeartbeat(frame.headers);
+              _results.push(typeof _this.connectCallback === "function" ? _this.connectCallback(frame) : void 0);
+              break;
+            case "MESSAGE":
+              onreceive = _this.subscriptions[frame.headers.subscription];
+              _results.push(typeof onreceive === "function" ? onreceive(frame) : void 0);
+              break;
+            case "RECEIPT":
+              _results.push(typeof _this.onreceipt === "function" ? _this.onreceipt(frame) : void 0);
+              break;
+            case "ERROR":
+              _results.push(typeof errorCallback === "function" ? errorCallback(frame) : void 0);
+              break;
+            default:
+              _results.push(typeof _this.debug === "function" ? _this.debug("Unhandled frame: " + frame) : void 0);
+          }
+        }
+        return _results;
+      };
+      this.ws.onclose = function() {
+        var msg;
+        msg = "Whoops! Lost connection to " + _this.ws.url;
+        if (typeof _this.debug === "function") {
+          _this.debug(msg);
+        }
+        _this._cleanUp();
+        return typeof errorCallback === "function" ? errorCallback(msg) : void 0;
+      };
+      return this.ws.onopen = function() {
+        var headers;
+        if (typeof _this.debug === "function") {
+          _this.debug('Web Socket Opened...');
+        }
+        headers = {
+          "accept-version": Stomp.VERSIONS.supportedVersions(),
+          "heart-beat": [_this.heartbeat.outgoing, _this.heartbeat.incoming].join(',')
+        };
+        if (vhost) {
+          headers.host = vhost;
+        }
+        if (login) {
+          headers.login = login;
+        }
+        if (passcode) {
+          headers.passcode = passcode;
+        }
+        return _this._transmit("CONNECT", headers);
+      };
+    };
+
+    Client.prototype.disconnect = function(disconnectCallback) {
+      this._transmit("DISCONNECT");
+      this.ws.onclose = null;
+      this.ws.close();
+      this._cleanUp();
+      return typeof disconnectCallback === "function" ? disconnectCallback() : void 0;
+    };
+
+    Client.prototype._cleanUp = function() {
+      this.connected = false;
+      if (this.pinger) {
+        if (typeof window !== "undefined" && window !== null) {
+          window.clearInterval(this.pinger);
+        }
+      }
+      if (this.ponger) {
+        return typeof window !== "undefined" && window !== null ? window.clearInterval(this.ponger) : void 0;
+      }
+    };
+
+    Client.prototype.send = function(destination, headers, body) {
+      if (headers == null) {
+        headers = {};
+      }
+      if (body == null) {
+        body = '';
+      }
+      headers.destination = destination;
+      return this._transmit("SEND", headers, body);
+    };
+
+    Client.prototype.subscribe = function(destination, callback, headers) {
+      if (headers == null) {
+        headers = {};
+      }
+      if (!headers.id) {
+        headers.id = "sub-" + this.counter++;
+      }
+      headers.destination = destination;
+      this.subscriptions[headers.id] = callback;
+      this._transmit("SUBSCRIBE", headers);
+      return headers.id;
+    };
+
+    Client.prototype.unsubscribe = function(id) {
+      delete this.subscriptions[id];
+      return this._transmit("UNSUBSCRIBE", {
+        id: id
+      });
+    };
+
+    Client.prototype.begin = function(transaction) {
+      return this._transmit("BEGIN", {
+        transaction: transaction
+      });
+    };
+
+    Client.prototype.commit = function(transaction) {
+      return this._transmit("COMMIT", {
+        transaction: transaction
+      });
+    };
+
+    Client.prototype.abort = function(transaction) {
+      return this._transmit("ABORT", {
+        transaction: transaction
+      });
+    };
+
+    Client.prototype.ack = function(messageID, subscription, headers) {
+      if (headers == null) {
+        headers = {};
+      }
+      headers["message-id"] = messageID;
+      headers.subscription = subscription;
+      return this._transmit("ACK", headers);
+    };
+
+    Client.prototype.nack = function(messageID, subscription, headers) {
+      if (headers == null) {
+        headers = {};
+      }
+      headers["message-id"] = messageID;
+      headers.subscription = subscription;
+      return this._transmit("NACK", headers);
+    };
+
+    return Client;
+
+  })();
+
+  Stomp = {
+    libVersion: "2.0.0-next",
+    VERSIONS: {
+      V1_0: '1.0',
+      V1_1: '1.1',
+      V1_2: '1.2',
+      supportedVersions: function() {
+        return '1.1,1.0';
+      }
+    },
+    client: function(url, protocols) {
+      var klass, ws;
+      if (protocols == null) {
+        protocols = ['v10.stomp', 'v11.stomp'];
+      }
+      klass = Stomp.WebSocketClass || WebSocket;
+      ws = new klass(url, protocols);
+      return new Client(ws);
+    },
+    over: function(ws) {
+      return new Client(ws);
+    },
+    Frame: Frame
+  };
+
+  if (typeof window !== "undefined" && window !== null) {
+    window.Stomp = Stomp;
+  } else if (typeof exports !== "undefined" && exports !== null) {
+    exports.Stomp = Stomp;
+    Stomp.WebSocketClass = require('./test/server.mock.js').StompServerMock;
+  } else {
+    self.Stomp = Stomp;
+  }
+
+}).call(this);

+ 4 - 0
initial/.gitignore

@@ -0,0 +1,4 @@
+/target
+.classpath
+.project
+.settings

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff