Skip to content

Instantly share code, notes, and snippets.

@theotherian
Last active December 30, 2023 04:15
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save theotherian/9906304 to your computer and use it in GitHub Desktop.
Save theotherian/9906304 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);
}
@yanchhuong
Copy link

yanchhuong commented Nov 23, 2016

excuse me ,How can configure if i want Unicode font support ?[(Failed to parse TextMessage payload=[SEND cont...],,)] cos i got this error when try others language .

@pankajojha
Copy link

How do login page comes from, where is the html for it ?

@dallaslu
Copy link

dallaslu commented May 9, 2017

Where is the login page?

@utsavkansara
Copy link

Login page is provided by spring when you enable spring security. You can customize it and put your own login page as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment