INTS-137: Add STOMP Chat Application

JIRA: https://jira.spring.io/browse/INTSAMPLES-137

* Upgrade to Boot 1.1.7
* Fix several Boot apps to get deal with `args`, e.g. `--debug` option can be useful for Boot
* Provide a loca copy for JavaScript files (`sock.js` and `stomp.js`) to avoid extra Internet connection on testing

INTS-137-2: Make JavaScript cross-browser

Some additional polishing to the server config, like `logging-channel-adapter` for server message flow
This commit is contained in:
Artem Bilan
2014-10-13 17:31:01 +03:00
committed by Gary Russell
parent a5c12343c6
commit 63232c2974
14 changed files with 580 additions and 6 deletions

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.samples.chat.stomp.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
/**
* @author Artem Bilan
* @since 3.0
*/
@Configuration
@EnableAutoConfiguration
@ImportResource("classpath:org/springframework/integration/samples/chat/stomp/server/stomp-server.xml")
public class Application {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext ctx = SpringApplication.run(Application.class, args);
System.out.println("Hit 'Enter' to terminate");
System.in.read();
ctx.close();
}
}

View File

@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:int-websocket="http://www.springframework.org/schema/integration/websocket"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:int-event="http://www.springframework.org/schema/integration/event"
xmlns:task="http://www.springframework.org/schema/task" xmlns:util="http://www.springframework.org/schema/util"
xmlns:int-groovy="http://www.springframework.org/schema/integration/groovy"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration
http://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/integration/event
http://www.springframework.org/schema/integration/event/spring-integration-event.xsd
http://www.springframework.org/schema/integration/websocket
http://www.springframework.org/schema/integration/websocket/spring-integration-websocket.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/integration/groovy
http://www.springframework.org/schema/integration/groovy/spring-integration-groovy.xsd">
<int:wire-tap channel="logger"/>
<int:logging-channel-adapter id="logger" level="INFO" log-full-message="true"/>
<task:executor id="executor"/>
<bean id="webSocketSessionStore" class="org.springframework.integration.metadata.SimpleMetadataStore"/>
<bean id="chatMessagesStore" class="org.springframework.integration.store.SimpleMessageStore"/>
<util:map id="chatRoomSessions" value-type="java.util.List">
<entry key="room1" value="#{new java.util.ArrayList()}"/>
<entry key="room2" value="#{new java.util.ArrayList()}"/>
</util:map>
<bean id="stompSubProtocolHandler" class="org.springframework.web.socket.messaging.StompSubProtocolHandler"/>
<int-websocket:server-container id="serverWebSocketContainer" path="/chat">
<int-websocket:sockjs/>
</int-websocket:server-container>
<int-event:inbound-channel-adapter event-types="org.springframework.web.socket.messaging.AbstractSubProtocolEvent"
payload-expression="message"
channel="routeStompEvents"/>
<int:header-value-router input-channel="routeStompEvents"
header-name="simpMessageType"
resolution-required="false"
default-output-channel="nullChannel">
<int:mapping value="#{T(org.springframework.messaging.simp.SimpMessageType).CONNECT.name()}"
channel="connectAck"/>
<int:mapping value="#{T(org.springframework.messaging.simp.SimpMessageType).SUBSCRIBE.name()}"
channel="subscribe"/>
<int:mapping value="#{T(org.springframework.messaging.simp.SimpMessageType).UNSUBSCRIBE.name()}"
channel="unsubscribe"/>
<int:mapping value="#{T(org.springframework.messaging.simp.SimpMessageType).DISCONNECT.name()}"
channel="disconnect"/>
</int:header-value-router>
<int:outbound-channel-adapter id="connectAck"
expression="@webSocketSessionStore.put(headers.simpSessionId, headers.nativeHeaders.login)"/>
<int:publish-subscribe-channel id="subscribe"/>
<int:service-activator input-channel="subscribe" output-channel="nullChannel"
expression="@chatRoomSessions[headers.simpDestination]
.add(T(reactor.tuple.Tuple).of(headers.simpSessionId, headers.simpSubscriptionId))"/>
<int:chain input-channel="subscribe" output-channel="sendMessage">
<int:header-enricher>
<int:header name="sessionToSend" expression="headers.simpSessionId"/>
<int:header name="subscriptionToSend" expression="headers.simpSubscriptionId"/>
</int:header-enricher>
<int:service-activator
expression="@chatMessagesStore.getMessageGroup(headers.simpDestination).messages"/>
<int:filter expression="!payload.empty"/>
<int:header-enricher default-overwrite="true">
<int:header name="#{T(org.springframework.messaging.simp.stomp.StompHeaderAccessor).SESSION_ID_HEADER}"
expression="headers.sessionToSend"/>
<int:header
name="#{T(org.springframework.messaging.simp.stomp.StompHeaderAccessor).STOMP_SUBSCRIPTION_HEADER}"
expression="headers.subscriptionToSend"/>
</int:header-enricher>
<int:splitter apply-sequence="false"/>
</int:chain>
<int:outbound-channel-adapter id="unsubscribe">
<int-groovy:script>
chatRoomSessions.each { k, v ->
v.remove(reactor.tuple.Tuple.of(headers.simpSessionId, headers.simpSubscriptionId))
}
null
</int-groovy:script>
</int:outbound-channel-adapter>
<int:channel id="receiveMessage"/>
<int-websocket:inbound-channel-adapter channel="receiveMessage" container="serverWebSocketContainer"
default-protocol-handler="stompSubProtocolHandler"/>
<int:transformer input-channel="receiveMessage" output-channel="storeMessageAndPublish"
expression="{user: @webSocketSessionStore.get(headers.simpSessionId), message: payload, date: new java.util.Date()}"/>
<int:publish-subscribe-channel id="storeMessageAndPublish"/>
<int:service-activator input-channel="storeMessageAndPublish" output-channel="nullChannel"
expression="@chatMessagesStore.addMessageToGroup(headers.simpDestination, #root)"/>
<int:splitter input-channel="storeMessageAndPublish" output-channel="sendMessage" apply-sequence="false">
<int-groovy:script>
chatRoomSessions[headers.simpDestination].collect {
org.springframework.integration.support.MessageBuilder.withPayload(payload)
.copyHeaders(headers)
.setHeader('simpSessionId', it.t1)
.setHeader('simpSubscriptionId', it.t2)
.build()
}
</int-groovy:script>
</int:splitter>
<int:channel id="sendMessage">
<int:dispatcher task-executor="executor"/>
</int:channel>
<int-websocket:outbound-channel-adapter channel="sendMessage" container="serverWebSocketContainer"
default-protocol-handler="stompSubProtocolHandler"/>
<int:outbound-channel-adapter id="disconnect">
<int-groovy:script>
webSocketSessionStore.remove(headers.simpSessionId)
chatRoomSessions.each { k, v -> v.removeAll { it.t1 == headers.simpSessionId } }
null
</int-groovy:script>
</int:outbound-channel-adapter>
</beans>

View File

@@ -0,0 +1,217 @@
<html>
<head>
<title>WebSocket Chat</title>
<script src="http://localhost:8080/sockjs.js"></script>
<script src="http://localhost:8080/stomp.js"></script>
<script type="text/javascript">
var sock, stompClient, currentUser, subscriptions = {};
function connect() {
var userValue = document.getElementById('user');
if (userValue.value != "") {
currentUser = userValue.value;
sock = new SockJS('http://localhost:8080/chat');
stompClient = Stomp.over(sock);
stompClient.connect({login: currentUser}, function (frame) {
document.getElementById("welcome").style.display = "none";
document.getElementById("chat").style.display = "";
document.getElementById("currentUser").innerHTML = currentUser;
userValue.value = "";
});
}
}
function subscribeToRoom(room) {
subscriptions[room] =
stompClient.subscribe('room' + room, function (message) {
var m = JSON.parse(message.body);
var tr = document.createElement('tr');
tr.innerHTML = "<td width='100'><div class='messageUser'>" +
m.user +
"</div><div class='messageDate'>" +
new Date(m.date).toLocaleTimeString(navigator.userLanguage,
{month: "short", day: "numeric", hour: "2-digit", minute: "2-digit"}) +
"</div></td><td>" +
m.message +
"</td>";
var messages = document.getElementById("messages" + room);
messages.appendChild(tr);
messages.scrollIntoView(false);
});
document.getElementById("join" + room).style.display = "none";
document.getElementById("leave" + room).style.display = "";
document.getElementById("message" + room).disabled = false;
document.getElementById("send" + room).disabled = false;
}
function sendMessage(room) {
var messageValue = document.getElementById("message" + room);
if (messageValue.value != "") {
stompClient.send('room' + room, {subscription: subscriptions[room].id}, messageValue.value);
messageValue.value = "";
}
}
function unsubscribeFromRoom(room) {
subscriptions[room].unsubscribe();
document.getElementById("join" + room).style.display = "";
document.getElementById("leave" + room).style.display = "none";
document.getElementById("message" + room).disabled = true;
document.getElementById("send" + room).disabled = true;
document.getElementById("messages" + room).innerHTML = "";
}
function disconnect() {
for (var key in subscriptions) {
if (subscriptions.hasOwnProperty(key)) {
unsubscribeFromRoom(key)
}
}
stompClient.disconnect(function (frame) {
document.getElementById("welcome").style.display = "";
document.getElementById("chat").style.display = "none";
});
}
</script>
<style>
.messageUser {
width: 100px;
overflow: hidden;
text-overflow: ellipsis;
}
.messageDate {
text-align: center;
margin-top: 5px;
}
</style>
</head>
<body style="margin: 0">
<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 id="welcome"
style="position: absolute;
bottom: 0;
font-size: 200%;
height: 200px;
margin: auto;
text-align: center;
top: 0;
width: 100%;">
Welcome to the Simple WebSocket Stomp Chat!
<br/>
Enter your name to connect:
<br/>
<br/>
<div align="center">
<form onsubmit="connect();return false;">
<table>
<tr>
<td>
<input id="user" type="text"
style="font-size: 24pt; width: 200px; font-weight: bold; margin-top: 2px;"/>
</td>
<td>
<input type="submit" value="Connect"
style="height: 43px; width: 200px; font-size: 24pt; font-weight: bold;"/>
</td>
</tr>
</table>
</form>
</div>
</div>
<div id="chat" align="center" style="font-size: 200%; padding-top: 50px; display: none;">
Welcome, <span id="currentUser"></span>!
<br/>
Please, join to chat rooms to send and receive messages to/from other users:
<br/>
<br/>
<table>
<tr>
<td style="padding-right: 20px;">
<div align="center" style="font-size: 25pt;">
Room 1
</div>
<div align="center">
<input id="join1" type="button" value="Join"
onclick="subscribeToRoom(1)"
style="height: 43px; width: 150px; font-size: 20pt; font-weight: bold;"/>
&nbsp;&nbsp;&nbsp;
<input id="leave1" type="button" value="Leave"
onclick="unsubscribeFromRoom(1)"
style="height: 43px; width: 150px; font-size: 20pt; font-weight: bold;display: none"/>
</div>
<br/>
<div style="height: 300px; border: 5px groove; overflow: auto; width: 500px;">
<table width="100%" border="1" cellspacing="0" style="border: 0">
<tbody id="messages1" valign="top">
</tbody>
</table>
</div>
<br/>
<form onsubmit="sendMessage(1);return false;" style="margin-left: -4px;">
<table>
<tr>
<td>
<input id="message1" type="text" disabled
style="font-size: 20pt; width: 429px; margin-top: 2px;"/>
</td>
<td>
<input id="send1" type="submit" value="Send" disabled
style="height: 40px; width: 77px; font-size: 20pt; font-weight: bold;"/>
</td>
</tr>
</table>
</form>
</td>
<td style="padding-left: 20px;">
<div align="center" style="font-size: 25pt;">
Room 2
</div>
<div align="center">
<input id="join2" type="button" value="Join"
onclick="subscribeToRoom(2)"
style="height: 43px; width: 150px; font-size: 20pt; font-weight: bold;"/>
&nbsp;&nbsp;&nbsp;
<input id="leave2" type="button" value="Leave"
onclick="unsubscribeFromRoom(2)"
style="height: 43px; width: 150px; font-size: 20pt; font-weight: bold; display: none"/>
</div>
<br/>
<div style="height: 300px; border: 5px groove; overflow: auto; width: 500px;">
<table width="100%" border="1" cellspacing="0" style="border: 0">
<tbody id="messages2" valign="top">
</tbody>
</table>
</div>
<br/>
<form onsubmit="sendMessage(2);return false;" style="margin-left: -4px;">
<table>
<tr>
<td>
<input id="message2" type="text" disabled
style="font-size: 20pt; width: 429px; margin-top: 2px;"/>
</td>
<td>
<input id="send2" type="submit" value="Send" disabled
style="height: 40px; width: 77px; font-size: 20pt; font-weight: bold;"/>
</td>
</tr>
</table>
</form>
</td>
</tr>
</table>
<br/><br/>
<input type="button" value="Disconnect" onclick="disconnect()"
style="height: 43px; width: 220px; font-size: 24pt; font-weight: bold;"/>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.samples.chat.stomp.server;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* @author Artem Bilan
* @since 3.0
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
@IntegrationTest
public class ApplicationTests {
@Test
public void testWebSockets() throws InterruptedException {
}
}