Subscribing to spring-websocket messages internally - spring

I am using spring-websocket to push messages to browser clients.
My setup is almost identical to the one in the portfolio example and I send out messages by using MessageSendingOperations:
MessageSendingOperations<String> messagingTemplate = //...;
this.messagingTemplate.convertAndSend("/topic/data/1", message);
This works perfectly.
But I would also like to be able to subscribe to the same messages internally.
MessageReceivingOperations almost looks like the one to use, but it only seems to support pulling messages. I would much prefer having the messages pushed to my service.
SubscribableChannel.subscribe() also looks promising, but how do I get hold of the correct channel?
I would really like to be able to call something like
messagingTemplate.subscribe("/topic/data/*",
new MessageHandler<String>{
public void handleMessage(String s){
// process message
}
});

The following works for me, but it would be nice with a more direct way to do it:
public interface MessageHandler<T> {
public void handleMessage(T message);
}
#Autowired
private AbstractSubscribableChannel brokerChannel;
private PathMatcher pathMatcher = new AntPathMatcher();
private <T> void subscribe(final String topic, final Handler<T> handler, final Class<T> messageClass){
brokerChannel.subscribe(new MessageHandler() {
#Override
public void handleMessage(Message<?> message) throws MessagingException {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(message);
final String destination = headers.getDestination();
if(pathMatcher.match(topic, destination)) {
final T messageObject = (T) messageConverter.fromMessage(message, messageClass);
handler.handleMessage(messageObject);
}
}
});
}

Related

Sequence of Rest and Soap calls to get the final result using Spring Boot 2

I have to make a sequence of calls like a chain to other microservices to get the final result.
Of these calls, some are SOAP and some REST.
Is there any good design pattern to chain these calls and handle exceptions using synchronous.
If I go with Asynchronous then CompletableFuture chain will be the right choice for SOAP calls?
For example
CompletableFuture.supplyAsync(() -> getEmp())
.thenApply(emp -> {
// handle soap client call
return soapClientService.getDepartmentResponse();
})
.thenApply(dept -> {
return restTemplateService.getRestDeptAPICalls();
})
.handle((result, exception) -> {
// handle exception and result
});
From your description, you can use Pipeline design pattern.
Pipe A calls Rest end-point A, map the response to a Message and return the
message.
Pipe B which implements a Soap service B, transforms the resulting message. It creates and return a new and different message.
Pipe C which implements a Rest end-point C, calls Soap service B using the
new message as input . It creates a new message from the result,
adds a flag and return the message.
And so on...
You can have something like that:
public interface Pipe{
void chain(Message input, Pipeline pipeline);
}
public abstract class RestCallPipe implements Pipe {
private RestTemplate restTemplate;
public RestCallPipe (String url){
this.restTemplate=new RestTemplate(url);
}
public void chain(Message input, Pipeline pipeline){
try {
Response response = restTemplate.exchange(....,input,Response.class);
Message output = onSuccess(response, input);
pipeline.next(output);
}catch(Exception exception){
onError(exception, input);
//Maybe, we can continue
//with an alternative message
}
}
public abstract Message onSuccess(Response response, Message originalMessage);
public abstract void onError(Throwable error, Message input);
}
public abstract class SoapCallPipe implements Pipe {
public void chain(Message input, Pipeline pipeline){
try {
// Perform your Soap call and get a response
Message output = onSuccess(response, input);
pipeline.next(output);
}catch(Exception exception){
onError(exception, input);
//Maybe, we can continue
//with an alternative message
}
}
public abstract Message onSuccess(Response response, Message originalMessage);
public abstract void onError(Throwable error, Message input);
}
public interface Message {
public Object get(String key);
}
public class MessageImpl implements Message {
private final Map<String, Object> index;
public MessageImpl(){...}
public void set(String key, Objetc value){...}
public Object get(String key){...}
}
The Pipeline is a chain of Pipes. Each pipe executes a command and tell the pipeline to continue with a new message.
Hope that helps.

Why isnt my sockets onsubscribe event getting used?

I am using java springboot with maven in order to get the spring boot starter socket package. My clients are using angular with stompjs and sockjs-client.
I am trying to set up a simple web socket application that allows for multiple rooms based on a roomId. When a client joins a room they should receive the last five messages sent in that room.
My Springboot app has three classes, the basic Application.java that I use to run the app, a web socket config class and a web socket controller:
#Controller
public class WebSocketController {
private final SimpMessagingTemplate template;
#Autowired
WebSocketController(SimpMessagingTemplate template){
this.template = template;
}
#MessageMapping("/meeting/{roomId}")
private void sendMessageTpPrivateRoom(
String message,
#DestinationVariable String roomId
) throws IOException {
System.out.println("message sent to: " + roomId);
this.template.convertAndSend("/meeting/" + roomId, message);
addToHistory(roomId, message);
}
#SubscribeMapping("/meeting/{roomId}")
public String chatInit(#DestinationVariable String roomId) {
System.out.println("Someone joined room: " + roomId);
return getLastFiveMessages(roomId);
}
}
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfiguration
extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/socket")
.setAllowedOrigins("*")
.withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app")
.enableSimpleBroker("/meeting");
}
}
my clients are subscribing to the socket like so:
stompClient.subscribe(`app/meeting/${roomId}`, (message) => {
if (message.body) {
console.log(message.body);
messages += '<br>' + message.body;
}
});
and sending messages like so:
this.stompClient.send(`/app/meeting/${this.roomId}` , {}, message);
The message sending and handling is working great, when I set up three clients, two in room one, and one in room two, the room two messages are not being seen in room one and the room one messages are seen by both clients.
However the on subscribe event is not firing no matter what room I join. It is very necessary that when a client joins room one, they should receive some sort of history of that room. Any advice as to why my SubscribeMapping method is not being triggered when a client subscribes to the room?
The /meeting part will be implicitly added to URL you provide when subscribing. So your mapping will look like this:
#SubscribeMapping("/${roomId}")
public String chatInit(#DestinationVariable String roomId) {
System.out.println("Someone joined room: " + roomId);
return getLastFiveMessages(roomId);
}
Source: https://docs.spring.io/spring/docs/5.0.0.BUILD-SNAPSHOT/spring-framework-reference/html/websocket.html

Send websocket message to user across dynos

I have a spring boot application running on heroku. I make use of websockets for sending messages to and from client and server for a specific user . I use spring boot's SimpMessagingTemplate.convertAndSendToUser to send and receive messages, which works fine for when a user needs get a message back from the server. I use Heroku session affinity which means that even if I scale up the number of sessions the user and websocket still share the same session.
My problem comes when I need a user to send a message to another user. It works fine if both users are sharing the session, but not if the message will not come through.
Is it possible to send a message from one user to another across different sessions using, SimpMessagingTemple? Or would I need to use a message broker, eg Redis.
I was looking into implementing sending a message using StringRedisTemplate but not sure how to send a message to a particular user.
private SimpMessagingTemplate messagingTemplate;
#Autowired
public MessageController(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
#MessageMapping("/secured/user-in")
public void sendToDevice(Message msg, #AuthenticationPrincipal User principal) throws Exception {
if (msg.getTo() != null) {
String email = msg.getTo();
Message out = new Message();
out.setMsg(msg.getMsg());
out.setFrom(msg.getFrom());
out.setTo(msg.getTo());
out.setSentTime(new Date());
out.setStatus(msg.getStatus());
messagingTemplate.convertAndSendToUser(email, "/secured/topic", out);
}
}
JS
function connect() {
var socket = new SockJS('/secured/user-in');
ST.stompClient = Stomp.over(socket);
var headers = {};
headers[ST.getHeader()] = ST.getToken();
ST.getStompClient().connect(headers, function (frame) {
retries = 1;
console.log('Connected: ' + frame);
ST.getStompClient().subscribe('/user/secured/topic', function (event){
var msg = JSON.parse(event.body);
showMessage(msg.msg);
});
});
}
UPDATE 1
I am guessing I could do something like this, as done here:
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor
.create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(sessionId);
headerAccessor.setLeaveMutable(true);
messagingTemplate.convertAndSendToUser(sessionId,"/queue/something", payload,
headerAccessor.getMessageHeaders());
But how could I get the session id of another user, I am using Redis to store session info: #EnableRedisHttpSession
I had my terminology a bit mixed up I was trying to send a message to another user on another dyno rather than session.
Ended up using redis sub/pub.
So when a message is receive by the controller it is published to redis, and the redis MessageListenerAdapter envokes the convertAndSendToUser method.
#MessageMapping("/secured/user-in")
public void sendToDevice(Message msg, #AuthenticationPrincipal User principal) throws Exception {
publishMessageToRedis(msg);
}
private void publishMessageToRedis(Message message) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
String messageString = objectMapper.writeValueAsString(message);
stringRedisTemplate.convertAndSend("message", messageString);
}
redis config
#Bean
RedisMessageListenerContainer container( MessageListenerAdapter chatMessageListenerAdapter) throws URISyntaxException {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory());
container.addMessageListener(chatMessageListenerAdapter, new PatternTopic("message"));
return container;
}
#Bean("chatMessageListenerAdapter")
MessageListenerAdapter chatMessageListenerAdapter(RedisReceiver redisReceiver) {
return new MessageListenerAdapter(redisReceiver, "receiveChatMessage");
}
public class RedisReceiver {
private static final Logger LOG = LogManager.getLogger(RedisReceiver.class);
private final WebSocketMessageService webSocketMessageService;
#Autowired
public RedisReceiver(WebSocketMessageService webSocketMessageService) {
this.webSocketMessageService = webSocketMessageService;
}
// Invoked when message is publish to "chat" channel
public void receiveChatMessage(String messageStr) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
Message message = objectMapper.readValue(messageStr, Message.class);
webSocketMessageService.sendChatMessage(message);
}
}
#Service
public class WebSocketMessageService {
private final SimpMessagingTemplate template;
private static final Logger LOG = LogManager.getLogger(WebSocketMessageService.class);
public WebSocketMessageService(SimpMessagingTemplate template) {
this.template = template;
}
public void sendChatMessage(Message message) {
template.convertAndSendToUser(message.getTo(), "/secured/topic", message);
}
}
Solution was based off this git repository

How to use JunitTest with Camel, ActiveMQ and external API

I'm trying to set up JunitTest using camel, activeMq and an Alfresco API
The route I want to test is :
from(Constantes.Direct.DIRECT_GET_AUTHENTIFICATION_TICKET)
.setBody().simple("{"
+ "\"userId\": \"userId\","
+"\"password\": \"password\""
+"}")
.setHeader(Exchange.HTTP_METHOD,constant(Constantes.Headers.HTTP_POST))
.setHeader(Exchange.HTTP_URI,simple(Constantes.Urls.OBTENIR_TICKET))
.to(Constantes.Urls.DUMMYHOST).convertBodyTo(String.class)
.unmarshal().json(JsonLibrary.Jackson, TicketAlfresco.class).process(new Dumper())
.process(new TokenBase64Proc())
.setHeader(Constantes.Headers.SENDER, constant(Constantes.Headers.ALFRESCO))
.setHeader(Constantes.Headers.API_ACTION, constant(SET_ALFRESCO_TOKEN))
.setHeader(Constantes.Headers.HEADER_AUTHORIZATION, simple("${body}"))
.inOut(Constantes.ActiveMq.ACTIVEMQ_IN)
.end();
The first "to" send a request to the Alfresco API and give back a new token.
The last inOut send the token to an activeMQ.
The problem is that when I want to test my route, when the test arrive to inOut inside the activeMq, the test fail because it didn't get any answer.
Do I need to install and embeded broker activeMQ or do I need to Mock the ActiveMQ ? And how can I do that?
For the moment to make it run I use :
mockEndpointsAndSkip("activemq:IN")
But I'm not sure that is the good solution.
Here is the test I have for the moment:
#RunWith(SpringRunner.class)
#EnableAutoConfiguration
#ComponentScan(basePackages = {"fr.gif.wsp.web.service.alfresco*"})
public class RouteGetAuthentificationTicketTest extends CamelTestSupport{
#Autowired private RouteGetAuthentificationTicket routeGetAuthentificationTicket;
//Route to test
private final static String FOURNISSEUR_GET_AUTHENTIFICATION_TICKET = Constantes.Direct.DIRECT_GET_AUTHENTIFICATION_TICKET;
private final static String MOCK_FOURNISSEUR_GET_AUTHENTIFICATION_TICKET = "mock:" + FOURNISSEUR_GET_AUTHENTIFICATION_TICKET;
// Mock result
private final static String MOCK_RESULT = "mock:result";
//Data
private final static String BODY = "Content of the body";
#Override
protected RoutesBuilder createRouteBuilder() {
return routeGetAuthentificationTicket;
}
#Before
public void setContextRoute() throws Exception {
context.getRouteDefinitions().get(0).adviceWith(context, new AdviceWithRouteBuilder() {
#Override
public void configure() throws Exception {
mockEndpointsAndSkip("activemq:IN");
weaveAddLast().to(MOCK_RESULT);
}
});
}
#Test
public void getAuthentificationTicket() throws InterruptedException {
final MockEndpoint resultEndpoint = context.getEndpoint(MOCK_FOURNISSEUR_GET_AUTHENTIFICATION_TICKET, MockEndpoint.class);
context.createProducerTemplate().sendBody(FOURNISSEUR_GET_AUTHENTIFICATION_TICKET, BODY);
resultEndpoint.assertIsSatisfied();
final Object result = context.createProducerTemplate().requestBody(FOURNISSEUR_GET_AUTHENTIFICATION_TICKET, BODY);
assertNotNull(result);
}}
Thanks for your time

ApacheConnector does not process request headers that were set in a WriterInterceptor

I am experiencing problems when configurating my Jersey Client with the ApacheConnector. It seems to ignore all request headers that I define in a WriterInterceptor. I can tell that the WriterInterceptor is called when I set a break point within WriterInterceptor#aroundWriteTo(WriterInterceptorContext). Contrary to that, I can observe that the modification of an InputStream is preserved.
Here is a runnable example demonstrating my problem:
public class ApacheConnectorProblemDemonstration extends JerseyTest {
private static final Logger LOGGER = Logger.getLogger(JerseyTest.class.getName());
private static final String QUESTION = "baz", ANSWER = "qux";
private static final String REQUEST_HEADER_NAME_CLIENT = "foo-cl", REQUEST_HEADER_VALUE_CLIENT = "bar-cl";
private static final String REQUEST_HEADER_NAME_INTERCEPTOR = "foo-ic", REQUEST_HEADER_VALUE_INTERCEPTOR = "bar-ic";
private static final int MAX_CONNECTIONS = 100;
private static final String PATH = "/";
#Path(PATH)
public static class TestResource {
#POST
public String handle(InputStream questionStream,
#HeaderParam(REQUEST_HEADER_NAME_CLIENT) String client,
#HeaderParam(REQUEST_HEADER_NAME_INTERCEPTOR) String interceptor)
throws IOException {
assertEquals(REQUEST_HEADER_VALUE_CLIENT, client);
// Here, the header that was set in the client's writer interceptor is lost.
assertEquals(REQUEST_HEADER_VALUE_INTERCEPTOR, interceptor);
// However, the input stream got gzipped so the WriterInterceptor has been partly applied.
assertEquals(QUESTION, new Scanner(new GZIPInputStream(questionStream)).nextLine());
return ANSWER;
}
}
#Provider
#Priority(Priorities.ENTITY_CODER)
public static class ClientInterceptor implements WriterInterceptor {
#Override
public void aroundWriteTo(WriterInterceptorContext context)
throws IOException, WebApplicationException {
context.getHeaders().add(REQUEST_HEADER_NAME_INTERCEPTOR, REQUEST_HEADER_VALUE_INTERCEPTOR);
context.setOutputStream(new GZIPOutputStream(context.getOutputStream()));
context.proceed();
}
}
#Override
protected Application configure() {
enable(TestProperties.LOG_TRAFFIC);
enable(TestProperties.DUMP_ENTITY);
return new ResourceConfig(TestResource.class);
}
#Override
protected Client getClient(TestContainer tc, ApplicationHandler applicationHandler) {
ClientConfig clientConfig = tc.getClientConfig() == null ? new ClientConfig() : tc.getClientConfig();
clientConfig.property(ApacheClientProperties.CONNECTION_MANAGER, makeConnectionManager(MAX_CONNECTIONS));
clientConfig.register(ClientInterceptor.class);
// If I do not use the Apache connector, I avoid this problem.
clientConfig.connector(new ApacheConnector(clientConfig));
if (isEnabled(TestProperties.LOG_TRAFFIC)) {
clientConfig.register(new LoggingFilter(LOGGER, isEnabled(TestProperties.DUMP_ENTITY)));
}
configureClient(clientConfig);
return ClientBuilder.newClient(clientConfig);
}
private static ClientConnectionManager makeConnectionManager(int maxConnections) {
PoolingClientConnectionManager connectionManager = new PoolingClientConnectionManager();
connectionManager.setMaxTotal(maxConnections);
connectionManager.setDefaultMaxPerRoute(maxConnections);
return connectionManager;
}
#Test
public void testInterceptors() throws Exception {
Response response = target(PATH)
.request()
.header(REQUEST_HEADER_NAME_CLIENT, REQUEST_HEADER_VALUE_CLIENT)
.post(Entity.text(QUESTION));
assertEquals(200, response.getStatus());
assertEquals(ANSWER, response.readEntity(String.class));
}
}
I want to use the ApacheConnector in order to optimize for concurrent requests via the PoolingClientConnectionManager. Did I mess up the configuration?
PS: The exact same problem occurs when using the GrizzlyConnector.
After further research, I assume that this is rather a misbehavior in the default Connector that uses a HttpURLConnection. As I explained in this other self-answered question of mine, the documentation states:
Whereas filters are primarily intended to manipulate request and
response parameters like HTTP headers, URIs and/or HTTP methods,
interceptors are intended to manipulate entities, via manipulating
entity input/output streams
A WriterInterceptor is not supposed to manipulate the header values while a {Client,Server}RequestFilter is not supposed to manipulate the entity stream. If you need to use both, both components should be bundled within a javax.ws.rs.core.Feature or within the same class that implements two interfaces. (This can be problematic if you need to set two different Prioritys though.)
All this is very unfortunate though, since JerseyTest uses the Connector that uses a HttpURLConnection such that all my unit tests succeeded while the real life application misbehaved since it was configured with an ApacheConnector. Also, rather than suppressing changes, I wished, Jersey would throw me some exceptions. (This is a general issue I have with Jersey. When I for example used a too new version of the ClientConnectionManager where the interface was renamed to HttpClientConnectionManager I simply was informed in a one line log statement that all my configuration efforts were ignored. I did not discover this log statement til very late in development.)

Resources