Spring integration headers on message events - spring

I have a simple TCP connection factory implemented in Spring Integration:
#Bean
#ServiceActivator(inputChannel = "toTcpChannel")
public TcpSendingMessageHandler tcpOutClient() throws Exception {
TcpSendingMessageHandler sender = new TcpSendingMessageHandler();
sender.setConnectionFactory(clientFactory());
sender.setClientMode(false);
sender.afterPropertiesSet();
return sender;
}
#Bean
public AbstractClientConnectionFactory clientFactory() {
final TcpNioClientConnectionFactory factory = new TcpNioClientConnectionFactory(tcpHost, tcpPort);
factory.setSingleUse(true);
return factory;
}
#EventListener
public void handleTcpConnectionOpenEvent(TcpConnectionOpenEvent event) throws Exception {
LOGGER.info("TCP connection OPEN event: {}", event.getConnectionId());
// HERE I would like to have "myCustomID" header here.
}
I am looking for getting the custom ID that I am providing via Gateway in the produced TcpConnectionOpenEvent (or similar via interceptors)
#Gateway(requestChannel="toTcpChannel")
public void sendToTcp(#Payload String message, #Header("myCustomID") Long myCustomID);
I know this is an event not a message but I do know how to get the Connection ID that I will receive in the input channel in any other way.
I am creating a type of hash map of my custom id – connection id.
I cannot use a custom correlation via aggregator because the response message will not contain any information about the previously sent message. Any suggestions will be welcome.

Oh! I see. Not sure what you are going to do from your custom TcpSendingMessageHandler, but as far as ApplicationEventPublisher is single-threaded, you can store the connectionId in the ThreadLocal variable and obtain it from there after send operation.

Related

Spring Integration Mqtt : DestinationResolutionException: no output-channel or replyChannel header available

Please can someone help me to understand where is the probleme in this config:
Versions :
org.springframework.integration:spring-integration-mqtt:5.5.2
org.springframework.boot:spring-boot-starter:2.5.3
org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5
#Configuration
public class MqttConfig {
#Bean
public MqttPahoClientFactory mqttClientFactory() {
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
MqttConnectOptions options = new MqttConnectOptions();
options.setServerURIs(new String[] { "tcp://localhost:1883" });
return factory;
}
#Bean
public MqttPahoMessageDrivenChannelAdapter inboundAdapter(MqttPahoClientFactory clientFactory) {
return new MqttPahoMessageDrivenChannelAdapter("MyApp", clientFactory, "ReplyTopic");
}
#Bean
IntegrationFlow inboundFlow(MqttPahoMessageDrivenChannelAdapter inboundAdapter) {
return IntegrationFlows.from(inboundAdapter)
.bridge()
.channel("replyChannel")
.get();
}
#Bean
public MessageChannel replyChannel() {
return MessageChannels.publishSubscribe().get();;
}
#Bean
public MqttPahoMessageHandler outboundAdapter(MqttPahoClientFactory clientFactory) {
return new MqttPahoMessageHandler("MyApp", clientFactory);
}
#Bean
public IntegrationFlow outboundFlow(MqttPahoMessageHandler outboundAdapter) {
return IntegrationFlows.from("requestChannel")
.handle(outboundAdapter).get()
}
#MessagingGateway
public interface MyGateway {
#Gateway(requestChannel = "requestChannel", replyChannel = "replyChannel")
String send(String request, #Header(MqttHeaders.TOPIC) String requestTopic);
}
}
Client code
#RestController
public class MyController {
#Autowired
private MyGateway myGateway;
#GetMapping("/sendRequest")
public String sendRequest() {
var response = myGateway.send("Hello", "MyTopic");
return response;
}
}
Usage:
curl http://localhost:8080/sendRequest
manual response from the mqtt broker (HiveMQ)
docker exec -it hivemq mqtt pub -t ReplyTopic -m "World" --debug
CLIENT mqttClient-MQTT_5_0-9ecded84-8416-4baa-a8f3-d593c692bc65: acknowledged PUBLISH: 'World' for PUBLISH to Topic: ReplyTopic
But I dont know why i have this message on the Spring application output
2022-10-25 18:04:33.171 ERROR 17069 --- [T Call: MyApp] .m.i.MqttPahoMessageDrivenChannelAdapter : Unhandled exception for GenericMessage [payload=World, headers={mqtt_receivedRetained=false, mqtt_id=0, mqtt_duplicate=false, id=9dbd5e14-66ed-5dc8-6cea-6d04ef19c6cc, mqtt_receivedTopic=ReplyTopic, mqtt_receivedQos=0, timestamp=1666713873170}]
org.springframework.messaging.MessageHandlingException: error occurred in message handler [org.springframework.integration.handler.BridgeHandler#6f63903c]; nested exception is org.springframework.messaging.core.DestinationResolutionException: no output-channel or replyChannel header available
Please can someone explain why i have this ?
no output-channel or replyChannel header available
I think the problem you are facing is not related to your bridge() configuration.
This comes from the MessagingGatewaySupport and its replyMessageCorrelator feature which is activated by your replyChannel = "replyChannel".
The real problem that you are trying to do what is not possible with MQTT v3. There is just no headers transferring over MQTT broker to carry on a required for gateway initiator a correlation key - the TemporaryReplyChannel. See more in docs about gateway: https://docs.spring.io/spring-integration/docs/current/reference/html/messaging-endpoints.html#gateway.
In other words: independently of the replyChannel configuration on gateway, the replyChannel header must be present in the reply message. This is the way how gateway correlates requests with replies.
You have to look into an aggregator to send the request message in parallel and to preserve the mentioned TemporaryReplyChannel header. Then when you receive a reply (inboundAdapter) you send it to this aggregator. You need to ensure some correlation key from a request and reply payload, so they can match and fulfill group for reply to be sent back to the gateway.
See more info in docs: https://docs.spring.io/spring-integration/docs/current/reference/html/message-routing.html#aggregator

Webflux and WebSocket, issue distributing new messages

I am currently trying to learn basics of Webflux and Websocket. Both are new concept for me, and I am making a small project to practice what I learned, followed with a VueJs front-end.
I managed to connect on the WebSocket, when I send a message I have an automatic reply with the data saved on Firestore which is what I wanted. But soon as I open a new browser and that I open a new connection, I don't receive the other browser chat messages, I just receive the message that I send from the browser where I am sending the message, they are completely ignoring each other messages even if they are connected on same URL.
Here is my URL handler mapping (it accepts all url that has /chatroom/ so I can create all the topics I want)
#Configuration
#AllArgsConstructor
public class WebFluxConfig {
private final WebFluxWebSocketHandler handler;
#Bean
public HandlerMapping handlerMapping(){
Map<String, WebFluxWebSocketHandler> handlerMap = Map.of(
"/chatroom/*", handler
);
return new SimpleUrlHandlerMapping(handlerMap, 1);
}
}
and here my handler on a class that implement WebSocketHandler :
#Service
#AllArgsConstructor
public class WebFluxWebSocketHandler implements WebSocketHandler {
private final ChatMessageService chatMessageService;
#Override
public Mono<Void> handle(WebSocketSession webSocketSession) {
Flux<WebSocketMessage> stringFlux = webSocketSession.receive()
.map(WebSocketMessage::getPayloadAsText)
.flatMap(SerializationUtility::convertJsonToChatMessage)
.doOnNext(chatMessage ->
chatMessage
.setTopic(webSocketSession.getHandshakeInfo()
.getUri()
.getPath()
.replace("/chatroom/", "")
)
)
.flatMap(chatMessage -> this.chatMessageService.save(Mono.just(chatMessage)))
.map(chatMessage -> webSocketSession.textMessage(SerializationUtility.convertChatMessageToJson(chatMessage)))
.log();
return webSocketSession.send(stringFlux);
}
}
Both browser I use to open a connection and send message are connected through the same URL which is
this.websocket = new WebSocket("ws://localhost:8095/chatroom/frontend-testing-room")
How can I manage to send the new message response to everybody connected on the same URL and not only to the one that is sending the message ?

Request response over HTTP with Spring and activemq

I am building a simple REST api which connects a web server to a back end service, which performs a simple check and sends a response.
So client (over HTTP) -> to Web Server (over ACTIVEMQ/CAMEL)-> to Checking-Service, and back again.
The endpoint for the GET request is "/{id}". I'm trying to make this send a message through queue:ws-out to queue:cs-in and map it all the way back again to the original GET request.
The Checking-Service (cs) code is fine, it simply changes a value in the CheckMessage object to true using jmslistener.
I've searched the web thoroughly for examples, but can't get anything to work. The closest one I found was the following.
This is what I have so far on the Web Server (ws).
RestController
import ...
#RestController
public class RESTController extends Exception{
#Autowired
CamelContext camelContext;
#Autowired
JmsTemplate jmsTemplate;
#GetMapping("/{id}")
public String testCamel(#PathVariable String id) {
//Object used to send out
CheckMessage outMsg = new CheckMessage(id);
//Object used to receive response
CheckMessage inMsg = new CheckMessage(id);
//Sending the message out (working)
jmsTemplate.convertAndSend("ws-out", outMsg);
//Returning the response to the client (need correlation to the out message"
return jmsTemplate.receiveSelectedAndConvert("ws-in", ??);
}
}
Listener on ws
#Service
public class WSListener {
//For receiving the response from Checking-Service
#JmsListener(destination = "ws-in")
public void receiveMessage(CheckMessage response) {
}
}
Thanks!
your receive messages from "ws-in" with 2 consumers jmsTemplate.receiveSelectedAndConvert and WSListener !! message from a queue is consumed by one of the 2.
you send messages to "ws-out" and consume from "ws-in" ?? last queue
is empty and not receive any message, you have to send messages to
it
you need a valid selector to retrieve the message with receiveSelectedAndConvert based on JMSCorrelationID as the example you mntioned or the id received from the rest request but you need to add this id to the message headers like below
this.jmsTemplate.convertAndSend("ws-out", id, new MessageCreator() {
#Override
public Message createMessage(Session session) throws JMSException {
TextMessage tm = session.createTextMessage(new CheckMessage(id));
tm.setJMSCorrelationID(id);
return tm;
}
});
return jmsTemplate.receiveSelectedAndConvert("ws-in", "JMSCorrelationID='" + id+ "'");
forward messages from "ws-out" to "ws-in"
#Service
public class WSListener {
//For receiving the response from Checking-Service
#JmsListener(destination = "ws-out")
public void receiveMessage(CheckMessage response) {
jmsTemplate.convertAndSend("ws-in", response);
}
}

Spring send message to Websocket Message Broker

I want to send a message to websocket subscribers of a specific record - when an action takes place in one of my service class.
I'm trying to read the Spring Websocket documentation but it's kind of ambiguous to the point of how to get all these things working together.
Here are my setup files (this is extending jHipster btw):
WebsocketConfiguration.java
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableStompBrokerRelay("/queue/", "/topic/", "/exchange/");
config.setApplicationDestinationPrefixes("/app");
config.setPathMatcher(new AntPathMatcher("."));
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS();
}
WebsocketSecurity.java
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
// message types other than MESSAGE and SUBSCRIBE
.nullDestMatcher().authenticated()
// matches any destination that starts with /rooms/
.simpDestMatchers("/topic/tracker").hasAuthority(AuthoritiesConstants.ADMIN)
.simpDestMatchers("/topic/**").authenticated()
// (i.e. cannot send messages directly to /topic/, /queue/)
// (i.e. cannot subscribe to /topic/messages/* to get messages sent to
// /topic/messages-user<id>)
.simpTypeMatchers(SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE).denyAll()
// catch all
.anyMessage().denyAll();
}
Controller class (attempt at implementing a simple broker I can test subscribing to from sockjs and recieving messages generated elsewhere in the application:
#MessageMapping("/ws")
#SendTo("/topic/sendactivity.{id}")
public void activity(#DestinationVariable string id, #Payload String message){
log.debug("Sending command center: "+message);
}
#RequestMapping(value = "/updateactivity", method = RequestMethod.PUT)
public ResponseEntity<Membership> updateMembership(
#RequestBody Membership membership) throws URISyntaxException {
// ...
String testString = "test";
messagingTemplate.convertAndSend("/topic/commandcenter"+membership.getId().toString(), testString);
// ...
}
When I put a breakpoint on the public void activity method, I don't get anything?
Sending a message to "/topic/commandcenterID" using the messaging template will send that message to the message broker, which will dispatch that message to clients subscribed to that topic. So it won't flow through your activity method.
When using #MessageMapping annotated methods, you're declaring those as application destinations. So sending a message to "/app/ws" should map to that method. Note that in that case I doubt it'll work since the destination variable you're expecting as a method argument is missing from the path definition in the #MessageMapping annotation.
Also, the #SendTo annotation in fact tells Spring that the value returned by the method should be converted to a message and sent to the given destination.
It seems you're mixing things up here, and I think you should:
read carefully the flow of messages in Spring STOMP support
look at a few example apps like the websocket portfolio and websocket chat

Sending Error message in Spring websockets

I am trying to send error messages in Spring websockets with STOMP over SockJS.
I am basically trying to achieve which is being done here.
This is my Exception Handler
#MessageExceptionHandler
#SendToUser(value = "/queue/error",broadcast = false)
public ApplicationError handleException(Exception message) throws ApplicationError {
return new ApplicationError("test");
}
And I am subscribing to
stompClient.subscribe('/user/queue/error', stompErrorCallback, {token: accessToken});
User in my case is not authenticated, but from here
While user destinations generally imply an authenticated user, it
isn’t required strictly. A WebSocket session that is not associated
with an authenticated user can subscribe to a user destination. In
such cases the #SendToUser annotation will behave exactly the same as
with broadcast=false, i.e. targeting only the session that sent the
message being handled.
All this works fine when I am throwing this error from myHandler which is my Websocket Handler defined in websocket config.
I have a ClientInboundChannelInterceptor which extends ChannelInterceptorAdapter which intercepts all the messages in preSend.
In case of any exception in this interceptor, I want to throw it back to the user session which sent this message,
public class ClientInboundChannelInterceptor extends ChannelInterceptorAdapter {
#Autowired
#Lazy(value = true)
#Qualifier("brokerMessagingTemplate")
private SimpMessagingTemplate simpMessagingTemplate;
#Override
public Message<?> preSend(Message message, MessageChannel channel) throws IllegalArgumentException{
if(some thing goes wrong)
throw new RuntimeException();
}
#MessageExceptionHandler
#SendToUser(value = "/queue/error",broadcast = false)
public ApplicationError handleException(RuntimeException message) throws ApplicationError {
return new ApplicationError("test");
}
}
#MessageExceptionHandler does not catch this exception. So I tried sending it to the user directly using simpMessagingTemplate.
I basically want to do :
simpMessagingTemplate.convertAndSendToUser(SOMETHING,"/queue/error",e);
SOMETHING should be the correct username but user is not authenticated in my case, so I can't use headerAccessor.getUser().getName()
I have even tried with
simpMessagingTemplate.convertAndSendToUser(headerAccessor.getHeader("","/queue/error",e, Collections.singletonMap(SimpMessageHeaderAccessor.SESSION_ID_HEADER, headerAccessor.getSessionId()));
but this is not working.
I have even tried headerAccessor.getSessionId() in the place of username, but that does not seem to work.
What is the correct way to do this?
What should I use as username in convertAndSendToUser?
My initial intuition was correct, sessionId is used as the username in case of unauthenticated user situations, but the problem was with headers.
After few hours of debugging through #SendToUser and simpMessagingTemplate.convertAndSendToUser(), I realised that if we use #SendToUser headers will be set automatically and we have to explicitly define the headers if we are using simpMessagingTemplate.convertAndSendToUser().
#SendToUser was setting two headers,
simpMessageType:SimpMessageType.MESSAGE,simpSessionId:sessionId
So I have tried adding the headers,
String sessionId = headerAccessor.getSessionId();
Map<String,Object> headerMap = new HashMap<>();
headerMap.put("simpMessageType", SimpMessageType.MESSAGE);
headerMap.put("simpSessionId",sessionId);
simpMessagingTemplate.convertAndSendToUser(headerAccessor.getSessionId(),"/queue/error",e,headerMap);
It did not work, I have tried giving the headers as MessageHeaders
String sessionId = headerAccessor.getSessionId();
Map<String,Object> headerMap = new HashMap<>();
headerMap.put("simpMessageType", SimpMessageType.MESSAGE);
headerMap.put("simpSessionId",sessionId);
MessageHeaders headers = new MessageHeaders(headerMap);
simpMessagingTemplate.convertAndSendToUser(headerAccessor.getSessionId(),"/queue/error",e,headers);
didn't work either.
After some more debugging I found out the correct way to set the headers, and probably this is the only way to create these headers(from SendToMethodReturnValueHandler.java).
private MessageHeaders createHeaders(String sessionId) {
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(sessionId);
headerAccessor.setLeaveMutable(true);
return headerAccessor.getMessageHeaders();
}
So finally,
String sessionId = headerAccessor.getSessionId();
template.convertAndSendToUser(sessionId,"/queue/error","tesssssts",createHeaders(sessionId));
did the trick.
You can use convertAndSendToUser() only if that user is subscribed to the destination:
super.convertAndSend(this.destinationPrefix + user + destination, payload, headers, postProcessor);
Where user can be just sessionId - headerAccessor.getSessionId()
The #MessageExceptionHandler does its work only withing #MessageMapping or #SubscribeMapping.
See SendToMethodReturnValueHandler source code for more info.

Resources