Skip to content

Instantly share code, notes, and snippets.

@kts102121
Forked from theotherian/ Spring STOMP chat
Created January 9, 2017 09:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kts102121/f7143f3ef15979e190bcffcd5286a965 to your computer and use it in GitHub Desktop.
Save kts102121/f7143f3ef15979e190bcffcd5286a965 to your computer and use it in GitHub Desktop.
Spring STOMP chat
Spring STOMP chat
function showActive(activeMembers) {
renderActive(activeMembers.body);
stompClient.send('/app/activeUsers', {}, '');
}
function renderActive(activeMembers) {
var previouslySelected = $('.user-selected').text();
var usersWithPendingMessages = new Object();
$.each($('.pending-messages'), function(index, value) {
usersWithPendingMessages[value.id.substring(5)] = true; // strip the user-
});
var members = $.parseJSON(activeMembers);
var userDiv = $('<div>', {id: 'users'});
$.each(members, function(index, value) {
if (value === whoami) {
return true;
}
var userLine = $('<div>', {id: 'user-' + value});
userLine.addClass('user-entry');
if (previouslySelected === value) {
userLine.addClass('user-selected');
}
else {
userLine.addClass('user-unselected');
}
var userNameDisplay = $('<span>');
userNameDisplay.html(value);
userLine.append(userNameDisplay);
userLine.click(function() {
var foo = this;
$('.chat-container').hide();
$('.user-entry').removeClass('user-selected');
$('.user-entry').addClass('user-unselected');
userLine.removeClass('user-unselected');
userLine.removeClass('pending-messages');
userLine.addClass('user-selected');
userLine.children('.newmessage').remove();
var chatWindow = getChatWindow(value);
chatWindow.show();
});
if (value in usersWithPendingMessages) {
userLine.append(newMessageIcon());
userLine.addClass('pending-messages');
}
userDiv.append(userLine);
});
$('#userList').html(userDiv);
}
package com.theotherian.chat;
import java.security.Principal;
import javax.inject.Inject;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;
@Controller
public class ActiveUserController {
private ActiveUserService activeUserService;
@Inject
public ActiveUserController(ActiveUserService activeUserService) {
this.activeUserService = activeUserService;
}
@MessageMapping("/activeUsers")
public void activeUsers(Message<Object> message) {
Principal user = message.getHeaders().get(SimpMessageHeaderAccessor.USER_HEADER, Principal.class);
activeUserService.mark(user.getName());
}
}
package com.theotherian.chat;
import java.util.Set;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.annotation.Scheduled;
public class ActiveUserPinger {
private SimpMessagingTemplate template;
private ActiveUserService activeUserService;
public ActiveUserPinger(SimpMessagingTemplate template, ActiveUserService activeUserService) {
this.template = template;
this.activeUserService = activeUserService;
}
@Scheduled(fixedDelay = 2000)
public void pingUsers() {
Set<String> activeUsers = activeUserService.getActiveUsers();
template.convertAndSend("/topic/active", activeUsers);
}
}
package com.theotherian.chat;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Sets;
public class ActiveUserService {
private LoadingCache<String, UserStats> statsByUser = CacheBuilder.newBuilder().build(new CacheLoader<String, UserStats>() {
@Override
public UserStats load(String key) throws Exception {
return new UserStats();
}
});
public void mark(String username) {
statsByUser.getUnchecked(username).mark();
}
public Set<String> getActiveUsers() {
Set<String> active = Sets.newTreeSet();
for (String user : statsByUser.asMap().keySet()) {
// has the user checked in within the last 5 seconds?
if ((System.currentTimeMillis() - statsByUser.getUnchecked(user).lastAccess()) < 5000) {
active.add(user);
}
}
return active;
}
private static class UserStats {
private AtomicLong lastAccess = new AtomicLong(System.currentTimeMillis());
private void mark() {
lastAccess.set(System.currentTimeMillis());
}
private long lastAccess() {
return lastAccess.get();
}
}
}
package com.theotherian.chat;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
/**
* Set up Spring boot and launch the application
*/
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
package com.theotherian.chat;
public class ChatMessage {
private String recipient;
public String getRecipient() { return recipient; }
public void setRecipient(String recipient) { this.recipient = recipient; }
private String sender;
public String getSender() { return sender; }
public void setSender(String sender) { this.sender = sender; }
private String message;
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
}
package com.theotherian.chat;
import javax.inject.Inject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
/**
* Override the scheduling configuration so that we can schedule our own scheduled bean and
* so that Spring's STOMP scheduling can continue to work
*/
@Configuration
@EnableScheduling
public class ChatSchedulingConfigurer implements SchedulingConfigurer {
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
return new ThreadPoolTaskScheduler();
}
/**
* This is setting up a scheduled bean which will see which users are active
*/
@Bean
@Inject
public ActiveUserPinger activeUserPinger(SimpMessagingTemplate template, ActiveUserService activeUserService) {
return new ActiveUserPinger(template, activeUserService);
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setTaskScheduler(taskScheduler());
}
}
function connect() {
socket = new SockJS('/chat');
stompClient = Stomp.over(socket);
stompClient.connect('', '', function(frame) {
whoami = frame.headers['user-name'];
console.log('Connected: ' + frame);
stompClient.subscribe('/user/queue/messages', function(message) {
showMessage(JSON.parse(message.body));
});
stompClient.subscribe('/topic/active', function(activeMembers) {
showActive(activeMembers);
});
});
}
function showMessage(message) {
var chatWindowTarget = (message.recipient === whoami) ? message.sender : message.recipient;
var chatContainer = getChatWindow(chatWindowTarget);
var chatWindow = chatContainer.children('.chat');
var userDisplay = $('<span>', {class: (message.sender === whoami ? 'chat-sender' : 'chat-recipient')});
userDisplay.html(message.sender + ' says: ');
var messageDisplay = $('<span>');
messageDisplay.html(message.message);
chatWindow.append(userDisplay).append(messageDisplay).append('<br/>');
chatWindow.animate({ scrollTop: chatWindow[0].scrollHeight}, 1);
if (message.sender !== whoami) {
var sendingUser = $('#user-' + message.sender);
if (!sendingUser.hasClass('user-selected') && !sendingUser.hasClass('pending-messages')) {
sendingUser.append(newMessageIcon());
sendingUser.addClass('pending-messages');
}
}
}
package com.theotherian.chat;
import java.security.Principal;
import javax.inject.Inject;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
@Controller
public class MessageController {
private SimpMessagingTemplate template;
@Inject
public MessageController(SimpMessagingTemplate template) {
this.template = template;
}
@MessageMapping("/chat")
public void greeting(Message<Object> message, @Payload ChatMessage chatMessage) throws Exception {
Principal principal = message.getHeaders().get(SimpMessageHeaderAccessor.USER_HEADER, Principal.class);
String authedSender = principal.getName();
chatMessage.setSender(authedSender);
String recipient = chatMessage.getRecipient();
if (!authedSender.equals(recipient)) {
template.convertAndSendToUser(authedSender, "/queue/messages", chatMessage);
}
template.convertAndSendToUser(recipient, "/queue/messages", chatMessage);
}
}
function newMessageIcon() {
var newMessage = $('<span>', {class: 'newmessage'});
newMessage.html('&#x2709;');
return newMessage;
}
package com.theotherian.chat;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.*;
/**
* An extremely basic auth setup for the sake of a demo project
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("ian").password("ian").roles("USER");
auth.inMemoryAuthentication().withUser("dan").password("dan").roles("USER");
auth.inMemoryAuthentication().withUser("chris").password("chris").roles("USER");
}
}
function sendMessageTo(user) {
var chatInput = '#input-chat-' + user;
var message = $(chatInput).val();
if (!message.length) {
return;
}
stompClient.send("/app/chat", {}, JSON.stringify({
'recipient': user,
'message' : message
}));
$(chatInput).val('');
$(chatInput).focus();
}
package com.theotherian.chat;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* Set up our websocket configuration, which uses STOMP, and configure our endpoints
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/queue", "/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat", "/activeUsers").withSockJS();
}
@Override
public void configureClientInboundChannel(ChannelRegistration channelRegistration) {
}
@Override
public void configureClientOutboundChannel(ChannelRegistration channelRegistration) {
}
@Override
public boolean configureMessageConverters(List<MessageConverter> converters) {
return true;
}
@Bean
public ActiveUserService activeUserService() {
return new ActiveUserService();
}
}
function getChatWindow(userName) {
var existingChats = $('.chat-container');
var elementId = 'chat-' + userName;
var containerId = elementId + '-container';
var selector = '#' + containerId;
var inputId = 'input-' + elementId;
if (!$(selector).length) {
var chatContainer = $('<div>', {id: containerId, class: 'chat-container'});
var chatWindow = $('<div>', {id: elementId, class: 'chat'});
var chatInput = $('<textarea>', {id: inputId, type: 'text', class: 'chat-input', rows: '2', cols: '75',
placeholder: 'Enter a message. Something deep and meaningful. Something you can be proud of.'});
var chatSubmit = $('<button>', {id: 'submit-' + elementId, type: 'submit', class: 'chat-submit'})
chatSubmit.html('Send');
chatInput.keypress(function(event) {
if (event.which == 13) {
var user = event.currentTarget.id.substring(11); // gets rid of 'input-chat-'
event.preventDefault();
sendMessageTo(user);
}
});
chatSubmit.click(function(event) {
var user = event.currentTarget.id.substring(12); // gets rid of 'submit-chat-'
sendMessageTo(user);
});
chatContainer.append(chatWindow);
chatContainer.append(chatInput);
chatContainer.append(chatSubmit);
if (existingChats.length) {
chatContainer.hide();
}
$('body').append(chatContainer);
}
return $(selector);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment